Skip to content

feat(plugin): artifact capture, continuation policy, decision journal (PR 5/5)#33

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

feat(plugin): artifact capture, continuation policy, decision journal (PR 5/5)#33
shaun0927 wants to merge 4 commits into
Q00:release/bootstrapfrom
shaun0927:feat/plugin-user-level-artifacts

Conversation

@shaun0927
Copy link
Copy Markdown

Summary

Final PR of the five-PR plan under SSOT #25 (see PR #3 / docs/userlevel-plugin-dispatch.md). Closes the loop from UserLevel plugin dispatch (PR 4) to verified follow-up:

  • artifacts are discovered via the plugin's own declared expected_artifacts globs
  • a conservative continuation policy decides whether to suggest or auto-run an ooo run seed_path=... step
  • every decision phase is appended to the existing Ourocode.Journal.Writer for audit and replay

Stacking

Requires PR #30, PR #31, PR #32 to be merged first. Stacked on feat/plugin-user-level-dispatch.

What ships

Ourocode.Plugin.UserLevel.ArtifactWatcher

Pure scan over a CommandCapability's expected_artifacts globs rooted at the dispatch cwd. Classifies :seed / :handoff / :report / :log / :other by basename. When lstat? is enabled (default), attaches size, sha256 digest (capped at 1 MiB), and generated_at. Never hardcodes plugin-internal storage paths (#25 non-goal honored).

Ourocode.Plugin.UserLevel.Continuation

Pure policy:

  • :read_only:none
  • :handoff_producing without a seed → :none
  • :handoff_producing with a seed → :suggest
  • :auto_run only when the user's prompt contains an explicit opt-in phrase: "then run the generated handoff", "then run the seed", "and run the seed", "이어서 실행", "이후 실행".
  • :destructive commands never auto-run.

Ourocode.Plugin.UserLevel.DecisionJournal

Appends one structured event per phase into the existing Ourocode.Journal.Writer:

  • user_level_preflight
  • user_level_dispatch
  • user_level_artifact (one event per discovered artifact)
  • user_level_continuation

Accepts a Path.t() or a 1-arity function so tests can capture events without filesystem writes.

Ourocode.Runtime.UserLevelPluginInvocation (extended)

After a successful runner call, scans declared artifacts, decides continuation, and attaches both to the result envelope as optional :artifacts and :continuation keys. When :decision_journal is provided in the dispatch context, every phase emits a structured event. Behaviour without these context keys is unchanged from PR 4 (backward-compatible).

What is intentionally NOT in this PR

Closes

Testing

  • 4 ExUnit modules (async: true):
    • ArtifactWatcher (5 tests, real tmp filesystem)
    • Continuation (10 tests across risk classes + opt-in phrases en/ko)
    • DecisionJournal (6 tests via capture function)
    • UserLevelPluginInvocation post-execution integration (4 tests: seed detection + auto_run intent + 4-phase journal emission + blocked-still-emits-preflight-and-dispatch)
  • Local Elixir toolchain unavailable in authoring environment; CI is the source of truth. Reviewers please confirm mix test is green before approving.

Plan completion

With this PR merged, the five-PR sequence under SSOT #25 is complete:

PR URL Status
1 — vision docs + umbrella close #3 open
2 — capability layer #30 open
3 — preflight resolver #31 open
4 — dispatch adapter #32 open
5 — artifact + continuation + journal (this) this PR open

🤖 Generated with Claude Code

shaun0927 and others added 4 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>
Wires the UserLevel plugin layer into the runtime dispatch contract so
`ooo <plugin> <command>` input can be routed through the existing
guarded external command runner without bypassing trust, risk class,
or shell-injection protection.

What is added:

- Ourocode.Runtime.UserLevelPluginInvocation
  Implements the runtime Adapter behaviour. Reads :capabilities from
  the dispatch context, runs the Resolver, evaluates trust state and
  risk class, and either invokes via :external_command_runner or
  returns a structured :blocked result. Argv is always a list — the
  Dispatcher's guarded_external_command_runner stays the
  shell-injection authority.

  Blocked reasons surfaced:
    :trust_missing, :ambiguous_match, :unknown_plugin_or_command,
    :destructive_action_requires_approval, :not_user_level_plugin_input,
    {:external_command_failed, reason}

  Destructive risk_class commands require explicit
  context.destructive_action_approved? = true. Default fails closed.

- Ourocode.Plugin.UserLevel.PreflightView
  JSON-safe projection of a PreflightResult shaped like the existing
  Ourocode.Command.CapabilityPreflight.Projection. Lets any UI render
  UserLevel plugin preflight using the same shape as the slash command
  preflight (trust, side_effects, candidates, match_explanation).

- Ourocode.Plugin.UserLevel.Entry
  Router refinement helper. Takes a parsed TaskRequest and a capability
  snapshot; when the input targets a known UserLevel plugin, swaps the
  routing_decision to :user_level_plugin and attaches plugin_id. Keeps
  Ourocode.Runtime.Router itself transport- and registry-agnostic.

- Ourocode.Runtime.Dispatcher.RouteResolution
  Adds :user_level_plugin to @supported_routes, adds adapter_keys/1
  clauses (plugin-id-scoped + generic fallback), reuses existing
  validate_adapter_route guard to reject decisions that incorrectly
  carry adapter_route.

- Ourocode.TaskRequest
  routing_decision type extended with :user_level_plugin kind /
  execution_route and optional :plugin_id field.

What is intentionally NOT in this PR:

- Registry supervision wiring in ApplicationServices. Until the live
  TUI integration ships, callers pass capability snapshots directly via
  context. PR 5 wires the registry into the runtime supervision tree
  alongside the artifact watcher.
- /plugins refresh slash command. Ships with PR 5 once the registry is
  supervised.
- Continuation/auto-run policy and artifact detection. PR 5.

Tests: 4 ExUnit modules (async: true) — adapter happy path, trust
blocked, unknown command blocked, destructive blocked + approved,
shell-injection argv passthrough, runner contract errors, view
projection across kinds, entry refinement (rewrites only when
applicable), and dispatcher route validation + adapter_keys.

Closes Q00#15
Closes Q00#17 (minimal — trust-blocked structured error path)
Closes Q00#20
Closes Q00#21

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rnal

Closes the loop from UserLevel plugin dispatch to verified follow-up:
artifacts are discovered via the plugin's own declared globs, a
conservative continuation policy decides whether to suggest or
auto-run an `ooo run seed_path=...` step, and every decision phase
is appended to the existing journal writer for audit and replay.

What is added:

- Ourocode.Plugin.UserLevel.ArtifactWatcher
  Pure scan over a CommandCapability's expected_artifacts globs
  rooted at the dispatch cwd. Classifies seed / handoff / report /
  log / other by basename. When lstat? is enabled (default),
  attaches size, sha256 digest (capped at 1 MiB), and generated_at.
  Never hardcodes plugin-internal storage paths.

- Ourocode.Plugin.UserLevel.Continuation
  Pure policy: read_only → :none, handoff_producing without a seed
  → :none, handoff_producing with a seed → :suggest, and :auto_run
  only when the user's prompt contains an explicit opt-in phrase
  ("then run the generated handoff", "이어서 실행", etc.).
  Destructive commands never auto-run.

- Ourocode.Plugin.UserLevel.DecisionJournal
  Appends one structured event per phase
  (:user_level_preflight, :user_level_dispatch,
  :user_level_artifact, :user_level_continuation) into the existing
  Ourocode.Journal.Writer. Accepts a Path.t or a 1-arity function so
  tests can capture events without filesystem writes.

What is modified:

- Ourocode.Runtime.UserLevelPluginInvocation
  After a successful runner call, scans declared artifacts, decides
  continuation, and attaches both to the result envelope as optional
  :artifacts and :continuation keys. When :decision_journal is
  provided in the dispatch context, every phase emits a structured
  event. Behaviour without these context keys is unchanged from
  PR 4 (backward-compatible).

What is intentionally NOT in this PR:

- Registry supervision wiring in ApplicationServices. The standalone
  registry is still usable today via Ourocode.Plugin.UserLevel.Registry
  callers (e.g. CLI integration); supervision wiring is a runtime
  integration concern that should be planned alongside TUI rendering
  and tracked in a follow-up.
- TUI panel rendering. PR 4 ships the JSON-safe PreflightView; the
  actual TUI integration is downstream UI work.
- /plugins refresh slash command. Follow-up.
- Full failure recovery (Q00#17 beyond structured trust-blocked errors)
  and durable session lifecycle (Q00#24). Deferred per
  docs/userlevel-plugin-dispatch.md "Deferred" section.

Tests: 4 ExUnit modules (async: true). ArtifactWatcher (5 tests with
real tmp filesystem), Continuation (10 tests across risk classes +
opt-in phrases), DecisionJournal (6 tests with capture function),
UserLevelPluginInvocation post-execution integration (4 tests with
end-to-end seed detection + auto_run intent + journal emission +
blocked-still-emits).

Closes Q00#7
Closes Q00#10
Closes Q00#11
Closes Q00#26
Closes Q00#28

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