You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Core cleanup: remove the vestigial enabled flag and enable_skill/disable_skill from the skill model
1. Problem statement
The fastskill-core crate's skill model carries an enabled: bool field on SkillDefinition, an enabled: Option<bool> field on both SkillUpdate and the event-bus-local SkillUpdate struct, an enabled: Option<bool> filter on SkillFilters, enable_skill and disable_skill methods on the SkillManagementService trait and its SkillManager implementation, corresponding SkillEnabled/SkillDisabled event variants in SkillEvent, and a mirrored enabled: bool field on SkillMetadata. The disable CLI command that drove this concept was retired as part of the CLI command-surface redesign (PR #185 / issue #183); the enable command was intentionally never surfaced. With no callers remaining in the CLI or any other crate, these symbols are dead weight that misrepresents the data model — a future developer reading the trait will incorrectly infer that a "disabled-but-installed" skill lifecycle exists. This issue removes all of that dead code in one cohesive change. Out of scope: removal of HotReloadConfig.enabled (an unrelated feature flag), any changes to the lock-file format, and any UI or server-side changes beyond removing the "enabled" key from the skill_metadata_json helper.
2. Goals
enable_skill and disable_skill MUST be removed from the SkillManagementService trait and from the SkillManager implementation.
SkillDefinition.enabled: bool MUST be removed from the struct definition and its new() constructor.
SkillUpdate.enabled: Option<bool> MUST be removed from both the skill_manager module's SkillUpdate and the event-bus module's SkillUpdate.
SkillFilters.enabled: Option<bool> MUST be removed, and the enabled-based retain filter in list_skills MUST be removed.
SkillMetadata.enabled: bool MUST be removed, including its population in the From<&SkillDefinition> conversion.
SkillEvent::SkillEnabled and SkillEvent::SkillDisabled variants MUST be removed from the enum and from all match arms that cover it (notify_handlers, LoggingEventHandler, MetricsEventHandler).
EventBus::publish_skill_enabled and EventBus::publish_skill_disabled convenience methods MUST be removed.
The "enabled" key MUST be removed from the skill_metadata_json helper in the HTTP handler.
A workspace-wide search for enable_skill, disable_skill, SkillEnabled, SkillDisabled, and \.enabled (scoped to skill contexts) MUST return no hits after the change.
The workspace MUST compile with zero dead-code warnings attributable to this removal, and all existing tests MUST pass.
Loading a skill whose SkillFrontmatter YAML contains an unrecognised enabled key MUST NOT produce an error (backward-compatible parsing, already satisfied by #[serde(flatten)] on SkillFrontmatter.extra; this MUST be verified by a test fixture).
3. Current behavior
Component
Role
Mutable at runtime?
SkillDefinition.enabled: bool
Runtime flag indicating whether a skill is considered active; defaults to true
Yes — via update_skill with SkillUpdate { enabled: Some(…) }
SkillManagementService::enable_skill
Trait method; sets enabled = true via update_skill
N/A (method)
SkillManagementService::disable_skill
Trait method; sets enabled = false via update_skill
N/A (method)
SkillFilters.enabled: Option<bool>
Optional predicate applied in list_skills; filters out skills whose enabled doesn't match
No
SkillUpdate.enabled: Option<bool> (skill_manager)
Patch field used to flip SkillDefinition.enabled
No
SkillUpdate.enabled: Option<bool> (event_bus)
Field on the event-bus-local SkillUpdate used in SkillEvent::SkillUpdated payloads
No
SkillMetadata.enabled: bool
Mirror of SkillDefinition.enabled; populated via From<&SkillDefinition>
No
SkillEvent::SkillEnabled / SkillDisabled
Event variants for skill lifecycle state changes; have matching handlers in LoggingEventHandler and MetricsEventHandler
Serialises "enabled": skill.enabled into the JSON blob embedded in SkillResponse.metadata
No
Current build and resume flow:SkillManager stores all skills in an in-memory Arc<RwLock<HashMap<SkillId, SkillDefinition>>>. There is no on-disk serialisation of SkillDefinition; the on-disk artefacts are skill-project.toml and skills.lock, neither of which carries an enabled field. Skills are reloaded into memory at startup from those manifests. Because SkillFrontmatter (parsed from SKILL.md) uses #[serde(flatten)] pub extra: HashMap<String, serde_yaml::Value>, any unknown YAML keys (including a legacy enabled: true) already go into the extra map without error — no additional tolerance work is needed.
4. Design
4.1 Removal strategy (dependency order)
Symbols MUST be removed in bottom-up dependency order to keep the workspace compilable at every step:
Remove enable_skill / disable_skill implementations from SkillManager.
Remove those methods from the SkillManagementService trait declaration.
Remove the enabled field from SkillUpdate (skill_manager module); remove the corresponding if let Some(enabled) branch in update_skill.
Remove SkillFilters.enabled and the retain block in list_skills. Because SkillFilters has no remaining fields after this removal, it MUST be removed entirely (Option B — user-confirmed design decision). The list_skills trait method signature changes from list_skills(&self, filters: Option<SkillFilters>) -> Result<Vec<SkillDefinition>, ServiceError> to list_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>. All call sites (9+) that previously passed None must drop the argument; any code importing SkillFilters must remove that import. See §4.3 for rationale.
Remove SkillDefinition.enabled and the enabled: true initializer in SkillDefinition::new.
Remove SkillMetadata.enabled and enabled: skill.enabled in the From impl.
Remove SkillEvent::SkillEnabled, SkillEvent::SkillDisabled variants. Remove the corresponding arms in notify_handlers, LoggingEventHandler::handle_event, MetricsEventHandler::handle_event. Remove the SkillUpdate.enabled field in the event-bus module's own SkillUpdate. Remove EventBus::publish_skill_enabled and publish_skill_disabled.
Remove "enabled": skill.enabled from skill_metadata_json in http/handlers/skills.rs.
4.2 Concurrency / locking
No concurrency changes. All mutations to SkillManager continue to go through the existing Arc<RwLock<…>> write lock in update_skill. The removed methods were thin wrappers around update_skill; eliminating them reduces the write-lock surface.
4.3 SkillFilters invariant
After removal, SkillFilters contains zero fields. Two options were considered:
Option A: Retain the empty struct as a placeholder, keeping list_skills(filters: Option<SkillFilters>) signature unchanged. Avoids a breaking change; leaves an extension point for future filters.
Option B (chosen): Remove SkillFilters entirely, changing the signature to list_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>. Produces the simplest possible API: a parameter that currently carries no data is eliminated entirely.
Decision: Option B. All 9+ active call sites already pass None, so the only required change at each call site is dropping the argument — a mechanical update with no semantic impact. An empty struct with no fields is API noise; removing it makes the trait immediately understandable. Future filter predicates, if ever needed, can be reintroduced as a new method overload or a builder pattern at that time, rather than preserving an empty placeholder indefinitely. This is a trait-level breaking change and is documented in §11.
4.4 HTTP API surface
The "enabled" key is removed from the JSON object returned in SkillResponse.metadata. This is a breaking change to the HTTP API contract. Callers that key on metadata.enabled will receive undefined / absent instead of true. Since disabled-skill support is retired, there is no longer any meaningful boolean to convey; removing the key is correct.
SkillFrontmatter already uses #[serde(flatten)] pub extra: HashMap<String, serde_yaml::Value> at line 48 of metadata.rs. Any SKILL.md file whose YAML frontmatter contains enabled: true or enabled: false will silently absorb the unknown key into extra. No code changes are needed for this. A regression test MUST verify this behaviour (see §7, criterion 11).
5. Schema and API
5.1 Structs after removal
// crates/fastskill-core/src/core/skill_manager.rs#[derive(Debug,Clone,Serialize,Deserialize)]pubstructSkillDefinition{pubid:SkillId,pubname:String,pubdescription:String,pubversion:String,pubauthor:Option<String>,// `enabled: bool` REMOVEDpubcreated_at:DateTime<Utc>,pubupdated_at:DateTime<Utc>,pubskill_file: std::path::PathBuf,pubreference_files:Option<Vec<std::path::PathBuf>>,pubscript_files:Option<Vec<std::path::PathBuf>>,pubasset_files:Option<Vec<std::path::PathBuf>>,pubexecution_environment:Option<String>,pubdependencies:Option<Vec<String>>,pubtimeout:Option<u64>,pubsource_url:Option<String>,pubsource_type:Option<SourceType>,pubsource_branch:Option<String>,pubsource_tag:Option<String>,pubsource_subdir:Option<PathBuf>,pubinstalled_from:Option<String>,pubcommit_hash:Option<String>,pubfetched_at:Option<DateTime<Utc>>,pubeditable:bool,}// `SkillFilters` struct REMOVED ENTIRELY (Option B — see §4.3)#[derive(Debug,Clone,Default)]pubstructSkillUpdate{pubname:Option<String>,pubdescription:Option<String>,pubversion:Option<String>,pubauthor:Option<String>,// `enabled: Option<bool>` REMOVEDpubsource_url:Option<String>,pubsource_type:Option<SourceType>,pubsource_branch:Option<String>,pubsource_tag:Option<String>,pubsource_subdir:Option<PathBuf>,pubinstalled_from:Option<String>,pubcommit_hash:Option<String>,pubfetched_at:Option<DateTime<Utc>>,pubeditable:Option<bool>,}
This change introduces no new error codes. It removes two code paths that previously returned ServiceError::SkillNotFound — specifically the paths through enable_skill and disable_skill (which delegated to update_skill). No existing error code semantics change.
Code
Category
Trigger
Change
ServiceError::SkillNotFound
Not found
Returned by update_skill when skill_id is absent from the in-memory store
No change to the code itself; two previously reachable paths via enable_skill/disable_skill are eliminated
7. Acceptance criteria
SkillManagementService trait MUST NOT declare enable_skill or disable_skill.
SkillManager MUST NOT implement enable_skill or disable_skill.
SkillDefinition MUST NOT contain an enabled field.
SkillUpdate (skill_manager module) MUST NOT contain an enabled field.
SkillFilters MUST be removed entirely from the codebase (struct definition, public export, and all imports). list_skills MUST NOT apply any enabled-based retain filter, and its signature MUST be list_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError> with no filters parameter.
SkillMetadata MUST NOT contain an enabled field.
SkillEvent MUST NOT declare SkillEnabled or SkillDisabled variants.
EventBus MUST NOT expose publish_skill_enabled or publish_skill_disabled methods.
The event-bus-local SkillUpdate struct MUST NOT contain an enabled field.
The skill_metadata_json helper in http/handlers/skills.rs MUST NOT include an "enabled" key in its output.
A test fixture MUST demonstrate that deserialising a SkillFrontmatter from YAML containing an enabled: true key succeeds without error and the extra map contains the key.
A workspace-wide grep -rn "enable_skill\|disable_skill\|SkillEnabled\|SkillDisabled\|SkillFilters" across crates/ MUST return zero hits.
cargo build --workspace MUST succeed with zero dead-code warnings related to the removed symbols.
cargo test --workspace MUST pass with no regressions.
All call sites of list_skills MUST be updated to drop the filters argument (previously None everywhere): fastskill-cli/src/commands/list.rs:181, fastskill-cli/src/commands/read.rs:179,374, http/handlers/skills.rs:45,76,177, http/handlers/status.rs:64,241, http/handlers/search.rs:113, http/handlers/registry.rs:202,309, core/metadata.rs:232, lib.rs:130. Each call site MUST compile after removing the argument.
The SkillFilters struct MUST NOT exist anywhere in the workspace after this change (Option B from §4.3). The list_skills trait method signature MUST be list_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError> with no filters parameter. A workspace-wide grep -rn "SkillFilters" across crates/ MUST return zero hits.
8. Functionality comparison (before vs after)
Capability
Before
After
Install a skill
Skill registered with enabled: true by default
Skill registered; no enabled field
List skills
list_skills(Some(SkillFilters { enabled: Some(true) })) returns only enabled skills
list_skills() returns all installed skills; SkillFilters type removed; no filtering by enabled state
Disable a skill (CLI)
disable command existed (since retired in PR #185)
GET /api/skills response includes metadata.enabled: true/false
metadata.enabled key absent from response
Event bus
SkillEnabled and SkillDisabled events publishable
Events do not exist; publish_skill_enabled/disabled removed
Skill metadata struct
SkillMetadata.enabled field present
Field removed
9. Benefit of implementing this functionality
Removing the dead code eliminates a model-level fiction: the trait, struct fields, and event variants collectively imply that a "disabled-but-installed" lifecycle exists when it does not. Deleting them reduces cognitive overhead for contributors reading the code, lowers the surface area that must be kept in sync during future refactors (e.g. if SkillDefinition gains new fields), and ensures any future consumer of the public API (CLI, HTTP, embedding pipeline) does not mistakenly build a feature on top of an absent concept. It also produces a cleaner, smaller binary by eliminating unreachable code paths, and avoids the possibility of a new test or integration accidentally calling enable_skill/disable_skill and inferring they are meaningful.
10. Stages
Stage 1 — Trait and impl removal (blocking for all subsequent stages)
Deliverable:crates/fastskill-core/src/core/skill_manager.rs compiles with enable_skill and disable_skill removed from both the trait and SkillManager impl.
Remove the enable_skill implementation body from SkillManager (lines 264–273).
Remove the disable_skill implementation body from SkillManager (lines 275–284).
Dependency: None (first stage).
Stage 2 — Remove enabled from SkillUpdate, remove SkillFilters entirely, update all list_skills call sites (depends on Stage 1)
Deliverable:SkillUpdate no longer carries enabled; SkillFilters is deleted entirely; list_skills signature is list_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>; all 9+ call sites compile with the argument dropped.
Scope:
Remove pub enabled: Option<bool> from SkillUpdate in skill_manager.rs (line 140).
Remove the if let Some(enabled) = updates.enabled { skill.enabled = enabled; } branch from update_skill in skill_manager.rs (lines 200–202).
Remove the entire SkillFilters struct from skill_manager.rs (line 129–132) — not just the enabled field, but the struct declaration itself.
Remove the if let Some(enabled) = filters.enabled { filtered_skills.retain(|skill| skill.enabled == enabled); } block from list_skills in skill_manager.rs (lines 254–259).
Update the list_skills signature in the SkillManagementService trait and SkillManager impl to list_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError> (drop filters: Option<SkillFilters> parameter).
Remove SkillFilters from the public re-export in crates/fastskill-core/src/core/mod.rs (line 112).
Update all call sites to drop the filters argument:
Dependency: Stage 1 must be complete (the enabled-update path was called only by enable_skill/disable_skill, now absent).
Stage 3 — Remove enabled from SkillDefinition (depends on Stage 2)
Deliverable:SkillDefinition struct and constructor no longer carry or initialise enabled.
Scope:
Remove pub enabled: bool field from SkillDefinition struct (line 30 of skill_manager.rs).
Remove enabled: true, from SkillDefinition::new() (line 66 of skill_manager.rs).
Dependency: Stage 2 must be complete; the enabled field is no longer read or written after Stage 2 removals.
Stage 4 — Remove enabled from SkillMetadata (depends on Stage 3)
Deliverable:SkillMetadata struct and its From<&SkillDefinition> impl no longer carry enabled.
Scope:
Remove pub enabled: bool from SkillMetadata in metadata.rs (line 16).
Remove enabled: skill.enabled, from the From<&SkillDefinition> for SkillMetadata impl in metadata.rs (line 29).
Dependency: Stage 3 must be complete; skill.enabled would be a compile error once the field is removed from SkillDefinition.
Stage 5 — Remove event-bus symbols (depends on Stage 3)
Deliverable:SkillEvent, EventBus, and the event-bus-local SkillUpdate no longer carry enabled-related symbols; all match arms are exhaustive.
Scope:
Remove SkillEnabled { skill_id: String } variant from SkillEvent enum in event_bus.rs (line 53).
Remove SkillDisabled { skill_id: String } variant from SkillEvent enum in event_bus.rs (line 56).
Remove pub enabled: Option<bool> from the event-bus-local SkillUpdate struct in event_bus.rs (line 71).
Remove the SkillEvent::SkillEnabled { .. } => "skill:enabled" arm from notify_handlers in event_bus.rs (line 207).
Remove the SkillEvent::SkillDisabled { .. } => "skill:disabled" arm from notify_handlers in event_bus.rs (line 208).
Remove the SkillEvent::SkillEnabled { skill_id } arm from LoggingEventHandler::handle_event in event_bus.rs (lines 303–305).
Remove the SkillEvent::SkillDisabled { skill_id } arm from LoggingEventHandler::handle_event in event_bus.rs (lines 306–308).
Remove the SkillEvent::SkillEnabled { .. } => "skill:enabled".to_string() arm from MetricsEventHandler::handle_event in event_bus.rs (line 352).
Remove the SkillEvent::SkillDisabled { .. } => "skill:disabled".to_string() arm from MetricsEventHandler::handle_event in event_bus.rs (line 353).
Remove EventBus::publish_skill_enabled method in event_bus.rs (lines 438–442).
Remove EventBus::publish_skill_disabled method in event_bus.rs (lines 444–448).
Dependency: Stage 3 must be complete (event variants referenced SkillDefinition indirectly through publish helpers; no compile blocker, but logical dependency on model being clean first).
Stage 6 — Remove enabled from HTTP handler (depends on Stage 3)
Deliverable:skill_metadata_json helper no longer serialises "enabled" into the JSON response.
Scope:
Remove "enabled": skill.enabled, from the serde_json::json! block in skill_metadata_json in http/handlers/skills.rs (line 29).
Dependency: Stage 3 must be complete; skill.enabled is a compile error once the field is removed.
Stage 7 — Add backward-compat test and final verification (depends on Stages 1–6)
Deliverable: A test fixture confirms that YAML frontmatter containing a legacy enabled field is parsed without error.
Scope:
Add a unit test in crates/fastskill-core/src/core/metadata.rs (or a new #[cfg(test)] block in that file) that calls parse_yaml_frontmatter with YAML content that includes enabled: true in the frontmatter and asserts the call succeeds and the returned SkillFrontmatter.extra contains the "enabled" key.
Run cargo build --workspace and confirm zero dead-code warnings related to removed symbols.
Run cargo test --workspace and confirm all tests pass.
Run grep -rn "enable_skill\|disable_skill\|SkillEnabled\|SkillDisabled\|SkillFilters" crates/ and confirm zero hits.
Dependency: All of Stages 1–6.
11. Breaking changes
Change
Affected surface
Impact
enable_skill removed from SkillManagementService trait
Any downstream crate that implements the trait or calls the method
Callers must remove their call sites; trait implementations must remove the method; compile error if not updated
disable_skill removed from SkillManagementService trait
GET /api/skills and GET /api/skills/{id} HTTP responses
Clients consuming metadata.enabled
Runtime break: key absent from JSON; no compile-time signal
12. Modified files and summary
File / path
Summary of modifications
crates/fastskill-core/src/core/skill_manager.rs
Remove enabled: bool from SkillDefinition; remove enabled: true from SkillDefinition::new(); remove enabled: Option<bool> from SkillUpdate; remove if let Some(enabled) = updates.enabled branch in update_skill; remove SkillFilters struct entirely; remove enabled-based retain block in list_skills; change list_skills signature to list_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>; remove enable_skill and disable_skill from SkillManagementService trait and SkillManager impl
crates/fastskill-core/src/core/metadata.rs
Remove enabled: bool from SkillMetadata; remove enabled: skill.enabled from From<&SkillDefinition>; update list_skills() call at line 232 (drop argument); add #[cfg(test)] block with backward-compat frontmatter parse test
crates/fastskill-core/src/events/event_bus.rs
Remove SkillEnabled and SkillDisabled variants from SkillEvent; remove enabled: Option<bool> from event-bus-local SkillUpdate; remove match arms for SkillEnabled/SkillDisabled in notify_handlers, LoggingEventHandler, MetricsEventHandler; remove publish_skill_enabled and publish_skill_disabled from EventBus
crates/fastskill-core/src/http/handlers/skills.rs
Remove "enabled": skill.enabled, from skill_metadata_json; update list_skills() calls at lines 45, 76, 177 (drop argument)
crates/fastskill-core/src/http/handlers/status.rs
Update list_skills() calls at lines 64, 241 (drop argument)
crates/fastskill-core/src/http/handlers/search.rs
Update list_skills() call at line 113 (drop argument)
Update list_skills() calls at lines 202, 309 (drop argument)
crates/fastskill-core/src/core/mod.rs
Remove SkillFilters from the public re-export (line 112); other re-exports automatically reflect struct changes
crates/fastskill-core/src/lib.rs
Update list_skills() call at line 130 (drop argument); re-exports of SkillDefinition, SkillManagementService automatically reflect changes
crates/fastskill-cli/src/commands/list.rs
Update list_skills() call at line 181 (drop argument)
crates/fastskill-cli/src/commands/read.rs
Update list_skills() calls at lines 179, 374 (drop argument)
No existing test files in crates/fastskill-cli/tests/ (lock_migration_test.rs, lock_determinism_test.rs, lock_scope_routing_test.rs) reference skill-enabled symbols; they operate on lock-file logic and do not require modification.
If this change lands before #183 merges, disable command would fail to compile because its call to disable_skill would be unresolvable
async-trait crate
External library
Non-blocking
N/A — already in use
N/A
serde / serde_yaml crates
External library
Non-blocking
Stage 7 (test)
N/A — already in use
No team dependencies
—
—
—
—
14. Proposed folder structure
This is not a new project. The files listed in §12 are modified. No new files are created except optionally a test fixture added inline via #[cfg(test)] in metadata.rs. No folder-structure changes are required.
Should SkillFilters be removed entirely or kept as an empty struct?
Remove entirely (Option B — user-confirmed)
A struct carrying zero fields is API noise. All 9+ call sites already pass None, so dropping the argument is a mechanical one-line change at each site. The simplest possible API (list_skills(&self)) is immediately understandable, and future filter predicates can be introduced as a new method or builder at that time rather than indefinitely preserving an empty placeholder
2
Should the SkillEvent::SkillUpdated variant's changes: SkillUpdate payload be kept (minus enabled)?
Yes, keep the variant and payload
SkillUpdated is a meaningful general-purpose event; removing it would be a separate, broader change beyond this issue's scope
3
Should a changelog entry be added?
No
The spec explicitly states no changelog entry is needed beyond what the CLI removal (PR #185) already covered
4
Should SkillDefinition remain Serialize + Deserialize?
Yes
Other code paths (e.g. HTTP response helpers, test serialisation) may use these derives; removing them is not part of this issue
5
Should the backward-compat YAML test use a file fixture or an inline string?
Inline string in #[cfg(test)]
Simpler, no file-system dependency in unit tests; the frontmatter parsing code (parse_yaml_frontmatter) operates on &str
6
Should HotReloadConfig.enabled be removed as part of this change?
No
HotReloadConfig.enabled is an unrelated feature flag for the hot-reload subsystem and is actively used in service.rs (line 357) and http/handlers/status.rs (line 251); explicitly out of scope
16. Out of scope (explicit)
Removal of HotReloadConfig.enabled — this is an active feature flag for the hot-reload subsystem, not related to the skill-enabled concept.
Any changes to the lock file format (skills.lock) or the manifest format (skill-project.toml).
Adding a migration step or tooling to update existing installations.
Any UI changes to the server dashboard or web interface.
Removal of SkillEvent::SkillUpdated or the event-bus-local SkillUpdate struct (beyond the enabled field).
Changes to SkillFrontmatter (the extra map already handles unknown keys gracefully).
Adding new filter predicates to SkillFilters (that is a separate feature).
Changes to crates/fastskill-evals — a grep confirms no references to enabled-skill symbols there.
issue: 184
branch: feature/184-core-cleanup-remove-vestigial-enabled-flag-and-enable-skill
board_item_id: PVTI_lADODpSNOs4BRC0yzgu-Uvc
source: github-project
Core cleanup: remove the vestigial
enabledflag andenable_skill/disable_skillfrom the skill model1. Problem statement
The
fastskill-corecrate's skill model carries anenabled: boolfield onSkillDefinition, anenabled: Option<bool>field on bothSkillUpdateand the event-bus-localSkillUpdatestruct, anenabled: Option<bool>filter onSkillFilters,enable_skillanddisable_skillmethods on theSkillManagementServicetrait and itsSkillManagerimplementation, correspondingSkillEnabled/SkillDisabledevent variants inSkillEvent, and a mirroredenabled: boolfield onSkillMetadata. ThedisableCLI command that drove this concept was retired as part of the CLI command-surface redesign (PR #185 / issue #183); theenablecommand was intentionally never surfaced. With no callers remaining in the CLI or any other crate, these symbols are dead weight that misrepresents the data model — a future developer reading the trait will incorrectly infer that a "disabled-but-installed" skill lifecycle exists. This issue removes all of that dead code in one cohesive change. Out of scope: removal ofHotReloadConfig.enabled(an unrelated feature flag), any changes to the lock-file format, and any UI or server-side changes beyond removing the"enabled"key from theskill_metadata_jsonhelper.2. Goals
enable_skillanddisable_skillMUST be removed from theSkillManagementServicetrait and from theSkillManagerimplementation.SkillDefinition.enabled: boolMUST be removed from the struct definition and itsnew()constructor.SkillUpdate.enabled: Option<bool>MUST be removed from both theskill_managermodule'sSkillUpdateand the event-bus module'sSkillUpdate.SkillFilters.enabled: Option<bool>MUST be removed, and the enabled-basedretainfilter inlist_skillsMUST be removed.SkillMetadata.enabled: boolMUST be removed, including its population in theFrom<&SkillDefinition>conversion.SkillEvent::SkillEnabledandSkillEvent::SkillDisabledvariants MUST be removed from the enum and from all match arms that cover it (notify_handlers,LoggingEventHandler,MetricsEventHandler).EventBus::publish_skill_enabledandEventBus::publish_skill_disabledconvenience methods MUST be removed."enabled"key MUST be removed from theskill_metadata_jsonhelper in the HTTP handler.enable_skill,disable_skill,SkillEnabled,SkillDisabled, and\.enabled(scoped to skill contexts) MUST return no hits after the change.SkillFrontmatterYAML contains an unrecognisedenabledkey MUST NOT produce an error (backward-compatible parsing, already satisfied by#[serde(flatten)]onSkillFrontmatter.extra; this MUST be verified by a test fixture).3. Current behavior
SkillDefinition.enabled: booltrueupdate_skillwithSkillUpdate { enabled: Some(…) }SkillManagementService::enable_skillenabled = trueviaupdate_skillSkillManagementService::disable_skillenabled = falseviaupdate_skillSkillFilters.enabled: Option<bool>list_skills; filters out skills whoseenableddoesn't matchSkillUpdate.enabled: Option<bool>(skill_manager)SkillDefinition.enabledSkillUpdate.enabled: Option<bool>(event_bus)SkillUpdateused inSkillEvent::SkillUpdatedpayloadsSkillMetadata.enabled: boolSkillDefinition.enabled; populated viaFrom<&SkillDefinition>SkillEvent::SkillEnabled / SkillDisabledLoggingEventHandlerandMetricsEventHandlerEventBus::publish_skill_enabled / publish_skill_disabledskill_metadata_jsonhelper"enabled": skill.enabledinto the JSON blob embedded inSkillResponse.metadataCurrent build and resume flow:
SkillManagerstores all skills in an in-memoryArc<RwLock<HashMap<SkillId, SkillDefinition>>>. There is no on-disk serialisation ofSkillDefinition; the on-disk artefacts areskill-project.tomlandskills.lock, neither of which carries anenabledfield. Skills are reloaded into memory at startup from those manifests. BecauseSkillFrontmatter(parsed from SKILL.md) uses#[serde(flatten)] pub extra: HashMap<String, serde_yaml::Value>, any unknown YAML keys (including a legacyenabled: true) already go into theextramap without error — no additional tolerance work is needed.4. Design
4.1 Removal strategy (dependency order)
Symbols MUST be removed in bottom-up dependency order to keep the workspace compilable at every step:
enable_skill/disable_skillimplementations fromSkillManager.SkillManagementServicetrait declaration.enabledfield fromSkillUpdate(skill_manager module); remove the correspondingif let Some(enabled)branch inupdate_skill.SkillFilters.enabledand theretainblock inlist_skills. BecauseSkillFiltershas no remaining fields after this removal, it MUST be removed entirely (Option B — user-confirmed design decision). Thelist_skillstrait method signature changes fromlist_skills(&self, filters: Option<SkillFilters>) -> Result<Vec<SkillDefinition>, ServiceError>tolist_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>. All call sites (9+) that previously passedNonemust drop the argument; any code importingSkillFiltersmust remove that import. See §4.3 for rationale.SkillDefinition.enabledand theenabled: trueinitializer inSkillDefinition::new.SkillMetadata.enabledandenabled: skill.enabledin theFromimpl.SkillEvent::SkillEnabled,SkillEvent::SkillDisabledvariants. Remove the corresponding arms innotify_handlers,LoggingEventHandler::handle_event,MetricsEventHandler::handle_event. Remove theSkillUpdate.enabledfield in the event-bus module's ownSkillUpdate. RemoveEventBus::publish_skill_enabledandpublish_skill_disabled."enabled": skill.enabledfromskill_metadata_jsoninhttp/handlers/skills.rs.4.2 Concurrency / locking
No concurrency changes. All mutations to
SkillManagercontinue to go through the existingArc<RwLock<…>>write lock inupdate_skill. The removed methods were thin wrappers aroundupdate_skill; eliminating them reduces the write-lock surface.4.3 SkillFilters invariant
After removal,
SkillFilterscontains zero fields. Two options were considered:list_skills(filters: Option<SkillFilters>)signature unchanged. Avoids a breaking change; leaves an extension point for future filters.SkillFiltersentirely, changing the signature tolist_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>. Produces the simplest possible API: a parameter that currently carries no data is eliminated entirely.Decision: Option B. All 9+ active call sites already pass
None, so the only required change at each call site is dropping the argument — a mechanical update with no semantic impact. An empty struct with no fields is API noise; removing it makes the trait immediately understandable. Future filter predicates, if ever needed, can be reintroduced as a new method overload or a builder pattern at that time, rather than preserving an empty placeholder indefinitely. This is a trait-level breaking change and is documented in §11.4.4 HTTP API surface
The
"enabled"key is removed from the JSON object returned inSkillResponse.metadata. This is a breaking change to the HTTP API contract. Callers that key onmetadata.enabledwill receiveundefined/ absent instead oftrue. Since disabled-skill support is retired, there is no longer any meaningful boolean to convey; removing the key is correct.4.5 Backward-compatible deserialization (SKILL.md frontmatter)
SkillFrontmatteralready uses#[serde(flatten)] pub extra: HashMap<String, serde_yaml::Value>at line 48 ofmetadata.rs. Any SKILL.md file whose YAML frontmatter containsenabled: trueorenabled: falsewill silently absorb the unknown key intoextra. No code changes are needed for this. A regression test MUST verify this behaviour (see §7, criterion 11).5. Schema and API
5.1 Structs after removal
5.2 Trait after removal
5.3 SkillMetadata after removal
5.4 SkillEvent after removal
5.5 Removed EventBus methods
6. Error codes
This change introduces no new error codes. It removes two code paths that previously returned
ServiceError::SkillNotFound— specifically the paths throughenable_skillanddisable_skill(which delegated toupdate_skill). No existing error code semantics change.ServiceError::SkillNotFoundupdate_skillwhenskill_idis absent from the in-memory storeenable_skill/disable_skillare eliminated7. Acceptance criteria
SkillManagementServicetrait MUST NOT declareenable_skillordisable_skill.SkillManagerMUST NOT implementenable_skillordisable_skill.SkillDefinitionMUST NOT contain anenabledfield.SkillUpdate(skill_manager module) MUST NOT contain anenabledfield.SkillFiltersMUST be removed entirely from the codebase (struct definition, public export, and all imports).list_skillsMUST NOT apply any enabled-basedretainfilter, and its signature MUST belist_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>with nofiltersparameter.SkillMetadataMUST NOT contain anenabledfield.SkillEventMUST NOT declareSkillEnabledorSkillDisabledvariants.EventBusMUST NOT exposepublish_skill_enabledorpublish_skill_disabledmethods.SkillUpdatestruct MUST NOT contain anenabledfield.skill_metadata_jsonhelper inhttp/handlers/skills.rsMUST NOT include an"enabled"key in its output.SkillFrontmatterfrom YAML containing anenabled: truekey succeeds without error and theextramap contains the key.grep -rn "enable_skill\|disable_skill\|SkillEnabled\|SkillDisabled\|SkillFilters"acrosscrates/MUST return zero hits.cargo build --workspaceMUST succeed with zero dead-code warnings related to the removed symbols.cargo test --workspaceMUST pass with no regressions.list_skillsMUST be updated to drop thefiltersargument (previouslyNoneeverywhere):fastskill-cli/src/commands/list.rs:181,fastskill-cli/src/commands/read.rs:179,374,http/handlers/skills.rs:45,76,177,http/handlers/status.rs:64,241,http/handlers/search.rs:113,http/handlers/registry.rs:202,309,core/metadata.rs:232,lib.rs:130. Each call site MUST compile after removing the argument.SkillFiltersstruct MUST NOT exist anywhere in the workspace after this change (Option B from §4.3). Thelist_skillstrait method signature MUST belist_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>with nofiltersparameter. A workspace-widegrep -rn "SkillFilters"acrosscrates/MUST return zero hits.8. Functionality comparison (before vs after)
enabled: trueby defaultlist_skills(Some(SkillFilters { enabled: Some(true) }))returns only enabled skillslist_skills()returns all installed skills;SkillFilterstype removed; no filtering by enabled statedisablecommand existed (since retired in PR #185)service.skill_manager().enable_skill(&id)/disable_skill(&id)GET /api/skillsresponse includesmetadata.enabled: true/falsemetadata.enabledkey absent from responseSkillEnabledandSkillDisabledevents publishablepublish_skill_enabled/disabledremovedSkillMetadata.enabledfield present9. Benefit of implementing this functionality
Removing the dead code eliminates a model-level fiction: the trait, struct fields, and event variants collectively imply that a "disabled-but-installed" lifecycle exists when it does not. Deleting them reduces cognitive overhead for contributors reading the code, lowers the surface area that must be kept in sync during future refactors (e.g. if
SkillDefinitiongains new fields), and ensures any future consumer of the public API (CLI, HTTP, embedding pipeline) does not mistakenly build a feature on top of an absent concept. It also produces a cleaner, smaller binary by eliminating unreachable code paths, and avoids the possibility of a new test or integration accidentally callingenable_skill/disable_skilland inferring they are meaningful.10. Stages
Stage 1 — Trait and impl removal (blocking for all subsequent stages)
Deliverable:
crates/fastskill-core/src/core/skill_manager.rscompiles withenable_skillanddisable_skillremoved from both the trait andSkillManagerimpl.Scope:
async fn enable_skill(&self, skill_id: &SkillId) -> Result<(), ServiceError>;fromSkillManagementServicetrait (line 106).async fn disable_skill(&self, skill_id: &SkillId) -> Result<(), ServiceError>;fromSkillManagementServicetrait (line 107).enable_skillimplementation body fromSkillManager(lines 264–273).disable_skillimplementation body fromSkillManager(lines 275–284).Dependency: None (first stage).
Stage 2 — Remove
enabledfromSkillUpdate, removeSkillFiltersentirely, update alllist_skillscall sites (depends on Stage 1)Deliverable:
SkillUpdateno longer carriesenabled;SkillFiltersis deleted entirely;list_skillssignature islist_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>; all 9+ call sites compile with the argument dropped.Scope:
pub enabled: Option<bool>fromSkillUpdateinskill_manager.rs(line 140).if let Some(enabled) = updates.enabled { skill.enabled = enabled; }branch fromupdate_skillinskill_manager.rs(lines 200–202).SkillFiltersstruct fromskill_manager.rs(line 129–132) — not just theenabledfield, but the struct declaration itself.if let Some(enabled) = filters.enabled { filtered_skills.retain(|skill| skill.enabled == enabled); }block fromlist_skillsinskill_manager.rs(lines 254–259).list_skillssignature in theSkillManagementServicetrait andSkillManagerimpl tolist_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>(dropfilters: Option<SkillFilters>parameter).SkillFiltersfrom the public re-export incrates/fastskill-core/src/core/mod.rs(line 112).filtersargument:fastskill-cli/src/commands/list.rs:181fastskill-cli/src/commands/read.rs:179, 374crates/fastskill-core/src/http/handlers/skills.rs:45, 76, 177crates/fastskill-core/src/http/handlers/status.rs:64, 241crates/fastskill-core/src/http/handlers/search.rs:113crates/fastskill-core/src/http/handlers/registry.rs:202, 309crates/fastskill-core/src/core/metadata.rs:232crates/fastskill-core/src/lib.rs:130Dependency: Stage 1 must be complete (the enabled-update path was called only by
enable_skill/disable_skill, now absent).Stage 3 — Remove
enabledfromSkillDefinition(depends on Stage 2)Deliverable:
SkillDefinitionstruct and constructor no longer carry or initialiseenabled.Scope:
pub enabled: boolfield fromSkillDefinitionstruct (line 30 ofskill_manager.rs).enabled: true,fromSkillDefinition::new()(line 66 ofskill_manager.rs).Dependency: Stage 2 must be complete; the
enabledfield is no longer read or written after Stage 2 removals.Stage 4 — Remove
enabledfromSkillMetadata(depends on Stage 3)Deliverable:
SkillMetadatastruct and itsFrom<&SkillDefinition>impl no longer carryenabled.Scope:
pub enabled: boolfromSkillMetadatainmetadata.rs(line 16).enabled: skill.enabled,from theFrom<&SkillDefinition> for SkillMetadataimpl inmetadata.rs(line 29).Dependency: Stage 3 must be complete;
skill.enabledwould be a compile error once the field is removed fromSkillDefinition.Stage 5 — Remove event-bus symbols (depends on Stage 3)
Deliverable:
SkillEvent,EventBus, and the event-bus-localSkillUpdateno longer carry enabled-related symbols; all match arms are exhaustive.Scope:
SkillEnabled { skill_id: String }variant fromSkillEventenum inevent_bus.rs(line 53).SkillDisabled { skill_id: String }variant fromSkillEventenum inevent_bus.rs(line 56).pub enabled: Option<bool>from the event-bus-localSkillUpdatestruct inevent_bus.rs(line 71).SkillEvent::SkillEnabled { .. } => "skill:enabled"arm fromnotify_handlersinevent_bus.rs(line 207).SkillEvent::SkillDisabled { .. } => "skill:disabled"arm fromnotify_handlersinevent_bus.rs(line 208).SkillEvent::SkillEnabled { skill_id }arm fromLoggingEventHandler::handle_eventinevent_bus.rs(lines 303–305).SkillEvent::SkillDisabled { skill_id }arm fromLoggingEventHandler::handle_eventinevent_bus.rs(lines 306–308).SkillEvent::SkillEnabled { .. } => "skill:enabled".to_string()arm fromMetricsEventHandler::handle_eventinevent_bus.rs(line 352).SkillEvent::SkillDisabled { .. } => "skill:disabled".to_string()arm fromMetricsEventHandler::handle_eventinevent_bus.rs(line 353).EventBus::publish_skill_enabledmethod inevent_bus.rs(lines 438–442).EventBus::publish_skill_disabledmethod inevent_bus.rs(lines 444–448).Dependency: Stage 3 must be complete (event variants referenced
SkillDefinitionindirectly through publish helpers; no compile blocker, but logical dependency on model being clean first).Stage 6 — Remove
enabledfrom HTTP handler (depends on Stage 3)Deliverable:
skill_metadata_jsonhelper no longer serialises"enabled"into the JSON response.Scope:
"enabled": skill.enabled,from theserde_json::json!block inskill_metadata_jsoninhttp/handlers/skills.rs(line 29).Dependency: Stage 3 must be complete;
skill.enabledis a compile error once the field is removed.Stage 7 — Add backward-compat test and final verification (depends on Stages 1–6)
Deliverable: A test fixture confirms that YAML frontmatter containing a legacy
enabledfield is parsed without error.Scope:
crates/fastskill-core/src/core/metadata.rs(or a new#[cfg(test)]block in that file) that callsparse_yaml_frontmatterwith YAML content that includesenabled: truein the frontmatter and asserts the call succeeds and the returnedSkillFrontmatter.extracontains the"enabled"key.cargo build --workspaceand confirm zero dead-code warnings related to removed symbols.cargo test --workspaceand confirm all tests pass.grep -rn "enable_skill\|disable_skill\|SkillEnabled\|SkillDisabled\|SkillFilters" crates/and confirm zero hits.Dependency: All of Stages 1–6.
11. Breaking changes
enable_skillremoved fromSkillManagementServicetraitdisable_skillremoved fromSkillManagementServicetraitSkillDefinition.enabledremovedskill.enabledSkillUpdate.enabledremoved (skill_manager module)SkillUpdate { enabled: Some(…) }SkillUpdate.enabledremoved (event_bus module)SkillUpdate { enabled: Some(…) }SkillFiltersremoved entirely;list_skillssignature simplifiedSkillFilters, and all 9+list_skillscall sitesfiltersargumentSkillMetadata.enabledremovedmetadata.enabledSkillEvent::SkillEnabled/SkillDisabledremovedSkillEventthat names these variantsEventBus::publish_skill_enabled/publish_skill_disabledremovedGET /api/skillsandGET /api/skills/{id}HTTP responsesmetadata.enabled12. Modified files and summary
crates/fastskill-core/src/core/skill_manager.rsenabled: boolfromSkillDefinition; removeenabled: truefromSkillDefinition::new(); removeenabled: Option<bool>fromSkillUpdate; removeif let Some(enabled) = updates.enabledbranch inupdate_skill; removeSkillFiltersstruct entirely; remove enabled-basedretainblock inlist_skills; changelist_skillssignature tolist_skills(&self) -> Result<Vec<SkillDefinition>, ServiceError>; removeenable_skillanddisable_skillfromSkillManagementServicetrait andSkillManagerimplcrates/fastskill-core/src/core/metadata.rsenabled: boolfromSkillMetadata; removeenabled: skill.enabledfromFrom<&SkillDefinition>; updatelist_skills()call at line 232 (drop argument); add#[cfg(test)]block with backward-compat frontmatter parse testcrates/fastskill-core/src/events/event_bus.rsSkillEnabledandSkillDisabledvariants fromSkillEvent; removeenabled: Option<bool>from event-bus-localSkillUpdate; remove match arms forSkillEnabled/SkillDisabledinnotify_handlers,LoggingEventHandler,MetricsEventHandler; removepublish_skill_enabledandpublish_skill_disabledfromEventBuscrates/fastskill-core/src/http/handlers/skills.rs"enabled": skill.enabled,fromskill_metadata_json; updatelist_skills()calls at lines 45, 76, 177 (drop argument)crates/fastskill-core/src/http/handlers/status.rslist_skills()calls at lines 64, 241 (drop argument)crates/fastskill-core/src/http/handlers/search.rslist_skills()call at line 113 (drop argument)crates/fastskill-core/src/http/handlers/registry.rslist_skills()calls at lines 202, 309 (drop argument)crates/fastskill-core/src/core/mod.rsSkillFiltersfrom the public re-export (line 112); other re-exports automatically reflect struct changescrates/fastskill-core/src/lib.rslist_skills()call at line 130 (drop argument); re-exports ofSkillDefinition,SkillManagementServiceautomatically reflect changescrates/fastskill-cli/src/commands/list.rslist_skills()call at line 181 (drop argument)crates/fastskill-cli/src/commands/read.rslist_skills()calls at lines 179, 374 (drop argument)13. Dependencies
disablecommand removal)disablecommand would fail to compile because its call todisable_skillwould be unresolvableasync-traitcrateserde/serde_yamlcrates14. Proposed folder structure
This is not a new project. The files listed in §12 are modified. No new files are created except optionally a test fixture added inline via
#[cfg(test)]inmetadata.rs. No folder-structure changes are required.15. Design decisions
SkillFiltersbe removed entirely or kept as an empty struct?None, so dropping the argument is a mechanical one-line change at each site. The simplest possible API (list_skills(&self)) is immediately understandable, and future filter predicates can be introduced as a new method or builder at that time rather than indefinitely preserving an empty placeholderSkillEvent::SkillUpdatedvariant'schanges: SkillUpdatepayload be kept (minusenabled)?SkillUpdatedis a meaningful general-purpose event; removing it would be a separate, broader change beyond this issue's scopeSkillDefinitionremainSerialize + Deserialize?#[cfg(test)]parse_yaml_frontmatter) operates on&strHotReloadConfig.enabledbe removed as part of this change?HotReloadConfig.enabledis an unrelated feature flag for the hot-reload subsystem and is actively used inservice.rs(line 357) andhttp/handlers/status.rs(line 251); explicitly out of scope16. Out of scope (explicit)
HotReloadConfig.enabled— this is an active feature flag for the hot-reload subsystem, not related to the skill-enabled concept.skills.lock) or the manifest format (skill-project.toml).SkillEvent::SkillUpdatedor the event-bus-localSkillUpdatestruct (beyond theenabledfield).SkillFrontmatter(theextramap already handles unknown keys gracefully).SkillFilters(that is a separate feature).crates/fastskill-evals— a grep confirms no references to enabled-skill symbols there.17. References
Implementers SHOULD start from these exact locations:
SkillDefinitionstructcrates/fastskill-core/src/core/skill_manager.rsSkillDefinition::new()constructorcrates/fastskill-core/src/core/skill_manager.rsSkillManagementServicetraitcrates/fastskill-core/src/core/skill_manager.rsSkillFiltersstruct (to be deleted entirely)crates/fastskill-core/src/core/skill_manager.rsSkillUpdatestruct (skill_manager)crates/fastskill-core/src/core/skill_manager.rsupdate_skillimpl (enabled branch)crates/fastskill-core/src/core/skill_manager.rslist_skillsimpl (enabled filter)crates/fastskill-core/src/core/skill_manager.rsenable_skillimplcrates/fastskill-core/src/core/skill_manager.rsdisable_skillimplcrates/fastskill-core/src/core/skill_manager.rsSkillMetadatastructcrates/fastskill-core/src/core/metadata.rsFrom<&SkillDefinition> for SkillMetadatacrates/fastskill-core/src/core/metadata.rsSkillFrontmatter.extra(serde flatten)crates/fastskill-core/src/core/metadata.rsSkillEventenumcrates/fastskill-core/src/events/event_bus.rsSkillUpdatestructcrates/fastskill-core/src/events/event_bus.rsnotify_handlersmatchcrates/fastskill-core/src/events/event_bus.rsLoggingEventHandler::handle_eventcrates/fastskill-core/src/events/event_bus.rsMetricsEventHandler::handle_eventcrates/fastskill-core/src/events/event_bus.rspublish_skill_enabled/publish_skill_disabledcrates/fastskill-core/src/events/event_bus.rsskill_metadata_jsonhelpercrates/fastskill-core/src/http/handlers/skills.rscrates/fastskill-core/src/core/mod.rscrates/fastskill-core/src/lib.rs