Thanks for considering a contribution. Small, focused pull requests land fastest.
This guide covers:
- What's in scope.
- Status labels.
- Change rules per artifact (prose, JSON Schemas, OpenAPI/AsyncAPI, conformance).
- Regenerating derived files and running the gate.
- Process, DCO sign-off and response times.
Other code lives in sibling repositories:
- SDKs:
openwop/openwop-sdks. - Example and reference hosts:
openwop/openwop-examples. - The demo app:
openwop/openwop-app. - Packs and the registry:
openwop/openwop-registry. - The site:
openwop/openwop-site.
Each has its own contribution rules and checks.
What's in scope
The corpus describes the wire contract between independent workflow hosts and the clients that talk to them. It does not prescribe:
- Internal data structures.
- Storage backends.
- How LLM prompts are built (beyond what the capabilities handshake declares).
- UI conventions. The spec defines only the data on the wire.
A proposal that adds to one of those surfaces probably belongs in an implementation's docs, not the spec.
v2 is the current major. Its contract is spec/v2/, schemas/v2/ and api/v2/. v1 reached end of support on 2026-10-04 (RFC 0234). spec/v1/ and the flat schemas/*.schema.json are a frozen tree: don't edit them.
Status labels
Every v2 document carries a status line near its header, for example > Status: Stable..
- Core documents (
spec/v2/core/*.md) areStable. - Extension documents (
spec/v2/ext/) use the labels defined inspec/v2/ext/README.md:Draft,Stable,RetiredandNote. Each label has a mechanical predicate, andscripts/check-ext-status-coherence.mjschecks the labels against committed conformance evidence. A label changes when its predicate is met, not by hand.
Change rules per artifact
Prose specs (spec/v2/**/*.md)
- Every document has a status line (see above).
- Use RFC 2119 keywords (MUST, SHOULD, MAY, MUST NOT, SHOULD NOT) only for genuinely normative requirements.
- Link companion documents by relative path. From the repo root, use
capabilities.md; inside a spec directory, link peers by filename. - The v2 prose is rendered verbatim on openwop.dev. Keep it readable: short paragraphs, bullets instead of MUST-stacked walls, citations on a Sources line.
scripts/check-spec-readability.mjsruns in the gate.
An RFC MUST NOT state a rule a core doc owns
Before you land any normative sentence in RFCS/*.md:
- **Open the
spec/v2/core/*.mdthat owns the rule** and read what it already says. - Grep
conformance/src/scenarios/for a scenario that asserts it.
The gate does not cross-check an RFC's normative claims against the core spec or the suite. The corpus can hold a direct contradiction with every check green. This has happened: an RFC reversed a reader rule owned by persistence.md and events.md, and it was caught only when a host implemented it.
Ownership is the test. When an RFC's argument is sound but the rule is not its to state, record the problem as an open question in the RFC and move the change to the owning document, with its conformance scenarios, in the same PR.
This stays a manual rule because it does not automate. Mechanical proxies (an RFC naming an error code another doc owns; an RFC MUST lacking a scenario) were measured against the corpus: they produce either noise or no signal. The defective RFC above satisfied every structural proxy; the contradiction was semantic.
JSON Schemas (schemas/v2/*.schema.json)
- Every schema declares
$schema: "https://json-schema.org/draft/2020-12/schema". - Every schema has an
$idunderhttps://openwop.dev/spec/v2/<name>.schema.json. (The frozen flat v1 schemas usehttps://openwop.dev/spec/v1/.) - Use
additionalProperties: falseon every object. Explicit field lists are mandatory, even if a runtime is more lenient. - New optional fields are additive. New required fields are breaking (see
COMPATIBILITY.md). Either way, add aCHANGELOG.mdline. - Don't run
scripts/derive-v2-schemas.mjs --write. The v2 schemas were seeded from v1 once and have since been edited by hand; the gate runs it with--checkonly.
Pack-internal schemas (inside a pack's schemas/ directory) follow the rules in openwop/openwop-registry. The pack-manifest schemas themselves stay normative here.
OpenAPI and AsyncAPI
There are two layers:
api/openapi.yamlandapi/asyncapi.yamlare the v1 wire documents. They are also the source thatscripts/derive-v2-api.pyreads.api/v2/openapi.yaml,api/v2/asyncapi.yamlandapi/seams-v2.yamlare derived. The script strips the/v1prefix, moves seam operations toapi/seams-v2.yaml, adds theOpenWOP-Versionheader, renames headers, points$refs intoschemas/v2/, and replaces reader-facing prose fromscripts/derive-v2-api-prose.yaml. Its header lists every rule.
So, to change the v2 API:
- Edit the source (
api/openapi.yaml/api/asyncapi.yaml), orscripts/derive-v2-api-prose.yamlfor a description string. Don't editapi/v2/by hand: the gate's--checkcompares a fresh derivation to the committed bytes. - Run
python3 scripts/derive-v2-api.py --write(requires PyYAML). - Commit the source and the regenerated outputs together.
Rules for the documents:
- Reference JSON Schemas by cross-file
$ref; never inline them. - New endpoints need a
tag, anoperationId, request and response schemas, and at least one error response. - Both layers must lint clean: the gate lints
api/openapi.yamlandapi/asyncapi.yaml(steps 2–3) andapi/v2/openapi.yaml,api/v2/asyncapi.yamlandapi/seams-v2.yaml(step 10). See "Useful commands" below.
Conformance suite (conformance/)
- Each scenario file in
conformance/src/scenarios/follows the existing pattern:
- A top-of-file docstring naming the spec document(s) it verifies. - describe('category: …', …) blocks per assertion group. - Assertions that cite the requirement they test, so a failure message names it.
- A new scenario file needs a row in
conformance/scenario-majors.json, which says which major(s) it targets. Regenerate it withconformance/scripts/generate-scenario-majors.mjs: a file namedv2-*targets major 2. A file with no row never runs. - New fixtures go in
conformance/fixtures/and in thefixtures.mdcatalog.spec-corpus-validity.test.tsfails otherwise. - Never return early in silence (RFC 0148 §A). A test that returns before its first
expectis a pass with zero assertions.
- Gate a profile with behaviorGate(profile, advertised). - For any other early return, say why: return softSkip('inapplicable', 'host does not advertise X'), or return seamAbsent(reason) when the host advertises a capability but the seam answers 404/403. - If you use vitest's ctx.skip(), call softSkip(...) first: ctx.skip() throws, so anything after it never runs. - The runner records a zero-assertion file with no note as blocked, and certification treats that as unclassified.
- Ask whether the condition is causable, not just whether the property is observable. If a scenario needs a host seam, an operator precondition or a process death to cause its condition, gate on that; don't assert the nearest thing the suite can cause.
- When a conforming host fails and a lenient one passes, suspect the scenario first. Check that every field the scenario requires is in the published contract.
Two rules for scripts
- Never write the literal you are policing. A checker that greps the tree for a string must not contain that string, in a comment, an example or its own error message. Build it from parts or read it from the registry the rule comes from.
- Assert every anchor before you edit anything. A script that edits several files in sequence stops at the first anchor it can't find, and every later edit silently doesn't happen. Resolve all anchors first, then write.
- Never run a global substitution over a lockfile or a generated file. A version bump touches a known set of sites; edit each by address. A blanket
sedoverpackage-lock.jsononce rewrote an unrelated dependency's version.
Regenerating derived files and running the gate
Many files are generated from others: the v2 API, the gap and register views, docs/PROTOCOL-STATUS.md, the requirement registry, the spec-artifacts package, and more. Never hand-edit them. After a change, run the generators in this order, then the gate:
python3 scripts/derive-v2-api.py --write
node scripts/generate-gaps.mjs --write
node scripts/generate-core-standard-manifest.mjs --write
node scripts/generate-assurance-status.mjs --write
node scripts/generate-protocol-status.mjs --write
(cd conformance && node scripts/generate-scenario-majors.mjs --write)
(cd conformance && node scripts/generate-requirement-registry.mjs --write)
node scripts/generate-spec-artifacts.mjs --write
node scripts/generate-review-packet.mjs --write
node scripts/generate-v1-eos-clock.mjs --write
node scripts/report-v2-witness-coverage.mjs --write
node scripts/check-spec-coherence.mjs --write
npm run openwop:check
Skip a generator only if you are sure none of its inputs changed. An edit made after the chain can make one stale again, so run the chain last.
npm run openwop:check (scripts/openwop-check.sh) is the merge gate. It mirrors .github/workflows/openwop-spec.yml and runs ten steps:
- Conformance suite: typecheck, host-free scenarios, and the corpus-coherence tests (every schema compiles, every fixture validates, links resolve, prose carries a status line).
api/openapi.yamllints clean (redocly).api/asyncapi.yamlvalidates (AsyncAPI CLI).- Generated surfaces are current, plus the register, RFC-status and waiver checks.
- Publish metadata and npm package contents.
- Security invariants: every protocol-tier MUST NOT in
SECURITY/invariants.yamlhas a matching public test. - Published-layout collection.
- Advertised package versions.
- Published-version identity.
- The v2 tree: declaration, generators, spec-readability budget, paths, deprecation dates, retirement and surface monotonicity, bundle maturity, the Accepted predicate, and lint/validation of
api/v2/openapi.yaml,api/v2/asyncapi.yamlandapi/seams-v2.yaml.
Beyond the gate, a PR also needs:
- A
CHANGELOG.mdline under[Unreleased]when it changes any artifact, in the form- Short lead. one sentencesaying what changed for an implementer. Leave out commit hashes, pass counts and how the change was found.scripts/check-changelog-shape.mjsenforces the shape. - A
Signed-off-by:trailer on every commit (see "Sign your commits" below).
Optional pre-commit hook
bash scripts/install-git-hooks.sh
This links scripts/hooks/pre-commit into .git/hooks/. It is fast (under a second) and fires only when staged paths match RFCS/*.md, catching an RFC change staged without regenerating docs/PROTOCOL-STATUS.md and README.md.
Process
- Pull requests go to this repository, labelled
openwop-specwhen they touch the spec corpus. - Issues: name the document, the section, the requirement that is unclear or contradictory, and the impact on implementations.
- Compatibility:
COMPATIBILITY.mddecides what is additive and what is breaking. - Normative changes need an RFC: see
RFCS/README.md. - Review rules (approvals, comment windows and how they are waived while the project has one maintainer) are in
GOVERNANCE.md, in "Decision making", "Spec change process" and "Sole-steward operation". Every waived comment window is recorded in the waiver ledger inMAINTAINERS.md.
Bootstrap-phase notes
Older RFCs cite this section for the comment-window waiver and one-approval review used while the project has a single maintainer. Those rules now live in GOVERNANCE.md §"Sole-steward operation", and every waiver is recorded in MAINTAINERS.md.
Sign your commits (DCO)
Every commit on a pull request MUST carry a Signed-off-by: trailer. This is the Developer Certificate of Origin, a lightweight alternative to a CLA. By signing off, you assert you have the right to submit the work under the project's license (Apache-2.0 for code, CC-BY-4.0 for spec text).
git commit -s -m "your message" # adds Signed-off-by automatically
git commit --amend -s --no-edit # add it to an existing commit
git rebase --signoff HEAD~3 # add it to the last 3 commits
The DCO bot runs on every PR and blocks merge until every commit is signed off. To fix a failing check, amend, force-push, and it re-runs.
While the project has one maintainer (see GOVERNANCE.md §"Sole-steward operation"), the steward's own commits to main don't go through a PR, so the DCO bot doesn't see them and they may lack the trailer. That exemption is recorded here for transparency. It ends when a second maintainer joins. External contributions MUST be signed.
Response times
A maintainer will respond to your PR or issue within:
- 24 hours for security-flagged issues (see
SECURITY.md). - 7 calendar days for everything else.
"Respond" means substantive: a review, a redirect, or a date by which it will be looked at. If 7 days pass without one, ping the lead maintainer listed in MAINTAINERS.md.
A PR waiting more than 14 days without a substantive response is a maintainer capacity problem, not a problem with your contribution.
Useful commands
# Host-free conformance subset
# (build the CLI once: cd conformance && npm install && npm run build:cli)
conformance/dist/cli.js --offline
# Lint OpenAPI (pinned to the gate's version)
npx -y -p @redocly/cli@2.31.4 redocly lint api/openapi.yaml
(cd api/v2 && npx -y -p @redocly/cli@2.31.4 redocly lint openapi.yaml)
npx -y -p @redocly/cli@2.31.4 redocly lint api/seams-v2.yaml --config api/v2/redocly.yaml
# Validate AsyncAPI (4.1.1 is the last Node-22-compatible release)
npx -y -p @asyncapi/cli@4.1.1 asyncapi validate api/asyncapi.yaml
npx -y -p @asyncapi/cli@4.1.1 asyncapi validate api/v2/asyncapi.yaml
# Check that api/v2 matches its source
python3 scripts/derive-v2-api.py --check