Skip to content

feat(plugin): add UserLevel plugin preflight resolver (PR 3/5)#31

Open
shaun0927 wants to merge 2 commits into
Q00:release/bootstrapfrom
shaun0927:feat/plugin-user-level-resolver
Open

feat(plugin): add UserLevel plugin preflight resolver (PR 3/5)#31
shaun0927 wants to merge 2 commits into
Q00:release/bootstrapfrom
shaun0927:feat/plugin-user-level-resolver

Conversation

@shaun0927
Copy link
Copy Markdown

Summary

PR 3 of the five-PR plan under SSOT #25 (see PR #3 / docs/userlevel-plugin-dispatch.md).

Adds a pure resolver from ooo <plugin> <command> [args ...]-shaped input to a structured PreflightResult. The resolver is the decision data the dispatch adapter and TUI panel (PR 4) will consume.

Stacking

Requires PR #30 (PR 2) to be merged first — this branch is stacked on top of feat/plugin-user-level-capability because the resolver consumes Ourocode.Plugin.UserLevel.Capability structs. Until PR #30 merges, this PR's diff will include PR 2's commits.

What ships

  • Ourocode.Plugin.UserLevel.PreflightResult — struct describing what dispatch would do: kind (:unique_match / :ambiguous / :unknown / :not_applicable), matched plugin & command, parsed argv tail, trust state, remediation string, risk class, expected artifacts, continuation policy, candidates (ambiguous case), and match_explanation (matched_by + confidence).
  • Ourocode.Plugin.UserLevel.Resolver
    • resolve/2 :: (task_input, capabilities) :: PreflightResult.t() — pure function.
    • applies_to?/2 — cheap predicate routing layers will call (PR 4) to decide whether to swap their routing decision before invoking dispatch.

Resolution rules (intentionally narrow)

  • Direct ooo/ouroboros prefix only. Free-form natural language is deferred until the exact path is stable.
  • Plugin id and command name/alias matching is exact (case-insensitive at lookup; capability fields are preserved verbatim).
  • Argument tokens are passed through with original casing — no shell parsing, no case folding. Shell-injection input becomes argv tokens that Dispatcher.guarded_external_command_runner will still guard at execution time (PR 4).
  • Duplicate plugin ids surface as :ambiguous with candidate list, never a silent guess.
  • Trust state: :allowed when the capability declares trust_scope, :missing otherwise (with a remediation suggestion).

What is intentionally NOT in this PR

  • No Router / Dispatcher changes. Those land in PR 4 together with UserLevelPluginInvocation, so an executable end-to-end path is reviewable in one place.
  • No TUI panel. PR 4 ships PreflightPanel.
  • No decision journal. PR 5.

Closes

Testing

  • 2 ExUnit modules (async: true) — 576 LOC total with lib code.
  • Covers unique_match (canonical + alias + mixed-case prefix + argument case preservation + shell-injection input), trust missing remediation, unknown plugin / unknown command / missing token reasons, ambiguous duplicate ids, not_applicable inputs, and the applies_to? predicate.
  • Local Elixir toolchain unavailable in authoring environment; CI is the source of truth for these tests. Reviewers please confirm mix test test/ourocode/plugin/user_level/{resolver,preflight_result}_test.exs is green.

🤖 Generated with Claude Code

shaun0927 and others added 2 commits May 25, 2026 18:13
Introduces the Ourocode.Plugin.UserLevel namespace that lets the runtime
discover installed Ouroboros UserLevel plugins and treat their commands
as first-class registry entries without reimplementing trust or storage
semantics.

What is added:

- Ourocode.Plugin.UserLevel.Capability + .Capability.Command
  Normalized identity and command surface (plugin_id, source, version,
  install scope, trust scopes, manifest digest, declared commands,
  expected artifacts, continuation hints). Identity stability via
  (plugin_id, version, manifest_digest) tuple so re-discovery without
  manifest changes returns the same struct.

- Ourocode.Plugin.UserLevel.Discovery
  Behaviour with a Discovery.run/2 helper that normalizes raw
  descriptors into Capability structs and surfaces per-descriptor
  validation errors separately so one bad command never loses a whole
  plugin.

- Ourocode.Plugin.UserLevel.Discovery.OuroborosCLI
  First discovery adapter; invokes `ouroboros plugin list --json` via a
  pluggable command runner. Tests inject a stub runner to avoid spawning
  real processes. Failure modes (exit != 0, runner unavailable,
  malformed JSON, unexpected shape) all surface as structured errors.

- Ourocode.Plugin.UserLevel.Registry
  Small Agent that caches the latest discovery snapshot with a 60 s
  TTL, explicit refresh, and identity-preserving merge. Discovery
  failure degrades the snapshot but preserves last good capabilities,
  so missing/broken ouroboros CLI never blocks boot.

- Ourocode.Plugin.UserLevel.RegistryEntry
  Projects Capability into the existing Command.Registry plugin-source
  entry shape (mirrors PluginSurfaceEntry's metadata so the existing
  CapabilityPreflight.Trust and Projection modules apply unchanged).

What is NOT changed in this PR:

- No supervision wiring — the registry is standalone and ships dead
  code until PR 4 wires it into application_services.ex alongside the
  dispatch adapter that needs it. This keeps PR 2 boot-safe.
- No new slash command — `/plugins refresh` ships with PR 4.
- No router/dispatcher changes — those land in PR 3.

Tests: 5 ExUnit files (1255 LOC total with lib code) cover capability
shape, identity, command lookup, discovery normalization,
OuroborosCLI parsing of the superpowers fixture, registry TTL +
degraded handling, identity-stable merge, and registry projection
into the existing plugin-source entry shape.

Closes Q00#5
Closes Q00#8
Closes Q00#9
Closes Q00#18
Closes Q00#27
Closes Q00#29

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a pure resolver from `ooo <plugin> <command> [args ...]`-shaped
input to a structured PreflightResult. The result is read-only: it
describes what dispatch would do without executing, mutating trust, or
touching the registry.

What is added:

- Ourocode.Plugin.UserLevel.PreflightResult
  Struct with kind (:unique_match | :ambiguous | :unknown |
  :not_applicable), task_input, matched plugin/command, parsed argv
  tail, trust state, remediation string, risk class, expected
  artifacts, continuation policy, candidates (ambiguous case), and a
  match_explanation (matched_by + confidence).

- Ourocode.Plugin.UserLevel.Resolver
  resolve/2 turns task_input + capabilities into a PreflightResult.
  applies_to?/2 is the cheap predicate routing layers call to decide
  whether to swap their routing decision before invoking dispatch.

Resolution rules (intentionally narrow):

  * Direct ooo/ouroboros prefix only. Free-form natural language is
    deferred until the exact path is stable.
  * Plugin id and command name/alias matching is exact (case
    insensitive at lookup time but capability fields are preserved).
  * Argument tokens are passed through verbatim — no shell parsing,
    no case folding. Shell injection input becomes argv tokens that
    Dispatcher.guarded_external_command_runner will still guard.
  * Duplicate plugin ids surface as :ambiguous with candidates, never
    silent guess.
  * Trust state: :allowed when the capability declares trust_scope,
    :missing otherwise (with a remediation suggestion that points the
    user at `ouroboros plugin trust ...`).

What is intentionally NOT in this PR:

- Router or Dispatcher route additions. Those land in PR 4 together
  with the UserLevelPluginInvocation adapter, so dispatch can be tested
  end-to-end in one place.
- TUI panel rendering. PR 4 ships PreflightPanel.
- Decision journal. PR 5.

Tests: 2 ExUnit modules (async: true) covering unique_match (canonical
+ alias + mixed-case + arg case preservation + shell injection input),
trust missing, unknown plugin/command, missing tokens, ambiguous
duplicate ids, not_applicable inputs, and applies_to? predicate.

Closes Q00#16
Closes Q00#23

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shaun0927 shaun0927 force-pushed the feat/plugin-user-level-resolver branch from 28605d9 to 48f0917 Compare May 25, 2026 09:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vision: make plugin command resolution explainable when names are ambiguous Vision: make plugin resolution an explicit preflight step

1 participant