Skip to content

Target Packs: Local-First Packaging for Target-Aware Capabilities

Context

Trailblaze’s current external-config story is converging on a clear workspace model:

  • trails/ is the workspace anchor
  • trails/config/trailblaze.yaml is the workspace manifest
  • trails/config/ is the artifact directory

That is a good base, but the current top-level config unit is still too narrow. A targets/*.yaml file can describe how to detect an app/site and which tools/toolsets to load, but the real thing we want to share is bigger than that:

  • target detection (gmail.com, Android package name, iOS bundle id)
  • tools and toolsets
  • waypoints
  • short recorded navigation segments between waypoints
  • runnable trails
  • optional JS/TS implementations backing some tools

This matters for both testing and agentic device control. A user may never author a test, but still benefits enormously if Trailblaze can recognize “this is Gmail” and immediately load better tools, stronger selectors, known-good waypoints, and reusable navigation segments.

Decision

Core model

Trailblaze will move toward a target-pack model.

  • A target remains the runtime concept: the thing Trailblaze is acting against.
  • A target pack is the reusable, composable, distributable unit.

Examples:

  • gmail
  • wikipedia
  • android-settings
  • consumer-payment-app
  • merchant-point-of-sale

Each target pack is self-describing and target-aware. It already knows how to recognize itself on supported platforms and what capabilities it exports once active.

One pack may span multiple platforms

A target pack may support:

  • web
  • android
  • ios

or only one of them.

The pack is the published/local dependency unit; the runtime resolves a more specific target variant such as gmail:web or gmail:android.

Packs replace standalone authored targets/*.yaml

The current standalone target YAML shape is too small to be the long-term top-level unit. Going forward, the authored unit should become pack.yaml, not a sibling targets/foo.yaml.

In other words:

  • today’s target.yaml content becomes part of a pack manifest
  • tomorrow’s pack.yaml owns target detection plus all exported artifacts

This keeps “what is this target?” and “what does this reusable bundle provide?” in one place.

Pack ownership is implicit by layout

Artifacts should normally inherit their owning pack from directory layout rather than repeating that ownership in every file.

For example:

trails/config/
  packs/
    gmail/
      pack.yaml
      tools/
        inputText.yaml
      toolsets/
      waypoints/
      routes/
      trails/

In this shape, tools/inputText.yaml implicitly belongs to the gmail pack because it lives under that pack’s root.

This matches how package ownership works in other ecosystems:

  • Python modules inherit package ownership from import path and directory structure
  • TypeScript modules inherit package ownership from the package manifest and file location

The system should still be able to represent ownership explicitly in generated or resolved output, but authored pack content should not require repetitive pack: gmail declarations in each file.

Waypoints are first-class

Waypoints are not a sidecar to trails. They are a core primitive alongside tools.

The model becomes:

  • target — what this thing is
  • tools — atomic capabilities
  • toolsets — grouped capabilities
  • waypoints — named, assertable locations/states
  • routes or segments — deterministic movement between waypoints
  • trails — runnable authored flows and validations

Trails become one consumer of this structure rather than the only organizing surface.

Declarative metadata, dynamic implementation

Pack discovery and composition must stay cheap.

That means:

  • YAML is the source of truth for discovery and metadata
  • JS/TS is the source of truth for behavior

Trailblaze must not need to execute TypeScript just to discover what tools a pack contains.

Tool metadata should be declared in YAML. JS/TS implementations are referenced by that metadata and executed lazily at runtime. Optional generators may help authors keep YAML and TS in sync, but runtime discovery should remain declarative.

Resolve is automatic; build is lazy

Users should not be required to run a separate resolve command during normal use.

Instead:

  • pack resolution is automatic
  • resolution is cheap
  • expensive JS/TS build work is lazy and cached

The split is:

  • resolve: load manifests, merge dependencies, compute active capabilities, validate ids, materialize normalized metadata
  • build: only when a JS/TS implementation is actually needed and no usable runtime artifact is already present

Prebuilt JS remains the preferred runtime artifact when available. Source TS is a convenient authoring surface, not a requirement for runtime startup.

Resolve produces one flattened runtime tree

Runtime should not operate directly on an unresolved forest of packs.

Instead, authored inputs are compiled into one resolved output tree, similar to how a JVM build turns many source files and dependencies into one set of generated classes or one packaged JAR.

That means:

  • source packs can be modular and layered
  • meta-packs can depend on other packs
  • runtime sees one clean, collision-checked output tree

Conceptually:

workspace/
├── trails/
│   ├── config/
│   │   ├── trailblaze.yaml
│   │   └── packs/
│   │       ├── consumer-payment-app/
│   │       ├── merchant-point-of-sale/
│   │       ├── dashboard-admin-app/
│   │       └── company-suite/
│   └── ... trail files ...
└── .trailblaze/
    └── resolved/
        ├── manifest.yaml
        ├── targets/
        ├── tools/
        ├── toolsets/
        ├── waypoints/
        ├── routes/
        ├── trails/
        └── mcp/

The resolved tree is the runtime source of truth. Duplicate ids and ambiguous ownership should be handled during resolve, not left for runtime to guess about.

Local-first scope

Trailblaze does not need a registry, remote installer, version solver, or signatures to prove this model.

The first implementation target is purely file-based local packs.

Recommended workspace shape:

your-workspace/
└── trails/
    ├── config/
    │   ├── trailblaze.yaml
    │   └── packs/
    │       ├── gmail/
    │       │   ├── pack.yaml
    │       │   ├── tools/
    │       │   ├── toolsets/
    │       │   ├── waypoints/
    │       │   ├── routes/
    │       │   ├── trails/
    │       │   └── mcp/
    │       └── wikipedia/
    │           └── ...
    └── ... trail files ...

trails/config/trailblaze.yaml then composes the packs the workspace wants to use.

That composition should allow both leaf packs and aggregate meta-packs:

  • consumer-payment-app, merchant-point-of-sale, dashboard-admin-app as app-specific packs
  • company-suite as a meta-pack that depends on those packs and resolves into one flattened output

This mirrors dependency-management systems where published package identity is unique, but local symbol names can remain simple inside a package.

Initial merge semantics should support:

  • use
  • extend
  • replace

Forking should be the escape hatch, not the default extension story.

Relationship to the older tool naming decision

Tool Naming Convention described the flat global tool namespace that Trailblaze still uses today.

That older decision remains historically accurate for the current runtime model, but part of its original rationale is no longer the right long-term constraint:

  • previously, tool naming was tightly coupled to finding the backing Kotlin class during serialization and registration
  • today, class-backed tools are described by YAML and can point to fully-qualified class names directly

So the old document is still relevant as a description of the current flat runtime namespace, but it should no longer be treated as the main design argument against pack ownership or pack-local simple names in the future.

Why this is the right cut

This preserves the value of the current config-alignment work while opening a clean path to a much stronger ecosystem:

  • local workspaces stay simple
  • downstream consumers can dogfood the model by moving app-specific behavior out of the binary
  • open-source examples can ship as real packs
  • future publishing is mostly packaging/distribution work, not a redesign of the runtime model

What lands now vs later

What should land with the current PR

The workspace/config alignment work is worth merging now:

  • trails/ as workspace anchor
  • trails/config/trailblaze.yaml as workspace manifest
  • trails/config/ as filesystem artifact directory
  • shared resolver/constants
  • generated docs describing the current binary behavior

That PR should not wait on the full pack model.

What should be tracked as follow-up work

  1. Introduce local file-based target packs and pack.yaml.
  2. Move standalone target YAML semantics into the pack manifest.
  3. Add workspace composition of packs (use / extend / replace).
  4. Make waypoints and routes/segments first-class pack artifacts.
  5. Shift scripted-tool discovery to declarative YAML metadata with lazy JS/TS execution.
  6. Dogfood the pack model by migrating downstream app-specific behavior out of forked binaries.

Naming

The naming stack locked by this decision:

  • target — runtime identity
  • target pack — reusable/distributable unit
  • waypoint — named app/site location or state
  • route or segment — movement between waypoints
  • trail — runnable authored flow

Deferred

Explicitly deferred until the local model proves itself:

  • remote registry
  • versioned public package distribution
  • trust scoring / signatures
  • semver constraint solving
  • automatic download/install based on target detection

These are packaging and ecosystem problems. The important near-term work is to make the local pack shape, merge rules, and lazy runtime behavior solid.