dsh-my-go 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/dist/client.core.js +50 -24
- package/dist/client.js +50 -24
- package/docs/ARCHITECTURE.md +14 -9
- package/docs/FORK-GUIDE.md +159 -0
- package/lib/index.js +181 -45
- package/package.json +2 -2
- package/preset/agent.cordis.yml +8 -0
- package/preset/tool-mask.mjs +27 -0
- package/preset/tools/broker.mjs +164 -35
- package/prompts/hephaestus.md +1 -0
- package/prompts/hermes.md +3 -2
- package/prompts/sisyphus.md +20 -6
- package/src/client.js +43 -24
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# dsh-my-go(Tisitan fork)架构导览
|
|
2
|
+
|
|
3
|
+
> 面向使用者的「逻辑图 + 文件目录 + 机制映射」说明:系统改成什么样了、
|
|
4
|
+
> 每个功能由哪个文件通过什么原理实现。配套修复台账见 [CHANGELOG.md](../CHANGELOG.md)。
|
|
5
|
+
|
|
6
|
+
## 一、全景逻辑图
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
┌──────────────────────── 同一 Node 进程(同一棵 cordis fiber 树)────────────────────────┐
|
|
10
|
+
│ │
|
|
11
|
+
用户 ──► DSH WebUI ──► │ 【client 半】dist/client.js(React 插件) │
|
|
12
|
+
(浏览器) │ ├─ 🧭 侧栏按钮 ──► shell.overlay 树状图面板(current/queue/help/history) │
|
|
13
|
+
│ ├─ 设置页「MyGO 编排」(8 工种 × provider/model/effort/dsv4p0813) │
|
|
14
|
+
│ └─ 600ms 轮询 ──┐ │
|
|
15
|
+
│ ▼ RPC call('/dsh-my-go', endpoint) │
|
|
16
|
+
│ 【host 半】lib/index.js(profile bundle,global 层注册) │
|
|
17
|
+
│ ├─ connection.rpc.handle('/dsh-my-go') │
|
|
18
|
+
│ │ ├─ snapshot ──► 优先读 Symbol.for('dsh-my-go.snapshot') 全局桥 ──────┐ │
|
|
19
|
+
│ │ │ (桥不在 → 回落自身状态机,非 MyGO 会话用) │ │
|
|
20
|
+
│ │ ├─ loadSettings / saveSettings ──► settings 命名空间 'dsh-my-go' │ │
|
|
21
|
+
│ │ └─ listModels ──► llm.listProviders/listModels │ │
|
|
22
|
+
│ ├─ ensurePresetInstalled:按版本标记同步 preset/ 到 ~/.dsh/.agent-presets/│ │
|
|
23
|
+
│ └─ fallback 编排全家桶(工具+状态机,非 MyGO 会话生效) │ │
|
|
24
|
+
│ │ 实时读 │
|
|
25
|
+
│ 【agent 半】preset/tools/broker.mjs(MyGO preset 装配时加载,preset 层注册)◄┘(零副本)│
|
|
26
|
+
│ ├─ Orchestration 状态机(编排真源) │ │
|
|
27
|
+
│ │ currentMap(单槽) / queue(FIFO) / helpRequests / history(≤200) │ │
|
|
28
|
+
│ │ ▲ 每次迁移 bump() ──► 发布 latestSnapshot 到 Symbol.for 全局桥 ──┘ │
|
|
29
|
+
│ ├─ 6 工具:go_work / continue / need_help / forward / │
|
|
30
|
+
│ │ orchestration_status / list_subagents(preset 层覆盖 global 同名) │
|
|
31
|
+
│ ├─ systemPrompt 注入:Sisyphus persona + 编排规则(主会话)/ 工种 persona(子代理) │
|
|
32
|
+
│ ├─ agent/request waterfall:按工种绑定 provider/model/reasoningEffort │
|
|
33
|
+
│ ├─ agent/created:拓扑闸(子代理禁派生)+ skill 隐藏(主会话) │
|
|
34
|
+
│ └─ subagent/end:结论落账 + 队列推进;agent|session/disposed:状态回收 │
|
|
35
|
+
│ │ │
|
|
36
|
+
│ ▼ ctx.subagents.startContinuable / followup / reportFrom │
|
|
37
|
+
│ 【DSH 内核】subagents 服务 ──► 7 种 continuable 子代理会话(独立 Session,可续接) │
|
|
38
|
+
└────────────────────────────────────────────────────────────────────────────────────────┘
|
|
39
|
+
|
|
40
|
+
工具可见性规则(dsh-scope 合并语义):global 层(lib 注册)+ preset 层(broker 注册),
|
|
41
|
+
同名时「最近的 scope 胜出」→ MyGO 会话用 broker 的工具;其他会话用 lib 的工具。互不串台。
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 二、文件目录(fork 现状)
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
dsh-my-go/
|
|
48
|
+
├── package.json # 包声明;版本 0.2.3-tisitan.4;test = 冒烟 + 单测
|
|
49
|
+
├── cordis.patch.yml # bundle patch:dsh plugin add 后自动把 lib 挂进 profile(global 层)
|
|
50
|
+
├── CHANGELOG.md # fork 修复台账(相对上游的全部差异)
|
|
51
|
+
├── README.md # 项目说明(含 fork 标识段)
|
|
52
|
+
│
|
|
53
|
+
├── lib/
|
|
54
|
+
│ └── index.js # 【host 半】settings 命名空间 + RPC 桥 + preset 同步器
|
|
55
|
+
│ # + global 层 fallback 编排(工具/状态机/模型绑定)
|
|
56
|
+
│
|
|
57
|
+
├── preset/ # agent preset「MyGO!!!!! 模式」(被同步到 ~/.dsh/.agent-presets/)
|
|
58
|
+
│ ├── preset.yml # preset 元信息(名称/排序)
|
|
59
|
+
│ ├── agent.cordis.yml # agent 平面组合:DSH 官方工具行 + 本地 broker 行 + tool-mask 行
|
|
60
|
+
│ ├── tool-mask.mjs # 工具屏蔽(nova 配方同步):藏 Rei 记忆工具 + OpenCode 桥 ×7
|
|
61
|
+
│ └── tools/
|
|
62
|
+
│ └── broker.mjs # 【agent 半 · 编排真源】状态机 + 6 工具 + prompt 注入
|
|
63
|
+
│ # + 模型绑定 + 拓扑闸 + 快照桥发布(★ 大部分修复在这里)
|
|
64
|
+
│
|
|
65
|
+
├── prompts/ # 8 个工种 persona(broker.mjs 运行时读取并注入)
|
|
66
|
+
│ ├── sisyphus.md # 总调度+质检官(persona 段进 deployment:persona,
|
|
67
|
+
│ │ # 「## 编排规则」之后进 dsh-my-go:orchestration section)
|
|
68
|
+
│ ├── hermes.md # 快速执行(指令明确的体力活)
|
|
69
|
+
│ ├── explore.md # 快速检索(只读)
|
|
70
|
+
│ ├── librarian.md # 文档查询
|
|
71
|
+
│ ├── looker.md # 多模态识别
|
|
72
|
+
│ ├── hephaestus.md # 代码编写
|
|
73
|
+
│ ├── prometheus.md # 需求规划(流程开始一次)
|
|
74
|
+
│ └── oracle.md # 疑难/终验兜底
|
|
75
|
+
│
|
|
76
|
+
├── src/
|
|
77
|
+
│ └── client.js # 【client 半源码】面板/设置页/自动跳转(React.createElement 手写)
|
|
78
|
+
├── scripts/
|
|
79
|
+
│ └── build-client.mjs # esbuild 打包:src/client.js → dist/client.js(CJS + ModuleLoader 包装)
|
|
80
|
+
├── dist/ # 构建产物(gitignore,发布/构建时生成)
|
|
81
|
+
│
|
|
82
|
+
├── test/
|
|
83
|
+
│ ├── apply.mjs # 冒烟:模块可加载 + client 可解析 + dist 存在
|
|
84
|
+
│ ├── orchestration.test.mjs# 状态机 12 单测(占位占锁/revive/requeue/幽灵求助/上限…)
|
|
85
|
+
│ └── bridge.test.mjs # 快照桥 2 集成测试(Symbol.for 发布/读取)
|
|
86
|
+
│
|
|
87
|
+
├── broker/ # ⚠️ 归档的 TS 参考实现(见 broker/README.md),不参与构建运行
|
|
88
|
+
├── docs/
|
|
89
|
+
│ ├── ARCHITECTURE.md # 原始架构设计(已与实现同步)
|
|
90
|
+
│ └── FORK-GUIDE.md # 本文档
|
|
91
|
+
└── AGENTS.md # 编排规格书(设计哲学 + 通信协议 + 禁止事项)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 三、机制映射(什么功能 → 哪个文件 → 怎么实现)
|
|
95
|
+
|
|
96
|
+
### 调度与编排
|
|
97
|
+
|
|
98
|
+
| 功能 | 实现位置 | 原理 |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| 派发子代理(go_work) | `preset/tools/broker.mjs` `dispatchWork()` | 检查 `isBusy()` → 忙则 `enqueue()` 排队(返回 `work-*` 占位 id);闲则 `beginSpawning()` 占位占锁(同步原子)→ `ctx.subagents.startContinuable()` 创建持久子会话 → `bindChild()` 绑定真实 childId |
|
|
101
|
+
| 单线阻塞 | `broker.mjs` `Orchestration.currentMap` | 单槽 Map;`isBusy()` 到 `beginSpawning()` 之间无 await,Node 单线程下天然原子 |
|
|
102
|
+
| 队列推进 | `broker.mjs` `advanceQueue()` | `subagent/end` 或 spawn 失败时触发:dequeue 队首 → dispatch;**失败自动 `requeueHead()` 回补**(fork 修复:任务不再蒸发) |
|
|
103
|
+
| 求助挂起(need_help) | `broker.mjs` need_help 工具 | `suspend()` 标记 waiting + `reportFrom` 把求助单注入 Sisyphus 下一步。注:台账层挂起,无强制 interrupt(评估结论见第五节) |
|
|
104
|
+
| 驳回/追问(continue) | `broker.mjs` continue 工具 | **先 `followup` 投递成功,后落账**(fork 修复时序病);目标 waiting 则 resolveHelp+resume;目标已结束则 `revive()` 重新入册 + 恢复 sessionTypes 登记(fork 修复:结论不再丢失、单线不再被打破) |
|
|
105
|
+
| 转发(forward) | `broker.mjs` forward 工具 | 同上「先投递后销账」;target 为工种名时等效 go_work,为 childId 时等效 continue |
|
|
106
|
+
| 结论回流 | DSH 内核通知 + `broker.mjs` `subagent/end` | 子会话结束时内核通知父会话(broker 不重复注入);broker 在 `subagent/end` 里 `finish()` 落账 → 清该子的求助单 → 删 sessionTypes → 推进队列。快速死亡的子会话(resolve 前就 end)归因到唯一 spawning 占位记录(fork 修复竞态冻结) |
|
|
107
|
+
| 状态回收 | `broker.mjs` `agent/disposed` / `session/disposed` 钩子 | 子代理被销毁但错过 end 事件:兜底清槽防队列冻结;Sisyphus 会话被删:丢弃其排队任务 |
|
|
108
|
+
|
|
109
|
+
### 模型与提示词
|
|
110
|
+
|
|
111
|
+
| 功能 | 实现位置 | 原理 |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| 按工种绑模型 | `broker.mjs` `dispatchWork()`(创建时 `agentOptions`)+ `agent/request` waterfall(请求时兜底) | 创建前 `modelExists()` 用 `llm.listModels` 验证模型真实存在才应用;只缓存非空结果(fork 修复负缓存中毒) |
|
|
114
|
+
| reasoningEffort | `broker.mjs` `supportedEfforts()` | 查 DSH 模型目录 `llm.resolveModelInfo`,**仅当模型实际支持该档位才设置**,否则留空走适配器默认(拒绝硬映射) |
|
|
115
|
+
| Sisyphus persona/规则 | `broker.mjs` 两个 `systemPrompt.section` | 读 `prompts/sisyphus.md`,按 `## 编排规则` 切两半:前段进 `deployment:persona`、后段进 `dsh-my-go:orchestration`;子代理会话返回空串(靠 parentSession 判定) |
|
|
116
|
+
| 子代理 persona | `broker.mjs` `dispatchWork()` | 把对应 `prompts/<type>.md` 全文包进 `<system-reminder>` 随首条 prompt 注入 |
|
|
117
|
+
| DSV4P0813 两阶段 | `broker.mjs` `system-prompt/assemble` 监听 | 开启该开关的工种:phase-1 只放行 persona section + 白名单工具(`bash/pwsh/read/write/edit/glob/grep`,fork 已修正为 DSH 真实工具名);首次 tool call 或 turn 结束后晋升放开全部 |
|
|
118
|
+
| skill 隐藏 | `broker.mjs` `agent/created` | 主会话 `tools.restrict({ deny: ['skill'] })`,使 skill catalog 注入守门失效,节省主会话上下文;子代理保留 |
|
|
119
|
+
|
|
120
|
+
### 安全与边界
|
|
121
|
+
|
|
122
|
+
| 功能 | 实现位置 | 原理 |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| 编排权限 | `broker.mjs` `canOrchestrate()` | go_work/continue/forward 仅「无 parentSession 的会话」可调(工具层强制,不靠 prompt 自觉);need_help 仅被跟踪的子代理可调 |
|
|
125
|
+
| 星型拓扑闸(fork 新增) | `broker.mjs` + `lib/index.js` `agent/created` | 子代理在工具目录层被摘除 `subagent/subagent_fork/workflow/ralph/go_work/continue/forward`——无法私自派生孙代,也无法直接调度(与运行时守卫双保险) |
|
|
126
|
+
| 沙箱外执行通道 | need_help `intent=execute` | 子代理把被拒命令发给 Sisyphus 代执行。注意:这是设计的权限提升通道,缓解靠 Sisyphus 的质检 prompt |
|
|
127
|
+
|
|
128
|
+
### UI 与配置
|
|
129
|
+
|
|
130
|
+
| 功能 | 实现位置 | 原理 |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| 树状图面板 | `src/client.js` `TreePanel` | `shell.overlay` 浮层 + 侧栏 🧭 开关;600ms 轮询 RPC `snapshot` 端点,seq 变化才重渲染(fork 修复:开关脱钩 + force bailout) |
|
|
133
|
+
| 快照桥(fork 新增) | `broker.mjs` 发布 ↔ `lib/index.js` RPC 消费 | broker 把 `() => latestSnapshot` 挂到 `globalThis[Symbol.for('dsh-my-go.snapshot')]`;lib 的 RPC handler 优先实时读取(零副本),桥不在则回落自身状态机 |
|
|
134
|
+
| 自动跳转 | `src/client.js` 定时器 | 子代理 running → `sessions.openSubagent()` 跟跳子会话;结束后 `sessions.open(parentSessionId)` 跳回(fork 补全跳回闭环 + 快照补上 parentSessionId) |
|
|
135
|
+
| 设置页 | `src/client.js` `SettingsPage` ↔ `lib/index.js` RPC | 8 工种 × 4 字段;loadSettings 失败时 `draft=null` 禁止保存(fork 修复:不再一键清空配置);saveSettings 空值 unset、显式 false 可表达(fork 修复) |
|
|
136
|
+
| settings 合并 | `broker.mjs` / `lib/index.js` | 永远从 `baseBindings`(默认值+插件 config)起算合并 stored(fork 修复:WebUI 取消配置可回落);`||` 语义统一(空串=未设置) |
|
|
137
|
+
| preset 同步 | `lib/index.js` `ensurePresetInstalled()` | 版本标记文件 `.dsh-my-go-version`:版本不变则跳过(fork 修复:不再每次强制覆盖用户手改) |
|
|
138
|
+
|
|
139
|
+
### 测试
|
|
140
|
+
|
|
141
|
+
| 层 | 文件 | 覆盖 |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| 冒烟 | `test/apply.mjs` | 模块加载/导出面/client 语法/dist 存在 |
|
|
144
|
+
| 单测 | `test/orchestration.test.mjs` | 状态机 12 例:占锁原子性、bindChild、finish 清求助、suspend/resume、revive、requeueHead、dropQueuedFor、history 200 上限、record/followupPrompt |
|
|
145
|
+
| 集成 | `test/bridge.test.mjs` | mock cordis ctx 跑 `broker.apply()`,验证 Symbol.for 快照访问器发布且为实时 getter |
|
|
146
|
+
|
|
147
|
+
## 四、fork 与上游的关系
|
|
148
|
+
|
|
149
|
+
- `upstream` = [daizihan233/dsh-my-go](https://github.com/daizihan233/dsh-my-go)(本 fork 基于 v0.2.3 @ cf2d802)
|
|
150
|
+
- 版本号规则:`上游版本-tisitan.N`;完整差异台账见 [CHANGELOG.md](../CHANGELOG.md)
|
|
151
|
+
- 同步策略:定期 `git fetch upstream`,交集分析上游新提交与本 fork 补丁面,定点合并
|
|
152
|
+
|
|
153
|
+
## 五、评估过但**未实施**的升级(含结论)
|
|
154
|
+
|
|
155
|
+
| 候选 | 结论 | 原因 |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| need_help 改真 interrupt 硬挂起 | **暂缓** | `ctx.subagents.interrupt(childId, { kind: 'ancestor', agent: parentAgent })` API 存在(dsh-subagent/lib/types/continuation.d.ts:232),但中断时机(reportFrom 送达前杀回合会丢求助单)、恢复后模型如何感知被取消的 tool call,均需运行时实测验证。当前软挂起下:乱跑的子代理无法破坏队列(单线由状态机强制),仅浪费自身 token,残余风险可接受 |
|
|
158
|
+
| isolate 服务 + `agentPresets.serviceFor` 官方桥 | **暂缓** | 需把 broker 拆成 isolate realm 服务行 + root 消费行(mount 审计强制 preset 服务入 isolate,而 broker 的 systemPrompt.section 必须留在 agent scope),重构面大且挂载行为无法离线验证。当前 Symbol.for 桥在拉取路径上功能等价(同进程、零副本、实时读) |
|
|
159
|
+
| projection 推流替代 600ms 轮询 | **未来方向** | `session.append('mygo/state', 全量快照)` + `sessionProjections.register` + client `useProjection`(dsh-goal 官方范式),可解锁推流与冷会话回放。需 session 级事件写入,等运行时环境验证后实施 |
|
package/lib/index.js
CHANGED
|
@@ -18,7 +18,7 @@ export const name = 'dsh-my-go'
|
|
|
18
18
|
|
|
19
19
|
export const inject = ['tools', 'subagents', 'systemPrompt', 'llm', 'settings']
|
|
20
20
|
|
|
21
|
-
import { access, cp, mkdir } from 'node:fs/promises'
|
|
21
|
+
import { access, cp, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
22
22
|
import { dirname, join } from 'node:path'
|
|
23
23
|
import { fileURLToPath } from 'node:url'
|
|
24
24
|
import { homedir } from 'node:os'
|
|
@@ -30,7 +30,9 @@ const AGENT_TYPES = ['hermes', 'explore', 'librarian', 'looker', 'hephaestus', '
|
|
|
30
30
|
* "MyGO!!!!! 模式" preset appears in the session picker after `dsh plugin
|
|
31
31
|
* add dsh-my-go`. DSH discovers presets only from configured roots
|
|
32
32
|
* (~/.dsh/.agent-presets/), never from node_modules, so the npm bundle must
|
|
33
|
-
* copy its preset/ directory there. Idempotent:
|
|
33
|
+
* copy its preset/ directory there. Idempotent: synced only when the package
|
|
34
|
+
* version changes (marker file `.dsh-my-go-version`), so manual tweaks to the
|
|
35
|
+
* installed preset survive same-version reloads.
|
|
34
36
|
* Failures are logged and swallowed — the host plugin must keep working even
|
|
35
37
|
* when the preset copy is not possible.
|
|
36
38
|
*/
|
|
@@ -41,6 +43,17 @@ async function ensurePresetInstalled() {
|
|
|
41
43
|
const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
42
44
|
const userPresetRoot = join(dshHome, '.agent-presets')
|
|
43
45
|
const target = join(userPresetRoot, 'dsh-my-go')
|
|
46
|
+
const markerPath = join(target, '.dsh-my-go-version')
|
|
47
|
+
// Version marker: skip sync when the installed copy matches this package
|
|
48
|
+
let version = '0.0.0'
|
|
49
|
+
try {
|
|
50
|
+
const pkg = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf-8'))
|
|
51
|
+
version = String(pkg.version ?? version)
|
|
52
|
+
} catch { /* fall through with default */ }
|
|
53
|
+
try {
|
|
54
|
+
const installed = (await readFile(markerPath, 'utf-8')).trim()
|
|
55
|
+
if (installed === version) return // already synced for this version
|
|
56
|
+
} catch { /* no marker → first install or legacy copy: sync below */ }
|
|
44
57
|
await mkdir(userPresetRoot, { recursive: true })
|
|
45
58
|
// Sync preset/ directory (composition + tools)
|
|
46
59
|
const presetSource = join(packageRoot, 'preset')
|
|
@@ -53,7 +66,8 @@ async function ensurePresetInstalled() {
|
|
|
53
66
|
await access(promptsSource)
|
|
54
67
|
await cp(promptsSource, promptsTarget, { recursive: true, force: true })
|
|
55
68
|
} catch { /* prompts/ optional — degrade gracefully */ }
|
|
56
|
-
|
|
69
|
+
await writeFile(markerPath, version, 'utf-8')
|
|
70
|
+
console.log(`[dsh-my-go] preset synced to ${target} (v${version})`)
|
|
57
71
|
} catch (error) {
|
|
58
72
|
console.error(`[dsh-my-go] could not sync preset: ${String(error)}`)
|
|
59
73
|
}
|
|
@@ -65,13 +79,6 @@ function agentLabel(type, summary) {
|
|
|
65
79
|
return `${AGENT_TYPE_PREFIX}${type}${summary ? `: ${summary}` : ''}`
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
function parseAgentType(label) {
|
|
69
|
-
if (typeof label !== 'string') return undefined
|
|
70
|
-
const match = /^dsh-my-go:([a-z-]+)/.exec(label)
|
|
71
|
-
if (!match) return undefined
|
|
72
|
-
return AGENT_TYPES.includes(match[1]) ? match[1] : undefined
|
|
73
|
-
}
|
|
74
|
-
|
|
75
82
|
/** Default bindings per AGENTS.md. Provider stays unset for light agents so
|
|
76
83
|
* they inherit Sisyphus's route; heavy agents pin the octopus gateway.
|
|
77
84
|
* reasoningEffort is only ever applied when the exact model supports that
|
|
@@ -215,12 +222,51 @@ class Orchestration {
|
|
|
215
222
|
updatedAt: Date.now(),
|
|
216
223
|
}
|
|
217
224
|
this.currentMap.delete(childId)
|
|
225
|
+
this.clearHelpFor(childId)
|
|
218
226
|
this.history = [...this.history, done]
|
|
219
227
|
if (this.history.length > 200) this.history = this.history.slice(-200)
|
|
220
228
|
this.emit()
|
|
221
229
|
return done
|
|
222
230
|
}
|
|
223
231
|
|
|
232
|
+
clearHelpFor(childId) {
|
|
233
|
+
let removed = false
|
|
234
|
+
for (const [id, help] of this.helpRequests) {
|
|
235
|
+
if (help.childId === childId) {
|
|
236
|
+
this.helpRequests.delete(id)
|
|
237
|
+
removed = true
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (removed) this.emit()
|
|
241
|
+
return removed
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
requeueHead(work) {
|
|
245
|
+
if (!work) return
|
|
246
|
+
this.queue.unshift(work)
|
|
247
|
+
this.emit()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
dropQueuedFor(parentId) {
|
|
251
|
+
const before = this.queue.length
|
|
252
|
+
this.queue = this.queue.filter((w) => w.parentId !== parentId)
|
|
253
|
+
if (this.queue.length !== before) this.emit()
|
|
254
|
+
return before - this.queue.length
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Move a done/failed history record back into currentMap as running (revive via continue/forward). */
|
|
258
|
+
revive(childId) {
|
|
259
|
+
if (this.currentMap.has(childId)) return this.currentMap.get(childId)
|
|
260
|
+
const idx = this.history.findIndex((r) => r.childId === childId)
|
|
261
|
+
if (idx < 0) return undefined
|
|
262
|
+
const rec = this.history[idx]
|
|
263
|
+
const next = { ...rec, status: 'running', updatedAt: Date.now() }
|
|
264
|
+
this.history = [...this.history.slice(0, idx), ...this.history.slice(idx + 1)]
|
|
265
|
+
this.currentMap.set(childId, next)
|
|
266
|
+
this.emit()
|
|
267
|
+
return next
|
|
268
|
+
}
|
|
269
|
+
|
|
224
270
|
abort(childId) {
|
|
225
271
|
this.currentMap.delete(childId)
|
|
226
272
|
this.emit()
|
|
@@ -255,7 +301,10 @@ export async function apply(ctx, config = {}) {
|
|
|
255
301
|
void ensurePresetInstalled()
|
|
256
302
|
const orchestration = new Orchestration()
|
|
257
303
|
const sessionTypes = new Map()
|
|
258
|
-
|
|
304
|
+
// 合并基线:默认值 + 插件 config。settings 覆盖永远从基线起算,
|
|
305
|
+
// 这样 WebUI 取消某字段后能正确回落默认,而不是残留旧的已合并值。
|
|
306
|
+
const baseBindings = { ...defaultBindings(), ...(config.bindings ?? {}) }
|
|
307
|
+
let bindings = { ...baseBindings }
|
|
259
308
|
const bindSisyphus = config.bindSisyphus === true
|
|
260
309
|
|
|
261
310
|
// Track authorized orchestrators: any agent on this preset that is NOT
|
|
@@ -297,14 +346,14 @@ export async function apply(ctx, config = {}) {
|
|
|
297
346
|
)
|
|
298
347
|
const stored = settings.get('dsh-my-go')
|
|
299
348
|
if (stored && typeof stored === 'object') {
|
|
300
|
-
const merged = { ...
|
|
349
|
+
const merged = { ...baseBindings }
|
|
301
350
|
for (const key of ['sisyphus', ...AGENT_TYPES]) {
|
|
302
351
|
const row = stored[key]
|
|
303
352
|
if (row && typeof row === 'object') {
|
|
304
353
|
merged[key] = {
|
|
305
|
-
provider: row.provider
|
|
306
|
-
model: row.model
|
|
307
|
-
reasoningEffort: row.reasoningEffort
|
|
354
|
+
provider: row.provider || merged[key]?.provider,
|
|
355
|
+
model: row.model || merged[key]?.model,
|
|
356
|
+
reasoningEffort: row.reasoningEffort || merged[key]?.reasoningEffort,
|
|
308
357
|
dsv4p0813: row.dsv4p0813 ?? merged[key]?.dsv4p0813 ?? false,
|
|
309
358
|
}
|
|
310
359
|
}
|
|
@@ -315,14 +364,14 @@ export async function apply(ctx, config = {}) {
|
|
|
315
364
|
if (ns !== 'dsh-my-go') return
|
|
316
365
|
const next = settings.get('dsh-my-go')
|
|
317
366
|
if (next && typeof next === 'object') {
|
|
318
|
-
const merged = { ...
|
|
367
|
+
const merged = { ...baseBindings }
|
|
319
368
|
for (const key of ['sisyphus', ...AGENT_TYPES]) {
|
|
320
369
|
const row = next[key]
|
|
321
370
|
if (row && typeof row === 'object') {
|
|
322
371
|
merged[key] = {
|
|
323
|
-
provider: row.provider
|
|
324
|
-
model: row.model
|
|
325
|
-
reasoningEffort: row.reasoningEffort
|
|
372
|
+
provider: row.provider || merged[key]?.provider,
|
|
373
|
+
model: row.model || merged[key]?.model,
|
|
374
|
+
reasoningEffort: row.reasoningEffort || merged[key]?.reasoningEffort,
|
|
326
375
|
dsv4p0813: row.dsv4p0813 ?? merged[key]?.dsv4p0813 ?? false,
|
|
327
376
|
}
|
|
328
377
|
}
|
|
@@ -339,9 +388,11 @@ export async function apply(ctx, config = {}) {
|
|
|
339
388
|
// NOT harness.handle, which is reserved for dynamic cordis plugins) ──────
|
|
340
389
|
let latestSnapshot = null
|
|
341
390
|
let snapshotSeq = 0
|
|
391
|
+
// Track the last orchestrator session for queue fallback and client auto-jump.
|
|
392
|
+
let lastOrchestratorSessionId = null
|
|
342
393
|
const bump = () => {
|
|
343
394
|
snapshotSeq += 1
|
|
344
|
-
latestSnapshot = { seq: snapshotSeq, ...orchestration.snapshot() }
|
|
395
|
+
latestSnapshot = { seq: snapshotSeq, parentSessionId: lastOrchestratorSessionId, ...orchestration.snapshot() }
|
|
345
396
|
}
|
|
346
397
|
orchestration.onChange(() => bump())
|
|
347
398
|
|
|
@@ -354,7 +405,11 @@ export async function apply(ctx, config = {}) {
|
|
|
354
405
|
// channel = "/dsh-my-go", endpoints = "snapshot" | "listModels"
|
|
355
406
|
rpc.handle('/dsh-my-go', async (endpoint, payload) => {
|
|
356
407
|
if (endpoint === 'snapshot') {
|
|
357
|
-
|
|
408
|
+
// 优先读 agent 平面 broker 发布的真实编排快照(Symbol.for 全局桥);
|
|
409
|
+
// 桥不存在时回落到本实例自己的状态机(preset 未装配的部署形态)。
|
|
410
|
+
const shared = globalThis[Symbol.for('dsh-my-go.snapshot')]
|
|
411
|
+
const value = typeof shared === 'function' ? shared() : latestSnapshot
|
|
412
|
+
return { ok: true, value: value ?? { seq: 0, current: null, queue: [], helpRequests: [], history: [] } }
|
|
358
413
|
}
|
|
359
414
|
if (endpoint === 'listModels') {
|
|
360
415
|
const llm = ctx.get('llm')
|
|
@@ -397,7 +452,7 @@ export async function apply(ctx, config = {}) {
|
|
|
397
452
|
for (const type of agentTypes) {
|
|
398
453
|
for (const field of fields) {
|
|
399
454
|
const val = draft[type]?.[field]
|
|
400
|
-
if (val === undefined || val === null || val === ''
|
|
455
|
+
if (val === undefined || val === null || val === '') {
|
|
401
456
|
ops.push({ op: 'unset', path: [type, field] })
|
|
402
457
|
} else {
|
|
403
458
|
ops.push({ op: 'set', path: [type, field], value: val })
|
|
@@ -415,17 +470,33 @@ export async function apply(ctx, config = {}) {
|
|
|
415
470
|
})
|
|
416
471
|
|
|
417
472
|
// ── internal go_work implementation (shared by the tool, forward, queue) ─
|
|
473
|
+
// 队列推进:取出队首并派发;派发失败时回补队首——任务不蒸发、队列不停摆。
|
|
474
|
+
function advanceQueue(parentHint) {
|
|
475
|
+
if (orchestration.isBusy()) return
|
|
476
|
+
const work = orchestration.dequeue()
|
|
477
|
+
if (!work) return
|
|
478
|
+
const agents = ctx.get('agents')
|
|
479
|
+
const parentAgent = (work.parentId && agents ? agents.get(work.parentId) : undefined) ?? parentHint
|
|
480
|
+
void dispatchWork(work.agentType, work.prompt, parentAgent, undefined).catch((error) => {
|
|
481
|
+
orchestration.requeueHead(work)
|
|
482
|
+
bump()
|
|
483
|
+
console.error('[dsh-my-go] queued dispatch failed, task requeued:', error)
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
418
487
|
async function dispatchWork(agentType, prompt, parent, signal) {
|
|
419
488
|
if (!AGENT_TYPES.includes(agentType)) throw new Error(`unknown agent type: ${String(agentType)}`)
|
|
420
489
|
const binding = bindings[agentType] ?? {}
|
|
421
490
|
// Parent may be absent during queue advancement (agent object not retained);
|
|
422
|
-
// fall back to the
|
|
491
|
+
// fall back to the last orchestrator session so delegation still works.
|
|
492
|
+
// (Do NOT use agents.roots()[0] — that leaks queued work into other sessions.)
|
|
423
493
|
if (!parent) {
|
|
424
494
|
const agents = ctx.get('agents')
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
495
|
+
const fallback = lastOrchestratorSessionId ? agents?.get?.(lastOrchestratorSessionId) : undefined
|
|
496
|
+
if (fallback) parent = fallback
|
|
427
497
|
}
|
|
428
498
|
if (!parent) throw new Error('go_work requires a live parent agent to delegate from')
|
|
499
|
+
lastOrchestratorSessionId = parent.id
|
|
429
500
|
if (orchestration.isBusy()) {
|
|
430
501
|
const workId = orchestration.enqueue(agentType, prompt, parent?.id)
|
|
431
502
|
bump()
|
|
@@ -455,6 +526,8 @@ export async function apply(ctx, config = {}) {
|
|
|
455
526
|
} catch (error) {
|
|
456
527
|
orchestration.abort(placeholder.childId)
|
|
457
528
|
bump()
|
|
529
|
+
// 槽位已腾出:立即推进队首,避免后续排队任务永久等待
|
|
530
|
+
advanceQueue(parent)
|
|
458
531
|
throw new Error(`go_work failed: ${String(error)}`)
|
|
459
532
|
}
|
|
460
533
|
}
|
|
@@ -468,6 +541,7 @@ export async function apply(ctx, config = {}) {
|
|
|
468
541
|
...AGENT_TYPES.map((t) => `- ${t}: ${describeAgent(t)}`),
|
|
469
542
|
'Single-line blocking: if a sub-agent is already running, this task is queued and starts when the current one finishes.',
|
|
470
543
|
'The result contains a childId you keep for later continue/forward operations.',
|
|
544
|
+
'If the task was queued (queued=true), the returned id is a queue placeholder (work-*), NOT a childId — once dispatched, find the real childId via orchestration_status.',
|
|
471
545
|
].join('\n'),
|
|
472
546
|
parameters: {
|
|
473
547
|
type: 'object',
|
|
@@ -528,17 +602,33 @@ export async function apply(ctx, config = {}) {
|
|
|
528
602
|
if (!parent) throw new Error('continue requires a calling agent (exec.agent was undefined)')
|
|
529
603
|
if (!canOrchestrate(parent)) throw new Error('continue is reserved for orchestrator sessions (agents without parentSession)')
|
|
530
604
|
const record = orchestration.record(args.id)
|
|
531
|
-
if (!record)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
605
|
+
if (!record) {
|
|
606
|
+
const queued = orchestration.snapshot().queue.find((w) => w.id === args.id)
|
|
607
|
+
if (queued) {
|
|
608
|
+
throw new Error(`task ${String(args.id)} (${queued.agentType}) is still queued — wait for dispatch, then use its real childId (see orchestration_status)`)
|
|
535
609
|
}
|
|
536
|
-
|
|
610
|
+
throw new Error(`unknown sub-agent id: ${String(args.id)}`)
|
|
537
611
|
}
|
|
612
|
+
const isFinished = !orchestration.currentMap.has(record.childId)
|
|
613
|
+
if (isFinished && orchestration.isBusy()) {
|
|
614
|
+
throw new Error('another sub-agent is currently running; wait for it to finish before reviving a completed sub-agent (single-line blocking)')
|
|
615
|
+
}
|
|
616
|
+
// 先投递,成功后再落账:投递失败不会留下假 running、也不会弄丢求助单
|
|
538
617
|
const messageId = await ctx.subagents.followup(parent, record.childId, [{ type: 'text', text: args.prompt }], {
|
|
539
618
|
source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id },
|
|
540
619
|
signal: exec?.signal,
|
|
541
620
|
})
|
|
621
|
+
if (record.status === 'waiting') {
|
|
622
|
+
for (const help of orchestration.snapshot().helpRequests) {
|
|
623
|
+
if (help.childId === record.childId) orchestration.resolveHelp(help.id)
|
|
624
|
+
}
|
|
625
|
+
orchestration.resume(record.childId)
|
|
626
|
+
} else if (isFinished) {
|
|
627
|
+
// 驳回/追问一个已结束的子智能体:重新入册并恢复类型登记,
|
|
628
|
+
// 否则它游离在单线阻塞之外,且再次结束时结论会被静默丢弃
|
|
629
|
+
orchestration.revive(record.childId)
|
|
630
|
+
sessionTypes.set(record.childId, record.agentType)
|
|
631
|
+
}
|
|
542
632
|
orchestration.followupPrompt(record.childId, args.prompt)
|
|
543
633
|
bump()
|
|
544
634
|
return { accepted: true, messageId }
|
|
@@ -639,19 +729,31 @@ export async function apply(ctx, config = {}) {
|
|
|
639
729
|
if (!help) throw new Error(`unknown help request id: ${String(args.from)}`)
|
|
640
730
|
const prompt = help.content
|
|
641
731
|
const target = String(args.target)
|
|
642
|
-
orchestration.resolveHelp(help.id)
|
|
643
732
|
if (AGENT_TYPES.includes(target)) {
|
|
644
733
|
// Dispatch a new sub-agent of that type.
|
|
645
734
|
const result = await dispatchWork(target, prompt, parent, exec?.signal)
|
|
735
|
+
orchestration.resolveHelp(help.id) // 投递成功后才销账,失败则求助单保留
|
|
646
736
|
bump()
|
|
647
737
|
return { kind: 'go_work', targetId: String(result?.childId ?? ''), resolved: true }
|
|
648
738
|
}
|
|
739
|
+
const record = orchestration.record(target)
|
|
740
|
+
if (!record) throw new Error(`unknown sub-agent id: ${target}`)
|
|
741
|
+
const isFinished = !orchestration.currentMap.has(target)
|
|
742
|
+
if (isFinished && orchestration.isBusy()) {
|
|
743
|
+
throw new Error('another sub-agent is currently running; wait for it to finish before forwarding to a completed sub-agent (single-line blocking)')
|
|
744
|
+
}
|
|
649
745
|
const messageId = await ctx.subagents.followup(parent, target, [{ type: 'text', text: prompt }], {
|
|
650
746
|
source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id },
|
|
651
747
|
signal: exec?.signal,
|
|
652
748
|
})
|
|
653
749
|
orchestration.followupPrompt(target, prompt)
|
|
654
|
-
|
|
750
|
+
if (record.status === 'waiting') {
|
|
751
|
+
orchestration.resume(target)
|
|
752
|
+
} else if (isFinished) {
|
|
753
|
+
orchestration.revive(target)
|
|
754
|
+
sessionTypes.set(target, record.agentType)
|
|
755
|
+
}
|
|
756
|
+
orchestration.resolveHelp(help.id)
|
|
655
757
|
bump()
|
|
656
758
|
return { kind: 'continue', targetId: messageId, resolved: true }
|
|
657
759
|
},
|
|
@@ -766,11 +868,18 @@ export async function apply(ctx, config = {}) {
|
|
|
766
868
|
const list = await llm.listModels(key)
|
|
767
869
|
for (const m of list) set.add(m.id)
|
|
768
870
|
} catch { /* provider may not support listing */ }
|
|
769
|
-
|
|
871
|
+
// 只缓存非空结果:瞬时失败/空列表不永久缓存(负缓存会让模型绑定
|
|
872
|
+
// 在本进程生命周期内静默失效),留待下次请求重试
|
|
873
|
+
if (set.size > 0) modelCache.set(key, set)
|
|
770
874
|
}
|
|
771
875
|
return set.has(String(model))
|
|
772
876
|
}
|
|
773
877
|
|
|
878
|
+
// 注意:此处刻意【不】挂 agent/created 钩子。lib 是 global 层插件,该事件
|
|
879
|
+
// 会收到 profile 内【所有】会话(含非 MyGO 会话)——skill 隐藏与拓扑闸只应
|
|
880
|
+
// 作用于 MyGO preset 会话,由 preset 作用域的 broker.mjs 负责(standing scope
|
|
881
|
+
// 的 listener 只接收 join 它的 agent 的事件)。在此全局挂钩会误伤其他 preset。
|
|
882
|
+
|
|
774
883
|
ctx.on('agent/request', async (payload, next) => {
|
|
775
884
|
const seed = await next()
|
|
776
885
|
const agent = payload?.agent
|
|
@@ -801,11 +910,47 @@ export async function apply(ctx, config = {}) {
|
|
|
801
910
|
return nextConfig
|
|
802
911
|
})
|
|
803
912
|
|
|
913
|
+
// ── 生命周期清理:会话/代理销毁时回收编排状态,防止跨会话泄漏 ──────────
|
|
914
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
915
|
+
const id = agent?.id
|
|
916
|
+
if (!id) return
|
|
917
|
+
const tracked = sessionTypes.delete(id)
|
|
918
|
+
if (orchestration.currentMap.has(id)) {
|
|
919
|
+
// 子代理被销毁但错过了 subagent/end:兜底清槽,否则队列永久冻结
|
|
920
|
+
orchestration.clearHelpFor(id)
|
|
921
|
+
orchestration.abort(id)
|
|
922
|
+
bump()
|
|
923
|
+
advanceQueue()
|
|
924
|
+
} else if (tracked) {
|
|
925
|
+
bump()
|
|
926
|
+
}
|
|
927
|
+
})
|
|
928
|
+
|
|
929
|
+
ctx.on('session/disposed', (session) => {
|
|
930
|
+
const id = session?.id
|
|
931
|
+
if (!id) return
|
|
932
|
+
if (id === lastOrchestratorSessionId) {
|
|
933
|
+
// Sisyphus 主会话被删除:丢弃其排队任务,避免悬挂到永远不会来的父会话
|
|
934
|
+
lastOrchestratorSessionId = null
|
|
935
|
+
orchestration.dropQueuedFor(id)
|
|
936
|
+
bump()
|
|
937
|
+
}
|
|
938
|
+
})
|
|
939
|
+
|
|
804
940
|
// ── conclusion injection + queue advancement on subagent/end ────────────
|
|
805
941
|
ctx.on('subagent/end', (info) => {
|
|
806
942
|
const childId = info?.id
|
|
807
|
-
|
|
808
|
-
if (type === undefined)
|
|
943
|
+
let type = sessionTypes.get(childId)
|
|
944
|
+
if (type === undefined) {
|
|
945
|
+
// 竞态兜底:快速失败的子会话可能在 startContinuable resolve 之前就触发
|
|
946
|
+
// subagent/end(此时 sessionTypes 尚未登记)。单线阻塞下至多存在一条
|
|
947
|
+
// spawning 记录,可安全归因到它,否则会永久卡在 spawning/running 冻结队列。
|
|
948
|
+
const spawning = [...orchestration.currentMap.values()].find((r) => r.status === 'spawning')
|
|
949
|
+
if (!spawning) return
|
|
950
|
+
type = spawning.agentType
|
|
951
|
+
orchestration.bindChild(spawning.childId, childId)
|
|
952
|
+
console.warn('[dsh-my-go] subagent/end arrived before spawn resolved; attributed to spawning record', childId)
|
|
953
|
+
}
|
|
809
954
|
const blocks = info?.lastAssistantMessage ?? []
|
|
810
955
|
const text = blocks
|
|
811
956
|
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
@@ -816,16 +961,7 @@ export async function apply(ctx, config = {}) {
|
|
|
816
961
|
sessionTypes.delete(childId)
|
|
817
962
|
bump()
|
|
818
963
|
// Advance queue.
|
|
819
|
-
|
|
820
|
-
const work = orchestration.dequeue()
|
|
821
|
-
if (work) {
|
|
822
|
-
// Resolve the recorded parent agent (the Sisyphus session) and
|
|
823
|
-
// dispatch the next queued work through the same internal path.
|
|
824
|
-
const agents = ctx.get('agents')
|
|
825
|
-
const parentAgent = work.parentId && agents ? agents.get(work.parentId) : undefined
|
|
826
|
-
void dispatchWork(work.agentType, work.prompt, parentAgent, undefined).catch(() => undefined)
|
|
827
|
-
}
|
|
828
|
-
}
|
|
964
|
+
advanceQueue()
|
|
829
965
|
})
|
|
830
966
|
|
|
831
967
|
return () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-my-go",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "DSH plugin: Sisyphus agent orchestration — star topology, single-line blocking, 7 specialist sub-agents (Hermes/Explore/Librarian/Looker/Hephaestus/Prometheus/Oracle) with per-type model binding, go_work/continue/need_help/forward communication tools, Web UI tree panel and settings.",
|
|
5
5
|
"author": "daizihan233",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"build": "bun run build:client",
|
|
28
28
|
"build:client": "node scripts/build-client.mjs",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
|
-
"test": "node test/apply.mjs"
|
|
30
|
+
"test": "node test/apply.mjs && node --test test/orchestration.test.mjs test/bridge.test.mjs"
|
|
31
31
|
},
|
|
32
32
|
"dsh": {
|
|
33
33
|
"bundle": { "patch": "./cordis.patch.yml" },
|
package/preset/agent.cordis.yml
CHANGED
|
@@ -162,3 +162,11 @@
|
|
|
162
162
|
config:
|
|
163
163
|
fetch: false
|
|
164
164
|
searchTimeoutMs: 60000
|
|
165
|
+
|
|
166
|
+
# ── tool mask ────────────────────────────────────────────────────────────────
|
|
167
|
+
# Hides Rei-persona memory tools and the OpenCode bridge from this preset's
|
|
168
|
+
# catalog — applies to Sisyphus and every sub-agent on the preset (synced from
|
|
169
|
+
# the `nova` preset recipe). Mask list lives in ./tool-mask.mjs (per-name
|
|
170
|
+
# try/catch, so an absent tool cannot fail the mount).
|
|
171
|
+
- id: tool-mask
|
|
172
|
+
name: ./tool-mask.mjs
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// MyGO preset tool mask: hide Rei-persona memory tools and the OpenCode
|
|
2
|
+
// bridge from this preset's catalog (Sisyphus AND sub-agents alike — the
|
|
3
|
+
// mask applies at preset scope). Synced from the `nova` preset recipe.
|
|
4
|
+
// Per-name try/catch: a tool absent from this deployment throws on restrict
|
|
5
|
+
// and is skipped (with a visible warning); the rest still apply and the
|
|
6
|
+
// preset mount never fails because of the mask.
|
|
7
|
+
export const inject = ['tools'];
|
|
8
|
+
|
|
9
|
+
const DENY = [
|
|
10
|
+
'mcp__vcp__daily_note_create_for_rei',
|
|
11
|
+
'mcp__vcp__daily_note_update_for_rei',
|
|
12
|
+
'mcp__vcp__light_memo_search_for_rei',
|
|
13
|
+
'mcp__vcp__rei_memo_write',
|
|
14
|
+
'mcp__vcp__opencode_task',
|
|
15
|
+
'mcp__vcp__opencode_list_sessions',
|
|
16
|
+
'mcp__vcp__opencode_delete_session',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function apply(ctx) {
|
|
20
|
+
for (const name of DENY) {
|
|
21
|
+
try {
|
|
22
|
+
ctx.tools.restrict({ deny: [name] });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn(`[dsh-my-go] tool-mask: could not deny "${name}" (absent or reserved): ${String(error)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|