Fills: Gap F-ECO-007 — "add unified extension activation explain output and acceptance tests across hooks, skills, plugins, and MCP tools." Daily users need to know why a hook fired, why a skill was loaded, which plugin registered a tool, and how to disable one surface without breaking the session.
Grounding (current state, verified). The reason data largely exists but is scattered and not surfaced:
- Hooks —
teaagent/hooks.py(495):HookRegistry+HookEventenum (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, Stop, SubagentStop, SessionEnd);run_pre_hooks/run_post_hooks/… dispatch by event;HookErrorvetoes a tool call. No record of which hook fired and why. - Skills —
teaagent/skill_loader.py(643):load_skills_with_report→SkillLoadReportcarryingSkillLoadWarningandSkillLoadSkipped(reason)(e.g. "provenance validation failed", "skill review failed").skill_index_to_prompt_sectioninjects skills into the prompt. Load reasons exist; activation/selection reasons don't surface. - Skill routing —
teaagent/skill_router.py(285):RoutingDecision{sandbox_type, reason, fallback_available, warnings}andSkillIsolationPlan{isolation, reason, …}— already carryreasonstrings, just not exposed to the operator. - Plugins —
teaagent/plugin_system.py(212):PluginRegistry,discover_plugins,PluginManifest,register_command/register_agent. No "which plugin registered this tool/command" lookup is surfaced.
So the gap is aggregation + exposure, not new instrumentation. Each subsystem already knows its reason; nothing assembles them into one answer.
A single producer build_activation_report(session) -> ActivationReport and a surface
teaagent explain activation [--tool X | --skill Y | --since-last-run]:
ActivationReport
├── hooks_fired [{event, hook_name, target_tool?, decision: ran|vetoed, reason}]
├── skills [{name, source_dir, status: loaded|skipped, reason, isolation, sandbox}]
├── plugins [{name, type, registered: [commands|agents|tools], manifest_path}]
├── mcp_tools [{server, tool, trusted, allowed, expiry?}] # from MCP trust spec
└── provenance [{item, signed?, verified?, validation_errors[]}]
- hooks_fired is the only field needing a thin addition:
HookRegistryrecords each dispatch (event, hook, outcome, reason) into a per-session ring buffer. - skills reads directly from
SkillLoadReport+RoutingDecision.reason+SkillIsolationPlan.reason(already present). - plugins reads the
PluginRegistry(which command/agent/tool came from which manifest — invert the existing registration map). - mcp_tools reuses
MCPTrustPolicy(seemcp-trust-onboarding-journeyspec).
The view must answer four operator questions directly:
| Question | Answer source | Disable path |
|---|---|---|
| Why did this hook fire? | hooks_fired (event + reason) |
--disable-hook <name> for the session |
| Why was this skill loaded? | skills (matched + isolation reason) |
--disable-skill <name> |
| Which plugin registered this tool/command? | plugins (inverted registry) |
--disable-plugin <name> |
| Why is this MCP tool available? | mcp_tools (trust + scope) |
revoke (MCP trust spec) |
Disable is non-destructive and session-scoped — it suppresses one surface for the current session without uninstalling or breaking others (the explicit F-ECO-007 ask: "disable one surface without breaking the session").
- Every active extension is attributable. No tool/command/skill is active without an entry explaining its origin and reason.
- Vetoes are visible. A
HookErrorthat blocked a tool call shows up inhooks_firedwithdecision: vetoed+ reason — never a silent block. - Provenance failures are explained, not hidden. A skill skipped for provenance
appears with
status: skipped+ the validation error (fromSkillLoadSkipped.reason). - Disable is reversible. Re-enabling within the session restores the surface; the disable list is session-scoped state, not config mutation.
test_activation_report_aggregates: with a hook, a loaded skill, a plugin command, and an MCP tool present, the report lists all four with reasons.test_hook_veto_explained: a vetoing pre-hook appears asvetoed+ reason.test_skill_skip_reason_surfaced: a provenance-failed skill showsskipped+ the validation error.test_plugin_origin_lookup:explain activation --tool <name>returns the registering plugin + manifest path.test_disable_surface_session_scoped:--disable-skill Xremoves X from this session's prompt section without affecting other skills or persisting.
- DQ-EXT-1: Should
hooks_firedbe a bounded ring buffer (last N) or full per-session log? Recommendation: bounded ring buffer (last N) + full in the audit log. - DQ-EXT-2: Is
explain activationa CLI verb, a TUI command, or both? Recommendation: both — it feeds the cockpit (CKP spec) as an "extensions" panel.
- Not a permissions system — explainability describes activation; permission modes (PMR) and MCP trust gate it. Keep the concerns separate.
- Not a plugin marketplace/trust-scoring feature (that's
skill_review/mcp_trust).
- MCP tool rows:
mcp-trust-onboarding-journey-2026-06-01.md. - Cockpit "extensions" panel:
operator-cockpit-contract-2026-06-01.md. - Building blocks:
teaagent/hooks.py,teaagent/skill_loader.py,teaagent/skill_router.py,teaagent/plugin_system.py.