Agent Skills & Plan Mode Guide
Two BaseAgent-framework capabilities that ship together (they share one migration): Skills — reusable capability bundles an agent can activate mid-run — and Plan Mode — a per-request human-in-the-loop gate that makes an agent get its plan approved before it acts.
Audience: developers building/configuring agents, authoring skills, or wiring skills/plan-mode into a chat surface.
Both are agent-framework features, not chat-widget features. They work for any agent invocation — the conversations UI, headless/scheduled runs, the API — because the logic lives in BaseAgent / AIEngineBase, and the chat surface is just one caller.
Part 1 — Skills
Section titled “Part 1 — Skills”1.1 The mental model
Section titled “1.1 The mental model”A Skill (MJ: AI Skills) is a bundle of three things:
| Piece | Column / table | Effect when the skill activates |
|---|---|---|
| Instructions | AISkill.Instructions (NVARCHAR MAX) | Appended to the agent’s context for the rest of the run |
| Actions | MJ: AI Skill Actions (junction → Action) | Added to the agent’s available tool surface |
| Sub-agents | MJ: AI Skill Sub Agents (junction → AIAgent) | Added to the agent’s available sub-agents |
The point is write-once, grant-to-many: instead of copy-pasting the same instruction block + action set into every agent’s system prompt, you author it once as a Skill and grant it to any agent that should have it. Skills are also shareable and portable (see §1.6, §1.7).
Activation is progressive-disclosure: an agent only ever sees a skill’s name + description in its prompt catalog. It sees the full Instructions only after it chooses to activate the skill — so a large library of skills costs very few prompt tokens until one is actually used.
Activation is not a nested agent run. It’s an in-loop step that appends instructions and widens the tool surface, then continues the same run.
1.2 The three-layer gate
Section titled “1.2 The three-layer gate”Whether a given agent may activate a given skill is resolved by AIEngineBase.GetSkillsForAgent(agent) (packages/AI/BaseAIEngine/src/BaseAIEngine.ts) through three independent gates — all must pass:
include skill ⟺ AIAgent.AcceptsSkills ≠ 'None' (per-agent gate) AND AISkill.Status = 'Active' (catalog gate) AND (AcceptsSkills = 'All' OR an Active MJ: AI Agent Skills grant exists) (grant gate)AIAgent.AcceptsSkills—None(default; opt-in, so existing agents are unaffected) ·All(any Active skill) ·Limited(only skills granted viaMJ: AI Agent Skills).AISkill.Status— onlyActiveskills are activatable.Deprecatedretires a skill without deleting it (defaults toActiveso a freshly authored skill works immediately for its owner).MJ: AI Agent Skills.Status— per-grantActive/Pending/Revoked, so a grant can be disabled without unlinking (only consulted whenAcceptsSkills = 'Limited').
At runtime a fourth, orthogonal gate also applies: an activated skill’s bundled Actions are still subject to Action.Status, and bundled sub-agents to AIAgent.Status — a deprecated Action never flows through any skill.
Action/Agent bundle IDs are read via GetSkillActionIDs(skillID) / GetSkillSubAgentIDs(skillID); the engine returns IDs only and lets callers resolve the full entities against their own ActionEngineServer / AIEngine caches (no cross-package dependency).
1.2a The double activation gate (v5.45) — availability vs. trigger
Section titled “1.2a The double activation gate (v5.45) — availability vs. trigger”The §1.2 gates answer “may this agent+user use this skill at all?” (availability). A second, orthogonal dimension answers “who may pull the trigger?” (activation), added after live testing showed an AcceptsSkills='All' agent correctly — but surprisingly — self-activating a skill the user never requested:
| Column | Values | Default | Governs |
|---|---|---|---|
AISkill.ActivationMode | Auto / RequestedOnly | RequestedOnly | May this skill ever be self-activated? |
AIAgent.SkillActivationMode | Auto / RequestedOnly | RequestedOnly | May this agent ever self-activate skills? |
Self-activation (the skill appearing in the agent’s prompt catalog + an agent-initiated Skill step) requires Auto on BOTH sides — resolved by AIEngineBase.GetAutoActivatableSkillsForAgent(agent, user?), which is GetSkillsForAgent(...) further filtered by the two dials. The catalog injection, validateSkillNextStep, and resolveSkillActivations all use this auto set.
The requested path is NOT gated by ActivationMode. A user’s explicit /skill mention (→ ExecuteAgentParams.requestedSkillIDs → preActivateRequestedSkills) honors a RequestedOnly skill — that’s the mode’s whole point — subject to all §1.2 availability gates.
Because both defaults are RequestedOnly, the Auto × Auto “super agent” posture (an agent that expands its own tool surface at runtime) always requires two deliberate opt-ins and can never arise accidentally — the defense against skill leakage into an agent. Configuration recipes:
- Research assistant that should reach for web/search/visualization skills on its own judgment: agent
SkillActivationMode='Auto'+ those skillsActivationMode='Auto'. - Consequential skill (e.g. Communications — it sends things): keep
ActivationMode='RequestedOnly'. No agent, regardless of posture, can self-activate it; users must/skillit in explicitly, and the skill’s own confirm-before-send instructions still gate the act. - Locked-down agent:
SkillActivationMode='RequestedOnly'— its prompt catalog is empty; skills only enter its runs when the user asks.
1.3 The runtime flow (Loop agent)
Section titled “1.3 The runtime flow (Loop agent)”gatherPromptTemplateData() ── injects skill CATALOG (name+description only) into the prompt │ ▼LLM emits nextStep.type='Skill', skills:[{name}] │ ▼LoopAgentType.DetermineNextStep ── → BaseAgentNextStep.step='Skill', skillActivations:[{name}] │ ▼validateSkillNextStep ── fuzzy-matches names vs GetSkillsForAgent; unknown/disallowed → Retry │ ▼executeSkillStep ── for each newly-activated skill: ├─ recordSkillActivationStep() → AIAgentRunStep (StepType='Skill') ├─ buildSkillActivationMessage() → appends Instructions to conversationMessages └─ enableSkillCapabilities() → pushes 'specific'-scoped add ActionChange/SubAgentChange │ ▼executePromptStep ── loop continues; next turn's gatherPromptTemplateData picks up the widened tool surfaceKey implementation notes (all in base-agent.ts):
'Skill'is a non-terminal step. Like'ClientTools', it is deliberately not part of the generatedAIAgentRun.FinalStepunion (that column is DB-CHECK-constrained to terminal outcomes).BaseAgentNextStep.stepis typed offFinalStep, so the switch sites use an explicit'Skill' as typeof …stepcast. It is in theAIAgentRunStep.StepTypeCHECK — that column records what executed, a different concern from a run’s final outcome.- Tool-surface widening uses
scope: 'specific'targeting the activating agent’s own ID (agentIds: [agent.ID]), pushed ontoparams.actionChanges/params.subAgentChanges. This applies to the activating agent at any depth (a sub-agent that activates a skill still gets its tools — a'root'scope would only apply at depth 0) and never cascades to that agent’s own sub-agents (viafilterActionChangesForSubAgent). Becauseparamsis the same object for the whole run, every later turn’sgatherPromptTemplateData()re-applies it automatically. - Idempotent re-activation.
_activatedSkillIDstracks what’s already active; re-requesting an active skill is a harmless no-op (no duplicate instructions, no duplicate change entries). _effectiveSubAgentsmirrors_effectiveActionsso a runtime-added sub-agent (from a skill) validates correctly invalidateSubAgentNextStep. (This closed a pre-existing gap in thesubAgentChangesmechanism that affected any consumer, not just Skills.)
executeSkillStep is decomposed into resolveSkillActivations / buildSkillActivationMessage / enableSkillCapabilities / recordSkillActivationStep, all protected — override any one for fine-grained control (custom instruction formatting, a licensing check on activation, cascading grants via 'all-subagents', etc.) without re-implementing the step.
1.3a Observability — AIAgentRunStep.Skills (v5.45)
Section titled “1.3a Observability — AIAgentRunStep.Skills (v5.45)”Every step touched by a skill records the linkage on AIAgentRunStep.Skills — a JSON-typed column (Array<AgentSkillInvocation> | null, generated accessor SkillsObject) so skill involvement is auditable per step, not inferred:
type AgentSkillInvocation = { SkillID: string; SkillName: string; // identity as of activation ActivationType: 'requested' | 'auto'; // /skill mention vs. agent self-activation Provenance: { // the gate values that admitted it AgentAcceptsSkills: string; SkillActivationMode: string; AgentSkillActivationMode: string; RequestedBy: 'user-request' | 'agent-decision'; }; Reason?: string; // agent-stated rationale (auto only)}Population rules (all in createStepEntity + the activation paths):
| Step type | Skills contains |
|---|---|
| Skill | The activation(s) that step performed — with Reason when the model self-activated (the loop response’s skills:[{name, reason}] now carries an optional one-sentence rationale) |
| Prompt | The full set of skills in effect for that turn — prompt injection is never invisible |
| Actions / Sub-Agent | The skill(s) through which the executed tool became available. NULL means the tool was a native grant — native authority takes precedence over skill attribution (getSkillAttributionForAction / getSkillAttributionForSubAgent) |
| anything else | NULL |
The runtime type twin is AgentSkillInvocation in @memberjunction/ai-core-plus; the CodeGen JSON-type interface lives at metadata/entities/JSONType-interfaces/AgentSkillInvocation.ts — keep them in sync.
UX: the agent-run form renders this end to end — Skill/Plan step nodes get dedicated icons, any step with Skills shows per-skill chips (auto-activations get a warning accent — the agent expanded its own surface), and the step drill-in panel gains a Skills tab showing each invocation’s activation type, provenance-of-authority grid, and reason.
1.4 Realtime and proxy agents
Section titled “1.4 Realtime and proxy agents”- Realtime agents don’t run the Loop; a skill’s instructions are appended at session build (session-static), not activated in-loop.
- Proxy agents delegate their whole loop to a remote system; skills are the remote’s concern.
1.5 Prompt template
Section titled “1.5 Prompt template”The Loop system prompt template (metadata/prompts/templates/system/loop-agent-type-system-prompt.template.md) gates the entire Skills section on {% if skillCount > 0 %} — agents with no available skills see nothing about skills, keeping their prompt unchanged. skillCount / skillsCatalog come from AgentContextData (populated in gatherPromptTemplateData).
1.6 Permissions: full agent-parity, open-by-default
Section titled “1.6 Permissions: full agent-parity, open-by-default”Skills use the same dedicated-table permission model as AI Agents — MJ: AI Skill Permissions (AISkillPermission) is the exact sibling of AIAgentPermission: a row grants a User (UserID) or a Role (RoleID) — never both, never neither (a server-side validator enforces this; see below) — one of CanView / CanRun / CanEdit / CanDelete. A skill’s owner is its CreatedByUserID.
There are two access paths over the same table, with different defaults and purposes (the general pattern is documented in the Unified Permissions Guide):
| Path | Class | Default (no rows) | Used for |
|---|---|---|---|
| Runtime helper | AISkillPermissionHelper (@memberjunction/ai-engine-base) | Open — anyone can View + Run; only owner Edits/Deletes | The hot path: the /skill picker filter and the server-side run-time guard |
| Unified provider | AISkillPermissionProvider (@memberjunction/core-entities, @RegisterClass(PermissionProviderBase, 'MJAISkillPermissionProvider'), domain AI Skill Permissions) | Closed — explicit grants only | Sharing Center / audit via the PermissionEngine |
Action mapping (both paths): CanView → Read, CanRun → Execute, CanEdit → Update, CanDelete → Delete. The helper applies the hierarchy Delete → Edit → Run → View and is a synchronous cache hit because AIEngineBase caches SkillPermissions alongside Skills.
- Grantee-exclusivity validator:
MJAISkillPermissionEntityServer(@memberjunction/core-entities-server) overridesValidate()to reject a row with both or neither grantee — the deterministic, version-controlled equivalent of the LLM-generatedValidateRoleIDAndUserIDExclusiveonAIAgentPermission. It fires on every server save path, so the sharing UI, Remote Operations, and scripts are all covered. - The permission filter:
AIEngineBase.GetSkillsForAgent(agent, user?)takes an optional user. Without it, you get pure agent-gating (§1.2). With it, the result is additionally intersected with the user’s Run permission — so the same call is the single source of truth for “what may this agent activate for this user”.BaseAgentpassesparams.contextUser, so the skills catalog the model sees is already permission-filtered — an agent is never even offered a skill the acting user can’t run.
Sharing is gated behind the dedicated Can Share Skills authorization (MJ: Authorizations); authoring/using your own skills never requires it. In the UI, the generated MJ: AI Skills form gets a Manage Permissions grid + Export/Import SKILL.md from the AISkillSharingPanel (a BaseFormPanel slot component), and the permission grid itself is a skill-scoped mirror of the agent permissions grid (SkillPermissionsPanel/SkillPermissionsDialog in @memberjunction/ng-agents). The generated form’s related-entity grid also edits MJ: AI Skill Permissions directly. (The earlier AI Skills Resource Type / MJ: Resource Permissions sharing was retired — skills now own their permission table, exactly like agents.)
1.6a /skill invocation — user-requested pre-activation
Section titled “1.6a /skill invocation — user-requested pre-activation”A user can activate a skill for a message by typing /skill-name in the conversation composer — the exact sibling of @agent and #entity mentions (the mention system’s MENTION_TRIGGERS now includes /). The picker (MentionAutocompleteService) lists only Active skills the user has Run permission for (filtered through AISkillPermissionHelper, open-by-default), and each chip renders with the skill’s own IconClass + Color UX metadata. Selected skill IDs are collected from the composer and threaded as ExecuteAgentParams.requestedSkillIDs (a string[]) down the same client→resolver→runtime chain as planMode:
/skill chips → message-input.component (collectRequestedSkillIDs) → conversation-agent.service.invokeSubAgent(requestedSkillIDs) [or ConversationAgentRunner.ProcessMessage] → RunAgentFromConversationDetailParams.RequestedSkillIDs → GraphQL $requestedSkillIDs:[String!] → RunAIAgentResolver → ExecuteAgentParams.requestedSkillIDs → BaseAgent.preActivateRequestedSkills() ← guarded pre-activation at run startpreActivateRequestedSkills runs once at run start (root agent only), and activates each requested skill only if it survives the guard: it must be in GetSkillsForAgent(agent, contextUser) — i.e. the agent accepts it and the user may run it. Anything else is silently dropped, so a client can never smuggle in a skill the user or agent isn’t entitled to. Surviving skills are activated through the same recordSkillActivationStep / enableSkillCapabilities / buildSkillActivationMessage machinery as a model-initiated Skill step, so their Instructions + bundled tools take effect from turn 1. Plan Mode is unaffected — pre-activation widens the tool surface, but the plan-approval gate still blocks executing those tools until approved.
1.7 SKILL.md — portable import/export
Section titled “1.7 SKILL.md — portable import/export”A skill can be exported to / imported from a portable SKILL.md file, so skills move across MJ instances:
---name: Report Builderdescription: Generates formatted business reports from query resultscategory: Reportingactions: - Run Query - Generate PDFsubAgents: - Report Formatter Agent---
Instructions body — plain markdown, appended to an accepting agent's systemprompt when the skill is activated.Why names, not IDs: Action/sub-agent references in the frontmatter are names, because names are the only stable cross-instance reference. On import, names are resolved against the target instance’s catalog; anything that doesn’t resolve becomes a non-fatal warning (the skill still imports with whatever did resolve) — a skill authored elsewhere may reference actions this instance doesn’t have.
Two layers, in @memberjunction/ai-agents:
SkillMarkdownConverter— pure, dependency-freeParse/Serialize. Fully unit-tested in isolation. Hand-rolled parser (the frontmatter shape is small and fixed) rather than a YAML dependency.SkillImportExportService— orchestrates against the DB:Config()s the AI + Action engines (idempotent) for name↔ID resolution, creates/updates theMJ: AI Skillsrow, and resyncs the two junction sets.
Both are exposed as typed, provider-routed Remote Operations — AISkill.ExportMarkdown / AISkill.ImportMarkdown (see REMOTE_OPERATIONS_GUIDE) — so the browser calls new AISkillExportMarkdownOperation().Execute(input, { provider }) with no bespoke resolver/GraphQL client.
Part 2 — Plan Mode
Section titled “Part 2 — Plan Mode”2.1 The mental model
Section titled “2.1 The mental model”Plan Mode makes an agent present a plan and get human approval before it executes any Actions or Sub-Agents. It’s a per-request opt-in built on the existing MJ: AI Agent Requests human-in-the-loop (HITL) pause/resume infrastructure — it adds no new persistence mechanism.
Three switches, resolved once per run in BaseAgent.resolvePlanModeGate:
| Switch | Column / param | Default | Meaning |
|---|---|---|---|
| Capability | AIAgent.SupportsPlanMode (BIT) | 1 (ON, opt-out) | Whether this agent can use plan mode at all |
| Per-request | ExecuteAgentParams.planMode | undefined (OFF) | Whether this run should be gated |
| Mandate (v5.45) | AIAgent.RequirePlanMode (BIT) | 0 (OFF) | Forces plan mode on every root run of this agent, regardless of the per-request flag. SupportsPlanMode is irrelevant when set. |
Plan Mode is active when the agent is the root (depth 0) AND (RequirePlanMode OR (SupportsPlanMode AND planMode === true)). Because both the per-request toggle and the mandate default OFF, default runtime behavior is unchanged — nothing is gated unless a caller opts in or an admin mandates it. Set RequirePlanMode on high-consequence agents (anything with outbound-communication capabilities) where human review must not depend on the caller remembering a toggle. Realtime agents are seeded SupportsPlanMode = 0 (they skip plan mode structurally); Remote Proxy agents will be too when that type ships.
Every plan-mode run is stamped AIAgentRun.PlanMode = 1 (v5.45) — the run-header UX renders a “Plan Mode” chip from it, and it’s the anchor for future plan-drift audits (approved plan vs. steps that actually executed).
2.2 The gate
Section titled “2.2 The gate”While Plan Mode is active and not yet approved, validateNextStep demotes any Actions or Sub-Agent next step to Retry with an explanatory message — the agent literally cannot execute until it presents a plan. Everything else stays allowed: Chat (ask a clarifying question first), Skill (load a skill’s instructions before planning), ForEach/While, ClientTools, Retry.
After approval, skill activations are blocked (v5.45): a post-approval agent-initiated Skill step is demoted to Retry directing the agent to present an updated plan — otherwise the agent could widen its tool surface beyond what the human reviewed. The rule’s shape: activations are legal only before approval (pre-activated /skill requests land before planning; pre-approval self-activations are visible in the plan), so the reviewed plan always reflects the full capability set.
The prompt template surfaces a “Plan Mode — REQUIRED before you may act” section, gated on {% if planModeActive and not planApproved %}.
2.3 The HITL flow
Section titled “2.3 The HITL flow”Plan Mode active, unapproved │ ▼LLM emits nextStep.type='Plan', plan:'…' │ ▼executePlanStep: ├─ records an AIAgentRunStep (StepType='Plan') ← the UI reads this to render a plan-approval card ├─ createFeedbackRequest(...) with an editable ← reuses the EXISTING MJ: AI Agent Requests flow │ plan-approval AgentResponseForm (textarea + Approve/Reject buttongroup) └─ terminates the run (returns a 'Chat'-shaped terminal step) ← awaits the human │ ▼human Approves / edits / Rejects → MJAIAgentRequestEntityServer.Save() auto-resumes (lastRunId set) │ ▼resolvePlanModeGate on the RESUMED run: ├─ re-enables planMode (because the request originated from a 'Plan' step) └─ reads the request's status: Approved/Responded → approved (proceed) · Rejected → unapproved (re-plan)Two subtleties worth calling out (both are load-bearing correctness points):
- The step returned to the framework is
'Chat', not'Plan'.'Plan'is only an intermediate classification and theAIAgentRunStep.StepTypeaudit value the UI keys on. The terminal step must stay withinAIAgentRun.FinalStep’s DB-CHECK-constrained vocabulary, which deliberately excludesPlan/Skill/ClientTools(same reason as §1.3). So a plan-approval pause is represented with the same terminal shapeexecuteChatStepuses. - Rejection forces a re-plan because
resumeAgentre-enablesplanModeonly when the resume originated from aPlanrun-step. Without that, the gate would be off on the resumed run and a rejected plan would silently execute anyway. Regular Chat-clarification resumes are unaffected — they leaveplanModeoff.
resolvePlanModeGate, validatePlanNextStep, executePlanStep, and buildPlanApprovalForm are all protected — override to change eligibility rules, plan-quality validation, or the approval card’s layout.
3. Schema at a glance
Section titled “3. Schema at a glance”v5.44 — Migration V202606301200__v5.44.x__Agent_Skills_And_Plan_Mode.sql. New tables: AISkill, AISkillAction, AISkillSubAgent, AIAgentSkill, AISkillPermission (User xor Role grantee + CanView/CanRun/CanEdit/CanDelete). Additive columns: AIAgent.SupportsPlanMode (default 1), AIAgent.AcceptsSkills (default 'None'), AISkill.IconClass + AISkill.Color (UX metadata for the /skill picker). AIAgentRunStep.StepType CHECK extended with 'Plan' and 'Skill'. Composition junctions (AISkillAction/AISkillSubAgent) are intentionally status-less — member lifecycle is governed by Action.Status/AIAgent.Status; Status lives only on the grant (AIAgentSkill) and the catalog (AISkill). The MJ: Permission Domains catalog row for AI Skill Permissions → MJAISkillPermissionProvider is seeded via metadata sync (metadata/permission-domains/), not the migration.
v5.45 — Migration V202607020230__v5.45.x__AISkill_ActivationMode.sql. Additive columns: AISkill.ActivationMode + AIAgent.SkillActivationMode (both 'Auto'/'RequestedOnly', default 'RequestedOnly' — the double activation gate, §1.2a), AIAgent.RequirePlanMode (BIT, default 0 — mandatory plan mode, §2.1), AIAgentRun.PlanMode (BIT, default 0 — run-level stamp), AIAgentRunStep.Skills (NVARCHAR MAX, JSON-typed Array<AgentSkillInvocation> — per-step observability, §1.3a). The Skills JSON-type binding is seeded via metadata sync (metadata/entities/.entity-field-jsontype-agent-run-step-skills.json + JSONType-interfaces/AgentSkillInvocation.ts).
4. Where to look
Section titled “4. Where to look”| Concern | File |
|---|---|
| Skill resolution + gate + user filter | packages/AI/BaseAIEngine/src/BaseAIEngine.ts (GetSkillsForAgent(agent, user?), GetAutoActivatableSkillsForAgent(agent, user?) — the double gate, SkillPermissions, GetSkillActionIDs/…SubAgentIDs) |
| Skill observability (provenance capture + per-step attribution) | packages/AI/Agents/src/base-agent.ts (buildSkillInvocation, getSkillAttributionForAction/…ForSubAgent, createStepEntity skills param); type in packages/AI/CorePlus/src/agent-types.ts (AgentSkillInvocation) |
| Skill observability UX (run header chip, step chips, Skills tab) | packages/Angular/Explorer/core-entity-forms/src/lib/custom/ai-agent-run/ (ai-agent-run.component.*, ai-agent-run-step-node.component.*, ai-agent-run-step-detail.component.*) |
| Skill permission runtime helper | packages/AI/BaseAIEngine/src/AISkillPermissionHelper.ts (open-by-default, cached) |
| Skill permission unified provider | packages/MJCoreEntities/src/custom/PermissionProviders/AISkillPermissionProvider.ts (+ index.ts LoadPermissionProviders) |
| Grantee-exclusivity validator | packages/MJCoreEntitiesServer/src/custom/MJAISkillPermissionEntityServer.server.ts |
| Skill step + Plan Mode runtime + pre-activation | packages/AI/Agents/src/base-agent.ts (executeSkillStep family, preActivateRequestedSkills, resolvePlanModeGate, executePlanStep) |
| Skill/Plan next-step parsing | packages/AI/Agents/src/agent-types/loop-agent-type.ts, loop-agent-response-type.ts |
| Step-type union + params | packages/AI/CorePlus/src/agent-types.ts (BaseAgentNextStep, AgentSkillActivationRequest, ExecuteAgentParams.planMode / .requestedSkillIDs) |
/skill composer UX | packages/Angular/Generic/conversations/src/lib/services/mention-autocomplete.service.ts, components/mention/mention-editor.component.ts, components/message/message-input.component.ts |
requestedSkillIDs transport | AgentsClient/src/generic/AgentClientTypes.ts + AgentClientSession.ts, GraphQLDataProvider/src/graphQLAIClient.ts, MJServer/src/resolvers/RunAIAgentResolver.ts, ConversationsRuntime/src/agent-runner/ConversationAgentRunner.ts |
| SKILL.md | packages/AI/Agents/src/SkillMarkdownConverter.ts, SkillImportExportService.ts, operations/AISkillMarkdownOperations.ts |
| Plan-approval resume | packages/AI/Agents/src/MJAIAgentRequestEntityServer.ts |
| Sharing / permission grid / import UI | packages/Angular/Explorer/core-entity-forms/src/lib/panels/ai-skill-sharing/, packages/Angular/Generic/agents/src/lib/{services/skill-permissions.service,components/skill-permissions-*} |
| Governance metadata | metadata/authorizations/ (Can Share Skills), metadata/permission-domains/ (AI Skill Permissions) |
| Tests | unit: packages/AI/Agents/src/__tests__/{skill-step,plan-mode-gate,loop-agent-type,SkillMarkdownConverter}.test.ts, packages/AI/BaseAIEngine/src/__tests__/AISkillPermissionHelper.test.ts, packages/MJCoreEntities/src/__tests__/PermissionProviders/AISkillPermissionProvider.test.ts, packages/MJCoreEntitiesServer/src/__tests__/MJAISkillPermissionEntityServer.test.ts; integration: packages/MJServer/integration-test-scripts/ai-skills-tests.ts |