Slots & bindings
How one way serves teams with different tools, and why the choice is declared instead of guessed.
Your development way says: when the work is ready, open a pull request and wait for review.
Your team is on GitHub. Your client's team is on GitLab. Same process, same phases, same review
discipline — but gh pr create and glab mr create are different commands.
A way that hardcodes gh only serves half of you. So a way never names a tool. It says it needs a
vcs-host, and each repo says which one it has.
One way, two toolchains
The way author writes the skill once, against the capability:
# in the way — no vendor anywhere
spec:
skills:
- path: skills/incu-way-development
uses: [vcs-host, issue-tracker]Each consuming repo fills those slots with what it actually uses. Your repo:
# ways.yaml
ways:
incu/dev:
bindings:
vcs-host: github
issue-tracker: jiraYour client's repo, same way, same pin:
# ways.yaml
ways:
incu/dev:
bindings:
vcs-host: gitlab
issue-tracker: linearOne way, installed from the same source at the same commit, driving two different toolchains. That is the whole point of a slot. Everything below is detail in service of it.
"But my git remote already says GitHub"
This is the first thing most people ask, and it's a fair question. Your environment does describe itself: the remote URL, the lockfiles, the CI config. Why write the binding down at all?
Two reasons.
The author needs the placeholder. Detection could tell your machine that you use GitHub. It
can't help the person writing the way, who has never seen your repo and has to phrase every
instruction without naming a vendor. The slot is what lets open a PR on the vcs-host exist as a
sentence at all. Take the slot away and the author is back to writing one way per toolchain.
A committed choice beats a per-machine guess. Inference happens on one laptop and leaves no
trace. bindings: { vcs-host: github } lives in ways.yaml, gets committed, and is the same fact
for every teammate and for CI. That's what makes doctor able to say the environment is wrong
rather than quietly adapting to whatever it finds.
In the common case this costs you one line, set once, when you first add the way. If the way ships a sensible default you don't even write that — you confirm it.
Binding, in practice
npx ways.sh bind # wizard over every unbound slot
npx ways.sh bind incu/dev vcs-host github # scripted, idempotent
npx ways.sh bind incu/dev # list slots and where they came from
npx ways.sh unbind incu/dev vcs-host # back to the way's default, or unboundYour selection lands in the repo's ways.yaml, which is committable:
ways:
incu/dev:
source: github:incu-tech/incuway
bindings:
vcs-host: githubProject bindings beat the way's defaults. A default is a pre-fill, so your binding always wins and
unbind falls back to it. (Team and user-global tiers are reserved in the contract for later.)
Where slots come from
A way's slot set isn't a hand-maintained list. It's derived from what each skill declares it uses, then optionally enriched:
spec:
skills:
- path: skills/incu-way-development
uses: [vcs-host] # this skill needs a vcs-host, so that IS a slot
needs: # optional: give the derived slot a human face
- id: vcs-host
displayName: Version-control host
description: Hosts the repo and PRs, and is where this way opens and gates them.
contract: ways.dev/vcs-host@1
required: true
options: [github, gitlab, bitbucket]
defaults:
bindings:
vcs-host: githubA slot id that appears only in skills[].uses becomes a slot with sane defaults
(contract: ways.dev/{id}@1, required: true, cardinality: single), so a simple way needs no
needs section at all. A needs[] entry enriches that derived slot: a human name and description,
suggested options for the wizard menu, a softened required, a default binding.
Because slots carry provenance, every surface that shows one — the bind wizard, doctor,
ways list --components — leads with the human name and says why the slot exists (needed by: <skill>, or declared by the way), keeping id and contract as dimmed technical detail.
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
doctorkeeps failing it (exit 1) until it's bound. There is no consumer-side flag to silence it.
Reference: contracts and catalogs
Everything up to here is what you need to use slots. This section is for way authors and for anyone registering in-house implementations.
A SlotContract defines what any vcs-host guarantees. A Binding profile says what satisfying
that contract with github actually requires, which is how doctor knows to check for the gh CLI
and a GH_TOKEN. A Binding's identity is the pair {contract, impl} rather than its filename, since
one impl like snyk can satisfy several contracts.
A slot's contract reference (ways.dev/vcs-host@1) resolves through its namespace, which
points at a catalog: a pinned, acquirable collection of SlotContract and Binding artifacts, shaped
like 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: register an in-house impl as a first-class option
# without forking any way.
contracts:
acme.internal: { source: ../our-catalog }Catalog layout is convention (contracts/, bindings/); resolution is by content, with contracts
keyed by metadata.name@<major> and bindings by {contract, impl}.
Binding profiles resolve across three tiers
ways.yaml → contracts:In-house impls you register for this repo. Highest precedence — no need to fork a way.bindings/ inside the wayWhat the way author shipped. Anything bundled today keeps winning over a shared catalog.spec.contracts namespace sourceThe acquirable, pinned collection a namespace points at (e.g. ways.dev). The default source.Pinned, approved, validated
- Pinned and reproducible. Resolved catalogs land in
ways.lockunder acontractssection (source,ref/sha,digest), andways installreproduces them offline. - Executable surface, so approved. A Binding contributes
requires.cli[].checkcommands thatdoctorruns, which makes a catalog trusted content:ways addlists catalogs in its approval plan and re-asks when a digest changes. Read-only commands (doctor,bind,ways contracts) are cache-only and never fetch, so a catalog only enters through an explicit approval.WAYS_NO_CATALOGS=1disables resolution entirely. - Real validation.
doctorreports the operations a bound impl'scoversOperationsomits (advisory by default, a failure under--strict), and an unresolvable contract asunknownwith the reason.conformancefails a contract reference that doesn't exist in its catalog and names the near-miss. - Inspect what resolved with
ways contracts: the namespaces, the contracts each provides, and the impls available per contract with coverage and provenance.
A bound slot whose Binding profile can't be found reports unknown in doctor: recorded and honest,
but unverifiable. See Doctor & trust.