diff --git a/.agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml b/.agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml index 6bb4aac193..a98beff699 100644 --- a/.agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-05-skill-system.md -2026-07-05-skill-system.md: dd2fb1d22949f55ea7cb2c9f280e7cfbfcbbb226 -2026-07-05-skill-system.zh.md: 96656a8e1dfc2ae1ce7301ba29e7739349b6aab6 +2026-07-05-skill-system.md: a998d70ec934aed4bf7ce32aa711abd47b508a1d +2026-07-05-skill-system.zh.md: 4fa7c4fd657c2f41f16b30679ec95e61a75f8a0c diff --git a/.agents/notes/implemented/feature/2026-07-05-skill-system.md b/.agents/notes/implemented/feature/2026-07-05-skill-system.md index dd2fb1d229..a998d70ec9 100644 --- a/.agents/notes/implemented/feature/2026-07-05-skill-system.md +++ b/.agents/notes/implemented/feature/2026-07-05-skill-system.md @@ -14,9 +14,11 @@ DeepSeek Harness uses the same primitive so project-specific review, plugin-auth `@deepseek-ai/dsh-skill` is the pure provider registry (`ctx.skills`), `@deepseek-ai/dsh-skill-local` is the shipped local filesystem provider, and `@deepseek-ai/dsh-tool-skill` owns the durable session catalog and model-facing loader tool. `dsh-agent-spine-demo` loads the registry, local provider, and consumer by default so TUI, headless, and ACP apps get the same behavior while embedded or remote providers contribute skills without changing the registry or consumer. Its `skills` config forwards `registry`, `local`, and `tool` branches to those owners. +Dedicated packaged providers can contribute immutable skills without filesystem discovery. The shipped CLI declares `@deepseek-ai/dsh-skill-badge` disabled by default; enabling its composition row contributes the official badge instructions through the same registry and consumer ([decision](2026-08-06-bundled-dsh-badge-skill.md)). + Provider plugins register synchronously during `apply()`. Provider membership is direct effect-owned state: registration and disposal invalidate completed catalogs synchronously, and discovery reads the current provider map on demand rather than observing registry-change events. Provider catalogs return ranked candidates from awaited `list()` calls, where remote providers perform initialization, authentication, and discovery while honoring the lookup abort signal. The registry validates each candidate, resolves same-name skills first-wins by rank, provider registration order, and provider-local order, then sorts summaries by skill name for deterministic consumers. It caches only completed catalog snapshots and retries when a provider/runtime revision changes during discovery, so an unload cannot freeze a stale, unresolvable skill into a session catalog. Runtime `ctx.skills.register(...)` remains a convenience for embedded in-process skills and uses project-over-user priority; `runtime` is reserved as the registry-owned provider name. -The local provider scans cwd-sensitive project roots, custom roots, and user roots in first-wins rank order: project `.dsh`, project `.agents`, `customSkillDirs`, user `.dsh`, then user `.agents`. The user `.dsh/skills` scan skips `.system` so a system-owned directory is not treated as normal user content. DeepSeek Harness does not ship built-in system skills; embedded or remote providers supply additional skills when configured. +The local provider scans cwd-sensitive project roots, custom roots, and user roots in first-wins rank order: project `.dsh`, project `.agents`, `customSkillDirs`, user `.dsh`, then user `.agents`. The user `.dsh/skills` scan skips `.system` so a system-owned directory is not treated as normal user content. The local provider does not synthesize built-in system skills; configured bundled roots and dedicated providers supply additional skills. Each skill is either `/SKILL.md` or `.md` with YAML frontmatter. `name` and `description` are required; `whenToUse`, `metadata`, `disable-model-invocation`, and `user-invocable` are optional. Names are kebab-case. The invocation fields project into a typed nested policy as defined by the [independent model and user invocation decision](2026-07-28-skill-invocation-policy.md); the parser rejects the old camel-case spellings. YAML frontmatter is parsed with the `yaml` package instead of `js-yaml` or a hand-written parser: `yaml` is the already-declared modern parser for this package's limited frontmatter needs, and a narrow parser would either reject valid YAML users expect to work or grow into an unreviewed YAML subset. @@ -48,7 +50,7 @@ The data structures and catalog/tool contract are documented in [skills.md](../. The agent-core spine includes one catalog contributor, one local provider, and one model-facing tool. Skill discovery is cwd-sensitive, so callers that create agents with different session cwd values can observe different project skill overrides by design. -The catalog is deterministic for a fixed root set and runtime registration revision, but disk changes are not watched; discovery is memoized until runtime registration invalidates the cache or the process restarts. +The catalog is deterministic for a fixed root set and runtime registration revision. The local provider watches configured roots and invalidates completed catalogs after relevant disk changes; runtime registration and provider disposal also invalidate them. ## Deferred diff --git a/.agents/notes/implemented/feature/2026-07-05-skill-system.zh.md b/.agents/notes/implemented/feature/2026-07-05-skill-system.zh.md index 96656a8e1d..4fa7c4fd65 100644 --- a/.agents/notes/implemented/feature/2026-07-05-skill-system.zh.md +++ b/.agents/notes/implemented/feature/2026-07-05-skill-system.zh.md @@ -14,9 +14,11 @@ DeepSeek Harness 使用同一原语,使项目特定的评审、插件编写和 `@deepseek-ai/dsh-skill` 是纯提供方注册表(`ctx.skills`),`@deepseek-ai/dsh-skill-local` 是随附的本地文件系统提供方,`@deepseek-ai/dsh-tool-skill` 负责持久化会话目录与面向模型的 loader 工具。`dsh-agent-spine-demo` 默认加载注册表、本地提供方和消费方,使 TUI、headless 与 ACP(Agent Client Protocol)应用获得相同行为,同时嵌入式或远程提供方可在不修改注册表或消费方的前提下贡献 skill。其 `skills` 配置将 `registry`、`local` 和 `tool` 分支分别转发给对应的所有者。 +专用的随包提供方可以贡献不可变的 skill,无需文件系统发现。交付的 CLI(命令行界面)默认将 `@deepseek-ai/dsh-skill-badge` 声明为禁用;启用其组合配置行,就会通过同一个注册表和消费方贡献官方徽章指令(见[决策](2026-08-06-bundled-dsh-badge-skill.md))。 + 提供方插件在 `apply()` 期间同步注册。提供方成员资格是由直接 effect 持有的状态:注册与 dispose(资源释放)同步地使已完成的目录失效,发现操作按需读取当前提供方映射而非监听注册表变更事件。提供方目录从等待的 `list()` 调用返回排序后的候选项,远程提供方在此过程中执行初始化、认证和发现,同时遵守查找的 abort 信号。注册表校验每个候选项,按排名、提供方注册顺序和提供方内部顺序以先到先得方式解决同名 skill 冲突,然后按 skill 名称排序摘要以保证消费方获得确定性结果。它仅缓存已完成的目录快照,并在发现过程中提供方/运行时修订版本发生变化时重试,因此卸载操作不会将一个陈旧且不可解析的 skill 冻结到会话目录中。运行时 `ctx.skills.register(...)` 仍作为嵌入式进程内 skill 的便捷方式保留,使用 project 优先于 user 的优先级;`runtime` 保留为注册表拥有的提供方名称。 -本地提供方按先到先得的排名顺序扫描 cwd 敏感的项目根目录、自定义根目录和用户根目录:项目 `.dsh`、项目 `.agents`、`customSkillDirs`、用户 `.dsh`,然后是用户 `.agents`。用户 `.dsh/skills` 扫描跳过 `.system`,以免系统拥有的目录被当作普通用户内容处理。DeepSeek Harness 不随附内置系统 skill;嵌入式或远程提供方在配置后提供额外 skill。 +本地提供方按先到先得的排名顺序扫描 cwd 敏感的项目根目录、自定义根目录和用户根目录:项目 `.dsh`、项目 `.agents`、`customSkillDirs`、用户 `.dsh`,然后是用户 `.agents`。用户 `.dsh/skills` 扫描跳过 `.system`,以免系统拥有的目录被当作普通用户内容处理。本地提供方不会合成内置系统 skill;已配置的 bundled 根目录和专用提供方会提供额外 skill。 每个 skill 是 `/SKILL.md` 或带 YAML frontmatter 的 `.md`。`name` 和 `description` 为必填;`whenToUse`、`metadata`、`disable-model-invocation` 和 `user-invocable` 为可选。名称采用 kebab-case。调用字段会投影到类型化的嵌套策略中,具体由[模型与用户独立调用决策](2026-07-28-skill-invocation-policy.md)定义;解析器会拒绝旧的驼峰拼写。YAML frontmatter 使用 `yaml` 包(package)解析,而非 `js-yaml` 或手写解析器:`yaml` 是本包有限 frontmatter 需求已声明的现代解析器,窄解析器要么拒绝用户预期可用的合法 YAML,要么膨胀为一个未经评审的 YAML 子集。 @@ -48,7 +50,7 @@ DeepSeek Harness 使用同一原语,使项目特定的评审、插件编写和 agent-core 主干包含一个目录贡献者、一个本地提供方和一个面向模型的工具。Skill 发现是 cwd 敏感的,因此以不同会话 cwd 值创建 agent 的调用方可以按设计观察到不同的项目 skill 覆盖。 -目录对于固定的根目录集合和运行时注册修订版本是确定性的,但不监视磁盘变化;发现结果被缓存,直到运行时注册使缓存失效或进程重启。 +目录对于固定的根目录集合和运行时注册修订版本是确定性的。本地提供方会监视已配置的根目录,并在发生相关磁盘变化后使已完成的目录失效;运行时注册和提供方释放也会使其失效。 ## 延后 diff --git a/.agents/notes/implemented/feature/2026-08-06-bundled-dsh-badge-skill.i18n.yaml b/.agents/notes/implemented/feature/2026-08-06-bundled-dsh-badge-skill.i18n.yaml new file mode 100644 index 0000000000..222ed1ebbe --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-06-bundled-dsh-badge-skill.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-06-bundled-dsh-badge-skill.md +2026-08-06-bundled-dsh-badge-skill.md: 512f67ca347ca311a1f80fef932f6af8c91b0fe9 +2026-08-06-bundled-dsh-badge-skill.zh.md: 88fcadf66944cb91419441be3916ae04968be663 diff --git a/.agents/notes/implemented/feature/2026-08-06-bundled-dsh-badge-skill.md b/.agents/notes/implemented/feature/2026-08-06-bundled-dsh-badge-skill.md new file mode 100644 index 0000000000..512f67ca34 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-06-bundled-dsh-badge-skill.md @@ -0,0 +1,25 @@ +# Agent Note: Bundled dsh badge skill + +Status: implemented + +English | [中文](2026-08-06-bundled-dsh-badge-skill.zh.md) + +## Problem + +The [Cordis tutorial](../../../../docs/cordis-tutorial/index.md) uses an official “powered by dsh” badge across its pages, but the shipped CLI has no reusable instructions or explicit opt-in provider for applying the same attribution elsewhere. + +## Decision + +`@deepseek-ai/dsh-skill-badge` is a native Cordis plugin that registers one immutable bundled provider on `ctx.skills`. The provider owns the `dsh-badge` summary, instruction body, and PNG resource base; `dsh-tool-skill` remains the sole owner of model-facing catalog and loader rendering. + +The shipped CLI composition declares `skill-badge` as disabled. Enabling that existing row is the explicit opt-in; disabled installations advertise no badge skill and gain no model-visible content. + +The provider uses the bundled rank after project, custom, and user filesystem sources, so a user-owned `dsh-badge` definition can override it through the ordinary registry precedence contract. Provider disposal removes the contribution through the registry-owned effect. + +## Alternatives considered + +**Mount packaged files through `dsh-skill-local`.** Rejected because filesystem discovery, parsing, and watching add lifecycle machinery that an immutable single-skill provider does not need. + +## Consequences + +The badge instructions and source PNG are versioned with DSH and resolve through a packaged directory resource base. The provider has no configuration surface. Package tests pin provider lifecycle and the official PNG bytes, while a keyless assembled-application snapshot pins the enabled catalog and loaded skill body. diff --git a/.agents/notes/implemented/feature/2026-08-06-bundled-dsh-badge-skill.zh.md b/.agents/notes/implemented/feature/2026-08-06-bundled-dsh-badge-skill.zh.md new file mode 100644 index 0000000000..88fcadf669 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-06-bundled-dsh-badge-skill.zh.md @@ -0,0 +1,25 @@ +# Agent Note: 内置 dsh 徽章 skill + +Status: implemented + +[English](2026-08-06-bundled-dsh-badge-skill.md) | 中文 + +## 问题 + +[Cordis 教程](../../../../docs/cordis-tutorial/index.md)的各个页面都使用官方「powered by dsh」徽章,但交付的 CLI(命令行界面)既没有用于在其他位置应用同样署名的可复用指令,也没有可显式选择加入的提供方。 + +## 决策 + +`@deepseek-ai/dsh-skill-badge` 是一个原生 Cordis 插件,会在 `ctx.skills` 上注册一个不可变的内置提供方。该提供方负责 `dsh-badge` 的摘要、指令正文和 PNG 资源基底;`dsh-tool-skill` 仍是面向模型的目录与 loader 渲染的唯一归属方。 + +交付的 CLI 组合将 `skill-badge` 声明为禁用。启用这个现有配置行就是显式选择加入;禁用它的安装实例不会公开任何徽章 skill(技能),也不会获得任何模型可见内容。 + +该提供方使用排在项目、自定义及用户文件系统来源之后的内置 rank,因此用户自有的 `dsh-badge` 定义可通过注册表的常规优先级契约覆盖它。提供方释放时,注册表拥有的 effect 会移除该贡献。 + +## 曾考虑的替代方案 + +**通过 `dsh-skill-local` 挂载随包文件。** 否决,因为文件系统发现、解析和监视会引入生命周期机制,而不可变的单一 skill 提供方并不需要这些机制。 + +## 后果 + +徽章指令和源 PNG 随 DSH 一同纳入版本管理,并通过以随包目录为基础的资源基底解析。该提供方没有配置面。包测试固定提供方生命周期和官方 PNG 的字节内容;无密钥的组装应用快照则固定启用后的目录和已加载的 skill 正文。 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index 45dc52561a..8bebf18040 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -80,6 +80,8 @@ flowchart LR cfg --> plugin_dsh_base_skill plugin_dsh_base_skill_local["skill-local
@deepseek-ai/dsh-skill-local"] cfg --> plugin_dsh_base_skill_local + plugin_dsh_base_skill_badge["skill-badge
@deepseek-ai/dsh-skill-badge"] + cfg --> plugin_dsh_base_skill_badge plugin_dsh_base_tool_skill["tool-skill
@deepseek-ai/dsh-tool-skill"] cfg --> plugin_dsh_base_tool_skill plugin_dsh_base_commands["commands
@deepseek-ai/dsh-commands"] @@ -196,6 +198,7 @@ flowchart LR | `workspace-context` | `@deepseek-ai/dsh-workspace-context` | | `skill` | `@deepseek-ai/dsh-skill` | | `skill-local` | `@deepseek-ai/dsh-skill-local` | +| `skill-badge` | `@deepseek-ai/dsh-skill-badge` | | `tool-skill` | `@deepseek-ai/dsh-tool-skill` | | `commands` | `@deepseek-ai/dsh-commands` | | `command-feedback` | `@deepseek-ai/dsh-command-feedback` | diff --git a/apps/cli/package.json b/apps/cli/package.json index 71b70b078d..a648901cb9 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -36,11 +36,14 @@ "node-addon-require-builtin": "^0.1.4" }, "devDependencies": { + "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-frontend-static": "workspace:^", "@deepseek-ai/dsh-host-apiproxy": "workspace:^", "@deepseek-ai/dsh-host-webserver": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-mock-server": "workspace:^", "@deepseek-ai/dsh-loader-smoke": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "@types/js-yaml": "^4.0.9", diff --git a/apps/cli/tests/dsh-badge.snapshot.ts b/apps/cli/tests/dsh-badge.snapshot.ts new file mode 100644 index 0000000000..00d5278f82 --- /dev/null +++ b/apps/cli/tests/dsh-badge.snapshot.ts @@ -0,0 +1,176 @@ +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' +const binScript = fileURLToPath(new URL('./fixtures/dsh-badge/snapshot.ts', import.meta.url)) +const configPath = fileURLToPath(new URL('./fixtures/dsh-badge/cordis.yml', import.meta.url)) +const defaultConfigPath = fileURLToPath(new URL('./fixtures/dsh-badge/default.cordis.yml', import.meta.url)) +const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) +const badgeAssetsPath = fileURLToPath(new URL('../../../packages/skill/skill-badge/assets/', import.meta.url)) + +describe('dsh badge assembled snapshot', () => { + it('advertises and loads the opt-in bundled skill through the shipped app', async () => { + const disabled = await runLoaderSmoke({ + label: 'disabled dsh badge skill snapshot', + tempDirPrefix: 'headless-snapshot-dsh-badge-disabled-', + binScript, + libBinScript: binScript, + configPath: defaultConfigPath, + tsconfigPath, + }) + const enabled = await runLoaderSmoke({ + label: 'dsh badge skill snapshot', + tempDirPrefix: 'headless-snapshot-dsh-badge-', + binScript, + libBinScript: binScript, + configPath, + tsconfigPath, + }) + const disabledSnapshot = JSON.parse(disabled.stdout) as unknown + const enabledSnapshot = JSON.parse( + enabled.stdout.replaceAll(badgeAssetsPath, '{{badgeAssetsPath}}'), + ) as unknown + + expect(disabled.stderr).toBe('') + expect(enabled.stderr).toBe('') + expect(disabledSnapshot).toMatchInlineSnapshot(` + { + "catalog": null, + "result": { + "content": [ + { + "text": "Error: skill "dsh-badge" is unknown or no longer available", + "type": "text", + }, + ], + "error": { + "message": "skill "dsh-badge" is unknown or no longer available", + }, + "isError": true, + }, + "summary": null, + } + `) + expect(enabledSnapshot).toMatchInlineSnapshot(` + { + "catalog": [ + { + "text": " + A skill is a reusable set of task-specific instructions. The following skills are available in this session: + + + - \`dsh-badge\`: Add the official “powered by dsh” badge to documents, pull requests, merge requests, and other content produced with DeepSeek Harness. Use whenever creating a pull request or merge request. Also use when the user asks for a dsh badge, powered-by-dsh attribution, or a reusable dsh badge asset or snippet. + + + If the user names a skill, or the task clearly matches a skill's description, call the \`skill\` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded. + A user may also invoke a skill directly; its block then appears in this conversation. Follow it, and do not call the \`skill\` tool again for that skill. + ", + "type": "text", + }, + ], + "result": { + "content": [ + { + "text": " + + Base directory for this skill: {{badgeAssetsPath}} + Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed. + + + + # dsh Badge + + Add the official “powered by dsh” badge without recreating or restyling it. + + ## Assets + + - Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20 + - Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\` + - Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\` + + ## Markdown + + Use this linked badge in Markdown: + + \`\`\`markdown + [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk) + \`\`\` + + If attribution should not be linked, use: + + \`\`\`markdown + ![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white) + \`\`\` + + ## Usage rules + + - For GitHub or GitLab Markdown, use the Shields.io URL and link it to the project URL unless the user asks for an unlinked image. + - For Feishu and other systems that import remote images unreliably, upload \`dsh-badge.png\` from this skill directory instead of generating another badge. + - Preserve the badge's 121×20 dimensions and aspect ratio. + - Place the badge at the end of the attributed document or section unless the user specifies another position. + - Do not substitute another color, logo, label, or project URL. + + + ", + "type": "text", + }, + ], + "isError": false, + "value": { + "content": "# dsh Badge + + Add the official “powered by dsh” badge without recreating or restyling it. + + ## Assets + + - Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20 + - Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\` + - Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\` + + ## Markdown + + Use this linked badge in Markdown: + + \`\`\`markdown + [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk) + \`\`\` + + If attribution should not be linked, use: + + \`\`\`markdown + ![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white) + \`\`\` + + ## Usage rules + + - For GitHub or GitLab Markdown, use the Shields.io URL and link it to the project URL unless the user asks for an unlinked image. + - For Feishu and other systems that import remote images unreliably, upload \`dsh-badge.png\` from this skill directory instead of generating another badge. + - Preserve the badge's 121×20 dimensions and aspect ratio. + - Place the badge at the end of the attributed document or section unless the user specifies another position. + - Do not substitute another color, logo, label, or project URL. + ", + "name": "dsh-badge", + "provider": "dsh-badge", + "resourceBase": { + "kind": "directory", + "path": "{{badgeAssetsPath}}", + }, + }, + }, + "summary": { + "description": "Add the official “powered by dsh” badge to documents, pull requests, merge requests, and other content produced with DeepSeek Harness. Use whenever creating a pull request or merge request. Also use when the user asks for a dsh badge, powered-by-dsh attribution, or a reusable dsh badge asset or snippet.", + "invocation": { + "modelInvocable": true, + "userInvocable": true, + }, + "name": "dsh-badge", + "provider": "dsh-badge", + "resourceBase": { + "kind": "directory", + "path": "{{badgeAssetsPath}}", + }, + "source": "bundled", + }, + } + `) + }, LOADER_SMOKE_TEST_TIMEOUT_MS * 2) +}) diff --git a/apps/cli/tests/fixtures/dsh-badge/cordis.yml b/apps/cli/tests/fixtures/dsh-badge/cordis.yml new file mode 100644 index 0000000000..b3bfdb1b04 --- /dev/null +++ b/apps/cli/tests/fixtures/dsh-badge/cordis.yml @@ -0,0 +1,9 @@ +- id: skill-badge + disabled: false + +- id: skill-local + config: + watch: false + +- id: telemetry-otel + disabled: true diff --git a/apps/cli/tests/fixtures/dsh-badge/default.cordis.yml b/apps/cli/tests/fixtures/dsh-badge/default.cordis.yml new file mode 100644 index 0000000000..ac3e48441a --- /dev/null +++ b/apps/cli/tests/fixtures/dsh-badge/default.cordis.yml @@ -0,0 +1,6 @@ +- id: skill-local + config: + watch: false + +- id: telemetry-otel + disabled: true diff --git a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts new file mode 100644 index 0000000000..8d6019ab53 --- /dev/null +++ b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts @@ -0,0 +1,56 @@ +import { fileURLToPath } from 'node:url' +import { Context } from 'cordis' +import { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent' +import { CallId } from '@deepseek-ai/dsh-llm' +import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot' +import { SessionId } from '@deepseek-ai/dsh-session' +import type {} from '@deepseek-ai/dsh-skill' +import type {} from '@deepseek-ai/dsh-tools' + +const overlayPath = process.argv[2] +if (overlayPath === undefined) throw new Error('dsh-badge snapshot requires an overlay path') +const rootConfigPath = fileURLToPath(new URL('../../../../../packages/bundle/base/tests/fixtures/root.cordis.yml', import.meta.url)) +const basePatchPath = fileURLToPath(new URL('../../../../../packages/bundle/base/cordis.patch.yml', import.meta.url)) +const ctx = await boot('dsh-badge-snapshot', rootConfigPath, [ + ...loadOverlayPatches('dsh-badge-snapshot', basePatchPath), + ...loadOverlayPatches('dsh-badge-snapshot', overlayPath), +]) + +try { + const agentId = SessionId('dsh-badge-snapshot') + const session = ctx.sessions.create(agentId, { meta: { cwd: process.cwd() } }) + const agent: Agent = { + ctx: new Context(), + id: agentId, + options: {}, + session, + inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }), + status: 'idle', + send: () => {}, + followup: () => {}, + steer: () => {}, + inject: () => { throw new Error('dsh-badge snapshot must receive the catalog at the step boundary') }, + cancel: () => {}, + runMaintenance: task => task(new AbortController().signal), + whenIdle: () => Promise.resolve(), + } + const decision = await agentEvents(ctx, agent).waterfall( + 'agent/pre-step', + { messages: [], turn: 1, step: 1, signal: new AbortController().signal }, + () => Promise.resolve({ kind: 'enter' as const, messages: [] }), + ) + const catalog = decision.kind === 'enter' + ? decision.messages.find(message => message.role === 'user' + && message.source.kind === 'skill-catalog')?.content + : undefined + const summary = (await ctx.skills.list()).find(skill => skill.name === 'dsh-badge') + const result = await ctx.tools.execute({ + callId: CallId('dsh-badge-snapshot'), + name: 'skill', + arguments: { name: 'dsh-badge' }, + signal: new AbortController().signal, + }) + process.stdout.write(`${JSON.stringify({ catalog: catalog ?? null, summary: summary ?? null, result })}\n`) +} finally { + await ctx.fiber.dispose() +} diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 778d74d406..1aee225b5d 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -90,6 +90,7 @@ flowchart LR svc_sessionProjectionCache["ctx.sessionProjectionCache
Persisted projection cache"] pkg_skill["skill"] svc_skills["ctx.skills
Skill provider registry"] + pkg_skill_badge["skill-badge"] pkg_skill_local["skill-local"] svc_agents["ctx.agents
Agent service"] pkg_acp["acp"] @@ -224,6 +225,7 @@ flowchart LR pkg_settings --> svc_settings pkg_settings_local --> svc_settings pkg_skill --> svc_skills + pkg_skill_badge --> svc_skills pkg_skill_local --> svc_skills pkg_spill --> svc_spillStore pkg_spill_local --> svc_spillStore @@ -383,7 +385,7 @@ flowchart LR | `ctx.commands` | `core` | [`commands`](../packages/ui/commands) | - | - | - | Plugins register direct human commands without sending invocations to the model. | | `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session-projection/session-projection) | - | [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session-title/session-title), [`host-apiproxy`](../packages/host/apiproxy) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values. | | `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session-projection/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. | -| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. | +| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. | | `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. | | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. | | `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 649f425c36..5e858de9cc 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1491,7 +1491,7 @@ export interface Config { } ``` -Source: [`packages/skill/skill/src/index.ts:262`](../packages/skill/skill/src/index.ts) +Source: [`packages/skill/skill/src/index.ts:265`](../packages/skill/skill/src/index.ts) ## `@deepseek-ai/dsh-skill-local` @@ -2578,6 +2578,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-session` ([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts)) - `@deepseek-ai/dsh-session-checkpoint-policy` — requires `llm` · `sessionPersistence` · `sessions` · `tools` ([`packages/session-persistence/session-checkpoint-policy/src/index.ts`](../packages/session-persistence/session-checkpoint-policy/src/index.ts)) - `@deepseek-ai/dsh-session-projection` ([`packages/session-projection/session-projection/src/index.ts`](../packages/session-projection/session-projection/src/index.ts)) +- `@deepseek-ai/dsh-skill-badge` — requires `skills` ([`packages/skill/skill-badge/src/index.ts`](../packages/skill/skill-badge/src/index.ts)) - `@deepseek-ai/dsh-storage` ([`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts)) - `@deepseek-ai/dsh-subagent` ([`packages/subagent/subagent/src/index.ts`](../packages/subagent/subagent/src/index.ts)) - `@deepseek-ai/dsh-subprocess-local` ([`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts)) diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index 5a43d60d96..92c2fade12 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -677,7 +677,7 @@ A skill provider, runtime contribution, or provider-backed catalog may have chan 'skills/change'(): void ``` -Source: [`packages/skill/skill/src/index.ts:280`](../../packages/skill/skill/src/index.ts) +Source: [`packages/skill/skill/src/index.ts:283`](../../packages/skill/skill/src/index.ts) ## `subagent/*` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index c44efaafa4..527d10b9d8 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -1946,7 +1946,7 @@ async get(name: string, options: SkillLookupOptions = {}): Promise/skills` | | 600 | `bundled` | `Config.bundledSkillDir` when configured | -The project root is the nearest ancestor containing `.git`; without one, the current cwd is used. When `ctx.fs` is available, the git-root walk probes `.git` through the filesystem service so remote or sandboxed workspaces do not fall back to the host filesystem boundary. The user DSH root skips its `.system` child. The local provider does not ship built-in system skills; deployments supply built-ins through another provider. +The project root is the nearest ancestor containing `.git`; without one, the current cwd is used. When `ctx.fs` is available, the git-root walk probes `.git` through the filesystem service so remote or sandboxed workspaces do not fall back to the host filesystem boundary. The user DSH root skips its `.system` child. The local provider does not synthesize built-in system skills; deployments supply packaged skills through configured bundled roots or dedicated providers. + +`dsh-skill-badge` registers one immutable `bundled` candidate at `BUNDLED_SKILL_RANK` and exposes its packaged asset directory through `resourceBase`. The shipped CLI declares the plugin disabled, so enabling its composition row is an explicit opt-in. Chokidar watches existing roots for direct bundle/flat-entry additions and removals plus direct skill-entry changes. A missing root is followed one absent path segment at a time from its nearest existing ancestor until Chokidar can attach. Resource files below a bundle are not catalog changes. Model-facing `write` and `edit` observations synchronously invalidate the provider when their target is catalog-relevant, while the host watcher covers IDE, Git, shell, and external-process mutations. Watcher failures make the current observation incomplete without hiding readable candidates from direct loads; project-scoped watchers use a configured bounded LRU. diff --git a/docs/core-data-structures/skills.zh.md b/docs/core-data-structures/skills.zh.md index 7d69b84f7b..3f8c034ec2 100644 --- a/docs/core-data-structures/skills.zh.md +++ b/docs/core-data-structures/skills.zh.md @@ -2,9 +2,9 @@ [English](skills.md) | 中文 -[skill(技能)能力族](../../packages/skill) 拆分为三个包:注册表([dsh-skill](../../packages/skill/skill),`ctx.skills`)合并各提供方的目录;本地提供方([dsh-skill-local](../../packages/skill/skill-local))扫描并监视项目、自定义和用户目录;消费方([dsh-tool-skill](../../packages/skill/tool-skill))拥有初始目录和替换目录,以及面向模型的 `skill` 工具。skill 是可选的指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.md)。 +[skill(技能)能力族](../../packages/skill) 包含注册表([dsh-skill](../../packages/skill/skill),`ctx.skills`)、本地提供方([dsh-skill-local](../../packages/skill/skill-local))、可选的随包徽章提供方([dsh-skill-badge](../../packages/skill/skill-badge))和消费方([dsh-tool-skill](../../packages/skill/tool-skill))。注册表合并各提供方的目录;提供方贡献本地或随包 skill;消费方拥有初始目录和替换目录,以及面向模型的 `skill` 工具。skill 是可选的指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.md)。 -源码:[`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts)、[`packages/skill/skill-local/src/index.ts`](../../packages/skill/skill-local/src/index.ts) 与 [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts)。 +源码:[`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts)、[`packages/skill/skill-local/src/index.ts`](../../packages/skill/skill-local/src/index.ts)、[`packages/skill/skill-badge/src/index.ts`](../../packages/skill/skill-badge/src/index.ts) 与 [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts)。 ## 提供方注册表 @@ -72,7 +72,9 @@ interface SkillProviderControl { | 500 | `user-agents` | `/skills` | | 600 | `bundled` | 配置了 `Config.bundledSkillDir` 时使用该目录 | -项目根目录为包含 `.git` 的最近祖先目录;找不到时使用当前 cwd。当 `ctx.fs` 可用时,git-root 向上查找通过文件系统服务探测 `.git`,使远程或沙箱工作区不会回退到宿主文件系统边界。用户 DSH 根目录会跳过其 `.system` 子目录。本地提供方不附带内置系统 skill;部署方通过另一个提供方提供内置 skill。 +项目根目录为包含 `.git` 的最近祖先目录;找不到时使用当前 cwd。当 `ctx.fs` 可用时,git-root 向上查找通过文件系统服务探测 `.git`,使远程或沙箱工作区不会回退到宿主文件系统边界。用户 DSH 根目录会跳过其 `.system` 子目录。本地提供方不会合成内置系统 skill;部署方通过已配置的 bundled 根目录或专用提供方提供随包 skill。 + +`dsh-skill-badge` 在 `BUNDLED_SKILL_RANK` 注册一个不可变的 `bundled` 候选项,并通过 `resourceBase` 公开其随包资产目录。交付的 CLI(命令行界面)将该插件声明为禁用,因此启用其组合配置行即为显式选择加入。 Chokidar 会监视现有根目录中直属 bundle 和平铺条目的添加与移除,以及直属 skill 条目的变更。缺失的根目录会从最近的现有祖先开始,逐个跟踪缺失路径段,直至 Chokidar 可以附加。bundle 下的资源文件变更不属于目录变更。面向模型的 `write` 和 `edit` 观测会在目标路径相关时同步使提供方目录失效,而宿主 watcher 覆盖 IDE、Git、shell 和外部进程产生的变更。watcher 失败会使当前观测不完整,但不会在直接加载时隐藏可读候选项;项目作用域 watcher 使用按配置设限的 LRU。 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 7cb5639533..5c60be95e1 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -36,7 +36,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` | | `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | -| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:280`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | +| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:283`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:160`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc), [`subagent`](../packages/subagent/subagent) | | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:134`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:140`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/module-graph.md b/docs/module-graph.md index c268ad317f..2d5300f312 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -57,6 +57,7 @@ flowchart TD end subgraph group_skill["packages/skill"] pkg_skill["skill"] + pkg_skill_badge["skill-badge"] pkg_skill_local["skill-local"] pkg_tool_skill["tool-skill"] end @@ -427,6 +428,8 @@ flowchart TD pkg_fs --> pkg_invariants pkg_fs --> pkg_llm pkg_fs --> pkg_sandbox + pkg_skill_badge --> pkg_invariants + pkg_skill_badge --> pkg_skill pkg_compact --> pkg_invariants pkg_compact --> pkg_llm pkg_compact --> pkg_session @@ -1211,6 +1214,7 @@ flowchart TD | [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) | | [`bash`](../packages/bash/bash) | `bash` | [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`subprocess`](../packages/subprocess/subprocess) | | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | +| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/support/invariants), [`skill`](../packages/skill/skill) | | [`compact`](../packages/compact/compact) | `compact` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) | | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`web`](../packages/web/web) | diff --git a/knip.json b/knip.json index 779787ac2d..a38bd7cbaf 100644 --- a/knip.json +++ b/knip.json @@ -680,7 +680,8 @@ "apps/cli": { "entry": [ "tests/**/*.spec.ts", - "tests/**/*.e2e.ts" + "tests/**/*.e2e.ts", + "tests/**/*.snapshot.ts" ], "project": [ "src/**/*.ts", diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index 197d68e831..799affc16b 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -218,6 +218,10 @@ - id: skill-local name: '@deepseek-ai/dsh-skill-local' + - id: skill-badge + name: '@deepseek-ai/dsh-skill-badge' + disabled: true + - id: tool-skill name: '@deepseek-ai/dsh-tool-skill' diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 9895d8834c..507952b43a 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -70,6 +70,7 @@ "@deepseek-ai/dsh-session-title-first-message-llm": "workspace:^", "@deepseek-ai/dsh-settings-local": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", + "@deepseek-ai/dsh-skill-badge": "workspace:^", "@deepseek-ai/dsh-skill-local": "workspace:^", "@deepseek-ai/dsh-spill-local": "workspace:^", "@deepseek-ai/dsh-spill-policy": "workspace:^", diff --git a/packages/bundle/base/tests/fixtures/root.cordis.yml b/packages/bundle/base/tests/fixtures/root.cordis.yml new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/bundle/base/tests/fixtures/root.cordis.yml @@ -0,0 +1 @@ +[] diff --git a/packages/skill/README.i18n.yaml b/packages/skill/README.i18n.yaml index 2d424c61dd..74875f2aa3 100644 --- a/packages/skill/README.i18n.yaml +++ b/packages/skill/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/skill/README.md -README.md: d10049ac3e741350fddb42f430b70f063da1d12d -README.zh.md: 67f2da6f75edecd180ff861122c6392684ed9bdb +README.md: 533904859ad998de4f371a073fde98b68660097b +README.zh.md: 1fad581cc61a05251f671577dcb7edab37281283 diff --git a/packages/skill/README.md b/packages/skill/README.md index d10049ac3e..533904859a 100644 --- a/packages/skill/README.md +++ b/packages/skill/README.md @@ -7,6 +7,7 @@ This family discovers reusable agent instructions and exposes them to the model | Package | Role | ctx key | |---|---|---| | [`skill/`](skill/README.md) | Defines skill provider registration and lookup | `ctx.skills` | +| [`skill-badge/`](skill-badge/README.md) | Contributes the optional bundled dsh badge skill | registers on `ctx.skills` | | [`skill-local/`](skill-local/README.md) | Discovers skills from local filesystems | registers on `ctx.skills` | | [`tool-skill/`](tool-skill/README.md) | Publishes the skill catalog and model-facing loader | registers on `ctx.tools` | diff --git a/packages/skill/README.zh.md b/packages/skill/README.zh.md index 67f2da6f75..1fad581cc6 100644 --- a/packages/skill/README.zh.md +++ b/packages/skill/README.zh.md @@ -7,6 +7,7 @@ | 包 | 职责 | ctx 键 | |---|---|---| | [`skill/`](skill/README.md) | 定义 skill 提供方注册和查找 | `ctx.skills` | +| [`skill-badge/`](skill-badge/README.md) | 贡献可选的内置 dsh 徽章 skill | 注册到 `ctx.skills` | | [`skill-local/`](skill-local/README.md) | 从本地文件系统发现 skill | 注册到 `ctx.skills` | | [`tool-skill/`](tool-skill/README.md) | 发布 skill 目录和面向模型的 loader | 注册到 `ctx.tools` | diff --git a/packages/skill/skill-badge/README.i18n.yaml b/packages/skill/skill-badge/README.i18n.yaml new file mode 100644 index 0000000000..4dda53481c --- /dev/null +++ b/packages/skill/skill-badge/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/skill/skill-badge/README.md +README.md: 49b38023a7c110bb52bb351668905c702e251216 +README.zh.md: bf7eb0d7d4c0552c07f9cdf5665f8a20df829483 diff --git a/packages/skill/skill-badge/README.md b/packages/skill/skill-badge/README.md new file mode 100644 index 0000000000..49b38023a7 --- /dev/null +++ b/packages/skill/skill-badge/README.md @@ -0,0 +1,22 @@ +# @deepseek-ai/dsh-skill-badge + +English | [中文](README.zh.md) + +Optional bundled skill provider that contributes `dsh-badge` to `ctx.skills`. The skill supplies the official “powered by dsh” Markdown snippets and the packaged PNG for systems that cannot import a remote image reliably. + +Mount the plugin to enable the provider. It has no configuration. The shipped CLI composition includes the plugin as `disabled: true`; users must explicitly enable its `skill-badge` row before the skill enters a catalog. + +The provider exposes its packaged `assets/` directory as the skill resource base. `dsh-badge.png` is the 726×120 source asset, and consumers render it at 121×20. + +## Model Experience + +Indirectly, through `@deepseek-ai/dsh-tool-skill`, which renders the catalog entry and selected skill body. + +#### KV Cache effect + +Disabled by default, the plugin changes no request. When enabled, its catalog entry and any loaded body change the provider KV prefix at their insertion points. + +## Known Limitations and Deferred Work + +- The provider contributes one fixed skill and has no runtime customization. +- Remote Markdown uses Shields.io; use the packaged PNG when the target cannot fetch remote images reliably. diff --git a/packages/skill/skill-badge/README.zh.md b/packages/skill/skill-badge/README.zh.md new file mode 100644 index 0000000000..bf7eb0d7d4 --- /dev/null +++ b/packages/skill/skill-badge/README.zh.md @@ -0,0 +1,22 @@ +# @deepseek-ai/dsh-skill-badge + +[English](README.md) | 中文 + +可选的内置 skill(技能)提供方,向 `ctx.skills` 贡献 `dsh-badge`。该 skill 提供官方「powered by dsh」Markdown 片段和随包分发的 PNG,供无法可靠导入远程图片的系统使用。 + +挂载该插件即可启用提供方。它没有配置。交付的 CLI(命令行界面)组合以 `disabled: true` 包含该插件;用户必须显式启用其 `skill-badge` 配置行,该 skill 才会进入目录。 + +该提供方将随包分发的 `assets/` 目录作为 skill 资源基底公开。`dsh-badge.png` 是尺寸为 726×120 的源图资源,消费方以 121×20 的尺寸渲染。 + +## 模型体验 + +通过 `@deepseek-ai/dsh-tool-skill` 间接影响模型;该包会渲染目录条目和所选 skill 的正文。 + +#### KV Cache 影响 + +该插件默认禁用,不会改变任何请求。启用后,其目录条目和任何已加载正文都会在各自插入点改变提供方的 KV 前缀。 + +## 已知限制与暂缓事项 + +- 该提供方只贡献一个固定 skill,不提供运行时自定义。 +- 远程 Markdown 使用 Shields.io;当目标环境无法可靠获取远程图片时,请使用随包分发的 PNG。 diff --git a/packages/skill/skill-badge/assets/dsh-badge.md b/packages/skill/skill-badge/assets/dsh-badge.md new file mode 100644 index 0000000000..5a789fac2f --- /dev/null +++ b/packages/skill/skill-badge/assets/dsh-badge.md @@ -0,0 +1,31 @@ +# dsh Badge + +Add the official “powered by dsh” badge without recreating or restyling it. + +## Assets + +- Local PNG: [`dsh-badge.png`](dsh-badge.png), 726×120 source image; render at 121×20 +- Shields.io image URL: `https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white` +- Project URL: `https://github.com/deepseek-ai/deepseek-harness-sdk` + +## Markdown + +Use this linked badge in Markdown: + +```markdown +[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness-sdk) +``` + +If attribution should not be linked, use: + +```markdown +![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white) +``` + +## Usage rules + +- For GitHub or GitLab Markdown, use the Shields.io URL and link it to the project URL unless the user asks for an unlinked image. +- For Feishu and other systems that import remote images unreliably, upload `dsh-badge.png` from this skill directory instead of generating another badge. +- Preserve the badge's 121×20 dimensions and aspect ratio. +- Place the badge at the end of the attributed document or section unless the user specifies another position. +- Do not substitute another color, logo, label, or project URL. diff --git a/packages/skill/skill-badge/assets/dsh-badge.png b/packages/skill/skill-badge/assets/dsh-badge.png new file mode 100644 index 0000000000..bf91ecca97 Binary files /dev/null and b/packages/skill/skill-badge/assets/dsh-badge.png differ diff --git a/packages/skill/skill-badge/package.json b/packages/skill/skill-badge/package.json new file mode 100644 index 0000000000..b9dc53d9a5 --- /dev/null +++ b/packages/skill/skill-badge/package.json @@ -0,0 +1,37 @@ +{ + "name": "@deepseek-ai/dsh-skill-badge", + "description": "Bundled dsh badge skill provider for DeepSeek Harness", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/invariant.js", + "assets", + "lib/types/**/*.d.ts" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", + "@deepseek-ai/dsh-skill": "^0.0.1", + "cordis": "^4.0.0-rc.7" + }, + "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-skill": "workspace:^", + "cordis": "^4.0.0-rc.7" + } +} diff --git a/packages/skill/skill-badge/src/index.ts b/packages/skill/skill-badge/src/index.ts new file mode 100644 index 0000000000..9cff2070fb --- /dev/null +++ b/packages/skill/skill-badge/src/index.ts @@ -0,0 +1,60 @@ +/** + * Bundled `dsh-badge` skill provider. + * + * @module @deepseek-ai/dsh-skill-badge + */ + +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import type { Context } from 'cordis' +import { + BUNDLED_SKILL_RANK, + type SkillCandidate, + type SkillDefinition, + type SkillProvider, +} from '@deepseek-ai/dsh-skill' + +const PROVIDER_NAME = 'dsh-badge' +const SKILL_BODY_URL = new URL('../assets/dsh-badge.md', import.meta.url) +const RESOURCE_BASE = { + kind: 'directory', + path: fileURLToPath(new URL('../assets/', import.meta.url)), +} as const +const INVOCATION = { modelInvocable: true, userInvocable: true } as const +const DESCRIPTION = 'Add the official “powered by dsh” badge to documents, pull requests, merge requests, and other content produced with DeepSeek Harness. Use whenever creating a pull request or merge request. Also use when the user asks for a dsh badge, powered-by-dsh attribution, or a reusable dsh badge asset or snippet.' +const CANDIDATE: SkillCandidate = { + name: 'dsh-badge', + description: DESCRIPTION, + invocation: INVOCATION, + provider: PROVIDER_NAME, + source: 'bundled', + resourceBase: RESOURCE_BASE, + rank: BUNDLED_SKILL_RANK, + locator: SKILL_BODY_URL, +} + +const provider: SkillProvider = { + name: PROVIDER_NAME, + list: () => Promise.resolve([CANDIDATE]), + async get(_candidate): Promise { + return { + name: CANDIDATE.name, + description: CANDIDATE.description, + invocation: CANDIDATE.invocation, + provider: CANDIDATE.provider, + source: CANDIDATE.source, + resourceBase: RESOURCE_BASE, + content: await readFile(SKILL_BODY_URL, 'utf8'), + } + }, +} + +/** Cordis plugin name. */ +export const name = 'skill-badge' +/** Service required by the bundled provider. */ +export const inject = ['skills'] + +/** Register the bundled `dsh-badge` provider on `ctx.skills`. */ +export function apply(ctx: Context): void { + ctx.skills.registerProvider(() => provider) +} diff --git a/packages/skill/skill-badge/src/invariant.ts b/packages/skill/skill-badge/src/invariant.ts new file mode 100644 index 0000000000..c087d5917f --- /dev/null +++ b/packages/skill/skill-badge/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-skill-badge`. + * @module @deepseek-ai/dsh-skill-badge/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-skill-badge' + +/** Cordis companion plugin name. */ +export const name = 'skill-badge-invariant' +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] + +/** + * No runtime invariant: the package owns one immutable provider registration, + * while the skill registry owns registration uniqueness and lifecycle checks. + */ +const install: InvariantInstaller = () => {} + +/** + * Register this package's invariant companion. + * @param ctx - Cordis context carrying the invariant service. + * @returns the installed registration's disposer after setup succeeds. + */ +export const apply = (ctx: Context): Promise<() => void> => + Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/skill/skill-badge/tests/skill-badge.spec.ts b/packages/skill/skill-badge/tests/skill-badge.spec.ts new file mode 100644 index 0000000000..e4d62f1c89 --- /dev/null +++ b/packages/skill/skill-badge/tests/skill-badge.spec.ts @@ -0,0 +1,40 @@ +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { Context } from 'cordis' +import { describe, expect, it } from 'vitest' +import SkillService from '@deepseek-ai/dsh-skill' +import * as SkillBadge from '@deepseek-ai/dsh-skill-badge' + +describe('dsh-skill-badge', () => { + it('registers and disposes the bundled badge skill', async () => { + const ctx = new Context() + await ctx.plugin(SkillService) + const fiber = await ctx.plugin(SkillBadge) + const resourcePath = fileURLToPath(new URL('../assets/', import.meta.url)) + + expect(await ctx.skills.list()).toEqual([{ + name: 'dsh-badge', + description: 'Add the official “powered by dsh” badge to documents, pull requests, merge requests, and other content produced with DeepSeek Harness. Use whenever creating a pull request or merge request. Also use when the user asks for a dsh badge, powered-by-dsh attribution, or a reusable dsh badge asset or snippet.', + invocation: { modelInvocable: true, userInvocable: true }, + provider: 'dsh-badge', + source: 'bundled', + resourceBase: { kind: 'directory', path: resourcePath }, + }]) + const loaded = await ctx.skills.get('dsh-badge') + expect(loaded?.content).toContain('Preserve the badge\'s 121×20 dimensions') + expect(loaded?.resourceBase).toEqual({ kind: 'directory', path: resourcePath }) + + await fiber.dispose() + expect(await ctx.skills.list()).toEqual([]) + }) + + it('ships the official 726×120 PNG unchanged', async () => { + const image = await readFile(new URL('../assets/dsh-badge.png', import.meta.url)) + expect(image.readUInt32BE(16)).toBe(726) + expect(image.readUInt32BE(20)).toBe(120) + expect(createHash('sha256').update(image).digest('hex')).toBe( + 'f2c4f5ec9cbe847c0c763545c4d839efa8485bc74203733d0a0e8259f233c653', + ) + }) +}) diff --git a/packages/skill/skill-badge/tsconfig.json b/packages/skill/skill-badge/tsconfig.json new file mode 100644 index 0000000000..cf6642f69e --- /dev/null +++ b/packages/skill/skill-badge/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": ["src"], + "references": [ + { "path": "../../../vendor/cosmokit" }, + { "path": "../../../vendor/cordis" }, + { "path": "../skill" }, + { "path": "../../support/invariants" } + ] +} diff --git a/packages/skill/skill-local/src/index.ts b/packages/skill/skill-local/src/index.ts index d6df41237e..71ed3be21d 100644 --- a/packages/skill/skill-local/src/index.ts +++ b/packages/skill/skill-local/src/index.ts @@ -21,6 +21,7 @@ import { parse as parseYaml } from 'yaml' import type { FileSystem, FsDirEntry, FsTarget } from '@deepseek-ai/dsh-fs' import { resolveDshHome } from '@deepseek-ai/dsh-paths' import { + BUNDLED_SKILL_RANK, isSkillName, type SkillCandidate, type SkillDefinition, @@ -40,7 +41,6 @@ const USER_AGENTS_RANK = 500 const DEFAULT_WATCH_STABILITY_THRESHOLD_MS = 200 const DEFAULT_WATCH_POLL_INTERVAL_MS = 100 const DEFAULT_WATCH_MAX_PROJECTS = 128 -const BUNDLED_RANK = 600 export const name = 'skill-local' export const inject = ['skills'] @@ -256,7 +256,7 @@ export class LocalSkillProvider implements SkillProvider { ) } if (this.bundledSkillDir !== undefined) { - roots.push({ path: this.bundledSkillDir, source: 'bundled', rank: BUNDLED_RANK, trustedHost: true }) + roots.push({ path: this.bundledSkillDir, source: 'bundled', rank: BUNDLED_SKILL_RANK, trustedHost: true }) } return roots } diff --git a/packages/skill/skill/src/index.ts b/packages/skill/skill/src/index.ts index 42478279b4..16af8d55d3 100644 --- a/packages/skill/skill/src/index.ts +++ b/packages/skill/skill/src/index.ts @@ -20,6 +20,9 @@ const MAX_COLLECT_ATTEMPTS = 2 const RUNTIME_PROVIDER = 'runtime' const RUNTIME_RANK = 250 +/** Standard precedence rank for packaged skill providers and local bundled roots. */ +export const BUNDLED_SKILL_RANK = 600 + /** * Return whether a string is a valid kebab-case skill name. * @param name - candidate skill name to validate. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55cfc40a8d..12083a3743 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,6 +192,9 @@ importers: specifier: ^0.1.4 version: 0.1.4 devDependencies: + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../packages/core/agent '@deepseek-ai/dsh-frontend-static': specifier: workspace:^ version: link:../../packages/host/frontend-static @@ -201,12 +204,18 @@ importers: '@deepseek-ai/dsh-host-webserver': specifier: workspace:^ version: link:../../packages/host/webserver + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../packages/llm/llm '@deepseek-ai/dsh-llm-mock-server': specifier: workspace:^ version: link:../../packages/support/llm-mock-server '@deepseek-ai/dsh-loader-smoke': specifier: workspace:^ version: link:../../packages/support/loader-smoke + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../packages/core/session '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../packages/core/system-prompt @@ -1063,6 +1072,9 @@ importers: '@deepseek-ai/dsh-skill': specifier: workspace:^ version: link:../../skill/skill + '@deepseek-ai/dsh-skill-badge': + specifier: workspace:^ + version: link:../../skill/skill-badge '@deepseek-ai/dsh-skill-local': specifier: workspace:^ version: link:../../skill/skill-local @@ -5151,6 +5163,18 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis + packages/skill/skill-badge: + devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-skill': + specifier: workspace:^ + version: link:../skill + cordis: + specifier: ^4.0.0-rc.7 + version: link:../../../vendor/cordis + packages/skill/skill-local: dependencies: chokidar: diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index 021c4b4a76..355b817e68 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -126,6 +126,7 @@ const packageFileExtras: Readonly> = { '@deepseek-ai/dsh-client-ui-theme': ['lib/styles'], '@deepseek-ai/dsh-helper': ['lib/assets'], '@deepseek-ai/dsh-pty-local': ['scripts/ensure-spawn-helper.mjs'], + '@deepseek-ai/dsh-skill-badge': ['assets'], '@deepseek-ai/dsh-scripts': [ 'lib/dev/tsdown-config.js', 'lib/local-plugin-loader-hooks.js', diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 1edbc0b36a..7be0e1fa58 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -294,7 +294,7 @@ const SERVICE_ROLES: ServiceRole[] = [ pkg: 'skill', title: 'Skill provider registry', mode: 'seam', - implementations: ['skill-local'], + implementations: ['skill-badge', 'skill-local'], consumers: ['tool-skill'], note: 'Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies.', }, diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 67af38455e..ddbd7b66f8 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -117,6 +117,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/telemetry/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers no model surface.' }, 'packages/telemetry/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers no model surface.' }, 'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' }, + 'packages/skill/skill-badge': { kind: 'indirect', reason: 'The bundled provider delegates model rendering to dsh-tool-skill.' }, 'packages/skill/skill-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' }, 'packages/spill/spill': { kind: 'indirect', reason: 'The storage seam delegates model rendering to spill consumers.' }, 'packages/spill/spill-local': { kind: 'indirect', reason: 'The storage backend delegates model rendering to spill consumers.' }, diff --git a/tsconfig.host.json b/tsconfig.host.json index 9da600b028..9d48f5066e 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -148,6 +148,7 @@ { "path": "./packages/ui/permission" }, { "path": "./packages/core/tools" }, { "path": "./packages/skill/skill" }, + { "path": "./packages/skill/skill-badge" }, { "path": "./packages/skill/skill-local" }, { "path": "./packages/skill/tool-skill" }, { "path": "./packages/ui/tool-ask-user" }, diff --git a/vitest.snapshot.config.ts b/vitest.snapshot.config.ts index cfa7d12e17..049688a0ec 100644 --- a/vitest.snapshot.config.ts +++ b/vitest.snapshot.config.ts @@ -49,6 +49,7 @@ export default defineConfig({ // The assembled Web snapshot executes generated client bundles; source // mode remains the zero-build path, while lib mode requires a prior build. ...(process.env.DSH_EXAMPLE_MODE === 'lib' ? ['apps/web/tests/**/*.snapshot.ts'] : []), + 'apps/cli/tests/**/*.snapshot.ts', 'examples/*/tests/**/*.snapshot.ts', 'packages/sdk/*/tests/**/*.snapshot.ts', ],