ways

Authoring a way

How to write a way — let an agent draft it, then the rules that make it installable.

A way is a directory with a way.yaml at its root, plus the files it declares. This page is about writing one; the manifest itself — every section, who places or validates it — is documented in The way.yaml.

Let an agent write it

Two authoring skills live in the ways repo, alongside the schema and the conformance rules they document. They do this page's work for you — and they iterate against validate and conformance until both are green, so what you get back is a way that actually installs.

npx skills add incu-tech/ways --skill author-way
npx skills add incu-tech/ways --skill author-way-family
  • author-way — one flow, kind: Way. Works greenfield ("a way for our QA regression process"), but earns its keep as a converter: point it at a folder of SKILL.md files, Cursor rules, or steering docs and it maps them onto spec.skills[] and spec.knowledge[] with the right applicability, infers spec.requires from the tools the process actually names, and asks you about the human gate instead of inventing one.
  • author-way-family — several flows under one identity, kind: WayFamily. Adds what a family changes: which payload is shared versus what stays inside a member, and how member paths anchor.

Both target conformant, not merely schema-valid — the distinction that gates and conformance explains. If you'd rather write the manifest by hand, keep The way.yaml open and mind the rules below.

Rules that bite

  • Namespace the name. metadata.name must be ns/name (incu/dev) — it's the collision key for the lock, the cache, and the 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 report unknown instead of pass.
  • 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 ..) — add pre-flights this and refuses non-self-contained ways. Use source/pack for third-party content.
  • Declare check commands for anything that matters. git --version, gh auth status — they run only after the consumer approves your way, and they're what upgrade a pass from "binary on PATH" to "actually works".
  • Never put credentials in bindingConfig — or anywhere else in the bundle. Secrets belong in requires.env (secret: true): the bind wizard refuses to prompt secret-shaped config keys, and doctor never echoes values. Ways Shield scans every file you ship and refuses the install for a well-known token shape, so a leaked key doesn't reach your consumers — it just means nobody can install your way. For a genuine false positive (quoting a vendor's own published example key in a doc), mark the line shield:allow — or shield:allow:<rule-id> to scope it to one rule, so a real secret two lines down still blocks.
  • Pin what your .mcp.json launches, and keep hook commands single. An npx/dlx server without an exact semver (@latest, @next, a range) and a hook command that chains or substitutes another (;, &, |, `, $(...)) are Shield warnings on every install of your way — visible in the plan, and a hard refusal for any consumer running --strict (which is what CI does).
  • 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 a needs[] entry only to enrich it (a displayName/description so bind/doctor read human, suggested options, a softened required). 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 under spec.contracts (ways.dev has a built-in default), or bundle the SlotContract + Binding yourself. 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 marked undeclared — not installed.
  • Placement → hooks.sh. add delegates each pack to hooks.sh (the third installer next to skills.sh and steering; override with WAYS_HOOKS_CMD), with the captured agent selection fanned out.
  • Pinned + inventoried. External hook sources are {sha, digest}-pinned in the lock (sibling of skills); ways install reproduces them and refuses on drift, and ways list --components shows a ⚡N hooks marker.
spec:
  hooks:
    - path: hooks/gate-guard   # PreToolUse on Bash(git commit*) — blocks commits while a gate is unapproved

Authoring 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

This is the loop the authoring skills run on your behalf — they don't hand back a manifest they haven't put through it.

On this page