Orchestrator Guide
The orchestrator is the system that recommends which BoB registry items (registry/skills/, registry/agents/, registry/commands/) a project should provision. It replaces the historical hardcoded stack_to_items() table inside scripts/provision.sh with a declarative metadata contract that lives on each registry item itself.
When you ask “what should I provision for this project?” or “is doc-keeper applicable here?” the orchestrator answers from the registry’s metadata, not from a hardcoded list inside a shell script.
This guide covers:
- How the orchestrator runs — invocation modes, interview flow, what it produces
- Registry metadata contract — the schema items declare
- Validating metadata — the validator + make target
- Migration status — which items are migrated, which are not
- Recommendation engine (#160) — the rules that turn metadata into a manifest
- Interview question bank (#161) — how questions map to engine flags
- Adding a new registry item — quick start
- Adding a new question — quick start
- Troubleshooting
- Downstream features — invocation paths, diff/confirm UX
How the orchestrator runs
There are two invocation modes, both consuming the same engine and the same question bank:
| Mode | Entry point | When to use |
|---|---|---|
| Claude Code (#162) | /provision init (interactive in Claude) | When you’re already in a Claude Code session and want a guided set-up |
| Pure CLI (#163) | cdprov --interview | From a terminal without Claude — useful for CI / scripts / first-machine bootstrap |
Both produce byte-identical manifests for identical inputs because they:
- Run the same stack detection (
detect_stack()inscripts/provision.sh). - Walk the same question bank (
scripts/orchestrator/questions.json). - Call the same recommendation engine (
scripts/orchestrator/recommend.js). - Write to the same
provisions/<project>.jsonshape.
Interview flow
detect_stack(project_dir) # auto-detect tech stack
↓
load questions.json # dynamic question bank
↓
ask question 1 (project_type)
ask question 2 (lifecycle_stage)
ask question 3 (capabilities, multi)
[ask question 4 (deploy_target) if applies_when matches]
↓
merge implies[] from each answer # post-processing
↓
recommend.js --stack ... --project-type ... --lifecycle ... --capabilities ...
↓
[show diff against existing manifest if --existing-manifest supplied] ← #164
↓
[confirm changes] ← #164
↓
write provisions/<project>.json
↓
re-run cdprov refresh # apply symlinks based on the manifest
Steps 1–3 are documented in detail below. Step 4 (diff-and-confirm) is #164.
Registry metadata contract
Every registry item (skill, agent, or command) declares orchestrator metadata in its YAML frontmatter. The full schema lives at schemas/registry-item.schema.json (JSON Schema draft-07). The fields:
| Field | Required | Type | Purpose |
|---|---|---|---|
name | yes | string (kebab-case) | Stable identifier — must match the file/dir basename |
description | yes | string | One- or two-line summary surfaced in interview UIs |
model | no | sonnet / haiku / opus | Preferred Claude model (agents only) |
applies_to.stacks | no | string array | Tech stacks (from detect_stack) that auto-include this item |
applies_to.project_types | no | string array | Project archetypes that auto-include this item (api, web-app, cms, …) |
recommended_for | no | string array | Activity categories (development, testing, ops, governance, …) |
category | no | enum | UI grouping (backend-runtime, testing, review, …) |
required_with | no | qualified-id array | Other items this implies (e.g. skills/api-designing) |
conflicts_with | no | qualified-id array | Mutually exclusive items |
default | no | boolean | Always include for every project, regardless of stack |
Example — registry/skills/cloudflare-dev/SKILL.md:
---
name: cloudflare-dev
description: "Cloudflare development expertise..."
applies_to:
stacks: [cloudflare-workers, cloudflare-pages, d1, r2, kv]
project_types: [api, backend, web-app]
recommended_for: [development, ops]
category: backend-runtime
required_with: [skills/api-designing]
---Example — registry/agents/code-reviewer.md:
---
name: code-reviewer
description: ...
model: sonnet
applies_to:
stacks: [any]
project_types: [any]
recommended_for: [review, development]
category: review
default: true
---default: true means every project gets this item regardless of stack — useful for cross-cutting agents (code-reviewer, code-debugger, unit-test-generator) and process commands (research, retrospective, standup, status).
Stack enum — must match one of the values detect_stack() produces in scripts/provision.sh:
cloudflare-workers, cloudflare-pages, d1, r2, kv, typescript, javascript, node, deno, bun, hono, express, fastify, sveltekit, nextjs, react, vue, solidjs, astro, drupal, wordpress, strapi, ghost, vitest, jest, playwright, python, rust, go, ruby, php, tailwind, shadcn, any.
Project-type enum: api, backend, web-app, cms, research, cli, library, framework, mixed, any.
Validating metadata
Validator script
scripts/checks/validate-registry-metadata.sh walks every registry item and reports its state:
ok— item declares migrated frontmatter and validates against the schemaunmigrated— item exists but hasn’t been migrated to the new contract yet (no orchestrator fields). Not an error — rollout is incremental.error: <details>— item declares migrated fields but they don’t validate. Fatal.
bash scripts/checks/validate-registry-metadata.sh /path/to/bob-sourceOutput (last line is the summary):
registry/skills/cloudflare-dev/SKILL.md: ok
registry/skills/api-designing/SKILL.md: unmigrated (legacy frontmatter only)
registry/agents/code-reviewer.md: ok
...
[validate-registry] total=75 migrated=3 unmigrated=72 errors=0
Makefile target
make check-registry-metadata runs the validator and exits non-zero on errors (not on unmigrated items). This target is part of make check, so the canonical verification entry point now gates on schema correctness for every migrated item.
make check-registry-metadata # standalone
make check # bundles it with the other checks
make ci # full check + test + docs-checkMigration status
| Category | Migrated examples |
|---|---|
| Skills | registry/skills/cloudflare-dev |
| Agents | registry/agents/code-reviewer.md |
| Commands | registry/commands/research.md |
The remaining items (~72 of 75) will be migrated in #159. Until then, the validator reports them as unmigrated and make check does not fail. The recommendation engine (#160) falls back to the existing stack_to_items() table for any item that hasn’t been migrated yet, so the orchestrator never breaks during rollout.
Downstream features
The schema is the foundation for the rest of the orchestrator capability:
- #159 — Backfill metadata across the entire registry. After this lands,
unmigratedcount goes to zero andmake checkbecomes strict. - #160 — Recommendation engine. Reads
applies_to,recommended_for,default,required_with,conflicts_withand emits a per-project manifest. - #161 — Interview question bank. Maps user answers (project type, activities) to
applies_to.project_typesandrecommended_for. - #162 — Claude Code invocation path (
/provision initinteractive). - #163 — Pure-CLI invocation path (
cdprov --interview). - #164 — Diff-and-confirm UX for re-runs.
- #165 — Golden-manifest test fixtures.
- #166 — Final orchestrator documentation (this guide gets expanded).
Recommendation engine (#160)
The engine at scripts/orchestrator/recommend.js is the single source of truth for “given this stack + project type + capabilities, which items belong in the manifest?” Both the Claude Code path (#162) and the pure-CLI path (#163) call it directly so identical inputs always produce byte-identical manifests.
node scripts/orchestrator/recommend.js \
--stack cloudflare-workers,d1,hono \
--project-type api \
--lifecycle development,testing \
--capabilities development,review,testing \
--existing-manifest provisions/foo.jsonOutput is a manifest JSON to stdout that conforms to schemas/manifest.schema.json. Fields:
_meta.input— the inputs that produced this manifest (audit trail)_meta.manual— items that were in--existing-manifestbut not in the recommended set (preserved as user customisations)skills,commands,agents,runbooks— sorted item lists
The engine implements four selection rules in order:
default: true→ always included.applies_to.stacks∩ user stack (oranywildcard).applies_to.project_typescontains the user’s project_type (orany).recommended_for∩ capabilities/lifecycle (or empty user list = unconditional match).
After filtering, expandRequirements() walks required_with edges and pulls in implied items. Then checkConflicts() returns non-zero with details if any conflicts_with edges overlap the selected set.
Interview question bank (#161)
Both invocation paths share a data-driven question bank at scripts/orchestrator/questions.json (validated by make check against schemas/orchestrator-questions.schema.json). Editing the JSON is the canonical way to change interview behavior — no code changes needed.
Each question declares:
| Field | Purpose |
|---|---|
id | Stable identifier |
prompt | What the user sees |
help (optional) | Extra context for the user |
type | single or multi |
maps_to | Which engine flag this question feeds (project_type, lifecycle, capabilities, stack) |
applies_when (optional) | Predicate gating when the question is asked (e.g. only ask “deploy target” when project_type ∈ {web-app, api, backend, cms}) |
options[] | Each option has label, value, and an optional implies[] for additional values to union into related flags |
The question bank’s option values use the same enums as schemas/registry-item.schema.json — project_types, stack names, and capability categories all match. That alignment is what lets the engine route an answer directly into a flag without translation.
The four current questions:
project_type(single) — web-app / api / backend / cms / research / cli / library / mixedlifecycle_stage(single) — greenfield / active / maintenance / audit-only (each implies a default capability set)capabilities(multi) — pre-selected from the engine’s recommendation; categories come from registry metadatadeploy_target(single, conditional) — Cloudflare Workers / Node / self-hosted / unknown
Adding a new registry item
When you add a new skill, agent, or command, declare orchestrator metadata in its frontmatter:
---
name: my-new-skill # required, kebab-case, must match dirname/filename
description: One-line summary. # required
applies_to:
stacks: [<from the schema enum>] # auto-include for these tech stacks
project_types: [api, backend, ...] # auto-include for these archetypes
recommended_for: [<activity categories>] # capability-driven inclusion
category: <ui-grouping> # how the interview groups this item
required_with: [skills/some-other] # implies these other items
default: false # set true to always include
---Then run make check-registry-metadata — the validator catches schema violations early. If you’re stuck on which category or recommended_for value to use, look at how a similar existing item is tagged: grep -rl "^category: testing" registry/.
When in doubt, use any for stacks/project_types — the engine’s other rules (capability match, required_with) still scope the item appropriately.
Adding a new question
The interview is data-driven; no code changes needed.
- Edit
scripts/orchestrator/questions.json. - Add an entry under
questions[]:{ "id": "my_question", "prompt": "What ...?", "type": "single", "maps_to": "capabilities", "applies_when": { "project_type": ["web-app", "api"] }, "options": [ { "label": "...", "value": "option-1", "implies": ["..."] } ] } - Run
make check-schemas— the schema validates the question’s shape. - Both invocation paths pick up the new question on next run; no rebuild needed.
maps_to must be one of project_type, lifecycle, capabilities, or stack. option.values should match the enums in schemas/registry-item.schema.json (otherwise the engine won’t know what to do with them). implies is the canonical extension point for unioning extra values into related flags.
Troubleshooting
”I asked for X but didn’t get item Y”
Check the engine’s filter rules in order:
- Does the item have
default: true? If so, it’s always included regardless of inputs. applies_to.stacks— does it intersect your--stack?anyis a wildcard.applies_to.project_types— does it contain your--project-type?anyis a wildcard.recommended_for— does it intersect your--capabilitiesor--lifecycle? Empty user list means unconditional match.- After matching,
required_withedges are pulled in. Did the item arrive because something else’srequired_withlisted it?
If everything looks right but the item is still missing, run the engine with explicit flags and inspect the filter:
node scripts/orchestrator/recommend.js --root . --stack <yours> --project-type <yours> --capabilities <yours> 2>&1“I got a conflict error”
checkConflicts() returned non-zero because two items in the recommended set declare each other in conflicts_with. The error message lists the offending pair. Resolution paths:
- Remove one of the items from the registry (if conflicting items shouldn’t both exist).
- Tighten the
applies_toof one so they don’t both match the same project (if they’re alternatives for different stacks). - Remove the
conflicts_withdeclaration if it’s overly aggressive.
”Same inputs produce different manifests”
That should never happen. The engine sorts items by name within each kind, and _meta.generated_at is the only non-deterministic field (a timestamp). Diff with jq 'del(._meta.generated_at) | del(._meta.input)' to compare.
If the diff is still non-empty, file a bug — there’s a non-determinism somewhere (a Set traversal order, an unordered readdir, etc.).
”An item shows up in the manifest with _meta.manual”
That’s intentional — the item was in the existing manifest but not in the engine’s recommended set, so it was preserved as a user customisation. To remove it, edit the manifest manually and re-run.
”validate-registry-metadata.sh reports unmigrated”
Items that haven’t been migrated to the orchestrator schema yet (applies_to, recommended_for, or category missing). Run scripts/migrate-registry-metadata.sh --force to backfill from the rule table, then audit the result.
If the rule table doesn’t have an entry for the item, add one and re-run. The migration script is idempotent — re-runs only touch items the rules cover.
Pure-CLI interview (#163)
cdprov --interview (or the PATH-friendly cdprov-interview binary) runs the same interview from a terminal — no Claude Code session required. It picks the best-available TUI:
| Preference | Tool | Usage |
|---|---|---|
| 1 | gum | gum choose for single-select, gum choose --no-limit --selected="…" for multi-select with pre-checks |
| 2 | fzf | fzf for single, fzf --multi for multi-select (no native pre-check; suggested set shown in the header instead) |
| 3 | whiptail | --menu / --checklist |
| 4 | plain read | Numbered list fallback |
cdprov --interview # interactive
cdprov --interview --yes # skip the confirm step (interview still runs)
cdprov --interview --non-interactive < answers.txt # CI / scripted
cdprov-interview # same thing, PATH-friendlyCI mode (--non-interactive)
Stdin is read line-by-line in question order:
project_type
lifecycle_stage
capabilities (comma-separated)
deploy_target (only when project_type ∈ web-app | api | backend | cms)
confirm (Apply | Cancel — omit if --yes)
The interview’s manifest must match the engine’s manifest for the same inputs — verified by tests/orchestrator/test-interview.sh (5 smoke tests covering write, key shape, engine parity, cancel path, and re-run no-op).
Diff engine (#164)
scripts/orchestrator/diff.js is the shared diff library used by both the Claude Code path (#162) and the pure-CLI path (#163). It compares a proposed manifest (from recommend.js) against an existing manifest and groups every entry into one of four buckets:
| Marker | Meaning |
|---|---|
+ | Added — in proposed only (the engine’s recommendation will introduce this) |
- | Removed — in existing only (the proposed manifest does not include it; it will be dropped unless preserved) |
= | Unchanged — in both |
! | Manually-added — in both, and proposed._meta.manual flagged it (the engine preserved a hand-added entry verbatim, even though it isn’t in the current recommendation set) |
CLI
node scripts/orchestrator/diff.js --proposed <path> [--existing <path>] [--json] [--quiet]Exit codes: 0 no diff, 1 changes present, 2 invalid input. Designed so caller scripts (#162 / #163) can if diff.js ...; then echo "no-op"; fi and only prompt the user when there’s something to confirm.
Library
const { diffManifests, formatDiff } = require('./diff');
const d = diffManifests(proposed, existing); // pure function, no I/O
if (d.no_diff) { /* skip the confirm prompt */ }
process.stdout.write(formatDiff(d));Manual-preservation contract
recommend.js already populates proposed._meta.manual with kind/name entries for any item in the existing manifest that the current recommendation didn’t pick. diff.js reads that list to mark those entries as ! rather than =, so the user sees they’re being kept on purpose. Re-running with the same answers is therefore a guaranteed no-op (covered by the re-run no-op test in tests/orchestrator/test-diff.js).
Downstream features (still to land)
- #162 — Claude Code invocation path:
/provision initruns the interview interactively inside Claude Code. Callsrecommend.js+diff.jsand writes the manifest. - #163 — Pure-CLI invocation path:
cdprov --interviewfor terminal use. Reads questions.json, prompts viagum(withfzf/whiptailfallbacks), and pipes inputs to the engine. Same output shape as #162. - (follow-up) — Deprecate
stack_to_items()inscripts/provision.shand route--initthrough the engine. - (follow-up) — A recorded asciinema demo for the guide.
See also
schemas/registry-item.schema.json— full schema sourceschemas/orchestrator-questions.schema.json— question bank schemascripts/orchestrator/recommend.js— recommendation engine (#160)scripts/orchestrator/diff.js— diff engine (#164)scripts/orchestrator/questions.json— interview question bank (#161)scripts/checks/validate-registry-metadata.sh— registry validatorscripts/provision.sh— the historicaldetect_stack+stack_to_items(deprecated; will route through #160 in a follow-up)- PROJECT-ORCHESTRATION-HANDBOOK §3.4 — manifest provisioning workflow