DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

Hercules-debug /

Hercules-debug/butler-git

Verified

把「我改完了」变成可验证的事实:Δ(预期改动)+ P(检测程序)全过才产生 commit。零依赖,只用 node 和 git。 | Turn 'done' into a verifiable fact — a commit exists only if Δ + P both pass.

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@1e56da44

butler-git —— 节点是「意图」,commit 是「证据」

门禁零依赖。 plan / status / commit / abandon / approve 只需要 node 和 git。

一个例外,写在这免得误解:bg html(给人看图的那条命令)用 mermaid 做布局, 从 CDN 取 —— 打开页面需要联网。渲染坏了不影响门禁,那条规矩在门禁上没破。 为什么破、代价多大,见「那张图在画什么」一节和 lib/mermaid.mjs 顶部。

节点先存在(你要达成什么),commit 后出现(你做完了什么)。
两者不是相等关系,而是**达成关系**:节点达成时,留下一个 commit 作为证据。

概念和状态机定稿见 DESIGN.md,那份是设计依据。 这份是怎么跑起来。

它取代了 README 里原来那套"两盏灯 + 断言谓词"的模型 —— 那套是过渡形态,讨论下来收敛到了现在这个。


跑起来

node test/run.mjs        # 57 条负向验收
node test/plugin.mjs     # 插件自测(26 条)
# 或
npm test

单步(在一个 git 仓库里):

BG="node bin/bg.mjs"
B=$(git rev-parse HEAD)

$BG plan     --id n7 --expect "让导出支持非 ASCII" --base $B \
             --verify "pytest -q" --delta "M:src/csv.py" --delta "A:src/enc.py"
$BG status   n7          # 改了什么;和 Δ 比差在哪(便宜,不跑 P)
$BG commit   n7          # 提交即门禁:Δ + P,过了才产生版本
$BG abandon  n7          # 放弃一个声明了但没做的任务

# 人的面
$BG tree                 # 看整棵树(灯 + P 原文 + 弱 P 标记 + 图外的提交)
$BG html                 # 生成 .bg/tree.html —— 一幅真 DAG 图(点=commit,线=git 父子)
$BG serve                # 前台实时查看器(默认 127.0.0.1:8731)
$BG recheck              # 对当前 commit 复查各历史节点(诊断,不是灯)
$BG health               # 心跳一行
$BG log                  # 从 commit trailer 读出来的历史
$BG crosscheck           # 图和 commit 对不对得上

bg html 的图需要联网。 它用 mermaid 做布局(从 jsdelivr CDN 取,3.3MB)。 断网时页面会明说加载失败,并把节点清单用纯文字列出来 —— 图没了,信息不丢。 取舍的原因和实测代价见 lib/mermaid.mjs 顶部。

门禁本身不受影响:plan / status / commit / abandon / approve 仍然只需要 node 和 git,渲染坏了也不影响它们。


三个概念

是什么 谁定
节点 一个意图(要达成什么) 声明它的那个
Δ 预期变化:精确集合,带方向 同上
P 检测程序:一条命令,退出码 0 才算过 同上(必须有)
commit 达成时留下的证据,不可变、可重放 门禁过了才产生

绿 = 一个关于某个 commit 的事实。 那个 commit 不可变,所以这个事实 永不过时、也不会被推翻。所以只需要一盏灯。

checkout 那个 commit
跑 (Δ, P)
-> 结果永远一样

绿只对 commit 说,不对工作区说。工作区有未提交改动时, 没有任何绿能代表它 —— 这时要明说"当前状态没被验过", 不许拿最后一个绿来顶。


Δ 的四个性质

① 精确集合,带方向      M / A / D / R   改了 / 新增 / 删除 / 重命名
② 不是"作用域"         精确集合本身就说了"必须"和"允许",不需要另一个 allow
③ 可以为空,空 = 没有预测  (不是"没改动"!)
④ 可以是预测或转录      但必须标出来 —— 强度不同,不同要可见

一次比较回答三件事:

diff(base, result) 和 Δ 比:
   少了       ->  漏做
   多了       ->  **预期外的改动**
   类型不符   ->  做错方向(该删的改了、该加的删了)

"多了"是最要紧的一条 —— 漏做模型自己会发现,预期外的改动它看不见。


门禁

一次提交 = 一次门禁。不通过,不许 commit。

node_commit(n):
  1. 固定工作区:算它的树 Y
  2. 查 Δ:diff(base, Y) 精确匹配 n.delta(为空的场合跳过)
  3. 跑 P:在 Y 上跑 n.verify
  全部通过  ->  commit,trailer 记下证据
  任何一条不过 -> **不提交**,逐条给出"要求"

顺序很重要:必须先固定树、再跑 P。否则:

跑 P -> 通过
[这中间工作区又变了]
commit -> 提交的是另一个状态,而 trailer 说它通过了

所以第 3 步用 git commit-tree 精确提交 Y,不是 git commit —— 验证程序留下的临时产物进不了那个 commit。实测: bg-verified-tree 恒等于 commit^{tree}。

三种结果,不是两种

ok        确实满足
fail      确实不满足(附一条"要求",模型能照着修)
unknown   **不知道** —— 观测能力受限

unknown 不算通过。它必须能被表达出来,否则"看不见"会被读成"没问题"。


权限:凭证,像目录权限

持有 X 的凭证  ->  能改 X 的**所有后代**(向下包含)
改 X 自己      ->  需要 parent(X) 的凭证   <- 这就是「向创建者提权」
根节点         ->  没有父,只能由人签发(--as-user)
已达成         ->  **冻结,任何凭证都改不动**

所以一个 agent 拿到一个节点的凭证,就在它下面任意控制; 但要改那个节点本身,得找创建它的那个(它的父)拿凭证。

$ bg plan --as-user --id root ...          # 人建根,拿回 🔑
$ bg plan --token <根凭证> --id n1 --parent root ...   # 拿回 n1 自己的 🔑

凭证明文只在创建那一刻出现一次,之后库里只有它的 hash。 所以泄漏了库也拿不到凭证。

为什么是凭证,不是 agent id

工具拿得到 exec.agent.id,但它不该被当成认证依据 —— 一个 subagent 报上来的身份,工具核验不了。拿它做权限, 等于把一道门建在自己都验证不了的东西上。

凭证绕开这件事:谁拿出凭证谁有权,不问你是谁。

代价要说清:它防的是误伤,不是恶意。一个 agent 想把自己的凭证 递给另一个,没有任何机制拦得住 —— 但递出去这件事会留在账本里。 和"绕过只能发现不能禁止"是同一条线:不拦,但不隐瞒。

冻结优先于凭证

冻结保护的是历史事实(那个 commit 真的被验过), 凭证保护的是"谁能改意图"。这是两件事。

让提权能破冻结,那条绿就变成可以事后修改的东西 —— 而 verified-tree 的全部意义就是它不可事后修改。所以:

✗ 节点 n1 已经**达成**过 —— 门禁冻结,**任何凭证都改不动它**。
  要变,起一个新节点(历史不改写)。

两道门

谁定 谁能改 靠什么
根节点 人 只有人 机器(没有父,凭证只能人签发)
子节点的 Δ / P 创建它的那个 持有父凭证的 凭证 + 可见性
已达成 —— 谁都不能 冻结(绝对)

所以中间节点的绿不承诺任何东西 —— 承诺在根上。 这是设计,不是缺陷。

要变,起一个新节点把它做出来 —— 历史不改写,旧节点保持绿。

可见性是唯一的问责机制,所以它得真的可见

① P 必须跟着版本走(进 trailer),不能只活在工具的运行时
② 弱 P 要能被机器判出来的那种就标出来:
     P 在基线时就通过  ->  ⚠ 它区分不了你做没做
③ 展示时 P 的**原文**要看得见,不能只显示"通过"

不拦,但不隐瞒。这是"模型自决"能站住的唯一方式 —— 自决不等于无痕。


所有节点一视同仁

这里原来叫「向根负责」,已删除。 那个模型是:

节点的验证 = 自己的 (Δ, P)  +  【根门禁的 P】      ← 删掉的
              ↑ 模型自选       ↑ 模型动不了

它建立在一条不变式上:"每一个绿的 commit 上,根门禁都是通过的"。

问题是它把根特殊化了。 而根 P 和子节点的 P 其实是同一种东西 —— 都只是"这件事做对了"的证明:

根节点     P = "整体可用"(如 node test/run.mjs && node test/plugin.mjs)
子节点 n1  P = "n1 这件事做对了"
子节点 n2  P = "n2 这件事做对了"

根不特殊,只是它恰好验的是"整体"。它只在自己提交时跑,不下来压每一个子节点。

原来的做法有两个毛病:

① 它把"整体可用"变成了每个子任务的义务。 而子任务只该对它自己那件事 负责 —— 拿整体去卡一个局部改动,不是这套东西的哲学。 ② "根"被迫特殊化。 取哪个根?多个 parent=null 的节点怎么办? rootNode() 只能 find 第一个 —— 而一个仓库明明可以有任意多个任务根。

所以现在:

任何节点达成时,只跑它自己的 P。根也一样。

代价(说清)

"整体可用"不再是自动保证的。

它变成根的 P 的责任:根节点拿到一个好用的 base,它的 P 保证整体可用; 子任务只管自己那一段。想要整体保证,就建一个 P 是整体验证的节点。

这不是退化,是把那个决定权交回给声明者 —— 什么时候验整体、验多全,由他定。

顺带:bg-root-verify 这个 trailer 字段也不再写了。老节点上的保持原样 (历史不改写),新节点没有它。


证据:commit trailer

让导出支持非 ASCII

bg-node: n7
bg-owner: model
bg-parent: n3
bg-base: 9330fc81…
bg-delta: M:src/csv.py A:src/enc.py
bg-delta-source: before-work
bg-verify: pytest -q
bg-verified-tree: 4db1466a…

为什么要记 verified-tree:commit 不可变,但可以被 amend。

走门禁 commit 了 V(带 trailer:"P 通过")
git commit --amend 改内容(保留 message 和 trailer)
  -> V' 内容变了,trailer 还写着"P 通过"  ->  一个伪造的绿

校验时比对 commit^{tree} == bg-verified-tree,不等就是改过。 (不能记 commit 自己的哈希 —— 鸡生蛋。但树哈希可以在 commit 之前算出来。)

这一条同时让证据可重放:

checkout <tree>
跑 bg-verify 里那条命令
-> 结果应当一致

bg recheck 会扫全部历史 —— 它只看 commit 自己,不依赖 .bg/, 所以图丢了、被换了一份、被人手改过,这条照样抓得住。实测:

✗ 有 1 个 commit 的**证据对不上自己写下的 trailer**:
  9972c5de(节点 n1) 的内容被改过:它的树是 9f012632,
  但它的 trailer 说验过的是 4d21dd80 —— **这是一个伪造的绿**

绕过:只能发现,但很好发现

模型有 bash,没法禁止它裸 git commit。所以不是"禁止",而是"发现":

有 bg-node trailer  ->  走门禁的
没有                ->  **绕过的**
$ bg tree
⚠ 有 2 个提交不在图里(a1b2c3d, e4f5a6b)
    它们没有 bg-node trailer —— 绕过了门禁

统一面 ≠ 强制面,但"绕过会被看见"就够强了。


面

给模型的四个工具

工具 干什么 跑 P 吗
node_plan 声明一个任务(base / expect / Δ? / P) 否
node_status 相对 base 改了什么;和 Δ 比差在哪 否(和 git status 一样便宜)
node_commit 提交即门禁:Δ + P,过了才产生版本 是
node_abandon 把一个声明了但没做的任务从图里移除 否

上一版 25 个(后来收到 6 个)是负担 —— 模型会挑一个差不多的用,或者开始乱试。四个。

"看树"和"合并"都不占模型的工具位:

看树   状态就是版本 -> git log 就是树。模型本来就有 bash。
       而且"看树"是**人的面** —— 人的面不该占模型的工具位。
合并   折进 node_commit —— 检测到 MERGE_HEAD 就走合并分支。
       实测:模型自己 git merge --no-commit 之后由工具提交,
       产出的提交**有两个父**,是一个真正的 merge commit。

但 node_abandon 砍不掉

node_plan 声明一个意图  ->  它必须被记下来(否则人看不见计划)
声明了不做              ->  那条记录必须**能被移除**

否则图里永远挂着一个"待办",而图会看起来像还在做这件事。

但它只做一件事:从图里移除声明。 "把工作区撤回去"不是它的职责 —— 那是 git reset --hard <base>, 模型有 bash,自己就能做。工具只管"图",不管"工作区"。

node_status 是这套东西的使用体验所在

当前版本    4db1466   "让导出支持非 ASCII"
基准版本    9330fc81

相对基准改了 3 个文件
  M  src/export/csv.py
  A  src/export/encoding.py
  D  src/export/legacy.py

和声明的 Δ 比
  ✓ M  src/export/csv.py
  ✓ A  src/export/encoding.py
  ✗ D  src/export/legacy.py     声明了,但**没发生**
  + M  src/util.py              **多出来的 —— 预期外的改动**

最后一行就是"防止预期外的改动"落地的地方。

人的面(CLI,不占模型的工具位)

bg tree       看整棵树(灯 + 状态 + 弱 P 标记 + 图外的提交)
bg html       生成 .bg/tree.html —— 一幅真 DAG 图(**需要联网**)
bg serve      前台实时查看器(默认 127.0.0.1:8731)
bg recheck    对当前 commit 复查各历史节点 —— 诊断,不是灯
bg health     心跳一行
bg log        从 commit trailer 读出来的历史
bg crosscheck 图和 commit 对不对得上

那张图在画什么

bg html 画的是真 git DAG,不是"按某个数值排成一行":

● = commit(只画有节点的点,普通提交压成线)
实线 -->    = git 父子(`git log %P`)—— 这就是主干,分叉/合并原样保留
虚线 -.->   = 意图父子(节点的 parent 字段)—— "谁拆给谁的"

两种线必须分开。 混画会让人以为"拆给谁的"就是"代码从哪儿来的",那是撒谎。

上一版把提交映射成一个标量(git rev-list --count)当坐标 —— 那在分叉历史里 会编造一个顺序。本仓库恰好是纯线性的,所以它看起来是对的,那是最坏的那种错。 现在只画 git 真实给出的父子关系,不映射成"第几个"。test/run.mjs 第 20 节 会另造一个真有分叉的仓库来验这件事 —— 直线历史证明不了任何事。

复查不是灯,是一次查询

一个灯会丢失"回归可见性":

node 5 弄坏了 node 2 的功能
  node 5 的 P 如果没覆盖  ->  它绿
  node 2 的灯是对它那个 commit 的事实  ->  它**还是绿**

补法(和"一个绿灯"不冲突):

bg recheck
  ->  对【当前 commit】重跑各历史节点的 (Δ, P)
  ->  指出谁现在会不通过

它是"看的时候才算"的诊断,不是节点的一个状态。 所以灯还是一个。


合并

合并也是一次提交,所以它也是一个节点。但有两处不同:

没有单一的 base   ->  它有多个父
不用 Δ            ->  无冲突时 Δ 可推导;有冲突时解决是刻意的

所以合并节点的门禁是:

1. 结果必须是**所有**父的后代
2. 每个父的 Δ,在它自己碰过的路径上,合并后仍然成立
3. P 在结果上通过

第 2 条是必需的,而且不是 Δ。 因为:

git merge-base --is-ancestor X_A X_M   ->  ✓ 通过
git diff X_A X_M -- feature.py         ->  M feature.py   ← A 的成果没了

"X_A 是祖先"只证明历史里包含 A,不证明 A 的内容还在。 解决冲突时可以把一个分支的成果整个撤销掉,而祖先检查看不出来。

用法:

git merge --no-commit --no-ff A B     # 有冲突就自己解决(用 bash)
bg commit m1                          # 查上面三条,过了才产生真正的 merge commit

实测(把 A 的成果删掉再"解决"冲突):

✗ nA(3133948) 的 Δ 里 A a.txt 在合并结果里找不到了
  —— **它的成果在解决冲突时被撤销了**

语义冲突没有机械解

A:把 total() 改成返回字符串
B:加了个 report(),依赖 total() 返回数字
-> 0 个冲突文件、两边各自的门禁都过、合并后 report([1,2]) == "33"

这个只能靠第 3 条兜(P 在合并结果上通过),而且 P 得够全。 合并出的绿是新的绿,不是两个绿的相加。


状态在哪里

<项目>/.bg/
├── nodes.json      图(**权威**:节点的当前状态在这里)
└── ledger.jsonl    账本(追加式,不改写)

插件目录里没有状态 —— 它只是代码。所以同一个插件可以服务多个项目。

图和 commit 的分工:

图(.bg/)      当前状态 —— 权威,读得快,可写
commit        做成的证据 —— 不可变,可重放,带 verified-tree 防篡改

绿是"这个节点达成了,并且那次达成的证据还在"。 两者对不上时 bg crosscheck 会报出来。

账本为什么必须是追加式

"达成过"是历史事实。历史要能被引用,前提是它不能被改写。

声明 / 改写  ->  追加一条,带上改前的门禁
达成         ->  追加一条,带上证据(树 / P / 时间)
放弃         ->  追加一条

没有任何一条操作会动已有的行。

指错目录会怎样

每个工具都能显式给 project;不给才退回会话 cwd。 "节点声明在 A 目录、会话在 B 目录"时,不给 project 就会 看不见自己刚声明的节点 —— 这个坑实测过,所以 test/plugin.mjs 有一条守卫逐个检查四个工具都带这个参数。


并行:一个工作区、两个 subagent

治得了的:状态文件

.bg/nodes.json 是读-改-写,两个进程并行时会互相覆盖。实测(修复前):

20 轮并行 declare(每轮 2 个,期望 40 个节点)
  -> 实际只有 20 个,**账本也只有 20 条** —— 丢的那 20 个没有任何痕迹
  -> 而且进程直接崩:ENOENT: rename '.bg/nodes.json.tmp'
     (固定临时名,两个进程抢同一个)

修法两条:

  1. 临时文件名唯一(pid + 随机数)—— 修崩溃
  2. plan 的"读 → 判 → 写 → 记账"整段进排它锁(.bg/.lock,wx 排它创建) —— 修丢节点。锁太久(持有者崩了)会被抢过来,否则一次崩溃会把项目永久锁死

实测修复后:40/40,账本 40 条,零崩溃。

治不了的:并行改同一个工作区时,归属无法确定

工作区是共享的。别人在我干活期间改的东西,会出现在我的 diff 里:

agentA 声明 Δ=src/a.py,agentB 声明 Δ=src/b.py
B 改了自己的 src/b.py
-> A 的 Δ 比对里会多出 "M src/b.py" —— **预期外的改动**

这不是冤枉:从 A 的角度看,那确实是一个它没声明的改动。 工具做的就是照实报出来,由 A 决定"那是别人的,我等它合进来再算"。

所以真要并行,只有三条路:

做法 代价
A. 串行 一次只让一个节点在飞 没有并行,但归属永远清楚
B. 每 agent 一个 worktree git worktree add 给每个 agent 独立的树 真并行;合并要单独设计
C. 并行 + 共享树 就是现在这样 Δ 会把别人的改动报成"预期外"

当前实现是 C,并且它会照实报出来(不是假装知道)。 B 是真正支持并行的方向 —— 每个 agent 有自己的树和自己的 .bg/。没做。

一句话:git 能告诉你"世界变了什么",但一个共享工作区里, 它没法告诉你"是谁改的"。 前者是 git 的能力,后者需要隔离。


目录

butler-git/
├── package.json      **插件包**(main = plugin/index.js)
├── bin/bg.mjs        命令行(人的面)
├── lib/
│   ├── git.mjs       git 原语:树哈希 / diff / commit-tree / 临时索引 / 临时 worktree
│   ├── store.mjs     图 + 追加式账本 + 排它锁
│   ├── delta.mjs     Δ:解析 / 自洽检查 / 三方比对(漏做·预期外·方向错)
│   ├── cap.mjs       凭证:签发 / hash / 向下包含判定
│   ├── gate.mjs      门禁求值:Δ + P -> ok / fail / unknown
│   ├── nodes.mjs     写路径:plan / commit / abandon + 冻结 + 权限 + trailer
│   ├── recheck.mjs   复查(诊断)+ 篡改检测 + 图和 commit 交叉校验
│   ├── view.mjs      渲染(只画,不算)—— 终端版
│   ├── html.mjs      HTML 外壳 + 取数据(buildTree)
│   ├── mermaid.mjs   图 -> mermaid flowchart(网页版;**唯一破了"零依赖"的地方**)
│   └── serve.mjs     前台实时查看器
├── plugin/           DSH 工具插件(四个工具)
└── test/             run.mjs(79 条负向验收)+ plugin.mjs(39 条)

逻辑只有一份:插件和 CLI 都调 lib/。这不是洁癖 —— 上一个实现把核心写了两遍(Python 一份、JS 一份),改了一边另一边静默落后, 于是"模型用的那半边没有新门禁"。test/plugin.mjs 有一条守卫查这件事。


已知边界(诚实的部分)

本节是已经接受的取舍 —— 它们不是 bug,是明码标价的代价。

  1. P 够不够全,判不了。 那是语义问题。 机器只能判"有没有"、"是不是恒真"(弱 P 探测)。
  2. Δ 为空时,没有任何东西防止意外改动。 只有 P 在承重。 —— 会在 status / tree / commit 里明确标出来,不沉默。
  3. 合并的语义冲突没有机械解。 靠合并节点的 P 兜。
  4. 网络副作用完全在观测之外。这是唯一一个无法验证的维度。
  5. verify 会被真的跑起来。 它是模型写的命令,在项目的正常工作区里跑 —— 和跑测试没有区别,但要知道它确实执行了。
  6. 宿主必须有可用的 git。 算不出树哈希时报 unknown,不假装查过。
  7. 凭证防的是误伤,不是恶意。 递出去没机制拦,但会留在账本里。

历史:BUG 清单(已全部处理)

本节的来历:本来要在本仓库上加一个"给人看的节点树可视化"(第三渲染前端)。 它被回退了 —— 因为 BUG-1 让 workingTreeHash 恒为 null, 于是"验收"这件事的证据锚永远是空的。在那种地基上交付任何东西,都没法真诚地验收。

回退是对的。下面 7 条是当时的盘点,现在全部已修。 每条都补了回归验收,所以它们不会再悄悄回来。

# 严重度 位置 一句话 状态
BUG-1 高 lib/git.mjs workingTreeHash 恒返回 null → 任何树判据永远不成立 已修(下面单列)
BUG-2 中 lib/view.mjs 把「从未通过过」说成「通过过,但现在坏了」 已修
BUG-3 中 lib/gate.mjs allow(可写边界)只声明不执行 已被设计取代(Δ 取代了 allow)
BUG-4 中 plugin/index.js 工具没有 project 参数,写死会话 cwd 已修(有守卫)
BUG-5 低 plugin/cordis.patch.yml 注释里的工具数与实现不符 已修(有守卫)
BUG-6 中 部署约定 安装副本是整目录复制,改源码不重装 = 改了但没生效 已写进文档
BUG-7 中 lib/nodes.mjs 没有"放弃一个计划"的原语 已修(node_abandon)

BUG-1(高)树哈希恒为 null

原报告::(exclude) 在 git 2.50.1 上"能排除文件,不能排除目录"。

核实结果:第二半不成立。 实测未跟踪目录排得掉。但真正的病根被找到了,而且更隐蔽:

git add -A -- . ':(exclude).bg'      // ← 原来这一句

:(exclude) 本身没问题。问题在于 git add 会先拿 pathspec 去匹配 .bg, 撞上 .gitignore 里的 .bg/ 就直接报错退出:

The following paths are ignored by one of your .gitignore files: .bg
exit=1

于是凡是把 .bg/ 写进 .gitignore 的仓库(也就是按文档推荐的写法), 树哈希恒为 null。

修法(两步,两种仓库都实测稳定):

git add -A -- .                            # 不再显式匹配 .bg,不会报错
git rm -r --cached --ignore-unmatch .bg     # 从**索引**里抹掉它

为什么原来的测试没抓到:所有临时测试仓库都没有 .gitignore —— 也就是恰好走的是那条"能工作"的路。这就是测试覆盖的缺口本身。

这段教训仍然活在 lib/git.mjs 的注释里 —— 它是这个设计里 "观测者不能出现在被观测对象里"这条规则的来源。

BUG-6(中)安装副本不会随源码更新

nodeLinker: hoisted 下 file: 依赖是整目录复制,不是软链。 而 patchReload: "live" 只重载组合层的 cordis.patch.yml,不重载 JS。

⟹ 改完源码必须 pnpm install(在 profile 目录里), 否则"改了但没生效"且没有任何提示。

—/ 5

No ratings yet

Verified DSH bundle

Commit 1e56da44019b

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout