Orchestrator Guide
TL;DR. The orchestrator recommends which registry items a project should provision, reading declarative metadata on each item instead of a hardcoded shell table. Ask “what should I provision for this project?” and it answers from the registry. Engine:
scripts/orchestrator/recommend.js.
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.
How the orchestrator runs
Section titled “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) | bob provision --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
Section titled “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 manifestSteps 1–3 are documented in detail below. Step 4 (diff-and-confirm) is #164.
Registry metadata contract
Section titled “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, …) |
applies_to.phases |
no | string array | Project lifecycle phases this item applies to (inception, build, stabilize, maintain, any). Absent = every phase (#1133) |
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-devdescription: "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-runtimerequired_with: [skills/api-designing]---Example — registry/agents/code-reviewer.md:
---name: code-reviewerdescription: ...model: sonnetapplies_to: stacks: [any] project_types: [any]recommended_for: [review, development]category: reviewdefault: true---default: true means every project gets this item regardless of stack — useful for cross-cutting agents (code-reviewer, code-debugger, unit-test-generator) process commands (retrospective, standup, status) and the research skill.
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
Section titled “Validating metadata”Validator script
Section titled “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: okregistry/skills/api-designing/SKILL.md: unmigrated (legacy frontmatter only)registry/agents/code-reviewer.md: ok...[validate-registry] total=75 migrated=3 unmigrated=72 errors=0Makefile target
Section titled “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 # standalonemake check # bundles it with the other checksmake ci # full check + test + docs-checkMigration status
Section titled “Migration status”| Category | Migrated examples |
|---|---|
| Skills | registry/skills/cloudflare-dev |
| Agents | registry/agents/code-reviewer.md |
| Commands | registry/commands/retrospective.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
Section titled “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 (
bob provision --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)
Section titled “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.project— the project name (from--project-name, falling back to the existing manifest’s_meta.project, thenbasename(cwd))_meta.path/_meta.stack/_meta.cloudflare_account— the schema’s declared metadata;pathandcloudflare_accountare preserved from--existing-manifestwhen present_meta.billing— job attribution (#4423):{job, billable}, the client job the project’s agent hours bill to and whether they bill at all; read bybob labor report --by joband printed bybob provision status(work attribution)_meta.manual— items that were in--existing-manifestbut not in the recommended set (preserved as user customisations)_meta.declined— items the operator declined, carried over from--existing-manifest; a declined item is never selected (see Declining an item)skills,commands,agents,runbooks— sorted item lists
The engine emits no provenance block (generated_at, generator, version, input) — git history records when/how a manifest was generated, and a volatile timestamp would defeat idempotent re-runs (#1269).
The engine implements five 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).applies_to.phasescontains the project’s phase (orany; absent field = every phase; no declared phase = no filtering — #1133).
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.
Phase as a recommendation input (#1133)
Section titled “Phase as a recommendation input (#1133)”The provision manifest’s _meta.phase is the single declared home for a
project’s lifecycle phase — vocabulary inception / build / stabilize /
maintain, enforced by schemas/manifest.schema.json. recommend.js --phase
overrides it for a one-off run; without the flag the engine reads it from
--existing-manifest, and it writes the resolved phase back into _meta.phase
so recompute stays self-contained and deterministic. A phase edit is therefore a
normal manifest edit: edit _meta.phase → recompute → review the diff →
bob provision refresh — a previously provisioned item that falls out of phase scope
is preserved as a _meta.manual keep (reviewable, never a silent drop), and a
fresh recompute at the new phase yields the leaner set. The loop is guarded
end-to-end by make test-phase-transition-e2e.
Flight-plan scope derivation (plan-scope.js, #1133)
Section titled “Flight-plan scope derivation (plan-scope.js, #1133)”scripts/orchestrator/plan-scope.js turns a flight-plan issue (#1068) into a
provision overlay — the registry items the plan’s issues need, on top of the
base manifest, never a mutation of it:
node scripts/orchestrator/plan-scope.js derive <plan-N> --project <name> # overlay doc (JSON)node scripts/orchestrator/plan-scope.js diff <plan-N> --project <name> # reviewable +added diffSignals are derived deterministically from the plan’s issues: stack tokens
matched word-bounded in title/body, labels mapped to capabilities (bug →
testing, documentation → docs, literal capability labels), and
area:<slug> labels matched against the capability/category vocabularies — all
vocabularies read from schemas/registry-item.schema.json at runtime. An item
is overlay-relevant when any signal hits it; required_with is expanded,
base-manifest items are subtracted, default: true items are excluded (the
base recompute owns them).
Kits from labels (#2227). A plan issue label that a kit declares in
registry/kits.json (labels) selects that kit, and the kit’s registry items
join the overlay — security → security, release → release, incident →
incident, cap / business-case / use-case / vision → vision-planning,
bug → bugfix. The map lives in the kit file, not in the script (Session
Kits → Labels).
Universal kit items are left out (bob init already links them) and
base-manifest items are subtracted, as for signals. The derive document names
each selected kit under derived_from.kits ({kit, labels, issues}) and every
overlay item’s provenance under sources — {"skills/tdd-flow": ["kit:bugfix"], "skills/cloudflare-dev": ["signal"]}, an item hit by both listing both.
diff shows the kit-sourced additions apart from the signal-sourced ones, and
overlay apply leaves out items that only a kit the machine’s role forbids
selected.
The overlay is applied/restored by
bob provision overlay — see the provisioning docs and the warp-drive how-to’s
flight-plan section for the session lifecycle (#1133).
Capability blocks (beyond item lists)
Section titled “Capability blocks (beyond item lists)”The engine also emits opt-in capability blocks that aren’t symlinked items. The
versioning block (#277) is emitted whenever the versioning command is selected
(it is default: true, so effectively every project — the BoB-wide-versioning goal
of #275), with a per-project-type trigger_paths filter (VERSIONING_TRIGGER_PATHS
in recommend.js): docroot/** for cms, the full source surface for framework,
src/**+package.json for apps/libraries, no filter for research/mixed. An
existing manifest’s versioning block is always preserved, so opt-outs
(enabled:false) and customizations survive a re-run. bob provision refresh then copies
the versioning template set into the project — see the versioning guide.
The docs_site block (#735) recommends a documentation site for applicable
project types — web-app, api, backend, cms, library, framework,
mixed — and is gracefully skipped for CLI-only and research repos. It
records the recommendation and the default engine ({ "enabled": true, "engine": "vitepress" }); VitePress is the single default and Starlight is an explicit,
evaluation-only opt-in (#734). Unlike the registry skills, docs-site is a
universal skill (always present), so the block is a recommendation to run
/docs-site init rather than a symlinked item — that’s why it gates on the
skill’s own applies_to.project_types (read from skills/docs-site/SKILL.md)
instead of being one of the scanned registry/ items. An existing docs_site
block is always preserved, so an opt-out or engine choice survives a re-run. See
the docs-site skill (skills/docs-site/SKILL.md).
Inference from vision and code (#1873)
Section titled “Inference from vision and code (#1873)”The interview is one way to tell the engine what a project is. The vision-aware pipeline
is the other: the same (stack, project_type, lifecycle, capabilities[]) tuple the
questionnaire produces is inferred from the project’s prose and code, fed to the engine,
explained item by item, and re-derived on demand when the project moves. Every stage is a
separate script with a stable document contract, so each can be run — and tested — alone.
docs/vision.md + README.md + CLAUDE.md ─┐ ├─ infer-profile.js ─ profile ─ recommend.js --profile ─ manifestpackage.json, wrangler.*, workflows, … ─┘ │ │ └─ explain.js ─────────────┴─ why: / would-add / would-remove │ │ gap-detect.js divergence cdprov reconcile [--apply] (/vision, /groom nudge) (apply the delta)| Stage | Script | Surfaces it | Issue |
|---|---|---|---|
| Infer the profile | scripts/orchestrator/infer-profile.js |
bob provision --init (default), recommend.js --infer |
#1877 |
| Recommend from it | scripts/orchestrator/recommend.js --profile |
bob provision --init [--auto], /provision init |
#1878 |
| Explain the recommendation | scripts/orchestrator/explain.js |
bob provision --init --auto, bob provision --diff [--json] |
#1879 |
| Reconcile the manifest | scripts/provision.sh reconcile |
bob provision reconcile [--apply] [--allow-remove] |
#1880 |
| Nudge on divergence | scripts/orchestrator/gap-detect.js divergence |
/vision, /groom |
#1881 |
Inputs
Section titled “Inputs”infer-profile.js reads, per project and each optional:
- Prose —
docs/vision.md,README.md,CLAUDE.md. Matched against the registry’s own category cue words (recommended_forvocabulary:testing,ops,governance,docs,review, …). There is no per-item keyword table and no new taxonomy. - Code —
package.json(deps,bin, scripts),wrangler.*,dev.json/checks.json,.github/workflows/*,tests/,docs-site.json,tsconfig/composer/Cargo/go.mod/pyproject, plus ashellsignal frombin//scripts/entrypoints. Stack detection isscripts/fleet/readiness.jsdetectStack— the same detector the fleet audit uses, so inference and audit can never disagree about what a project is built with.
node scripts/orchestrator/infer-profile.js --root <dir> # human: fields + evidencenode scripts/orchestrator/infer-profile.js --root <dir> --json # schema bob-orchestrator-profile/1The output is deterministic and side-effect-free — file reads only, no processes, no network,
fixed key and array order — so the same tree is byte-identical across runs and machines.
Golden fixture projects under tests/fixtures/orchestrator/ pin the outcomes (#1882: a CF
Workers API with a vision, a shell CLI without one, a docs-only repo); make test-infer-profile
runs them.
Evidence model
Section titled “Evidence model”Every inferred field carries evidence. The profile’s evidence[] rows are
{field, value, source, note}:
sourceis a file:line for prose (docs/vision.md:L12) and a file:key for code (package.json:bin,package.json:devDependencies.vitest,.github/workflows/ci.yml,tests/).- A conflict — vision says “website”,
package.json:binsays CLI — is recorded as aconflict:noteon the field’s evidence row (the code value, the prose source and its excerpt). It is never silently resolved. - Absence is evidence too. Missing prose degrades to stack-only inference, and the
profile’s
sources.absentnames what was not there, so a stack-only result is distinguishable from a vision that simply evidenced nothing.
The evidence rows are what make every later stage explainable: explain.js maps each
recommended item’s matching facet back to the row that produced it, and the divergence nudge
quotes the same row (vision now mentions capability governance (docs/vision.md:L9)).
Precedence
Section titled “Precedence”Three sources can describe the same project — interview answers, code, prose — and they do not carry equal weight:
| Field | Wins | Then | Notes |
|---|---|---|---|
stack |
interview answer (explicit flag) | code | prose never sets a stack |
project_type |
interview answer | code | prose is used only when code gives no signal; a prose/code conflict is recorded, code applies |
lifecycle |
interview answer | code | prose may refine it |
capabilities |
interview answer | vision prose | code adds testing / ops / docs signals; README / CLAUDE.md prose contributes only when a vision exists |
In short: interview > code > vision for stack and type; vision contributes capabilities.
- Explicit flags override inferred fields, field by field.
recommend.js --stack,--project-type,--capabilities,--lifecyclegiven alongside--profilewin — that is how interview answers layer on top of inference. - Vision gate. Prose-derived capabilities are applied only when
docs/vision.mdwas among the profile’s consulted sources. Without a vision the profile contributes stack + type only, so the result is the stack-only baseline — a CLI tool with no vision gets exactly what it got before #1878. The baseline is the wider set (no capability filter); a vision narrows it to the capabilities it evidences. - Lifecycle is folded, not filtered. The inferred stage is translated into capabilities
through the interview’s own
impliestable (questions.jsonlifecycle_stage—maintenanceaddsops+review, and so on) and deliberately not passed as--lifecycle: that match is a restrictive filter onrecommended_for, and anactivestage would veto every docs/governance item the vision asked for.
Limits
Section titled “Limits”Inference is a heuristic, and its limits are by design:
- Prose matching is cue-word matching. A vision that says “we value rigorous verification”
does not evidence
testing; one that says “unit tests and e2e” does. Phrasing that misses the cue list is a missed capability, not an error — the interview (or an explicit--capabilities) is the override. - Prose never sets the stack, and sets the type only in a codeless tree. A vision describing a future stack contributes nothing until the code exists.
- No vision, no capability filter. The stack-only baseline is intentionally wide; the
first
/visionwrite is what narrows it (and what triggers the divergence nudge). - Evidence is per file:line, not per sentence meaning. Two capabilities cued from one line
share a source; the
notedistinguishes them. - Registry items without orchestrator metadata are invisible to inference and explain
alike: they are never recommended and, if declared, surface as a
would-removewhose reason is not a registry item with orchestrator metadata (cannot be evidenced) — a metadata gap to fix, not an item to drop. - Opt-in items are never inferred (#1980). A registry item with
opt_in: truein its frontmatter is a product- or vendor-specific integration (eloqua-dev,openreplay-dev,openreplay-ops): the stack says nothing about whether the project uses that product, so no profile ever recommends it — it enters a manifest only through an explicitbob provision add. Itsapplies_to/recommended_forstay informational. The flag matters because the L3 provisioning preflight auto-adds every missing recommended item: a stack-matched vendor skill would otherwise land in every JS or Workers project.
Feeding the engine (--profile / --infer, #1878)
Section titled “Feeding the engine (--profile / --infer, #1878)”The profile document is an input source for recommend.js equivalent to interview
answers:
node scripts/orchestrator/recommend.js --profile <profile.json|-> # a saved / piped profilenode scripts/orchestrator/recommend.js --infer <project-dir> # infer in-process, then recommendbob provision --init and bob provision --init --auto take this path by default: they run
infer-profile.js on the project, print the inferred stack / type / capabilities (and which
prose was consulted), and hand the profile to the engine. --no-infer restores the stack-only
input (detect_stack + infer_project_type, no capabilities); an inference failure falls back
to the same path with a warning rather than aborting. The existing-manifest contract is
unchanged — a re-run over a manifest keeps items the new recommendation drops as
_meta.manual, so switching between --no-infer and the default never silently removes
anything.
Explainable output (explain.js, why: lines, #1879)
Section titled “Explainable output (explain.js, why: lines, #1879)”Every recommendation is explainable. scripts/orchestrator/explain.js re-runs the engine’s
matching rule with the facets that fired recorded (explainInclude() in recommend.js — the
same rule shouldInclude() answers, so the explanation can never disagree with the manifest)
and maps each facet back to the profile’s evidence:
node scripts/orchestrator/explain.js --project <dir> [--manifest <path>] [--json]# + skill cloudflare-dev — why: stack cloudflare-workers (wrangler.toml); capability ops (.github/workflows/ci.yml); required by skills/d1-expert# = agent qa-strategist — why: capability testing (package.json:devDependencies.vitest); capability governance (docs/vision.md)# - skill shell-cli-design — would-remove: no evidence supports it: not matched by stack/project type/capabilities, not default, not required_with by a recommended item+would-add — recommended, not in the manifest;=keep — recommended and declared. Thewhy:names the facet(s) that matched — one stack (with its file), the project type, every matched capability with its source (docs/vision.md:L9for prose,package.json:…for code),default for every project, orrequired by <kind/name>.-would-remove — a manifest item that neither evidence,default: true, nor arequired_withedge from a recommended item supports. Items listed in_meta.manualare flagged (kept). A declaredopt_in: trueitem is akeep, not a would-remove — the declaration is its evidence (why: opt-in item — never auto-recommended; declared by hand), so a reconcile never proposes dropping a vendor skill a human provisioned (#1980).- Report-only, never silent.
explain.jswrites nothing, and no provisioning write removes a manifest item:bob provision --init --autore-runs preserve unsupported items as_meta.manual, and only an explicitbob provision remove <type> <name>or an acceptedbob provision reconcile --apply --allow-removedrops one. A would-remove is a prompt for a human decision, not an action. --jsoncarries the same structure for tooling — schemabob-orchestrator-explain/1:recommended[] {kind, name, status, why, evidence[] {facet, value, source, note}},would_remove[] {kind, name, manual, why},counts,context,sources.
bob provision surfaces it in two places: bob provision --init --auto prints a Recommendation block
with a why: line per item after generating the manifest, and bob provision --diff appends a
Recommendation (inferred) section — would-add with why:, would-remove with its reason —
after the link diff. bob provision --diff --json emits the explain.js document instead of the human
report. Both honour --no-infer.
Reconcile (bob provision reconcile, #1880)
Section titled “Reconcile (bob provision reconcile, #1880)”The explain document is also the input to reconciliation — re-deriving a long-lived project’s manifest after its vision or code has moved, instead of hand-editing it:
bob provision reconcile # dry run: + would-add (why) / - would-remove (reason); nothing writtenbob provision reconcile --apply # apply: single-item delta at L2+ fast-paths end-to-end; # multi-item delta or L1 opens the staged review PRbob provision reconcile --apply --allow-remove # also drop would-remove items (never by default)bob provision --diff --infer is the same dry run. --apply builds the new manifest and hands it
to exactly the machinery bob provision add/remove use — the #785 staged review PR with the #1478
config-fastpath on top, --now, or the ungated apply route; the #791 pending-PR manifest is
the edit base when one is open. It is idempotent (a reconcile after an applied one reports
no delta and creates no git refs), removals are withheld unless --allow-remove is passed
or an interactive run answers yes, and _meta.manual items are never removal candidates. Full
routing rules: handbook §3.4.
Tests: make test-cdprov-reconcile.
Declining an item (bob provision decline, #3468)
Section titled “Declining an item (bob provision decline, #3468)”A recommendation the project does not want is declined in the manifest, so the decision outlives the session. The session waiver (provision-preflight.js waive) is forgotten by clear at session end, and before #3468 a fresh Level 3 run added a pruned item straight back.
bob provision decline command/deploy --reason "the project deploys with npm run deploy" # staged review PRbob provision decline command/deploy --reason "the project deploys with npm run deploy" --now # straight to the operator repoThe command writes one entry to _meta.declined in provisions/<project>.json and removes the item from its list if the manifest declares it:
"_meta": { "declined": [ { "item": "command/deploy", "reason": "the project deploys with npm run deploy", "date": "2026-10-08" } ]}item is <type>/<name> with the singular type the CLI uses. All three fields are required, and schemas/manifest.schema.json validates them. The write lands the way add/remove and reconcile --apply do: a staged review PR, --now, or the ungated route, always in the operator repo (#2270). Declining an item twice is a no-op.
A declined item counts as satisfied everywhere the orchestrator grades a manifest:
provision-preflight.js checknever lists it as missing or proposes afixfor it. It is reported underdeclined.recommend.jsnever selects it, and carries_meta.declinedacross re-runs.explain.jsshows it asdeclined (<reason>, <date>)(anxline), neverwould-add, sobob provision reconcilenever adds it back.
To take a decline back, delete its entry from _meta.declined and run bob provision add. Tests: make test-cdprov-reconcile, make test-provision-preflight.
Divergence nudge (gap-detect.js divergence, #1881)
Section titled “Divergence nudge (gap-detect.js divergence, #1881)”The last stage closes the loop from a vision edit back to provisioning. gap-detect.js divergence takes the inferred profile instead of a free-text task and nudges — with the same
#412 dedupe, the same fix: line and the same exit codes as the mid-session match — for every
registry item the explain document would-add but the manifest does not declare:
gap-detect: 2 unprovisioned registry item(s) implied by the inferred profile (action: confirm, L2): skills policy-audit — vision now mentions capability governance (docs/vision.md:L9) — registry item policy-audit covers it fix: cdprov add skills/policy-audit --nowIts second section (#2078) covers the opposite drift — items the manifest does declare
but the session ledger has never seen invoked
(ledger.js unused). Each carries a why: line from its frontmatter and a try: command
(the same description bob tour uses, #2075):
gap-detect: 1 provisioned item(s) never invoked (session ledger, all time): agents triage-bot why: Triage incoming issues. try: ask Claude to use the triage-bot agentExit 3 when either section is non-empty. A project with no ledger events at all skips the
section (unused_skipped: "no ledger events" in --json) — no evidence is not evidence of
disuse — and --no-unused opts out.
/vision runs it after writing or updating docs/vision.md; /groom runs it once per
reconciliation pass (step 2c). Both print the nudges verbatim and never apply them — the
manifest is edited only by bob provision add or bob provision reconcile --apply. make test-gap-detect
covers it alongside match.
bob provision flags for the pipeline
Section titled “bob provision flags for the pipeline”| Invocation | What it does | Writes? |
|---|---|---|
bob provision --init |
Infer the profile (default), recommend, stage the manifest for review (#785) | manifest (staged) |
bob provision --init --auto |
Same, applied directly in one shot, then refreshed; prints a why: line per item |
manifest |
bob provision --init --no-infer |
Stack-only input — detect_stack + infer_project_type, no capabilities |
manifest |
bob provision --init --from-links |
Derive the manifest from the project’s existing links into BOB_HOME instead of inferring it: registry links become items, bob init universal links are excluded, everything else is listed unmapped with a reason; --json emits bob-cdprov-from-links/1 (#2065) |
no |
bob provision --init --from-links --apply [--force] |
Write that manifest — staged review PR (#785), --now, or ungated; an existing different manifest only with --force, an identical one is a no-op; never refreshes |
manifest (routed) |
bob provision --diff |
Link diff + Recommendation (inferred): would-add with why:, would-remove with reason |
no |
bob provision --diff --json |
The explain.js document |
no |
bob provision reconcile / --diff --infer |
The would-add / would-remove delta against the manifest | no |
bob provision reconcile --json |
The same delta as the explain document | no |
bob provision reconcile --apply |
Apply adds (single item L2+ → fast path; multi-item / L1 → review PR); removals withheld | manifest (routed) |
bob provision reconcile --apply --allow-remove |
Also apply would-remove items | manifest (routed) |
bob provision detect |
Stack + project type as JSON — the stack-only input, for tooling | no |
--no-infer is honoured by every inferring invocation (--init, --diff, reconcile). A
would-remove is never applied by --init, --init --auto, --diff or a bare
reconcile --apply.
Interview question bank (#161)
Section titled “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
Section titled “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/filenamedescription: One-line summary. # requiredapplies_to: stacks: [<from the schema enum>] # auto-include for these tech stacks project_types: [api, backend, ...] # auto-include for these archetypesrecommended_for: [<activity categories>] # capability-driven inclusioncategory: <ui-grouping> # how the interview groups this itemrequired_with: [skills/some-other] # implies these other itemsdefault: 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
Section titled “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
Section titled “Troubleshooting”“I asked for X but didn’t get item Y”
Section titled ““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”
Section titled ““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”
Section titled ““Same inputs produce different manifests””That should never happen. The engine sorts items by name within each kind and its output is fully deterministic (since #1269 there is no timestamp or other volatile field). Diff the two outputs directly 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”
Section titled ““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”
Section titled ““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)
Section titled “Pure-CLI interview (#163)”bob provision --interview (or the PATH-friendly bob provision --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 |
bob provision --interview # interactivebob provision --interview --yes # skip the confirm step (interview still runs)bob provision --interview --non-interactive < answers.txt # CI / scripted
bob provision --interview # same thing, PATH-friendlyCI mode (--non-interactive)
Section titled “CI mode (--non-interactive)”Stdin is read line-by-line in question order:
project_typelifecycle_stagecapabilities (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)
Section titled “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) |
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
Section titled “Library”const { diffManifests, formatDiff } = require('./diff');const d = diffManifests(proposed, existing); // pure function, no I/Oif (d.no_diff) { /* skip the confirm prompt */ }process.stdout.write(formatDiff(d));Manual-preservation contract
Section titled “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)
Section titled “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:
bob provision --interviewfor terminal use. Reads questions.json, prompts viagum(withfzf/whiptailfallbacks), and pipes inputs to the engine. Same output shape as #162. - #506 ✅ — Retired
stack_to_items()inscripts/provision.shand routed--init(and the one-shot--init --auto) through the engine, so--initand--interviewrecommend identical items from identical metadata. - #1877 / #1878 ✅ — Profile inference (
infer-profile.js) andrecommend.js --profile/--infer;bob provision --initfeeds the inferred profile by default (--no-infer= stack-only). Fixture projects pin the outcomes (#1882). - #1879 ✅ — Explainable provisioning:
explain.jswhy:lines from evidence,would-removefor unsupported manifest items (report-only, never silent), surfaced bybob provision --init --autoandbob provision --diff/--diff --json. - #1881 ✅ —
gap-detect.js divergence: the inferred-profile nudge for unprovisioned registry items (vision now mentions … — registry item … covers it+bob provision addfix), run by/visionafter a vision write and by/groomonce per pass; observable-only. - #1880 ✅ —
bob provision reconcile [--apply] [--allow-remove]: re-infer and apply the manifest delta through the staged-PR / fast-path route; dry run by default, idempotent, removals never applied without--allow-remove. - (follow-up) — A recorded asciinema demo for the guide.
See also
Section titled “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—detect_stack+infer_project_type;--init/--init --autoroute through the engine (#506,stack_to_itemsretired)- PROJECT-ORCHESTRATION-HANDBOOK §3.4 — manifest provisioning workflow