Plugin System Redesign — SP1 Implementation Plan (2026)
Plugin System Redesign — SP1 Implementation Plan
Section titled “Plugin System Redesign — SP1 Implementation Plan”For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development(recommended) orsuperpowers:executing-plansto implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Parent spec: plugin-system-redesign-2026.md
Goal: Land the plugin SSOT — schemas, the vox-plugin-catalog crate, reference docs, vox-build-meta deprecation shims, and a CI parity guard — without introducing a runtime loader, ABI, or CLI plugin commands. This unblocks SP2 (host ABI + loader) and SP4 (skill plugin migration).
Architecture: A new crate vox-plugin-catalog owns catalog.toml (the SSOT) and exposes typed accessors (all_plugins, all_bundles, bundle_resolved). Build-time validation runs in build.rs; runtime access uses include_str! + std::sync::OnceLock for a single lazy parse. A docs module emits two .generated.md files invoked through a new vox ci generate-plugin-catalog-docs command. vox-build-meta’s has/require/active_features API stays present but becomes deprecation shims that direct users to vox plugin install <id>. A new vox ci plugin-catalog-parity command enforces every in-tree Plugin.toml has a catalog entry.
Tech Stack:
- Rust edition 2024 (workspace default)
serde+toml(workspace deps) for catalog parsingthiserrorfor error typesstd::sync::OnceLockfor lazy runtime access (no extra dependency)- Existing
vox-clicommand infrastructure for the CI guards - Existing
vox-doc-pipelinefor SUMMARY.md rollup — no changes to that crate
File Structure
Section titled “File Structure”New files
Section titled “New files”| Path | Responsibility |
|---|---|
crates/vox-plugin-catalog/Cargo.toml | Crate manifest. |
crates/vox-plugin-catalog/build.rs | Build-time catalog validation (parses + checks invariants). |
crates/vox-plugin-catalog/catalog.toml | The SSOT — every first-party plugin and bundle definition. |
crates/vox-plugin-catalog/src/lib.rs | Public API (all_plugins, all_bundles, bundle_resolved, errors). |
crates/vox-plugin-catalog/src/schema.rs | Serde types: PluginCatalogEntry, BundleEntry, PayloadKind. |
crates/vox-plugin-catalog/src/docs.rs | Generators for the two .generated.md files. |
crates/vox-plugin-catalog/tests/catalog_validation.rs | Build-time guarantees re-asserted as runtime tests. |
crates/vox-plugin-catalog/tests/bundle_resolution.rs | extends-chain resolution tests. |
crates/vox-plugin-catalog/tests/docs_generation.rs | Asserts generators produce expected sections. |
docs/src/reference/plugin-manifest.md | Hand-rolled Plugin.toml schema doc, all three payload kinds. |
docs/src/reference/plugin-catalog.md | Hand-rolled catalog concept doc. |
docs/src/reference/distribution-bundles.md | Hand-rolled bundle concept doc + decision tree. |
docs/src/reference/plugin-catalog.generated.md | Auto-generated catalog reference. Initial commit; regenerated by CLI. |
docs/src/reference/distribution-bundles.generated.md | Auto-generated bundle reference. Initial commit; regenerated by CLI. |
crates/vox-cli/src/commands/ci/plugin_catalog_parity.rs | CI guard: every in-tree Plugin.toml matches a catalog entry. |
crates/vox-cli/src/commands/ci/generate_plugin_catalog_docs.rs | CLI command that invokes vox-plugin-catalog::docs generators. |
Modified files
Section titled “Modified files”| Path | Change |
|---|---|
crates/vox-build-meta/Cargo.toml | Empty [features] block (delete all stubs). |
crates/vox-build-meta/build.rs | Replace env probes with a no-op and a cargo:warning deprecation note. |
crates/vox-build-meta/src/lib.rs | has/require/active_features become deprecation shims. |
crates/vox-cli/src/commands/ci/mod.rs | Register the two new CI subcommands. |
crates/vox-cli/src/commands/ci/cmd_enums.rs | Add enum variants for the new subcommands. |
crates/vox-cli/src/commands/ci/run_body.rs | Wire dispatch for the new subcommands. |
crates/vox-cli/Cargo.toml | Add vox-plugin-catalog workspace dep. |
Cargo.toml (workspace) | Add vox-plugin-catalog to [workspace.dependencies]. |
AGENTS.md | Add the two new .generated.md files to the auto-generated list. |
vox-plugin-catalog is auto-included as a workspace member via the existing members = ["crates/*"] glob — no separate registration needed.
Task 1: Scaffold the vox-plugin-catalog crate
Section titled “Task 1: Scaffold the vox-plugin-catalog crate”Files:
-
Create:
crates/vox-plugin-catalog/Cargo.toml -
Create:
crates/vox-plugin-catalog/src/lib.rs -
Step 1: Write the failing test
Create crates/vox-plugin-catalog/tests/smoke.rs:
#[test]fn crate_compiles() { // If this file compiles and runs, the skeleton is in place.}- Step 2: Run test to verify the crate doesn’t exist yet
Run from worktree root:
cargo test -p vox-plugin-catalog --test smokeExpected: error “could not find package vox-plugin-catalog”.
- Step 3: Create the Cargo.toml
Write crates/vox-plugin-catalog/Cargo.toml:
[package]name = "vox-plugin-catalog"version.workspace = trueedition.workspace = truelicense.workspace = truerepository.workspace = truedescription = "SSOT catalog of all first-party Vox plugins and distribution bundles."
[dependencies]serde = { workspace = true, features = ["derive"] }toml = { workspace = true }thiserror = { workspace = true }workspace-hack = { workspace = true }
[build-dependencies]serde = { workspace = true, features = ["derive"] }toml = { workspace = true }
[lints]workspace = true- Step 4: Create the empty lib.rs
Write crates/vox-plugin-catalog/src/lib.rs:
//! SSOT catalog of all first-party Vox plugins and distribution bundles.//!//! See `docs/src/architecture/plugin-system-redesign-2026.md`.- Step 5: Run test to verify it passes
Run:
cargo test -p vox-plugin-catalog --test smokeExpected: PASS, 1 test, crate_compiles ... ok.
- Step 6: Commit
git add crates/vox-plugin-catalog/Cargo.toml crates/vox-plugin-catalog/src/lib.rs crates/vox-plugin-catalog/tests/smoke.rs Cargo.lockgit commit -m "feat(plugin-catalog): scaffold vox-plugin-catalog crate"Task 2: Define schema types in schema.rs
Section titled “Task 2: Define schema types in schema.rs”Files:
-
Create:
crates/vox-plugin-catalog/src/schema.rs -
Modify:
crates/vox-plugin-catalog/src/lib.rs -
Test:
crates/vox-plugin-catalog/tests/schema_roundtrip.rs -
Step 1: Write the failing test
Create crates/vox-plugin-catalog/tests/schema_roundtrip.rs:
use vox_plugin_catalog::schema::{BundleEntry, PayloadKind, PluginCatalogEntry};
#[test]fn parses_a_minimal_code_plugin_entry() { let toml_src = r#" id = "mens-candle-cuda" payload-kind = "code" description = "Candle ML backend with CUDA." extension-points = ["MlBackend"] default-source = "github:vox-foundation/vox-plugin-mens-candle-cuda" bundled-in = ["vox-ml", "vox-dev"] "#; let entry: PluginCatalogEntry = toml::from_str(toml_src).expect("should parse"); assert_eq!(entry.id, "mens-candle-cuda"); assert!(matches!(entry.payload_kind, PayloadKind::Code)); assert_eq!(entry.extension_points.as_deref(), Some(&["MlBackend".to_string()][..]));}
#[test]fn parses_a_minimal_skill_plugin_entry() { let toml_src = r#" id = "skill-compiler" payload-kind = "skill" description = "Compiler skill." exposes-tools = ["vox_validate_file"] default-source = "github:vox-foundation/vox-plugin-skill-compiler" bundled-in = ["vox-fullstack"] "#; let entry: PluginCatalogEntry = toml::from_str(toml_src).expect("should parse"); assert!(matches!(entry.payload_kind, PayloadKind::Skill)); assert_eq!(entry.exposes_tools.as_deref(), Some(&["vox_validate_file".to_string()][..]));}
#[test]fn parses_a_bundle_with_extends() { let toml_src = r#" id = "vox-ml" description = "Fullstack + ML." extends = "vox-fullstack" plugins = ["mens-candle-cuda", "tensor-burn-wgpu"] "#; let bundle: BundleEntry = toml::from_str(toml_src).expect("should parse"); assert_eq!(bundle.extends.as_deref(), Some("vox-fullstack")); assert_eq!(bundle.plugins.len(), 2);}- Step 2: Run test to verify it fails
Run:
cargo test -p vox-plugin-catalog --test schema_roundtripExpected: FAIL — unresolved import vox_plugin_catalog::schema.
- Step 3: Implement schema.rs
Write crates/vox-plugin-catalog/src/schema.rs:
//! Catalog schema: plugin and bundle entry types parsed from `catalog.toml`.
use serde::{Deserialize, Serialize};
/// One entry in the plugin catalog.#[derive(Debug, Clone, Serialize, Deserialize)]#[serde(rename_all = "kebab-case")]pub struct PluginCatalogEntry { /// Globally unique short id, e.g. "mens-candle-cuda" or "skill-compiler". pub id: String,
/// Which payload kind this plugin ships. pub payload_kind: PayloadKind,
/// One-line human description. pub description: String,
/// For `code` payloads: extension-point trait names this plugin provides. #[serde(default)] pub extension_points: Option<Vec<String>>,
/// For `skill` payloads: MCP tool names this skill exposes to agents. #[serde(default)] pub exposes_tools: Option<Vec<String>>,
/// Optional capability tag (e.g. "nvidia-gpu") informational only. #[serde(default)] pub requires_tag: Option<String>,
/// Where to fetch the plugin from for `vox plugin install <id>`. /// Always present for first-party plugins (1a guarantee — every plugin /// is standalone-installable, not bundle-only). pub default_source: String,
/// Advisory list of first-party bundles that pre-install this plugin. /// Shown by `vox plugin info`. Does not gate standalone install. #[serde(default)] pub bundled_in: Vec<String>,}
/// Discriminator for plugin payload kind.#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "kebab-case")]pub enum PayloadKind { Code, Skill, Composite,}
/// One distribution-bundle entry in the catalog.#[derive(Debug, Clone, Serialize, Deserialize)]#[serde(rename_all = "kebab-case")]pub struct BundleEntry { pub id: String, pub description: String,
/// Optional parent bundle whose plugin set is inherited. #[serde(default)] pub extends: Option<String>,
/// Plugins added on top of any inherited set. May be empty. #[serde(default)] pub plugins: Vec<String>,}- Step 4: Wire it from lib.rs
Edit crates/vox-plugin-catalog/src/lib.rs to:
//! SSOT catalog of all first-party Vox plugins and distribution bundles.//!//! See `docs/src/architecture/plugin-system-redesign-2026.md`.
pub mod schema;- Step 5: Run test to verify it passes
Run:
cargo test -p vox-plugin-catalog --test schema_roundtripExpected: PASS, 3 tests.
- Step 6: Commit
git add crates/vox-plugin-catalog/src/schema.rs crates/vox-plugin-catalog/src/lib.rs crates/vox-plugin-catalog/tests/schema_roundtrip.rsgit commit -m "feat(plugin-catalog): add PluginCatalogEntry and BundleEntry schema types"Task 3: Create initial catalog.toml with one code + one skill + one bundle
Section titled “Task 3: Create initial catalog.toml with one code + one skill + one bundle”Files:
-
Create:
crates/vox-plugin-catalog/catalog.toml -
Modify:
crates/vox-plugin-catalog/src/lib.rs -
Test:
crates/vox-plugin-catalog/tests/catalog_load.rs -
Step 1: Write the failing test
Create crates/vox-plugin-catalog/tests/catalog_load.rs:
use vox_plugin_catalog::{all_bundles, all_plugins};
#[test]fn catalog_has_at_least_one_plugin() { let plugins = all_plugins(); assert!(!plugins.is_empty(), "catalog has zero plugins"); assert!(plugins.iter().any(|p| p.id == "mens-candle-cuda"));}
#[test]fn catalog_has_at_least_one_bundle() { let bundles = all_bundles(); assert!(!bundles.is_empty(), "catalog has zero bundles"); assert!(bundles.iter().any(|b| b.id == "vox-base"));}- Step 2: Run test to verify it fails
Run:
cargo test -p vox-plugin-catalog --test catalog_loadExpected: FAIL — cannot find function all_plugins.
- Step 3: Create the seed catalog.toml
Write crates/vox-plugin-catalog/catalog.toml:
# vox-plugin-catalog SSOT## Every first-party Vox plugin and distribution bundle is declared here.# After editing: `cargo build -p vox-plugin-catalog` validates the file.# Regenerate the human-readable docs with:# vox ci generate-plugin-catalog-docs## See: docs/src/architecture/plugin-system-redesign-2026.md# See: docs/src/reference/plugin-catalog.md
[[plugin]]id = "mens-candle-cuda"payload-kind = "code"description = "ML training backend using Candle with CUDA acceleration."extension-points = ["MlBackend"]requires-tag = "nvidia-gpu"default-source = "github:vox-foundation/vox-plugin-mens-candle-cuda"bundled-in = ["vox-ml", "vox-dev"]
[[plugin]]id = "skill-compiler"payload-kind = "skill"description = "Agent-facing skill describing the Vox compiler tools."exposes-tools = ["vox_validate_file", "vox_run_tests", "vox_check_workspace"]default-source = "github:vox-foundation/vox-plugin-skill-compiler"bundled-in = ["vox-fullstack", "vox-ml", "vox-dev"]
[[bundle]]id = "vox-base"description = "Bare host binary, no plugins."plugins = []- Step 4: Implement
all_pluginsandall_bundlesin lib.rs
Replace crates/vox-plugin-catalog/src/lib.rs with:
//! SSOT catalog of all first-party Vox plugins and distribution bundles.//!//! See `docs/src/architecture/plugin-system-redesign-2026.md`.
pub mod schema;
use schema::{BundleEntry, PluginCatalogEntry};use serde::Deserialize;use std::sync::OnceLock;
/// Embedded raw catalog source. Validated at build time by `build.rs`.const CATALOG_SRC: &str = include_str!("../catalog.toml");
#[derive(Deserialize)]struct CatalogFile { #[serde(default, rename = "plugin")] plugins: Vec<PluginCatalogEntry>, #[serde(default, rename = "bundle")] bundles: Vec<BundleEntry>,}
fn parsed() -> &'static CatalogFile { static CACHED: OnceLock<CatalogFile> = OnceLock::new(); CACHED.get_or_init(|| { toml::from_str::<CatalogFile>(CATALOG_SRC) .expect("catalog.toml should parse — build.rs validates this") })}
/// All first-party plugins declared in `catalog.toml`.pub fn all_plugins() -> &'static [PluginCatalogEntry] { &parsed().plugins}
/// All distribution bundles declared in `catalog.toml`.pub fn all_bundles() -> &'static [BundleEntry] { &parsed().bundles}- Step 5: Run test to verify it passes
Run:
cargo test -p vox-plugin-catalog --test catalog_loadExpected: PASS, 2 tests.
- Step 6: Commit
git add crates/vox-plugin-catalog/catalog.toml crates/vox-plugin-catalog/src/lib.rs crates/vox-plugin-catalog/tests/catalog_load.rsgit commit -m "feat(plugin-catalog): add catalog.toml seed with all_plugins/all_bundles accessors"Task 4: Add the remaining 8 code-plugin entries
Section titled “Task 4: Add the remaining 8 code-plugin entries”Files:
- Modify:
crates/vox-plugin-catalog/catalog.toml - Test:
crates/vox-plugin-catalog/tests/catalog_load.rs
The 9 code plugins are derived from the 9 features in vox-build-meta/Cargo.toml:
gpu, oratio, oratio-mic, script-execution, mens-candle-cuda, cloud, execution-api, stub-check, populi.
Mapping to plugin ids and extension points:
vox-build-meta feature | New plugin id | Extension point |
|---|---|---|
gpu | tensor-burn-wgpu | TensorBackend |
mens-candle-cuda | mens-candle-cuda | MlBackend |
oratio | oratio | AudioCapture |
oratio-mic | oratio-mic | AudioCapture |
cloud | cloud | CloudSync |
populi | populi-mesh | MeshDriver |
script-execution | script-execution | ScriptExecutor |
execution-api | execution-api | ScriptExecutor |
stub-check | stub-check | ScriptExecutor |
mens-candle-cuda is already in the seed catalog. Eight to add.
- Step 1: Write the failing assertion
Append to crates/vox-plugin-catalog/tests/catalog_load.rs:
#[test]fn catalog_has_all_nine_code_plugins() { let plugins = all_plugins(); let code_ids: Vec<&str> = plugins .iter() .filter(|p| matches!(p.payload_kind, vox_plugin_catalog::schema::PayloadKind::Code)) .map(|p| p.id.as_str()) .collect(); let expected = [ "tensor-burn-wgpu", "mens-candle-cuda", "oratio", "oratio-mic", "cloud", "populi-mesh", "script-execution", "execution-api", "stub-check", ]; for id in expected { assert!(code_ids.contains(&id), "missing code plugin: {id}"); }}- Step 2: Run to verify it fails
cargo test -p vox-plugin-catalog --test catalog_load catalog_has_all_nine_code_pluginsExpected: FAIL — multiple missing ids.
- Step 3: Add the 8 entries to catalog.toml
Insert in crates/vox-plugin-catalog/catalog.toml after the existing mens-candle-cuda block:
[[plugin]]id = "tensor-burn-wgpu"payload-kind = "code"description = "Tensor backend on Burn + wgpu (cross-vendor GPU)."extension-points = ["TensorBackend"]default-source = "github:vox-foundation/vox-plugin-tensor-burn-wgpu"bundled-in = ["vox-ml", "vox-dev"]
[[plugin]]id = "oratio"payload-kind = "code"description = "Audio capture and pipeline integration (Oratio)."extension-points = ["AudioCapture"]default-source = "github:vox-foundation/vox-plugin-oratio"bundled-in = ["vox-dev"]
[[plugin]]id = "oratio-mic"payload-kind = "code"description = "Microphone input adapter for Oratio."extension-points = ["AudioCapture"]default-source = "github:vox-foundation/vox-plugin-oratio-mic"bundled-in = ["vox-dev"]
[[plugin]]id = "cloud"payload-kind = "code"description = "Cloud sync provider (Mens cloud + Populi cloud)."extension-points = ["CloudSync"]default-source = "github:vox-foundation/vox-plugin-cloud"bundled-in = ["vox-mesh", "vox-cloud-only", "vox-dev"]
[[plugin]]id = "populi-mesh"payload-kind = "composite"description = "Populi mesh transport + the agent skill that documents it."extension-points = ["MeshDriver"]exposes-tools = ["vox_populi_join", "vox_populi_dispatch"]default-source = "github:vox-foundation/vox-plugin-populi-mesh"bundled-in = ["vox-mesh", "vox-server", "vox-dev"]
[[plugin]]id = "script-execution"payload-kind = "code"description = "Script execution sandbox for `.vox run` and similar."extension-points = ["ScriptExecutor"]default-source = "github:vox-foundation/vox-plugin-script-execution"bundled-in = ["vox-dev"]
[[plugin]]id = "execution-api"payload-kind = "code"description = "Programmatic execution API surface for embedders."extension-points = ["ScriptExecutor"]default-source = "github:vox-foundation/vox-plugin-execution-api"bundled-in = []
[[plugin]]id = "stub-check"payload-kind = "code"description = "Stub-validation diagnostics for the executor."extension-points = ["ScriptExecutor"]default-source = "github:vox-foundation/vox-plugin-stub-check"bundled-in = []- Step 4: Run to verify it passes
cargo test -p vox-plugin-catalog --test catalog_loadExpected: all 3 tests PASS.
- Step 5: Commit
git add crates/vox-plugin-catalog/catalog.toml crates/vox-plugin-catalog/tests/catalog_load.rsgit commit -m "feat(plugin-catalog): add 8 remaining code-plugin entries derived from vox-build-meta features"Task 5: Add the 9 skill-plugin entries
Section titled “Task 5: Add the 9 skill-plugin entries”The 9 skills today live as files under crates/vox-skills/skills/:
compiler, git, memory, orchestrator, populi, rag, testing, testing.validate, v0.
Each becomes a skill-payload plugin with id skill-<name> (with dots replaced by hyphens). skill-populi is already covered by the composite populi-mesh plugin’s skill side; in the catalog it does NOT get a separate entry — composite plugins serve both purposes.
So 8 skill-payload entries to add: skill-compiler (already in seed), plus 7 new: skill-git, skill-memory, skill-orchestrator, skill-rag, skill-testing, skill-testing-validate, skill-v0.
Files:
-
Modify:
crates/vox-plugin-catalog/catalog.toml -
Test:
crates/vox-plugin-catalog/tests/catalog_load.rs -
Step 1: Write the failing assertion
Append to crates/vox-plugin-catalog/tests/catalog_load.rs:
#[test]fn catalog_has_all_skill_plugins() { let plugins = all_plugins(); let skill_ids: Vec<&str> = plugins .iter() .filter(|p| matches!(p.payload_kind, vox_plugin_catalog::schema::PayloadKind::Skill)) .map(|p| p.id.as_str()) .collect(); let expected = [ "skill-compiler", "skill-git", "skill-memory", "skill-orchestrator", "skill-rag", "skill-testing", "skill-testing-validate", "skill-v0", ]; for id in expected { assert!(skill_ids.contains(&id), "missing skill plugin: {id}"); }}- Step 2: Run to verify it fails
cargo test -p vox-plugin-catalog --test catalog_load catalog_has_all_skill_pluginsExpected: FAIL — 7 missing ids.
- Step 3: Add the 7 entries to catalog.toml
Insert in crates/vox-plugin-catalog/catalog.toml after the existing skill-compiler block:
[[plugin]]id = "skill-git"payload-kind = "skill"description = "Agent-facing skill describing Vox git integration tools."exposes-tools = ["vox_git_status", "vox_git_diff", "vox_git_resolve_conflict"]default-source = "github:vox-foundation/vox-plugin-skill-git"bundled-in = ["vox-fullstack", "vox-dev"]
[[plugin]]id = "skill-memory"payload-kind = "skill"description = "Agent-facing skill for memory and context management."exposes-tools = ["vox_memory_get", "vox_memory_set", "vox_memory_search"]default-source = "github:vox-foundation/vox-plugin-skill-memory"bundled-in = ["vox-fullstack", "vox-edge", "vox-cloud-only", "vox-server", "vox-dev"]
[[plugin]]id = "skill-orchestrator"payload-kind = "skill"description = "Agent-facing skill for task submission, status, budget, multi-agent coordination."exposes-tools = ["vox_orchestrator_submit", "vox_orchestrator_status", "vox_orchestrator_budget"]default-source = "github:vox-foundation/vox-plugin-skill-orchestrator"bundled-in = ["vox-fullstack", "vox-cloud-only", "vox-server", "vox-mesh", "vox-dev"]
[[plugin]]id = "skill-rag"payload-kind = "skill"description = "Agent-facing skill for retrieval-augmented generation."exposes-tools = ["vox_rag_query", "vox_rag_index"]default-source = "github:vox-foundation/vox-plugin-skill-rag"bundled-in = ["vox-fullstack", "vox-dev"]
[[plugin]]id = "skill-testing"payload-kind = "skill"description = "Agent-facing skill for test-runner integration."exposes-tools = ["vox_test_run", "vox_test_select"]default-source = "github:vox-foundation/vox-plugin-skill-testing"bundled-in = ["vox-fullstack", "vox-dev"]
[[plugin]]id = "skill-testing-validate"payload-kind = "skill"description = "Agent-facing skill for test validation diagnostics."exposes-tools = ["vox_test_validate"]default-source = "github:vox-foundation/vox-plugin-skill-testing-validate"bundled-in = ["vox-fullstack", "vox-dev"]
[[plugin]]id = "skill-v0"payload-kind = "skill"description = "Agent-facing skill providing legacy v0 compatibility surface."exposes-tools = ["vox_v0_compat"]default-source = "github:vox-foundation/vox-plugin-skill-v0"bundled-in = ["vox-fullstack", "vox-edge"]- Step 4: Run to verify it passes
cargo test -p vox-plugin-catalog --test catalog_loadExpected: all 4 tests PASS.
- Step 5: Commit
git add crates/vox-plugin-catalog/catalog.toml crates/vox-plugin-catalog/tests/catalog_load.rsgit commit -m "feat(plugin-catalog): add 7 remaining skill-plugin entries (composite populi-mesh covers skill-populi)"Task 6: Add the 8 bundle entries
Section titled “Task 6: Add the 8 bundle entries”Files:
-
Modify:
crates/vox-plugin-catalog/catalog.toml -
Test:
crates/vox-plugin-catalog/tests/catalog_load.rs -
Step 1: Write the failing assertion
Append to crates/vox-plugin-catalog/tests/catalog_load.rs:
#[test]fn catalog_has_all_eight_bundles() { let bundles = all_bundles(); let ids: Vec<&str> = bundles.iter().map(|b| b.id.as_str()).collect(); let expected = [ "vox-base", "vox-fullstack", "vox-ml", "vox-mesh", "vox-server", "vox-edge", "vox-cloud-only", "vox-dev", ]; for id in expected { assert!(ids.contains(&id), "missing bundle: {id}"); }}- Step 2: Run to verify it fails
cargo test -p vox-plugin-catalog --test catalog_load catalog_has_all_eight_bundlesExpected: FAIL — 7 missing.
- Step 3: Add the 7 bundles to catalog.toml
Append to crates/vox-plugin-catalog/catalog.toml:
[[bundle]]id = "vox-fullstack"description = "Default developer experience with all built-in skill plugins."plugins = [ "skill-compiler", "skill-testing", "skill-testing-validate", "skill-memory", "skill-git", "skill-orchestrator", "skill-rag", "skill-v0",]
[[bundle]]id = "vox-ml"description = "Fullstack plus ML/GPU code plugins (NVIDIA CUDA stack)."extends = "vox-fullstack"plugins = ["tensor-burn-wgpu", "mens-candle-cuda"]
[[bundle]]id = "vox-mesh"description = "Server-side mesh deployment with cloud sync."extends = "vox-base"plugins = ["populi-mesh", "cloud", "skill-orchestrator"]
[[bundle]]id = "vox-server"description = "Headless backend deployment: orchestrator + mesh, no GUI/ML."extends = "vox-base"plugins = ["populi-mesh", "cloud", "skill-orchestrator", "skill-memory"]
[[bundle]]id = "vox-edge"description = "Edge / on-device deployment: lightweight runtime + local skills, no cloud or mesh."extends = "vox-base"plugins = ["skill-compiler", "skill-memory", "skill-v0"]
[[bundle]]id = "vox-cloud-only"description = "Cloud-managed deployment: cloud sync only, no local ML or mesh transport."extends = "vox-base"plugins = ["cloud", "skill-orchestrator", "skill-memory"]
[[bundle]]id = "vox-dev"description = "Contributor / power-user development environment: fullstack + ML + mesh + audio."extends = "vox-fullstack"plugins = [ "tensor-burn-wgpu", "mens-candle-cuda", "populi-mesh", "cloud", "oratio", "oratio-mic",]- Step 4: Run to verify it passes
cargo test -p vox-plugin-catalog --test catalog_loadExpected: all 5 tests PASS.
- Step 5: Commit
git add crates/vox-plugin-catalog/catalog.toml crates/vox-plugin-catalog/tests/catalog_load.rsgit commit -m "feat(plugin-catalog): add 7 remaining bundle definitions"Task 7: Implement bundle_resolved with extends-chain resolution
Section titled “Task 7: Implement bundle_resolved with extends-chain resolution”Files:
-
Modify:
crates/vox-plugin-catalog/src/lib.rs -
Test:
crates/vox-plugin-catalog/tests/bundle_resolution.rs -
Step 1: Write the failing test
Create crates/vox-plugin-catalog/tests/bundle_resolution.rs:
use vox_plugin_catalog::{bundle_resolved, ResolveError};
#[test]fn vox_base_resolves_to_zero_plugins() { let plugins = bundle_resolved("vox-base").expect("should resolve"); assert!(plugins.is_empty());}
#[test]fn vox_fullstack_resolves_to_eight_skills() { let plugins = bundle_resolved("vox-fullstack").expect("should resolve"); assert_eq!(plugins.len(), 8); assert!(plugins.iter().any(|p| p.id == "skill-compiler"));}
#[test]fn vox_ml_resolves_through_extends_chain() { // vox-ml extends vox-fullstack which has 8 skills. // vox-ml adds 2 ML plugins. Total = 10. let plugins = bundle_resolved("vox-ml").expect("should resolve"); assert_eq!(plugins.len(), 10); assert!(plugins.iter().any(|p| p.id == "mens-candle-cuda")); assert!(plugins.iter().any(|p| p.id == "skill-compiler"));}
#[test]fn unknown_bundle_returns_error() { match bundle_resolved("nope") { Err(ResolveError::UnknownBundle(id)) => assert_eq!(id, "nope"), other => panic!("expected UnknownBundle, got {other:?}"), }}
#[test]fn duplicate_plugin_in_chain_is_deduped() { // skill-orchestrator is in vox-fullstack AND vox-mesh; vox-dev pulls both. // It must appear exactly once in the resolved set. let plugins = bundle_resolved("vox-dev").expect("should resolve"); let count = plugins.iter().filter(|p| p.id == "skill-orchestrator").count(); assert_eq!(count, 1, "skill-orchestrator should be deduped, got {count}");}- Step 2: Run to verify it fails
cargo test -p vox-plugin-catalog --test bundle_resolutionExpected: FAIL — cannot find function bundle_resolved.
- Step 3: Implement bundle_resolved
Append to crates/vox-plugin-catalog/src/lib.rs:
use schema::PayloadKind;use thiserror::Error;
#[derive(Debug, Error)]pub enum ResolveError { #[error("unknown bundle: {0}")] UnknownBundle(String), #[error("unknown plugin '{plugin}' referenced by bundle '{bundle}'")] UnknownPlugin { bundle: String, plugin: String }, #[error("bundle '{0}' has a cyclic extends chain")] CyclicExtends(String),}
/// Resolve a bundle id to its full plugin set, walking the `extends` chain/// and deduplicating by plugin id. Order: parent plugins first, then child/// additions. First-occurrence wins for duplicates.pub fn bundle_resolved(id: &str) -> Result<Vec<&'static PluginCatalogEntry>, ResolveError> { let mut seen_ids: std::collections::HashSet<String> = std::collections::HashSet::new(); let mut chain: Vec<&'static BundleEntry> = Vec::new(); let mut current = id.to_string(); loop { let bundle = all_bundles() .iter() .find(|b| b.id == current) .ok_or_else(|| ResolveError::UnknownBundle(current.clone()))?; if !seen_ids.insert(bundle.id.clone()) { return Err(ResolveError::CyclicExtends(id.to_string())); } chain.push(bundle); match &bundle.extends { Some(parent) => current = parent.clone(), None => break, } } // Walk parents-first. let mut out: Vec<&'static PluginCatalogEntry> = Vec::new(); let mut included: std::collections::HashSet<&str> = std::collections::HashSet::new(); for bundle in chain.iter().rev() { for plugin_id in &bundle.plugins { if included.insert(plugin_id.as_str()) { let plugin = all_plugins() .iter() .find(|p| &p.id == plugin_id) .ok_or_else(|| ResolveError::UnknownPlugin { bundle: bundle.id.clone(), plugin: plugin_id.clone(), })?; out.push(plugin); } } } Ok(out)}
// Silence the "unused import" warning when PayloadKind isn't yet used by the// public API surface; downstream sub-projects will use it.#[allow(dead_code)]fn _payload_kind_keepalive(_: PayloadKind) {}- Step 4: Run to verify it passes
cargo test -p vox-plugin-catalog --test bundle_resolutionExpected: all 5 tests PASS.
- Step 5: Commit
git add crates/vox-plugin-catalog/src/lib.rs crates/vox-plugin-catalog/tests/bundle_resolution.rsgit commit -m "feat(plugin-catalog): implement bundle_resolved with extends-chain walking and dedup"Task 8: Build-time validation in build.rs
Section titled “Task 8: Build-time validation in build.rs”Files:
- Create:
crates/vox-plugin-catalog/build.rs - Test: covered by all-tests run after this task.
The build script parses catalog.toml at compile time and fails the build with a clear error if any structural rule is violated. This catches catalog typos before runtime.
- Step 1: Write the test that asserts build.rs catches a malformed catalog
This is hard to express as a normal cargo test (build.rs runs once per cargo invocation). Instead, write an integration test that re-runs the same validators on the catalog source string:
Create crates/vox-plugin-catalog/tests/catalog_validation.rs:
//! Mirrors what `build.rs` checks. If this passes, the catalog is well-formed.//! `build.rs` runs the same logic at compile time so structural breakage is//! a build error, not a runtime error.
use vox_plugin_catalog::{all_bundles, all_plugins};use vox_plugin_catalog::schema::PayloadKind;
#[test]fn every_plugin_id_is_unique() { let mut seen = std::collections::HashSet::new(); for plugin in all_plugins() { assert!(seen.insert(&plugin.id), "duplicate plugin id: {}", plugin.id); }}
#[test]fn every_bundle_id_is_unique() { let mut seen = std::collections::HashSet::new(); for bundle in all_bundles() { assert!(seen.insert(&bundle.id), "duplicate bundle id: {}", bundle.id); }}
#[test]fn every_bundled_in_reference_exists() { let bundle_ids: std::collections::HashSet<&str> = all_bundles().iter().map(|b| b.id.as_str()).collect(); for plugin in all_plugins() { for bundle_id in &plugin.bundled_in { assert!( bundle_ids.contains(bundle_id.as_str()), "plugin '{}' lists bundled-in='{}', but no such bundle exists", plugin.id, bundle_id ); } }}
#[test]fn every_bundle_plugin_reference_exists() { let plugin_ids: std::collections::HashSet<&str> = all_plugins().iter().map(|p| p.id.as_str()).collect(); for bundle in all_bundles() { for plugin_id in &bundle.plugins { assert!( plugin_ids.contains(plugin_id.as_str()), "bundle '{}' lists plugin '{}', but no such plugin exists", bundle.id, plugin_id ); } }}
#[test]fn every_extends_target_exists() { let bundle_ids: std::collections::HashSet<&str> = all_bundles().iter().map(|b| b.id.as_str()).collect(); for bundle in all_bundles() { if let Some(parent) = &bundle.extends { assert!( bundle_ids.contains(parent.as_str()), "bundle '{}' extends '{}', but no such bundle exists", bundle.id, parent ); } }}
#[test]fn code_plugins_declare_extension_points() { for plugin in all_plugins() { if matches!(plugin.payload_kind, PayloadKind::Code | PayloadKind::Composite) { assert!( plugin.extension_points.is_some() && !plugin.extension_points.as_ref().unwrap().is_empty(), "code/composite plugin '{}' must declare extension-points", plugin.id ); } }}
#[test]fn skill_plugins_declare_exposed_tools() { for plugin in all_plugins() { if matches!(plugin.payload_kind, PayloadKind::Skill | PayloadKind::Composite) { assert!( plugin.exposes_tools.is_some() && !plugin.exposes_tools.as_ref().unwrap().is_empty(), "skill/composite plugin '{}' must declare exposes-tools", plugin.id ); } }}
#[test]fn every_plugin_has_default_source() { // 1a guarantee: every plugin is standalone-installable. for plugin in all_plugins() { assert!( !plugin.default_source.is_empty(), "plugin '{}' has empty default-source", plugin.id ); }}- Step 2: Run to verify they pass against the current catalog
cargo test -p vox-plugin-catalog --test catalog_validationExpected: all 8 tests PASS. (The catalog from Tasks 3–6 is already well-formed.)
- Step 3: Write build.rs that runs equivalent validation at compile time
Write crates/vox-plugin-catalog/build.rs:
//! Build-time validation of catalog.toml. Runs the same structural checks the//! integration tests in tests/catalog_validation.rs do, but at compile time so//! a malformed catalog fails the build instead of a runtime test.
use serde::Deserialize;use std::collections::HashSet;
#[derive(Deserialize)]#[serde(rename_all = "kebab-case")]struct PluginEntry { id: String, payload_kind: String, #[serde(default)] extension_points: Option<Vec<String>>, #[serde(default)] exposes_tools: Option<Vec<String>>, default_source: String, #[serde(default)] bundled_in: Vec<String>,}
#[derive(Deserialize)]#[serde(rename_all = "kebab-case")]struct BundleEntry { id: String, #[serde(default)] extends: Option<String>, #[serde(default)] plugins: Vec<String>,}
#[derive(Deserialize)]struct CatalogFile { #[serde(default, rename = "plugin")] plugins: Vec<PluginEntry>, #[serde(default, rename = "bundle")] bundles: Vec<BundleEntry>,}
fn main() { println!("cargo:rerun-if-changed=catalog.toml"); let src = std::fs::read_to_string("catalog.toml") .expect("catalog.toml not found"); let cat: CatalogFile = match toml::from_str(&src) { Ok(v) => v, Err(e) => { panic!("catalog.toml failed to parse: {e}"); } };
let mut errors: Vec<String> = Vec::new();
// Unique ids let mut plugin_ids = HashSet::new(); for p in &cat.plugins { if !plugin_ids.insert(p.id.clone()) { errors.push(format!("duplicate plugin id: {}", p.id)); } } let mut bundle_ids = HashSet::new(); for b in &cat.bundles { if !bundle_ids.insert(b.id.clone()) { errors.push(format!("duplicate bundle id: {}", b.id)); } }
// Cross-references for p in &cat.plugins { for b in &p.bundled_in { if !bundle_ids.contains(b) { errors.push(format!( "plugin '{}' lists bundled-in='{}', but no such bundle exists", p.id, b )); } } } for b in &cat.bundles { for p in &b.plugins { if !plugin_ids.contains(p) { errors.push(format!( "bundle '{}' lists plugin '{}', but no such plugin exists", b.id, p )); } } if let Some(parent) = &b.extends { if !bundle_ids.contains(parent) { errors.push(format!( "bundle '{}' extends '{}', but no such bundle exists", b.id, parent )); } } }
// Per-payload-kind requirements for p in &cat.plugins { match p.payload_kind.as_str() { "code" => { if p.extension_points.as_ref().is_none_or(|v| v.is_empty()) { errors.push(format!( "code plugin '{}' must declare extension-points", p.id )); } } "skill" => { if p.exposes_tools.as_ref().is_none_or(|v| v.is_empty()) { errors.push(format!( "skill plugin '{}' must declare exposes-tools", p.id )); } } "composite" => { if p.extension_points.as_ref().is_none_or(|v| v.is_empty()) { errors.push(format!( "composite plugin '{}' must declare extension-points", p.id )); } if p.exposes_tools.as_ref().is_none_or(|v| v.is_empty()) { errors.push(format!( "composite plugin '{}' must declare exposes-tools", p.id )); } } other => { errors.push(format!( "plugin '{}' has unknown payload-kind '{}' (must be code|skill|composite)", p.id, other )); } } if p.default_source.is_empty() { errors.push(format!("plugin '{}' has empty default-source", p.id)); } }
if !errors.is_empty() { for e in &errors { println!("cargo:warning={e}"); } panic!( "catalog.toml validation failed with {} error(s); see warnings above", errors.len() ); }}- Step 4: Run a clean rebuild to confirm build.rs runs and validates
cargo clean -p vox-plugin-catalogcargo build -p vox-plugin-catalogExpected: clean build, no warnings about catalog validation.
- Step 5: Verify build.rs catches a deliberate breakage
Temporarily corrupt catalog.toml by changing the vox-base bundle’s id to vox-base-DUPE and adding another bundle below it with id = "vox-base-DUPE". Run:
cargo clean -p vox-plugin-catalogcargo build -p vox-plugin-catalog 2>&1 | head -10Expected: build fails with panicked at 'catalog.toml validation failed' and a warning line cargo:warning=duplicate bundle id: vox-base-DUPE.
Revert the change (git checkout crates/vox-plugin-catalog/catalog.toml) and rebuild to confirm green.
- Step 6: Commit
git add crates/vox-plugin-catalog/build.rs crates/vox-plugin-catalog/tests/catalog_validation.rsgit commit -m "feat(plugin-catalog): add build.rs validation matching runtime tests"Task 9: Doc generators in docs.rs
Section titled “Task 9: Doc generators in docs.rs”Files:
- Create:
crates/vox-plugin-catalog/src/docs.rs - Modify:
crates/vox-plugin-catalog/src/lib.rs - Test:
crates/vox-plugin-catalog/tests/docs_generation.rs
These produce the body text of the two .generated.md files. The CLI command in Task 11 writes them to disk; here we just produce strings so they’re testable.
- Step 1: Write the failing test
Create crates/vox-plugin-catalog/tests/docs_generation.rs:
use vox_plugin_catalog::docs::{render_bundles_md, render_catalog_md};
#[test]fn catalog_md_lists_every_plugin_and_includes_payload_kind() { let md = render_catalog_md(); for plugin in vox_plugin_catalog::all_plugins() { assert!(md.contains(&plugin.id), "catalog md missing plugin: {}", plugin.id); } assert!(md.contains("payload-kind")); assert!(md.contains("default-source"));}
#[test]fn bundles_md_lists_every_bundle_and_resolved_plugin_count() { let md = render_bundles_md(); for bundle in vox_plugin_catalog::all_bundles() { assert!(md.contains(&bundle.id), "bundles md missing bundle: {}", bundle.id); } // Should include resolved plugin count for at least one bundle. assert!(md.contains("plugins"), "bundles md should mention plugin counts");}
#[test]fn generated_md_starts_with_do_not_edit_marker() { let cat = render_catalog_md(); let bun = render_bundles_md(); let marker = "<!-- AUTOGENERATED"; assert!(cat.starts_with(marker), "catalog md missing autogen marker"); assert!(bun.starts_with(marker), "bundles md missing autogen marker");}- Step 2: Run to verify it fails
cargo test -p vox-plugin-catalog --test docs_generationExpected: FAIL — unresolved import vox_plugin_catalog::docs.
- Step 3: Implement docs.rs
Write crates/vox-plugin-catalog/src/docs.rs:
//! Markdown renderers for the auto-generated catalog reference docs.//! The output of these functions is written to disk by//! `vox ci generate-plugin-catalog-docs` (see vox-cli).
use crate::{all_bundles, all_plugins, bundle_resolved, schema::PayloadKind};
const HEADER: &str = "<!-- AUTOGENERATED by `vox ci generate-plugin-catalog-docs` from \ crates/vox-plugin-catalog/catalog.toml. DO NOT EDIT BY HAND. -->\n\n";
pub fn render_catalog_md() -> String { let mut out = String::from(HEADER); out.push_str("# Plugin Catalog (Generated)\n\n"); out.push_str( "Authoritative list of every first-party Vox plugin. Sourced from \ `crates/vox-plugin-catalog/catalog.toml`. See \ [plugin-catalog.md](plugin-catalog.md) for the prose explanation.\n\n", );
out.push_str("## Code-payload plugins\n\n"); out.push_str("| id | extension points | default-source | bundled-in |\n"); out.push_str("|----|------------------|----------------|------------|\n"); for p in all_plugins().iter().filter(|p| matches!(p.payload_kind, PayloadKind::Code)) { out.push_str(&format_plugin_row(p)); } out.push('\n');
out.push_str("## Skill-payload plugins\n\n"); out.push_str("| id | exposes tools | default-source | bundled-in |\n"); out.push_str("|----|---------------|----------------|------------|\n"); for p in all_plugins().iter().filter(|p| matches!(p.payload_kind, PayloadKind::Skill)) { out.push_str(&format_skill_row(p)); } out.push('\n');
out.push_str("## Composite plugins\n\n"); out.push_str("| id | extension points | exposes tools | default-source |\n"); out.push_str("|----|------------------|---------------|----------------|\n"); for p in all_plugins().iter().filter(|p| matches!(p.payload_kind, PayloadKind::Composite)) { out.push_str(&format_composite_row(p)); } out.push('\n');
out}
pub fn render_bundles_md() -> String { let mut out = String::from(HEADER); out.push_str("# Distribution Bundles (Generated)\n\n"); out.push_str( "First-party Vox distribution bundles. Each bundle is the same host \ binary plus a curated `plugins/` directory. See \ [distribution-bundles.md](distribution-bundles.md) for the decision \ tree.\n\n", );
out.push_str("| bundle | extends | direct plugins | total resolved plugins |\n"); out.push_str("|--------|---------|----------------|------------------------|\n"); for b in all_bundles() { let resolved_count = bundle_resolved(&b.id) .map(|v| v.len().to_string()) .unwrap_or_else(|e| format!("ERR: {e}")); out.push_str(&format!( "| `{}` | {} | {} | {} |\n", b.id, b.extends.as_deref().map(|s| format!("`{s}`")).unwrap_or_else(|| "—".to_string()), b.plugins.len(), resolved_count, )); } out.push('\n');
out.push_str("## Per-bundle plugin lists\n\n"); for b in all_bundles() { out.push_str(&format!("### `{}`\n\n", b.id)); out.push_str(&format!("{}\n\n", b.description)); if let Some(parent) = &b.extends { out.push_str(&format!("Extends: `{parent}`\n\n")); } match bundle_resolved(&b.id) { Ok(plugins) if plugins.is_empty() => { out.push_str("_No plugins (bare host binary)._\n\n"); } Ok(plugins) => { for p in plugins { out.push_str(&format!("- `{}` — {}\n", p.id, p.description)); } out.push('\n'); } Err(e) => { out.push_str(&format!("**ERROR:** {e}\n\n")); } } }
out}
fn format_plugin_row(p: &crate::schema::PluginCatalogEntry) -> String { format!( "| `{}` | {} | `{}` | {} |\n", p.id, p.extension_points.as_ref().map(|v| v.iter().map(|s| format!("`{s}`")).collect::<Vec<_>>().join(", ")).unwrap_or_default(), p.default_source, if p.bundled_in.is_empty() { "—".to_string() } else { p.bundled_in.iter().map(|s| format!("`{s}`")).collect::<Vec<_>>().join(", ") }, )}
fn format_skill_row(p: &crate::schema::PluginCatalogEntry) -> String { format!( "| `{}` | {} | `{}` | {} |\n", p.id, p.exposes_tools.as_ref().map(|v| v.iter().map(|s| format!("`{s}`")).collect::<Vec<_>>().join(", ")).unwrap_or_default(), p.default_source, if p.bundled_in.is_empty() { "—".to_string() } else { p.bundled_in.iter().map(|s| format!("`{s}`")).collect::<Vec<_>>().join(", ") }, )}
fn format_composite_row(p: &crate::schema::PluginCatalogEntry) -> String { format!( "| `{}` | {} | {} | `{}` |\n", p.id, p.extension_points.as_ref().map(|v| v.iter().map(|s| format!("`{s}`")).collect::<Vec<_>>().join(", ")).unwrap_or_default(), p.exposes_tools.as_ref().map(|v| v.iter().map(|s| format!("`{s}`")).collect::<Vec<_>>().join(", ")).unwrap_or_default(), p.default_source, )}- Step 4: Wire it from lib.rs
Add pub mod docs; to crates/vox-plugin-catalog/src/lib.rs (under the existing pub mod schema; line).
- Step 5: Run to verify it passes
cargo test -p vox-plugin-catalog --test docs_generationExpected: 3 tests PASS.
- Step 6: Commit
git add crates/vox-plugin-catalog/src/docs.rs crates/vox-plugin-catalog/src/lib.rs crates/vox-plugin-catalog/tests/docs_generation.rsgit commit -m "feat(plugin-catalog): add markdown renderers for catalog and bundles reference docs"Task 10: Add vox-plugin-catalog to workspace deps and vox-cli
Section titled “Task 10: Add vox-plugin-catalog to workspace deps and vox-cli”Files:
-
Modify:
Cargo.toml(workspace root) -
Modify:
crates/vox-cli/Cargo.toml -
Step 1: Confirm vox-plugin-catalog isn’t yet a workspace dep
Run:
grep -n "vox-plugin-catalog" Cargo.tomlExpected: only the members = ["crates/*"] line implicitly includes it; no entry in [workspace.dependencies].
- Step 2: Add to workspace dependencies
Edit Cargo.toml (workspace root) [workspace.dependencies] section, inserting in alphabetical order among the other vox-* entries:
vox-plugin-catalog = { path = "crates/vox-plugin-catalog" }- Step 3: Add as a dependency in vox-cli
Edit crates/vox-cli/Cargo.toml [dependencies] section, inserting in alphabetical order:
vox-plugin-catalog = { workspace = true }- Step 4: Verify the workspace still builds
Run:
cargo check -p vox-cliExpected: green check, no errors.
- Step 5: Commit
git add Cargo.toml crates/vox-cli/Cargo.toml Cargo.lockgit commit -m "chore(plugin-catalog): expose vox-plugin-catalog as a workspace dep and to vox-cli"Task 11: vox ci generate-plugin-catalog-docs CLI command
Section titled “Task 11: vox ci generate-plugin-catalog-docs CLI command”Files:
- Create:
crates/vox-cli/src/commands/ci/generate_plugin_catalog_docs.rs - Modify:
crates/vox-cli/src/commands/ci/mod.rs - Modify:
crates/vox-cli/src/commands/ci/cmd_enums.rs - Modify:
crates/vox-cli/src/commands/ci/run_body.rs
Before editing, read each of the three modify-target files to understand the existing dispatch pattern. The command catalog model in command_catalog.rs and the registry in command_registry_model.rs describe how subcommands are registered; follow the pattern used by other ci/<verb>.rs files such as capability_snapshot.rs.
- Step 1: Read the existing pattern
Run (review only, no edits yet):
cat crates/vox-cli/src/commands/ci/capability_snapshot.rscat crates/vox-cli/src/commands/ci/cmd_enums.rs | head -50cat crates/vox-cli/src/commands/ci/run_body.rs | head -30cat crates/vox-cli/src/commands/ci/mod.rs | head -30Note where CapabilitySnapshot (or similar) is declared, exported, and dispatched. The new variant must follow the same wiring.
- Step 2: Write the failing test
Create crates/vox-cli/tests/generate_plugin_catalog_docs_smoke.rs:
//! Smoke test that the new CLI command can be invoked and writes both//! generated docs to disk.
use std::process::Command;
#[test]fn generates_both_docs() { let tmp = tempfile::tempdir().expect("tempdir"); let cat_path = tmp.path().join("plugin-catalog.generated.md"); let bun_path = tmp.path().join("distribution-bundles.generated.md");
let status = Command::new(env!("CARGO_BIN_EXE_vox")) .args([ "ci", "generate-plugin-catalog-docs", "--catalog-out", cat_path.to_str().unwrap(), "--bundles-out", bun_path.to_str().unwrap(), ]) .status() .expect("vox should run");
assert!(status.success(), "command should exit 0"); let cat = std::fs::read_to_string(&cat_path).expect("catalog file should exist"); let bun = std::fs::read_to_string(&bun_path).expect("bundles file should exist"); assert!(cat.contains("Plugin Catalog (Generated)")); assert!(bun.contains("Distribution Bundles (Generated)"));}Add tempfile to crates/vox-cli/Cargo.toml [dev-dependencies] if not already present:
tempfile = { workspace = true }- Step 3: Run to verify it fails
cargo test -p vox-cli --test generate_plugin_catalog_docs_smokeExpected: command unrecognized — process exits non-zero.
- Step 4: Implement the command
Write crates/vox-cli/src/commands/ci/generate_plugin_catalog_docs.rs:
//! `vox ci generate-plugin-catalog-docs`//!//! Regenerates the two auto-generated reference docs from//! `crates/vox-plugin-catalog/catalog.toml`. CI calls this with `--check`//! to fail on drift; humans call it with no `--check` flag to update.
use anyhow::{Context, Result};use std::path::PathBuf;
const DEFAULT_CATALOG_OUT: &str = "docs/src/reference/plugin-catalog.generated.md";const DEFAULT_BUNDLES_OUT: &str = "docs/src/reference/distribution-bundles.generated.md";
pub fn run(catalog_out: Option<PathBuf>, bundles_out: Option<PathBuf>, check: bool) -> Result<()> { let catalog_out = catalog_out.unwrap_or_else(|| PathBuf::from(DEFAULT_CATALOG_OUT)); let bundles_out = bundles_out.unwrap_or_else(|| PathBuf::from(DEFAULT_BUNDLES_OUT));
let cat = vox_plugin_catalog::docs::render_catalog_md(); let bun = vox_plugin_catalog::docs::render_bundles_md();
if check { let on_disk_cat = std::fs::read_to_string(&catalog_out) .with_context(|| format!("reading {}", catalog_out.display()))?; let on_disk_bun = std::fs::read_to_string(&bundles_out) .with_context(|| format!("reading {}", bundles_out.display()))?; if on_disk_cat != cat || on_disk_bun != bun { anyhow::bail!( "Generated catalog docs are out of date.\nRun: vox ci generate-plugin-catalog-docs" ); } println!("✓ plugin catalog docs are up to date"); return Ok(()); }
if let Some(parent) = catalog_out.parent() { std::fs::create_dir_all(parent).ok(); } if let Some(parent) = bundles_out.parent() { std::fs::create_dir_all(parent).ok(); } std::fs::write(&catalog_out, &cat) .with_context(|| format!("writing {}", catalog_out.display()))?; std::fs::write(&bundles_out, &bun) .with_context(|| format!("writing {}", bundles_out.display()))?; println!( "✓ wrote {} ({} bytes) and {} ({} bytes)", catalog_out.display(), cat.len(), bundles_out.display(), bun.len() ); Ok(())}- Step 5: Wire into ci/mod.rs
In crates/vox-cli/src/commands/ci/mod.rs, add the module declaration in alphabetical order:
pub mod generate_plugin_catalog_docs;- Step 6: Wire into cmd_enums.rs and run_body.rs following the existing pattern
In cmd_enums.rs, add the variant. The exact form depends on how the existing enum is structured — match the shape of CapabilitySnapshot or CapabilitySync. Example:
GeneratePluginCatalogDocs { #[arg(long)] catalog_out: Option<std::path::PathBuf>, #[arg(long)] bundles_out: Option<std::path::PathBuf>, #[arg(long)] check: bool,},In run_body.rs, add the dispatch arm matching the surrounding pattern:
CiCommand::GeneratePluginCatalogDocs { catalog_out, bundles_out, check } => { crate::commands::ci::generate_plugin_catalog_docs::run(catalog_out, bundles_out, check)}(Adapt names — CiCommand may be called something else; follow the file’s existing convention.)
- Step 7: Run the smoke test to verify it passes
cargo test -p vox-cli --test generate_plugin_catalog_docs_smokeExpected: PASS.
- Step 8: Run the command for real to produce the initial generated docs
cargo run -p vox-cli -- ci generate-plugin-catalog-docsExpected: writes docs/src/reference/plugin-catalog.generated.md and docs/src/reference/distribution-bundles.generated.md.
- Step 9: Commit
git add crates/vox-cli/src/commands/ci/generate_plugin_catalog_docs.rs crates/vox-cli/src/commands/ci/mod.rs crates/vox-cli/src/commands/ci/cmd_enums.rs crates/vox-cli/src/commands/ci/run_body.rs crates/vox-cli/Cargo.toml crates/vox-cli/tests/generate_plugin_catalog_docs_smoke.rs docs/src/reference/plugin-catalog.generated.md docs/src/reference/distribution-bundles.generated.mdgit commit -m "feat(cli): add vox ci generate-plugin-catalog-docs command + initial generated outputs"Task 12: Hand-rolled docs (plugin-manifest.md, plugin-catalog.md, distribution-bundles.md)
Section titled “Task 12: Hand-rolled docs (plugin-manifest.md, plugin-catalog.md, distribution-bundles.md)”Files:
- Create:
docs/src/reference/plugin-manifest.md - Create:
docs/src/reference/plugin-catalog.md - Create:
docs/src/reference/distribution-bundles.md
These are concept docs sitting alongside the auto-generated reference. They explain WHY and HOW; the generated docs list WHAT.
- Step 1: Write
plugin-manifest.md
Create docs/src/reference/plugin-manifest.md:
---title: "Plugin Manifest (Plugin.toml)"description: "Schema for the Plugin.toml file every Vox plugin ships."category: "Language Reference"status: "current"training_eligible: true---
# Plugin Manifest (Plugin.toml)
Every Vox plugin ships a `Plugin.toml` in its install directory describing what the plugin is, what it provides, and what the host needs to know to load it. This page is the schema reference. For the design rationale see [Plugin System Redesign (2026)](../architecture/plugin-system-redesign-2026.md).
## Common header
```toml[plugin]id = "<short-hyphenated-id>"name = "<human-readable name>"version = "<semver>"description = "<one-line description>"authors = ["..."]license = "<SPDX identifier>"homepage = "<url>"
[plugin.host]min-vox-version = "<semver>"Payload kinds
Section titled “Payload kinds”The [plugin.payload] block discriminates on kind:
code— ships acdylibper OS/arch and provides one or more code extension points.skill— ships aSKILL.mdand registers MCP tools.composite— ships both.
Code payload
Section titled “Code payload”[plugin.payload]kind = "code"abi-version = 1
[plugin.payload.provides]extension-points = ["MlBackend"]
[plugin.payload.requires]os = ["windows", "linux"]arch = ["x86_64"]native-libs = [ { name = "cudart", min-version = "12.0" }, { name = "cublas" },]
[plugin.payload.artifacts]"windows-x86_64" = "vox_plugin_<id>.dll""linux-x86_64" = "libvox_plugin_<id>.so""macos-aarch64" = "libvox_plugin_<id>.dylib"Skill payload
Section titled “Skill payload”[plugin.payload]kind = "skill"format-version = 1skill-md = "<filename>.skill.md"
[plugin.payload.tools]exposes = ["vox_tool_one", "vox_tool_two"]Composite payload
Section titled “Composite payload”[plugin.payload]kind = "composite"
[plugin.payload.code]abi-version = 1provides.extension-points = ["MeshDriver"]artifacts."linux-x86_64" = "libvox_plugin_<id>.so"
[plugin.payload.skill]format-version = 1skill-md = "<filename>.skill.md"tools.exposes = ["vox_tool_one"]Validation
Section titled “Validation”The manifest is parsed at host startup and validated against this schema. Failures are reported by vox plugin doctor with the offending field path.
- [ ] **Step 2: Write `plugin-catalog.md`**
Create `docs/src/reference/plugin-catalog.md`:
```markdown---title: "Plugin Catalog"description: "What the Vox plugin catalog is and how it relates to per-plugin manifests."category: "Language Reference"status: "current"training_eligible: true---
# Plugin Catalog
The plugin catalog is the SSOT of every first-party Vox plugin. It lives at [`crates/vox-plugin-catalog/catalog.toml`](../../../crates/vox-plugin-catalog/catalog.toml) and is exposed to the rest of the codebase via the `vox-plugin-catalog` crate.
## What the catalog is for
- Source of truth for `vox plugin list` (shows installed + available + incompatible).- Source of truth for `vox plugin install <id>` (resolves the install URL).- Source of truth for distribution bundle composition.- Source of truth for the auto-generated reference docs ([plugin catalog](plugin-catalog.generated.md), [distribution bundles](distribution-bundles.generated.md)).
## What the catalog is **not**
- Not the plugin manifest itself. Each installed plugin ships its own [`Plugin.toml`](plugin-manifest.md). The catalog is the directory of *known* plugins; the manifest is what an installed plugin declares about *itself*.- Not a marketplace. There is no central server, no ratings, no provenance beyond the URL in `default-source`. Third-party plugins are not in the catalog (and are not yet supported in v1; see the parent design spec).- Not a feature-flag table. Catalog entries describe *plugins* — units that can be installed at runtime — not Cargo features.
## Editing the catalog
1. Edit `catalog.toml`.2. `cargo build -p vox-plugin-catalog` — `build.rs` validates the file and fails the build with a clear error if any invariant is violated.3. `vox ci generate-plugin-catalog-docs` — regenerates the two `.generated.md` files.4. Commit the catalog change and the regenerated docs in the same commit.
CI guards (`vox ci plugin-catalog-parity`, `vox ci generate-plugin-catalog-docs --check`) enforce both invariants on every PR.- Step 3: Write
distribution-bundles.md
Create docs/src/reference/distribution-bundles.md:
---title: "Distribution Bundles"description: "What Vox bundles are, how to pick one, and how to roll your own."category: "Language Reference"status: "current"training_eligible: true---
# Distribution Bundles
A Vox bundle is a tarball with the `vox` host binary plus a curated `plugins/` directory. Every bundle ships *the same* host binary; what differs is which plugins are pre-installed.
## Picking a bundle
| If you want to… | Pick || -------------------------------------------------------- | -------------- || Try Vox locally with the default agent skills | `vox-fullstack`|| Train ML models locally on NVIDIA hardware | `vox-ml` || Run a headless backend server with mesh + cloud sync | `vox-server` || Run a node in a Populi mesh | `vox-mesh` || Use Vox as a managed cloud client (no local ML/mesh) | `vox-cloud-only`|| Run on edge / on-device with skills only | `vox-edge` || Hack on Vox itself with everything available | `vox-dev` || Build your own custom bundle | `vox-base` + plugins |
## Building a bundle
```bashvox bundle build vox-ml --target linux-x86_64 --out vox-ml-1.0.0-linux-x86_64.tar.gzBundles are reproducible from the catalog: the same Vox version + same catalog SHA produces byte-identical tarballs.
Defining your own bundle
Section titled “Defining your own bundle”External users add a bundle entry in their own catalog overlay (mechanism deferred to a follow-up sub-project) or assemble plugins manually with vox plugin install <id> after starting from vox-base.
For the first-party bundle list, see the auto-generated distribution-bundles.generated.md.
- [ ] **Step 4: Verify mdBook still parses (no dead links to in-tree files)**
Run:cargo run -p vox-doc-pipeline — —check 2>&1 | tail -20
Expected: green or warnings unrelated to the new docs. (The new docs are referenced by [research-index.md](research-index.md) indirectly through the parent spec; SUMMARY.md regeneration may be needed in Task 13.)
- [ ] **Step 5: Commit**git add docs/src/reference/plugin-manifest.md docs/src/reference/plugin-catalog.md docs/src/reference/distribution-bundles.md git commit -m “docs(reference): add hand-rolled plugin-manifest, plugin-catalog, and distribution-bundles concept docs”
---
### Task 13: Regenerate `SUMMARY.md` via `vox-doc-pipeline`
**Files:**- Modify (regenerated): `docs/src/SUMMARY.md`- Modify (regenerated): `docs/src/architecture/architecture-index.md`- Modify (regenerated): `docs/src/feed.xml`
Per [`AGENTS.md`](../../../AGENTS.md): these files are auto-generated, NEVER hand-edited.
- [ ] **Step 1: Regenerate**
Run:cargo run -p vox-doc-pipeline
Expected: writes updated `SUMMARY.md`, `architecture-index.md`, `feed.xml`. Inspect with `git diff docs/src/SUMMARY.md` to confirm the new reference docs appear.
- [ ] **Step 2: Commit**git add docs/src/SUMMARY.md docs/src/architecture/architecture-index.md docs/src/feed.xml git commit -m “chore(docs): regenerate SUMMARY for plugin-catalog reference additions”
---
### Task 14: Empty out `vox-build-meta` features and rewrite `build.rs`
**Files:**- Modify: `crates/vox-build-meta/Cargo.toml`- Modify: `crates/vox-build-meta/build.rs`
- [ ] **Step 1: Replace the `[features]` block**
Edit `crates/vox-build-meta/Cargo.toml`. Replace the existing `[features]` block with:
```toml[features]# vox-build-meta's feature stubs have been retired. Optional capabilities are# now installable plugins managed by `vox plugin install <id>`. See# crates/vox-plugin-catalog/catalog.toml and# docs/src/architecture/plugin-system-redesign-2026.md.(An empty [features] block is fine.)
- Step 2: Replace
build.rs
Replace crates/vox-build-meta/build.rs entirely with:
//! vox-build-meta's feature-probing build script has been retired. Optional//! capabilities are now installable plugins managed by `vox plugin install`.//!fn main() { // Keep the env var defined so existing call sites compile, but always // emit the empty list. Real capability discovery now goes through // vox-plugin-host (lands in SP2). println!("cargo:rustc-env=VOX_BUILD_FEATURES=[]");}- Step 3: Verify it still builds
cargo build -p vox-build-metaExpected: green build, no warnings.
- Step 4: Commit
git add crates/vox-build-meta/Cargo.toml crates/vox-build-meta/build.rsgit commit -m "chore(build-meta): retire feature-stub probes; capabilities are now plugins"Task 15: Convert vox-build-meta API to deprecation shims
Section titled “Task 15: Convert vox-build-meta API to deprecation shims”Files:
-
Modify:
crates/vox-build-meta/src/lib.rs -
Test:
crates/vox-build-meta/tests/deprecation.rs -
Step 1: Write the failing test
Create crates/vox-build-meta/tests/deprecation.rs:
use vox_build_meta::{active_features, has, require};
#[test]fn active_features_is_empty() { assert_eq!(active_features(), Vec::<&'static str>::new());}
#[test]fn has_always_returns_false() { assert!(!has("gpu")); assert!(!has("mens-candle-cuda")); assert!(!has("any-feature-name"));}
#[test]fn require_returns_error_with_install_command_in_message() { let err = require("mens-candle-cuda", "vox plugin install mens-candle-cuda") .expect_err("require should fail when feature is absent"); let msg = format!("{err}"); assert!(msg.contains("vox plugin install mens-candle-cuda"), "msg was: {msg}");}- Step 2: Run to verify it fails or is green-with-old-behavior
cargo test -p vox-build-meta --test deprecationExpected: probably PASS for active_features and has (both already return empty/false because Task 14 set VOX_BUILD_FEATURES=[]), but the require message still references “rebuild with” instead of “vox plugin install”. The third test should FAIL or be marginal.
- Step 3: Rewrite lib.rs
Replace crates/vox-build-meta/src/lib.rs with:
//! vox-build-meta is in deprecation. Its API is preserved as shims so existing//! call sites compile, but every shim returns "no features available" and//! `require` emits an error pointing at `vox plugin install <id>`.//!//! Direct callers should migrate to `vox-plugin-host`'s registry queries//! (lands in SP2). Removal of this crate is scheduled for SP6.//!//! See: docs/src/architecture/plugin-system-redesign-2026.md
#[deprecated(note = "vox-build-meta is being removed; query vox-plugin-host instead. See plugin-system-redesign-2026.md")]pub const FEATURES_JSON: &str = env!("VOX_BUILD_FEATURES");
#[allow(deprecated)]pub fn active_features() -> Vec<&'static str> { serde_json::from_str(FEATURES_JSON).unwrap_or_default()}
pub fn has(_feature: &str) -> bool { // Always false: feature stubs are retired. Capability presence now flows // through the plugin host registry. false}
#[derive(Debug, thiserror::Error)]#[error( "This Vox capability requires the '{feature}' plugin, which is not installed.\n\nTo install it, run:\n\n {install_cmd}\n\nSee: docs/src/reference/plugins.md")]pub struct FeatureMissingError { pub feature: &'static str, pub install_cmd: &'static str,}
/// Deprecated. Always returns `Err`. The `install_cmd` argument is preserved/// for backwards compatibility but should be the new `vox plugin install <id>`/// invocation, not a `cargo build --features` invocation.pub fn require( feature: &'static str, install_cmd: &'static str,) -> Result<(), FeatureMissingError> { Err(FeatureMissingError { feature, install_cmd, })}Note: this changes the field name rebuild_cmd → install_cmd. If existing callers reference the field by name, they break. Run cargo check --workspace after this step (next).
- Step 4: Find and update existing callers
Run:
rg "rebuild_cmd|FeatureMissingError|vox_build_meta::require|vox_build_meta::has" --type rustFor each callsite found, replace rebuild_cmd: "cargo build --features ..." with install_cmd: "vox plugin install <id>". Use the mapping from the parent spec (Task 4 of this plan) to pick the right <id> per feature. If a callsite uses literal "cargo build --features gpu,mens-candle-cuda", the new value is "vox plugin install tensor-burn-wgpu mens-candle-cuda".
- Step 5: Verify the workspace still builds
cargo check --workspaceExpected: green, with possibly some deprecated warnings on the FEATURES_JSON constant. That’s intentional — the warning prompts callers to migrate.
- Step 6: Run the deprecation test
cargo test -p vox-build-meta --test deprecationExpected: 3 tests PASS.
- Step 7: Commit
git add crates/vox-build-meta/src/lib.rs crates/vox-build-meta/tests/deprecation.rs# Plus any modified callsites:git add <files-from-step-4>git commit -m "feat(build-meta): convert API to deprecation shims pointing at vox plugin install"Task 16: vox ci plugin-catalog-parity CLI command
Section titled “Task 16: vox ci plugin-catalog-parity CLI command”Files:
- Create:
crates/vox-cli/src/commands/ci/plugin_catalog_parity.rs - Modify:
crates/vox-cli/src/commands/ci/mod.rs - Modify:
crates/vox-cli/src/commands/ci/cmd_enums.rs - Modify:
crates/vox-cli/src/commands/ci/run_body.rs
This guard ensures every in-tree Plugin.toml (under crates/vox-plugin-*/Plugin.toml and similar) corresponds to a catalog entry, and every catalog entry whose default-source is local:<path> points to an existing path. In SP1 there are no in-tree Plugin.toml files yet — they land in SP3+. So the guard’s body for SP1 is “scan and pass if no Plugin.toml files exist”.
- Step 1: Read existing pattern
cat crates/vox-cli/src/commands/ci/capability_sync.rsNote the structure used for “scan + assert” CI guards.
- Step 2: Write the failing test
Create crates/vox-cli/tests/plugin_catalog_parity_smoke.rs:
use std::process::Command;
#[test]fn parity_passes_when_no_plugin_tomls_exist() { let status = Command::new(env!("CARGO_BIN_EXE_vox")) .args(["ci", "plugin-catalog-parity"]) .status() .expect("vox should run"); assert!(status.success(), "parity should pass with empty plugin tree");}- Step 3: Run to verify it fails (command unrecognized)
cargo test -p vox-cli --test plugin_catalog_parity_smokeExpected: command exits non-zero with “unrecognized subcommand”.
- Step 4: Implement the command
Write crates/vox-cli/src/commands/ci/plugin_catalog_parity.rs:
//! `vox ci plugin-catalog-parity`//!//! Enforces that every in-tree `Plugin.toml` corresponds to a catalog entry//! and every catalog entry referencing a local path resolves. In SP1 the//! tree contains no Plugin.toml files yet — guard passes trivially. SP3+//! adds real plugins and the guard starts checking.
use anyhow::{Context, Result};use serde::Deserialize;use std::path::Path;
#[derive(Deserialize)]#[serde(rename_all = "kebab-case")]struct ManifestHead { plugin: PluginHead,}
#[derive(Deserialize)]struct PluginHead { id: String,}
pub fn run() -> Result<()> { let mut errors: Vec<String> = Vec::new(); let catalog_ids: std::collections::HashSet<&str> = vox_plugin_catalog::all_plugins() .iter() .map(|p| p.id.as_str()) .collect();
// Scan for Plugin.toml under crates/. let crates_root = Path::new("crates"); if crates_root.is_dir() { for entry in walkdir::WalkDir::new(crates_root) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.file_name() == "Plugin.toml") { let path = entry.path(); let raw = std::fs::read_to_string(path) .with_context(|| format!("reading {}", path.display()))?; let head: ManifestHead = match toml::from_str(&raw) { Ok(v) => v, Err(e) => { errors.push(format!("{}: parse error: {e}", path.display())); continue; } }; if !catalog_ids.contains(head.plugin.id.as_str()) { errors.push(format!( "{}: plugin id '{}' is not in the catalog (add to crates/vox-plugin-catalog/catalog.toml)", path.display(), head.plugin.id )); } } }
if errors.is_empty() { println!("✓ plugin catalog parity ok ({} entries in catalog)", catalog_ids.len()); Ok(()) } else { for e in &errors { eprintln!("✗ {e}"); } anyhow::bail!("plugin catalog parity failed with {} error(s)", errors.len()) }}- Step 5: Add walkdir to vox-cli dependencies
Verify walkdir is already a workspace dep:
grep "^walkdir" Cargo.tomlIf present, add walkdir = { workspace = true } to crates/vox-cli/Cargo.toml [dependencies]. If absent, add it both to the workspace [workspace.dependencies] (with walkdir = "2") and to vox-cli.
- Step 6: Wire dispatch (mod.rs, cmd_enums.rs, run_body.rs)
Same pattern as Task 11. Add module declaration, enum variant PluginCatalogParity, and dispatch arm:
CiCommand::PluginCatalogParity => { crate::commands::ci::plugin_catalog_parity::run()}- Step 7: Run the smoke test
cargo test -p vox-cli --test plugin_catalog_parity_smokeExpected: PASS.
- Step 8: Run for real
cargo run -p vox-cli -- ci plugin-catalog-parityExpected: ✓ plugin catalog parity ok (18 entries in catalog).
- Step 9: Commit
git add crates/vox-cli/src/commands/ci/plugin_catalog_parity.rs crates/vox-cli/src/commands/ci/mod.rs crates/vox-cli/src/commands/ci/cmd_enums.rs crates/vox-cli/src/commands/ci/run_body.rs crates/vox-cli/Cargo.toml crates/vox-cli/tests/plugin_catalog_parity_smoke.rs Cargo.lockgit commit -m "feat(cli): add vox ci plugin-catalog-parity guard"Task 17: Update AGENTS.md auto-generated list
Section titled “Task 17: Update AGENTS.md auto-generated list”Files:
-
Modify:
AGENTS.md -
Step 1: Read the current auto-generated list
grep -A 12 "Auto-generated documentation" AGENTS.md- Step 2: Add the two new generated files
In AGENTS.md, in the ## Auto-generated documentation files (do not edit manually) section, add bullets for the new files. Insert in the same style as the existing cli-command-surface.generated.md entry:
- `docs/src/reference/plugin-catalog.generated.md`, `docs/src/reference/distribution-bundles.generated.md` — generated reference rolled from `crates/vox-plugin-catalog/catalog.toml`. Regenerate with `cargo run -p vox-cli -- ci generate-plugin-catalog-docs`. CI guard: `cargo run -p vox-cli -- ci generate-plugin-catalog-docs --check`.- Step 3: Commit
git add AGENTS.mdgit commit -m "docs(agents): list plugin-catalog generated docs in auto-generated section"Task 18: Final acceptance — workspace check + spec criteria
Section titled “Task 18: Final acceptance — workspace check + spec criteria”Files: none (verification only)
- Step 1: Workspace builds clean
cargo check --workspace 2>&1 | tail -5Expected: zero errors. May include deprecation warnings on vox_build_meta::FEATURES_JSON, which are intentional.
- Step 2: All vox-plugin-catalog tests pass
cargo test -p vox-plugin-catalogExpected: every test green. Test count: ~14 (smoke + schema_roundtrip + catalog_load + bundle_resolution + catalog_validation + docs_generation).
- Step 3: All vox-build-meta tests pass
cargo test -p vox-build-metaExpected: 3 deprecation tests PASS.
- Step 4: New CLI commands work end-to-end
cargo run -p vox-cli -- ci generate-plugin-catalog-docs --checkcargo run -p vox-cli -- ci plugin-catalog-parityExpected: both exit 0.
- Step 5: Spec acceptance — verify against parent spec’s SP1 acceptance section
From plugin-system-redesign-2026.md SP1 acceptance:
| Criterion | Status |
|---|---|
cargo build --workspace succeeds with zero --features flags. | Verify with cargo build --workspace. |
vox-plugin-catalog::all_plugins() returns ≥18 entries (9 code + 9 skill). | Verify: cargo run -p vox-plugin-catalog --example count (or rg test count). |
bundle_resolved("vox-ml") resolves through extends and returns the union. | Verified by tests/bundle_resolution.rs (Task 7). |
| Generated reference docs list every plugin and every bundle. | Verified by tests/docs_generation.rs (Task 9). |
If any criterion fails, fix before declaring SP1 done.
- Step 6: Confirm no leftover untracked or modified files
git statusExpected: clean working tree (or only .claude/settings.local.json modifications, which are local-only).
- Step 7: SP1 done. Final note for the engineer:
SP1 is structurally complete. The catalog, schemas, generators, deprecation shims, and CI guards are in place. The next sub-project (SP2: Host ABI & Loader) introduces
vox-plugin-apiandvox-plugin-host; both depend on this catalog being the SSOT. Until SP2 lands there is no runtime plugin behavior —vox plugin installdoes not yet exist.
Spec coverage check (self-review)
Section titled “Spec coverage check (self-review)”| SP1 spec deliverable | Plan task |
|---|---|
vox-plugin-catalog crate with catalog.toml, build.rs, lib.rs | 1, 3, 8, 9 |
all_plugins, all_bundles, bundle_resolved accessors | 3, 7 |
| Catalog entries for 9 retired Cargo-feature plugins | 4 |
| Catalog entries for current built-in skills | 5 |
| Bundle entries (8 total) | 6 |
Hand-rolled docs (plugin-manifest.md, plugin-catalog.md, distribution-bundles.md) | 12 |
Auto-generated reference (plugin-catalog.generated.md, distribution-bundles.generated.md) | 9, 11 |
vox-build-meta feature-stub deletion + deprecation shim | 14, 15 |
vox ci plugin-catalog-parity CI guard | 16 |
vox ci generate-plugin-catalog-docs (with --check for CI drift) | 11 |
| AGENTS.md auto-generated file list update | 17 |
| SUMMARY.md / architecture-index regeneration | 13 |
| Workspace check + spec acceptance | 18 |
All SP1 deliverables map to at least one task. No gaps.