Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/verify-deepseekbot/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ One file per user-facing feature. Each file has four sections: `Sub-features`, `
| Historical Overview: idle PersonaBot (retired by #2026) | no | `scripts/e2e-activity-idle.mjs` | [activity-center-idle.md](activity-center-idle.md) |
| Temporary Workspace Grant authorization, read and revocation | no | `scripts/e2e-workspace-grant.mjs` | [workspace-grant.md](workspace-grant.md) |
| Default Bot Browser open, observe, stop and Access refusal | no | `scripts/e2e-browser-lifecycle.mjs` | [browser-lifecycle.md](browser-lifecycle.md) |
| Skill Invocation from `/` in a Bot DM | yes for the reply | `verify-app` Bot DM and `skills-list` / `send --skill` | [skill-invocation.md](skill-invocation.md) |
| Composer footer customization and optional reply speed | yes for a speed sample | none (Web interaction path) | [composer-footer.md](composer-footer.md) |
| Disposable Memory history, restore and stale-head refusal | no | `scripts/e2e-memory-restore.mjs` | [memory-restore.md](memory-restore.md) |
| Version-bound Memory images, Markdown previews and CLI export | no | `scripts/e2e-memory-images.mjs`, `scripts/e2e-memory-markdown.mjs`, `scripts/e2e-memory-formats.mjs` | [memory-images.md](memory-images.md) |
Expand Down
32 changes: 32 additions & 0 deletions .agents/skills/verify-deepseekbot/features/skill-invocation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Skill Invocation in a Bot DM

A Human types `/` in a PersonaBot DM, picks a Skill, and sends it. The Host stores the Skill's text with the message and the Bot reads it as a `<skill_invocation>` block ([ADR-0346](../../../../docs/adr/0346-skill-invocation-is-snapshotted-text-that-addresses-no-one.md)).

## Sub-features

- `menu` lists composer commands first, then Skills: Host Skills and the Project Skills under the Bot's Memory Repository `.agents/skills` or `.dsh/skills`. Each row shows its owner (Bot avatar or Skill icon), `/name` and a one-line description.
- `rank` orders Skills by name prefix, then name subsequence, then description text.
- `chip` inserts the picked Skill as one token; Backspace removes it whole; the draft keeps it across reloads.
- `snapshot` stores `{name, owner, path, content}` with the message; editing or deleting the `SKILL.md` later does not change the sent message.
- `deliver` gives the Bot the message with `[/name]` and the `<skill_invocation>` block, so DSH does not load the Skill a second time.
- `cli` `deepseekbot skills-list --bot <slug> [--query]` and `send <slug> --body "/name …" --skill <owner>/<name>`.

## How to get to it (user POV)

- Enter Bot mode, open a Bot DM, type `/` and a few letters in the composer, choose a Skill with the arrow keys and Enter (or click it), write the rest of the message, press Enter.

## Driving it with verify-app

Preconditions: `launch` reported a model credential and `doctor` passes.

1. **Seed a Project Skill.** Get the Bot's Memory Repository from `rpc botharness/list` and the Host profile layout, then write `<memory>/.agents/skills/qa-echo/SKILL.md` with frontmatter `name: qa-echo`, `description: Reply with a fixed QA word` and a body such as `Reply with exactly SKILL_QA_7731 and nothing else.`
2. **Menu.** `shot skill-menu --bot-mode --fill "Message <Bot>=/qa"` and read `skill-menu.aria.txt`: a `/qa-echo · <Bot>` menu item appears after any matching command.
3. **Send.** `shot skill-send --bot-mode --fill "Message <Bot>=/qa" --press Enter --press Enter --wait-text SKILL_QA_7731`. The first Enter picks the Skill, the second sends.
4. **Canonical state.** `rpc botharness/channelTimeline '{"channelId":"<dm id>"}'`: the Human entry has `skillInvocations[0]` with `owner {kind: bot}`, the `path` of the seeded file and its body; the Bot entry replies `SKILL_QA_7731`.
5. **CLI.** `deepseekbot skills-list --bot <slug> --query qa` lists `qa-echo`; `deepseekbot send <slug> --body "/qa-echo" --skill bot:<slug>/qa-echo` returns a committed reply.

## Gotchas

- The `/` menu reads the catalog when the DM opens. A Skill written while the DM is open appears after reopening the DM until S3 (live refresh) lands.
- DSH discovers `<memory>/.agents/skills` because the Orchestrator's cwd is the Memory Repository; a Skill placed in `.botharness/` is not found.
- Recalling an older message with ArrowUp, or restoring a failed send, keeps `/name` as plain text without the chip.
11 changes: 11 additions & 0 deletions changes/4232-dm-skill-invocation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
section: Added
---

## en

- Type `/` in a PersonaBot DM to pick a Skill: commands come first, then Host Skills and the Skills in the Bot's Memory Repository `.agents/skills`, each with its owner and a one-line description. The picked Skill becomes a chip, its text is saved with the message, and the Bot reads that text with your message; `deepseekbot skills-list` and `send --skill` do the same from the CLI ([#4232](https://github.com/BotHarness/DeepSeekBot/issues/4232)).

## zh

- 在 PersonaBot 私聊中输入 `/` 即可选择 Skill:先列命令,再列 Host Skill 和该 Bot Memory Repository `.agents/skills` 下的 Skill,每行显示归属和单行描述。选中的 Skill 显示为 chip,其文本随消息保存,Bot 会连同你的消息一起读到;CLI 中可用 `deepseekbot skills-list` 和 `send --skill` 完成同样的操作([#4232](https://github.com/BotHarness/DeepSeekBot/issues/4232))。
11 changes: 11 additions & 0 deletions changes/4232-skill-invocation-review-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
section: Fixed
---

## en

- Fixed repeated Skill selection in the CLI, selected Skill and mention rendering in Bot prompts, oversized Skill sends being silently truncated, private or archived Bot Skills being accepted in Group sends, and Skill offsets after All Bots expansion ([#4232](https://github.com/BotHarness/BotHarness/issues/4232)).

## zh

- 修复 CLI 重复选择同名 Skill、Bot 提示词中已选 Skill 与提及的渲染,超长 Skill 发送时被静默截断、群聊接受无权读取或已归档 Bot 的 Skill,以及所有 Bot 提及展开后的 Skill 位置错误([#4232](https://github.com/BotHarness/BotHarness/issues/4232))。
20 changes: 20 additions & 0 deletions docs/adr/0345-project-skills-travel-with-their-directory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
Status: Proposed
Date: 2026-11-15
---

# Project Skills travel with their directory

A PersonaBot's Skills are the **Project Skills** of its Memory Repository: `SKILL.md` folders under `<Memory Repository>/.agents/skills/`. They are not a separate kind of Skill and have no store of their own. The Orchestrator Session's cwd is already the Memory Repository, so DSH's file-skill discovery finds them, and in that PersonaBot's own Sessions they override a **Host Skill** of the same name, without BotHarness registration code. A folder covered by a Workspace Grant contributes Project Skills the same way. BotHarness scans only the granted root itself (`.agents/skills` and `.dsh/skills`) and does not walk up to an enclosing Git root, so visibility never exceeds the Grant.

Installing a Skill is a file write. A PersonaBot decides where a Skill belongs. If it is the bot's own capability, it goes in the Memory Repository, which the bot may write freely. If it is a project convention, it goes in a Workspace folder, under the existing write authority. Memory's Git history is the record of these writes, so no separate activity entry is created. A Human removes a Project Skill by deleting its files; there is no disabled state.

Because Project Skills are ordinary text files in the Memory Repository, they travel with every existing way of sharing a PersonaBot: the Memory Git repository, a Bot Zip and a Profile Backup. This differs from ADR-0044. That ADR rejects embedding or auto-installing Skills as **Host** runtime dependencies. A Project Skill is part of the PersonaBot's own content, not a Host dependency. On a target Host, the Project Skills arrive with the rest of the PersonaBot's Memory and are discoverable as soon as the PersonaBot is installed or imported. They get no separate Skill review: they carry the same trust as the Memory content they live in. The marketplace detail page and the PersonaBot profile show a Skills section listing the Memory Repository's Project Skills. Workspace Skills belong to the project and are not listed there.

## Considered Options

- **A `.botharness/skills/` folder with a BotHarness skill provider** — rejected: DSH does not scan it. BotHarness would need a per-bot provider, but DSH registrations are scoped only to an agent preset. It would also diverge from the `.agents/skills` layout that Humans and cross-agent tooling already use.
- **A separate "Bot Skill" concept** — rejected: it has the same mechanism as any Project Skill. A second name would suggest storage or rules that do not exist.
- **Walk up to the enclosing Git root like DSH** — rejected for BotHarness listings: a Grant on a monorepo subfolder would expose Skills outside the granted folder.
- **Export only Desired Dependency References and leave the Skill files behind** — rejected: Skills are a defining part of what a PersonaBot can do, and they are plain text that already lives in its Memory.
- **Hold imported Skills in a pending folder until a Human reviews them** — rejected: the rest of the Memory Repository, which also shapes the PersonaBot's behavior, is not held back. A Skill-only gate would add a second copy of each Skill, a move on acceptance, and conflicts with the Memory Git repository shared between Hosts, without closing a gap the Memory itself leaves open.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
Status: Proposed
Date: 2026-11-15
---

# Skill Invocation is snapshotted text that addresses no one

Typing `/` in a Channel or DM composer lists commands first, then Skills. The Skills come from the Host Skills plus the Project Skills of every PersonaBot in the conversation: each bot's Memory Repository and its Workspace Grants. A DM counts only that one PersonaBot. Each row shows its owner (a PersonaBot avatar, a folder for a Workspace, or a Host mark), its name and a one-line description. Picking a row inserts a chip, as `@` does.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

A **Skill Invocation** addresses no one. It inserts the Skill's text into the message. Which PersonaBot reads and acts on the message follows its mentions and normal routing, never the Skill's owner. Picking Alice's Skill and mentioning Bob gives Bob that text. Ownership affects only automatic discovery: a bot's model catalog contains Host Skills and its own Memory Repository's Project Skills, and nothing else.

The Host resolves the Skill when the message is sent and stores a snapshot in a typed `ChannelMessage` part: the name, the owner (`host`, `bot:<slug>` or `workspace:<name>`), the `SKILL.md` path and the content. Every recipient therefore gets identical text, even one whose Sessions cannot discover that Skill. History stays reproducible after the Skill changes or is deleted. Bot Inbox delivery renders the snapshot as a `<skill_invocation>` block carrying its owner and path. A recipient without read authority for that path receives the text but not the files: ADR-0067's path guard still applies. External IM mirrors fall back to the plain text `/name`.

When a command and a Skill share a name, the command keeps the bare name.

Delivery is staged under #4231. The first slice covers Human DMs with Host Skills and the PersonaBot's Memory Repository Skills, so `SkillOwner` has only the `host` and `bot` kinds. Group Channels and Workspace Grant Skills, with the `workspace:<name>` owner, arrive in later slices.

## Considered Options

- **A Skill Invocation implicitly mentions the Skill's owner** — rejected: it adds hidden rules ("whose Skill is it?") to what Humans already understand as inserted text plus explicit mentions.
- **Expand at delivery time, per recipient** — rejected: recipients could see different text, history would not reproduce, and a recipient that cannot discover the Skill would fail.
- **Plain `/name` text expanded by DSH's user-message scanner** — rejected: Channel messages reach bots as Bot Inbox text, not DSH user messages. The scanner also matches `/word` anywhere and cannot say which owner's Skill was meant.
4 changes: 4 additions & 0 deletions docs/bot-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ From a built checkout, `node scripts/cli-smoke.mjs --preset <id> [--zip ./shared
deepseekbot send <bot-id> --body "Reply with QA_OK" --timeout 60
deepseekbot send <bot-id> --body-stdin --message-id human-<UUID>
deepseekbot send <bot-id> --body "Yes, this one" --reply-to <message-id>
deepseekbot skills-list --bot <bot-id> [--query <text>]
deepseekbot send <bot-id> --body "/summarize-pr #42" --skill bot:<bot-id>/summarize-pr
deepseekbot send-status <bot-id> --message-id <receipt-message-id>
deepseekbot channel-messages <channel-id> --host http://127.0.0.1:31917 --limit 20
deepseekbot tool-approval-status <channel-id> --message-id <card-id>
Expand All @@ -236,6 +238,8 @@ deepseekbot folder-browse [<folder-path>] [--offset <n>]
deepseekbot grant-create <bot-id> --workspace <workspace-id>
```

`skills-list` returns the Skills the `/` menu offers in that conversation, ranked like the menu by `--query` (name prefix, then name subsequence, then description): Host Skills (`owner.kind` `host`) and the Project Skills under each PersonaBot's Memory Repository `.agents/skills` or `.dsh/skills` (`owner.kind` `bot`), as that Bot's Orchestrator sees them. Pass `--channel <channel-id>` instead of `--bot` for any Channel. `send --skill <owner>/<name>` (owner `host` or `bot:<bot-id>`, repeatable) turns the first unused `/name` token in the body into a Skill Invocation for each flag, in flag order (the token must start and end at whitespace or a body boundary; the send is refused if no unused occurrence remains): the Host stores the Skill's text as it is now with the message and the Bot reads it as a `<skill_invocation>` block. The Skill does not choose who replies; a Skill that is gone refuses with `skill-unavailable`, and Skill text longer than 64,000 characters refuses with `skill-too-large` instead of being truncated. See [ADR-0346](adr/0346-skill-invocation-is-snapshotted-text-that-addresses-no-one.md).

`tool-approval-status` returns `status` and, while the card is still `pending`, its `expiresAt`, the time the Web card counts down to. `--outcome allowed-always-kind` is the `secret_cli` card's "Always allow this kind": it allows this call and saves a rule for the same program, account and environment variable names, which `tool-approval-rules` lists with kind `secret-cli`; any other card refuses it.

`user-question-answer` accepts the same offered selections as the Web card. A durable timed
Expand Down
4 changes: 4 additions & 0 deletions docs/bot-cli.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ Prompt Preset 用于编辑 System Prompt 和每轮模板里的行为文字;围
deepseekbot send <bot-id> --body "回复 QA_OK" --timeout 60
deepseekbot send <bot-id> --body-stdin --message-id human-<UUID>
deepseekbot send <bot-id> --body "就这条" --reply-to <消息-id>
deepseekbot skills-list --bot <bot-id> [--query <文本>]
deepseekbot send <bot-id> --body "/summarize-pr #42" --skill bot:<bot-id>/summarize-pr
deepseekbot send-status <bot-id> --message-id <回执中的消息-id>
deepseekbot channel-messages <channel-id> --host http://127.0.0.1:31917 --limit 20
deepseekbot tool-approval-status <channel-id> --message-id <卡片-id>
Expand All @@ -231,6 +233,8 @@ deepseekbot folder-browse [<文件夹路径>] [--offset <n>]
deepseekbot grant-create <bot-id> --workspace <workspace-id>
```

`skills-list` 返回该对话 `/` 菜单提供的 Skill,并按 `--query` 以与菜单相同的规则排序(名称前缀、名称子序列、描述):包括 Host Skill(`owner.kind` 为 `host`),以及各 PersonaBot Memory Repository 下 `.agents/skills` 或 `.dsh/skills` 中的 Project Skill(`owner.kind` 为 `bot`)。任意 Channel 可用 `--channel <channel-id>` 代替 `--bot`。`send --skill <owner>/<name>`(owner 为 `host` 或 `bot:<bot-id>`,可重复)按参数顺序,为每个参数把正文中第一个尚未选中的 `/name` token 变成 Skill Invocation(token 两端必须为空白或正文边界;没有未用匹配时拒绝发送):Host 把该 Skill 当前的文本随消息保存,Bot 以 `<skill_invocation>` 块读到它。Skill 不决定由谁回复;已不存在的 Skill 会以 `skill-unavailable` 拒绝;Skill 文本超过 64,000 个字符时会以 `skill-too-large` 拒绝,不会截断。见 [ADR-0346](adr/0346-skill-invocation-is-snapshotted-text-that-addresses-no-one.md)。

`tool-approval-status` 返回 `status`;卡片仍为 `pending` 时还返回 `expiresAt`,即网页卡片倒计时的截止时间。`--outcome allowed-always-kind` 对应 `secret_cli` 卡片上的“总是允许此类”:批准这一次,并为同一程序、账号和环境变量名保存一条规则;`tool-approval-rules` 会以 `secret-cli` 类型列出它。其他卡片会拒绝这个选项。

`user-question-answer` 接受与网页卡片相同的已提供选项。持久化限时提问可在 Host 重启后回答,
Expand Down
11 changes: 10 additions & 1 deletion packages/client/src/client/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ import type {
import type { ShareDraftView } from '../../../core/src/marketplace/share-draft.js';
import type { MarketplaceLanguage } from '../../../core/src/marketplace/language.js';
import type { InstallReviewResult } from '../../../core/src/marketplace/install-review.js';
import { loadAllBotPreview } from './bridge.js';
import { loadAllBotPreview, loadChannelSkillCatalog } from './bridge.js';
import { loadComposerIdentity, validateComposerDraft } from './bridge.js';
import type { AllBotPreview, AllBotMention } from '../../../core/src/channels/all-bot-mention.js';
import type { ChannelSkillInvocationToken } from '../../../core/src/channels/channel.js';
import type { SkillCatalogEntry } from '../../../core/src/skills/entries.js';

import type {
ChannelBridgeInput,
Expand Down Expand Up @@ -1031,7 +1033,9 @@ export interface BridgeActions {
grantRequestResolution?: ChannelMessage['grantRequestResolution'],
allBotMention?: AllBotMention,
submissionId?: string,
skillInvocations?: ChannelSkillInvocationToken[],
): Promise<boolean>;
skillCatalog?(channelId: string): Promise<SkillCatalogEntry[]>;
submissions: {
subscribe(listener: () => void): () => void;
snapshot(channelId: string): SubmissionSnapshot | undefined;
Expand Down Expand Up @@ -3313,6 +3317,7 @@ export function createActions(
await loadSessionsFor(slug, selection);
},
allBotPreview: (channelId) => loadAllBotPreview(call, channelId),
skillCatalog: (channelId) => loadChannelSkillCatalog(call, channelId),
validateComposerDraft: (channelId, attachments, replyTo) =>
validateComposerDraft(call, channelId, attachments, replyTo),
async send(
Expand All @@ -3325,6 +3330,7 @@ export function createActions(
grantRequestResolution,
allBotMention,
submissionId,
skillInvocations,
) {
let snapshot = clientStore.getSnapshot();
const channel = snapshot.conversation.channel;
Expand All @@ -3342,6 +3348,7 @@ export function createActions(
!replyTo &&
!mentions?.length &&
!channelRefs?.length &&
!skillInvocations?.length &&
!memorySwitchTarget &&
!grantRequestResolution
) {
Expand Down Expand Up @@ -3428,6 +3435,8 @@ export function createActions(
grantRequestResolution,
undefined,
allBotMention,
undefined,
skillInvocations,
);
remainingFailures(channel.id, [message]);
settleSubmission(channel.id, localId, 'sent');
Expand Down
Loading
Loading