From 72ae355e0e8080829edcd1501fa0dd28591f4f45 Mon Sep 17 00:00:00 2001 From: "openclaw-docs-sync[bot]" Date: Tue, 21 Apr 2026 20:30:41 +0000 Subject: [PATCH] chore(sync): mirror docs from openclaw/openclaw@674feda214f8151f7fd5a2aff1d10162993cf6c1 --- .openclaw-sync/source.json | 4 +- .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- docs/channels/msteams.md | 30 +- docs/channels/telegram.md | 3 +- docs/cli/message.md | 28 +- docs/plan/ui-channels.md | 254 ++++++ docs/plugins/architecture.md | 19 +- docs/plugins/message-presentation.md | 338 ++++++++ docs/plugins/sdk-overview.md | 4 +- docs/plugins/skill-workshop.md | 731 ++++++++++++++++++ docs/tools/skills.md | 18 + 11 files changed, 1388 insertions(+), 45 deletions(-) create mode 100644 docs/plan/ui-channels.md create mode 100644 docs/plugins/message-presentation.md create mode 100644 docs/plugins/skill-workshop.md diff --git a/.openclaw-sync/source.json b/.openclaw-sync/source.json index 10b77110c..57e259018 100644 --- a/.openclaw-sync/source.json +++ b/.openclaw-sync/source.json @@ -1,5 +1,5 @@ { "repository": "openclaw/openclaw", - "sha": "78030d0d52bbe9aa191fa3ad422a44531e585f2f", - "syncedAt": "2026-04-21T20:27:23.543Z" + "sha": "674feda214f8151f7fd5a2aff1d10162993cf6c1", + "syncedAt": "2026-04-21T20:30:40.370Z" } diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index ad04d7d6f..1bcbad917 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -f135ddc1802b7f8b2d29bf495fd0ac1f497a89bab8164ca8c7c8f18efc010e6e plugin-sdk-api-baseline.json -a47d06095ec5c3701a94888a11e89700d8a8511db46fa3122fb9407e160707b6 plugin-sdk-api-baseline.jsonl +cfeee4630cb43ffc4d702f207d28d35962c6458aa8fd2b1671c35e0be158bb35 plugin-sdk-api-baseline.json +af4fbf19861c6ec000b41ac5a3ded597700e45bb15f8b1d74bb2d1f550bd09b6 plugin-sdk-api-baseline.jsonl diff --git a/docs/channels/msteams.md b/docs/channels/msteams.md index 4d618c6df..74af3463b 100644 --- a/docs/channels/msteams.md +++ b/docs/channels/msteams.md @@ -611,7 +611,7 @@ Teams markdown is more limited than Slack or Discord: - Basic formatting works: **bold**, _italic_, `code`, links - Complex markdown (tables, nested lists) may not render correctly -- Adaptive Cards are supported for polls and arbitrary card sends (see below) +- Adaptive Cards are supported for polls and semantic presentation sends (see below) ## Configuration @@ -783,11 +783,11 @@ OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API) - The gateway must stay online to record votes. - Polls do not auto-post result summaries yet (inspect the store file if needed). -## Adaptive Cards (arbitrary) +## Presentation Cards -Send any Adaptive Card JSON to Teams users or conversations using the `message` tool or CLI. +Send semantic presentation payloads to Teams users or conversations using the `message` tool or CLI. OpenClaw renders them as Teams Adaptive Cards from the generic presentation contract. -The `card` parameter accepts an Adaptive Card JSON object. When `card` is provided, the message text is optional. +The `presentation` parameter accepts semantic blocks. When `presentation` is provided, the message text is optional. **Agent tool:** @@ -796,10 +796,9 @@ The `card` parameter accepts an Adaptive Card JSON object. When `card` is provid action: "send", channel: "msteams", target: "user:", - card: { - type: "AdaptiveCard", - version: "1.5", - body: [{ type: "TextBlock", text: "Hello!" }], + presentation: { + title: "Hello", + blocks: [{ type: "text", text: "Hello!" }], }, } ``` @@ -809,10 +808,10 @@ The `card` parameter accepts an Adaptive Card JSON object. When `card` is provid ```bash openclaw message send --channel msteams \ --target "conversation:19:abc...@thread.tacv2" \ - --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello!"}]}' + --presentation '{"title":"Hello","blocks":[{"type":"text","text":"Hello!"}]}' ``` -See [Adaptive Cards documentation](https://adaptivecards.io/) for card schema and examples. For target format details, see [Target formats](#target-formats) below. +For target format details, see [Target formats](#target-formats) below. ## Target formats @@ -837,9 +836,9 @@ openclaw message send --channel msteams --target "user:John Smith" --message "He # Send to a group chat or channel openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" --message "Hello" -# Send an Adaptive Card to a conversation +# Send a presentation card to a conversation openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \ - --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello"}]}' + --presentation '{"title":"Hello","blocks":[{"type":"text","text":"Hello"}]}' ``` **Agent tool examples:** @@ -858,10 +857,9 @@ openclaw message send --channel msteams --target "conversation:19:abc...@thread. action: "send", channel: "msteams", target: "conversation:19:abc...@thread.tacv2", - card: { - type: "AdaptiveCard", - version: "1.5", - body: [{ type: "TextBlock", text: "Hello" }], + presentation: { + title: "Hello", + blocks: [{ type: "text", text: "Hello" }], }, } ``` diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index bea8c1c0f..37fab1f16 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -803,7 +803,8 @@ openclaw message poll --channel telegram --target -1001234567890:topic:42 \ Telegram send also supports: - - `--buttons` for inline keyboards when `channels.telegram.capabilities.inlineButtons` allows it + - `--presentation` with `buttons` blocks for inline keyboards when `channels.telegram.capabilities.inlineButtons` allows it + - `--pin` or `--delivery '{"pin":true}'` to request pinned delivery when the bot can pin in that chat - `--force-document` to send outbound images and GIFs as documents instead of compressed photo or animated-media uploads Action gating: diff --git a/docs/cli/message.md b/docs/cli/message.md index 6c8cff399..1aa8a3731 100644 --- a/docs/cli/message.md +++ b/docs/cli/message.md @@ -67,15 +67,13 @@ Name lookup: - `send` - Channels: WhatsApp/Telegram/Discord/Google Chat/Slack/Mattermost (plugin)/Signal/iMessage/Matrix/Microsoft Teams - - Required: `--target`, plus `--message` or `--media` - - Optional: `--media`, `--interactive`, `--buttons`, `--components`, `--card`, `--reply-to`, `--thread-id`, `--gif-playback`, `--force-document`, `--silent` - - Shared interactive payloads: `--interactive` sends a channel-native interactive JSON payload when supported - - Telegram only: `--buttons` (requires `channels.telegram.capabilities.inlineButtons` to allow it) + - Required: `--target`, plus `--message`, `--media`, or `--presentation` + - Optional: `--media`, `--presentation`, `--delivery`, `--pin`, `--reply-to`, `--thread-id`, `--gif-playback`, `--force-document`, `--silent` + - Shared presentation payloads: `--presentation` sends semantic blocks (`text`, `context`, `divider`, `buttons`, `select`) that core renders through the selected channel's declared capabilities. See [Message Presentation](/plugins/message-presentation). + - Generic delivery preferences: `--delivery` accepts delivery hints such as `{ "pin": true }`; `--pin` is shorthand for pinned delivery when the channel supports it. - Telegram only: `--force-document` (send images and GIFs as documents to avoid Telegram compression) - Telegram only: `--thread-id` (forum topic id) - Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field) - - Discord only: `--components` JSON payload - - Adaptive-card channels: `--card` JSON payload when supported - Telegram + Discord: `--silent` - WhatsApp only: `--gif-playback` @@ -208,22 +206,22 @@ openclaw message send --channel discord \ --target channel:123 --message "hi" --reply-to 456 ``` -Send a Discord message with components: +Send a message with semantic buttons: ``` openclaw message send --channel discord \ --target channel:123 --message "Choose:" \ - --components '{"text":"Choose a path","blocks":[{"type":"actions","buttons":[{"label":"Approve","style":"success"},{"label":"Decline","style":"danger"}]}]}' + --presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Approve","value":"approve","style":"success"},{"label":"Decline","value":"decline","style":"danger"}]}]}' ``` -See [Discord components](/channels/discord#interactive-components) for the full schema. +Core renders the same `presentation` payload into Discord components, Slack blocks, Telegram inline buttons, Mattermost props, or Teams/Feishu cards depending on channel capability. See [Message Presentation](/plugins/message-presentation) for the full contract and fallback rules. -Send a shared interactive payload: +Send a richer presentation payload: ```bash openclaw message send --channel googlechat --target spaces/AAA... \ --message "Choose:" \ - --interactive '{"text":"Choose a path","blocks":[{"type":"actions","buttons":[{"label":"Approve"},{"label":"Decline"}]}]}' + --presentation '{"title":"Deploy approval","tone":"warning","blocks":[{"type":"text","text":"Choose a path"},{"type":"buttons","buttons":[{"label":"Approve","value":"approve"},{"label":"Decline","value":"decline"}]}]}' ``` Create a Discord poll: @@ -277,19 +275,19 @@ openclaw message react --channel signal \ --emoji "✅" --target-author-uuid 123e4567-e89b-12d3-a456-426614174000 ``` -Send Telegram inline buttons: +Send Telegram inline buttons through generic presentation: ``` openclaw message send --channel telegram --target @mychat --message "Choose:" \ - --buttons '[ [{"text":"Yes","callback_data":"cmd:yes"}], [{"text":"No","callback_data":"cmd:no"}] ]' + --presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Yes","value":"cmd:yes"},{"label":"No","value":"cmd:no"}]}]}' ``` -Send a Teams Adaptive Card: +Send a Teams card through generic presentation: ```bash openclaw message send --channel msteams \ --target conversation:19:abc@thread.tacv2 \ - --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Status update"}]}' + --presentation '{"title":"Status update","blocks":[{"type":"text","text":"Build completed"}]}' ``` Send a Telegram image as a document to avoid compression: diff --git a/docs/plan/ui-channels.md b/docs/plan/ui-channels.md new file mode 100644 index 000000000..85effa3db --- /dev/null +++ b/docs/plan/ui-channels.md @@ -0,0 +1,254 @@ +--- +title: Channel Presentation Refactor Plan +summary: Decouple semantic message presentation from channel native UI renderers. +read_when: + - Refactoring channel message UI, interactive payloads, or native channel renderers + - Changing message tool capabilities, delivery hints, or cross-context markers + - Debugging Discord Carbon import fanout or channel plugin runtime laziness +--- + +# Channel Presentation Refactor Plan + +## Status + +Implemented for the shared agent, CLI, plugin capability, and outbound delivery surfaces: + +- `ReplyPayload.presentation` carries semantic message UI. +- `ReplyPayload.delivery.pin` carries sent-message pin requests. +- Shared message actions expose `presentation`, `delivery`, and `pin` instead of provider-native `components`, `blocks`, `buttons`, or `card`. +- Core renders or auto-degrades presentation through plugin-declared outbound capabilities. +- Discord, Slack, Telegram, Mattermost, MS Teams, and Feishu renderers consume the generic contract. +- Discord channel control-plane code no longer imports Carbon-backed UI containers. + +Canonical docs now live in [Message Presentation](/plugins/message-presentation). +Keep this plan as historical implementation context; update the canonical guide +for contract, renderer, or fallback behavior changes. + +## Problem + +Channel UI is currently split across several incompatible surfaces: + +- Core owns a Discord-shaped cross-context renderer hook through `buildCrossContextComponents`. +- Discord `channel.ts` can import native Carbon UI through `DiscordUiContainer`, which pulls runtime UI dependencies into the channel plugin control plane. +- The agent and CLI expose native payload escape hatches such as Discord `components`, Slack `blocks`, Telegram or Mattermost `buttons`, and Teams or Feishu `card`. +- `ReplyPayload.channelData` carries both transport hints and native UI envelopes. +- The generic `interactive` model exists, but it is narrower than the richer layouts already used by Discord, Slack, Teams, Feishu, LINE, Telegram, and Mattermost. + +This makes core aware of native UI shapes, weakens plugin runtime laziness, and gives agents too many provider-specific ways to express the same message intent. + +## Goals + +- Core decides the best semantic presentation for a message from declared capabilities. +- Extensions declare capabilities and render semantic presentation into native transport payloads. +- Web Control UI remains separate from chat native UI. +- Native channel payloads are not exposed through the shared agent or CLI message surface. +- Unsupported presentation features auto-degrade to the best text representation. +- Delivery behavior such as pinning a sent message is generic delivery metadata, not presentation. + +## Non Goals + +- No backwards compatibility shim for `buildCrossContextComponents`. +- No public native escape hatches for `components`, `blocks`, `buttons`, or `card`. +- No core imports of channel-native UI libraries. +- No provider-specific SDK seams for bundled channels. + +## Target Model + +Add a core-owned `presentation` field to `ReplyPayload`. + +```ts +type MessagePresentationTone = "neutral" | "info" | "success" | "warning" | "danger"; + +type MessagePresentation = { + tone?: MessagePresentationTone; + title?: string; + blocks: MessagePresentationBlock[]; +}; + +type MessagePresentationBlock = + | { type: "text"; text: string } + | { type: "context"; text: string } + | { type: "divider" } + | { type: "buttons"; buttons: MessagePresentationButton[] } + | { type: "select"; placeholder?: string; options: MessagePresentationOption[] }; + +type MessagePresentationButton = { + label: string; + value?: string; + url?: string; + style?: "primary" | "secondary" | "success" | "danger"; +}; + +type MessagePresentationOption = { + label: string; + value: string; +}; +``` + +`interactive` becomes a subset of `presentation` during migration: + +- `interactive` text block maps to `presentation.blocks[].type = "text"`. +- `interactive` buttons block maps to `presentation.blocks[].type = "buttons"`. +- `interactive` select block maps to `presentation.blocks[].type = "select"`. + +The external agent and CLI schemas now use `presentation`; `interactive` remains an internal legacy parser/rendering helper for existing reply producers. + +## Delivery Metadata + +Add a core-owned `delivery` field for send behavior that is not UI. + +```ts +type ReplyPayloadDelivery = { + pin?: + | boolean + | { + enabled: boolean; + notify?: boolean; + required?: boolean; + }; +}; +``` + +Semantics: + +- `delivery.pin = true` means pin the first successfully delivered message. +- `notify` defaults to `false`. +- `required` defaults to `false`; unsupported channels or failed pinning auto-degrade by continuing delivery. +- Manual `pin`, `unpin`, and `list-pins` message actions remain for existing messages. + +Current Telegram ACP topic binding should move from `channelData.telegram.pin = true` to `delivery.pin = true`. + +## Runtime Capability Contract + +Add presentation and delivery render hooks to the runtime outbound adapter, not the control-plane channel plugin. + +```ts +type ChannelPresentationCapabilities = { + supported: boolean; + buttons?: boolean; + selects?: boolean; + context?: boolean; + divider?: boolean; + tones?: MessagePresentationTone[]; +}; + +type ChannelDeliveryCapabilities = { + pinSentMessage?: boolean; +}; + +type ChannelOutboundAdapter = { + presentationCapabilities?: ChannelPresentationCapabilities; + + renderPresentation?: (params: { + payload: ReplyPayload; + presentation: MessagePresentation; + ctx: ChannelOutboundSendContext; + }) => ReplyPayload | null; + + deliveryCapabilities?: ChannelDeliveryCapabilities; + + pinDeliveredMessage?: (params: { + cfg: OpenClawConfig; + accountId?: string | null; + to: string; + threadId?: string | number | null; + messageId: string; + notify: boolean; + }) => Promise; +}; +``` + +Core behavior: + +- Resolve target channel and runtime adapter. +- Ask for presentation capabilities. +- Degrade unsupported blocks before rendering. +- Call `renderPresentation`. +- If no renderer exists, convert presentation to text fallback. +- After successful send, call `pinDeliveredMessage` when `delivery.pin` is requested and supported. + +## Channel Mapping + +Discord: + +- Render `presentation` to components v2 and Carbon containers in runtime-only modules. +- Keep accent color helpers in light modules. +- Remove `DiscordUiContainer` imports from channel plugin control-plane code. + +Slack: + +- Render `presentation` to Block Kit. +- Remove agent and CLI `blocks` input. + +Telegram: + +- Render text, context, and dividers as text. +- Render actions and select as inline keyboards when configured and allowed for the target surface. +- Use text fallback when inline buttons are disabled. +- Move ACP topic pinning to `delivery.pin`. + +Mattermost: + +- Render actions as interactive buttons where configured. +- Render other blocks as text fallback. + +MS Teams: + +- Render `presentation` to Adaptive Cards. +- Keep manual pin/unpin/list-pins actions. +- Optionally implement `pinDeliveredMessage` if Graph support is reliable for the target conversation. + +Feishu: + +- Render `presentation` to interactive cards. +- Keep manual pin/unpin/list-pins actions. +- Optionally implement `pinDeliveredMessage` for sent-message pinning if API behavior is reliable. + +LINE: + +- Render `presentation` to Flex or template messages where possible. +- Fall back to text for unsupported blocks. +- Remove LINE UI payloads from `channelData`. + +Plain or limited channels: + +- Convert presentation to text with conservative formatting. + +## Refactor Steps + +1. Reapply the Discord release fix that splits `ui-colors.ts` from Carbon-backed UI and removes `DiscordUiContainer` from `extensions/discord/src/channel.ts`. +2. Add `presentation` and `delivery` to `ReplyPayload`, outbound payload normalization, delivery summaries, and hook payloads. +3. Add `MessagePresentation` schema and parser helpers in a narrow SDK/runtime subpath. +4. Replace message capabilities `buttons`, `cards`, `components`, and `blocks` with semantic presentation capabilities. +5. Add runtime outbound adapter hooks for presentation render and delivery pinning. +6. Replace cross-context component construction with `buildCrossContextPresentation`. +7. Delete `src/infra/outbound/channel-adapters.ts` and remove `buildCrossContextComponents` from channel plugin types. +8. Change `maybeApplyCrossContextMarker` to attach `presentation` instead of native params. +9. Update plugin-dispatch send paths to consume only semantic presentation and delivery metadata. +10. Remove agent and CLI native payload params: `components`, `blocks`, `buttons`, and `card`. +11. Remove SDK helpers that create native message-tool schemas, replacing them with presentation schema helpers. +12. Remove UI/native envelopes from `channelData`; keep only transport metadata until each remaining field is reviewed. +13. Migrate Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, and LINE renderers. +14. Update docs for message CLI, channel pages, plugin SDK, and capability cookbook. +15. Run import fanout profiling for Discord and affected channel entrypoints. + +Steps 1-11 and 13-14 are implemented in this refactor for the shared agent, CLI, plugin capability, and outbound adapter contracts. Step 12 remains a deeper internal cleanup pass for provider-private `channelData` transport envelopes. Step 15 remains follow-up validation if we want quantified import-fanout numbers beyond the type/test gate. + +## Tests + +Add or update: + +- Presentation normalization tests. +- Presentation auto-degrade tests for unsupported blocks. +- Cross-context marker tests for plugin dispatch and core delivery paths. +- Channel render matrix tests for Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, LINE, and text fallback. +- Message tool schema tests proving native fields are gone. +- CLI tests proving native flags are gone. +- Discord entrypoint import-laziness regression covering Carbon. +- Delivery pin tests covering Telegram and generic fallback. + +## Open Questions + +- Should `delivery.pin` be implemented for Discord, Slack, MS Teams, and Feishu in the first pass, or only Telegram first? +- Should `delivery` eventually absorb existing fields such as `replyToId`, `replyToCurrent`, `silent`, and `audioAsVoice`, or stay focused on post-send behaviors? +- Should presentation support images or file references directly, or should media remain separate from UI layout for now? diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index 9a7ec5fb1..b4b2d18db 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -1251,16 +1251,21 @@ Compatibility note: ## Message tool schemas Plugins should own channel-specific `describeMessageTool(...)` schema -contributions. Keep provider-specific fields in the plugin, not in shared core. +contributions for non-message primitives such as reactions, reads, and polls. +Shared send presentation should use the generic `MessagePresentation` contract +instead of provider-native button, component, block, or card fields. +See [Message Presentation](/plugins/message-presentation) for the contract, +fallback rules, provider mapping, and plugin author checklist. -For shared portable schema fragments, reuse the generic helpers exported through -`openclaw/plugin-sdk/channel-actions`: +Send-capable plugins declare what they can render through message capabilities: -- `createMessageToolButtonsSchema()` for button-grid style payloads -- `createMessageToolCardSchema()` for structured card payloads +- `presentation` for semantic presentation blocks (`text`, `context`, `divider`, `buttons`, `select`) +- `delivery-pin` for pinned-delivery requests -If a schema shape only makes sense for one provider, define it in that plugin's -own source instead of promoting it into the shared SDK. +Core decides whether to render the presentation natively or degrade it to text. +Do not expose provider-native UI escape hatches from the generic message tool. +Deprecated SDK helpers for legacy native schemas remain exported for existing +third-party plugins, but new plugins should not use them. ## Channel target resolution diff --git a/docs/plugins/message-presentation.md b/docs/plugins/message-presentation.md new file mode 100644 index 000000000..5924134c0 --- /dev/null +++ b/docs/plugins/message-presentation.md @@ -0,0 +1,338 @@ +--- +title: "Message Presentation" +summary: "Semantic message cards, buttons, selects, fallback text, and delivery hints for channel plugins" +read_when: + - Adding or modifying message card, button, or select rendering + - Building a channel plugin that supports rich outbound messages + - Changing message tool presentation or delivery capabilities + - Debugging provider-specific card/block/component rendering regressions +--- + +# Message Presentation + +Message presentation is OpenClaw's shared contract for rich outbound chat UI. +It lets agents, CLI commands, approval flows, and plugins describe the message +intent once, while each channel plugin renders the best native shape it can. + +Use presentation for portable message UI: + +- text sections +- small context/footer text +- dividers +- buttons +- select menus +- card title and tone + +Do not add new provider-native fields such as Discord `components`, Slack +`blocks`, Telegram `buttons`, Teams `card`, or Feishu `card` to the shared +message tool. Those are renderer outputs owned by the channel plugin. + +## Contract + +Plugin authors import the public contract from: + +```ts +import type { + MessagePresentation, + ReplyPayloadDelivery, +} from "openclaw/plugin-sdk/interactive-runtime"; +``` + +Shape: + +```ts +type MessagePresentation = { + title?: string; + tone?: "neutral" | "info" | "success" | "warning" | "danger"; + blocks: MessagePresentationBlock[]; +}; + +type MessagePresentationBlock = + | { type: "text"; text: string } + | { type: "context"; text: string } + | { type: "divider" } + | { type: "buttons"; buttons: MessagePresentationButton[] } + | { type: "select"; placeholder?: string; options: MessagePresentationOption[] }; + +type MessagePresentationButton = { + label: string; + value?: string; + url?: string; + style?: "primary" | "secondary" | "success" | "danger"; +}; + +type MessagePresentationOption = { + label: string; + value: string; +}; + +type ReplyPayloadDelivery = { + pin?: + | boolean + | { + enabled: boolean; + notify?: boolean; + required?: boolean; + }; +}; +``` + +Button semantics: + +- `value` is an application action value routed back through the channel's + existing interaction path when the channel supports clickable controls. +- `url` is a link button. It can exist without `value`. +- `label` is required and is also used in text fallback. +- `style` is advisory. Renderers should map unsupported styles to a safe + default, not fail the send. + +Select semantics: + +- `options[].value` is the selected application value. +- `placeholder` is advisory and may be ignored by channels without native + select support. +- If a channel does not support selects, fallback text lists the labels. + +## Producer Examples + +Simple card: + +```json +{ + "title": "Deploy approval", + "tone": "warning", + "blocks": [ + { "type": "text", "text": "Canary is ready to promote." }, + { "type": "context", "text": "Build 1234, staging passed." }, + { + "type": "buttons", + "buttons": [ + { "label": "Approve", "value": "deploy:approve", "style": "success" }, + { "label": "Decline", "value": "deploy:decline", "style": "danger" } + ] + } + ] +} +``` + +URL-only link button: + +```json +{ + "blocks": [ + { "type": "text", "text": "Release notes are ready." }, + { + "type": "buttons", + "buttons": [{ "label": "Open notes", "url": "https://example.com/release" }] + } + ] +} +``` + +Select menu: + +```json +{ + "title": "Choose environment", + "blocks": [ + { + "type": "select", + "placeholder": "Environment", + "options": [ + { "label": "Canary", "value": "env:canary" }, + { "label": "Production", "value": "env:prod" } + ] + } + ] +} +``` + +CLI send: + +```bash +openclaw message send --channel slack \ + --target channel:C123 \ + --message "Deploy approval" \ + --presentation '{"title":"Deploy approval","tone":"warning","blocks":[{"type":"text","text":"Canary is ready."},{"type":"buttons","buttons":[{"label":"Approve","value":"deploy:approve","style":"success"},{"label":"Decline","value":"deploy:decline","style":"danger"}]}]}' +``` + +Pinned delivery: + +```bash +openclaw message send --channel telegram \ + --target -1001234567890 \ + --message "Topic opened" \ + --pin +``` + +Pinned delivery with explicit JSON: + +```json +{ + "pin": { + "enabled": true, + "notify": true, + "required": false + } +} +``` + +## Renderer Contract + +Channel plugins declare render support on their outbound adapter: + +```ts +const adapter: ChannelOutboundAdapter = { + deliveryMode: "direct", + presentationCapabilities: { + supported: true, + buttons: true, + selects: true, + context: true, + divider: true, + }, + deliveryCapabilities: { + pin: true, + }, + renderPresentation({ payload, presentation, ctx }) { + return renderNativePayload(payload, presentation, ctx); + }, + async pinDeliveredMessage({ target, messageId, pin }) { + await pinNativeMessage(target, messageId, { notify: pin.notify === true }); + }, +}; +``` + +Capability fields are intentionally simple booleans. They describe what the +renderer can make interactive, not every native platform limit. Renderers still +own platform-specific limits such as maximum button count, block count, and +card size. + +## Core Render Flow + +When a `ReplyPayload` or message action includes `presentation`, core: + +1. Normalizes the presentation payload. +2. Resolves the target channel's outbound adapter. +3. Reads `presentationCapabilities`. +4. Calls `renderPresentation` when the adapter can render the payload. +5. Falls back to conservative text when the adapter is absent or cannot render. +6. Sends the resulting payload through the normal channel delivery path. +7. Applies delivery metadata such as `delivery.pin` after the first successful + sent message. + +Core owns fallback behavior so producers can stay channel-agnostic. Channel +plugins own native rendering and interaction handling. + +## Degradation Rules + +Presentation must be safe to send on limited channels. + +Fallback text includes: + +- `title` as the first line +- `text` blocks as normal paragraphs +- `context` blocks as compact context lines +- `divider` blocks as a visual separator +- button labels, including URLs for link buttons +- select option labels + +Unsupported native controls should degrade rather than fail the whole send. +Examples: + +- Telegram with inline buttons disabled sends text fallback. +- A channel without select support lists select options as text. +- A URL-only button becomes either a native link button or a fallback URL line. +- Optional pin failures do not fail the delivered message. + +The main exception is `delivery.pin.required: true`; if pinning is requested as +required and the channel cannot pin the sent message, delivery reports failure. + +## Provider Mapping + +Current bundled renderers: + +| Channel | Native render target | Notes | +| --------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Discord | Components and component containers | Preserves legacy `channelData.discord.components` for existing provider-native payload producers, but new shared sends should use `presentation`. | +| Slack | Block Kit | Preserves legacy `channelData.slack.blocks` for existing provider-native payload producers, but new shared sends should use `presentation`. | +| Telegram | Text plus inline keyboards | Buttons/selects require inline button capability for the target surface; otherwise text fallback is used. | +| Mattermost | Text plus interactive props | Other blocks degrade to text. | +| Microsoft Teams | Adaptive Cards | Plain `message` text is included with the card when both are provided. | +| Feishu | Interactive cards | Card header can use `title`; body avoids duplicating that title. | +| Plain channels | Text fallback | Channels without a renderer still get readable output. | + +Provider-native payload compatibility is a transition affordance for existing +reply producers. It is not a reason to add new shared native fields. + +## Presentation vs InteractiveReply + +`InteractiveReply` is the older internal subset used by approval and interaction +helpers. It supports: + +- text +- buttons +- selects + +`MessagePresentation` is the canonical shared send contract. It adds: + +- title +- tone +- context +- divider +- URL-only buttons +- generic delivery metadata through `ReplyPayload.delivery` + +Use helpers from `openclaw/plugin-sdk/interactive-runtime` when bridging older +code: + +```ts +import { + interactiveReplyToPresentation, + normalizeMessagePresentation, + presentationToInteractiveReply, + renderMessagePresentationFallbackText, +} from "openclaw/plugin-sdk/interactive-runtime"; +``` + +New code should accept or produce `MessagePresentation` directly. + +## Delivery Pin + +Pinning is delivery behavior, not presentation. Use `delivery.pin` instead of +provider-native fields such as `channelData.telegram.pin`. + +Semantics: + +- `pin: true` pins the first successfully delivered message. +- `pin.notify` defaults to `false`. +- `pin.required` defaults to `false`. +- Optional pin failures degrade and leave the sent message intact. +- Required pin failures fail delivery. +- Chunked messages pin the first delivered chunk, not the tail chunk. + +Manual `pin`, `unpin`, and `pins` message actions still exist for existing +messages where the provider supports those operations. + +## Plugin Author Checklist + +- Declare `presentation` from `describeMessageTool(...)` when the channel can + render or safely degrade semantic presentation. +- Add `presentationCapabilities` to the runtime outbound adapter. +- Implement `renderPresentation` in runtime code, not control-plane plugin + setup code. +- Keep native UI libraries out of hot setup/catalog paths. +- Preserve platform limits in the renderer and tests. +- Add fallback tests for unsupported buttons, selects, URL buttons, title/text + duplication, and mixed `message` plus `presentation` sends. +- Add delivery pin support through `deliveryCapabilities.pin` and + `pinDeliveredMessage` only when the provider can pin the sent message id. +- Do not expose new provider-native card/block/component/button fields through + the shared message action schema. + +## Related Docs + +- [Message CLI](/cli/message) +- [Plugin SDK Overview](/plugins/sdk-overview) +- [Plugin Architecture](/plugins/architecture#message-tool-schemas) +- [Channel Presentation Refactor Plan](/plan/ui-channels) diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index 9613d5f9e..321016878 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -109,13 +109,13 @@ explicitly promotes one as public. | `plugin-sdk/allowlist-config-edit` | Allowlist config edit/read helpers | | `plugin-sdk/group-access` | Shared group-access decision helpers | | `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers | - | `plugin-sdk/interactive-runtime` | Interactive reply payload normalization/reduction helpers | + | `plugin-sdk/interactive-runtime` | Semantic message presentation, delivery, and legacy interactive reply helpers. See [Message Presentation](/plugins/message-presentation) | | `plugin-sdk/channel-inbound` | Compatibility barrel for inbound debounce, mention matching, mention-policy helpers, and envelope helpers | | `plugin-sdk/channel-mention-gating` | Narrow mention-policy helpers without the broader inbound runtime surface | | `plugin-sdk/channel-location` | Channel location context and formatting helpers | | `plugin-sdk/channel-logging` | Channel logging helpers for inbound drops and typing/ack failures | | `plugin-sdk/channel-send-result` | Reply result types | - | `plugin-sdk/channel-actions` | `createMessageToolButtonsSchema`, `createMessageToolCardSchema` | + | `plugin-sdk/channel-actions` | Channel message-action helpers, plus deprecated native schema helpers kept for plugin compatibility | | `plugin-sdk/channel-targets` | Target parsing/matching helpers | | `plugin-sdk/channel-contract` | Channel contract types | | `plugin-sdk/channel-feedback` | Feedback/reaction wiring | diff --git a/docs/plugins/skill-workshop.md b/docs/plugins/skill-workshop.md new file mode 100644 index 000000000..686425c0f --- /dev/null +++ b/docs/plugins/skill-workshop.md @@ -0,0 +1,731 @@ +--- +title: "Skill Workshop Plugin" +summary: "Experimental capture of reusable procedures as workspace skills with review, approval, quarantine, and hot skill refresh" +read_when: + - You want agents to turn corrections or reusable procedures into workspace skills + - You are configuring procedural skill memory + - You are debugging skill_workshop tool behavior + - You are deciding whether to enable automatic skill creation +--- + +# Skill Workshop Plugin + +Skill Workshop is **experimental**. It is disabled by default, its capture +heuristics and reviewer prompts may change between releases, and automatic +writes should be used only in trusted workspaces after reviewing pending-mode +output first. + +Skill Workshop is procedural memory for workspace skills. It lets an agent turn +reusable workflows, user corrections, hard-won fixes, and recurring pitfalls +into `SKILL.md` files under: + +```text +/skills//SKILL.md +``` + +This is different from long-term memory: + +- **Memory** stores facts, preferences, entities, and past context. +- **Skills** store reusable procedures the agent should follow on future tasks. +- **Skill Workshop** is the bridge from a useful turn to a durable workspace + skill, with safety checks and optional approval. + +Skill Workshop is useful when the agent learns a procedure such as: + +- how to validate externally sourced animated GIF assets +- how to replace screenshot assets and verify dimensions +- how to run a repo-specific QA scenario +- how to debug a recurring provider failure +- how to repair a stale local workflow note + +It is not intended for: + +- facts like “the user likes blue” +- broad autobiographical memory +- raw transcript archiving +- secrets, credentials, or hidden prompt text +- one-off instructions that will not repeat + +## Default State + +The bundled plugin is **experimental** and **disabled by default** unless it is +explicitly enabled in `plugins.entries.skill-workshop`. + +The plugin manifest does not set `enabledByDefault: true`. The `enabled: true` +default inside the plugin config schema applies only after the plugin entry has +already been selected and loaded. + +Experimental means: + +- the plugin is supported enough for opt-in testing and dogfooding +- proposal storage, reviewer thresholds, and capture heuristics can evolve +- pending approval is the recommended starting mode +- auto apply is for trusted personal/workspace setups, not shared or hostile + input-heavy environments + +## Enable + +Minimal safe config: + +```json5 +{ + plugins: { + entries: { + "skill-workshop": { + enabled: true, + config: { + autoCapture: true, + approvalPolicy: "pending", + reviewMode: "hybrid", + }, + }, + }, + }, +} +``` + +With this config: + +- the `skill_workshop` tool is available +- explicit reusable corrections are queued as pending proposals +- threshold-based reviewer passes can propose skill updates +- no skill file is written until a pending proposal is applied + +Use automatic writes only in trusted workspaces: + +```json5 +{ + plugins: { + entries: { + "skill-workshop": { + enabled: true, + config: { + autoCapture: true, + approvalPolicy: "auto", + reviewMode: "hybrid", + }, + }, + }, + }, +} +``` + +`approvalPolicy: "auto"` still uses the same scanner and quarantine path. It +does not apply proposals with critical findings. + +## Configuration + +| Key | Default | Range / values | Meaning | +| -------------------- | ----------- | ------------------------------------------- | -------------------------------------------------------------------- | +| `enabled` | `true` | boolean | Enables the plugin after the plugin entry is loaded. | +| `autoCapture` | `true` | boolean | Enables post-turn capture/review on successful agent turns. | +| `approvalPolicy` | `"pending"` | `"pending"`, `"auto"` | Queue proposals or write safe proposals automatically. | +| `reviewMode` | `"hybrid"` | `"off"`, `"heuristic"`, `"llm"`, `"hybrid"` | Chooses explicit correction capture, LLM reviewer, both, or neither. | +| `reviewInterval` | `15` | `1..200` | Run reviewer after this many successful turns. | +| `reviewMinToolCalls` | `8` | `1..500` | Run reviewer after this many observed tool calls. | +| `reviewTimeoutMs` | `45000` | `5000..180000` | Timeout for the embedded reviewer run. | +| `maxPending` | `50` | `1..200` | Max pending/quarantined proposals kept per workspace. | +| `maxSkillBytes` | `40000` | `1024..200000` | Max generated skill/support file size. | + +Recommended profiles: + +```json5 +// Conservative: explicit tool use only, no automatic capture. +{ + autoCapture: false, + approvalPolicy: "pending", + reviewMode: "off", +} +``` + +```json5 +// Review-first: capture automatically, but require approval. +{ + autoCapture: true, + approvalPolicy: "pending", + reviewMode: "hybrid", +} +``` + +```json5 +// Trusted automation: write safe proposals immediately. +{ + autoCapture: true, + approvalPolicy: "auto", + reviewMode: "hybrid", +} +``` + +```json5 +// Low-cost: no reviewer LLM call, only explicit correction phrases. +{ + autoCapture: true, + approvalPolicy: "pending", + reviewMode: "heuristic", +} +``` + +## Capture Paths + +Skill Workshop has three capture paths. + +### Tool Suggestions + +The model can call `skill_workshop` directly when it sees a reusable procedure +or when the user asks it to save/update a skill. + +This is the most explicit path and works even with `autoCapture: false`. + +### Heuristic Capture + +When `autoCapture` is enabled and `reviewMode` is `heuristic` or `hybrid`, the +plugin scans successful turns for explicit user correction phrases: + +- `next time` +- `from now on` +- `remember to` +- `make sure to` +- `always ... use/check/verify/record/save/prefer` +- `prefer ... when/for/instead/use` +- `when asked` + +The heuristic creates a proposal from the latest matching user instruction. It +uses topic hints to choose skill names for common workflows: + +- animated GIF tasks -> `animated-gif-workflow` +- screenshot or asset tasks -> `screenshot-asset-workflow` +- QA or scenario tasks -> `qa-scenario-workflow` +- GitHub PR tasks -> `github-pr-workflow` +- fallback -> `learned-workflows` + +Heuristic capture is intentionally narrow. It is for clear corrections and +repeatable process notes, not for general transcript summarization. + +### LLM Reviewer + +When `autoCapture` is enabled and `reviewMode` is `llm` or `hybrid`, the plugin +runs a compact embedded reviewer after thresholds are reached. + +The reviewer receives: + +- the recent transcript text, capped to the last 12,000 characters +- up to 12 existing workspace skills +- up to 2,000 characters from each existing skill +- JSON-only instructions + +The reviewer has no tools: + +- `disableTools: true` +- `toolsAllow: []` +- `disableMessageTool: true` + +It can return: + +```json +{ "action": "none" } +``` + +or one skill proposal: + +```json +{ + "action": "create", + "skillName": "media-asset-qa", + "title": "Media Asset QA", + "reason": "Reusable animated media acceptance workflow", + "description": "Validate externally sourced animated media before product use.", + "body": "## Workflow\n\n- Verify true animation.\n- Record attribution.\n- Store a local approved copy.\n- Verify in product UI before final reply." +} +``` + +It can also append to an existing skill: + +```json +{ + "action": "append", + "skillName": "qa-scenario-workflow", + "title": "QA Scenario Workflow", + "reason": "Animated media QA needs reusable checks", + "description": "QA scenario workflow.", + "section": "Workflow", + "body": "- For animated GIF tasks, verify frame count and attribution before passing." +} +``` + +Or replace exact text in an existing skill: + +```json +{ + "action": "replace", + "skillName": "screenshot-asset-workflow", + "title": "Screenshot Asset Workflow", + "reason": "Old validation missed image optimization", + "oldText": "- Replace the screenshot asset.", + "newText": "- Replace the screenshot asset, preserve dimensions, optimize the PNG, and run the relevant validation gate." +} +``` + +Prefer `append` or `replace` when a relevant skill already exists. Use `create` +only when no existing skill fits. + +## Proposal Lifecycle + +Every generated update becomes a proposal with: + +- `id` +- `createdAt` +- `updatedAt` +- `workspaceDir` +- optional `agentId` +- optional `sessionId` +- `skillName` +- `title` +- `reason` +- `source`: `tool`, `agent_end`, or `reviewer` +- `status` +- `change` +- optional `scanFindings` +- optional `quarantineReason` + +Proposal statuses: + +- `pending` - waiting for approval +- `applied` - written to `/skills` +- `rejected` - rejected by operator/model +- `quarantined` - blocked by critical scanner findings + +State is stored per workspace under the Gateway state directory: + +```text +/skill-workshop/.json +``` + +Pending and quarantined proposals are deduplicated by skill name and change +payload. The store keeps the newest pending/quarantined proposals up to +`maxPending`. + +## Tool Reference + +The plugin registers one agent tool: + +```text +skill_workshop +``` + +### `status` + +Count proposals by state for the active workspace. + +```json +{ "action": "status" } +``` + +Result shape: + +```json +{ + "workspaceDir": "/path/to/workspace", + "pending": 1, + "quarantined": 0, + "applied": 3, + "rejected": 0 +} +``` + +### `list_pending` + +List pending proposals. + +```json +{ "action": "list_pending" } +``` + +To list another status: + +```json +{ "action": "list_pending", "status": "applied" } +``` + +Valid `status` values: + +- `pending` +- `applied` +- `rejected` +- `quarantined` + +### `list_quarantine` + +List quarantined proposals. + +```json +{ "action": "list_quarantine" } +``` + +Use this when automatic capture appears to do nothing and the logs mention +`skill-workshop: quarantined `. + +### `inspect` + +Fetch a proposal by id. + +```json +{ + "action": "inspect", + "id": "proposal-id" +} +``` + +### `suggest` + +Create a proposal. With `approvalPolicy: "pending"`, this queues by default. + +```json +{ + "action": "suggest", + "skillName": "animated-gif-workflow", + "title": "Animated GIF Workflow", + "reason": "User established reusable GIF validation rules.", + "description": "Validate animated GIF assets before using them.", + "body": "## Workflow\n\n- Verify the URL resolves to image/gif.\n- Confirm it has multiple frames.\n- Record attribution and license.\n- Avoid hotlinking when a local asset is needed." +} +``` + +Force a safe write: + +```json +{ + "action": "suggest", + "apply": true, + "skillName": "animated-gif-workflow", + "description": "Validate animated GIF assets before using them.", + "body": "## Workflow\n\n- Verify true animation.\n- Record attribution." +} +``` + +Force pending even in `approvalPolicy: "auto"`: + +```json +{ + "action": "suggest", + "apply": false, + "skillName": "screenshot-asset-workflow", + "description": "Screenshot replacement workflow.", + "body": "## Workflow\n\n- Verify dimensions.\n- Optimize the PNG.\n- Run the relevant gate." +} +``` + +Append to a section: + +```json +{ + "action": "suggest", + "skillName": "qa-scenario-workflow", + "section": "Workflow", + "description": "QA scenario workflow.", + "body": "- For media QA, verify generated assets render and pass final assertions." +} +``` + +Replace exact text: + +```json +{ + "action": "suggest", + "skillName": "github-pr-workflow", + "oldText": "- Check the PR.", + "newText": "- Check unresolved review threads, CI status, linked issues, and changed files before deciding." +} +``` + +### `apply` + +Apply a pending proposal. + +```json +{ + "action": "apply", + "id": "proposal-id" +} +``` + +`apply` refuses quarantined proposals: + +```text +quarantined proposal cannot be applied +``` + +### `reject` + +Mark a proposal rejected. + +```json +{ + "action": "reject", + "id": "proposal-id" +} +``` + +### `write_support_file` + +Write a supporting file inside an existing or proposed skill directory. + +Allowed top-level support directories: + +- `references/` +- `templates/` +- `scripts/` +- `assets/` + +Example: + +```json +{ + "action": "write_support_file", + "skillName": "release-workflow", + "relativePath": "references/checklist.md", + "body": "# Release Checklist\n\n- Run release docs.\n- Verify changelog.\n" +} +``` + +Support files are workspace-scoped, path-checked, byte-limited by +`maxSkillBytes`, scanned, and written atomically. + +## Skill Writes + +Skill Workshop writes only under: + +```text +/skills// +``` + +Skill names are normalized: + +- lowercased +- non `[a-z0-9_-]` runs become `-` +- leading/trailing non-alphanumerics are removed +- max length is 80 characters +- final name must match `[a-z0-9][a-z0-9_-]{1,79}` + +For `create`: + +- if the skill does not exist, Skill Workshop writes a new `SKILL.md` +- if it already exists, Skill Workshop appends the body to `## Workflow` + +For `append`: + +- if the skill exists, Skill Workshop appends to the requested section +- if it does not exist, Skill Workshop creates a minimal skill then appends + +For `replace`: + +- the skill must already exist +- `oldText` must be present exactly +- only the first exact match is replaced + +All writes are atomic and refresh the in-memory skills snapshot immediately, so +the new or updated skill can become visible without a Gateway restart. + +## Safety Model + +Skill Workshop has a safety scanner on generated `SKILL.md` content and support +files. + +Critical findings quarantine proposals: + +| Rule id | Blocks content that... | +| -------------------------------------- | --------------------------------------------------------------------- | +| `prompt-injection-ignore-instructions` | tells the agent to ignore prior/higher instructions | +| `prompt-injection-system` | references system prompts, developer messages, or hidden instructions | +| `prompt-injection-tool` | encourages bypassing tool permission/approval | +| `shell-pipe-to-shell` | includes `curl`/`wget` piped into `sh`, `bash`, or `zsh` | +| `secret-exfiltration` | appears to send env/process env data over the network | + +Warn findings are retained but do not block by themselves: + +| Rule id | Warns on... | +| -------------------- | -------------------------------- | +| `destructive-delete` | broad `rm -rf` style commands | +| `unsafe-permissions` | `chmod 777` style permission use | + +Quarantined proposals: + +- keep `scanFindings` +- keep `quarantineReason` +- appear in `list_quarantine` +- cannot be applied through `apply` + +To recover from a quarantined proposal, create a new safe proposal with the +unsafe content removed. Do not edit the store JSON by hand. + +## Prompt Guidance + +When enabled, Skill Workshop injects a short prompt section that tells the agent +to use `skill_workshop` for durable procedural memory. + +The guidance emphasizes: + +- procedures, not facts/preferences +- user corrections +- non-obvious successful procedures +- recurring pitfalls +- stale/thin/wrong skill repair through append/replace +- saving reusable procedure after long tool loops or hard fixes +- short imperative skill text +- no transcript dumps + +The write mode text changes with `approvalPolicy`: + +- pending mode: queue suggestions; apply only after explicit approval +- auto mode: apply safe workspace-skill updates when clearly reusable + +## Costs and Runtime Behavior + +Heuristic capture does not call a model. + +LLM review uses an embedded run on the active/default agent model. It is +threshold-based so it does not run on every turn by default. + +The reviewer: + +- uses the same configured provider/model context when available +- falls back to runtime agent defaults +- has `reviewTimeoutMs` +- uses lightweight bootstrap context +- has no tools +- writes nothing directly +- can only emit a proposal that goes through the normal scanner and + approval/quarantine path + +If the reviewer fails, times out, or returns invalid JSON, the plugin logs a +warning/debug message and skips that review pass. + +## Operating Patterns + +Use Skill Workshop when the user says: + +- “next time, do X” +- “from now on, prefer Y” +- “make sure to verify Z” +- “save this as a workflow” +- “this took a while; remember the process” +- “update the local skill for this” + +Good skill text: + +```markdown +## Workflow + +- Verify the GIF URL resolves to `image/gif`. +- Confirm the file has multiple frames. +- Record source URL, license, and attribution. +- Store a local copy when the asset will ship with the product. +- Verify the local asset renders in the target UI before final reply. +``` + +Poor skill text: + +```markdown +The user asked about a GIF and I searched two websites. Then one was blocked by +Cloudflare. The final answer said to check attribution. +``` + +Reasons the poor version should not be saved: + +- transcript-shaped +- not imperative +- includes noisy one-off details +- does not tell the next agent what to do + +## Debugging + +Check whether the plugin is loaded: + +```bash +openclaw plugins list --enabled +``` + +Check proposal counts from an agent/tool context: + +```json +{ "action": "status" } +``` + +Inspect pending proposals: + +```json +{ "action": "list_pending" } +``` + +Inspect quarantined proposals: + +```json +{ "action": "list_quarantine" } +``` + +Common symptoms: + +| Symptom | Likely cause | Check | +| ------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| Tool is unavailable | Plugin entry is not enabled | `plugins.entries.skill-workshop.enabled` and `openclaw plugins list` | +| No automatic proposal appears | `autoCapture: false`, `reviewMode: "off"`, or thresholds not met | Config, proposal status, Gateway logs | +| Heuristic did not capture | User wording did not match correction patterns | Use explicit `skill_workshop.suggest` or enable LLM reviewer | +| Reviewer did not create a proposal | Reviewer returned `none`, invalid JSON, or timed out | Gateway logs, `reviewTimeoutMs`, thresholds | +| Proposal is not applied | `approvalPolicy: "pending"` | `list_pending`, then `apply` | +| Proposal disappeared from pending | Duplicate proposal reused, max pending pruning, or was applied/rejected/quarantined | `status`, `list_pending` with status filters, `list_quarantine` | +| Skill file exists but model misses it | Skill snapshot not refreshed or skill gating excludes it | `openclaw skills` status and workspace skill eligibility | + +Relevant logs: + +- `skill-workshop: queued ` +- `skill-workshop: applied ` +- `skill-workshop: quarantined ` +- `skill-workshop: heuristic capture skipped: ...` +- `skill-workshop: reviewer skipped: ...` +- `skill-workshop: reviewer found no update` + +## QA Scenarios + +Repo-backed QA scenarios: + +- `qa/scenarios/plugins/skill-workshop-animated-gif-autocreate.md` +- `qa/scenarios/plugins/skill-workshop-pending-approval.md` +- `qa/scenarios/plugins/skill-workshop-reviewer-autonomous.md` + +Run the deterministic coverage: + +```bash +pnpm openclaw qa suite \ + --scenario skill-workshop-animated-gif-autocreate \ + --scenario skill-workshop-pending-approval \ + --concurrency 1 +``` + +Run reviewer coverage: + +```bash +pnpm openclaw qa suite \ + --scenario skill-workshop-reviewer-autonomous \ + --concurrency 1 +``` + +The reviewer scenario is intentionally separate because it enables +`reviewMode: "llm"` and exercises the embedded reviewer pass. + +## When Not To Enable Auto Apply + +Avoid `approvalPolicy: "auto"` when: + +- the workspace contains sensitive procedures +- the agent is working on untrusted input +- skills are shared across a broad team +- you are still tuning prompts or scanner rules +- the model frequently handles hostile web/email content + +Use pending mode first. Switch to auto mode only after reviewing the kind of +skills the agent proposes in that workspace. + +## Related Docs + +- [Skills](/tools/skills) +- [Plugins](/tools/plugin) +- [Testing](/reference/test) diff --git a/docs/tools/skills.md b/docs/tools/skills.md index 33827136c..4e979cdfe 100644 --- a/docs/tools/skills.md +++ b/docs/tools/skills.md @@ -90,6 +90,24 @@ You can gate them via `metadata.openclaw.requires.config` on the plugin’s conf entry. See [Plugins](/tools/plugin) for discovery/config and [Tools](/tools) for the tool surface those skills teach. +## Skill Workshop + +The optional, experimental Skill Workshop plugin can create or update workspace +skills from reusable procedures observed during agent work. It is disabled by +default and must be explicitly enabled through +`plugins.entries.skill-workshop`. + +Skill Workshop writes only to `/skills`, scans generated content, +supports pending approval or automatic safe writes, quarantines unsafe +proposals, and refreshes the skill snapshot after successful writes so new +skills can become available without a Gateway restart. + +Use it when you want corrections such as “next time, verify GIF attribution” or +hard-won workflows such as media QA checklists to become durable procedural +instructions. Start with pending approval; use automatic writes only in trusted +workspaces after reviewing its proposals. Full guide: +[Skill Workshop Plugin](/plugins/skill-workshop). + ## ClawHub (install + sync) ClawHub is the public skills registry for OpenClaw. Browse at