Bundle
dsh-spec-forge
需求锻造:需求澄清、提示词提炼与经验沉淀。把模糊的编程需求锻造成可执行规格,并在每次任务完成后沉淀为可自动复用的个人提示词模板库。
- Source
- winston-hoo
- stars
- 2 stars
- License
- MIT
- Updated
- Updated 9 hours ago
Readme
# dsh-spec-forge · 需求锻造
一个 DeepSeek Harness(dsh)插件。它管两件事:**在你动手前,把模糊需求问清楚;在你做完后,把这次的经验存下来**,下次再遇到同类需求时自动顶上来。
> 实测于 `@deepseek-ai/dsh` 0.1.2-alpha.4(Windows + Web profile)。
> dsh 仍是 developer preview,API 会有破坏性变更;插件已锁定其 API 面,升级 dsh 后如失效请看 CHANGELOG。
---
## 它解决的是什么
编程对话里最磨人的不是写代码,而是这几件事反复发生:
- **需求说不清就开工。** "帮我优化一下那个查询"——哪个查询?优化成什么样?做完才发现理解错了。
- **同一套规矩每次都重新交代。** "common/Result.java 别动""Controller 别写业务逻辑"——换了个会话,模型又踩一次。
- **同一个套路,每次都从零描述。** 你第 N 次让模型"加个分页查询接口",它还是第 1 次见到这个需求的样子。
- **好用的提示词用完就丢。** 你花半小时调教出来的一段标准改法,关掉对话就没了。
这个插件把这些经验沉淀成**明文 Markdown 模板库**,存在 `~/.dsh/spec-forge/` 下。你看得见、改得动、能进 Git——它不是黑盒。
---
## 装好之后,一次任务是这样走的
整条链路是五个工具串起来的闭环:
```
收到需求 → spec_recall(翻历史模板、项目禁区,并给出 fastTrack 分级)
↓
fastTrack=true(L1:原子操作 / 带参考物的自包含新建)──→ 直接动手
↓ 否则 (跳过体检与提炼)
spec_triage(四维体检 + 定级)
↓
L1 执行清单 / L2 按默认执行(都不追问) / L3 完整 Grill-me
↓
spec_distill(L3 与安全阀场景按需)→ 动手写代码
↓
spec_retro(任务链收尾沉淀)→ 下次同类需求被召回
```
举一个真实触发过 L1 的例子。用户只发了这么一句:
> "index.vue 这个物业管理员管理页面的新增/修改接口增加一个主管管员字段 isMainAdmin,值为1是,0否,默认为否,这个字段用开关来显示,请帮我完成这个需求"
`spec_recall` 当场返回 `fastTrack: true`,直接给出执行清单(**跳过体检与提炼**),**一个问题都不问**:
- 改动文件:`index.vue`
- 字段名:`isMainAdmin`
- UI 组件:按"开关"推断为 `el-switch`
- 默认值:`否`
- 列表展示:默认不展示(保守方案,没说就不加)
- 风格自举:先扫 `index.vue` 最近的表单代码,沿用现有写法
在 v0.1.0 里,同样是这条需求,模型会一口气连问 5 个问题——哪怕每个问题它自己都能推断出答案。这就是 L1 通道要治的病:**该闭嘴干活时别装严谨**。0.4.0 又扩了一类快速通道:**带参考物的自包含新建**——"参考现有列表页新建一个订单页"这类需求,实现细节可以从参考物自举,同样一轮做完、不问。
L2、L3 则反过来:需求模块级、一句话推不全时(比如"帮我优化一下那个查询"),0.4.0 起的策略是 **L2 直接按报告给出的默认值动手,不再列问题**——把"问"的额度全部留给真正需要的 L3。唯一的例外是「L2 安全阀」:需求确实**过短、缺一半以上维度、且没有任何可推断锚点**(无文件、无字段、无组件、无默认值)时,才允许**一次性**问清核心,问完立刻动手。L3 架构级重构才放开完整 Grill-me,追问前有硬性急停:**不允许预扫工作区**去"更懂业务",省 token 也省时间。
三个等级判据与响应的速查:
| 等级 | 什么算这类 | 插件怎么做 |
| --- | --- | --- |
| **L1 原子操作 / 快速通道** | 单文件 CRUD,字段/组件/默认值明确;带参考物的自包含新建;或消息含"直接做/速做/不用问/别问/不要问/极速模式" | 不许追问,直接给执行清单 + 扫描目标文件风格自举,疑虑标 `// TODO: [待确认]` |
| **L2 模块变更** | 模块级新增/调整,一句话推不全 | **按报告默认值直接执行,不追问**;仅"过短 + ≥2 维缺失 + 零锚点"转一次性追问 |
| **L3 架构重构** | 含架构/重构/拆分/迁移/升级/建表/跨文件/多模块 | 完整 Grill-me,问透为止 |
L1 还有一张"保守默认表"兜底:列表默认不展示新列、默认不加业务校验、默认后端接口已就绪——只有用户明确说要才会放开,宁可少做不瞎猜。
---
## 沉淀:不是每轮对话都要存
一开始我们担心两个方向:存太勤,模板库会被琐碎对话灌水;存太懒,真正值钱的套路又漏掉。现在沉淀由**三层漏斗**把关:
1. **代码层硬门槛。** 插件自己判断这个会话有没有"资格"沉淀:必须真实改过代码(有 edit/write 类工具调用),且工具调用次数达标。纯问答、只读诊断、一次性咨询——直接拦下,不提示、不打扰。
2. **价值三问(留给模型自检)。** 调用沉淀工具前过一遍:这做法下次还会用吗?结论跨项目成立吗?用户会反复提同类需求吗?任一为否就跳过。
3. **任务链合并。** 一条任务链只沉淀一次,中途的小修小补(改个编译错、修个警告)并进最终那份模板,不产生碎片。
模型判断失误漏了沉淀?兜底在 `spec_recall`:下次这个会话又提出编程需求时,插件会检查"上一轮明明改过代码却没沉淀",随召回结果提醒一句。你也可以随时主动说——**"把这次沉淀成模板"** 会无条件触发。
沉淀出来的模板是同名幂等的:同一类需求再次沉淀会**覆盖更新**旧模板而不是无限堆积。每份模板长这样(六个段落,沉淀和复用共用一套结构):
````markdown
# Spring Boot 新增分页查询接口
分类:`feature/api` 标签:`java` `spring-boot`
## 触发场景
当用户要求新增支持分页的查询接口时适用。
## 需求澄清清单
- 分页参数用 pageNum/pageSize 还是 offset/limit?
- 返回 VO 是否包含关联表字段?
## 标准改法
1. XxxController 新增方法
2. XxxService 与 XxxServiceImpl 实现
3. Mapper XML 写查询 SQL
## 禁区
- 不要修改 common/Result.java 的返回结构
## 提示词模板
```text
按 Controller → Service → ServiceImpl → Mapper 四层实现……
```
## 验收标准
- [ ] mvn -q test 通过
````
完整示例见 `templates/example-spring-pagination.md`。
**禁区会写进项目档案**,不只是跟着模板走:每次会话识别到的"不能改",会累积到该仓库的 `profile.md`,此后所有同类需求自动注入,不用你反复交代。
存储是双层明文目录,项目层优先于全局层:
```
<storageRoot>/
├── global/ # 全局层:跨仓库通用的习惯
│ ├── templates/<id>.md
│ └── profile.md
└── projects/<repo-hash>/ # 项目层:按仓库路径哈希隔离
├── templates/<id>.md
└── profile.md # 该项目的禁区与约定
```
仓库哈希 = 工作目录路径归一化后的 sha256 前 12 位(Windows 路径同样适用)。所有落盘都是原子写(先 `.tmp` 再 rename),崩溃不会留下半个文件。
### 模板库放哪里:三种存储模式
| 模式 | 实际路径 | 适用场景 |
| --- | --- | --- |
| `workspace`(默认,0.3.3+) | `<启动 dsh 的 cwd>/.dsh-spec-forge/` | 工作区与 `$DSH_HOME` 不同盘,避免跨盘 EPERM;模板库跟随当前项目 |
| `home` | `$DSH_HOME/spec-forge/` | 旧版(≤0.3.2)默认;适合把模板库统一存在用户目录 |
| `storageHome` 显式 | 你给的任意绝对路径 | 想放到自定义位置(如 OneDrive 同步盘) |
跨盘写会触发 dsh 的 `workspace-write` 沙箱 EPERM(用户常反馈的"C 盘被拒绝"就是这个)。**新装用户无须配置**——0.3.3 起默认跟随工作区;老用户升级后如果还在用旧路径,调用 `spec_library({ action: 'info' })` 即可看到当前模式与路径,必要时调 `spec_library({ action: 'migrate' })` 把 `$DSH_HOME/spec-forge` 拷过来(默认复制保留源,验证后再传 `move: true` 删除源)。
> **0.4.1 布局修正**:0.3.3/0.4.0 因把已是数据根的 `home` 又追加了一层,数据实际落在 `<root>/spec-forge/…`(`home` 模式下导致历史模板全部读不到、`migrate` 写到了插件不读的位置)。0.4.1 起 `<root>` 即数据根,与实际目录一致;启动时会自动把遗留的 `<root>/spec-forge/…` 一次性上移归位(新位置已有数据则不动)。若你在此期间用过 `workspace` 模式,无需手工处理。
>
> **0.4.2**:上移归位后若源目录已空,会顺手删掉它——0.4.1 会在曾踩坑的机器上永久留下一个 `<root>/spec-forge/` 空壳。
### 手动覆盖配置:改的是 `cordis.patch.yml`
profile 的行配置**不是**写成一个 `spec-forge:` 缩进块,而是改在 profile 目录的 patch 文件里,且它必须是**顶层 YAML 数组**:
```
~/.dsh/profiles/<profile>/cordis.patch.yml
```
```yaml
# 顶层数组;条目用 id 定位到已有行。
# ⚠️ id 定向补丁会「整体替换」该行的 config,不是合并——
# 所以下面必须完整重述所有字段,否则漏掉的字段会回落到插件默认值。
- id: spec-forge
config:
autoRecall: true
autoRetro: true
matchThreshold: 0.35
maxInjectTemplates: 2
injectMaxChars: 4000
defaultScope: project
storageRoot: home # 改这里:workspace(默认)| home
storageHome: '' # 非空则强制覆盖 storageRoot;建议留空
retroMinToolCalls: 2
retroRequireCodeChange: true
strictDistill: true
```
改完重启 dsh,用 `dsh --profile <profile> --dump-config` 确认 `# == dsh-spec-forge` 段里的值与预期一致。
**模板库也会旧。** `spec_library` 会统计超过 90 天未被命中的过期模板并列出名字——模板不是越多越好,旧模板会稀释召回精度。但它只报告不擅自动手,**只有你明确说"清理过期模板"才会物理删除**(删了不可恢复)。
---
## 召回:它怎么认出"这是同类需求"
`spec_recall` 收到需求后做三件事:把需求原文拆成加权指纹 → 和模板库里每一份比对打分 → 超过阈值就把最像的(默认最多 2 份,可配)连同项目禁区一起注入上下文。
打分是透明的,六个维度:
```
总分 = 0.62×词汇相似度 + 0.14×同分类 + 0.08×标签重叠
+ 0.08×同仓库 + 0.05×新鲜度 + 0.03×使用热度
词汇相似度 = 0.6×加权余弦 + 0.4×覆盖率
```
- **词汇相似度是主项。** 指纹按 token 加权:路径(4) > 技术词(3) > 标识符(2) > 中文 2-gram(1),"改 `index.vue`"比"有个页面"值钱得多。
- **查询侧先聚焦再比。** 用户需求常常很长(带路径、叙述、寒暄),`fingerprint` 默认取 24 个 token 里一多半是权重 1 的 2-gram,会稀释余弦与覆盖率。0.3.2 起查询指纹先削掉低信号尾巴(强 token 全保留 + 最多 8 个 2-gram)再进打分——同一条真实需求,聚焦前后词汇分差约 0.03~0.1,跨仓库(无同仓库加分)时这点余量就是命中与否的分界线。落盘模板的指纹保持完整不动。
- **同分类按一级比。** 模板的二级分类(如 `feature/api` 里的 `api`)由模型沉淀时自由填写、不可控,所以 `feature/api` 与 `feature/pagination` 视为同类。查询侧的分类和标签是插件从需求原文现推断的,不依赖模型自觉。
- **标签重叠做了别名归一。** 模板里存的是 `a-switch`,需求里写的是"开关",两边要能对得上——组件别名(`a-switch`→`switch`、`element-plus`→`elementplus`)、中英技术词("分页"→`pagination`)都会归一到同一把钥匙上;重叠率按"模板标签被查询覆盖的比例"算,模板标签都能在需求里找到说法就算高重叠。
- **同仓库 + 新鲜度 + 热度是调节项。** 本仓库沉淀过的模板优先;90 天半衰期衰减;命中越多越可信但用对数压平,避免马太效应。
阈值默认 0.35,`matchThreshold` 可调:调低更易命中(会引入误召回),调高更严格。实测分离度(用真实模板,非构造数据):
| 需求 | 得分 | 结果 |
| --- | --- | --- |
| 同类:Vue 管理页加 `isMainAdmin` 开关字段 | 0.63 | ✅ 命中 |
| 无关:node_modules 加 .gitignore + 写 README | 0.11 | ❌ 不命中 |
召回侧还有一处工程优化:模板列表在进程内带读缓存(写盘版本号 + 文件名集合双重失效),模板库到几百份时也不会每次召回都全量读盘解析。
**诚实边界**:这不是向量检索,是零依赖、零成本、可解释的关键词指纹。对"说法完全不同但语义相同"的需求召回有限——这是刻意取舍,调阈值只能缓解不能根治。
---
## 成本:它花多少,大头在哪
一次真实会话被逐事件解码后的账(详见 `spec-forge-成本归因分析`):
| 项 | 量级 | 说明 |
| --- | --- | --- |
| 插件固定税 | ≈ 2.7K token / 请求 | 常驻提示段 ≈ 358 + 五个工具定义 ≈ 2330(只出现在发往模型的完整请求里) |
| 插件单次调用产出 | 几百 token | `spec_recall` 未命中仅 43 token;`spec_triage` 报告 350~530 token |
| 插件占整轮成本 | ≈ 4% | 8 次 `spec_*` 调用,返回 14.5K 字符,占全部工具返回的 3.2% |
| **真正的成本大头** | **≈ 50%** | 两个 >100K 字符的文件被**整读**后,在其后约 73 步里被反复重计 |
结论:**插件不是成本黑洞,整读大文件才是。** 所以 0.4.0 把「大文件纪律」写进了系统提示与 Skill:
- `>20K` 字符的文件禁止整文件 read,先 `grep` 定位行号再用 offset/limit 分段读
- 确需整体理解时,读一次后立刻落一份「要点摘要」,后续只引用摘要
- 不要为了"摸清业务"预扫工作区(需求没问清前的代码大概率用不上)
配套的两个省钱开关(不在插件里,在 dsh / 模型侧):长会话开启上下文压缩(compaction)避免每步重发全量上下文;预算敏感时降低 `reasoningEffort`——一次实测里 `high` 档的推理 token 占输出的 56%。
---
## 五个工具
| 工具 | 什么时候被调用 | 干什么 |
| --- | --- | --- |
| `spec_recall` | 收到编程需求的**第一件事** | 翻模板库,把命中模板的澄清清单/标准改法/验收标准 + 项目禁区注入上下文,并给出 `level` 与 `fastTrack` |
| `spec_triage` | 召回返回 `fastTrack=false` 时 | 四维体检 + 定 L1/L2/L3。L1 出执行清单,L2 出「已按默认执行」(仅安全阀转一次性追问),L3 出完整 Grill-me |
| `spec_distill` | L3 与安全阀场景澄清完毕、动手之前 | 把需求 + 澄清答案 + 禁区蒸馏成一份实现提示词;禁区为空会拦下 |
| `spec_retro` | 任务收尾 | 把这次经验沉淀/更新成模板;digest 留空时自动从会话事件流提取摘要 |
| `spec_library` | 用户问"模板库里有什么""放哪""怎么搬过来" | `action: list` 展示数据目录/模板清单/命中统计/项目禁区/过期模板;`action: info` 查看存储模式、路径与跨盘判定;`action: migrate` 把旧库一次性迁移过来(`move: true` 迁移后删源) |
> 0.4.0 把原先独立的 `spec_store` 并入了 `spec_library`(`action: info / migrate`),工具数从 6 降到 5,省下约 480 token/请求的常驻工具定义成本。
注入分三层,成本不同:
| 层次 | 机制 | 内容 | 成本 |
| --- | --- | --- | --- |
| 常驻 | 系统提示词 section | 路由规则 + 等级响应 + 大文件纪律 + 急停规则,约 358 token(0.4.0 重写后实测) | 始终占用 |
| 按需 | 运行时 Skill | 完整流程说明书(含三层漏斗决策与大文件纪律) | 用到才加载 |
| 执行 | 五个工具 | 上面这张表 | 调用才产生 |
---
## 安装
**前置**:dsh 可用、Node 22+、`pnpm` 在 PATH 上(`dsh plugin` 内部转发 pnpm,没装就 `npm i -g pnpm`)。
方式一(推荐,装正式发布):
```bash
dsh plugin --profile web add github:<你的账号>/dsh-spec-forge
dsh web # 必须重启,插件才会组合进插件树
```
方式二(本地源码调试):插件里的裸导入(`@deepseek-ai/dsh-tools` 等)会从插件目录向上找 `node_modules`。已装过 dsh + pnpm 就直接用方式一;源码调试则把 `node_modules/@deepseek-ai` 指到 profile 的公共依赖目录(Windows 用 junction、macOS/Linux 用 `ln -s`),或用 `--patch` 叠加启动(Windows 下 patch 里的 `name` 必须写成 `file:///` URL,裸盘符会报 `ERR_UNSUPPORTED_ESM_URL_SCHEME`)。本仓库 `dev/` 下的 patch 含本机绝对路径、已被 `.gitignore` 排除,仅供本地调试。
验证装没装上:
```bash
# Bash / Git Bash(dsh 不在 PATH 时经 pnpm 调用)
pnpm dsh --profile web --dump-config | grep spec-forge
# PowerShell(没有 grep,用 Select-String)
pnpm dsh --profile web --dump-config | Select-String spec-forge
```
输出里应出现一段 `# == dsh-spec-forge` 及其配置块。`--dump-config` 只合成插件树、不启动服务,是排障第一招。
## 配置
在 profile 的 `cordis.patch.yml` 中调整(写法见上文「手动覆盖配置」小节——**id 定向补丁会整体替换 config,必须完整重述所有字段**),均有默认值:
| 配置 | 默认 | 说明 |
| --- | --- | --- |
| `autoRecall` | `true` | 是否注入常驻路由提示 |
| `autoRetro` | `true` | 是否在任务完成后提示沉淀 |
| `matchThreshold` | `0.35` | 命中阈值,低更易命中、高更严格 |
| `maxInjectTemplates` | `2` | 单次最多注入几份模板 |
| `injectMaxChars` | `4000` | 注入上下文上限字符数 |
| `defaultScope` | `project` | 沉淀默认落项目层还是全局层 |
| `retroMinToolCalls` | `2` | 自动复盘要求的最少工具调用数 |
| `retroRequireCodeChange` | `true` | 沉淀提醒要求真实改过代码,纯问答/只读不提醒 |
| `strictDistill` | `true` | 提炼时强制要求禁区,空则报错 |
| `storageRoot` | `workspace` | 存储模式:`workspace`(0.3.3 默认,跟工作区)/ `home`(放 `$DSH_HOME`,兼容旧版)/ 设置 `storageHome` 绝对路径时此字段被忽略 |
| `storageHome` | 空 | 自定义数据目录(绝对路径)。非空时优先于 `storageRoot` |
---
## 验证
```bash
npm test # 单元测试:168 个,覆盖指纹(含序列化回归)/匹配/会话提取/存储(含布局与空壳回归)/渲染/分类器/沉淀门槛/读缓存/查询聚焦/路径解析/迁移
npm run smoke # 端到端冒烟:沉淀→召回→注入→体检→完成判定→幂等 整条链路
npm run verify # 加载验证:mock ctx 执行 apply(),确认工具都能注册、schema 合规
npm run token-audit # 静态 token 预算审计:常驻/工具定义/SKILL/单次调用产出/真实库命中注入
```
冒烟的真实输出(可作验收基线):
```
1. 沉淀:一次任务结束后写入模板 [PASS] 模板已落盘
指纹与布局自检 [PASS] 指纹 8 项损坏 0 项 / 落盘无 [object Object] / 无多余 spec-forge 层级
2. 召回:同类需求命中 [PASS] 得分 0.499,lexical=0.635(确认靠词汇相似度而非仅元数据)
3. 召回:异类需求不命中 [PASS] 得分 0.136
4. 注入:禁区/澄清清单/标准改法进上下文 [PASS]
5. 体检:模糊需求被拦下要求澄清 [PASS] 缺失 要实现什么/哪些不能改/上下文
6. 完成判定:不做完的活不误判为完成 [PASS]
7. 幂等:同类需求再次沉淀是覆盖非堆积 [PASS] 仍 1 份
```
真实环境验收,装完之后可以照着试:
1. 发一条简单 CRUD 需求(如"index.vue 加 isMainAdmin 字段,开关,默认 0"),观察是否**直接动手不追问**;
2. 发一条模块级需求("给订单模块加导出,要 Excel 和 CSV"),观察是否**按报告默认值直接执行、不列问题**;再发一条极短且无锚点的("帮我改一下那个查询"),观察是否**只一次性问清核心**(安全阀);
3. 发一条完整需求(文件路径 + 禁区 + 验收命令),观察是否走完 召回→体检→提炼→实现→沉淀;
4. 发一条带"直接做"的需求,观察是否无条件进 L1 快通道;发一条"参考现有列表页新建一个订单页"的需求,观察是否因**带参考物的自包含新建**直接 fastTrack;
5. 检查模板落盘:`ls <工作区>/.dsh-spec-forge/projects/<hash>/templates/`(老模式为 `~/.dsh/spec-forge/...`);
6. 再发一条同类需求,确认 `spec_recall` 召回刚沉淀的模板(模型会引用其中的澄清清单)。
---
## 它不做什么(已知边界)
1. **dsh 还是开发者预览版。** 插件锁定的 API 面是 `ctx.tools.register` / `systemPrompt` / `skills` / `exec.agent.session`;0.3.0 起不再依赖 `turn/end` 事件(它不含会话事件流)。升级 dsh 后失效,先 `--dump-config` 排查再看 CHANGELOG。
2. **匹配不是向量检索。** 关键词指纹对"说法完全不同但语义相同"的需求召回有限,这是刻意的零依赖取舍。
3. **沉淀时机有三层保障但非绝对。** 代码层硬门槛 + 模型价值三问 + 任务链合并,理论上仍可能漏——漏了 `spec_recall` 会提醒,或直接说"把这次沉淀成模板"。
4. **"任务完整结束"是启发式判定**,依据事件流结构判断,准但不敢说绝对可靠。
5. **急停规则是提示词约束,不是硬拦截。** "提问前不许扫工作区"写死在模型可见的四层文本里,实测有效,但模型仍可能违背——等 dsh 出工具级前置钩子才能根治。
6. **复杂度分级是启发式。** 判据基于正则信号,生僻表述可能漏检;漏检一律回退 L2(按默认执行),不会默认 L1 瞎干。过短且无锚点的需求会触发「L2 安全阀」只问一次。DDL 信号做了前后视断言防误伤("新建订单**列表页**""搜索**表单**组件"不会被当成建表),但中文复合词仍有边界,遇到误判可在 `lib/classify.js` 的 `L3_PATTERNS` 里调整。
7. **插件与宿主同进程同权限。** 它只读写 `<storageRoot>/spec-forge`(默认 `<启动 dsh 的 cwd>/.dsh-spec-forge/`,可在配置里改成 `$DSH_HOME/spec-forge` 或绝对路径),不联网、不执行 shell、不读凭据;源码公开,装前可自行审查。
---
## 卸载
```bash
dsh plugin --profile web remove dsh-spec-forge
# 若声明了 dsh.bundle.patch,还需清理 profile 的 cordis.patch.yml 对应行
dsh web # 重启生效
```
模板数据在插件目录之外,卸载不删沉淀。要彻底清空:`rm -rf ~/.dsh/spec-forge`
## 开发与发布
变更记录见 `CHANGELOG.md`。发布流程:改代码 → `npm test` → 升 `package.json` 版本 → CHANGELOG 顶部加条目 → 同步 README → commit & push → 在 profile 目录 `env -u NODE_OPTIONS pnpm update dsh-spec-forge` 刷新锁文件(pnpm-lock 会钉住 GitHub 依赖的提交 SHA,不改锁文件重装仍是旧代码)。
## 许可证
MIT
Install
dsh plugin --profile web add github:winston-hoo/dsh-spec-forge
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-spec-forge from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.