Using scopes
Declare scopes, pin a blueprint to one, and gate shell steps on the active scope.
A scope is a string label attached to a blueprint that tells contractor which slice of the repo the change belongs to. In a monorepo, scopes typically name the affected package — cli, web, protocol. They're optional, but once declared they unlock two useful behaviors: {{scope}} substitution in pipeline shell steps, and a when: scope guard that lets the same pipeline serve scoped and unscoped blueprints.
This guide walks through declaring a scope set, pinning a blueprint to one, and using both substitution and the guard in a pipeline.
1. Declare the scope whitelist
List the valid scope values under scopes: in contractor/config.yaml:
# contractor/config.yaml
scopes:
- cli
- protocol
- web
- wwwOnce scopes is declared, the validation rules kick in:
contractor blueprint new --scope <value>rejects any value not in the list with an error that prints the declared set.- The interactive
--scopeprompt offers a select instead of free text. contractor blueprint list --scope <name>filters the listing;--scope unscopedshows blueprints with no scope.
If you don't declare scopes, the --scope flag accepts any string. That's fine for a small repo where you don't want the formality, but most teams want the validation as soon as the second scope shows up.
2. Pin a blueprint to a scope
Pass --scope when creating the blueprint:
contractor blueprint new add-resume-support --scope cliThis writes scope: cli into contractor/blueprints/add-resume-support/.contractor.yaml and tells the propose-phase agents (and the implement agent's preamble) to focus changes within packages/cli/. The blueprint can be re-scoped later by editing the YAML, but typically the scope is set at creation time and stays put.
A blueprint with no --scope and no scope in its .contractor.yaml is unscoped. That's a valid state — it just means {{scope}} substitutions and when: scope guards behave as documented below.
3. Use {{scope}} in shell steps
Pipeline shell steps support {{scope}} substitution. The placeholder is replaced with the blueprint's scope value before the command is spawned:
# contractor/config.yaml
pipelines:
scoped-test:
description: Run tests for the active scope only.
steps:
- id: implement
kind: agent
slashCommand: implement.md
critical: true
needs_prepared_worktree: true
- kind: shell
command: pnpm --filter @myorg/{{scope}} test
when: scope
critical: true
- id: review
kind: agent
slashCommand: review.md
critical: trueFor a blueprint with scope: cli, the shell step expands to pnpm --filter @myorg/cli test. Substitution happens at spawn time, not at parse time, so the same pipeline definition serves every scope.
{{scope}} is only supported in kind: shell steps. It is not substituted in agent step prompts, slash command names, or other fields. Adding new template variables is a separate proposal — see Pipelines for the current step reference.
4. Guard scoped-only steps with when: scope
A kind: shell step that depends on {{scope}} should be guarded with when: scope:
- kind: shell
command: pnpm --filter @myorg/{{scope}} test
when: scope
critical: trueThe guard says: "skip this step when the active blueprint has no scope." Skipped steps are recorded as skipped in the pipeline run's child rows — not failed, not silently dropped. The pipeline continues as if the step weren't there.
Without the guard, an unscoped blueprint would either fail loudly (an unguarded {{scope}} reference with no scope value is rejected at spawn) or run with an empty substitution (which contractor explicitly does not allow — there's no pnpm --filter @myorg/ test). The guard is the supported way to make the same pipeline work for both scoped and unscoped blueprints.
5. Override the scope at run time
contractor run --scope <name> overrides the blueprint's pinned scope for a single invocation:
contractor run --scope protocolThis is useful for experiments — running an unscoped blueprint as if it were scoped to protocol to see how the scoped shell step behaves. The override is in-memory; it does not rewrite .contractor.yaml. For a permanent change, edit the YAML.
If --scope names a value not in the declared whitelist (when one exists), the run is rejected with the same error blueprint new --scope produces.
6. Verify with contractor doctor
Doctor's "Schema graph" and project config sections call out scope-related issues:
- A pipeline shell step with
{{scope}}but nowhen: scopeguard, in a repo whose blueprints can be unscoped. - A
--scopevalue that doesn't match the declared set. - Stray
{{scope}}references in non-shell steps (which are not substituted, so the literal string ends up in the prompt).
If contractor run --scope cli rejects with Unknown scope, check that cli is listed under scopes: in contractor/config.yaml (not in a blueprint's .contractor.yaml — the whitelist is project-level).
A complete example
# contractor/config.yaml
scopes:
- cli
- protocol
- web
pipelines:
ship-it:
description: Implement, scoped tests, review, gate, close.
steps:
- id: implement
kind: agent
slashCommand: implement.md
critical: true
needs_prepared_worktree: true
- kind: shell
command: pnpm --filter @myorg/{{scope}} test
when: scope
critical: true
- id: review
kind: agent
slashCommand: review.md
critical: true
- kind: gate
id: pre-close
description: Final review before close.
- id: close
kind: agent
slashCommand: close.md
critical: falseThe shell test step runs only for scoped blueprints. Unscoped blueprints skip it and proceed straight to review. If you want a fallback for unscoped runs, a separate pipeline (no when: scope guard, full-suite command) is the simplest answer today: select it with --pipeline for unscoped blueprints. The only guard literal when: accepts is "scope"; richer predicates like negation or boolean composition are not supported.
See also
- Pipelines — step kinds, the
when:field, andcritical:. contractor/config.yaml— thescopesfield reference..contractor.yaml— the per-blueprintscopefield.