ways
Concepts

Slots & bindings

How a way stays tool-agnostic — capability slots, Binding profiles, and the project binding map.

A portable way can't say "use GitHub" — that would only serve teams that use GitHub. Instead it declares capability slots: abstract needs the consumer fills with their own tools. This is the mechanism that lets one development way serve a Jira+Sentry+GitHub shop and a Linear+Rollbar+GitLab shop.

The three decoupled layers

LayerArtifactAnswers
Interfacekind: SlotContractwhat does any vcs-host guarantee?
Implementationkind: Bindingto satisfy vcs-host with github, what's required?
Selectionways.yamlbindings:this repo binds vcs-host → github

Where slots come from — the derived model

A way's slot set isn't a hand-maintained list; it's derived from what each skill declares it uses, then optionally enriched. Two sources, unioned by slot id:

spec:
  skills:
    - path: skills/incu-way-development
      uses: [vcs-host]                # this skill needs a vcs-host — that IS a slot
  needs:                              # optional: enrich a derived slot, or declare a way-level one
    - id: vcs-host
      displayName: Version-control host
      description: Hosts the repo and PRs — where this way opens and gates pull requests.
      contract: ways.dev/vcs-host@1   # SlotContract reference, id@major
      required: true
      options: [github, gitlab, bitbucket]   # wizard suggestions
  defaults:
    bindings:
      vcs-host: github                # a pre-fill, never a lock
  • A slot id that appears only in skills[].uses becomes a slot with defaults { contract: ways.dev/{id}@1, required: true, cardinality: single } — a simple way can declare slots without a needs section at all.
  • A needs[] entry enriches or overrides the derived slot for its id: human displayName and description, suggested options, a softened required, a default binding.
  • spec.needs is the current name; capabilitySlots is its deprecated alias — still valid for the lifetime of ways/v1alpha1, but conformance nudges the rename, and declaring both is a schema error.

Because slots carry provenance, every surface — the bind wizard, ways bind, doctor, and ways list --components — leads with the human name and description and says why the slot exists (needed by: <skill> or declared by the way), with the id·contract·required as dimmed technical detail.

A Binding profile maps {contract, impl} → requires{} — it's what doctor expands to know that choosing github means checking the gh CLI and a GH_TOKEN. Its identity is the pair {contract, impl}, not its name (one impl like snyk can satisfy several contracts). A way can ship profiles inside its bundle, or they can be resolved from a shared catalog — see Resolving contracts below.

Binding

npx ways.sh bind                          # wizard: walks unbound + default-resolved slots
npx ways.sh bind incu/dev vcs-host github # scripted, idempotent
npx ways.sh bind incu/dev                 # list slots + provenance
npx ways.sh unbind incu/dev vcs-host      # back to the way's default (or unbound)

Your selection lands in the repo's ways.yaml — the composition root — and is committable, so the whole team and CI validate against the same tools:

ways:
  incu/dev:
    source: github:incu-tech/incuway
    bindings:
      vcs-host: github

Precedence: project > way defaults. A default is a pre-fill, not a lock — your binding always wins, and unbind falls back. (Team profiles and user-global tiers are reserved in the contract for later.)

Resolving contracts — catalogs

A slot's contract (ways.dev/vcs-host@1) used to be a string nobody resolved, and its Binding profile existed only if the way happened to bundle one. As of 0.5.0 a contract's namespace declares where its catalog comes from — an acquirable, pinned collection of SlotContract + Binding artifacts, in the shape of Terraform's required_providers:

# in a way — the ways.dev namespace has a built-in default source, so a way that only uses
# ways.dev/* contracts can omit this block entirely.
spec:
  contracts:
    ways.dev: { source: github:incu-tech/ways-contracts, version: ^1.0.0 }
  needs:
    - { id: sast, contract: ways.dev/sast@1 }
# in a project's ways.yaml — highest precedence: register an in-house impl as a first-class
# option without forking any way.
contracts:
  acme.internal: { source: ../our-catalog }

A catalog holds SlotContract definitions (what vcs-host guarantees) and Binding profiles (what satisfying it with github requires). Layout is convention (contracts/, bindings/); resolution is by content — contracts key by metadata.name@<major>, bindings by {contract, impl}.

Binding profiles resolve across three tiers

Anything a way bundles today keeps winning over a shared catalog, so nothing existing changes; the project catalog is the escape hatch for an in-house impl.

Pinned, approved, validated

  • Pinned & reproducible. Resolved catalogs land in ways.lock under a contracts section (source, ref/sha, digest); ways install reproduces them offline.
  • Executable surface → approved. A Binding contributes requires.cli[].check commands doctor runs, so a catalog is trusted content: ways add lists the catalogs in its approval plan (attributed to catalog + impl) and re-asks when a digest changes. Read-only commands (doctor, bind, ways contracts) are cache-only and never fetch — a catalog only enters through an explicit approval. WAYS_NO_CATALOGS=1 disables resolution entirely.
  • Real validation. doctor now reports the operations a bound impl's coversOperations omits (advisory ⚠ by default, a failure under --strict), and an unresolvable contract as unknown with the reason. conformance fails a contract reference that doesn't exist in its catalog, and names the near-miss (the typo lint).
  • Inspect what resolved with ways contracts: the namespaces (source, pin, digest, origin), the contracts each provides, and the impls available per contract with coverage and provenance.

Required means required

required is the way author's contract, and the consumer can't demote it:

  • You can bind a required slot to any impl — required constrains that it's filled, not with what.
  • The wizard lets you skip a required slot only after an explicit confirmation — and doctor will keep failing it (exit 1) until it's bound. There is no consumer-side flag to silence it.

A bound slot whose Binding profile can't be found reports unknown in doctor — recorded, honest, but unverifiable. See Doctor & trust.

On this page