pipeline

package
v0.37.0 Latest Latest
Warning

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

Go to latest
Published: Jun 10, 2026 License: MIT Imports: 33 Imported by: 4

Documentation

Overview

ABOUTME: Resolves the on-disk location of the integrity-protected activity log. ABOUTME: Threat model for the relocation lives in CLAUDE.md (#213).

ABOUTME: AgentBackend interface and config types for pluggable execution backends. ABOUTME: Supports native (agent.Session), Claude Code (CLI subprocess), and ACP (Agent Client Protocol) backends.

ABOUTME: Pipeline-level token, cost, and wall-time ceilings enforced between nodes. ABOUTME: Halts execution with OutcomeBudgetExceeded when any configured limit is breached.

ABOUTME: Checkpoint serialization for pipeline execution resume support. ABOUTME: Tracks completed nodes, retry counts, and context state as JSON on disk.

ABOUTME: Thread-safe key-value store shared across all pipeline nodes during execution. ABOUTME: Provides Get/Set/Merge/Snapshot operations and separate internal state for engine bookkeeping. ABOUTME: Supports per-node namespace scoping via ScopeToNode — dirty keys are copied into node.<id>.<key> ABOUTME: after each node completes, preserving individual node outputs without breaking global backward compat.

ABOUTME: Adapter that converts Dippin IR (from dippin-lang parser) to Tracker's Graph model. ABOUTME: Provides FromDippinIR() to enable tracker to execute .dip files natively.

ABOUTME: Implicit edge synthesis for the dippin adapter. ABOUTME: Converts parallel/fan-in IR config into explicit graph edges.

ABOUTME: Loads .dipx bundles produced by dippin v0.24+ — verifies hashes, ABOUTME: converts pre-parsed IR to tracker Graphs, returns content-addressed identity.

Package pipeline implements the core execution engine for multi-agent LLM workflows. Pipelines are directed graphs of nodes (agents, humans, tools, parallel fan-out) connected by conditional edges.

Pipeline Formats

Pipelines can be defined in two formats:

  • .dip (Dippin format) — the current format, parsed by dippin-lang. Use FromDippinIR to convert parsed IR to a Graph.

  • .dot (DOT/Graphviz format) — deprecated, will be removed in v1.0. Use ParseDOT for backward compatibility only.

New pipelines should use .dip format exclusively.

ABOUTME: Core pipeline execution engine that traverses graphs, executes handlers, and manages control flow. ABOUTME: Supports edge selection (conditions, labels, weights), retries, goal gates, and checkpoint resume.

ABOUTME: Checkpoint and graph utility methods extracted from engine.go. ABOUTME: Handles checkpoint load/save, downstream clearing, goal gates, and restart limits.

ABOUTME: Edge selection logic extracted from engine.go to reduce function complexity. ABOUTME: Implements priority-based edge routing: condition > label > suggested > weight > lexical.

ABOUTME: Extracted helper methods for Engine.Run to reduce cyclomatic/cognitive complexity. ABOUTME: Handles node preparation, execution, outcome processing, retries, restarts, and checkpoint resume.

ABOUTME: Event types emitted during pipeline execution for UI and logging. ABOUTME: Mirrors the Layer 2 EventHandler pattern with pipeline-specific event types.

ABOUTME: JSONL activity log writer — appends every event as a JSON line to a file. ABOUTME: Captures pipeline, agent, and LLM trace events for a complete audit trail in <runDir>/activity.jsonl.

ABOUTME: Logging event handler that prints pipeline lifecycle events to an io.Writer. ABOUTME: Provides human-readable output for CLI and debugging use cases.

ABOUTME: Variable interpolation for ${namespace.key} syntax in prompts and attributes. ABOUTME: Supports three namespaces: ctx (pipeline context), params (subgraph parameters), graph (graph attributes).

ABOUTME: Fidelity modes control how much prior context gets injected into node prompts. ABOUTME: Provides parsing, degradation chain, resolution from attrs, and context compaction.

ABOUTME: Git-backed artifact tracking for pipeline runs. ABOUTME: Initializes the artifact dir as a git repo and commits after each terminal-outcome node.

ABOUTME: Git environment preflight — runs before any node executes. ABOUTME: Honors workflow `requires:` declarations and the --git= policy flag.

ABOUTME: Core data model for pipeline graphs: Graph, Node, Edge structs. ABOUTME: Provides shape-to-handler mapping and graph traversal helpers.

ABOUTME: Handler interface and registry for pipeline node execution dispatch. ABOUTME: Each node shape maps to a handler; the registry resolves and executes them.

ABOUTME: Tracker-specific lint rules (TRK1XX). Encodes tracker's runtime ABOUTME: defaults — 64KB tool output cap, tail-window capture semantics — ABOUTME: that don't belong in dippin-lang itself but warrant validate-time ABOUTME: warnings because tracker owns the runtime.

ABOUTME: Typed view over Node.Attrs for each handler kind — replaces ad-hoc ABOUTME: map[string]string parsing scattered across handlers and engine code.

ABOUTME: OverrideDetail describes a single validation-override event captured at edge selection. ABOUTME: Actor enum identifies who took the override edge; ErrValidationOverridden is the CLI exit sentinel.

ABOUTME: Parses Graphviz DOT format into the pipeline Graph model. ABOUTME: Uses gographviz for parsing and extracts nodes, edges, and attributes.

ABOUTME: Named retry policies with configurable backoff strategies for pipeline nodes. ABOUTME: Provides resolution logic that checks node attrs, graph attrs, and falls back to defaults.

ABOUTME: CSS-like model stylesheet parser for per-node LLM configuration. ABOUTME: Supports universal (*), class (.name), and ID (#name) selectors with specificity ordering.

ABOUTME: Handler that executes a referenced sub-pipeline as a single node step. ABOUTME: Enables composition of pipelines via the "subgraph" node shape.

ABOUTME: TerminalStatus named string type for EngineResult.Status taxonomy. ABOUTME: Carries IsSuccess() helper used by CLI exit-code, audit, and JSON consumers.

ABOUTME: Structured execution trace recording for pipeline runs. ABOUTME: Captures node execution timing, handler outcomes, edge selections, and errors.

ABOUTME: Variable expansion and context injection for pipeline node attributes. ABOUTME: Expands $goal, graph-level variables, and appends prior node outputs to LLM prompts.

ABOUTME: Validates pipeline graph structure for correctness before execution. ABOUTME: Tracker-specific checks such as shapes and conditional routing always run. ABOUTME: Structural checks that dippin-lang already covers, including duplicate-edge checks, are skipped when DippinValidated=true.

ABOUTME: Semantic validation for pipeline graphs beyond structural checks. ABOUTME: Verifies handler registration, condition syntax, and node attribute types.

Index

Constants

View Source
const (
	ContextKeyOutcome        = "outcome"
	ContextKeyPreferredLabel = "preferred_label"
	ContextKeyGoal           = "graph.goal"
	ContextKeyLastResponse   = "last_response"
	ContextKeyHumanResponse  = "human_response"
	ContextKeyToolStdout     = "tool_stdout"
	ContextKeyToolStderr     = "tool_stderr"
	// ContextKeyToolMarker holds the routing marker extracted from a tool
	// node's stdout via the marker_grep attr (#210). The value is the last
	// regex match in stdout (capture group 1 if the regex has groups, full
	// match otherwise). LLM-origin (the tool subprocess emitted it), so
	// NOT in the tool_command safe-key allowlist; conditions can read it,
	// tool_command interpolation cannot.
	ContextKeyToolMarker = "tool_marker"
	// ContextKeyToolMarkerError carries the regex-compile error message
	// when marker_grep is configured with an invalid pattern. The runtime
	// owns this key; declared writes cannot collide with it (same model
	// as writes_error / writes_warning).
	ContextKeyToolMarkerError = "tool_marker_error"
	// ContextKeyToolRoute is the convention-based routing channel
	// (issue #212): the tool handler scans captured stdout for lines
	// matching `^\s*_TRACKER_ROUTE=(.+?)\s*$` and populates this key
	// with the LAST match's captured value (anchored, so an arbitrary
	// "_TRACKER_ROUTE" substring in the middle of another line does
	// not match). The complement to marker_grep (#210): same routing
	// channel idea, but author opts in by emitting the sentinel line
	// instead of declaring a regex on the node. LLM-origin (the
	// subprocess emitted the value), so NOT in the tool_command
	// safe-key allowlist and reserved from declared writes.
	ContextKeyToolRoute          = "tool_route"
	ContextKeySuggestedNextNodes = "suggested_next_nodes"

	// ContextKeyResponsePrefix is prepended to a node ID to form a per-node
	// response key (e.g. "response.mynode"). Downstream nodes can reference
	// specific upstream outputs without relying on last_response being current.
	ContextKeyResponsePrefix = "response."

	// ContextKeyTurnLimitMsg holds a diagnostic message when an agent exhausts
	// its turn limit or enters a tool call loop. Present only in failure outcomes
	// from turn-limit exhaustion; absent on normal success.
	ContextKeyTurnLimitMsg = "turn_limit_msg"

	// ContextKeyTurnBreachClass classifies a turn-limit breach under the guard
	// policy (#303). Set by the codergen handler on a breach; read by pipeline
	// edge conditions (e.g. `when ctx.turn_breach_class = operator_decision`).
	// Absent on normal success and on the turn_breach_policy: fail opt-out path
	// (which reproduces today's guillotine exactly).
	ContextKeyTurnBreachClass = "turn_breach_class"

	// Turn-breach classification values (#303).
	TurnBreachClassPathological     = "pathological"      // loop / no-progress → stop
	TurnBreachClassVerifiedGreen    = "verified_green"    // breach verify passed → advance as success
	TurnBreachClassOperatorDecision = "operator_decision" // steady progress, non-green → operator/fallback

	// ContextKeyEpisodeSummary stores the most recent codergen session's episode
	// summary (tool attempts + outcomes).
	ContextKeyEpisodeSummary = "episode_summary"

	// ContextKeyEpisodeSummaries stores the running list of prior episode
	// summaries encoded as a JSON array of strings.
	ContextKeyEpisodeSummaries = "episode_summaries"

	// Interview mode context keys. Overridable via questions_key/answers_key
	// node attributes in .dip files. These are the defaults when the attrs
	// are not specified.
	ContextKeyInterviewQuestions = "interview_questions"
	ContextKeyInterviewAnswers   = "interview_answers"

	// ContextKeyNodePrefix is the prefix for per-node scoped context keys.
	// After each node completes, ScopeToNode copies dirty keys into this namespace
	// so downstream nodes can read e.g. "node.MyAgent.last_response" without
	// colliding with the global "last_response" written by later nodes.
	ContextKeyNodePrefix = "node."
)

Built-in context keys used by the engine and handlers.

View Source
const ActivityLogSentinel = "\x1f\x1e"

ActivityLogSentinel is the two-byte prefix the runtime writes ahead of every JSONL line in the secure activity log. The bytes are ASCII Unit Separator (0x1F) and Record Separator (0x1E) — control characters that a normal text-mode subprocess effectively never emits. Their presence is the runtime's "I wrote this" mark; their absence on a line in the secure file is the injection signal.

This is detection, not authentication: the bytes are not secret and an attacker who reads tracker's source (open source) can emit them too. See the "Activity log integrity" section of CLAUDE.md for the threat model and the intentional scope of this defense.

View Source
const DefaultTruncateLimit = 500

DefaultTruncateLimit is the maximum character length for context values in truncate fidelity mode. Truncation is character-based (not token-based).

View Source
const EdgePriorityOverride = "override"

EdgePriorityOverride identifies an edge selected at advanceToNextNode that carries Edge.Override == true. Emitted as the EdgePriority on the EventDecisionEdge event for the override-edge selection. EventValidationOverridden rides alongside (not instead of) the DecisionEdge event for these traversals.

Untyped string constant to match the existing edge_priority values ("condition", "label", "suggested", "weight", "lexical") which are inlined as bare string literals at the call sites in engine_edges.go.

View Source
const (
	InternalKeyArtifactDir = "_artifact_dir"
)

Internal context keys used by the engine for bookkeeping.

Variables

View Source
var (
	ErrNilWorkflow            = errors.New("nil workflow")
	ErrMissingStart           = errors.New("workflow missing Start node")
	ErrMissingExit            = errors.New("workflow missing Exit node")
	ErrUnknownNodeKind        = errors.New("unknown node kind")
	ErrUnknownConfig          = errors.New("unknown config type")
	ErrInvalidSteerContextKey = errors.New("steer_context key contains ':' which breaks block-form round-trip through the .dip formatter")
	ErrMissingManagerLoopCfg  = errors.New("manager_loop node is missing required ir.ManagerLoopConfig")
	// ErrParenthesizedParsedCondition is returned by convertEdge when a Parsed-only
	// ir.Condition formats to an expression containing parentheses. The pipeline
	// edge evaluator (pipeline/condition.go) does not support parens — it tokenizes
	// on plain strings.Split("||") and strings.Split("&&"), so `a || (b && c)`
	// would become tokens like `(b` and `c)`. In EvaluateCondition, those are not
	// hard runtime errors: they are treated as unknown variable names, a warning is
	// logged, and they evaluate as empty strings, which can silently produce false
	// or otherwise incorrect results. The adapter rejects these expressions up
	// front to avoid that mis-evaluation. Authors should populate Condition.Raw
	// with a flat form (e.g. `a=1 || b=2 || c=3`) or simplify the Parsed tree so
	// no parens are emitted.
	ErrParenthesizedParsedCondition = errors.New("formatted Parsed condition uses parentheses, which the pipeline edge evaluator does not support")
)
View Source
var (
	// ErrGitNotInstalled — git missing from PATH and the workflow requires it.
	ErrGitNotInstalled = errors.New("git not installed")
	// ErrGitWorkdirNotRepo — workdir is not inside a git repository and the workflow requires it.
	ErrGitWorkdirNotRepo = errors.New("workdir is not a git repository")
	// ErrGitUnbornHEAD — workdir is inside a git work tree but HEAD is unborn
	// (no commits yet). Distinct from ErrGitWorkdirNotRepo: `git rev-parse
	// --is-inside-work-tree` returns true here, but `git worktree add ...
	// HEAD`, `git log`, `git merge` all fail until at least one commit
	// exists. Surfaced at preflight so requires:git workflows fail fast
	// instead of mid-run after burning LLM turns.
	ErrGitUnbornHEAD = errors.New("workdir is a git repository with no commits (unborn HEAD)")
	// ErrGitAutoInitRefused — --git=init requested but a safety latch fired (home, root, nested).
	ErrGitAutoInitRefused = errors.New("auto-init refused by safety latch")
)
View Source
var ErrValidationOverridden = errors.New("run completed via validation_overridden")

ErrValidationOverridden is the sentinel error returned by interpretRunResult when --fail-on-override is set and the run terminated as validation_overridden. The cobra entry checks errors.Is(err, ErrValidationOverridden) and exits with code 2 (distinct from generic-fail exit 1).

Functions

func ApplyGraphParamOverrides

func ApplyGraphParamOverrides(g *Graph, overrides map[string]string) error

ApplyGraphParamOverrides applies runtime overrides to graph-level params. Each override key must already exist in graph attrs as "params.<key>".

func AutoFix

func AutoFix(g *Graph) []string

AutoFix applies automatic corrections to a graph and returns descriptions of each fix applied. Currently fixes conditional nodes missing fail edges by adding a self-referencing retry edge.

func CompactContext

func CompactContext(ctx *PipelineContext, completedNodes []string, fidelity Fidelity, artifactDir string, runID string) map[string]string

CompactContext reads the checkpoint context and optionally artifact files from disk, returning a compacted version based on the fidelity level.

func CompactContextWithPinnedKeys

func CompactContextWithPinnedKeys(ctx *PipelineContext, completedNodes []string, fidelity Fidelity, artifactDir string, runID string, pinnedKeys []string) map[string]string

CompactContextWithPinnedKeys is like CompactContext but keeps the provided keys in medium/truncate modes in addition to the built-in medium key set.

func EvaluateCondition

func EvaluateCondition(expr string, ctx *PipelineContext) (bool, error)

EvaluateCondition evaluates a condition expression against the pipeline context. Empty or whitespace-only conditions always return true. Parsing priority: || (lowest) then && (higher) then individual clauses.

func ExpandGraphVariables

func ExpandGraphVariables(text string, vars map[string]string) string

ExpandGraphVariables substitutes $key references in text with values from graph-level attributes. The vars map should come from GraphVarMap. For example, graph[target_name="foo"] expands $target_name to "foo". This applies to any node attribute (prompt, tool_command, etc.) so all handlers get uniform variable expansion.

func ExpandPromptVariables

func ExpandPromptVariables(prompt string, ctx *PipelineContext) string

func ExpandVariables

func ExpandVariables(
	text string,
	ctx *PipelineContext,
	params map[string]string,
	graphAttrs map[string]string,
	strict bool,
	toolCommandMode ...bool,
) (string, error)

ExpandVariables replaces ${namespace.key} patterns with values from the provided sources. Supports three namespaces:

  • ctx: runtime context (from PipelineContext)
  • params: subgraph parameters (passed explicitly)
  • graph: graph-level attributes (from Graph.Attrs)

In lenient mode (strict=false), undefined variables expand to empty string. In strict mode (strict=true), undefined variables return an error.

When toolCommandMode is true (optional variadic parameter), only allowlisted ctx.* keys can be expanded — all others return an error to prevent LLM output injection into shell commands.

Examples:

${ctx.human_response} → value from PipelineContext
${params.model} → value from subgraph params
${graph.goal} → value from graph attributes

func ExponentialBackoff

func ExponentialBackoff(attempt int, base time.Duration) time.Duration

ExponentialBackoff returns 2^attempt * base with ±25% jitter, capped at 60s.

func ExtractDeclaredWrites

func ExtractDeclaredWrites(writes []string, rawJSON string) (updates map[string]string, extras []string, err error)

ExtractDeclaredWrites parses rawJSON as a top-level JSON object and extracts each declared key as a context update value.

String fields are written as plain strings. Non-string fields are written as compact JSON text (for example arrays, objects, numbers, booleans, null).

func ExtractJSONFromText

func ExtractJSONFromText(text string) (string, bool)

ExtractJSONFromText tries to find a valid JSON OBJECT embedded in text. It iterates ```...``` fenced blocks first (returning the first whose content parses as a JSON object — so a non-JSON fence ahead of a valid JSON fence doesn't block discovery), then falls back to scanning for top-level {…} spans (preferring the first balanced span that parses, not the outermost-brace shortcut, which fails when prose contains stray brace pairs around real JSON).

Returns the extracted JSON string and true if a valid JSON object was found, or ("", false) otherwise.

Intended for use by pipeline handlers; the Go-package boundary requires it to be exported, but it isn't part of the stable embedder API.

func ExtractParamsFromGraphAttrs

func ExtractParamsFromGraphAttrs(graphAttrs map[string]string) map[string]string

ExtractParamsFromGraphAttrs returns params declared in graph attrs under the "params." prefix.

func GitProbeEnv

func GitProbeEnv() []string

GitProbeEnv returns the safe env with locale forced to C so any stderr we parse (`not a git repository`, `is-inside-work-tree`) is stable regardless of the operator's LANG/LC_* settings. Use this for the preflight + doctor probes that classify git output; the artifact repo's user-visible operations stay on GitSafeEnv so commits/branches still respect the user's locale for messages they actually see.

func GitSafeEnv

func GitSafeEnv() []string

GitSafeEnv returns a copy of the current environment with sensitive variables (API keys, secrets, tokens, passwords) stripped before passing to a git subprocess. Exported so external callers (tracker doctor's probeGitForDoctor) can match the runtime preflight's sanitized-environment posture without duplicating the helper. Honors the TRACKER_PASS_ENV=1 escape hatch.

func GraphParamAttrKey

func GraphParamAttrKey(key string) string

GraphParamAttrKey returns the graph.Attrs key used to store workflow param `key` (i.e. "params."+key). Callers should use this helper rather than hard-coding the prefix so the shape stays consistent if it ever changes.

func GraphVarMap

func GraphVarMap(ctx *PipelineContext) map[string]string

GraphVarMap extracts graph-level variables from the pipeline context as a $key → value map. Call once per node and pass the result to ExpandGraphVariables to avoid repeated Snapshot() copies.

func HasBornHEAD

func HasBornHEAD(ctx context.Context, workDir string) (bool, error)

HasBornHEAD reports whether HEAD in workDir points at a real commit. Thin public alias of hasBornHEAD; exported so the doctor's git-requires preview can model the same born-HEAD check the runtime preflight does. Returns (false, nil) for an unborn repo — error is reserved for unexpected I/O.

func InjectPipelineContext

func InjectPipelineContext(prompt string, ctx *PipelineContext) string

InjectPipelineContext appends relevant pipeline context values to the prompt so the LLM can see prior node outputs, human responses, etc.

func IsToolCommandSafeCtxKey

func IsToolCommandSafeCtxKey(key string) bool

IsToolCommandSafeCtxKey reports whether key is on the tool_command safe-key allowlist. The allowlist exists so that LLM-origin ctx.* keys cannot be expanded into shell input. Workflow authors that declare a `writes:` key colliding with this list would funnel LLM output into a reserved name and bypass the sanitization gate; declared-writes processing uses this to reject such collisions before they're written.

func LinearBackoff

func LinearBackoff(attempt int, base time.Duration) time.Duration

LinearBackoff returns (attempt+1) * base with ±25% jitter, capped at 60s. Like ExponentialBackoff, attempt is 0-indexed: attempt 0 = 1*base.

func LintTrackerRules

func LintTrackerRules(g *Graph) []string

LintTrackerRules runs tracker-specific lint rules (TRK1XX). DIP1XX lint (DIP101–DIP137, etc.) is owned by dippin-lang and runs at .dip load time via LoadDippinWorkflow → validator.Lint; tracker does not duplicate it, so this is the only lint entry point tracker should expose.

func LoadDipxBundle

func LoadDipxBundle(ctx context.Context, path string) (*Graph, map[string]*Graph, BundleInfo, []validator.Diagnostic, error)

LoadDipxBundle opens a .dipx file, verifies all SHA-256 hashes via dipx.Open (strict mode), converts the entry workflow and every transitively- referenced subgraph from pre-parsed IR to tracker Graphs, and returns the graphs plus a BundleInfo carrying the bundle's content-addressed identity.

The subgraphs map is keyed by canonical bundle path (matching manifest.Files entries). dipx has already verified ref closure and acyclicity, so no recursive walk is needed on tracker's side.

After IR-to-Graph conversion, every subgraph_ref attr on every loaded graph is rewritten from the author's source ref (e.g., "sub.dip") to the canonical bundle path (e.g., "workflows/sub.dip") so refs match the subgraphs map keys.

Diagnostics from the bundled IR (lint warnings; DIP126 "subgraph ref does not exist" is filtered since dipx already verified ref closure) are returned for the caller to log. The library deliberately does not write to os.Stderr here so embedded callers can route output through their own logger; the CLI wrapper prints them to stderr, mirroring the .dip path.

func ParseDeclaredKeys

func ParseDeclaredKeys(attr string) []string

ParseDeclaredKeys splits a comma-separated attr value into trimmed keys.

func ParseSubgraphParams

func ParseSubgraphParams(paramsStr string) map[string]string

ParseSubgraphParams parses a comma-separated string of key=value pairs into a map. Format: "key1=val1,key2=val2" Returns an empty map if the input is empty.

func ParseToolList

func ParseToolList(csv string) []string

ParseToolList splits a comma-separated tool list, trimming whitespace.

func Preflight

func Preflight(ctx context.Context, cfg PreflightConfig) error

Preflight runs the dependency checks declared by the workflow header against the environment, honoring the resolved policy. Returns nil on pass / bypass / downgraded-to-warning. Returns a typed error on hard fail.

Safe to call multiple times — only side effect is the optional `git init` triggered by --git=init.

func SafetyLatches

func SafetyLatches(ctx context.Context, workDir string) error

SafetyLatches returns a wrapped ErrGitAutoInitRefused when auto-init would be unsafe at workDir, or nil if it would proceed. Exported so the tracker doctor preview check can model --git=init --allow-init behavior without duplicating the latch logic. Doctor callers can pass context.Background() or a real ctx; cancellation aborts the git subprocess.

Refusal cases — see safetyLatches for the full list. This is a thin public alias.

func SaveCheckpoint

func SaveCheckpoint(cp *Checkpoint, path string) error

SaveCheckpoint writes the checkpoint to disk as JSON, creating directories as needed.

func SecureActivityLogPath

func SecureActivityLogPath(runID string) (string, error)

SecureActivityLogPath returns the absolute path where the runtime writes the integrity-protected activity log for runID. The path is outside any directory a tool subprocess sees as cmd.Dir, so the most common LLM-tool-mistake attack vectors (relative-path shell redirection, find-in-cwd globs) cannot reach it.

Resolution order:

  1. $TRACKER_AUDIT_DIR/<runID>/activity.jsonl — explicit operator override.
  2. $XDG_STATE_HOME/tracker/runs/<runID>/activity.jsonl — when XDG_STATE_HOME is set.
  3. $HOME/.local/state/tracker/runs/<runID>/activity.jsonl — Linux + macOS default.
  4. %LOCALAPPDATA%\tracker\runs\<runID>\activity.jsonl — Windows fallback.
  5. os.TempDir()/tracker-audit/<runID>/activity.jsonl — last-resort when no $HOME (containers, restricted envs).

runID must be a single clean path element — no separators, no "..", no ".". This guards against tampered checkpoints that could try to escape the secure base via path traversal (the resume path reads runID from <workDir>/.tracker/runs/<runID>/checkpoint.json, which is attacker-reachable).

$TRACKER_AUDIT_DIR and $XDG_STATE_HOME must be absolute paths. A relative value would re-anchor the "secure" path to the process CWD — defeating the relocation entirely — so a relative env value is silently ignored and the resolver falls through to the next candidate.

func ShapeToHandler

func ShapeToHandler(shape string) (string, bool)

ShapeToHandler returns the handler name for a DOT node shape. Returns ("", false) if the shape is not recognized.

func ValidPreflight

func ValidPreflight(v GitPreflight) bool

ValidPreflight reports whether v is a recognized policy value. The empty string is valid and resolves to auto.

func Validate

func Validate(g *Graph) error

Validate checks a parsed Graph for structural correctness. Returns nil if the graph has no errors. Warning-only results return nil so that callers treating non-nil as fatal do not block valid graphs. Use ValidateAll to retrieve both errors and warnings.

func ValidateSemantic

func ValidateSemantic(g *Graph, registry *HandlerRegistry) (errors error, warnings []string)

ValidateSemantic checks a Graph for semantic correctness against a handler registry. It always runs tracker-runtime-specific checks that dippin-lang cannot perform — handler registration (tracker owns the registry) and edge condition syntax (tracker owns the runtime evaluator dialect) — plus tracker-specific lint rules (TRK1XX).

Typed node-attribute checks (max_retries, cache_tool_results, context_compaction, context_compaction_threshold) are dippin's domain: these are typed fields on *ir.Workflow, validated by dippin's parser and lint (e.g. DIP116 for compaction_threshold range). They run here only when the graph did NOT come from a .dip source (DOT inputs and programmatically-constructed graphs in library callers and tests) — otherwise tracker would duplicate dippin's work and risk diverging from `dippin doctor`. DIP1XX lint flows separately through Graph.LintWarnings populated at load time.

func ValidateToolLists

func ValidateToolLists(allowed, disallowed []string) error

ValidateToolLists returns an error if both allowed and disallowed are set.

func WorkdirHasContent

func WorkdirHasContent(workDir string) (bool, error)

WorkdirHasContent reports whether workDir contains any entry other than `.git`. Thin public alias of workdirHasContent; exported so the doctor's --git=init --allow-init preview can model the auto-init workdir-content latch without duplicating the rule.

func WriteStageArtifacts

func WriteStageArtifacts(rootDir, nodeID, prompt, response string, outcome Outcome) error

func WriteStatusArtifact

func WriteStatusArtifact(rootDir, nodeID string, outcome Outcome) error

Types

type ACPConfig

type ACPConfig struct {
	Agent string // explicit agent binary: "claude-agent-acp", "codex-acp", "gemini" (overrides provider mapping)
}

ACPConfig holds ACP-backend-specific settings.

type Actor

type Actor string

Actor identifies who took a validation-override edge. Stored on OverrideDetail.Actor. Defined as a named string type so JSON marshals as the bare string and the constant set is grep-able.

const (
	ActorHuman     Actor = "human"     // human-driven interviewer (TUI or non-TUI console)
	ActorAutopilot Actor = "autopilot" // any autopilot variant (LLM-backed or deterministic auto-approve)
	ActorWebhook   Actor = "webhook"   // external callback via WebhookInterviewer
	ActorUnknown   Actor = "unknown"   // third-party or future Interviewer with no recognized Actor() value
)

type AgentBackend

type AgentBackend interface {
	Run(ctx context.Context, cfg AgentRunConfig, emit func(agent.Event)) (agent.SessionResult, error)
}

AgentBackend executes an agent session and streams events.

type AgentNodeConfig

type AgentNodeConfig struct {
	Backend         string
	WorkingDir      string
	McpServers      string // raw JSON; parsed by the handler
	AllowedTools    string
	DisallowedTools string
	MaxBudgetUSD    float64
	PermissionMode  string
	ACPAgent        string

	// ToolAccess restricts the agent's tool surface. When non-empty (any
	// value), the runtime registers zero tools, sets ToolChoice=none on
	// LLM requests, scrubs tool-naming text from the system prompt, and
	// rejects Params bypass keys. Canonical: case-insensitive, whitespace-
	// trimmed. Fail-closed for typos. See agent.SessionConfig.ToolAccess.
	// Issue: github.com/2389-research/tracker#258.
	ToolAccess string

	AutoStatus        bool
	ReflectOnError    bool // initialized to true by AgentConfig; explicit "false" disables
	ReflectOnErrorSet bool // true when the attr was present on the node

	VerifyAfterEdit    bool
	VerifyAfterEditSet bool
	VerifyCommand      string
	MaxVerifyRetries   int

	PlanBeforeExecute    bool
	PlanBeforeExecuteSet bool

	Model            string
	Provider         string
	SystemPrompt     string
	MaxTurns         int
	TurnBreachPolicy string // #303: "guard" (default) or "fail" (opt-out)
	CommandTimeout   time.Duration
	ReasoningEffort  string

	ResponseFormat string
	ResponseSchema string

	// WritablePaths bounds the file paths this agent's tools may write,
	// as author-chosen globs resolved against the session root. Non-empty
	// triggers the runtime fs-jail (Linux Landlock for Bash subprocess +
	// openat2 for in-process Write/Edit/ApplyPatch). Distinguishing absent
	// from present-but-empty requires WritablePathsSet — the configureJail
	// gate refuses-to-start when Set && len == 0 so a malformed/whitespace
	// attr can never silently degrade to unbounded. See issue #272.
	WritablePaths []string

	// WritablePathsSet records whether the writable_paths attr was present
	// on the node. Distinguishes "absent" (Set=false, jail disabled) from
	// "present but parses to no entries" (Set=true, fail-CLOSED at the
	// codergen configureJail gate per issue #272). Mirrors the three-state
	// pattern of ReflectOnErrorSet et al. above.
	WritablePathsSet bool

	CacheToolResults    bool
	CacheToolResultsSet bool

	// ContextCompaction carries the raw attr value. "auto" enables automatic
	// compaction; any other non-empty value (e.g. "none") disables it; ""
	// means the attr was absent. Kept as string so future modes can be added
	// without a type change.
	ContextCompaction    string
	ContextCompactionSet bool
	CompactionThreshold  float64
}

AgentNodeConfig is a typed view over a codergen (agent) node's attributes.

For the subset of attrs that support graph-level defaults (llm_model, llm_provider, reasoning_effort, verify_after_edit, verify_command, max_verify_retries, plan_before_execute, cache_tool_results, context_compaction), AgentConfig resolves the value from graphAttrs first, then lets node.Attrs override when the same key is set on the node. The remaining fields are node-only and have no graph fallback.

Unless documented otherwise on the specific field, fields absent from their applicable source use the Go zero value. ReflectOnError is the one exception: it defaults to true in the returned struct even when the attr is absent, matching the runtime default. The companion *Set bool on three- state booleans distinguishes "explicitly configured" from "absent" so consumers that want to treat absence as "leave the existing value" can.

type AgentRunConfig

type AgentRunConfig struct {
	Prompt       string
	SystemPrompt string
	Model        string
	Provider     string
	WorkingDir   string
	MaxTurns     int
	Timeout      time.Duration
	Extra        any // backend-specific: *ClaudeCodeConfig for claude-code backend

	// ToolAccess restricts the agent's tool surface. When non-empty, every
	// backend must enforce: native via agent.SessionConfig.IsToolAccessRestricted;
	// claude-code via the DisallowedTools list set by applyClaudeCodeToolAccess;
	// ACP refuses session creation (no verified deny-equivalent — see
	// backend_acp.go for the refusal site). Issue: github.com/2389-research/tracker#258.
	ToolAccess string
}

AgentRunConfig carries common config all backends need.

type BudgetBreach

type BudgetBreach struct {
	Kind    BudgetBreachKind
	Message string
}

BudgetBreach describes the outcome of a guard check.

type BudgetBreachKind

type BudgetBreachKind int

BudgetBreachKind classifies which limit was hit.

const (
	BudgetOK BudgetBreachKind = iota
	BudgetTokens
	BudgetCost
	BudgetWallTime
)

func (BudgetBreachKind) String

func (k BudgetBreachKind) String() string

String returns a human-readable label for a breach kind. Used as the halt reason in EngineResult.BudgetLimitsHit.

type BudgetGuard

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

BudgetGuard evaluates BudgetLimits against a UsageSummary snapshot and a run start time. The zero value is not usable; construct via NewBudgetGuard.

func NewBudgetGuard

func NewBudgetGuard(limits BudgetLimits) *BudgetGuard

NewBudgetGuard constructs a BudgetGuard with the given limits. Returns nil when limits.IsZero() so callers can use the nil-guard pattern to skip checks when no limits are configured.

func (*BudgetGuard) Check

func (g *BudgetGuard) Check(usage *UsageSummary, started time.Time) BudgetBreach

Check reports whether the given usage snapshot breaches any configured limit. A nil guard and a nil usage are both safe and return BudgetOK. Token and cost ceilings are inclusive — the exact limit is not a breach.

type BudgetLimits

type BudgetLimits struct {
	MaxTotalTokens int
	MaxCostCents   int
	MaxWallTime    time.Duration
}

BudgetLimits configures hard ceilings for a pipeline run. A zero-value field means "no limit" for that dimension.

func (BudgetLimits) IsZero

func (l BudgetLimits) IsZero() bool

IsZero reports whether every limit is unset.

type BundleInfo

type BundleInfo struct {
	Identity  string
	EntryPath string
	Manifest  dipx.Manifest
}

BundleInfo carries the metadata extracted from a loaded .dipx bundle. Identity is the canonical "sha256:<64 hex>" form of the bundle's content-addressed hash (SHA-256 of manifest.json bytes-as-stored). EntryPath is the canonical bundle-relative path of the entry workflow.

type Checkpoint

type Checkpoint struct {
	RunID          string            `json:"run_id"`
	CurrentNode    string            `json:"current_node"`
	CompletedNodes []string          `json:"completed_nodes"`
	RetryCounts    map[string]int    `json:"retry_counts"`
	Context        map[string]string `json:"context"`
	Timestamp      time.Time         `json:"timestamp"`
	RestartCount   int               `json:"restart_count"`

	// EdgeSelections stores the selected outgoing edge target for each
	// completed node (nodeID -> selected edge To). Used on resume to
	// replay routing decisions instead of re-evaluating stale conditions.
	EdgeSelections map[string]string `json:"edge_selections,omitempty"`

	// FallbackTaken tracks which goal-gate nodes have already used their
	// one-shot fallback/escalation route. Persisted in checkpoint JSON so
	// the guard survives checkpoint save/restore cycles.
	FallbackTaken map[string]bool `json:"fallback_taken,omitempty"`

	// WIPRefs maps a failed/exhausted node ID to the recoverable git ref
	// (a tag tracker/wip/<runID>/<nodeID>) where its uncommitted work was
	// preserved before the engine routed away from it (#302). Additive;
	// omitempty keeps older checkpoints without the field loading cleanly.
	WIPRefs map[string]string `json:"wip_refs,omitempty"`

	// BundleIdentity is the content-addressed identity of the .dipx bundle
	// the run was started against ("sha256:<hex>"). Empty for runs started
	// from a plain .dip file. Used for strict resume verification.
	BundleIdentity string `json:"bundle_identity,omitempty"`

	// ValidationOverrides persists the override sticky list across resume and
	// bundle export. Appended at the flip-point in advanceToNextNode whenever
	// an override edge is traversed; never cleared by clearDownstream or
	// handleLoopRestart. omitempty for backwards compat with pre-v0.35
	// checkpoints (absent = "no overrides happened").
	ValidationOverrides []OverrideDetail `json:"validation_overrides,omitempty"`
	// contains filtered or unexported fields
}

Checkpoint captures the execution state of a pipeline run for resume support.

func LoadCheckpoint

func LoadCheckpoint(path string) (*Checkpoint, error)

LoadCheckpoint reads a checkpoint from a JSON file on disk.

func (*Checkpoint) ClearCompleted

func (cp *Checkpoint) ClearCompleted(nodeID string)

ClearCompleted removes a node from the completed set so it will re-execute.

func (*Checkpoint) GetEdgeSelection

func (cp *Checkpoint) GetEdgeSelection(nodeID string) (string, bool)

GetEdgeSelection returns the stored edge selection for a node, if any.

func (*Checkpoint) IncrementRetry

func (cp *Checkpoint) IncrementRetry(nodeID string)

IncrementRetry increments the retry counter for the given node by one.

func (*Checkpoint) IsCompleted

func (cp *Checkpoint) IsCompleted(nodeID string) bool

IsCompleted returns true if the given node ID has been marked as completed.

func (*Checkpoint) MarkCompleted

func (cp *Checkpoint) MarkCompleted(nodeID string)

MarkCompleted adds the given node ID to the completed nodes list. Duplicate IDs are ignored.

func (*Checkpoint) RecordWIPRef added in v0.36.0

func (cp *Checkpoint) RecordWIPRef(nodeID, ref string)

RecordWIPRef records the recoverable git ref where a failed/exhausted node's uncommitted work was preserved (#302).

func (*Checkpoint) RetryCount

func (cp *Checkpoint) RetryCount(nodeID string) int

RetryCount returns the number of retries recorded for the given node. Returns 0 if the node has no retry history or if the map is nil.

func (*Checkpoint) SetEdgeSelection

func (cp *Checkpoint) SetEdgeSelection(nodeID, edgeTo string)

SetEdgeSelection records the selected outgoing edge for a completed node.

type ChildRunContext

type ChildRunContext struct {
	// BudgetGuard is the parent engine's budget guard. Child runs should
	// pass it via WithBudgetGuard so the same limits enforce within the
	// child. Nil when the parent has no budget configured.
	BudgetGuard *BudgetGuard

	// Baseline is an immutable snapshot of the parent's aggregated usage
	// at the moment the child was launched. Child runs should pass it via
	// WithBaselineUsage so the child's budget check folds baseline + its
	// own trace aggregate before comparing to limits. Without this, a
	// nested budget check would only see child-local spend and the
	// effective ceiling inside a subgraph would grow by the parent's
	// already-consumed amount.
	Baseline *UsageSummary
}

ChildRunContext is the execution context a handler may need when it launches a child run (subgraph, manager_loop). Carries the parent engine's BudgetGuard and a snapshot of usage already consumed so the child can enforce limits combined with the parent's running total. Retrieved via ChildRunContextFromContext.

func ChildRunContextFromContext

func ChildRunContextFromContext(ctx context.Context) *ChildRunContext

ChildRunContextFromContext returns the ChildRunContext stashed on ctx by the engine before dispatching a handler, or nil when no such value exists (top-level contexts outside a running engine).

type ClaudeCodeConfig

type ClaudeCodeConfig struct {
	MCPServers      []MCPServerConfig
	AllowedTools    []string
	DisallowedTools []string
	MaxBudgetUSD    float64
	PermissionMode  PermissionMode
}

ClaudeCodeConfig holds Claude-Code-specific settings.

type ConditionEval

type ConditionEval struct {
	EdgeTo    string `json:"edge_to"`
	Condition string `json:"condition"`
}

ConditionEval records a single conditional edge whose condition evaluated false during routing. Used as the payload of EventConditionalFallthrough so diagnostics can show which routing intents missed before the fallback fired.

type CostSnapshot

type CostSnapshot struct {
	TotalTokens    int
	TotalCostUSD   float64
	ProviderTotals map[string]ProviderUsage
	WallElapsed    time.Duration
	Estimated      bool
}

CostSnapshot is the payload for EventCostUpdated and EventBudgetExceeded events. It is a point-in-time view of the run's aggregate token usage, cost, and wall-clock elapsed time. Estimated is true when any session contributing to this snapshot was heuristic-derived (e.g. ACP rune-count estimator); per-provider detail is carried inside ProviderTotals via ProviderUsage.Estimated.

type DecisionDetail

type DecisionDetail struct {
	// Edge selection fields.
	EdgeFrom     string `json:"edge_from,omitempty"`
	EdgeTo       string `json:"edge_to,omitempty"`
	EdgePriority string `json:"edge_priority,omitempty"` // "condition", "label", "suggested", "weight", "lexical", "override"

	// Condition evaluation fields.
	EdgeCondition  string `json:"edge_condition,omitempty"`
	ConditionMatch bool   `json:"condition_match,omitempty"`

	// Node outcome fields.
	OutcomeStatus  string            `json:"outcome_status,omitempty"`
	ContextUpdates map[string]string `json:"context_updates,omitempty"`

	// Context snapshot at the decision point (routing-relevant keys).
	ContextSnapshot map[string]string `json:"context_snapshot,omitempty"`

	// Restart/loop fields.
	RestartCount int      `json:"restart_count,omitempty"`
	ClearedNodes []string `json:"cleared_nodes,omitempty"`

	// Session stats from handler outcome.
	TokenInput  int `json:"token_input,omitempty"`
	TokenOutput int `json:"token_output,omitempty"`

	// ConditionsTried lists the conditional edges that were evaluated
	// false on the way to a fallback selection. Populated only on
	// EventConditionalFallthrough events. Each entry pairs the target
	// node with the literal condition string so a diagnostics consumer
	// can render "your routing intents X, Y, Z all missed" without
	// re-evaluating expressions.
	ConditionsTried []ConditionEval `json:"conditions_tried,omitempty"`
}

DecisionDetail carries structured data about a pipeline decision point. It is attached to PipelineEvent via the Decision field for audit trail events.

type Edge

type Edge struct {
	From      string
	To        string
	Label     string
	Condition string
	// Override marks the edge as a validation-override path. When the engine
	// traverses an override edge from a wait.human gate, the run's terminal
	// EngineResult.Status becomes OutcomeValidationOverridden (audit-only;
	// routing is unaffected). Valid only on edges from wait.human-handler
	// nodes; enforced by the adapter at graph construction time.
	Override bool
	Attrs    map[string]string
}

Edge represents a directed connection between two nodes.

type Engine

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

Engine executes a pipeline graph by traversing nodes, dispatching handlers, selecting edges, and managing retries and checkpoints.

func NewEngine

func NewEngine(graph *Graph, registry *HandlerRegistry, opts ...EngineOption) *Engine

NewEngine creates a pipeline engine for the given graph and handler registry.

func (*Engine) Graph

func (e *Engine) Graph() *Graph

Graph returns the graph this engine executes. Used by library callers that need to inspect graph attributes after construction.

func (*Engine) Run

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

Run executes the pipeline to completion or failure.

type EngineOption

type EngineOption func(*Engine)

EngineOption configures optional Engine behavior.

func WithArtifactDir

func WithArtifactDir(dir string) EngineOption

WithArtifactDir sets the base directory for pipeline run artifacts. Node artifacts are written to <artifactDir>/<nodeID>/ instead of the working directory.

func WithBaselineUsage

func WithBaselineUsage(baseline *UsageSummary) EngineOption

WithBaselineUsage pre-loads the engine's BudgetGuard with usage already consumed by a parent run. Used by subgraph execution so the child's guard check sees parent spend + child trace combined, preventing the "subgraph sandbox" escape where an operator's --max-tokens / --max-cost ceiling would otherwise be silently non-binding for nodes nested in a subgraph. Nil baselines are no-ops.

func WithBudgetGuard

func WithBudgetGuard(guard *BudgetGuard) EngineOption

WithBudgetGuard attaches a BudgetGuard evaluated after every terminal node outcome. Nil guards are no-ops.

func WithBundleIdentity

func WithBundleIdentity(id string) EngineOption

WithBundleIdentity stamps every PipelineEvent the engine emits with the given content-addressed identity string (typically "sha256:<hex>"). Used to thread .dipx bundle identity into the activity log so every line of activity.jsonl carries provenance. Empty string (the default) is a no-op and matches the behavior for plain .dip runs.

func WithCheckpointPath

func WithCheckpointPath(path string) EngineOption

WithCheckpointPath enables checkpoint save/resume at the given file path.

func WithGitArtifacts

func WithGitArtifacts(enabled bool) EngineOption

WithGitArtifacts enables git-backed artifact tracking. When enabled, the artifact dir is initialized as a git repo at run start, and each terminal node outcome produces one commit capturing the artifact state at that point. Checkpoint saves made via saveCheckpointWithTag (not all saveCheckpoint call sites) also create a lightweight git tag of the form checkpoint/<runID>/<nodeID> pointing at the most recent node-outcome commit, intended as the basis for future checkpoint-replay support (Layer 2 of issue #77 — not wired up by this option).

Requires git in PATH. Silently no-ops if artifactDir is not set.

func WithInitialContext

func WithInitialContext(ctx map[string]string) EngineOption

WithInitialContext pre-populates the pipeline context with the given values. Used by subgraph execution to pass parent context into child pipelines.

func WithPipelineEventHandler

func WithPipelineEventHandler(h PipelineEventHandler) EngineOption

WithPipelineEventHandler sets the event handler for pipeline lifecycle events.

func WithSteeringChan

func WithSteeringChan(ch <-chan map[string]string) EngineOption

WithSteeringChan provides a channel for injecting context updates into the pipeline between node executions. The engine drains pending updates after each node's outcome is applied, making steered values visible to edge selection and the next node's prompt expansion. Nil channels are no-ops.

func WithStylesheetResolution

func WithStylesheetResolution(enabled bool) EngineOption

WithStylesheetResolution enables model stylesheet resolution on nodes before execution.

type EngineResult

type EngineResult struct {
	RunID           string
	Status          TerminalStatus
	CompletedNodes  []string
	Context         map[string]string
	Trace           *Trace
	Usage           *UsageSummary
	BudgetLimitsHit []string // populated when a BudgetGuard halted the run
	// ValidationOverrides is the list of override edges traversed during this
	// run, in chronological order. Populated for every terminal path (success,
	// fail, budget, validation_overridden) so failure-after-override forensics
	// still see the override. Empty for runs with no override edges.
	//
	// The terminal-status rule writes Status=OutcomeValidationOverridden when
	// len(ValidationOverrides) > 0 AND the run reached the success exit;
	// failure paths return fail/budget regardless of override presence.
	ValidationOverrides []OverrideDetail
}

EngineResult holds the final outcome of a pipeline execution run.

type Fidelity

type Fidelity string

Fidelity represents a context fidelity level for pipeline execution.

const (
	// FidelityFull includes complete context from checkpoint (default for first execution).
	FidelityFull Fidelity = "full"
	// FidelitySummaryHigh includes all context keys plus trimmed artifact responses.
	FidelitySummaryHigh Fidelity = "summary:high"
	// FidelitySummaryMedium includes key decisions only: outcome, last_response, human_response.
	FidelitySummaryMedium Fidelity = "summary:medium"
	// FidelitySummaryLow includes one-line per completed node.
	FidelitySummaryLow Fidelity = "summary:low"
	// FidelityCompact includes only current task context, no prior node history.
	FidelityCompact Fidelity = "compact"
	// FidelityTruncate drops oldest context entries to fit a configurable token budget.
	FidelityTruncate Fidelity = "truncate"
)

func DegradeFidelity

func DegradeFidelity(f Fidelity) Fidelity

DegradeFidelity returns the next lower fidelity level. Truncate is the floor and degrades to itself.

func ParseFidelity

func ParseFidelity(s string) (Fidelity, error)

ParseFidelity parses a string into a Fidelity value, returning an error for unrecognized values.

func ResolveFidelity

func ResolveFidelity(node *Node, graphAttrs map[string]string) Fidelity

ResolveFidelity determines the fidelity level for a node by checking the node attribute "fidelity", then the graph attribute "default_fidelity", then falling back to FidelityFull.

type GitPreflight

type GitPreflight string

GitPreflight is the resolved preflight policy passed to Preflight. The empty string ("") resolves to "auto".

const (
	GitPreflightAuto    GitPreflight = ""
	GitPreflightOff     GitPreflight = "off"
	GitPreflightWarn    GitPreflight = "warn"
	GitPreflightRequire GitPreflight = "require"
	GitPreflightInit    GitPreflight = "init"
)

type Graph

type Graph struct {
	Name      string
	Nodes     map[string]*Node
	Edges     []*Edge
	Attrs     map[string]string
	StartNode string
	ExitNode  string

	// NodeOrder preserves the declaration order of nodes from the source file.
	// Used by the TUI to display nodes in a sensible order (declaration order)
	// rather than BFS order which puts "Done" in the middle.
	NodeOrder []string

	// DippinValidated is set to true when the graph was produced from a .dip
	// source that has already passed dippin-lang's structural validator
	// (DIP001–DIP009). Tracker's own validateGraph skips checks that overlap
	// with those diagnostics, preventing false positives and divergence between
	// `dippin doctor` and `tracker validate`.
	//
	// CONTRACT: this flag reflects the graph's state at the moment dippin
	// validation ran. If the graph is subsequently mutated (nodes or edges
	// added/removed programmatically), callers MUST clear this flag or
	// re-run dippin validation before calling Validate again — otherwise
	// tracker's structural checks will be skipped for a shape that has
	// changed since dippin last validated it.
	DippinValidated bool

	// LintWarnings carries pre-formatted warning lines from dippin-lang's
	// lint pass (DIP1XX). Populated by LoadDippinWorkflowFromIR for .dip /
	// .dipx sources and empty for DOT graphs. tracker.ValidateAll surfaces
	// these alongside its own structural warnings so callers (validate /
	// simulate / doctor) see a single warnings list without re-running any
	// DIP-coded check on the tracker side. Format matches tracker's
	// single-line convention ("warning[DIPxxx]: ...") to render cleanly
	// inside bulleted "Validation Warnings" output.
	LintWarnings []string
	// contains filtered or unexported fields
}

Graph represents a parsed pipeline as a directed graph.

func FromDippinIR

func FromDippinIR(workflow *ir.Workflow) (*Graph, error)

FromDippinIR converts a Dippin IR Workflow to a Tracker Graph. The resulting Graph is semantically equivalent to one produced by ParseDOT for the same workflow, enabling transparent interoperability.

Field mappings:

  • IR Workflow.Name → Graph.Name
  • IR Workflow.Start → Graph.StartNode
  • IR Workflow.Exit → Graph.ExitNode
  • IR Workflow.Defaults → Graph.Attrs (flattened)
  • IR Node → Graph.Node (with kind → shape mapping)
  • IR Edge → Graph.Edge (with condition serialization)

Returns an error if:

  • workflow is nil
  • Start or Exit are empty
  • A node has an unknown NodeKind

func InjectParamsIntoGraph

func InjectParamsIntoGraph(g *Graph, params map[string]string) (*Graph, error)

InjectParamsIntoGraph creates a new graph with variable expansion applied to all node attributes. This is used by the subgraph handler to inject params before execution.

func LoadDippinWorkflow

func LoadDippinWorkflow(source, filename string) (*Graph, []validator.Diagnostic, error)

LoadDippinWorkflow parses a dippin-lang source, then delegates to LoadDippinWorkflowFromIR for validation, lint, and conversion to a Graph. This ensures all .dip entry points apply consistent validation semantics.

filename is used for error messages (e.g., "inline.dip" or "/path/to/file.dip") and as the anchor for file directives: command_file / prompt_file / system_prompt_file paths resolve relative to filepath.Dir(filename), matching the dippin CLI. For synthetic filenames like "inline.dip" the anchor is "." (cwd-relative) — the same semantics as `dippin check inline.dip` run from cwd. A directive resolution failure (missing/unreadable file, path escape) is fatal.

Returns the graph and any validation/lint diagnostics (warnings only). Validation errors are returned as fatal errors.

func LoadDippinWorkflowFromIR

func LoadDippinWorkflowFromIR(workflow *ir.Workflow, filename string) (*Graph, []validator.Diagnostic, error)

LoadDippinWorkflowFromIR runs dippin's structural validator (DIP001–DIP009) and linter (DIP101–DIP115) on an already-parsed IR workflow, then converts it to tracker's Graph representation and marks it dippin-validated.

Diagnostics returned cover both validate and lint passes; validation errors are fatal, lint warnings are non-fatal. This function exists separately from LoadDippinWorkflow so that callers which already hold a parsed *ir.Workflow (e.g., the .dipx bundle loader, which gets one back from dipx.Open) can reuse the validate/lint/convert tail without re-parsing source.

func NewGraph

func NewGraph(name string) *Graph

NewGraph creates an empty Graph with the given name.

func ParseDOT deprecated

func ParseDOT(dot string) (*Graph, error)

ParseDOT parses a DOT-format string into a Graph. Returns an error if the DOT syntax is invalid or the input is empty.

Deprecated: Use .dip format with FromDippinIR instead. DOT support will be removed in v1.0.

func (*Graph) AddEdge

func (g *Graph) AddEdge(e *Edge)

AddEdge adds a directed edge to the graph. No referential integrity check is performed; use Validate to enforce that endpoints exist.

func (*Graph) AddNode

func (g *Graph) AddNode(n *Node)

AddNode adds a node to the graph and resolves its handler from its shape. If the node has an Mdiamond shape, it is set as the start node. If the node has an Msquare shape, it is set as the exit node. Duplicate node IDs silently replace the previous node; use Validate to enforce uniqueness.

func (*Graph) IncomingEdges

func (g *Graph) IncomingEdges(nodeID string) []*Edge

IncomingEdges returns all edges terminating at the given node ID. Returns a copy to prevent callers from mutating internal state.

func (*Graph) OutgoingEdges

func (g *Graph) OutgoingEdges(nodeID string) []*Edge

OutgoingEdges returns all edges originating from the given node ID. Returns a copy to prevent callers from mutating internal state.

func (*Graph) RequiredDeps

func (g *Graph) RequiredDeps() []string

RequiredDeps returns the parsed comma-separated list from g.Attrs["requires"]. Whitespace around each entry is trimmed; empty entries are dropped; duplicates are removed in declaration order. Returns nil for empty/missing attrs.

The "requires" attr is populated by the dippin adapter from the workflow header's `requires:` field (dippin-lang v0.26.0+). The adapter's extractRequires also deduplicates, so RequiredDeps is defensive: a caller that synthesizes a Graph directly (or reads pre-v0.29.0 attrs) still gets a clean list. pipeline.Preflight consumes this list and emits one warning per unrecognized dep — without dedup, duplicates would surface duplicate warnings.

type Handler

type Handler interface {
	Name() string
	Execute(ctx context.Context, node *Node, pctx *PipelineContext) (Outcome, error)
}

Handler defines the interface for pipeline node execution. Each handler has a unique name and an Execute method that processes a node within a pipeline context.

type HandlerRegistry

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

HandlerRegistry stores named handlers and dispatches execution to the appropriate handler based on a node's Handler field.

func NewHandlerRegistry

func NewHandlerRegistry() *HandlerRegistry

NewHandlerRegistry creates an empty handler registry.

func (*HandlerRegistry) Execute

func (r *HandlerRegistry) Execute(ctx context.Context, node *Node, pctx *PipelineContext) (Outcome, error)

Execute looks up the handler for the given node and delegates execution to it. Returns an error if no handler is registered for the node's Handler field.

func (*HandlerRegistry) Get

func (r *HandlerRegistry) Get(name string) Handler

Get returns the handler registered under the given name, or nil if not found.

func (*HandlerRegistry) Has

func (r *HandlerRegistry) Has(name string) bool

Has reports whether a handler with the given name is registered.

func (*HandlerRegistry) Register

func (r *HandlerRegistry) Register(h Handler)

Register adds a handler to the registry, keyed by its Name(). If a handler with the same name already exists, it is overwritten.

type HumanNodeConfig

type HumanNodeConfig struct {
	Mode          string // "" or "choice" (default — presents outgoing edge labels), "yes_no", "interview", "freeform"
	DefaultChoice string // "default_choice" attr, falling back to "default"
	Prompt        string
	QuestionsKey  string
	AnswersKey    string
	Timeout       time.Duration
	TimeoutAction string // "fail", "default", or "" (unset — treated as "default")
	// Writes carries the raw "writes:" attr; consumers that need the parsed
	// key list should still call pipeline.ParseDeclaredKeys.
	Writes string
}

HumanNodeConfig is a typed view over a wait.human node's attributes. DefaultChoice resolves "default_choice" with fallback to "default" so callers don't have to check two keys.

type JSONLEventHandler

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

JSONLEventHandler appends every pipeline event as a JSON line to a file. The runtime writes to an integrity-protected path outside any directory a tool subprocess sees as cmd.Dir (see SecureActivityLogPath); every line is prefixed with ActivityLogSentinel so post-hoc readers can flag injection. At Close() a sentinel-stripped snapshot is copied to the legacy path under artifactDir so bundle export (#213) and any pre-existing tooling that reads <runDir>/activity.jsonl still works.

artifactDir is retained on the handler solely as the destination for that snapshot — live writes during the run never go to artifactDir. Safe for concurrent use from multiple goroutines.

func NewJSONLEventHandler

func NewJSONLEventHandler(artifactDir string) *JSONLEventHandler

NewJSONLEventHandler creates a JSONL event logger. The live log lands at the SecureActivityLogPath for the run's runID; on Close a stripped snapshot is written to <artifactDir>/<runID>/activity.jsonl. The file is opened lazily on first event so callers that never feed events produce no on-disk footprint.

func (*JSONLEventHandler) Close

func (h *JSONLEventHandler) Close() error

Close flushes the secure activity log, writes a sentinel-stripped snapshot to <artifactDir>/<runID>/activity.jsonl for bundle/export consumers, and closes the underlying file. The snapshot is best-effort: snapshot errors don't break Close (the secure file is the authoritative record) but they're stashed on h.snapshotErr so callers that care about bundle/export coverage can inspect them via SnapshotErr().

func (*JSONLEventHandler) HandlePipelineEvent

func (h *JSONLEventHandler) HandlePipelineEvent(evt PipelineEvent)

HandlePipelineEvent implements PipelineEventHandler.

func (*JSONLEventHandler) SetBundleIdentity

func (h *JSONLEventHandler) SetBundleIdentity(id string)

SetBundleIdentity sets the .dipx bundle identity ("sha256:<hex>") that will be stamped onto subsequent WriteAgentEvent and WriteLLMEvent writes. Empty (the default) is a no-op so plain .dip runs see no change. Called once at run-start after the handler is constructed.

Note: events that flow through HandlePipelineEvent already get stamped at the engine and registry levels; this setter only affects the JSONL writes that bypass those chokepoints (agent and llm events).

func (*JSONLEventHandler) SnapshotErr

func (h *JSONLEventHandler) SnapshotErr() error

SnapshotErr returns the error (if any) from the most recent Close-time snapshot mirror to the legacy run-dir path. Callers that depend on the legacy snapshot for bundle export or external tooling can check this after Close. Nil when the snapshot succeeded or was skipped (no artifactDir / no events). The secure file remains authoritative regardless of this value.

func (*JSONLEventHandler) WriteAgentEvent

func (h *JSONLEventHandler) WriteAgentEvent(evtType, nodeID, toolName, toolOutput, toolError, text, errMsg, provider, model string)

WriteAgentEvent logs an agent event to the activity log. The caller is responsible for passing the event; the handler writes it to the same JSONL file as pipeline events. The nodeID identifies which pipeline node (branch) produced this event.

func (*JSONLEventHandler) WriteBundleMismatchForced

func (h *JSONLEventHandler) WriteBundleMismatchForced(runID, originalIdentity, currentIdentity string)

WriteBundleMismatchForced records a forced bundle-identity override on resume. Called once at run-start (before the engine fires any pipeline events) when --force-bundle-mismatch allowed resume despite a mismatch between the checkpoint's stored identity and the current bundle's identity. Both identities are preserved in the log entry so post-hoc auditors can see what was overridden.

The entry's bundle_identity field is stamped with the CURRENT identity (what the run actually executes against), so post-hoc scans grouping activity.jsonl lines by bundle see this override clustered with the rest of the run.

runID is needed to open the activity log file lazily — this is the first event the handler ever writes, so the file isn't open yet (HandlePipelineEvent's lazy openFile hasn't run). Pass the resume run ID here.

func (*JSONLEventHandler) WriteLLMEvent

func (h *JSONLEventHandler) WriteLLMEvent(kind, provider, model, toolName, preview string)

WriteLLMEvent logs an LLM trace event to the activity log.

type LoggingEventHandler

type LoggingEventHandler struct {
	Writer io.Writer
	// contains filtered or unexported fields
}

LoggingEventHandler writes pipeline events to a Writer in a human-readable format. Batches rapid-fire "previously completed" events from checkpoint resume into a single summary line so the console isn't flooded on resume.

func (*LoggingEventHandler) HandlePipelineEvent

func (h *LoggingEventHandler) HandlePipelineEvent(evt PipelineEvent)

HandlePipelineEvent formats and writes a pipeline event to the configured Writer.

type MCPServerConfig

type MCPServerConfig struct {
	Name    string
	Command string
	Args    []string
}

MCPServerConfig defines an MCP server to attach to a session.

func ParseMCPServers

func ParseMCPServers(raw string) ([]MCPServerConfig, error)

ParseMCPServers parses the mcp_servers attr format: one server per line, name=command arg1 arg2. Splits on first = only.

type MarkerDetail

type MarkerDetail struct {
	Pattern      string `json:"pattern"`
	CapturedTail string `json:"captured_tail,omitempty"`
	Error        string `json:"error,omitempty"`
}

MarkerDetail is the payload for EventToolMarkerMissing. Pattern is the raw regex declared on the node's marker_grep attribute; CapturedTail is up to 256 bytes from the end of the captured stdout for diagnostic context (the operator needs to see what didn't match without the audit log carrying arbitrarily large tool output). Error is non-empty when the failure was a regex-compile error rather than a missing-match — the node fails either way, but the surface in `tracker diagnose` differs (broken pipeline definition vs. unexpected tool output).

type Node

type Node struct {
	ID      string
	Shape   string
	Label   string
	Attrs   map[string]string
	Handler string
}

Node represents a single step in the pipeline.

func (*Node) AgentConfig

func (n *Node) AgentConfig(graphAttrs map[string]string) AgentNodeConfig

AgentConfig returns the typed agent config for the node, merging graphAttrs defaults with node.Attrs overrides. Graph-level values apply to all agent nodes unless a node explicitly overrides the same attr. Unparseable numeric strings fall back to the zero value (matching the previous permissive behavior of the handler apply* methods).

func (*Node) HumanConfig

func (n *Node) HumanConfig() HumanNodeConfig

HumanConfig returns the typed human-gate config for the node.

func (*Node) ParallelConfig

func (n *Node) ParallelConfig() ParallelNodeConfig

ParallelConfig returns the typed parallel/fan-in config for the node.

func (*Node) RetryConfig

func (n *Node) RetryConfig(graphAttrs map[string]string) RetryConfig

RetryConfig returns the typed retry config parsed from node and graph attributes. Node-level values win over graph-level defaults for each field independently. Unparseable node values fall through to the graph default rather than silently dropping the whole field — matching the previous cascading behavior in Engine.maxRetries.

func (*Node) ToolConfig

func (n *Node) ToolConfig() ToolNodeConfig

ToolConfig returns the typed tool config for the node.

type Outcome

type Outcome struct {
	Status             string
	ContextUpdates     map[string]string
	PreferredLabel     string
	SuggestedNextNodes []string
	Stats              *SessionStats // optional, populated by codergen handler
	// ChildUsage is the aggregated usage of a child run that executed under
	// this node (subgraph, manager_loop). When non-nil, Trace.AggregateUsage
	// folds it into totals and per-provider rollups so the parent trace
	// reflects spend that happened inside the child. Required for
	// BudgetGuard enforcement to see child spend once control returns to
	// the parent.
	ChildUsage *UsageSummary
	// Truncations records output streams that exceeded their per-stream
	// cap during this node's execution. The engine emits one
	// EventToolOutputTruncated per entry so `tracker diagnose` and the
	// audit log can correlate routing misses with truncation (issue #208).
	// Currently populated only by the tool handler.
	Truncations []TruncationDetail
	// MissingMarker records a marker_grep regex that produced no match
	// (issue #210). When non-nil the engine emits EventToolMarkerMissing
	// before the engine's normal post-node accounting; the handler also
	// sets Status = OutcomeFail so routing does not silently fall through
	// to an unconditional edge. Populated only by the tool handler.
	MissingMarker *MarkerDetail
	// MissingRoute records the absence of a _TRACKER_ROUTE= sentinel
	// in captured stdout when the node had route_required: true
	// (#212). Same flow as MissingMarker: handler sets Status = Fail,
	// engine emits EventToolRouteMissing. Sentinel extraction itself
	// runs unconditionally; this field is populated only when the
	// missing-sentinel + route_required combination triggers a fail.
	MissingRoute *RouteDetail
	// OverrideActor is the Actor classification of the interviewer that produced
	// this outcome. Populated by HumanHandler from its bound interviewer's
	// Actor() method (via actorOf helper). The engine reads this at edge-selection
	// time when an override edge is traversed, to populate OverrideDetail.Actor.
	// Empty for non-wait.human handlers (they cannot originate override edges).
	OverrideActor Actor
	// ChildOverride carries OverrideDetail entries propagated up from a child
	// run (subgraph, manager_loop, or parallel branch with a subgraph child).
	// The engine's applyOutcome path appends these to the parent's
	// runState.validationOverrides after the handler returns.
	ChildOverride []OverrideDetail
}

Outcome represents the result of executing a handler on a pipeline node.

type OverrideDetail

type OverrideDetail struct {
	// GateNodeID is the source node of the override edge (the wait.human gate).
	GateNodeID string `json:"gate_node_id"`

	// Label is the edge label of the override edge ("accept", "mark done", etc.).
	// Empty when the override edge has no label.
	Label string `json:"label,omitempty"`

	// Actor identifies who took the override edge.
	Actor Actor `json:"actor"`

	// SubgraphPath is populated when this override was propagated from a child
	// run via Outcome.ChildOverride. Outermost-to-innermost subgraph node IDs;
	// the leaf gate node ID lives in GateNodeID, not in SubgraphPath. Empty for
	// overrides taken in the run's own graph.
	SubgraphPath []string `json:"subgraph_path,omitempty"`

	// Timestamp is the moment the override edge was traversed. In the JSONL wire
	// format, the enclosing event line carries its own timestamp; this field is
	// primarily for Checkpoint persistence where there is no enclosing timestamp.
	Timestamp time.Time `json:"timestamp"`
}

OverrideDetail describes a single validation-override event: the gate that produced it, the label that selected the override edge, who acted, and the subgraph path (if propagated from a child run). Persisted on Checkpoint.ValidationOverrides and EngineResult.ValidationOverrides; emitted on PipelineEvent.Override when an override edge is traversed.

func PrependSubgraphPath

func PrependSubgraphPath(in []OverrideDetail, parentNodeID string) []OverrideDetail

PrependSubgraphPath returns a copy of in with parentNodeID prepended to each entry's SubgraphPath. Used by subgraph and manager_loop handlers to lift child ValidationOverrides into parent-visible OverrideDetails with outermost- to-innermost ordering: at depth N each level prepends its own subgraph node ID, so by the time control returns to the outermost run the path enumerates the nesting chain top-down (leaf gate node ID stays on GateNodeID, never in SubgraphPath).

The input is not mutated. Each output entry has a fresh SubgraphPath slice so callers may safely retain or mutate either side independently. Returns nil for nil/empty input so callers can rely on `if len(out) > 0` checks downstream.

type ParallelNodeConfig

type ParallelNodeConfig struct {
	ParallelTargets string
	FanInSources    string
	JoinID          string
	MaxConcurrency  int
	BranchTimeout   time.Duration
}

ParallelNodeConfig is a typed view over a parallel node's attributes. ParallelTargets is the comma-separated list of branch target node IDs; the handler still splits and trims. FanInSources mirrors the same for a fan-in node that collects results from multiple upstream branches. JoinID is the fan-in node that branches should reconverge on. MaxConcurrency and BranchTimeout cap concurrent branches and per-branch wall time; zero means unlimited / no timeout.

type PermissionMode

type PermissionMode string

PermissionMode controls Claude Code's tool approval behavior.

const (
	PermissionPlan              PermissionMode = "plan"
	PermissionAcceptEdits       PermissionMode = "acceptEdits"
	PermissionBypassPermissions PermissionMode = "bypassPermissions"
	PermissionDefault           PermissionMode = "default"
	PermissionDontAsk           PermissionMode = "dontAsk"
	PermissionAuto              PermissionMode = "auto"
)

func (PermissionMode) Valid

func (m PermissionMode) Valid() bool

Valid returns true if the permission mode is a recognized Claude Code value. Empty string is not valid — callers should default to PermissionBypassPermissions before validation (see buildClaudeCodeConfig).

type PipelineContext

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

PipelineContext is a thread-safe key-value store shared across all pipeline nodes during execution. It has two namespaces: user-visible values and internal engine bookkeeping (retry counters, loop state).

Per-node scoping: every key written via Set or Merge is recorded in a dirty set. After a node's handler completes, the engine calls ScopeToNode(nodeID) to copy those dirty keys into "node.<nodeID>.<key>" entries. The dirty set is then cleared, ready for the next node. Bare keys continue to be overwritten globally (last-writer-wins), preserving full backward compatibility.

func NewPipelineContext

func NewPipelineContext() *PipelineContext

NewPipelineContext creates an empty pipeline context.

func NewPipelineContextFrom

func NewPipelineContextFrom(values map[string]string) *PipelineContext

NewPipelineContextFrom creates a PipelineContext pre-populated with the given values. Used by the parallel handler to give each branch an isolated snapshot of the shared context.

Preloaded values are written directly without marking them dirty, so the first ScopeToNode call after construction only scopes keys that were written after construction — not the entire baseline snapshot.

func (*PipelineContext) ClearDirty

func (c *PipelineContext) ClearDirty()

ClearDirty resets the dirty set without scoping any keys. Call this after all bootstrap writes (graph attrs, initial context, checkpoint restore) are done and before the main engine loop starts, so that baseline values are not copied into the first node's scoped namespace.

func (*PipelineContext) DiffFrom

func (c *PipelineContext) DiffFrom(baseline map[string]string) map[string]string

DiffFrom returns all keys in the current context whose values differ from the given baseline snapshot, including keys that exist in the context but not in baseline. Keys present in baseline but absent here are not reported (PipelineContext has no delete operation).

func (*PipelineContext) Get

func (c *PipelineContext) Get(key string) (string, bool)

Get retrieves a value from the user-visible context.

func (*PipelineContext) GetInternal

func (c *PipelineContext) GetInternal(key string) (string, bool)

GetInternal retrieves a value from the internal engine namespace.

func (*PipelineContext) GetScoped

func (c *PipelineContext) GetScoped(nodeID, key string) (string, bool)

GetScoped retrieves the value of key from the per-node namespace for nodeID. It is equivalent to Get("node.<nodeID>.<key>") but more readable at call sites. Returns ("", false) if the scoped key has not been written.

func (*PipelineContext) Merge

func (c *PipelineContext) Merge(updates map[string]string)

Merge applies all key-value pairs from updates into the user-visible context and marks each key as dirty so it will be included in the next ScopeToNode call.

func (*PipelineContext) MergeWithoutDirty

func (c *PipelineContext) MergeWithoutDirty(updates map[string]string)

MergeWithoutDirty applies updates to the user-visible context without marking keys as dirty. Used for externally-injected values (e.g. steering channel updates from a supervisor) that should be globally visible but not attributed to any node's scoped namespace.

func (*PipelineContext) ScopeToNode

func (c *PipelineContext) ScopeToNode(nodeID string)

ScopeToNode copies all dirty (recently-written) keys into the per-node namespace "node.<nodeID>.<key>" and then clears the dirty set. This lets downstream nodes read a specific upstream node's output — for example "node.MyAgent.last_response" — without being affected by later writes to the bare "last_response" key. The bare keys are NOT removed; they retain their last-writer-wins global semantics for backward compatibility.

Keys that already start with ContextKeyNodePrefix (e.g. "node.X.foo") are skipped — scoping them would create confusing doubly-nested keys like "node.<id>.node.X.foo". The engine passes the node's graph ID directly.

func (*PipelineContext) Set

func (c *PipelineContext) Set(key, value string)

Set stores a value in the user-visible context and marks the key as dirty so it will be included in the next ScopeToNode call.

func (*PipelineContext) SetInternal

func (c *PipelineContext) SetInternal(key, value string)

SetInternal stores a value in the internal engine namespace.

func (*PipelineContext) Snapshot

func (c *PipelineContext) Snapshot() map[string]string

Snapshot returns a shallow copy of the user-visible context values.

type PipelineEvent

type PipelineEvent struct {
	Type       PipelineEventType
	Timestamp  time.Time
	RunID      string
	NodeID     string
	Message    string
	Err        error
	Decision   *DecisionDetail   // non-nil for decision audit trail events
	Cost       *CostSnapshot     // non-nil for EventCostUpdated and EventBudgetExceeded events
	Truncation *TruncationDetail // non-nil for EventToolOutputTruncated
	Marker     *MarkerDetail     // non-nil for EventToolMarkerMissing
	Route      *RouteDetail      // non-nil for EventToolRouteMissing
	// Override is non-nil on EventValidationOverridden events. Carries the
	// gate, label, actor, and subgraph_path of the traversed override edge.
	Override *OverrideDetail

	// BundleIdentity is the content-addressed identity of the .dipx bundle
	// the run was started against ("sha256:<hex>"). Empty for runs from a
	// plain .dip file. The engine stamps this on every emitted event so
	// activity.jsonl carries provenance on every line.
	BundleIdentity string
}

PipelineEvent carries data about a single pipeline lifecycle occurrence.

type PipelineEventHandler

type PipelineEventHandler interface {
	HandlePipelineEvent(evt PipelineEvent)
}

PipelineEventHandler receives pipeline events for observability purposes.

var PipelineNoopHandler PipelineEventHandler = pipelineNoopHandler{}

PipelineNoopHandler is a handler that does nothing, useful as a default.

func NodeScopedPipelineHandler

func NodeScopedPipelineHandler(parentNodeID string, inner PipelineEventHandler) PipelineEventHandler

NodeScopedPipelineHandler wraps a PipelineEventHandler and prefixes every event's NodeID with parentNodeID + "/". Child pipeline lifecycle events (started/completed/failed) are filtered out because the parent engine already tracks the subgraph node's lifecycle.

func PipelineMultiHandler

func PipelineMultiHandler(handlers ...PipelineEventHandler) PipelineEventHandler

PipelineMultiHandler fans out each event to every provided handler. Nil handlers in the list are safely skipped.

type PipelineEventHandlerFunc

type PipelineEventHandlerFunc func(evt PipelineEvent)

PipelineEventHandlerFunc is an adapter that lets ordinary functions serve as PipelineEventHandler.

func (PipelineEventHandlerFunc) HandlePipelineEvent

func (f PipelineEventHandlerFunc) HandlePipelineEvent(evt PipelineEvent)

type PipelineEventType

type PipelineEventType string

PipelineEventType identifies the kind of lifecycle event emitted during pipeline execution.

const (
	EventPipelineStarted   PipelineEventType = "pipeline_started"
	EventPipelineCompleted PipelineEventType = "pipeline_completed"
	EventPipelineFailed    PipelineEventType = "pipeline_failed"
	EventStageStarted      PipelineEventType = "stage_started"
	EventStageCompleted    PipelineEventType = "stage_completed"
	EventStageFailed       PipelineEventType = "stage_failed"
	EventStageRetrying     PipelineEventType = "stage_retrying"
	EventCheckpointSaved   PipelineEventType = "checkpoint_saved"
	EventCheckpointFailed  PipelineEventType = "checkpoint_failed"
	EventParallelStarted   PipelineEventType = "parallel_started"
	EventParallelCompleted PipelineEventType = "parallel_completed"
	EventManagerCycleTick  PipelineEventType = "manager_cycle_tick"
	EventLoopRestart       PipelineEventType = "loop_restart"
	EventWarning           PipelineEventType = "warning"
	EventEdgeTiebreaker    PipelineEventType = "edge_tiebreaker"

	// Decision audit trail events — capture decision points for post-run reconstruction.
	EventDecisionEdge      PipelineEventType = "decision_edge"
	EventDecisionCondition PipelineEventType = "decision_condition"
	EventDecisionOutcome   PipelineEventType = "decision_outcome"
	EventDecisionRestart   PipelineEventType = "decision_restart"

	// Cost governance events — emitted after each completed node and when a budget is exceeded.
	EventCostUpdated    PipelineEventType = "cost_updated"
	EventBudgetExceeded PipelineEventType = "budget_exceeded"

	// EventToolOutputTruncated fires after a tool node when one or both of
	// its output streams exceeded the per-stream cap and head bytes were
	// elided. Carries TruncationDetail. Surfaced by `tracker diagnose` so
	// operators can correlate routing mismatches with truncation (issue
	// #208).
	EventToolOutputTruncated PipelineEventType = "tool_output_truncated"

	// EventConditionalFallthrough fires when at least one conditional
	// outgoing edge from a node was evaluated, none matched, and routing
	// fell through to an unconditional fallback. The runtime already
	// emits per-condition results and a final decision_edge event, but
	// this aggregating signal is what `tracker diagnose` correlates with
	// EventToolOutputTruncated to surface "your routing marker may have
	// been dropped" (issue #208). It does NOT fire on intentional
	// unconditional-only routing.
	EventConditionalFallthrough PipelineEventType = "conditional_fallthrough"

	// EventBundleMismatchForced is emitted to activity.jsonl when resume
	// proceeds despite a bundle-identity mismatch because --force-bundle-mismatch
	// was set. Records both the original (checkpoint) and current identities
	// in the entry's Message field for post-hoc audit. Emitted once per run by
	// JSONLEventHandler.WriteBundleMismatchForced before any engine work begins.
	EventBundleMismatchForced PipelineEventType = "bundle_mismatch_forced"

	// EventToolMarkerMissing fires when a tool node declared marker_grep
	// but the regex matched nothing in captured stdout. The node fails
	// with OutcomeFail rather than silently falling through to an
	// unconditional edge — the whole point of marker_grep is to remove
	// the silent-fallback foot-gun the #208 root cause exploited.
	// Carries MarkerDetail with the configured regex pattern (#210).
	EventToolMarkerMissing PipelineEventType = "tool_marker_missing"

	// EventToolRouteMissing fires when a tool node declared
	// route_required: true but no _TRACKER_ROUTE= sentinel line was
	// present in captured stdout (#212). Same shape as
	// EventToolMarkerMissing, different mechanism: route_required is
	// the strictness flag for the convention-based sentinel channel;
	// marker_grep is the strictness flag for the regex-attribute
	// channel. Carries RouteDetail with the captured stdout tail.
	EventToolRouteMissing PipelineEventType = "tool_route_missing"

	// EventValidationOverridden fires when the engine traverses an
	// Edge.Override-marked edge at advanceToNextNode. Carries an
	// OverrideDetail payload via PipelineEvent.Override. Stage-level
	// event (NodeID = gate node), same shape as
	// EventConditionalFallthrough. Rides alongside (not instead of) the
	// EventDecisionEdge event emitted for the same selection — the
	// DecisionEdge carries EdgePriorityOverride on its EdgePriority
	// field so external consumers that only watch DecisionEdge can still
	// identify the traversal as an override.
	EventValidationOverridden PipelineEventType = "validation_overridden"
)

type PreflightConfig

type PreflightConfig struct {
	WorkDir        string                           // absolute path; required
	Requires       []string                         // from graph.Attrs["requires"]
	Policy         GitPreflight                     // resolved from CLI > library > default ""
	AllowInit      bool                             // required when Policy == GitPreflightInit and !InteractiveTTY
	InteractiveTTY bool                             // when true, --git=init may prompt instead of needing --allow-init
	Warner         func(format string, args ...any) // optional; defaults to a no-op
	// PromptYN is used by --git=init in interactive mode. Tests inject a stub.
	// When nil, the default reads from stdin.
	PromptYN func(prompt string) bool
}

PreflightConfig captures everything Preflight needs to make a decision. All fields are inputs only; no I/O happens until Preflight runs.

type ProviderUsage

type ProviderUsage struct {
	InputTokens      int     `json:"input_tokens"`
	OutputTokens     int     `json:"output_tokens"`
	TotalTokens      int     `json:"total_tokens"`
	CostUSD          float64 `json:"cost_usd"`
	ReasoningTokens  int     `json:"reasoning_tokens"`
	CacheReadTokens  int     `json:"cache_read_tokens"`
	CacheWriteTokens int     `json:"cache_write_tokens"`
	SessionCount     int     `json:"session_count"`
	Estimated        bool    `json:"estimated,omitempty"`
}

ProviderUsage is the per-provider rollup embedded in UsageSummary. Estimated is true when at least one contributing session reported heuristic-derived usage (e.g. the ACP rune-count estimator). A mixed bucket — some sessions metered, some estimated — still flags Estimated so the operator knows the total is not fully trustworthy.

type RegistryFactory

type RegistryFactory func(graph *Graph, parentNodeID string) *HandlerRegistry

RegistryFactory creates a child HandlerRegistry with event handlers scoped to the given parentNodeID. This allows subgraph handlers to create child registries where agent events are prefixed with the parent node's ID, without the pipeline package needing to import the agent package.

type RetryConfig

type RetryConfig struct {
	PolicyName    string // "" means "use graph default or 'standard'"
	MaxRetries    int    // 0 and MaxRetriesSet=false means "unset"
	MaxRetriesSet bool
	BaseDelay     time.Duration
	BaseDelaySet  bool
}

RetryConfig is a typed view over the retry-related attributes shared across handlers and the engine's retry logic. It is a lightweight companion to RetryPolicy: RetryConfig carries the *raw configured values* (which may be absent), while ResolveRetryPolicy folds them into a concrete *RetryPolicy with defaults. Use RetryConfig when you need to distinguish "unset" from "set to zero"; use ResolveRetryPolicy when you need the effective policy to run with.

type RetryPolicy

type RetryPolicy struct {
	Name       string
	MaxRetries int
	BaseDelay  time.Duration
	BackoffFn  func(attempt int, base time.Duration) time.Duration
}

RetryPolicy defines a named retry strategy with configurable attempt count and backoff.

func ParseRetryPolicy

func ParseRetryPolicy(name string) (*RetryPolicy, bool)

ParseRetryPolicy returns the named policy and true, or nil and false for unknown names.

func ResolveRetryPolicy

func ResolveRetryPolicy(node *Node, graphAttrs map[string]string) *RetryPolicy

ResolveRetryPolicy determines the retry policy for a node by checking: 1. Node attr "retry_policy" 2. Graph attr "default_retry_policy" 3. Falls back to "standard" The resolved policy's MaxRetries is then overridden by node attr "max_retries" or graph attr "default_max_retry" if either is set.

type RouteDetail

type RouteDetail struct {
	CapturedTail string `json:"captured_tail,omitempty"`
}

RouteDetail is the payload for EventToolRouteMissing (#212). The matcher itself is built-in (^\s*_TRACKER_ROUTE=(.+?)\s*$) so there is no Pattern field — just the captured stdout tail for diagnosis.

type SessionStats

type SessionStats struct {
	Turns            int            `json:"turns"`
	ToolCalls        map[string]int `json:"tool_calls,omitempty"`
	TotalToolCalls   int            `json:"total_tool_calls"`
	FilesModified    []string       `json:"files_modified,omitempty"`
	FilesCreated     []string       `json:"files_created,omitempty"`
	Compactions      int            `json:"compactions"`
	LongestTurn      time.Duration  `json:"longest_turn"`
	CacheHits        int            `json:"cache_hits"`
	CacheMisses      int            `json:"cache_misses"`
	InputTokens      int            `json:"input_tokens"`
	OutputTokens     int            `json:"output_tokens"`
	TotalTokens      int            `json:"total_tokens"`
	CostUSD          float64        `json:"cost_usd"`
	ReasoningTokens  int            `json:"reasoning_tokens"`
	CacheReadTokens  int            `json:"cache_read_tokens"`
	CacheWriteTokens int            `json:"cache_write_tokens"`
	Provider         string         `json:"provider,omitempty"`
	// Estimated is true when the token/cost numbers come from a heuristic
	// rather than metered usage (e.g. the ACP backend's rune-count
	// estimator). Downstream consumers — CLI summary, TUI header, tracker
	// diagnose — use this to mark provider rollups with an "(estimated)"
	// suffix so operators don't confuse approximate spend with metered
	// spend. EstimateSource names the heuristic (e.g. "acp-chars-heuristic")
	// and is reserved for future per-source reporting. Derived in
	// buildSessionStats from llm.Usage.Raw.
	Estimated      bool   `json:"estimated,omitempty"`
	EstimateSource string `json:"estimate_source,omitempty"`
	// BreachVerify is the verify-on-breach result (#303): 0=not-run, 1=passed,
	// 2=failed. Mirrors agent.BreachVerifyState as an int so the trace JSON is
	// self-contained. Non-zero only on a turn-limit breach under guard policy.
	BreachVerify int `json:"breach_verify,omitempty"`
}

SessionStats captures agent session metrics for a pipeline node. Only populated for codergen (LLM agent) nodes.

type StyleRule

type StyleRule struct {
	Selector   string
	Properties map[string]string
}

StyleRule represents a single CSS-like rule with a selector and property map.

type Stylesheet

type Stylesheet struct {
	Rules []StyleRule
}

Stylesheet holds an ordered list of style rules parsed from a CSS-like input.

func ParseStylesheet

func ParseStylesheet(input string) (*Stylesheet, error)

ParseStylesheet parses a CSS-like stylesheet string into a Stylesheet. Each rule has the form: selector { key: value; key: value; }

func (*Stylesheet) Resolve

func (ss *Stylesheet) Resolve(node *Node) map[string]string

Resolve applies the stylesheet to a node and returns the final resolved property map. Rules are applied in specificity order (low to high), so higher-specificity selectors override lower ones. Explicit node attributes override all stylesheet rules.

type SubgraphHandler

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

SubgraphHandler executes a named sub-pipeline inline as a single handler step. It looks up the referenced graph by the node's "subgraph_ref" attribute and runs it with the parent's context values as initial context.

func NewSubgraphHandler

func NewSubgraphHandler(
	graphs map[string]*Graph,
	registry *HandlerRegistry,
	pipelineEvents PipelineEventHandler,
	factory RegistryFactory,
) *SubgraphHandler

NewSubgraphHandler creates a handler that can execute any of the provided named graphs. The pipelineEvents handler receives scoped events from child engine execution. The registryFactory creates child registries with scoped agent event handlers.

func (*SubgraphHandler) Execute

func (h *SubgraphHandler) Execute(ctx context.Context, node *Node, pctx *PipelineContext) (Outcome, error)

Execute runs the referenced sub-pipeline and maps its result to an Outcome. If the subgraph node has params, they are injected into the child graph before execution. Pipeline and agent events from the child engine are scoped with the parent node ID so the TUI can distinguish subgraph nodes from parent nodes.

func (*SubgraphHandler) Name

func (h *SubgraphHandler) Name() string

Name returns the handler name used for registry lookup.

type TerminalStatus

type TerminalStatus string

TerminalStatus is the run-level terminal status carried on EngineResult.Status, tracker.Result.Status, tracker.AuditReport.Status, and tracker.RunSummary.Status.

The known values are:

  • OutcomeSuccess "success"
  • OutcomeFail "fail"
  • OutcomeBudgetExceeded "budget_exceeded"
  • OutcomeValidationOverridden "validation_overridden"

The enum is open — future minor releases may add new values. Consumers should use IsSuccess() to classify rather than switching on the raw string.

const (
	OutcomeSuccess TerminalStatus = "success"
	OutcomeRetry   TerminalStatus = "retry"
	OutcomeFail    TerminalStatus = "fail"
)
const OutcomeBudgetExceeded TerminalStatus = "budget_exceeded"

OutcomeBudgetExceeded signals that a BudgetGuard halted the run.

const OutcomeValidationOverridden TerminalStatus = "validation_overridden"

OutcomeValidationOverridden signals that the run reached the success exit after traversing at least one Edge.Override == true edge. Engine-terminal-only: handlers never return this value; the engine writes it post-loop based on the runState.validationOverrides slice. See docs/superpowers/specs/2026-05-29-validation-overridden-design.md.

func (TerminalStatus) IsSuccess

func (s TerminalStatus) IsSuccess() bool

IsSuccess reports whether the terminal status represents a run that completed without failure. Currently true for {success, validation_overridden}. Any unrecognized value returns false (fail-closed).

type ToolNodeConfig

type ToolNodeConfig struct {
	Command     string
	OutputLimit int // bytes; 0 means use default
	WorkingDir  string
	PassEnv     string        // comma-separated env var names to pass through
	Timeout     time.Duration // raw parsed timeout from node attrs; zero means the attr was absent, unparseable, or parsed to 0. ToolHandler.parseTimeout rejects non-positive values at execution time.
	MarkerGrep  string        // regex applied to captured stdout to extract a routing marker into ctx.tool_marker (issue #210). Empty disables. If non-empty and no match, the node fails with OutcomeFail and an EventToolMarkerMissing audit event is emitted.
	// RouteRequired is true when the node MUST receive a _TRACKER_ROUTE=
	// sentinel line in its captured stdout (issue #212). Sentinel
	// extraction itself runs unconditionally; this flag controls whether
	// the absence of a match fails the node. When true, no match →
	// OutcomeFail + EventToolRouteMissing. Symmetric to marker_grep's
	// failure path, but the matcher is built-in (no per-node regex).
	RouteRequired bool
}

ToolNodeConfig is a typed view over a tool node's attributes. Tool nodes execute shell commands and surface stdout/stderr to the pipeline context.

type Trace

type Trace struct {
	RunID     string       `json:"run_id"`
	Entries   []TraceEntry `json:"entries"`
	StartTime time.Time    `json:"start_time"`
	EndTime   time.Time    `json:"end_time"`
}

Trace captures the full execution history of a pipeline run.

func (*Trace) AddEntry

func (tr *Trace) AddEntry(entry TraceEntry)

AddEntry appends a trace entry to the trace log.

func (*Trace) AggregateToolCalls

func (t *Trace) AggregateToolCalls() map[string]int

AggregateToolCalls sums tool call counts from all trace entries with session stats.

func (*Trace) AggregateUsage

func (tr *Trace) AggregateUsage() *UsageSummary

AggregateUsage sums token usage and cost from all trace entries with session stats and any child-run rollups. Child usage is folded in so that subgraph/manager_loop spend is visible in the parent's budget snapshots and CLI summaries rather than disappearing into the child engine's own trace. Without this fold, BudgetGuard evaluating on the parent's trace could never see child spend and --max-tokens / --max-cost would be silently non-binding for any node that runs a child pipeline.

func (*Trace) Summary

func (tr *Trace) Summary() string

Summary returns a human-readable summary of the trace.

type TraceEntry

type TraceEntry struct {
	Timestamp   time.Time     `json:"timestamp"`
	NodeID      string        `json:"node_id"`
	HandlerName string        `json:"handler_name"`
	Status      string        `json:"status"`
	Duration    time.Duration `json:"duration"`
	EdgeTo      string        `json:"edge_to,omitempty"`
	Error       string        `json:"error,omitempty"`
	Stats       *SessionStats `json:"stats,omitempty"`
	// ChildUsage is the aggregated usage of a child run that executed under
	// this node (subgraph, manager_loop). Populated when the handler's
	// Outcome carries child-run totals so AggregateUsage can include them
	// in the parent's rollup. Omitted from JSON when nil.
	ChildUsage *UsageSummary `json:"child_usage,omitempty"`
	// WIPRef is the recoverable git ref (tag tracker/wip/<runID>/<nodeID>)
	// where this node's uncommitted work was preserved before the engine
	// routed away from a failure/exhaustion (#302). Empty when the tree was
	// clean or no git artifact adapter was configured.
	WIPRef string `json:"wip_ref,omitempty"`
}

TraceEntry records the execution of a single pipeline node.

type TruncationDetail

type TruncationDetail struct {
	Stream        string `json:"stream"`         // "stdout" or "stderr"
	Limit         int    `json:"limit"`          // per-stream cap in effect
	CapturedBytes int    `json:"captured_bytes"` // bytes preserved in the captured tail
	DroppedBytes  int    `json:"dropped_bytes"`  // bytes elided from the head
	TotalBytes    int    `json:"total_bytes"`    // CapturedBytes + DroppedBytes for convenience
}

TruncationDetail carries structured data about a tool-output truncation event. Attached to PipelineEvent via the Truncation field when a tool stream's tail-window cap was hit. Issue #208.

type UsageSummary

type UsageSummary struct {
	TotalInputTokens      int                      `json:"total_input_tokens"`
	TotalOutputTokens     int                      `json:"total_output_tokens"`
	TotalTokens           int                      `json:"total_tokens"`
	TotalCostUSD          float64                  `json:"total_cost_usd"`
	TotalReasoningTokens  int                      `json:"total_reasoning_tokens"`
	TotalCacheReadTokens  int                      `json:"total_cache_read_tokens"`
	TotalCacheWriteTokens int                      `json:"total_cache_write_tokens"`
	SessionCount          int                      `json:"session_count"`
	ProviderTotals        map[string]ProviderUsage `json:"provider_totals,omitempty"`
	Estimated             bool                     `json:"estimated,omitempty"`
}

UsageSummary aggregates token usage and cost across all pipeline nodes. Estimated is true when any contributing session was heuristic-derived. CLI / TUI / diagnose surfaces render the run total with an "~" or "(estimated)" marker when this is set, so operators don't interpret mixed metered+estimated totals as fully metered.

type ValidationError

type ValidationError struct {
	Errors   []string
	Warnings []string
}

ValidationError collects multiple validation failures and warnings into one error.

func ValidateAll

func ValidateAll(g *Graph) *ValidationError

ValidateAll checks a parsed Graph and returns a ValidationError containing both errors and warnings. Returns nil only if neither exists.

func ValidateAllWithLint

func ValidateAllWithLint(g *Graph, registry *HandlerRegistry) *ValidationError

ValidateAllWithLint checks a parsed Graph for structural and semantic issues, including Dippin lint warnings. Returns a ValidationError with both errors and warnings.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Directories

Path Synopsis
ABOUTME: LLM-backed autopilot interviewer that replaces human gates with automated decisions.
ABOUTME: LLM-backed autopilot interviewer that replaces human gates with automated decisions.

Jump to

Keyboard shortcuts

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