Authoring a way
way.yaml anatomy — skills, knowledge, requirements, slots, flow — and the rules that make it installable.
A way is a directory with a way.yaml at its root, plus the files it declares. The full JSON
Schemas ship with the package (schemas/ways/v1alpha1/); this page is the working tour.
Anatomy
apiVersion: ways/v1alpha1
kind: Way # or RulePack for standalone rules
metadata:
name: incu/dev # MUST be namespaced ns/name — conformance-enforced
version: 1.0.0
discipline: development
publisher: incu
spec:
skills: # → placed by skills.sh
- path: skills/incu-way-development # bundled (relative, no "..")
uses: [vcs-host] # slot ids this skill uses → derives the way's slots
- source: github:acme/skills/review # or external — resolved by skills.sh
knowledge: # → placed by steering, converted per agent
- id: ts-rules
file: rules/typescript.md
applicability: { mode: fileMatch, globs: ["**/*.ts"] }
- pack: { name: incu/nestjs-rules } # external RulePack
hooks: # → placed by hooks.sh — auto-executing event handlers
- path: hooks/gate-guard # bundled pack (dir with hooks.json)
- source: github:acme/hooks/pr-lint # or external — acquired + pinned
requires:
cli:
- { name: git, required: true, check: "git --version" }
- { name: gh, required: false, check: "gh --version", install: "brew install gh" }
env:
- { name: GH_TOKEN, required: false, secret: true, docsUrl: "https://github.com/settings/tokens" }
mcp:
- { name: io.sentry/sentry, required: false } # server.json vocabulary
contracts: # where each contract NAMESPACE resolves from (a catalog)
ways.dev: { source: github:incu-tech/ways-contracts, version: ^1.0.0 }
# ways.dev has a built-in default — omit this block if you only use ways.dev/* contracts
needs: # slots — enrich the derived ones or declare way-level ones
- id: vcs-host # (capabilitySlots is the deprecated alias of needs)
displayName: Version-control host
description: Hosts the repo and PRs.
contract: ways.dev/vcs-host@1
required: true
options: [github, gitlab] # wizard menu suggestions
defaults:
bindings: { vcs-host: github }
flow: # see Gates & conformance
phases: [...]
gates: [...]
generates: [...]
state: .ways/state.json # one state file per worktree — see State filesRules that bite (learned from real ways)
- Namespace the name.
metadata.namemust bens/name(incu/dev) — it's the collision key for the lock, the cache, and the future registry. Non-namespaced ways are not conformant and won't install. - Ship Binding profiles inside the bundle (e.g.
bindings/github.yaml). Profiles that live outside the way directory don't travel with the install, and bound slots will reportunknowninstead ofpass. - A Binding's identity is
{contract, impl}, not its filename — one impl may satisfy several contracts, so author one profile per contract (snyk-sast.yaml,snyk-sca.yaml). - Bundled paths must exist and stay inside the bundle (no
..) —addpre-flights this and refuses non-self-contained ways. Usesource/packfor third-party content. - Declare
checkcommands for anything that matters.git --version,gh auth status— they run only after the consumer approves your way, and they're what upgrade apassfrom "binary on PATH" to "actually works". - Never put credentials in
bindingConfig. Secrets belong inrequires.env(secret: true) — the bind wizard refuses to prompt secret-shaped config keys, and doctor never echoes values. - At least one hard human gate.
approval: user+enforcement: blocking, or the way isn't conformant — by design. - Let slots derive from
skills[].uses. Declare what each skill uses and the slot appears automatically; add aneeds[]entry only to enrich it (adisplayName/descriptionsobind/doctorread human, suggestedoptions, a softenedrequired). Useneeds, not the deprecatedcapabilitySlotsalias — conformance warns, and declaring both is a schema error. Slot ids must be unique within the declaration. - Point contract namespaces at a catalog. A slot's
contract(acme.internal/scanner@1) must resolve — declare its namespace underspec.contracts(ways.devhas a built-in default), or bundle theSlotContract+Bindingyourself. Conformance fails an unknown reference and names the near-miss. See Resolving contracts.
Hooks
spec.hooks[] declares hook packs — auto-executing event handlers — the same way skills[] declares skills: { path } for a
bundled pack (a directory with a hooks.json + scripts) or { source } for an external one. ways
gives them first-class treatment:
- Full-clarity approval. The install plan expands each pack to its
hook <event>[matcher] → <command>lines under the executable surface — you see exactly what runs on which event before approving. A hook file the manifest doesn't declare is disclosed and markedundeclared — not installed. - Placement →
hooks.sh.adddelegates each pack tohooks.sh(the third installer next toskills.shandsteering; override withWAYS_HOOKS_CMD), with the captured agent selection fanned out. - Pinned + inventoried. External hook sources are
{sha, digest}-pinned in the lock (sibling ofskills);ways installreproduces them and refuses on drift, andways list --componentsshows a⚡N hooksmarker.
spec:
hooks:
- path: hooks/gate-guard # PreToolUse on Bash(git commit*) — blocks commits while a gate is unapprovedAuthoring a family
Shipping more than one flow under one identity? Don't fake a single flow — package them as a
WayFamily: spec.members[] references each member way (each keeps
its own flow), and shared skills/knowledge/needs/requires are declared once at the family
level. A family declares no flow of its own.
Validate while you author
npx ways.sh validate ./my-way/way.yaml # schema (any kind — Way, WayFamily, RulePack, …)
npx ways.sh conformance ./my-way/way.yaml # the rules above
npx ways.sh add ./my-way && npx ways.sh doctor # the full consumer experience, locally