@mars.liu/dsh-cybernetics 0.8.9 → 0.8.10
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 +6 -3
- package/README.zh-CN.md +6 -3
- package/docs/CHANGELOG.md +11 -0
- package/lib/client.js +38 -2
- package/package.json +1 -1
- package/src/controller.ts +5 -1
- package/src/executor.ts +4 -1
- package/src/index.ts +88 -27
package/README.md
CHANGED
|
@@ -42,7 +42,9 @@ flowchart TB
|
|
|
42
42
|
|
|
43
43
|
When enabled, injects the "ALL-GOALS MODE ACTIVE" routing protocol: **every user request first creates a goal, then executes**; the session agent is forbidden from completing tasks in place with read tools — even a single-file lookup or one calculation goes through the loop. Simple read-only requests → a single `quick` round; complex analysis / write tasks → progressive multi-round `full` (async `start` + round-by-round `collect` convergence). When the mode is off, the prompt segments and revise behavior are exactly the same as v0.6.
|
|
44
44
|
|
|
45
|
-
### Hard gate (`hardGate`, default true — active out of the box)
|
|
45
|
+
### Hard gate (`hardGate`, default true — active out of the box; v0.8.10: toggleable on the settings card)
|
|
46
|
+
|
|
47
|
+
**Hot toggle (v0.8.10).** `hardGate` is a **volatile** field: the plugins-page settings card renders a live switch for it. Saving the toggle commits into the running instance and the plugin disposes or (re)installs the gate listeners (assembly strip + dispatch deny + approval relay) immediately — no restart, no plugin reload. A hot false→true re-derives the mode from the current `gateMode` resolution (explicit config wins; otherwise the structural allowlist). The other gate fields (`gateMode`/`gatedTools`/`gateAllowlist`/`gateAllowlistExtra`/`gateApprovalRelay`) remain non-volatile config-document fields; edit those in the profile config document (saving triggers a plugin reload). Deployment nuance: the first time this build ships, the running host still holds the pre-0.8.10 code — restart the host once to load it; after that the toggle is hot (`lib/client.js` needs only a browser refresh).
|
|
46
48
|
|
|
47
49
|
**Effective mode (v0.8.6, default-safe).** When `hardGate` is on and `gateMode` is not configured, the gate runs in **`allowlist`** mode: the top-level session agent keeps only read + conversation + presentation + goal/orchestration verbs (the built-in `DEFAULT_GATE_ALLOWLIST`, extendable via `gateAllowlist` / `gateAllowlistExtra`), and every change-class tool is removed at assembly **and denied at dispatch**. This is the default stance because the old `denylist` default was a trap: it hid only the three names in `gatedTools` (bash/edit/write) and left `bsk_*`, `taskboard_*`, `schedule_*`, `farm_*`, `uno_*`, `ppt_*`, `config_restore` and the rest of the mutation long tail callable — a hard gate in name only. `hardGate: false` installs no gate, exactly as before. An explicitly configured `gateMode` is honored verbatim; the schema has **no default** for `gateMode`, so "unset" stays distinguishable from "explicitly denylist".
|
|
48
50
|
|
|
@@ -145,7 +147,8 @@ Configure directly on this plugin's card on the Web editor's "Plugins" page —
|
|
|
145
147
|
- **All-goals mode** (`allGoalsMode`, volatile): toggle, takes effect immediately on save, prompt segments remount instantly;
|
|
146
148
|
- **task/verify/triage agent models** (`taskAgentModel` / `verifyAgentModel` / `triageAgentModel`, volatile): dropdown-backed text fields — pick from the live system model list (shown as `providerName · provider/model`) or type "provider/model" / a bare model name; Enter or blur saves, empty inherits from the session agent (triage = the convergence-decision role, full rounds only). A configured value missing from the live model list shows an unavailable warning (runtime falls back to the session agent model) with a one-click Clear;
|
|
147
149
|
- **Recursion depth / parallel cap** (`roundsPerSubgoal` integer ≥1 / `maxConcurrentRounds` integer ≥0, volatile): number fields, Enter or blur saves, blank restores the default (an `unset` write);
|
|
148
|
-
- **Hard gate
|
|
150
|
+
- **Hard gate** (`hardGate`, volatile since v0.8.10): a boolean toggle on the card — strips write/execution tools from the top-level agent at assembly and routes every change through the goal loop; takes effect immediately on save (gate listeners hot-installed/disposed), no restart needed;
|
|
151
|
+
- **Other gate fields** (`gateMode`/`gatedTools`/`gateAllowlist`/`gateAllowlistExtra`/`gateApprovalRelay`, non-volatile): not served to the card; edit them in the profile config document (saving triggers a plugin reload). `gateMode` has no default: unset + `hardGate: true` runs `allowlist`; an explicit `denylist` is honored and carries the residual risk documented above.
|
|
149
152
|
|
|
150
153
|
### Advanced configuration: roles (profile yaml, agent-guided)
|
|
151
154
|
|
|
@@ -204,7 +207,7 @@ Companion auditing: each round's structured result lands in `.cybernetics/round-
|
|
|
204
207
|
## Known Limitations
|
|
205
208
|
|
|
206
209
|
1. **Host defect in settings writes**: as described in the "Plugin-page configuration" section, on this runtime, card saves may be rejected by the host before reaching disk (upstream [#12045](https://github.com/deepseek-ai/dsh/discussions/12045)); workarounds: restart the host + hard-refresh the browser, or manual patch rows + restart.
|
|
207
|
-
2. **Save-semantics difference between volatile and non-volatile fields**: volatile fields (`allGoalsMode`, `taskAgentModel`, `verifyAgentModel`, `triageAgentModel`, `roundsPerSubgoal`, `maxConcurrentRounds`) take effect immediately on save without remount; non-volatile fields (`roles` since v0.8.4, `
|
|
210
|
+
2. **Save-semantics difference between volatile and non-volatile fields**: volatile fields (`allGoalsMode`, `hardGate` since v0.8.10, `taskAgentModel`, `verifyAgentModel`, `triageAgentModel`, `roundsPerSubgoal`, `maxConcurrentRounds`) take effect immediately on save without remount; non-volatile fields (`roles` since v0.8.4, `gateMode`, `gatedTools`, `gateAllowlist*`, `gateApprovalRelay`, `toolName`, `implicitTrigger`) trigger a plugin-row remount on save, requiring a host reload to complete. Moreover, the host settings form only serves and only accepts volatile paths — if the running host's schema is too old, even volatile writes are rejected (a restart fixes it).
|
|
208
211
|
3. **`link:` installation caveats**: the client bundle's registration id must equal the package.json `name` (`@mars.liu/dsh-cybernetics`); the loader row id may use the short name `dsh-cybernetics`. The plugin must hang at the **top level** of the profile's loader entries (the client scan enumerates only top-level rows); `src/` changes require a host reload, `lib/` changes require a host restart + browser refresh.
|
|
209
212
|
4. Other protocol boundaries (the hard gate operating at the assembly layer rather than the execution layer, dsh-goal's focus slot not migrating on supersede, etc.) are covered in the "Known Boundaries" subsections of [docs/protocol.md](docs/protocol.md).
|
|
210
213
|
|
package/README.zh-CN.md
CHANGED
|
@@ -42,7 +42,9 @@ flowchart TB
|
|
|
42
42
|
|
|
43
43
|
开启后注入「ALL-GOALS MODE ACTIVE」路由协议:**任何用户请求一律先建 goal 再执行**,session agent 禁止用读工具就地完成任务——哪怕单文件查询或一次计算也走回路。简单只读请求 → `quick` 单轮;复杂分析/写任务 → `full` 多轮渐进(`start` 异步启动 + 逐轮 `collect` 收敛)。关闭模式时提示段与 revise 行为与 v0.6 完全一致。
|
|
44
44
|
|
|
45
|
-
### 硬门(`hardGate`,默认 true
|
|
45
|
+
### 硬门(`hardGate`,默认 true——安装即激活;v0.8.10 起设置卡片可开关)
|
|
46
|
+
|
|
47
|
+
**热切换(v0.8.10)**:`hardGate` 已是 **volatile** 字段——插件页设置卡片上有实时开关。保存即把新值提交进运行实例,插件随即卸载或重装门禁监听(装配摘工具 + 分发拒绝 + 审批透传),**即时生效,无需重启、无需插件重载**。从关到开的热启按当前 `gateMode` 解析结果接线(显式配置优先,否则结构性 allowlist)。其余门字段(`gateMode`/`gatedTools`/`gateAllowlist`/`gateAllowlistExtra`/`gateApprovalRelay`)仍是非 volatile 配置文档字段,请在 profile 配置文档中维护(保存触发插件重载)。部署注意:本构建首次上线时运行中宿主仍是旧代码——重启宿主一次加载新代码,此后开关即热生效(`lib/client.js` 只需刷新浏览器)。
|
|
46
48
|
|
|
47
49
|
**生效语义(v0.8.6,默认安全)**:`hardGate` 开启且未配置 `gateMode` 时,门按 **`allowlist`** 运行——顶层 session agent 只保留读 + 对话 + 展示 + goal/编排动词(内置 `DEFAULT_GATE_ALLOWLIST`,可经 `gateAllowlist`/`gateAllowlistExtra` 扩展),所有变更类工具在装配期被**移除**,并在**调用分发层被拒绝**。之所以改为这个默认:旧的 `denylist` 默认是个陷阱——它只隐藏 `gatedTools` 的三个名字(bash/edit/write),`bsk_*`、`taskboard_*`、`schedule_*`、`farm_*`、`uno_*`、`ppt_*`、`config_restore` 以及整个变更工具长尾全都仍可调用,名为硬门、实为半门。`hardGate: false` 与以前完全一致(不装门);显式配置的 `gateMode` 一律原样尊重;schema 不再给 `gateMode` 设默认值,因此「未设置」与「显式 denylist」可以被区分。
|
|
48
50
|
|
|
@@ -146,7 +148,8 @@ Web 编辑器「插件」页的本插件卡片上直接配置——v0.8.1 起卡
|
|
|
146
148
|
- **task/verify/triage agent 模型**(`taskAgentModel` / `verifyAgentModel` / `triageAgentModel`,volatile):下拉加持的文本框——从实时系统模型列表选择(显示 `providerName · provider/model`)或手工输入「provider/model」/裸模型名,回车或移开焦点即保存,留空继承 session agent(triage = 收敛决策角色,仅 full 轮启用)。配置值不在实时模型列表中时显示不可用警告(运行时回退到 session agent 模型)并附一键清除;
|
|
147
149
|
- **递归深度 / 并行上限**(`roundsPerSubgoal` 整数 ≥1 / `maxConcurrentRounds` 整数 ≥0,volatile):数字框,回车或失焦保存,留空恢复默认(`unset` 写入);
|
|
148
150
|
- **roles 高级配置**(`roles`,非 volatile):profile 配置 yaml 直接维护——见下方小节;
|
|
149
|
-
-
|
|
151
|
+
- **硬编排门**(`hardGate`,v0.8.10 起 volatile):卡片布尔开关——装配期从顶层 agent 摘除写/执行工具,所有变更经 goal 循环完成;保存即时生效(门禁监听热装卸),无需重启;
|
|
152
|
+
- **其余门字段**(`gateMode`/`gatedTools`/`gateAllowlist`/`gateAllowlistExtra`/`gateApprovalRelay`,非 volatile):不进入卡片,请在 profile 配置文档中维护(保存触发插件重载)。`gateMode` 无默认值:未设置 + `hardGate: true` 按 `allowlist` 运行;显式 `denylist` 会被尊重,并带有上文记录的残留风险。
|
|
150
153
|
|
|
151
154
|
### roles 高级配置(profile yaml,agent 指导)
|
|
152
155
|
|
|
@@ -205,7 +208,7 @@ session agent 在会话内通过 `cybernetics` 工具(`toolName` 可配置)
|
|
|
205
208
|
## 已知限制
|
|
206
209
|
|
|
207
210
|
1. **settings 写入的宿主缺陷**:如「插件页配置」一节所述,该运行时上卡片保存可能被宿主在落盘前拒绝(上游 [#12045](https://github.com/deepseek-ai/dsh/discussions/12045));workaround:重启宿主 + 强刷浏览器,或手工 patch 行 + 重启。
|
|
208
|
-
2. **volatile 与非 volatile 字段的保存语义差异**:volatile 字段(`allGoalsMode`、`taskAgentModel`、`verifyAgentModel`、`triageAgentModel`、`roundsPerSubgoal`、`maxConcurrentRounds`)保存即时生效、不 remount;非 volatile 字段(v0.8.4 起的 `roles`、`
|
|
211
|
+
2. **volatile 与非 volatile 字段的保存语义差异**:volatile 字段(`allGoalsMode`、v0.8.10 起的 `hardGate`、`taskAgentModel`、`verifyAgentModel`、`triageAgentModel`、`roundsPerSubgoal`、`maxConcurrentRounds`)保存即时生效、不 remount;非 volatile 字段(v0.8.4 起的 `roles`、`gateMode`、`gatedTools`、`gateAllowlist*`、`gateApprovalRelay`、`toolName`、`implicitTrigger`)保存触发插件行 remount,需宿主重载完成。且宿主设置表单只服务也只接受 volatile 路径——运行中宿主的 schema 过旧时,volatile 写也会被拒(重启即愈)。
|
|
209
212
|
3. **`link:` 安装注意事项**:客户端 bundle 的注册 id 必须等于 package.json 的 `name`(`@mars.liu/dsh-cybernetics`);loader 行 id 可用短名 `dsh-cybernetics`。插件需挂在本 profile 的**顶层** loader entries(客户端扫描只枚举顶层行);`src/` 改动需宿主重载、`lib/` 改动需重启宿主 + 刷新浏览器。
|
|
210
213
|
4. 其他协议边界(硬门在装配层不在执行层、dsh-goal 焦点槽不随 superseded 迁移等)见 [docs/protocol.md](docs/protocol.md) 各「已知边界」小节。
|
|
211
214
|
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
> 当下的功能与使用方式以 [README.md](../README.md) 为准;协议细节见 [protocol.md](protocol.md)。
|
|
5
5
|
> 约定:`src/` 改动需宿主重载生效;`lib/client.js` 改动需重启宿主(客户端模块图在宿主启动时构建)并刷新浏览器;`workflows/round.js` 改动热生效。
|
|
6
6
|
|
|
7
|
+
## v0.8.10(2026-11-05):hardGate 上设置卡片,volatile 热切换
|
|
8
|
+
|
|
9
|
+
**需求**:用户要在插件页配置卡片上直接开关硬编排门(像 `allGoalsMode` 一样保存即时生效,无需重启)。此前 `hardGate` 是非 volatile 字段,宿主设置表单只服务 volatile 路径,GUI 完全写不了它。
|
|
10
|
+
|
|
11
|
+
- **schema(`src/index.ts`)**:`hardGate` 加 `.volatile()`(保留 `default(true)`),description 更新为「设置卡片可开关,保存后即时生效(热装卸门禁监听),无需重启宿主」。`gateMode`/`gatedTools`/`gateAllowlist`/`gateAllowlistExtra`/`gateApprovalRelay` 保持非 volatile 不动。
|
|
12
|
+
- **apply() 门禁接线可装卸**:装配 waterfall 摘工具 + `tools/pre-execute` 分发拒绝 + `gateApprovalRelay` 透传(hardGate 伴随项)提取为 `installGate()`(内部 `gateDisposers`,每次调用先卸旧装再按需安装,幂等)。`hardGate` 改为按使用时读取的函数(`readConfigField` 解 volatile live reference;普通配置缺省仍为不装门,与旧语义一致,schema 默认 true 经 live ref 到达运行时)。监听 `loader/volatile-update`:翻转时先 `registerGuidanceSection()`(HARD GATE 提示段随开关重推导)再 `installGate()`——与 `registerGuidanceSection` 同一重建模式。热关→开按当刻 `resolveGateMode()` 解析结果接线(显式 gateMode 优先,否则结构性 allowlist)。插件卸载 disposer 链不变。
|
|
13
|
+
- **client 卡片(`lib/client.js`)**:`SETTINGS_FIELDS` 首位新增 `{ field: 'hardGate', kind: 'boolean' }`;`fieldRow` 新增 boolean 分支(`booleanRow`,与头部 allGoalsMode 开关同款 switch 控件,走同一 `writeField` 写路径)。双语 locale 补 `settings.hardGate` / `settings.hardGateHint`:开启后顶层 agent 写工具被摘除、所有变更经 goal 循环;即时生效,无需重启。
|
|
14
|
+
- **测试(`test/v03-offline.mjs`,增至 72 项)**:新增用例 71——(a) 初始 `hardGate=false` 经 live ref 热切 true:装配中 bash 被摘、pre-execute 拒绝、guidance 含 HARD GATE;(b) true→false:bash 恢复、assemble/pre-execute 监听已 dispose、guidance 摘除段;(c) 反复切换幂等(始终恰好一组活监听,末态 hardGate=true 仍拒绝);(d) schema 断言 hardGate volatile、其余门字段非 volatile。同步更新既有断言:用例 51/56/57 中 hardGate 从「非 volatile 负向对照」改为 volatile 正向(负向对照换 `gateMode`),volatileForm 服务字段六→七。既有全部用例通过。
|
|
15
|
+
- **文档**:README / README.zh-CN「硬门」小节与「插件页配置」「已知限制」更新——hardGate 从「配置文件维护/重启生效」改为「GUI 卡片开关、volatile 即时生效」;注明首次部署需重启宿主一次加载新代码,此后开关热生效。
|
|
16
|
+
- **生效手段**:`src/index.ts` 改动——重启宿主一次加载新代码,此后开关热生效;`lib/client.js` 改动——重启宿主 + 刷新浏览器(本仓库 `lib/` 即源码)。
|
|
17
|
+
|
|
7
18
|
## v0.8.7(2026-11-04):模型下拉 focus-parking(lib/client.js,仅客户端)
|
|
8
19
|
|
|
9
20
|
**问题**:原生 datalist 按输入框当前文本过滤选项——已配置模型的字段聚焦后下拉只剩当前那一个模型,用户看不到完整目录,容易误以为没有别的选择。改进方向不是自绘下拉,而是顺应原生 datalist 的过滤语义。
|
package/lib/client.js
CHANGED
|
@@ -115,6 +115,9 @@ window.__ModuleLoader__.load({
|
|
|
115
115
|
// v0.8.4: roles 高级配置不再上卡片(手工编辑 JSON 对用户不友善)——
|
|
116
116
|
// 由 agent 指导用户直接编辑 profile 配置 yaml(见 README)。
|
|
117
117
|
'settings.invalidNumber': '请输入符合范围的整数,或留空恢复默认。',
|
|
118
|
+
// v0.8.10: hardGate 上卡片(volatile,热切换)。
|
|
119
|
+
'settings.hardGate': '硬编排门',
|
|
120
|
+
'settings.hardGateHint': '开启后,顶层 agent 的写/执行工具在装配期被摘除,所有变更必须经 cybernetics goal + task agent 委派完成。开关保存后即时生效(热装卸门禁监听),无需重启。',
|
|
118
121
|
};
|
|
119
122
|
const en = {
|
|
120
123
|
'view.goal': 'Goal',
|
|
@@ -180,6 +183,9 @@ window.__ModuleLoader__.load({
|
|
|
180
183
|
'settings.maxConcurrentRounds': 'Concurrent round cap',
|
|
181
184
|
'settings.maxConcurrentRoundsHint': 'Global cap on rounds in flight (integer ≥0, default 0 = unlimited); empty restores the default. Enter or blur saves.',
|
|
182
185
|
'settings.invalidNumber': 'Enter an integer in range, or leave blank to restore the default.',
|
|
186
|
+
// v0.8.10: hardGate joins the card (volatile, hot-toggled).
|
|
187
|
+
'settings.hardGate': 'Hard orchestration gate',
|
|
188
|
+
'settings.hardGateHint': 'When on, write/execution tools are stripped from the TOP-LEVEL agent at assembly time; every change must go through a cybernetics goal + task agent. Takes effect immediately on save (gate listeners are hot-installed/disposed); no restart needed.',
|
|
183
189
|
};
|
|
184
190
|
//#endregion
|
|
185
191
|
|
|
@@ -544,10 +550,16 @@ window.__ModuleLoader__.load({
|
|
|
544
550
|
// an `unset` op). v0.8.4: roles is no longer on the card — hand-editing
|
|
545
551
|
// a JSON object in the GUI is unfriendly; roles became non-volatile and
|
|
546
552
|
// is configured directly in the profile yaml (agent-guided, README).
|
|
547
|
-
// The card's field list is back to
|
|
548
|
-
//
|
|
553
|
+
// The card's field list is back to seven (v0.8.10):
|
|
554
|
+
// hardGate (boolean toggle) / allGoalsMode-toggle header +
|
|
555
|
+
// taskAgentModel / verifyAgentModel / triageAgentModel /
|
|
549
556
|
// roundsPerSubgoal / maxConcurrentRounds.
|
|
550
557
|
const SETTINGS_FIELDS = [
|
|
558
|
+
// v0.8.10: hardGate is volatile and joins the card as a boolean toggle
|
|
559
|
+
// (same write path as allGoalsMode: settings mutate → loader volatile
|
|
560
|
+
// commit → loader/volatile-update → the server hot-installs/disposes
|
|
561
|
+
// the gate listeners, no restart).
|
|
562
|
+
{ field: 'hardGate', labelKey: 'settings.hardGate', hintKey: 'settings.hardGateHint', kind: 'boolean' },
|
|
551
563
|
// v0.8.3: the three model fields are kind 'model' — datalist-backed
|
|
552
564
|
// dropdown over the live host catalog, manual entry still accepted
|
|
553
565
|
// (a datalist input is a free text input); invalid configured values
|
|
@@ -814,7 +826,31 @@ window.__ModuleLoader__.load({
|
|
|
814
826
|
return jsx.jsx('span', { children: snap.status === 'ready' ? t(on ? 'settings.summaryOn' : 'settings.summaryOff') : t('settings.unavailable') });
|
|
815
827
|
}
|
|
816
828
|
|
|
829
|
+
// v0.8.10: boolean fields render the same switch face as the header
|
|
830
|
+
// allGoalsMode toggle, per field. The write is the shared
|
|
831
|
+
// writeField(field, !current) path (set op, no drafts involved).
|
|
832
|
+
const booleanRow = (spec) => {
|
|
833
|
+
const on = snap.status === 'ready' && snap.value != null && snap.value[spec.field] === true;
|
|
834
|
+
return jsx.jsx('div', { style: settingsStyles.field, children:
|
|
835
|
+
jsx.jsxs('div', { style: { ...settingsStyles.row }, children: [
|
|
836
|
+
jsx.jsxs('div', { style: settingsStyles.main, children: [
|
|
837
|
+
jsx.jsx('div', { style: settingsStyles.title, children: t(spec.labelKey) }),
|
|
838
|
+
jsx.jsx('div', { style: settingsStyles.desc, children: t(spec.hintKey) }),
|
|
839
|
+
] }),
|
|
840
|
+
snap.status === 'ready'
|
|
841
|
+
? jsx.jsx('button', {
|
|
842
|
+
type: 'button', role: 'switch', 'aria-checked': on, 'aria-label': t(spec.labelKey),
|
|
843
|
+
disabled, onClick: () => writeField(spec.field, !on),
|
|
844
|
+
style: { ...settingsStyles.toggle, ...(on ? settingsStyles.trackOn : settingsStyles.trackOff) },
|
|
845
|
+
children: jsx.jsx('span', { style: { ...settingsStyles.knob, ...(on ? settingsStyles.knobOn : settingsStyles.knobOff) } }),
|
|
846
|
+
})
|
|
847
|
+
: null,
|
|
848
|
+
] })
|
|
849
|
+
}, spec.field);
|
|
850
|
+
};
|
|
851
|
+
|
|
817
852
|
const fieldRow = (spec) => {
|
|
853
|
+
if (spec.kind === 'boolean') return booleanRow(spec);
|
|
818
854
|
// v0.8.3: availability verdict for the configured (snapshot) value —
|
|
819
855
|
// drafts are uncommitted text and never warn. Only a loaded catalog
|
|
820
856
|
// can flag; an empty/failed catalog keeps every value "available".
|
package/package.json
CHANGED
package/src/controller.ts
CHANGED
|
@@ -322,7 +322,11 @@ export async function startRound(
|
|
|
322
322
|
goal: { objective: goal.objective },
|
|
323
323
|
state: { round, doneSummary: state.doneSummary },
|
|
324
324
|
subgoal: state.subgoal,
|
|
325
|
-
|
|
325
|
+
// Never hand the workflow binder an own property whose value is
|
|
326
|
+
// undefined: structuredClone keeps it and the host-side json()
|
|
327
|
+
// (snapshotJsonValue) rejects nested undefined — the round dies at
|
|
328
|
+
// bind time. Omit the key entirely when the goal has no work order.
|
|
329
|
+
...(state.brief !== undefined ? { brief: state.brief } : {}),
|
|
326
330
|
depth: state.depth ?? 'full',
|
|
327
331
|
role: state.role,
|
|
328
332
|
roles: config.roles ?? {},
|
package/src/executor.ts
CHANGED
|
@@ -366,7 +366,10 @@ export async function startTeamRound(
|
|
|
366
366
|
goal: { objective: input.objective },
|
|
367
367
|
state: { round: input.round, doneSummary: input.doneSummary },
|
|
368
368
|
subgoal: input.subgoal,
|
|
369
|
-
|
|
369
|
+
// Same binder rule as controller.startRound: an own property with
|
|
370
|
+
// value undefined is kept by structuredClone and rejected by the
|
|
371
|
+
// host-side json() — omit the key when there is no work order.
|
|
372
|
+
...(state.brief !== undefined ? { brief: state.brief } : {}),
|
|
370
373
|
depth: 'full',
|
|
371
374
|
role: state.role,
|
|
372
375
|
roles: config.roles ?? {},
|
package/src/index.ts
CHANGED
|
@@ -148,9 +148,11 @@ export interface Config extends ControllerConfig {
|
|
|
148
148
|
// effect on the next goal round. roles stays in the schema but is NOT
|
|
149
149
|
// volatile (v0.8.4): the panel serves only volatile fields, so it no longer
|
|
150
150
|
// shows in the GUI — advanced users (agent-guided) configure it directly in
|
|
151
|
-
// the profile config yaml; a host reload applies it.
|
|
152
|
-
//
|
|
153
|
-
//
|
|
151
|
+
// the profile config yaml; a host reload applies it. v0.8.10: hardGate itself
|
|
152
|
+
// is volatile and toggleable from the settings card (the gate wiring is
|
|
153
|
+
// installed/disposed live on loader/volatile-update); the fine-grained gate
|
|
154
|
+
// fields (gateMode/gatedTools/gateAllowlist/gateAllowlistExtra) and
|
|
155
|
+
// gateApprovalRelay stay non-volatile config-document fields.
|
|
154
156
|
export const Config = z.object({
|
|
155
157
|
/** Model-facing tool name. */
|
|
156
158
|
toolName: z.string().default('cybernetics').description('模型侧工具名(缺省 cybernetics)。'),
|
|
@@ -206,8 +208,14 @@ export const Config = z.object({
|
|
|
206
208
|
* Volatile: enforced at every launch, live on the next tool call.
|
|
207
209
|
*/
|
|
208
210
|
maxConcurrentRounds: z.number().default(0).volatile().description('全局并行轮数上限(缺省 0 = 不限):在跑轮数达到上限时拒绝启动新一轮,防止并行风暴。保存后即时生效,无需重载。'),
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Hard orchestration gate (default true). Volatile (v0.8.10): the settings
|
|
213
|
+
* card exposes a live toggle; on loader/volatile-update apply() disposes or
|
|
214
|
+
* (re)installs the gate wiring (assemble waterfall + tools/pre-execute deny
|
|
215
|
+
* + approval relay) without a plugin remount, mirroring the
|
|
216
|
+
* registerGuidanceSection re-derive pattern.
|
|
217
|
+
*/
|
|
218
|
+
hardGate: z.boolean().default(true).volatile().description('硬编排门(默认开启):装配期从顶层 session agent 摘除执行工具,所有改动必须经 cybernetics goal + task agent。设置卡片可开关,保存后即时生效(热装卸门禁监听),无需重启宿主。'),
|
|
211
219
|
/** Tool names hidden in denylist mode (default DEFAULT_GATED_TOOLS). */
|
|
212
220
|
gatedTools: z.array(z.string()).description('denylist 模式下隐藏的工具名(缺省内置 DEFAULT_GATED_TOOLS)。'),
|
|
213
221
|
/**
|
|
@@ -586,15 +594,20 @@ function resolveGateMode(config: Partial<Config>, hardGate: boolean): 'denylist'
|
|
|
586
594
|
|
|
587
595
|
export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
588
596
|
const toolName = config.toolName ?? 'cybernetics'
|
|
589
|
-
|
|
597
|
+
// v0.8.10: hardGate is a volatile field, so it may arrive as a live
|
|
598
|
+
// reference whose value the settings card commits in place — read it at use
|
|
599
|
+
// time instead of capturing it once (same pattern as allGoalsMode below).
|
|
600
|
+
// Fallback false keeps the plain-object contract unchanged: a config that
|
|
601
|
+
// simply omits hardGate installs no gate, exactly as before; the schema's
|
|
602
|
+
// default(true) reaches the runtime through the live reference the volatile
|
|
603
|
+
// resolution provides.
|
|
604
|
+
const hardGate = (): boolean => readConfigField(config.hardGate, false) === true
|
|
590
605
|
// v0.8.6 default-safe gate semantics: a user who turns hardGate on but says
|
|
591
606
|
// nothing about gateMode gets the structural allowlist, not the leaky
|
|
592
607
|
// denylist. hardGate=false resolves to denylist but no gate is installed,
|
|
593
|
-
// so its behavior is unchanged.
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
? (gateMode === 'allowlist' ? HARD_GATE_GUIDANCE_ALLOWLIST : HARD_GATE_GUIDANCE_DENYLIST)
|
|
597
|
-
: ''
|
|
608
|
+
// so its behavior is unchanged. gateMode itself stays non-volatile: its
|
|
609
|
+
// plain value is re-read on every (re)install, so a hot hardGate flip
|
|
610
|
+
// re-derives the mode via resolveGateMode at that moment.
|
|
598
611
|
// allGoalsMode is a volatile field: under the loader's schema resolution it
|
|
599
612
|
// arrives as a live reference whose value the settings panel commits in
|
|
600
613
|
// place (no remount), so read it at use time instead of capturing it once.
|
|
@@ -624,6 +637,12 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
624
637
|
if (disposeSection !== undefined) disposeSection()
|
|
625
638
|
disposeSection = undefined
|
|
626
639
|
if (config.implicitTrigger === false) return
|
|
640
|
+
// v0.8.10: the hard-gate wording is re-derived per call — hardGate is
|
|
641
|
+
// volatile, so its guidance segment follows the live toggle too.
|
|
642
|
+
const gateMode = resolveGateMode(config, hardGate())
|
|
643
|
+
const hardGateGuidance = hardGate()
|
|
644
|
+
? (gateMode === 'allowlist' ? HARD_GATE_GUIDANCE_ALLOWLIST : HARD_GATE_GUIDANCE_DENYLIST)
|
|
645
|
+
: ''
|
|
627
646
|
disposeSection = ctx.systemPrompt.section({
|
|
628
647
|
name: `tool:${toolName}`,
|
|
629
648
|
order: ctx.systemPrompt.getSectionOrder('TOOL_WORKFLOW'),
|
|
@@ -632,15 +651,22 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
632
651
|
}
|
|
633
652
|
registerGuidanceSection()
|
|
634
653
|
disposers.push(() => { if (disposeSection !== undefined) disposeSection() })
|
|
635
|
-
ctx.on?.('loader/volatile-update', () => { registerGuidanceSection() })
|
|
636
654
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
655
|
+
/**
|
|
656
|
+
* v0.8.10: install (or tear down) the hard-gate wiring in step with the
|
|
657
|
+
* volatile hardGate toggle. Idempotent: each call first disposes the
|
|
658
|
+
* previous installation, so repeated toggles never double-register. A
|
|
659
|
+
* hot false→true re-derives gateMode via resolveGateMode from the CURRENT
|
|
660
|
+
* resolution (explicit gateMode config wins; otherwise allowlist).
|
|
661
|
+
*/
|
|
662
|
+
let disposeGate: (() => void) | undefined
|
|
663
|
+
function installGate(): void {
|
|
664
|
+
if (disposeGate !== undefined) { disposeGate(); disposeGate = undefined }
|
|
665
|
+
if (!hardGate()) return
|
|
666
|
+
const gateDisposers: Array<() => void> = []
|
|
667
|
+
// The gate is being installed, so hardGate() is true here; resolveGateMode
|
|
668
|
+
// then honors an explicit gateMode or derives the structural allowlist.
|
|
669
|
+
const gateMode = resolveGateMode(config, true)
|
|
644
670
|
const allowlistMode = gateMode === 'allowlist'
|
|
645
671
|
// NOTE: schema-resolved config turns an unset z.array field into [] (not
|
|
646
672
|
// undefined), so fall back on length, not on ?? — otherwise the gate
|
|
@@ -651,6 +677,10 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
651
677
|
toolName,
|
|
652
678
|
])
|
|
653
679
|
const gated = new Set(config.gatedTools?.length ? config.gatedTools : DEFAULT_GATED_TOOLS)
|
|
680
|
+
// The hard gate: a system-prompt/assemble waterfall listener that strips
|
|
681
|
+
// execution tools from TOP-LEVEL agents' assemblies. Workflow subagents
|
|
682
|
+
// (task/verify/triage) and team teammates are never in agents.roots(),
|
|
683
|
+
// so their assemblies pass through untouched with full tool access.
|
|
654
684
|
const listenerDisposer = ctx.on('system-prompt/assemble', (assembly: any, context: any, next: () => unknown) => {
|
|
655
685
|
const scope = context?.scope
|
|
656
686
|
if (!scope) return next()
|
|
@@ -673,7 +703,7 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
673
703
|
}
|
|
674
704
|
return next()
|
|
675
705
|
})
|
|
676
|
-
|
|
706
|
+
gateDisposers.push(() => listenerDisposer?.())
|
|
677
707
|
|
|
678
708
|
// Execution-layer gate (the assembly filter above only hides names; the
|
|
679
709
|
// host resolves any registered tool by name, so an unadvertised call must
|
|
@@ -699,7 +729,7 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
699
729
|
'Delegate the change: create a cybernetics goal (action "start" with title+brief) and let a task agent execute it.',
|
|
700
730
|
})
|
|
701
731
|
})
|
|
702
|
-
|
|
732
|
+
gateDisposers.push(() => preDisposer?.())
|
|
703
733
|
|
|
704
734
|
// v0.8.5 approval relay (gateApprovalRelay, default true): delegated
|
|
705
735
|
// children (workflow subagents, teammates) inherit the host's PINNED
|
|
@@ -806,7 +836,7 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
806
836
|
// A relay must never break delegation — leave the pinned policy.
|
|
807
837
|
}
|
|
808
838
|
})
|
|
809
|
-
|
|
839
|
+
gateDisposers.push(() => createdDisposer?.())
|
|
810
840
|
|
|
811
841
|
const RELAY_QUESTION_ID = 'cybernetics-approval-relay'
|
|
812
842
|
const RELAY_ALLOW = '允许一次(Allow once)'
|
|
@@ -957,9 +987,17 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
957
987
|
const outcome = await relayToSessionAgent(req, root)
|
|
958
988
|
return outcome === undefined ? next() : outcome
|
|
959
989
|
}, { prepend: true })
|
|
960
|
-
|
|
990
|
+
gateDisposers.push(() => relayDisposer?.())
|
|
961
991
|
}
|
|
992
|
+
|
|
993
|
+
disposeGate = () => { gateDisposers.forEach((d) => d()) }
|
|
962
994
|
}
|
|
995
|
+
installGate()
|
|
996
|
+
disposers.push(() => { if (disposeGate !== undefined) disposeGate() })
|
|
997
|
+
ctx.on?.('loader/volatile-update', () => {
|
|
998
|
+
registerGuidanceSection()
|
|
999
|
+
installGate()
|
|
1000
|
+
})
|
|
963
1001
|
|
|
964
1002
|
const disposer = (ctx.tools as { register: (d: unknown) => () => void }).register({
|
|
965
1003
|
name: toolName,
|
|
@@ -1217,9 +1255,12 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1217
1255
|
}
|
|
1218
1256
|
}
|
|
1219
1257
|
|
|
1220
|
-
// Per-call overrides.
|
|
1258
|
+
// Per-call overrides. 'revise' is excluded from the brief override: the
|
|
1259
|
+
// call targets the OLD goal (which gets frozen as superseded below), so
|
|
1260
|
+
// writing the new work order onto it would pollute frozen history — the
|
|
1261
|
+
// revise branch assigns the brief to the successor goal itself.
|
|
1221
1262
|
if (args.subgoal) loop.subgoal = args.subgoal
|
|
1222
|
-
if (args.brief) loop.brief = args.brief
|
|
1263
|
+
if (args.brief && action !== 'revise') loop.brief = args.brief
|
|
1223
1264
|
if (args.depth) loop.depth = args.depth
|
|
1224
1265
|
if (args.executor) loop.executor = args.executor
|
|
1225
1266
|
if (args.role) loop.role = args.role
|
|
@@ -1263,6 +1304,20 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1263
1304
|
? `cybernetics: 已有 ${running.size} 个运行中的轮,达到全局并行上限 ${cap}(可在设置面板调高 maxConcurrentRounds 或等待完成)`
|
|
1264
1305
|
: undefined
|
|
1265
1306
|
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Brief visibility in tool results: without it the model cannot tell
|
|
1309
|
+
* whether a round was launched WITH a work order — the result looked
|
|
1310
|
+
* identical either way, and a briefless round silently sent the task
|
|
1311
|
+
* agent in with only objective/title (a no-feedback loop).
|
|
1312
|
+
*/
|
|
1313
|
+
const briefWarning = (target: LoopState): { warning?: string } =>
|
|
1314
|
+
typeof target.brief === 'string' && target.brief.trim().length > 0
|
|
1315
|
+
? {}
|
|
1316
|
+
: {
|
|
1317
|
+
warning:
|
|
1318
|
+
'该 goal 没有工作委托书(brief):task agent 只能看到 objective/title。' +
|
|
1319
|
+
'复杂任务请用 cybernetics revise 补 brief(含背景、路径、验收标准)。',
|
|
1320
|
+
}
|
|
1266
1321
|
const objective = loop.objective ?? goalView?.objective ?? loop.title ?? ''
|
|
1267
1322
|
|
|
1268
1323
|
/**
|
|
@@ -1396,10 +1451,16 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1396
1451
|
revised: true,
|
|
1397
1452
|
round,
|
|
1398
1453
|
started: true,
|
|
1454
|
+
// Work-order visibility: the successor's effective brief (supplied
|
|
1455
|
+
// or inherited), so the caller can verify the task agent's mandate.
|
|
1456
|
+
workOrder: next.brief
|
|
1457
|
+
? { present: true, chars: next.brief.length, preview: next.brief.slice(0, 80) }
|
|
1458
|
+
: { present: false },
|
|
1399
1459
|
...(aborted ? { aborted } : {}),
|
|
1400
1460
|
abort: effectiveAbort,
|
|
1401
1461
|
revision: { from: previous.goalId, to: newGoalId, reason, at },
|
|
1402
1462
|
...(revisionLog ? { revisionLog } : {}),
|
|
1463
|
+
...briefWarning(next),
|
|
1403
1464
|
}
|
|
1404
1465
|
}
|
|
1405
1466
|
|
|
@@ -1449,7 +1510,7 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1449
1510
|
|
|
1450
1511
|
if (action === 'start') {
|
|
1451
1512
|
const { round } = await launchBackground(loop)
|
|
1452
|
-
return { goalId: loop.goalId, round, started: true }
|
|
1513
|
+
return { goalId: loop.goalId, round, started: true, ...briefWarning(loop) }
|
|
1453
1514
|
}
|
|
1454
1515
|
|
|
1455
1516
|
// Blocking round (default). Both substrates expose {round, result, dispose}.
|
|
@@ -1464,7 +1525,7 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1464
1525
|
applyDecision(loop, result.round, result, decision)
|
|
1465
1526
|
// Round settlement decision applied — republish (round / status / subgoal).
|
|
1466
1527
|
await publishBoard()
|
|
1467
|
-
return { goalId: loop.goalId, round: result.round, decision, result }
|
|
1528
|
+
return { goalId: loop.goalId, round: result.round, decision, result, ...briefWarning(loop) }
|
|
1468
1529
|
},
|
|
1469
1530
|
})
|
|
1470
1531
|
|