fix: focus built-in skill defaults

The Skills catalog exposed nine product-marketing recipes as independent
capabilities and enabled every marketing package for every Runtime by default.
It now keeps five top-level Skills, loads the nine recipes on demand through
the product-marketing package, and leaves the three marketing Skills disabled
until users enable them. Existing saved Skill settings remain authoritative.

Release note: 内置 Skills 已从 14 个顶层入口精简为 5 个;去 AI 味和长文工具默认启用,产品营销类默认停用,已保存的用户设置保持不变。
This commit is contained in:
mesalogo
2026-09-02 00:44:44 +08:00
parent 4f3e0bdcfd
commit 06c1123caa
28 changed files with 209 additions and 162 deletions
@@ -78,6 +78,16 @@ stdio MCP 配置的绝对 command 表达用户对该 MCP 的明确覆盖,原
分阶段交付期间,未实现的“工具执行环境”不进入 Tab 状态集合。第一阶段状态集合只有
`skills | mcp`,后续实现完成后才扩展为 `skills | mcp | tool-environment`。
### INV-11 内置 Skill 默认值不覆盖用户状态
只有状态存储中没有对应 Skill 记录时才使用产品默认值。通用文档工具
`deai-writing`、`longdoc-docx` 默认启用;产品营销类 `product-evidence`、
`product-marketing`、`product-presentation` 默认停用。用户显式保存的启停状态和
Runtime 分配优先,升级不得重置。
九种产品营销文档配方属于 `product-marketing` 包内工作流,不是独立能力状态维度,
不进入顶层 Skill 目录,也不单独注入 Runtime。
## 3. 状态维度
### 3.1 来源选择
@@ -179,6 +179,13 @@ Python、虚拟环境或 Conda 环境时,也不能被 GoodBuddy 静默替换
`Skills`、`MCP`、`工具执行环境` 三个水平 Tab。始终默认打开 `Skills`。
- 三个 Tab 使用共享 `PageTabs`,保留 `tablist`、`tab`、`tabpanel`、方向键和可见焦点。
- `Skills` 继续承载现有 Skill 启停、导入、状态和 Runtime 分配。
- 内置 Skill 顶层目录只展示五个独立能力:`deai-writing`、`longdoc-docx`、
`product-evidence`、`product-marketing`、`product-presentation`。产品营销的功能目录、
一页纸、技术方案、白皮书、招标、演示套件、客户案例和竞品定位作为
`product-marketing` 内部工作流,不重复显示为独立 Skill。
- `deai-writing` 和 `longdoc-docx` 默认启用;`product-evidence`、
`product-marketing`、`product-presentation` 默认停用。已保存的用户启停和 Runtime
分配始终优先于默认值;导入 Skill 的默认行为保持不变。
- `MCP` 继续承载现有 MCP 配置、凭据、状态和 Runtime 分配;其现有内部分组页签保持不变。
- `工具执行环境` 承载工具下载源、Node.js、Python、诊断和生效范围。
- 合并导航不改写 Skills/MCP 业务数据;未来新增定向入口时必须进入对应水平 Tab。
@@ -32,6 +32,9 @@
- [x] 定义设置页结构、状态、文案和无障碍要求。
- [x] 定义 Main 服务、shim、探测、诊断、PATH 注入和 Python 生命周期。
- [x] 远程主机设置副标题已说明本机 Skills 和 MCP 不作用于远程主机。
- [x] 将内置 Skill 顶层目录从 14 个收敛为 5 个,九种产品营销配方改为
`product-marketing` 包内按需工作流。
- [x] 通用文档工具默认启用,三个产品营销 Skill 默认停用,同时保留用户显式保存状态。
## 待实施
@@ -83,9 +86,10 @@
- `npm run typecheck` 通过。
- `npm run lint` 通过。
- `npm run build` 通过。
- `npm test` 通过:`3412` 通过、`50` 跳过。
- Skill 聚焦测试通过:Capability/Bundled Skills `39/39`,产品营销路由 `10/10`。
- `npm test -- src/renderer/src/SettingsPanel.test.tsx` 通过:`80/80`。
- 最近一次 `npm test` 为 `3410` 通过、`50` 跳过、`1` 失败;失败是并行修改中的浏览器
控制用例超时,该用例随后单独复跑通过。本功能聚焦测试全部通过。
- 设置页测试确认左侧不再显示独立 Skills/MCP,统一入口默认打开 Skills,MCP 原内层页签
保持可用,且不显示工具执行环境占位。
- 修复能力与工具 Tab 包装层在可滚动设置布局中被 Flexbox 压缩到零高度的问题,并增加与
@@ -232,6 +232,17 @@ URL 只存在于 Main/构建目录,不进入 Renderer。两个 Target 共用
SKILL 稳定调用命令名,不接收真实安装路径。
- `resources/skills` 顶层只保留五个带 `SKILL.md` 的独立包:
`deai-writing`、`longdoc-docx`、`product-evidence`、`product-marketing`、
`product-presentation`。
- 九种纯 Markdown 产品营销配方位于 `product-marketing/workflows/<route-id>/`,
使用 `WORKFLOW.md` 与同目录 `templates/`,不参与顶层 Skill 发现。
- `CapabilityService` 仅在状态中没有该 ID 时应用默认值。三个产品营销 Skill 默认
`enabled: false`;两个通用文档工具和导入 Skill 维持默认启用。显式保存状态不迁移、
不覆盖。
- 默认停用的 Skill 不进入 Runtime 指令和原生 Skill 包列表;用户启用后沿用现有四个
Runtime 分配。
stdio MCP:
- 裸 `node`、`npm`、`npx`、`python`、`python3`、`pip` 使用工具 bin。
@@ -56,6 +56,7 @@
Skills 面板
现有 Skill 启停、导入、状态和 Runtime 分配
五个内置 Skill;产品营销三个默认停用,通用文档工具两个默认启用
MCP 面板
现有 MCP 配置、凭据、状态和 Runtime 分配
@@ -80,6 +81,10 @@ MCP 面板
测试环境
```
产品营销的九种文档配方不单独占用 Skill 卡片。用户启用 `product-marketing` 后,
由该 Skill 根据任务按需读取内部工作流。界面只展示能够独立启停和分配 Runtime 的
五个顶层内置 Skill;已保存状态覆盖产品默认值。
工具执行环境面板内的区块顺序固定为工具下载源、Node.js、Python,不再增加页签。
## 3. 三个水平 Tab
@@ -308,3 +308,16 @@
- 切换三个外层 Tab 后返回时保留本次设置面板会话中的编辑状态
- 关闭并重新打开设置时,普通入口仍默认显示 Skills,不持久化上次外层 Tab
- 未来新增 Skills/MCP 定向入口时打开对应水平 Tab
### US-F3 聚焦默认内置能力
作为普通用户,我希望默认只加载通用文档工具,不让低频产品营销说明占用每次会话的上下文。
验收:
- Skills 列表只把五个具备独立入口或执行器的内置能力显示为顶层 Skill
- 产品营销的九种文档配方从 `product-marketing` 内部按需读取
- `deai-writing` 和 `longdoc-docx` 在没有保存状态时默认启用
- 三个产品营销 Skill 在没有保存状态时默认停用
- 用户显式保存的启停状态和 Runtime 分配不因版本升级而重置
- 用户导入的 Skill 继续使用现有默认启用和 Runtime 分配规则
+25 -24
View File
@@ -1,40 +1,40 @@
---
name: product-marketing
version: 1.0.0
version: 1.1.0
description: |
编排产品事实、功能列表、招标参数、产品 PPT、技术方案、一页纸、白皮书、招标响应、
演示套件、客户案例和竞品定位等多个独立技能。用于一次请求需要选择或组合多种
产品市场产物,并确保它们共享同一事实版本、术语和承诺边界。
演示套件、客户案例和竞品定位。用于一次请求需要选择或组合多种产品市场产物,
并确保它们共享同一事实版本、术语和承诺边界。
allowed-tools:
- Read
- Grep
- Glob
- Execute
compatibility: 可独立规划;执行节点需要对应子技能可用
compatibility: 内置九种文档工作流;事实校验和 PPT 构建需要对应独立 Skill
---
# 产品市场总编排
本技能只负责需求澄清、路由、依赖、门禁和跨产物一致性。各产物的方法和模板由
对应独立技能负责,不能在总技能中重新实现一份简化版本。
本技能负责需求澄清、路由、依赖、门禁和跨产物一致性。九种文档产物的方法与模板
内置在 `workflows/`;需要哪种产物才读取对应 `WORKFLOW.md`,不要一次加载全部。
## 子技能
## 内置工作流
| 技能 | 唯一职责 |
|---|---|
| `product-evidence` | 冻结产品事实、状态、证据和限制 |
| `product-feature-catalog` | 功能目录和版本矩阵 |
| `tender-technical-spec` | 可采购、可验收的招标技术规格 |
| `product-presentation` | 产品介绍 PPT 与讲稿 |
| `technical-proposal` | 客户或投标技术方案 |
| `product-one-pager` | 一页纸产品概览和彩页文案 |
| `solution-whitepaper` | 原理、架构、证据和边界白皮书 |
| `tender-response-matrix` | 招标要求逐条响应、缺口和偏离 |
| `sales-demo-kit` | 可执行演示套件、回退和演练材料 |
| `customer-case-study` | 经授权且可复核的客户案例 |
| `competitive-positioning` | 有来源的竞品矩阵和定位 |
| 路由标识 | 工作流文件 | 唯一职责 |
|---|---|---|
| `product-feature-catalog` | `workflows/product-feature-catalog/WORKFLOW.md` | 功能目录和版本矩阵 |
| `tender-technical-spec` | `workflows/tender-technical-spec/WORKFLOW.md` | 可采购、可验收的招标技术规格 |
| `technical-proposal` | `workflows/technical-proposal/WORKFLOW.md` | 客户或投标技术方案 |
| `product-one-pager` | `workflows/product-one-pager/WORKFLOW.md` | 一页纸产品概览和彩页文案 |
| `solution-whitepaper` | `workflows/solution-whitepaper/WORKFLOW.md` | 原理、架构、证据和边界白皮书 |
| `tender-response-matrix` | `workflows/tender-response-matrix/WORKFLOW.md` | 招标要求逐条响应、缺口和偏离 |
| `sales-demo-kit` | `workflows/sales-demo-kit/WORKFLOW.md` | 可执行演示套件、回退和演练材料 |
| `customer-case-study` | `workflows/customer-case-study/WORKFLOW.md` | 经授权且可复核的客户案例 |
| `competitive-positioning` | `workflows/competitive-positioning/WORKFLOW.md` | 有来源的竞品矩阵和定位 |
`deai-writing` 和 `longdoc-docx` 是可选质量与导出技能,不承担产品事实判断。
`product-evidence` 负责冻结产品事实、状态、证据和限制;`product-presentation`
负责产品 PPT 与讲稿构建。两者保留为独立 Skill,使用前必须确认已启用。
`deai-writing` 和 `longdoc-docx` 也是独立的质量与导出 Skill,不承担产品事实判断。
## 第一步:形成任务简报
@@ -60,8 +60,9 @@ compatibility: 可独立规划;执行节点需要对应子技能可用
<python> "<skill-dir>/scripts/validate_route_plan.py" ./route-plan.json
```
所有产物必须直接或间接依赖唯一的 `product-evidence` 节点。节点缺少对应技能时
明确报告缺失,不允许由总技能静默生成低质量替代物。
所有产物必须直接或间接依赖唯一的 `product-evidence` 节点。路由计划 v1 继续用
`skill` 字段记录工作流或独立 Skill 标识,避免破坏已有计划。缺少或未启用
`product-evidence`、`product-presentation` 时明确报告,不允许静默生成替代物。
`depends_on` 表示硬依赖:被依赖节点未 `completed` 时,本节点不能进入 `running`
或 `completed`。可选输入(例如尚无授权的客户案例)不要写进 `depends_on`,而是
@@ -179,7 +180,7 @@ deliverables/
internal/ # 仅内部
```
各子技能的 `output` 一律指向 `deliverables/<密级>/`,核验产物一律写入
各工作流的 `output` 一律指向 `deliverables/<密级>/`,核验产物一律写入
`build/check/`。交付目录内只允许出现 DOCX 与 PPTX。
单文件产物直接放 `build/<产物名>.md`。技术方案、白皮书等需要分章节的长文,改用
@@ -1,18 +1,8 @@
---
name: competitive-positioning
version: 1.0.0
description: |
基于公开、可定位且有日期的来源生成竞品矩阵、差异化定位和销售边界。用于市场
分析、产品定位和受控销售准备;不允许推断竞品“不支持”或生成无证据攻击性话术。
allowed-tools:
- Read
- Grep
- Glob
compatibility: Markdown/JSON;需要提供或获准获取竞品公开来源
---
# 竞品与定位分析
基于公开、可定位且有日期的来源生成竞品矩阵、差异化定位和销售边界。用于市场
分析、产品定位和受控销售准备;不允许推断竞品“不支持”或生成无证据攻击性话术。
## 必要输入
- 本产品事实与证据。
@@ -1,18 +1,8 @@
---
name: customer-case-study
version: 1.0.0
description: |
基于客户授权、实施记录和可复核指标生成客户案例、成功故事和案例摘要。用于公开
宣传、销售材料或受控投标引用;没有披露授权或历史结果时不得生成可发布案例。
allowed-tools:
- Read
- Grep
- Glob
compatibility: Markdown;建议配合 product-evidence
---
# 客户案例
基于客户授权、实施记录和可复核指标生成客户案例、成功故事和案例摘要。用于公开
宣传、销售材料或受控投标引用;没有披露授权或历史结果时不得生成可发布案例。
## 发布前硬门禁
必须具备:
@@ -1,19 +1,9 @@
---
name: product-feature-catalog
version: 1.0.0
description: |
基于产品事实与证据生成结构化功能列表、模块树和版本能力矩阵。用于产品规划、
售前交流、招标附件或交付范围梳理;重点回答产品有什么功能、谁使用、产生什么
结果,不负责撰写技术参数或长篇方案。
allowed-tools:
- Read
- Grep
- Glob
compatibility: Markdown;建议配合 product-evidence
---
# 产品功能列表
基于产品事实与证据生成结构化功能列表、模块树和版本能力矩阵。用于产品规划、
售前交流、招标附件或交付范围梳理;重点回答产品有什么功能、谁使用、产生什么
结果,不负责撰写技术参数或长篇方案。
## 必要输入
- `product-evidence.json`,至少包含 `product`、`features`、`limitations`。
@@ -1,18 +1,8 @@
---
name: product-one-pager
version: 1.0.0
description: |
将已核验产品信息压缩为一页纸产品概览、宣传彩页或官网下载页文案。用于首次触达、
展会资料和销售跟进;不替代完整功能清单、技术规格或技术方案。
allowed-tools:
- Read
- Grep
- Glob
compatibility: Markdown/HTML;建议配合 product-evidence
---
# 产品一页纸
将已核验产品信息压缩为一页纸产品概览、宣传彩页或官网下载页文案。用于首次触达、
展会资料和销售跟进;不替代完整功能清单、技术规格或技术方案。
## 必要输入
- 产品定位、目标受众和本次一页纸唯一目标。
@@ -1,19 +1,8 @@
---
name: sales-demo-kit
version: 1.0.0
description: |
生成可执行的产品演示故事线、环境清单、操作脚本、讲解词、失败回退和演练检查表。
用于售前 Demo、POC 汇报或验收演示;不负责制作通用产品介绍 PPT。
allowed-tools:
- Read
- Grep
- Glob
- Execute
compatibility: Markdown/JSON;演示自动化需另行使用可用的控制工具
---
# 售前演示套件
生成可执行的产品演示故事线、环境清单、操作脚本、讲解词、失败回退和演练检查表。
用于售前 Demo、POC 汇报或验收演示;不负责制作通用产品介绍 PPT。
## 必要输入
- 演示目标、受众、时长和希望获得的下一步。
@@ -1,20 +1,9 @@
---
name: solution-whitepaper
version: 1.0.0
description: |
编写解释行业问题、技术原理、参考架构、实现方法、测试证据和适用边界的产品或
解决方案白皮书。用于技术传播和决策评估;不编制客户项目计划,也不把宣传口号
当作技术论证。
allowed-tools:
- Read
- Grep
- Glob
- Execute
compatibility: Markdown;可配合 longdoc-docx 导出 Word(PDF 仅用于核验)
---
# 解决方案白皮书
编写解释行业问题、技术原理、参考架构、实现方法、测试证据和适用边界的产品或
解决方案白皮书。用于技术传播和决策评估;不编制客户项目计划,也不把宣传口号
当作技术论证。
## 必要输入
- 白皮书主题、目标读者、研究问题和发布范围。
@@ -1,20 +1,9 @@
---
name: technical-proposal
version: 1.0.0
description: |
基于客户需求、产品事实和项目约束编制完整技术方案,覆盖需求分析、总体架构、
详细设计、实施、交付、质量、安全、风险和验收。用于投标技术方案或客户解决
方案;不负责制定招标参数,也不替代逐条招标响应矩阵。
allowed-tools:
- Read
- Grep
- Glob
- Execute
compatibility: Markdown;可配合 longdoc-docx 导出 Word(PDF 仅用于核验)
---
# 技术方案
基于客户需求、产品事实和项目约束编制完整技术方案,覆盖需求分析、总体架构、
详细设计、实施、交付、质量、安全、风险和验收。用于投标技术方案或客户解决
方案;不负责制定招标参数,也不替代逐条招标响应矩阵。
## 必要输入
- 客户需求原文及编号、评分点或验收目标。
@@ -1,18 +1,8 @@
---
name: tender-response-matrix
version: 1.0.0
description: |
将招标文件技术要求逐条拆解并生成符合性响应矩阵、缺口清单和方案章节映射。用于
投标前要求解析、响应检查和技术偏离管理;不制定采购参数,也不代写整篇方案。
allowed-tools:
- Read
- Grep
- Glob
compatibility: Markdown/CSV;建议配合 product-evidence
---
# 招标响应矩阵
将招标文件技术要求逐条拆解并生成符合性响应矩阵、缺口清单和方案章节映射。用于
投标前要求解析、响应检查和技术偏离管理;不制定采购参数,也不代写整篇方案。
## 必要输入
- 招标文件原文及可定位的章节、页码或条款编号。
@@ -1,19 +1,9 @@
---
name: tender-technical-spec
version: 1.0.0
description: |
将已核验产品能力转写为可采购、可测试、可验收的招标技术规格和参数表。用于编制
招标文件技术要求、采购参数或技术规格书;不负责判断投标方是否符合,也不负责
撰写整篇投标技术方案。
allowed-tools:
- Read
- Grep
- Glob
compatibility: Markdown;建议配合 product-evidence
---
# 招标技术参数
将已核验产品能力转写为可采购、可测试、可验收的招标技术规格和参数表。用于编制
招标文件技术要求、采购参数或技术规格书;不负责判断投标方是否符合,也不负责
撰写整篇投标技术方案。
## 必要输入
- 产品事实与证据清单,尤其是功能、参数、限制、部署和兼容性。
+38 -7
View File
@@ -56,28 +56,55 @@ describe('bundled skills', () => {
it('parses every bundled SKILL.md', async () => {
const snapshot = await (await createService()).getSnapshot()
expect(snapshot.skills.length).toBeGreaterThan(0)
expect(snapshot.skills.map((skill) => skill.id)).toEqual(
expect.arrayContaining([
'deai-writing',
'longdoc-docx',
'product-evidence',
'product-marketing',
'product-presentation'
])
)
expect(snapshot.skills).toHaveLength(5)
expect(snapshot.skills.every((skill) => skill.source === 'builtin')).toBe(
true
)
expect(snapshot.skills.map((skill) => skill.id)).toContain(
'product-marketing'
expect(
snapshot.skills
.filter((skill) => skill.enabled)
.map((skill) => skill.id)
).toEqual(
expect.arrayContaining(['deai-writing', 'longdoc-docx'])
)
expect(
snapshot.skills
.filter((skill) => !skill.enabled)
.map((skill) => skill.id)
).toEqual(
expect.arrayContaining([
'product-evidence',
'product-marketing',
'product-presentation'
])
)
expect(snapshot.skills.map((skill) => skill.id)).not.toContain(
'web-3d-game'
)
})
it('injects every enabled bundled skill with its resolved directory', async () => {
it('injects enabled tools without loading disabled marketing skills', async () => {
const service = await createService()
const snapshot = await service.getSnapshot()
const instructions = await service.getSkillInstructions('continue')
expect(instructions).not.toContain('因超出注入上限未加载')
for (const skill of snapshot.skills) {
for (const skill of snapshot.skills.filter((item) => item.enabled)) {
expect(instructions).toContain(join(builtinSkillsRoot, skill.id))
}
for (const skill of snapshot.skills.filter((item) => !item.enabled)) {
expect(instructions).not.toContain(join(builtinSkillsRoot, skill.id))
}
})
it('exposes bundled Skills as native Harness packages', async () => {
@@ -88,8 +115,12 @@ describe('bundled skills', () => {
).resolves.toMatchObject({
packages: expect.arrayContaining([
{
id: 'product-marketing',
directory: join(builtinSkillsRoot, 'product-marketing')
id: 'deai-writing',
directory: join(builtinSkillsRoot, 'deai-writing')
},
{
id: 'longdoc-docx',
directory: join(builtinSkillsRoot, 'longdoc-docx')
}
])
})
@@ -358,6 +358,54 @@ describe('CapabilityService', () => {
})
})
it('disables product marketing skills by default and preserves explicit enablement', async () => {
const { filePath, builtinRoot, importedRoot, service } =
await createService()
await Promise.all([
writeSkill(builtinRoot, 'product-marketing', '产品市场'),
writeSkill(builtinRoot, 'product-evidence', '产品证据'),
writeSkill(builtinRoot, 'product-presentation', '产品演示'),
writeSkill(builtinRoot, 'deai-writing', '中文审校'),
writeSkill(builtinRoot, 'longdoc-docx', '长文导出')
])
const snapshot = await service.getSnapshot()
expect(snapshot.skills).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: 'product-marketing',
enabled: false
}),
expect.objectContaining({
id: 'product-evidence',
enabled: false
}),
expect.objectContaining({
id: 'product-presentation',
enabled: false
}),
expect.objectContaining({ id: 'deai-writing', enabled: true }),
expect.objectContaining({ id: 'longdoc-docx', enabled: true })
])
)
await service.setSkillEnabled('product-marketing', true)
const reloaded = new CapabilityService(
filePath,
builtinRoot,
importedRoot,
cipher
)
await expect(reloaded.getSnapshot()).resolves.toMatchObject({
skills: expect.arrayContaining([
expect.objectContaining({
id: 'product-marketing',
enabled: true
})
])
})
})
it('imports and removes a managed SKILL.md package', async () => {
const { directory, service } = await createService()
const packageRoot = join(directory, 'source-skill')
+15 -5
View File
@@ -78,6 +78,11 @@ const MAX_SKILL_PACKAGE_BYTES = 10 * 1024 * 1024
const MAX_SKILL_PACKAGE_FILES = 128
const MAX_SKILL_DEPTH = 6
const MAX_SKILL_DISCOVERY_DEPTH = 4
const DEFAULT_DISABLED_BUILTIN_SKILL_IDS = new Set([
'product-marketing',
'product-evidence',
'product-presentation'
])
const MAX_SKILL_DISCOVERY_RESULTS = 64
const MAX_SKILL_INSTRUCTION_CHARACTERS = 262_144
const SKILL_DISCOVERY_IGNORED_DIRECTORIES = new Set([
@@ -304,9 +309,13 @@ function emptyStoredCapabilities(
}
}
function defaultSkillState(): z.infer<typeof skillStateSchema> {
function defaultSkillState(
skill?: Pick<SkillSummary, 'id' | 'source'>
): z.infer<typeof skillStateSchema> {
return {
enabled: true,
enabled:
skill?.source !== 'builtin' ||
!DEFAULT_DISABLED_BUILTIN_SKILL_IDS.has(skill.id),
assignments: [
'model',
'opencode',
@@ -932,7 +941,7 @@ export class CapabilityService {
skills: catalog
.map((skill) => ({
...skill,
...(state.skills[skill.id] ?? defaultSkillState())
...(state.skills[skill.id] ?? defaultSkillState(skill))
}))
.sort((left, right) =>
left.source === right.source
@@ -1664,7 +1673,8 @@ export class CapabilityService {
return this.queue(async () => {
const id = skillIdSchema.parse(skillId)
const catalog = await this.getSkillCatalog()
if (!catalog.some((skill) => skill.id === id)) {
const skill = catalog.find((item) => item.id === id)
if (!skill) {
throw new Error('Skill 不存在')
}
const state = await this.load()
@@ -1673,7 +1683,7 @@ export class CapabilityService {
skills: {
...state.skills,
[id]: {
...(state.skills[id] ?? defaultSkillState()),
...(state.skills[id] ?? defaultSkillState(skill)),
...update
}
}