Skill Registry¶
How Skills and Workflow definitions are registered, how Workflows resolve by stable identity and executable aliases, and how probe-disabled Skills recover without a module reload.
Details¶
What¶
The shared registry in work_buddy/mcp_server/registry.py has distinct lookup roles:
_REGISTRYcontains active Skills under Skill names and Workflows under their canonicalworkflow_name._WORKFLOW_ID_INDEXresolves stableworkflow_idvalues._WORKFLOW_ALIAS_INDEXresolves canonical and historicalworkflow_aliasesaddresses._DISABLED_SKILL_REGISTRYholds Skills removed from active dispatch because a build-time tool probe failed.
At build time, the requirements filter moves a Skill whose probe failed from _REGISTRY to _DISABLED_SKILL_REGISTRY and adds a row to work_buddy.tools.DISABLED_SKILLS listing the missing tools. Workflows are not moved into the disabled-Skill map: their modes, preferences, component requirements, executor facilities, and authorization are evaluated for each invocation by WorkflowAdmission.
The Obsidian-bridge tool family is the one exception. When the bridge itself is down, the filter skips not just obsidian but every tool that transitively depends on it in the probe graph — the in-Obsidian plugins datacore, google_calendar (work_buddy.tools.obsidian_backed_tools()). The bridge is a transiently-flaky shared dependency, not a genuinely-absent one, so those skills stay admitted and are governed at runtime by a circuit breaker on gateway dispatch (see architecture/resilience and work_buddy/mcp_server/dispatch_resilience.py): they fail fast per call while the bridge is down and recover the instant it returns — no session-long disable, no reload. This carve-out is transitive-only: it applies only when the bridge itself is down. If the bridge is up but a plugin is genuinely missing (e.g. datacore not installed), that plugin's skills still hard-disable here. The build-time disable below therefore applies to genuinely-absent dependencies (a missing plugin while the bridge is up, hindsight, thunderbird, ...).
Disabled state is cached. A skill disabled by a transient probe failure (e.g. the hindsight memory service unreachable for 200ms during sidecar startup) stays disabled until something explicitly re-probes it.
Skill schema (selected fields)¶
name,description,parameters,callable— the dispatch surface.requires: list[str]— tool IDs the dispatcher gates on.mutates_state: bool,retry_policy: "manual" | "replay" | "verify_first"— inform the gateway's auto-enqueue policy.consent_operations: list[str]— declarations for the gateway's pre-flight consent bundling.op_id: str | None— set when the skill was resolved from an inert declaration rather than instantiated directly (see "Declaration-based skills" below);Nonefor directly registered skills.effects: list[EffectSpec]— manifest of externally-visible effects for skills that produce more than one. When non-empty, the post-write-verify recovery path usesverify_post_write_effects(walks every declared effect; can returnpartial) instead of single-effect verify. Skills with declared effects MUST be idempotent under retry. Schema lives atwork_buddy.obsidian.effects.EffectSpec; recovery semantics inarchitecture/retry-queue.timeout_seconds: float | None | Callable[[params], float | None]— the wall-time budget for one gateway dispatch, owned by the operation (never the caller). The gateway timeout is opt-in: a scalar is a fixed ceiling, a callable derives the budget from the actual params (for operations whose runtime scales with input), and unset (None) is unbounded — the gateway imposes no cap. No flat default is applied: skills that deliberately block or run long (human-in-the-looprequest_send/request_poll, theobsidian_retry/retrywrappers,llm_submit) would be wrongly cut off by a too-low default, so a real default must be calibrated from observed dispatch p99 and paired with explicit exemptions. Resolved at dispatch inwork_buddy/mcp_server/dispatch_resilience.py; a skill that declares a finite budget and overruns it getserror_kind="mcp_gateway_timeout".
Compiled Workflow identity¶
A compiled WorkflowDefinition carries:
- stable
workflow_idand deterministicworkflow_revision; - canonical
name, human-facingdisplay_name, and executablealiases; - the full transitive
requiresclosure; required_components, which omits dependencies reachable only through optional Steps.
get_entry(address) resolves a canonical Workflow name, executable alias, or stable ID. Exact search_registry lookups use the same resolver and return identity, revision, display name, aliases, and Step target metadata.
Registry construction rejects duplicate definition IDs, duplicate executable addresses, addresses in the reserved wfd_ namespace, Workflow address/ID collisions, and collisions between Workflow addresses or IDs and Skill names. An authored Workflow reference is compiled from its source address to WorkflowStep.target_workflow_id. Dependency compilation follows Skill invokes chains and referenced Workflows transitively, including dependencies of disabled Skills, so filtering a child Skill cannot hide a required component from Workflow admission.
Two recovery paths — use the right one¶
Per-skill re-probe (preferred for runtime recovery)¶
work_buddy.recovery.recheck_disabled_skill(name, *, force=False) re-probes ONLY the skill's missing tools, with per-tool cool-down (default 30s, env-overridable via WB_RECHECK_COOLDOWN_SECS), single _RECOVERY_LOCK (RLock) for concurrent-caller safety. On success, mutates _REGISTRY in place to restore the skill. No rebuild, no module purge.
Returns True if the skill is now in the live registry, False if it remains disabled (with DISABLED_SKILLS[name] updated to reflect any partially recovered tools).
Companion: recheck_tool(tool_id, *, force=False) for re-probing a single tool without scoping to a skill. Same cool-down, same lock.
Used by:
- The gateway's wb_run dispatch path (work_buddy/mcp_server/tools/gateway.py). On hitting a disabled skill, the gateway calls recheck_disabled_skill before returning the disabled error.
- The sidecar's retry sweep _replay (work_buddy/sidecar/retry_sweep.py). On hitting a disabled skill during a queued retry, the sweep calls recheck_disabled_skill rather than reporting "not found in registry". It falls back to invoking the disabled entry's callable when recheck still says no, since the bridge call inside raises a typed transient exception and the operation re-queues correctly.
Data-only registry reload (declaration / workflow / param-schema changed)¶
reload_skill_data skill (calls reload_skill_data() in registry.py) resets the knowledge-store cache and clears _REGISTRY, then rebuilds in place via get_registry() — WITHOUT purging sys.modules. Because no module is re-imported, Skill / WorkflowDefinition class identity stays stable and the long-lived FastMCP gateway reads the rebuilt registry directly. It costs about 6–8 seconds because _build_registry re-probes every tool.
Use when you edited or added a Skill declaration or a Workflow unit and want it live without a restart. For Workflows the rebuild refreshes canonical entries, stable-ID and alias indexes, compiled references, dependency closures, revisions, and parameter schemas. It also re-enables a Skill whose tool just came back because the rebuild re-probes and re-runs the requirements filter.
It does NOT pick up edited Op code or a brand-new Op module — re-importing Python is what a process restart (Ctrl+R) does safely.
Retired:
mcp_registry_reload(the functioninvalidate_registry()lives on, dormant) purgedwork_buddy.*fromsys.modulesto pick up code. In the long-lived FastMCP gateway that silently did nothing —wb_run/wb_searchare frozen against the boot module generation, so the rebuilt registry never reached dispatch, while the purge corrupted the then-namedCapabilityclass identity. It was removed from the agent surface; usereload_skill_datafor data and aCtrl+Rrestart for code. Seedev/mcp-reloadand.data/designs/mcp-registry-reload.
Do NOT use reload_skill_data for transient probe failures. The dispatch path already auto-recovers a disabled skill via recheck_disabled_skill (a per-skill re-probe); the full rebuild is heavier than needed for that.
Decision tree¶
Skill is disabled / not found in active registry
- Declaration / workflow / param-schema changed -> reload_skill_data
- Op code or new Op module changed -> restart the gateway (Ctrl+R)
- Probe transient-failed -> recheck_disabled_skill(name)
- Returns True -> skill is back in _REGISTRY, proceed
- Returns False -> tools still down
- Caller wants to retry later -> re-queue / surface
- Caller can run the skill anyway ->
invoke disabled_entry.callable(...)
(typed bridge exception -> @bridge_retry handles)
Declaration-based skills¶
Not every skill is a Skill(...) instance written directly in registry.py. A skill can also be an inert declaration in the knowledge store that names an Op (a callable registered by ID in the Op registry). The skill loader resolves declarations at registry-build time and merges the resulting Skill objects into _REGISTRY alongside directly registered ones — a declared skill is indistinguishable at dispatch time except for its op_id field. See architecture/data-first-skills for the Op registry, loader, and load-time validation.
Key files¶
work_buddy/mcp_server/registry.py— shared Skill/Workflow registry, stable-ID and alias indexes, compiled Workflow references and dependencieswork_buddy/mcp_server/op_registry.py— Op registry backing declaration-based skills (seearchitecture/data-first-skills)work_buddy/knowledge/skill_loader.py— resolves skill declarations against the Op registrywork_buddy/workflows/identity.py— definition-ID grammar, generation, read-compat derivation, and deterministic revision hashingwork_buddy/workflows/admission.py— invocation-time Workflow policywork_buddy/workflows/service.py— application façade used by driving adapterswork_buddy/recovery.py—recheck_disabled_skill,recheck_tool,_RECOVERY_LOCK,_LAST_RECHECK_ATwork_buddy/obsidian/effects.py—EffectSpecschema for theSkill.effectsmanifestwork_buddy/obsidian/post_write_verify.py—verify_post_write_effectswalkerwork_buddy/tools.py—DISABLED_SKILLS,is_tool_available,reprobe_onework_buddy/mcp_server/tools/gateway.py— dispatch path with effects-aware PWU handlerwork_buddy/sidecar/retry_sweep.py— sweep path with disabled-cap recovery and effects-aware pre-verify
When in doubt¶
Per-skill re-probe is almost always right at runtime. reload_skill_data is for data changes (declarations, workflows, param schemas) and full inventory rebuilds; an Op code change or a new Op module needs a gateway restart (Ctrl+R). The 30-second cool-down on each skill stops aggressive callers from hammering a genuinely-down tool.