@jayyuen66/dsh-quality-gate 0.1.0 → 0.1.2
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/CHANGELOG.md +103 -0
- package/README.md +5 -106
- package/README.zh-CN.md +102 -0
- package/THIRD_PARTY_NOTICES.md +61 -0
- package/client.js +11 -11
- package/cordis.patch.yml +2 -2
- package/host.js +167 -107
- package/package.json +47 -38
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
格式遵循 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),版本号遵循
|
|
4
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html)。最新版本在最上。
|
|
5
|
+
|
|
6
|
+
## 0.1.1
|
|
7
|
+
|
|
8
|
+
宿主生态与工具链跟到发布线,发布面的判据补严。**门禁判定规则、四条命令白名单、注入文案与
|
|
9
|
+
默认设置都没改**,改的是「跟谁对齐」与「发出去的东西长什么样」。
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `@deepseek-ai/*` 家族从 `0.2.1-alpha.1` 升到 `0.2.1-alpha.2`:devDependencies、
|
|
14
|
+
peerDependencies 的 `@deepseek-ai/dsh`、`pnpm-workspace.yaml` 的 `overrides`、`engines.dsh`
|
|
15
|
+
四处同值(`test/publish-manifest.test.ts` 要求 `dsh-*` 与 peer 逐项相等)。
|
|
16
|
+
`@deepseek-ai/cordis` `4.0.5-alpha.1` 与 `@deepseek-ai/schemastery` `3.18.5-alpha.1` **不动**:
|
|
17
|
+
那是 alpha.2 自己钉的两枚(`~4.0.5-alpha.1` / `~3.18.5-alpha.1`),registry 上没有更新的 alpha。
|
|
18
|
+
- 上游逐包比对过发布件:本包消费的声明面没有 breaking。`dsh-agent` / `dsh-credentials` /
|
|
19
|
+
`dsh-settings` / `dsh-shell` / `dsh-sandbox-policy` / `dsh-package-manifest` 的 shipped 内容
|
|
20
|
+
与 alpha.1 逐字节相同;`dsh-session` / `dsh-session-query` / `dsh-client-ui-plugin-manager`
|
|
21
|
+
只有增量。打包契约(`cordis.patch.yml`、`dsh.bundle.patch`、`dsh.client`、`manifestVersion: 1`)
|
|
22
|
+
不变,因此 `host.ts` 无必须的适配改动。
|
|
23
|
+
- `@jayyuen66/dsh-plugin-shared` `^0.1.2` → `^0.1.4`。这一枚改变了**发布物形状**:0.1.2 把
|
|
24
|
+
`@deepseek-ai/dsh-jobs` / `dsh-tools` / `dsh-session` 等宿主包列在自己的 `dependencies` 里,
|
|
25
|
+
消费方 profile(`nodeLinker: hoisted` + `autoInstallPeers: false`)于是会物化出第二份宿主
|
|
26
|
+
类型面——正是「`@deepseek-ai/*` 不进 dependencies」那条门禁管不到的依赖树下一层。0.1.3 把它们
|
|
27
|
+
挪进 `peerDependencies`,0.1.4 再把那几枚 peer 钉到 `0.2.1-alpha.2`。
|
|
28
|
+
- 入口面:`main` / `exports` 指向构建产物 `host.js` / `client.js`,入口覆盖不再写在
|
|
29
|
+
`publishConfig` 里(pnpm 发布时合并到顶层、npm 忽略并打印 Unknown publishConfig——同一仓库
|
|
30
|
+
换个发布器就会发出一个 import 必失败的包)。
|
|
31
|
+
- 工具链(`pnpm-workspace.yaml` 的 `catalog`)跟到发布线:`@types/node` 26.6.3→26.6.5、
|
|
32
|
+
`vitest` 与 `@vitest/coverage-v8` 5.0.2→5.0.3、`vite` 8.3.1→8.3.4、`rolldown` 1.2.11→1.2.13、
|
|
33
|
+
`oxlint` 1.86.0→1.87.0、`oxfmt` 0.71.0→0.72.0、`oxlint-tsgolint` 7.0.2003→7.0.2004。
|
|
34
|
+
`typescript` 7.0.2、`react` / `@types/react` 19.3.0、`eslint-plugin-sonarjs` 4.2.2 已在发布线。
|
|
35
|
+
- 新增 `engines.node`(`>=22.12.0`):产物用到 `AbortSignal.any` 这类较新的运行面。
|
|
36
|
+
- `tsconfig.client.json` 删掉 `"types": ["node"]` 这枚本地覆盖:shared 的 client 预设本来就是
|
|
37
|
+
`types: []`,这一枚覆盖把 Node 全局又塞回了 client 半。删掉后 client 工程与宿主预设同形,
|
|
38
|
+
类型检查照绿——client 面从未依赖 Node 全局。
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- 文档拆分:`README.md` 为英文入口、`README.zh-CN.md` 为中文,两行头部互链。
|
|
43
|
+
- `CHANGELOG.md` 与 `THIRD_PARTY_NOTICES.md`(ECC `stop-format-typecheck` /
|
|
44
|
+
`post-edit-accumulator` 的机制来源与差异表),两者一并写进 `files`。
|
|
45
|
+
- `test/publish-manifest.test.ts` 的发布面门禁:
|
|
46
|
+
- 「仓库根目录每个 `.md` 都必须出现在发布面里」——只有 readme 形态的文件靠发布器无条件附带,
|
|
47
|
+
`CHANGELOG.md` 之类不写进 `files` 就发不出去,而"文档写了但读者看不到"此前无人能发现。
|
|
48
|
+
- `ALWAYS_SHIPPED` 改为按 `pnpm pack --dry-run` 实测的四枚,删掉不成立的条目。
|
|
49
|
+
- 构建产物的内容扫描:随包发布的 `host.js` / `client.js` 里不许出现日期、本机绝对路径、
|
|
50
|
+
pnpm 的 store 目录名。
|
|
51
|
+
- `engines.dsh` 必须等于 peer `@deepseek-ai/dsh` 去掉区间算子的下界;`engines.node` 不得
|
|
52
|
+
低于宿主发布线。
|
|
53
|
+
- 注释门禁(同一文件)从「只禁完整日期」扩到:只到月份的日期、指向作者主目录的本机路径、
|
|
54
|
+
pnpm 的 store 目录名、内部工作项编号(字母+数字形态)、指向未随包发布的笔记的行号指针、
|
|
55
|
+
上游源码仓的工作区路径,以及描述工作过程而非约束本身的那一类词(黑名单逐条见
|
|
56
|
+
`test/publish-manifest.test.ts` 的 `FORBIDDEN_IN_COMMENTS`)。据此把 `host.ts`、`lib/*.ts`、
|
|
57
|
+
`src/*.ts` 的注释重写了一遍:只留约束本身,删掉过程记录。指向已安装依赖的声明文件行号不在
|
|
58
|
+
禁面内——版本钉住之后那是人人可查的证据。
|
|
59
|
+
- `pack-check.mjs` + `check:pack` 脚本:对 `pnpm pack` 产出的**真 tarball** 做体检——入口与
|
|
60
|
+
无条件附带项都在包里、源码与本地状态目录没混进去、`@deepseek-ai/*` 没落进 `dependencies`、
|
|
61
|
+
代码面里每个裸说明符都由 dependencies ∪ peerDependencies 提供、产物内容不含日期与本机路径。
|
|
62
|
+
已接进 `check`,因此本地与 CI 同一口径。
|
|
63
|
+
- `test/dist-size.test.ts`:两个构建产物的字节预算。判据落在体积上是因为「依赖被内联进产物」
|
|
64
|
+
这一类回归没有更早的报警器——类型检查与单测都不看产物。
|
|
65
|
+
- `ci.yml`:push / PR 上跑 `pnpm install --frozen-lockfile` + `pnpm run check`(含上面那条
|
|
66
|
+
tarball 体检),并断言门禁没有改写受版本控制的文件。运行时只跑 node 24,与发布流程同一条线。
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- `@deepseek-ai/dsh-credentials` 的声明位置与注释不一致:注释说它落在 `dependencies`,
|
|
71
|
+
实际由「`@deepseek-ai/*` 不进 dependencies」的门禁禁掉。注释按真实机制改写——它靠
|
|
72
|
+
**peerDependencies** 进入 `build-host.mjs` 的外部化面(外部化集合 = dependencies ∪
|
|
73
|
+
peerDependencies),因此产物保留裸说明符、官方函数体不被内联。
|
|
74
|
+
- README 里「另一枚运行期依赖是 `@deepseek-ai/schemastery`」的措辞:那一枚由宿主解析、
|
|
75
|
+
不由本包安装,文档改为按真实来源描述,并补上宿主版本要求。
|
|
76
|
+
|
|
77
|
+
### Not changed, deliberately
|
|
78
|
+
|
|
79
|
+
- 四条门禁命令的白名单与优先级、`not-run` 与 `code-failure` 的分界、注入配额、预算分摊、
|
|
80
|
+
默认设置值、双语文案内容。
|
|
81
|
+
- `@deepseek-ai/*` 一律 type-only(`dsh-credentials` 的两枚纯字符串函数除外)。
|
|
82
|
+
- 覆盖率阈值仍由 `@jayyuen66/dsh-plugin-shared/config/vitest.base` 单源给出(100%),
|
|
83
|
+
本包不放宽;`coverageInclude` 仍只覆盖 host 半(`host.ts` + `lib/*.ts`),client 半没有
|
|
84
|
+
DOM 环境可跑,见 `test/client-card.test.ts` 的说明。
|
|
85
|
+
|
|
86
|
+
### Tests
|
|
87
|
+
|
|
88
|
+
- 门禁全套通过:typecheck(两个 tsconfig)、`oxlint`(0 warning / 0 error)、两个 rolldown
|
|
89
|
+
构建、`vitest run --coverage`、`oxfmt --check`。
|
|
90
|
+
- 构建产物在真实 Node 上直接 `import()` 装载成功,裸说明符全部解析得到。
|
|
91
|
+
- `pnpm pack` 的 tarball 逐项目检:入口指向构建产物、`@deepseek-ai/*` 不在
|
|
92
|
+
`dependencies`、两份 README 与两份新文档都在发布面里。
|
|
93
|
+
|
|
94
|
+
## 0.1.0
|
|
95
|
+
|
|
96
|
+
`@jayyuen66` 作用域下的首个公共 npm 发布。
|
|
97
|
+
|
|
98
|
+
### Added
|
|
99
|
+
|
|
100
|
+
- 回合收口质量门禁:`agent/turn-stopping` 按项目形态跑 `pnpm check` / `cargo check` /
|
|
101
|
+
`ruff check .` / `npx tsc --noEmit`,未过则注入修复指令开新回合。
|
|
102
|
+
- 显式三态判定(`pass` / `code-failure` / `not-run`)与会话级配额、总预算、编辑记账。
|
|
103
|
+
- 设置卡(web client 面)与中英双语文案;记忆网关反馈默认关闭,密钥走宿主凭据通道。
|
package/README.md
CHANGED
|
@@ -1,108 +1,6 @@
|
|
|
1
1
|
# @jayyuen66/dsh-quality-gate
|
|
2
2
|
|
|
3
|
-
[中文](
|
|
4
|
-
|
|
5
|
-
## 中文
|
|
6
|
-
|
|
7
|
-
### 它做什么
|
|
8
|
-
|
|
9
|
-
- 回合收口质量门禁:本轮改过代码/配置文件时,在回合收口前自动跑该项目自己声明的检查命令;不通过就经 `agent.followup` 开新回合、把修复指令注入给模型。
|
|
10
|
-
- 一次检查只有三种结论:`pass`(真的跑完且 exit 0)、`code-failure`(跑了且报代码错)、`not-run`(没跑成)。一条没真正跑完的检查绝不会被记成通过。
|
|
11
|
-
- 它不改代码、不拦截危险命令(那是 danger-guard 的职责)、也不写任何文件。
|
|
12
|
-
|
|
13
|
-
### 触发时机与不触发条件
|
|
14
|
-
|
|
15
|
-
- 触发点:宿主事件 `agent/turn-stopping`。流程是读增量事件窗 → 取本会话**成功**的编辑 → 每个文件向上找最近清单根 → 探测门禁 → 执行。
|
|
16
|
-
- 编辑口径:`edit` / `write` / `str_replace_editor` 的写入;`view` 只读不算。
|
|
17
|
-
- 清单根:最多向上找 10 层;monorepo 子包升到 `pnpm-workspace.yaml` 所在的工作区根。
|
|
18
|
-
- 不触发:
|
|
19
|
-
- `enabled: false`;本回合没有可检编辑(只改 md/txt/图片/`.sh`,或被拒、执行失败的编辑)。
|
|
20
|
-
- 子代理会话(`origin === "subagent"` 或 `delegationDepth > 0`);inbox 已有待处理消息(不抢话)。
|
|
21
|
-
- 探测不出门禁的根;回合 signal 已取消或插件已卸载。
|
|
22
|
-
- 宿主没有 shell 服务时 warn 一次并放行;plan mode 不豁免——规划回合改了代码同样收口。
|
|
23
|
-
|
|
24
|
-
### 安装
|
|
25
|
-
|
|
26
|
-
```sh
|
|
27
|
-
dsh plugin --profile web add @jayyuen66/dsh-quality-gate
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
- 包在公共 npm(`registry.npmjs.org`)上,安装侧不需要凭据。
|
|
31
|
-
- 运行时依赖 `@jayyuen66/dsh-plugin-shared` 必须已在同一 registry 上,否则整组 404。
|
|
32
|
-
- 另一枚运行期依赖是 `@deepseek-ai/schemastery`(宿主 fork):0.1.7 的 `.volatile()` 活引用只有它解析得出来,装公共 `schemastery` 会让改设置要重启才生效。
|
|
33
|
-
- 本地目录形态安装需 pnpm ≥ 12.3.0(12.0.0 的本地目录 `add` 有已知缺陷)。
|
|
34
|
-
|
|
35
|
-
### 在 dsh 里启用
|
|
36
|
-
|
|
37
|
-
- 装载由包内 `cordis.patch.yml` 负责(`package.json` 的 `dsh.bundle.patch` 指向它),插一行 `- id: quality-gate`、`name` 为本包名;`dsh plugin --profile web add/remove` 维护该行。
|
|
38
|
-
- Host 半只硬依赖宿主的 `settings`(`default.inject = ["settings"]`);`shell` / `sandboxPolicy` / `lessonLoop` / `credentials` / `sessionQuery` 一律 `ctx.get` 可选读,缺失不会让插件加载失败。
|
|
39
|
-
- 优先级:设置卡的运行时值 > 该行 `config:` > 包内置默认。client 半(`dsh.client.platform = web`、`immediately: true`)往插件管理页的 `plugins.bundle.config` 槽注入一张卡(key = 本包名;`configForms.get()` 与 settings 命名空间用的是裸条目 id `quality-gate`)。
|
|
40
|
-
- 卡的保存是显式的:改动先留在本地暂存,点「保存」才逐键写 settings(状态条提示「有未保存的修改,点「保存」生效」)。
|
|
41
|
-
- 写入通道把 `Promise<boolean>` 受理位原样交给卡片——首个 `false` 即停手、显示 `saveRejected` 那行并保留未落盘的改动(旧版是盲 `setTimeout` 清暂存,宿主拒写时卡仍显示已保存);写入 reject 显示 `saveFailed`,「撤销」丢弃暂存。
|
|
42
|
-
|
|
43
|
-
### 设置的检查命令
|
|
44
|
-
|
|
45
|
-
四条白名单命令在类型层锁死(只有这四条能进执行面),每根择一,优先级 workspace > Rust > Python > 裸 TS:
|
|
46
|
-
|
|
47
|
-
- `pnpm check`:根同时有 `pnpm-workspace.yaml` 与 `package.json`,且根 scripts 声明了非空 `check`;没声明就退回下一候选——宁可不给门禁,也不替项目发明一条稳定假失败的命令。
|
|
48
|
-
- `cargo check`(`Cargo.toml`)、`ruff check .`(`pyproject.toml` 或 `ruff.toml`)、`npx tsc --noEmit`(`package.json` + `tsconfig.json`,缺 `tsconfig.json` 不算 TS 项目)。
|
|
49
|
-
- 预算与并发:总预算 `gateBudgetMs` 封顶,每根取 `max(10s, 预算/根数)` 与实际剩余的小值。
|
|
50
|
-
- 剩余不足 5s 的尾段不再起跑,按「未执行」注入清单。
|
|
51
|
-
- 门禁顺序执行,一次只注入一个失败。
|
|
52
|
-
- 证据上限:每条流内存留存 64 KiB,被截断时从落盘文件有界读回 4 KiB 头部(首条错误才是根因);单会话编辑记账 500 个文件,超出按插入序淘汰并 warn。
|
|
53
|
-
|
|
54
|
-
### 设置项
|
|
55
|
-
|
|
56
|
-
- `enabled`:boolean,默认 `true`。关掉即收口不再核验。
|
|
57
|
-
- `maxInjectsPerTurn`:1–5,默认 `2`。修复指令注入配额,按会话跨回合累计。
|
|
58
|
-
- `gateBudgetMs`:10000–600000,默认 `300000`。超时按失败注入,不静默放行。
|
|
59
|
-
- `memoryFeedback`:boolean,默认 `false`。是否把代码型失败额外沉淀到记忆网关。
|
|
60
|
-
- `memoryGatewayUrl`:string,默认空 = 不启用;设置值优先,留空回落 env `TDAI_GATEWAY_URL`。
|
|
61
|
-
- `memoryGatewayKeyRef`:string(凭据引用名),默认 `TDAI_GATEWAY_KEY`;设置里只存引用名。
|
|
62
|
-
- 另有两枚非 volatile 的部署值:设置卡上没有它们的行,只在注册行的 `config:` 上给(cordis 交进 apply 的是值而非引用,改值随重启生效)。
|
|
63
|
-
- `maxRootDepth` 默认 `10`——「最多向上找 10 层」说的就是它;`gateStdoutMaxBytes` 默认 `65536`——「每条流内存留存 64 KiB」说的就是它。
|
|
64
|
-
|
|
65
|
-
### 对外接口
|
|
66
|
-
|
|
67
|
-
- 无 HTTP 面:本包不注册任何路由或端点,也没有供外部调用的接口。
|
|
68
|
-
- 对宿主:settings 命名空间 `quality-gate` 是 0.1.7 的隐式注册(= 条目 id,可编辑字段由条目 `Config` 上标了 `.volatile()` 的项决定,本包不再调 `settings.register`),schema 与行 `config` 共用同一份 `Config`。
|
|
69
|
-
- 只剩页面策略要声明:`svc.inject(["settings"], …)` 的子上下文里 `settings.configure({ auto: false }, svc.fiber)`——本包自带卡片,别让宿主再生成一份自动表单。
|
|
70
|
-
- 注入的 followup 为 `role: "user"`、`source: { kind: "plugin:quality-gate" }`、id 为 `quality-gate-` 前缀加一个 UUID。
|
|
71
|
-
- 日志走宿主具名 logger(`ctx.logger("quality-gate")`,本包用的方法面是 `info` / `warn` / `error`),该服务缺席才回退 `console`。
|
|
72
|
-
- 注入给模型的文案只有四类,消息统一带 `[quality-gate]` 前缀与命令/根上下文:
|
|
73
|
-
- 修复指令。
|
|
74
|
-
- 门禁「未能执行」说明。
|
|
75
|
-
- 配额耗尽后的放行说明。
|
|
76
|
-
- 嵌在「未能执行」里的预算不足清单。
|
|
77
|
-
- 两语文案随官方 settings 命名空间 `locale` 的偏好,未注册即中文,改语言下一回合生效。
|
|
78
|
-
- 卡片文案另走官方 client locale:`ctx.locale.register(NS, UI_MESSAGES)` 交字典、`ctx.locale.bind(NS)` 出取文案函数,切语言由宿主重渲染 slot、不必重载页面。
|
|
79
|
-
- 对总线:`ctx.get("lessonLoop")?.report({ source: "quality-gate", category: "gate-failure" | "gate-not-run", cwd, sessionId, turn, signature: 命令串, detail: 全文证据, evidence: { root, command, kind } })`。
|
|
80
|
-
- 可选外发:向 `memoryGatewayUrl` 配的地址发 `POST /v2/conversation/add`,5s 超时封顶。
|
|
81
|
-
- 请求头带 Bearer 令牌与 `x-tdai-service-id`(env 可覆盖,默认 `default`)。
|
|
82
|
-
- body 为 `session_id: "dsh-agent"` + 一条 `assistant` 消息,`agent_id` 取会话 cwd 的尾目录名加 8 位 sha256 前缀(与 lesson-loop 项目分桶同源)。
|
|
83
|
-
|
|
84
|
-
### 数据与隐私
|
|
85
|
-
|
|
86
|
-
- 默认零外发:`memoryFeedback` 关 + 地址空是双保险,任一为空都不推送、也不去查凭据。
|
|
87
|
-
- 开启后送出的只有命令、项目根与全文报错(不截断),落到你自配的网关。
|
|
88
|
-
- 失败只 warn 且每次装载只说一次,绝不阻断收口。
|
|
89
|
-
- 密钥值由宿主凭据服务持有(`$DSH_HOME` 下的凭据文件),引用名不合 POSIX 环境变量语法即视为未配置。
|
|
90
|
-
- 与 lesson-loop 的关系是**可选**的:总线缺失时报告直接丢弃,门禁照常运行;装了才多一份教训沉淀,`memoryFeedback` 开关不影响总线上报。
|
|
91
|
-
- 本包只读盘(清单存在性、根 `package.json` 的 scripts、截断落盘文件的头部),不写文件、不遥测。
|
|
92
|
-
|
|
93
|
-
### 常见问题
|
|
94
|
-
|
|
95
|
-
- pnpm 工程为什么没跑 `pnpm check`?根 `package.json` 没声明非空 `check` 脚本,这是刻意取舍:假失败比漏检查更贵。
|
|
96
|
-
- 门禁说「未能执行」是代码错了吗?不是。
|
|
97
|
-
- 沙箱策略拒绝、runner 起不来、超时、取消、exit 126/127、`npx` 抓到 stub 包、预算不足都属此类。
|
|
98
|
-
- 注入文案明确要求「不要为此修改代码」,不写记忆,并以 `gate-not-run` 单独上报总线。
|
|
99
|
-
- 会不会无限修?配额耗尽后注入一次用户可见的「放行说明」并停止自动修复,直到某次全部干净通过才重新计数。
|
|
100
|
-
- 临时停掉:设置卡关 `enabled`,或 `dsh plugin --profile web remove @jayyuen66/dsh-quality-gate`。
|
|
101
|
-
- 子代理改的代码呢?in-process 子代理跳过,由父会话下一回合统一核验。
|
|
102
|
-
- out-of-process 子代理(ACP/claude-code/codex)不发 dsh 事件,天然不在门禁面内。
|
|
103
|
-
- 许可 MIT,仓库见 `package.json` 的 `repository.url`。
|
|
104
|
-
|
|
105
|
-
## English
|
|
3
|
+
[中文](./README.zh-CN.md)
|
|
106
4
|
|
|
107
5
|
### What it does
|
|
108
6
|
|
|
@@ -114,7 +12,7 @@ dsh plugin --profile web add @jayyuen66/dsh-quality-gate
|
|
|
114
12
|
|
|
115
13
|
- Trigger: the host event `agent/turn-stopping`. Flow: read the incremental event window → take this session's **successful** edits → walk each file up to the nearest manifest root → detect the gate → run it.
|
|
116
14
|
- Edits counted: `edit` / `write` / `str_replace_editor` writes; `view` is read-only and does not count.
|
|
117
|
-
- Manifest root: searched up from each file, max 10 levels; a monorepo package is upgraded to the workspace root holding `pnpm-workspace.yaml
|
|
15
|
+
- Manifest root: searched up from each file, max 10 levels; a monorepo package is upgraded to the workspace root holding `pnpm-workspace.yaml` only when that root yields a gate — otherwise the package keeps its own root (an outer workspace without a `check` script must not swallow the package's `tsc`/`ruff` gate).
|
|
118
16
|
- Skipped when:
|
|
119
17
|
- `enabled: false`; the turn has no checkable edit (only md/txt/images/`.sh`, or edits that were denied or failed).
|
|
120
18
|
- Subagent sessions (`origin === "subagent"` or `delegationDepth > 0`); the inbox already has pending items (the gate does not talk over the user).
|
|
@@ -128,8 +26,9 @@ dsh plugin --profile web add @jayyuen66/dsh-quality-gate
|
|
|
128
26
|
```
|
|
129
27
|
|
|
130
28
|
- The packages live on the public npm registry, so installs need no credentials.
|
|
29
|
+
- Requires a host at `@deepseek-ai/dsh` 0.2.1-rc.2 or newer (`engines.dsh`); the plugin is compiled against that host's type surface.
|
|
131
30
|
- The runtime dependency `@jayyuen66/dsh-plugin-shared` must already be on the same registry, otherwise the whole set 404s.
|
|
132
|
-
-
|
|
31
|
+
- `@deepseek-ai/schemastery` is resolved from the host, not installed by this package: the host's own fork is the only one whose `resolve` wraps `.volatile()` fields into live references, so a public `schemastery` would turn "edit a setting" into "restart to apply".
|
|
133
32
|
- Installing a local directory needs pnpm ≥ 12.3.0 (12.0.0 has a known bug in that path).
|
|
134
33
|
|
|
135
34
|
### Enabling it in dsh
|
|
@@ -139,7 +38,7 @@ dsh plugin --profile web add @jayyuen66/dsh-quality-gate
|
|
|
139
38
|
- Precedence: card runtime value > the line's `config:` > built-in default.
|
|
140
39
|
- The client half (`dsh.client.platform = web`, `immediately: true`) injects a card into the `plugins.bundle.config` slot of the plugin page (key = this package name, while `configForms.get()` uses the bare entry id `quality-gate`).
|
|
141
40
|
- Saving on the card is staged: edits stay in local pending state until Save writes them key by key into settings.
|
|
142
|
-
- The write channel hands the card the `Promise<boolean>` acceptance bit verbatim: the first `false` stops the loop, shows the `saveRejected` line and keeps the unwritten changes - the
|
|
41
|
+
- The write channel hands the card the `Promise<boolean>` acceptance bit verbatim: the first `false` stops the loop, shows the `saveRejected` line and keeps the unwritten changes - swallowing the acceptance bit would let the card claim "saved" while the host had refused. A rejecting write shows `saveFailed`; Revert discards the pending edits.
|
|
143
42
|
|
|
144
43
|
### The commands it runs
|
|
145
44
|
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @jayyuen66/dsh-quality-gate
|
|
2
|
+
|
|
3
|
+
[English](./README.md)
|
|
4
|
+
|
|
5
|
+
### 它做什么
|
|
6
|
+
|
|
7
|
+
- 回合收口质量门禁:本轮改过代码/配置文件时,在回合收口前自动跑该项目自己声明的检查命令;不通过就经 `agent.followup` 开新回合、把修复指令注入给模型。
|
|
8
|
+
- 一次检查只有三种结论:`pass`(真的跑完且 exit 0)、`code-failure`(跑了且报代码错)、`not-run`(没跑成)。一条没真正跑完的检查绝不会被记成通过。
|
|
9
|
+
- 它不改代码、不拦截危险命令(那是 danger-guard 的职责)、也不写任何文件。
|
|
10
|
+
|
|
11
|
+
### 触发时机与不触发条件
|
|
12
|
+
|
|
13
|
+
- 触发点:宿主事件 `agent/turn-stopping`。流程是读增量事件窗 → 取本会话**成功**的编辑 → 每个文件向上找最近清单根 → 探测门禁 → 执行。
|
|
14
|
+
- 编辑口径:`edit` / `write` / `str_replace_editor` 的写入;`view` 只读不算。
|
|
15
|
+
- 清单根:最多向上找 10 层;monorepo 子包升到 `pnpm-workspace.yaml` 所在的工作区根——但只在该根**产得出门禁**时升级,否则保留子包自己的根(外层没有 `check` 脚本的 workspace 不得吃掉子包的 `tsc` / `ruff` 门禁)。
|
|
16
|
+
- 不触发:
|
|
17
|
+
- `enabled: false`;本回合没有可检编辑(只改 md/txt/图片/`.sh`,或被拒、执行失败的编辑)。
|
|
18
|
+
- 子代理会话(`origin === "subagent"` 或 `delegationDepth > 0`);inbox 已有待处理消息(不抢话)。
|
|
19
|
+
- 探测不出门禁的根;回合 signal 已取消或插件已卸载。
|
|
20
|
+
- 宿主没有 shell 服务时 warn 一次并放行;plan mode 不豁免——规划回合改了代码同样收口。
|
|
21
|
+
|
|
22
|
+
### 安装
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
dsh plugin --profile web add @jayyuen66/dsh-quality-gate
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- 包在公共 npm(`registry.npmjs.org`)上,安装侧不需要凭据。
|
|
29
|
+
- 宿主需 `@deepseek-ai/dsh` 0.2.1-rc.2 起(`engines.dsh`):本包按这一版宿主的类型面编译。
|
|
30
|
+
- 运行时依赖 `@jayyuen66/dsh-plugin-shared` 必须已在同一 registry 上,否则整组 404。
|
|
31
|
+
- `@deepseek-ai/schemastery` 由宿主解析、不由本包安装:只有宿主 fork 的那一枚能把 `.volatile()` 字段解析成活引用,换成公共 `schemastery` 就变成改设置要重启才生效。
|
|
32
|
+
- 本地目录形态安装需 pnpm ≥ 12.3.0(12.0.0 的本地目录 `add` 有已知缺陷)。
|
|
33
|
+
|
|
34
|
+
### 在 dsh 里启用
|
|
35
|
+
|
|
36
|
+
- 装载由包内 `cordis.patch.yml` 负责(`package.json` 的 `dsh.bundle.patch` 指向它),插一行 `- id: quality-gate`、`name` 为本包名;`dsh plugin --profile web add/remove` 维护该行。
|
|
37
|
+
- Host 半只硬依赖宿主的 `settings`(`default.inject = ["settings"]`);`shell` / `sandboxPolicy` / `lessonLoop` / `credentials` / `sessionQuery` 一律 `ctx.get` 可选读,缺失不会让插件加载失败。
|
|
38
|
+
- 优先级:设置卡的运行时值 > 该行 `config:` > 包内置默认。client 半(`dsh.client.platform = web`、`immediately: true`)往插件管理页的 `plugins.bundle.config` 槽注入一张卡(key = 本包名;`configForms.get()` 与 settings 命名空间用的是裸条目 id `quality-gate`)。
|
|
39
|
+
- 卡的保存是显式的:改动先留在本地暂存,点「保存」才逐键写 settings(状态条提示「有未保存的修改,点「保存」生效」)。
|
|
40
|
+
- 写入通道把 `Promise<boolean>` 受理位原样交给卡片——首个 `false` 即停手、显示 `saveRejected` 那行并保留未落盘的改动(吞掉受理位会让宿主拒写时卡仍显示已保存);写入 reject 显示 `saveFailed`,「撤销」丢弃暂存。
|
|
41
|
+
|
|
42
|
+
### 设置的检查命令
|
|
43
|
+
|
|
44
|
+
四条白名单命令在类型层锁死(只有这四条能进执行面),每根择一,优先级 workspace > Rust > Python > 裸 TS:
|
|
45
|
+
|
|
46
|
+
- `pnpm check`:根同时有 `pnpm-workspace.yaml` 与 `package.json`,且根 scripts 声明了非空 `check`;没声明就退回下一候选——宁可不给门禁,也不替项目发明一条稳定假失败的命令。
|
|
47
|
+
- `cargo check`(`Cargo.toml`)、`ruff check .`(`pyproject.toml` 或 `ruff.toml`)、`npx tsc --noEmit`(`package.json` + `tsconfig.json`,缺 `tsconfig.json` 不算 TS 项目)。
|
|
48
|
+
- 预算与并发:总预算 `gateBudgetMs` 封顶,每根取 `max(10s, 预算/根数)` 与实际剩余的小值。
|
|
49
|
+
- 剩余不足 5s 的尾段不再起跑,按「未执行」注入清单。
|
|
50
|
+
- 门禁顺序执行,一次只注入一个失败。
|
|
51
|
+
- 证据上限:每条流内存留存 64 KiB,被截断时从落盘文件有界读回 4 KiB 头部(首条错误才是根因);单会话编辑记账 500 个文件,超出按插入序淘汰并 warn。
|
|
52
|
+
|
|
53
|
+
### 设置项
|
|
54
|
+
|
|
55
|
+
- `enabled`:boolean,默认 `true`。关掉即收口不再核验。
|
|
56
|
+
- `maxInjectsPerTurn`:1–5,默认 `2`。修复指令注入配额,按会话跨回合累计。
|
|
57
|
+
- `gateBudgetMs`:10000–600000,默认 `300000`。超时按失败注入,不静默放行。
|
|
58
|
+
- `memoryFeedback`:boolean,默认 `false`。是否把代码型失败额外沉淀到记忆网关。
|
|
59
|
+
- `memoryGatewayUrl`:string,默认空 = 不启用;设置值优先,留空回落 env `TDAI_GATEWAY_URL`。
|
|
60
|
+
- `memoryGatewayKeyRef`:string(凭据引用名),默认 `TDAI_GATEWAY_KEY`;设置里只存引用名。
|
|
61
|
+
- 另有两枚非 volatile 的部署值:设置卡上没有它们的行,只在注册行的 `config:` 上给(cordis 交进 apply 的是值而非引用,改值随重启生效)。
|
|
62
|
+
- `maxRootDepth` 默认 `10`——「最多向上找 10 层」说的就是它;`gateStdoutMaxBytes` 默认 `65536`——「每条流内存留存 64 KiB」说的就是它。
|
|
63
|
+
|
|
64
|
+
### 对外接口
|
|
65
|
+
|
|
66
|
+
- 无 HTTP 面:本包不注册任何路由或端点,也没有供外部调用的接口。
|
|
67
|
+
- 对宿主:settings 命名空间 `quality-gate` 是 0.1.7 的隐式注册(= 条目 id,可编辑字段由条目 `Config` 上标了 `.volatile()` 的项决定,本包不再调 `settings.register`),schema 与行 `config` 共用同一份 `Config`。
|
|
68
|
+
- 只剩页面策略要声明:`svc.inject(["settings"], …)` 的子上下文里 `settings.configure({ auto: false }, svc.fiber)`——本包自带卡片,别让宿主再生成一份自动表单。
|
|
69
|
+
- 注入的 followup 为 `role: "user"`、`source: { kind: "plugin:quality-gate" }`、id 为 `quality-gate-` 前缀加一个 UUID。
|
|
70
|
+
- 日志走宿主具名 logger(`ctx.logger("quality-gate")`,本包用的方法面是 `info` / `warn` / `error`),该服务缺席才回退 `console`。
|
|
71
|
+
- 注入给模型的文案只有四类,消息统一带 `[quality-gate]` 前缀与命令/根上下文:
|
|
72
|
+
- 修复指令。
|
|
73
|
+
- 门禁「未能执行」说明。
|
|
74
|
+
- 配额耗尽后的放行说明。
|
|
75
|
+
- 嵌在「未能执行」里的预算不足清单。
|
|
76
|
+
- 两语文案随官方 settings 命名空间 `locale` 的偏好,未注册即中文,改语言下一回合生效。
|
|
77
|
+
- 卡片文案另走官方 client locale:`ctx.locale.register(NS, UI_MESSAGES)` 交字典、`ctx.locale.bind(NS)` 出取文案函数,切语言由宿主重渲染 slot、不必重载页面。
|
|
78
|
+
- 对总线:`ctx.get("lessonLoop")?.report({ source: "quality-gate", category: "gate-failure" | "gate-not-run", cwd, sessionId, turn, signature: 命令串, detail: 全文证据, evidence: { root, command, kind } })`。
|
|
79
|
+
- 可选外发:向 `memoryGatewayUrl` 配的地址发 `POST /v2/conversation/add`,5s 超时封顶。
|
|
80
|
+
- 请求头带 Bearer 令牌与 `x-tdai-service-id`(env 可覆盖,默认 `default`)。
|
|
81
|
+
- body 为 `session_id: "dsh-agent"` + 一条 `assistant` 消息,`agent_id` 取会话 cwd 的尾目录名加 8 位 sha256 前缀(与 lesson-loop 项目分桶同源)。
|
|
82
|
+
|
|
83
|
+
### 数据与隐私
|
|
84
|
+
|
|
85
|
+
- 默认零外发:`memoryFeedback` 关 + 地址空是双保险,任一为空都不推送、也不去查凭据。
|
|
86
|
+
- 开启后送出的只有命令、项目根与全文报错(不截断),落到你自配的网关。
|
|
87
|
+
- 失败只 warn 且每次装载只说一次,绝不阻断收口。
|
|
88
|
+
- 密钥值由宿主凭据服务持有(`$DSH_HOME` 下的凭据文件),引用名不合 POSIX 环境变量语法即视为未配置。
|
|
89
|
+
- 与 lesson-loop 的关系是**可选**的:总线缺失时报告直接丢弃,门禁照常运行;装了才多一份教训沉淀,`memoryFeedback` 开关不影响总线上报。
|
|
90
|
+
- 本包只读盘(清单存在性、根 `package.json` 的 scripts、截断落盘文件的头部),不写文件、不遥测。
|
|
91
|
+
|
|
92
|
+
### 常见问题
|
|
93
|
+
|
|
94
|
+
- pnpm 工程为什么没跑 `pnpm check`?根 `package.json` 没声明非空 `check` 脚本,这是刻意取舍:假失败比漏检查更贵。
|
|
95
|
+
- 门禁说「未能执行」是代码错了吗?不是。
|
|
96
|
+
- 沙箱策略拒绝、runner 起不来、超时、取消、exit 126/127、`npx` 抓到 stub 包、预算不足都属此类。
|
|
97
|
+
- 注入文案明确要求「不要为此修改代码」,不写记忆,并以 `gate-not-run` 单独上报总线。
|
|
98
|
+
- 会不会无限修?配额耗尽后注入一次用户可见的「放行说明」并停止自动修复,直到某次全部干净通过才重新计数。
|
|
99
|
+
- 临时停掉:设置卡关 `enabled`,或 `dsh plugin --profile web remove @jayyuen66/dsh-quality-gate`。
|
|
100
|
+
- 子代理改的代码呢?in-process 子代理跳过,由父会话下一回合统一核验。
|
|
101
|
+
- out-of-process 子代理(ACP/claude-code/codex)不发 dsh 事件,天然不在门禁面面内。
|
|
102
|
+
- 许可 MIT,仓库见 `package.json` 的 `repository.url`。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
本包自身编写的代码以 MIT 发布,见 [`LICENSE`](./LICENSE)。本文件记录**机制来源**与
|
|
4
|
+
**运行期依赖**:本包的哪一部分来自第三方、在什么意义上「来自」,以及本包自身代码与第三方代码
|
|
5
|
+
的边界在哪里。
|
|
6
|
+
|
|
7
|
+
## 机制来源:ECC(Everything Claude Code)
|
|
8
|
+
|
|
9
|
+
- 上游项目:`ECC`(Everything Claude Code)
|
|
10
|
+
- 上游仓库:https://github.com/affaan-m/ECC
|
|
11
|
+
- 上游许可:MIT,版权归 Affaan Mustafa 所有
|
|
12
|
+
- 上游机制名:`stop-format-typecheck`(回合收口跑项目自己的检查)、`post-edit-accumulator`
|
|
13
|
+
(编辑记账)
|
|
14
|
+
|
|
15
|
+
### 移植的是什么
|
|
16
|
+
|
|
17
|
+
**机制与设计思路**,两处:
|
|
18
|
+
|
|
19
|
+
1. **回合收口跑项目声明的检查**(`host.ts` 的 `agent/turn-stopping` 批处理、
|
|
20
|
+
`lib/gate-detect.ts` 按清单根择一)——收口时按项目形态运行它自己声明的检查命令,失败则把
|
|
21
|
+
报错回给模型。
|
|
22
|
+
2. **编辑记账**(`lib/accumulator.ts`)——不逐文件跑检查(太贵),而是回合收口时一次算总账:
|
|
23
|
+
本轮编辑过的代码/配置文件按插入序累计,容量到上限按插入序淘汰。
|
|
24
|
+
|
|
25
|
+
### 本包在其之上加了什么
|
|
26
|
+
|
|
27
|
+
上游是「一条 hook 命令」的形状,本包是跑在 dsh 宿主里的常驻插件,两者不等价:
|
|
28
|
+
|
|
29
|
+
| 维度 | 上游 ECC | 本包 |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| 结论形状 | 退出码二值 | 显式三态 `pass` / `code-failure` / `not-run`,没真正跑完的检查永不记为通过 |
|
|
32
|
+
| 失败处理 | 回显给人 | 经 `agent.followup` 开新回合注入修复指令,带跨回合配额与放行说明 |
|
|
33
|
+
| 预算 | 无 | 会话总预算封顶、按根分摊、尾段不足则注入「未执行」清单 |
|
|
34
|
+
| 证据 | 直接回显 | 每条流有内存上限,截断时从落盘文件有界读回**头部**(首条错误才是根因) |
|
|
35
|
+
| 项目定位 | 当前工作目录 | 从编辑路径向上找最近清单根(monorepo 子包升到工作区根),多根各自择一 |
|
|
36
|
+
| 沙箱 | 不涉及 | 按会话解析沙箱策略后执行;策略拒绝与 runner 失败归 `not-run`,不污染记忆与自进化闭环 |
|
|
37
|
+
| 语言 | 单语 | 注入文案与设置卡中英双语,随宿主 locale 偏好 |
|
|
38
|
+
|
|
39
|
+
没有复制上游的任何一行实现代码:`host.ts` / `lib/*.ts` 全部为本包编写,语言、结构、错误处理
|
|
40
|
+
都与上游不同。
|
|
41
|
+
|
|
42
|
+
## 运行期依赖
|
|
43
|
+
|
|
44
|
+
本包产物(`host.js`、`client.js`)把依赖**外部化**:产物里保留裸说明符,运行时解析到宿主
|
|
45
|
+
profile 里那一份实例,因此不存在"把第三方源码打进发布物"的情形,本文件也就不需要附各包的
|
|
46
|
+
许可证全文。
|
|
47
|
+
|
|
48
|
+
- `@jayyuen66/dsh-plugin-shared`(MIT)——唯一的普通依赖。`lib/locale`、`lib/tool-events`、
|
|
49
|
+
`lib/lesson-bus`、`lib/record`、`lib/project-key` 五个子路径来自它。
|
|
50
|
+
- `@deepseek-ai/*`(一律 MIT,由宿主提供,声明为 peer / devDependency,**不进** `dependencies`)
|
|
51
|
+
——`dsh`、`cordis`、`schemastery`、`dsh-settings`、`dsh-shell`、`dsh-sandbox`、
|
|
52
|
+
`dsh-sandbox-policy`、`dsh-session`、`dsh-session-query`、`dsh-session-projection`、
|
|
53
|
+
`dsh-session-title`、`dsh-agent`、`dsh-llm`、`dsh-credentials`、`dsh-scope`、
|
|
54
|
+
`dsh-subprocess`、`dsh-package-manifest`、`dsh-api-remotes`、`dsh-client-locale`、
|
|
55
|
+
`dsh-client-store`、`dsh-client-ui-layout`、`dsh-client-ui-plugin-manager`、
|
|
56
|
+
`dsh-client-ui-renderer`、`dsh-client-ui-settings`、`dsh-client-ui-slots`。
|
|
57
|
+
除 `dsh-credentials`(值导入 `credentialRef` / `isCredentialRefName`)与 `schemastery`
|
|
58
|
+
(值导入 `Schema`)之外,其余都只作类型消费。
|
|
59
|
+
|
|
60
|
+
依赖形状由 `test/publish-manifest.test.ts` 钉住:`@deepseek-ai/*` 出现在 `dependencies` 即失败
|
|
61
|
+
——那会在消费方 profile 里物化出第二份宿主类型面。
|
package/client.js
CHANGED
|
@@ -6,6 +6,13 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
var exports = module.exports;
|
|
7
7
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
8
8
|
let react = require("react");
|
|
9
|
+
//#region node_modules/@jayyuen66/dsh-plugin-shared/dist/card-diff.js
|
|
10
|
+
function e(e, t) {
|
|
11
|
+
let n = [];
|
|
12
|
+
for (let r of Object.keys(e)) JSON.stringify(e[r] ?? null) !== JSON.stringify(t[r] ?? null) && n.push(r);
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
9
16
|
//#region src/ui-messages.ts
|
|
10
17
|
const UI_MESSAGES = {
|
|
11
18
|
zh: {
|
|
@@ -171,12 +178,6 @@ function TextInputRow(props) {
|
|
|
171
178
|
}
|
|
172
179
|
})));
|
|
173
180
|
}
|
|
174
|
-
/** touched 层与快照的差异字段(值语义比较;undefined 与缺失等价)。 */
|
|
175
|
-
function diffTouched(touched, value) {
|
|
176
|
-
const out = [];
|
|
177
|
-
for (const key of Object.keys(touched)) if (JSON.stringify(touched[key] ?? null) !== JSON.stringify(value[key] ?? null)) out.push(key);
|
|
178
|
-
return out;
|
|
179
|
-
}
|
|
180
181
|
function SaveBar(props) {
|
|
181
182
|
const { t } = props;
|
|
182
183
|
const dis = !props.writable || props.busy;
|
|
@@ -199,9 +200,9 @@ function SaveBar(props) {
|
|
|
199
200
|
}
|
|
200
201
|
/**
|
|
201
202
|
* 逐键 await 写入并**消费受理位**(0.1.7 的 set/unset 回 Promise<boolean>:
|
|
202
|
-
* true=宿主受理;false
|
|
203
|
+
* true=宿主受理;false=拒绝或写入被跳过(官方语义:"refuses stale revisions";
|
|
203
204
|
* 传输失败才 reject)。false 或 rejection 都**不清 touched**——否则宿主拒写时
|
|
204
|
-
* 卡片仍显示"已保存"
|
|
205
|
+
* 卡片仍显示"已保存",用户修改被静默丢弃。
|
|
205
206
|
*/
|
|
206
207
|
async function writeTouched(channels) {
|
|
207
208
|
const { t, set, unset, keys, touched, setBusy, setSaveError, setTouched } = channels;
|
|
@@ -358,7 +359,7 @@ function DgCard(props) {
|
|
|
358
359
|
const [saveError, setSaveError] = (0, react.useState)(null);
|
|
359
360
|
/** 渲染取值:touched 优先,快照兜底(未触碰字段实时跟随外部快照)。 */
|
|
360
361
|
const eff = (field) => field in touched ? touched[field] : value[field];
|
|
361
|
-
const dirty =
|
|
362
|
+
const dirty = e(touched, value).length > 0;
|
|
362
363
|
const setField = (field, val) => {
|
|
363
364
|
setTouched((prev) => ({
|
|
364
365
|
...prev,
|
|
@@ -372,7 +373,7 @@ function DgCard(props) {
|
|
|
372
373
|
}));
|
|
373
374
|
};
|
|
374
375
|
const save = () => {
|
|
375
|
-
const keys =
|
|
376
|
+
const keys = e(touched, value);
|
|
376
377
|
if (keys.length === 0) return;
|
|
377
378
|
setBusy(true);
|
|
378
379
|
setSaveError(null);
|
|
@@ -472,7 +473,6 @@ function apply(ctx) {
|
|
|
472
473
|
}
|
|
473
474
|
//#endregion
|
|
474
475
|
exports.apply = apply;
|
|
475
|
-
exports.diffTouched = diffTouched;
|
|
476
476
|
exports.inject = inject;
|
|
477
477
|
return module.exports;
|
|
478
478
|
},
|
package/cordis.patch.yml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# quality-gate
|
|
1
|
+
# quality-gate:回合收口质量门禁。
|
|
2
2
|
# ECC(MIT, github.com/affaan-m/ECC)stop-format-typecheck 移植:
|
|
3
3
|
# - 编辑记账:本回合编辑过的代码/配置文件(O(1),md 等非代码面豁免);
|
|
4
4
|
# - agent/turn-stopping 批处理:从编辑路径向上探测最近清单根
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# 设置卡:enabled / 注入配额 / 门禁预算 / 记忆反馈(开关 + 网关地址 + 密钥凭据引用,
|
|
12
12
|
# 记忆反馈默认关闭且地址默认为空 = 不启用;密钥走 dsh 凭据通道,不落设置)。
|
|
13
13
|
#
|
|
14
|
-
#
|
|
14
|
+
# 组合包层:本文件由 package.json 的 dsh.bundle.patch
|
|
15
15
|
# 指向,随 `dsh plugin --profile web add/remove ./plugins/quality-gate` 维护。
|
|
16
16
|
# 行按裸包名引用本包(client-modules 只扫描裸包名条目的 dsh.client 声明)。
|
|
17
17
|
# 注意 pnpm ≥12.3.0:12.0.0 的 add 本地目录有 #14437 bug。
|
package/host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// GENERATED by build-host.mjs — edit host.ts / lib/*.ts, then run `node build-host.mjs`.
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { closeSync, existsSync, openSync, readFileSync, readSync } from "node:fs";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import path from "node:path";
|
|
@@ -30,7 +31,7 @@ const DEFAULT_MAX_FILES = 500;
|
|
|
30
31
|
var EditAccumulator = class {
|
|
31
32
|
files = /* @__PURE__ */ new Set();
|
|
32
33
|
maxFiles;
|
|
33
|
-
/** 容量淘汰累计丢弃数:drain
|
|
34
|
+
/** 容量淘汰累计丢弃数:drain 时清零(静默淘汰要可观测)。 */
|
|
34
35
|
dropped = 0;
|
|
35
36
|
constructor(options = {}) {
|
|
36
37
|
this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
@@ -44,11 +45,10 @@ var EditAccumulator = class {
|
|
|
44
45
|
this.files.add(filePath);
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* 单次淘汰的行为(删最旧 + 计数)与原样一致。
|
|
48
|
+
* 淘汰插入序最旧的一项并计数。`maxFiles` 配成 0 时调用点对空集也成立,所以取到的首项
|
|
49
|
+
* 可以不存在——那一支什么都不做,由该退化配置的用例钉住。
|
|
50
|
+
* 这里只需要**取首项这一次动作**,不需要迭代:`for…of` + 无条件 `break` 正是
|
|
51
|
+
* eslint(no-unreachable-loop) 判的「循环体只允许一次迭代」——循环结构在这里不承载任何语义。
|
|
52
52
|
*/
|
|
53
53
|
evictOldest() {
|
|
54
54
|
const [oldest] = this.files;
|
|
@@ -56,7 +56,7 @@ var EditAccumulator = class {
|
|
|
56
56
|
this.files.delete(oldest);
|
|
57
57
|
this.dropped += 1;
|
|
58
58
|
}
|
|
59
|
-
/** 自上次 drain
|
|
59
|
+
/** 自上次 drain 以来容量淘汰丢弃的文件数(>0 时 host 侧 console.warn)。 */
|
|
60
60
|
droppedCount() {
|
|
61
61
|
return this.dropped;
|
|
62
62
|
}
|
|
@@ -106,11 +106,7 @@ function gateForRoot(entry) {
|
|
|
106
106
|
};
|
|
107
107
|
else if (has("package.json") && has("tsconfig.json")) gate = {
|
|
108
108
|
root,
|
|
109
|
-
command: [
|
|
110
|
-
"npx",
|
|
111
|
-
"tsc",
|
|
112
|
-
"--noEmit"
|
|
113
|
-
]
|
|
109
|
+
command: ["tsc", "--noEmit"]
|
|
114
110
|
};
|
|
115
111
|
return gate;
|
|
116
112
|
}
|
|
@@ -144,7 +140,6 @@ const MESSAGES = {
|
|
|
144
140
|
toolMissing: "命令不存在(工具未安装)",
|
|
145
141
|
toolNotExecutable: "命令不可执行",
|
|
146
142
|
toolNotRun: "{cause},门禁没有运行。",
|
|
147
|
-
toolStub: "门禁工具本身不可用(npx 抓到的是 stub 包,不是真工具)。",
|
|
148
143
|
policyUnresolvable: "沙箱策略无法解析({message}),门禁拒绝在未确认的沙箱边界下执行。",
|
|
149
144
|
sandboxUnavailable: "沙箱在本机不可用({message}):命令被拒绝执行,未做任何检查。",
|
|
150
145
|
gateStartFailed: "门禁未能启动({message})。",
|
|
@@ -168,7 +163,6 @@ const MESSAGES = {
|
|
|
168
163
|
toolMissing: "command not found (tool not installed)",
|
|
169
164
|
toolNotExecutable: "command not executable",
|
|
170
165
|
toolNotRun: "{cause} — the gate did not run.",
|
|
171
|
-
toolStub: "The gate tool itself is unavailable (npx fetched a stub package, not the real tool).",
|
|
172
166
|
policyUnresolvable: "The sandbox policy could not be resolved ({message}); the gate refuses to run under an unconfirmed sandbox boundary.",
|
|
173
167
|
sandboxUnavailable: "The sandbox is unavailable on this machine ({message}): the command was denied and no check was performed.",
|
|
174
168
|
gateStartFailed: "The gate failed to start ({message}).",
|
|
@@ -190,8 +184,7 @@ function renderTemplate(text, params) {
|
|
|
190
184
|
//#endregion
|
|
191
185
|
//#region lib/feedback-content.ts
|
|
192
186
|
/** 门禁失败 → 记忆正文:单行化(防换行噪音)。
|
|
193
|
-
*
|
|
194
|
-
* 多根错误的中间根因吃掉;记忆网关的入库限额由网关侧把关,这里送全文。
|
|
187
|
+
* 不截断:截尾会吃掉多根错误里的中间根因;记忆网关的入库限额由网关侧把关,这里送全文。
|
|
195
188
|
* 正文语言随官方 locale 偏好走(messages 由调用方按回合取出后传进来)——这条文本会被
|
|
196
189
|
* auto-recall 注入后续会话的上下文,是模型读的,不是日志。 */
|
|
197
190
|
function buildFeedbackText({ command, root, failure }, messages) {
|
|
@@ -213,15 +206,32 @@ function envOr(key, fallback) {
|
|
|
213
206
|
}
|
|
214
207
|
/**
|
|
215
208
|
* 网关基址:设置值优先,留空时回落到 TDAI_GATEWAY_URL(逃生门),**两处都空则为空串**。
|
|
216
|
-
* 空串即"未配置"
|
|
217
|
-
*
|
|
218
|
-
*
|
|
209
|
+
* 空串即"未配置"——调用方据此不启用记忆反馈。默认值不是任何本机地址:把某个开发机的
|
|
210
|
+
* 端口写死成默认,等于让别人的每次门禁失败都去连一个不存在的服务。设置优先于 env 的理由:
|
|
211
|
+
* 设置卡是用户显式配置面,env 只能兜底,
|
|
219
212
|
* 否则用户改了设置卡仍被一个看不见的环境变量越权。
|
|
220
213
|
*/
|
|
221
214
|
function resolveGatewayUrl(settingUrl) {
|
|
222
215
|
const configured = settingUrl.trim();
|
|
223
216
|
return (configured === "" ? envOr("TDAI_GATEWAY_URL", "") : configured).replace(/\/+$/u, "");
|
|
224
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* 凭据只允许发往 https 或回环地址:明文 http 到远端会把 Bearer 令牌暴露给同网段
|
|
220
|
+
* (设置卡默认值就是本机 http,属有意用法)。返回 false 时调用方按"未配置"处理,
|
|
221
|
+
* 不解析、不发送凭据。
|
|
222
|
+
*/
|
|
223
|
+
function isCredentialSafeUrl(url) {
|
|
224
|
+
let parsed;
|
|
225
|
+
try {
|
|
226
|
+
parsed = new URL(url);
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (parsed.protocol === "https:") return true;
|
|
231
|
+
if (parsed.protocol !== "http:") return false;
|
|
232
|
+
const host = parsed.hostname.replaceAll(/^\[|\]$/gu, "");
|
|
233
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
234
|
+
}
|
|
225
235
|
/** x-tdai-service-id:env 可覆盖,默认是网关协议的匿名服务名(不含任何机器信息)。 */
|
|
226
236
|
function resolveServiceId() {
|
|
227
237
|
return envOr("TDAI_SERVICE_ID", "default");
|
|
@@ -266,12 +276,13 @@ async function pushGateFeedback(input, target, messages) {
|
|
|
266
276
|
//#region host.ts
|
|
267
277
|
/**
|
|
268
278
|
* 常见项目根标记文件:向上找最近的含清单目录。
|
|
269
|
-
* ruff.toml
|
|
279
|
+
* ruff.toml 必须在列:只有 ruff 配置、无 pyproject.toml 的 Python
|
|
270
280
|
* 工程若不在此登记,gate-detect 的 has("ruff.toml") 就是死码——那类工程根本得不到根,
|
|
271
281
|
* 也就永远没有门禁。
|
|
272
282
|
*/
|
|
283
|
+
const PKG_MANIFEST = "package.json";
|
|
273
284
|
const MANIFESTS = [
|
|
274
|
-
|
|
285
|
+
PKG_MANIFEST,
|
|
275
286
|
"Cargo.toml",
|
|
276
287
|
"pyproject.toml",
|
|
277
288
|
"pnpm-workspace.yaml",
|
|
@@ -292,14 +303,15 @@ const GATE_SOURCE_KIND = "plugin:quality-gate";
|
|
|
292
303
|
const GATE_KIND_CODE_FAILURE = "code-failure";
|
|
293
304
|
/** 设置面(= profile 条目 `quality-gate` 的可编辑表单)与 loader 行 config 共用同一份
|
|
294
305
|
* schema(单源,防漂移)。六个字段**全部** `.volatile()`:没有任何 volatile 字段的条目
|
|
295
|
-
* 会被宿主 describe() 整条跳过(
|
|
296
|
-
*
|
|
306
|
+
* 会被宿主 describe() 整条跳过(installed @deepseek-ai/dsh-settings/lib/index.js:413-418
|
|
307
|
+
* 只在 volatileForm(schema) 有值时才登记条目),写入则抛
|
|
308
|
+
* `has no volatile fields`(:505-506)。
|
|
297
309
|
* 两个策略陷阱(都锁在下面对应的行内注释里,改默认前先读):
|
|
298
310
|
* - `memoryGatewayUrl` 的默认是**空串**而不是缺省:`""` + feedback-off = 永不推送,
|
|
299
311
|
* 换成 undefined 会把"未配置"变成"字段不存在",行为不再等价;
|
|
300
312
|
* - `memoryGatewayKeyRef` 的 `.role('credential-ref')` 与 `.volatile()` **共存**:role 只
|
|
301
|
-
* 写 meta.role(fork 的 role/volatile
|
|
302
|
-
*
|
|
313
|
+
* 写 meta.role(fork 的 role/volatile 都走 `extra`=`{...meta, k: v}` 展开,见
|
|
314
|
+
* installed @deepseek-ai/schemastery/src/index.ts:385-389),不会抹掉 meta.volatile。 */
|
|
303
315
|
const Config = Schema.object({
|
|
304
316
|
enabled: Schema.boolean().default(true).volatile(),
|
|
305
317
|
maxInjectsPerTurn: Schema.natural().min(1).max(5).default(2).volatile(),
|
|
@@ -330,8 +342,8 @@ function messageOf(error) {
|
|
|
330
342
|
return message;
|
|
331
343
|
}
|
|
332
344
|
/**
|
|
333
|
-
* 会话级 Map 淘汰:把 keep 先删后插移到队尾(JS Map
|
|
334
|
-
*
|
|
345
|
+
* 会话级 Map 淘汰:把 keep 先删后插移到队尾(JS Map 迭代按插入序),再淘汰队首超出
|
|
346
|
+
* `SESSION_BUDGET` 的项 → keep 永不被淘汰,且总大小收敛。泛型复用:注入配额 Map(InjectState)、
|
|
335
347
|
* 事件游标 Map(number)与编辑累加器 Map(EditAccumulator)共用同一策略。
|
|
336
348
|
* keep 不在表内 → 直接返回(inbox-busy 分支的 injectState 就会命中:该会话从未失败过)。
|
|
337
349
|
*/
|
|
@@ -348,13 +360,12 @@ function pruneToSessionBudget(map, keep) {
|
|
|
348
360
|
}
|
|
349
361
|
}
|
|
350
362
|
/**
|
|
351
|
-
*
|
|
352
|
-
* (delegationDepth>0 或 origin==='subagent')跳过门禁。
|
|
363
|
+
* 根会话判定:子代理会话(delegationDepth>0 或 origin==='subagent')跳过门禁。
|
|
353
364
|
*
|
|
354
|
-
* agent/turn-stopping 对每个 agent(含 in-process fork/spawn
|
|
355
|
-
*
|
|
365
|
+
* agent/turn-stopping 对每个 agent(含 in-process fork/spawn 子代理)都触发,不过滤的后果有
|
|
366
|
+
* 两条:①父会话与子代理对同一项目根各跑一遍门禁(token/耗时翻倍);
|
|
356
367
|
* ②失败注入文案面向用户,却 followup 进子代理——结算前到达会多拖一轮子代理回合,
|
|
357
|
-
* 结算后到达则滞留 inbox
|
|
368
|
+
* 结算后到达则滞留 inbox 被丢弃(门禁静默失效)。所以只对根会话收口。
|
|
358
369
|
*
|
|
359
370
|
* out-of-process 子代理(ACP/claude-code/codex)
|
|
360
371
|
* 在独立进程运行,不发射 dsh 的 agent 事件,其编辑天然不在任何门禁面内;in-process
|
|
@@ -374,7 +385,7 @@ function inboxHasPending(inbox) {
|
|
|
374
385
|
* (官方 agent.ts:turn-stopping 在 running 相内 serial 分发,转 idle 发生在回合结束之后),
|
|
375
386
|
* 故绝不能用 status 判 idle。改用数据信号:turn signal 已取消、或 inbox 有待处理
|
|
376
387
|
* (用户接管/已排队工作)→ 不注入(对齐官方 "Data decides" 语义)。
|
|
377
|
-
* 插件卸载的拦截不在这里:
|
|
388
|
+
* 插件卸载的拦截不在这里:disposer abort 在飞门禁批次,迟到结果在批次层就地丢弃
|
|
378
389
|
* (见 processGate),到不了注入点——卸载收口比"注入前拦一下"更强。
|
|
379
390
|
*/
|
|
380
391
|
function preInjectCheck(agent, signal) {
|
|
@@ -392,17 +403,34 @@ function preInjectCheck(agent, signal) {
|
|
|
392
403
|
};
|
|
393
404
|
}
|
|
394
405
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* `
|
|
398
|
-
* 它打印横幅并以 exit 1 退出——不是 126/127,所以只按退出码判定会误报为代码失败。
|
|
399
|
-
* 实测:`npx --no-install` 在本机拦不住 stub(已从 npx 缓存解析),
|
|
400
|
-
* 所以必须在输出侧识别,不能只靠加 flag。
|
|
406
|
+
* 本次门禁要跑的 tsc 入口:优先工程自己装的 typescript(与工程惯用的编译器版本一致),
|
|
407
|
+
* 装不到时用本包依赖的那份(随包安装,运行期无网络动作)。两处都解析不到时回落到 PATH 上的
|
|
408
|
+
* `tsc`,由 126/127 那条「工具不可用」出口报成未执行——绝不走 `npx` 的自动下载。
|
|
401
409
|
*/
|
|
402
|
-
function
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
410
|
+
function tscEntry(root) {
|
|
411
|
+
let entry;
|
|
412
|
+
for (const from of [path.join(root, PKG_MANIFEST), import.meta.url]) try {
|
|
413
|
+
const manifest = createRequire(from).resolve("typescript/package.json");
|
|
414
|
+
entry = path.join(path.dirname(manifest), "bin", "tsc");
|
|
415
|
+
break;
|
|
416
|
+
} catch {}
|
|
417
|
+
return entry;
|
|
418
|
+
}
|
|
419
|
+
/** 门禁命令的实际 argv:tsc 的门禁只带逻辑名,入口在运行时按工程根解析。 */
|
|
420
|
+
function gateCommandArgs(gate) {
|
|
421
|
+
if (gate.command[0] !== "tsc") return gate.command;
|
|
422
|
+
/* v8 ignore next 1 —— 本包自己的 typescript 解析不到(安装面坏了)时才走 PATH 回落;
|
|
423
|
+
正常安装不可达,而那一路由 126/127 报成未执行。 */
|
|
424
|
+
return [tscEntry(gate.root) ?? "tsc", "--noEmit"];
|
|
425
|
+
}
|
|
426
|
+
/** 拼 shell 串:路径可能带空格,含空白或元字符的参数加单引号。
|
|
427
|
+
* 导出只为直接测这段引号处理:tsc 入口是绝对路径,它所在目录未必没有空格。 */
|
|
428
|
+
function shellArg(part) {
|
|
429
|
+
return /^[\w./:=@+-]+$/u.test(part) ? part : `'${part.replaceAll("'", `'\\''`)}'`;
|
|
430
|
+
}
|
|
431
|
+
/** 门禁命令的展示/执行串(与 gateCommandArgs 同源)。 */
|
|
432
|
+
function gateCommandText(gate) {
|
|
433
|
+
return gateCommandArgs(gate).map((part) => shellArg(part)).join(" ");
|
|
406
434
|
}
|
|
407
435
|
/** harness run() 抛出的 SandboxUnavailableError(code 或 name 命中即可)。 */
|
|
408
436
|
function isSandboxUnavailable(error) {
|
|
@@ -433,11 +461,11 @@ function headExcerpt(file) {
|
|
|
433
461
|
* 证据文本。
|
|
434
462
|
*
|
|
435
463
|
* tsc/cargo 等错误自顶向下,**首条才是根因**,而 harness CollectedOutput 截断时保留的是
|
|
436
|
-
* **尾部**(
|
|
437
|
-
*
|
|
464
|
+
* **尾部**(installed @deepseek-ai/dsh-subprocess/lib/types/types.d.ts:18-23)——
|
|
465
|
+
* 于是不读 `truncated` 就把根因错误静默丢掉,注入给模型的"修这些错"只剩残尾。必须:
|
|
438
466
|
* ①显式标注截断;②从 spillPath 有界读回头部摘录放在最前;③无落盘文件时明确说明
|
|
439
467
|
* 首个错误可能已丢弃,让模型与用户都知道证据不完整。
|
|
440
|
-
*
|
|
468
|
+
* 未截断时不叠加第二层截断(不为省 token 降能力,上限由 shell 的
|
|
441
469
|
* stdoutMaxBytes 把关)。
|
|
442
470
|
*/
|
|
443
471
|
function evidenceText(messages, stream, label) {
|
|
@@ -455,7 +483,7 @@ function evidenceText(messages, stream, label) {
|
|
|
455
483
|
return out;
|
|
456
484
|
}
|
|
457
485
|
const gateText = (messages, gate, out) => renderTemplate(messages.gateFailed, {
|
|
458
|
-
command: gate
|
|
486
|
+
command: gateCommandText(gate),
|
|
459
487
|
root: gate.root,
|
|
460
488
|
output: out
|
|
461
489
|
});
|
|
@@ -464,22 +492,22 @@ const gateText = (messages, gate, out) => renderTemplate(messages.gateFailed, {
|
|
|
464
492
|
* 让模型去修一个装不上的工具/越不过去的沙箱,只会配着配额环烧 token。
|
|
465
493
|
*/
|
|
466
494
|
const notRunText = (messages, gate, reason) => renderTemplate(messages.gateNotRun, {
|
|
467
|
-
command: gate
|
|
495
|
+
command: gateCommandText(gate),
|
|
468
496
|
root: gate.root,
|
|
469
497
|
reason
|
|
470
498
|
});
|
|
471
499
|
/** 配额耗尽后的"放行说明"(用户可见,不再静默 return)。 */
|
|
472
500
|
const giveUpText = (messages, gate, attempts) => renderTemplate(messages.gaveUp, {
|
|
473
501
|
attempts,
|
|
474
|
-
command: gate
|
|
502
|
+
command: gateCommandText(gate),
|
|
475
503
|
root: gate.root
|
|
476
504
|
});
|
|
477
505
|
/** 未执行清单的一行(模块级:不让 renderTemplate 套进 map 再套进 renderTemplate)。 */
|
|
478
506
|
const skippedGateLine = (messages, gate) => renderTemplate(messages.skippedGateItem, {
|
|
479
|
-
command: gate
|
|
507
|
+
command: gateCommandText(gate),
|
|
480
508
|
root: gate.root
|
|
481
509
|
});
|
|
482
|
-
/** 预算耗尽后剩余门禁的"未执行"
|
|
510
|
+
/** 预算耗尽后剩余门禁的"未执行"清单:只 warn 等于静默通过,所以要注入给模型与用户。 */
|
|
483
511
|
const skippedText = (messages, gates) => renderTemplate(messages.gatesSkipped, {
|
|
484
512
|
budgetMs: MIN_USEFUL_GATE_MS,
|
|
485
513
|
list: gates.map((gate) => skippedGateLine(messages, gate)).join("\n")
|
|
@@ -535,12 +563,12 @@ function interruptedRunOutcome(messages, budgetMs, res) {
|
|
|
535
563
|
*
|
|
536
564
|
* 环境问题非代码问题:走失败注入会让模型去"修"装不了的工具(配合配额环更糟)。
|
|
537
565
|
* 但它同样是"没跑成的检查",所以按 not-run 注入"请手动确认",不再算作通过
|
|
538
|
-
*
|
|
566
|
+
* ——没跑成 ≠ 通过。
|
|
539
567
|
*/
|
|
540
568
|
function unavailableToolOutcome(messages, gate, res, log) {
|
|
541
569
|
let outcome;
|
|
542
570
|
if (res.exitCode === 126 || res.exitCode === 127) {
|
|
543
|
-
log.warn(`[quality-gate] gate tool unavailable (exit ${res.exitCode}): ${gate
|
|
571
|
+
log.warn(`[quality-gate] gate tool unavailable (exit ${res.exitCode}): ${gateCommandText(gate)} — reported as not-run, not as pass`);
|
|
544
572
|
const cause = res.exitCode === 127 ? messages.toolMissing : messages.toolNotExecutable;
|
|
545
573
|
outcome = {
|
|
546
574
|
kind: "not-run",
|
|
@@ -552,13 +580,14 @@ function unavailableToolOutcome(messages, gate, res, log) {
|
|
|
552
580
|
/**
|
|
553
581
|
* 把一次 shell.execute().result() 结果分类成三态(纯函数:不进 ctx,每条分支都可测)。
|
|
554
582
|
*
|
|
555
|
-
* harness 的 ShellRunResult.sandbox 与退出码**正交**上报(
|
|
583
|
+
* harness 的 ShellRunResult.sandbox 与退出码**正交**上报(installed
|
|
584
|
+
* @deepseek-ai/dsh-shell/lib/types/types.d.ts:28:
|
|
556
585
|
* "Facts are reported independently of process exit status so callers can distinguish
|
|
557
|
-
* command failures from policy denials and runner failures"
|
|
558
|
-
*
|
|
586
|
+
* command failures from policy denials and runner failures"),所以必须读它——不读就会把
|
|
587
|
+
* 策略拒绝当成代码失败注入"修这些错"。
|
|
559
588
|
*
|
|
560
589
|
* 判定序(三支各自成函数,顺序与 warn 次数都不变):沙箱面拦截 → 进程没跑完 →
|
|
561
|
-
* 工具装不上(126/127)→ exit 0(部分生效沙箱只 warn)→
|
|
590
|
+
* 工具装不上(126/127)→ exit 0(部分生效沙箱只 warn)→ 代码失败。
|
|
562
591
|
*/
|
|
563
592
|
function classifyRun(messages, gate, budgetMs, res, log) {
|
|
564
593
|
const facts = res.sandbox;
|
|
@@ -571,16 +600,9 @@ function classifyRun(messages, gate, budgetMs, res, log) {
|
|
|
571
600
|
const unusable = unavailableToolOutcome(messages, gate, res, log);
|
|
572
601
|
if (unusable !== void 0) return unusable;
|
|
573
602
|
if (res.exitCode === 0) {
|
|
574
|
-
if (facts?.enforcement === "partial") log.warn(`[quality-gate] gate passed under PARTIAL sandbox enforcement (${sandboxNote(messages, facts)}): ${gate
|
|
603
|
+
if (facts?.enforcement === "partial") log.warn(`[quality-gate] gate passed under PARTIAL sandbox enforcement (${sandboxNote(messages, facts)}): ${gateCommandText(gate)} @ ${gate.root}`);
|
|
575
604
|
return { kind: "pass" };
|
|
576
605
|
}
|
|
577
|
-
if (gateToolUnavailable(res.stdout.text, res.stderr.text)) {
|
|
578
|
-
log.warn(`[quality-gate] gate tool unavailable (npx fetched a stub instead of the real tool): ${gate.command.join(" ")} in ${gate.root} — reported as not-run`);
|
|
579
|
-
return {
|
|
580
|
-
kind: "not-run",
|
|
581
|
-
text: `${messages.toolStub}\n${evidenceText(messages, res.stderr, "stderr")}`
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
606
|
return {
|
|
585
607
|
kind: GATE_KIND_CODE_FAILURE,
|
|
586
608
|
text: `exit=${res.exitCode}\n${evidenceText(messages, res.stdout, "stdout")}${evidenceText(messages, res.stderr, "stderr")}`
|
|
@@ -614,21 +636,21 @@ function isArrayDelivery(value) {
|
|
|
614
636
|
/**
|
|
615
637
|
* 解析事件游标窗口:fork 切点下限 vs 上次已读 seq 取大,返回起始 seq 与本批事件。
|
|
616
638
|
* 事件流取自官方 `ctx.sessionQuery.observeSession(sessionId)`(见上面 SessionQueryFace:
|
|
617
|
-
* 一次交回 `events` 与 `inheritedEventCount
|
|
618
|
-
* 返回,**游标不推进**、不误报):① `
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
639
|
+
* 一次交回 `events` 与 `inheritedEventCount`)。两条降级路径一律 `undefined`(调用方直接
|
|
640
|
+
* 返回,**游标不推进**、不误报):① `observeSession` 抛错/拒绝(会话已卸载、存储读不出);
|
|
641
|
+
* ② 交回的 events 不是数组(契约外交付,由 `isArrayDelivery` 那道**布尔**判据挡——不用
|
|
642
|
+
* `Array.isArray` 作守卫收窄,它会把官方 `readonly SessionEvent[]` 塌成 `any[]`,类型面
|
|
643
|
+
* 当场作废并把 any 传进下游记账)。`sessionQuery` 服务本身缺席是第三条降级,判定在
|
|
644
|
+
* `readEditedFiles`(那里才能发 warn-once)。
|
|
623
645
|
* 窗口仍由**本地按 from 切片**:观察面给的是自日志开头的连续切片(`seq = log.length`
|
|
624
|
-
* 连续性契约,installed dsh-session/lib/index.js:
|
|
646
|
+
* 连续性契约,installed dsh-session/lib/index.js:1476 的注记),而 `from` 是本包游标算出的
|
|
625
647
|
* 裸 number——这样连 `SessionLogOffset` 的品牌都不必造(官方只要 `SessionId`,那一位从
|
|
626
648
|
* `Session.id` 上拿回来就是品牌的)。
|
|
627
649
|
*/
|
|
628
650
|
async function resolveEventWindow(query, sessionId, lastSeq) {
|
|
629
651
|
let window;
|
|
630
|
-
|
|
631
|
-
const observation = await query.observeSession(sessionId);
|
|
652
|
+
try {
|
|
653
|
+
const observation = await query.observeSession(sessionId, { projectionMode: "none" });
|
|
632
654
|
const forkCut = observation.inheritedEventCount ?? 0;
|
|
633
655
|
const from = Math.max(forkCut, lastSeq.get(sessionId) ?? forkCut);
|
|
634
656
|
if (isArrayDelivery(observation.events)) window = {
|
|
@@ -661,7 +683,7 @@ function collectEdits(events) {
|
|
|
661
683
|
const noCallId = [];
|
|
662
684
|
const failed = /* @__PURE__ */ new Set();
|
|
663
685
|
const { calls, results } = scanToolEvents(events);
|
|
664
|
-
for (const call of calls) if (
|
|
686
|
+
for (const call of calls) if (isEditTool(call.name) && !call.badArguments) {
|
|
665
687
|
const target = editPathOf(call);
|
|
666
688
|
if (target.kind === "write") storeEdit(call.callId, {
|
|
667
689
|
name: call.name,
|
|
@@ -676,7 +698,7 @@ function collectEdits(events) {
|
|
|
676
698
|
};
|
|
677
699
|
}
|
|
678
700
|
/** 单文件向上找最近的清单根;hitDepthLimit 标记是否因超深而无根(供可观测 warn)。
|
|
679
|
-
* maxDepth 来自 Config 的 maxRootDepth
|
|
701
|
+
* maxDepth 来自 Config 的 maxRootDepth(非 volatile 部署值)。 */
|
|
680
702
|
function findManifestRoot(file, maxDepth) {
|
|
681
703
|
let dir = path.dirname(file);
|
|
682
704
|
let found;
|
|
@@ -746,8 +768,8 @@ const injectMessage = (text) => ({
|
|
|
746
768
|
/**
|
|
747
769
|
* 总线上报(自进化闭环):门禁失败是事实,无论注入与否都要沉淀
|
|
748
770
|
* ——修复环里同一门禁反复失败会形成 violation 序列,度量"提醒有没有用"。
|
|
749
|
-
* 但"没跑成"以 gate-not-run
|
|
750
|
-
*
|
|
771
|
+
* 但"没跑成"以 gate-not-run 单独上报:环境/策略问题不得被学成代码教训——假的失败指令
|
|
772
|
+
* 一旦既进注入又进记忆/lesson-loop,闭环就学会了错的东西。
|
|
751
773
|
* report 自身容错(同步抛错与异步拒绝经 settleLessonCall 落进同一条 warn);
|
|
752
774
|
* 总线缺失(未装 lesson-loop)时 get 返回 undefined 直接跳过。
|
|
753
775
|
*/
|
|
@@ -758,7 +780,7 @@ function reportProblemToBus(get, attribution, gate, problem, log) {
|
|
|
758
780
|
cwd: attribution.cwd,
|
|
759
781
|
sessionId: attribution.sessionId,
|
|
760
782
|
turn: attribution.turn,
|
|
761
|
-
signature: gate
|
|
783
|
+
signature: gateCommandText(gate),
|
|
762
784
|
detail: problem.text,
|
|
763
785
|
evidence: {
|
|
764
786
|
root: gate.root,
|
|
@@ -785,6 +807,10 @@ async function resolveFeedbackTarget(cfg, credentials) {
|
|
|
785
807
|
ok: false,
|
|
786
808
|
reason: "memoryGatewayUrl 未配置(留空 = 不启用记忆反馈)"
|
|
787
809
|
};
|
|
810
|
+
if (!isCredentialSafeUrl(url)) return {
|
|
811
|
+
ok: false,
|
|
812
|
+
reason: `memoryGatewayUrl ${url} 是明文 http 且非回环:Bearer 凭据只发往 https 或本机`
|
|
813
|
+
};
|
|
788
814
|
if (!isCredentialRefName(cfg.memoryGatewayKeyRef)) return {
|
|
789
815
|
ok: false,
|
|
790
816
|
reason: `memoryGatewayKeyRef "${cfg.memoryGatewayKeyRef}" 不是合法凭据引用名`
|
|
@@ -828,9 +854,8 @@ async function pushFeedbackToMemory(get, notice, cfg, messages, input) {
|
|
|
828
854
|
notice.log.warn(`[quality-gate] memory feedback skipped: ${messageOf(error)}`);
|
|
829
855
|
}
|
|
830
856
|
}
|
|
831
|
-
/**
|
|
832
|
-
*
|
|
833
|
-
* packages/llm/llm-deepseek/src/config.ts 的 `plainOptions(config)`。 */
|
|
857
|
+
/** 一次读全的解析后设置快照(`scope.get()` 的等价物):门禁按回合现读,纯值往下传,
|
|
858
|
+
* 不必让每个消费方都持有引用——所有 `.volatile()` 引用一次 `.get()` 成普通值。 */
|
|
834
859
|
function settingsOf(config) {
|
|
835
860
|
return {
|
|
836
861
|
enabled: config.enabled.get(),
|
|
@@ -858,20 +883,40 @@ function isGateHost(value) {
|
|
|
858
883
|
return typeof get === "function" && typeof on === "function" && typeof effect === "function" && typeof inject === "function" && isRecord(fiber) && typeof fieldOf(settings, "describe") === "function" && typeof fieldOf(settings, "configure") === "function";
|
|
859
884
|
}
|
|
860
885
|
/**
|
|
861
|
-
*
|
|
886
|
+
* 一个文件的最近清单根 → 实际要跑的根:workspace 根优先,**但只在外层真能产出门禁时**才升级。
|
|
887
|
+
*
|
|
888
|
+
* 只判「上面有没有 pnpm-workspace.yaml」就升级是漏门禁的一条路:外层若是"有 workspace 清单、
|
|
889
|
+
* 却没有 check 脚本也没有 tsconfig"的根,gate-detect 对它返回空表,子包本来有的
|
|
890
|
+
* `npx tsc --noEmit` / `ruff check` 会被这次上溯抹掉,且没有任何输出。
|
|
891
|
+
*/
|
|
892
|
+
function rootForFile(useRoot, maxDepth) {
|
|
893
|
+
const inner = probeProjectRoot(useRoot);
|
|
894
|
+
const upgraded = workspaceRootUpgrade(useRoot, maxDepth);
|
|
895
|
+
if (upgraded === useRoot) return inner;
|
|
896
|
+
const outer = probeProjectRoot(upgraded);
|
|
897
|
+
if (detectGate([outer]).length > 0) return outer;
|
|
898
|
+
return detectGate([inner]).length > 0 ? inner : outer;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* 从编辑文件路径向上找最近的清单根(层数上限 = Config 的 maxRootDepth,非 volatile
|
|
862
902
|
* 部署值),每个根探一次真实盘上事实(清单名 + 已声明脚本)。找不到清单根的文件跳过
|
|
863
|
-
*
|
|
903
|
+
* (无门禁面)。同一内层根只判一次盘:一轮改几百个文件时逐文件重复探盘没有意义。
|
|
864
904
|
*/
|
|
865
905
|
function projectRootsOf(files, maxDepth, log) {
|
|
866
906
|
const roots = /* @__PURE__ */ new Map();
|
|
907
|
+
const chosenByInner = /* @__PURE__ */ new Map();
|
|
867
908
|
for (const file of files) {
|
|
868
909
|
const hit = findManifestRoot(file, maxDepth);
|
|
869
910
|
const useRoot = hit.root;
|
|
870
911
|
if (useRoot === void 0) {
|
|
871
912
|
if (hit.hitDepthLimit) log.warn(`[quality-gate] no manifest within ${maxDepth} levels of ${file}; gate skipped for it`);
|
|
872
913
|
} else {
|
|
873
|
-
|
|
874
|
-
if (
|
|
914
|
+
let chosen = chosenByInner.get(useRoot);
|
|
915
|
+
if (chosen === void 0) {
|
|
916
|
+
chosen = rootForFile(useRoot, maxDepth);
|
|
917
|
+
chosenByInner.set(useRoot, chosen);
|
|
918
|
+
}
|
|
919
|
+
if (!roots.has(chosen.root)) roots.set(chosen.root, chosen);
|
|
875
920
|
}
|
|
876
921
|
}
|
|
877
922
|
return [...roots.values()];
|
|
@@ -879,15 +924,15 @@ function projectRootsOf(files, maxDepth, log) {
|
|
|
879
924
|
/**
|
|
880
925
|
* 执行一个门禁:只有 exitCode 0 才是 pass。
|
|
881
926
|
* 超时/取消/信号/策略拒绝/工具装不上都走 classifyRun 的显式三态;
|
|
882
|
-
* **catch
|
|
883
|
-
*
|
|
927
|
+
* **catch 一律不报通过**:workdir 不存在、spawn 失败、SandboxUnavailableError
|
|
928
|
+
* 都会落进 catch,而被当 PASS 就是漏门禁。
|
|
884
929
|
*/
|
|
885
930
|
async function runGate(messages, shell, gate, budgetMs, context) {
|
|
886
931
|
const { policy, signal, stdoutMaxBytes, log } = context;
|
|
887
932
|
let outcome;
|
|
888
933
|
try {
|
|
889
934
|
const spec = shell.resolve({
|
|
890
|
-
command: gate
|
|
935
|
+
command: gateCommandText(gate),
|
|
891
936
|
workdir: gate.root,
|
|
892
937
|
stdoutMaxBytes,
|
|
893
938
|
timeoutMs: budgetMs,
|
|
@@ -896,7 +941,7 @@ async function runGate(messages, shell, gate, budgetMs, context) {
|
|
|
896
941
|
});
|
|
897
942
|
outcome = classifyRun(messages, gate, budgetMs, await (await shell.execute(spec)).result(), log);
|
|
898
943
|
} catch (error) {
|
|
899
|
-
log.error(`[quality-gate] gate did not run: ${gate
|
|
944
|
+
log.error(`[quality-gate] gate did not run: ${gateCommandText(gate)} @ ${gate.root} — ${messageOf(error)}`);
|
|
900
945
|
const cause = messageOf(error);
|
|
901
946
|
outcome = isSandboxUnavailable(error) ? {
|
|
902
947
|
kind: "not-run",
|
|
@@ -914,17 +959,17 @@ async function runGate(messages, shell, gate, budgetMs, context) {
|
|
|
914
959
|
* **不可写成 `svc.shell`**:cordis ctx 的 Proxy get 陷阱对未列入 `inject` 的属性
|
|
915
960
|
* 直接抛 `cannot get property "shell" without inject`(见 cordis reflect.ts 的
|
|
916
961
|
* `ReflectService.handler.get`),于是下面的 `=== undefined` 判定永远走不到,
|
|
917
|
-
*
|
|
962
|
+
* 无 shell 时的静默放行分支形同虚设,且每回合结束都冒一个 turn 错误卡。
|
|
918
963
|
* shell 服务由 host 组合(@deepseek-ai/dsh-shell 的 Service 构造即注册),
|
|
919
|
-
* 正常环境下 get
|
|
964
|
+
* 正常环境下 get 命中;异常环境才走静默放行。
|
|
920
965
|
*
|
|
921
966
|
* **也不加 `?? svc.shell` 兜底**:shell 真缺失时右值会重新触发同一个抛错,
|
|
922
|
-
*
|
|
967
|
+
* 兜底反而把静默放行再打断。mock 需实现 get() 才有意义。
|
|
923
968
|
*/
|
|
924
969
|
function getShell(rt) {
|
|
925
970
|
return rt.svc.get("shell");
|
|
926
971
|
}
|
|
927
|
-
/**
|
|
972
|
+
/** 取本会话的解析后沙箱策略。`session` 交官方 `Session` 类本身:
|
|
928
973
|
* 官方 `SandboxPolicyRequest.session` 就是这个类(@deepseek-ai/dsh-sandbox-policy/lib/
|
|
929
974
|
* types/index.d.ts:52),而 `Session` 带私有字段、按名义比 —— 本包那个只服务退役中同步
|
|
930
975
|
* 读面的 SessionRef 视图交不进去,编译器原话:`Type 'SessionRef' is missing the following
|
|
@@ -966,12 +1011,12 @@ function accumulatorOf(rt, sessionId) {
|
|
|
966
1011
|
}
|
|
967
1012
|
/**
|
|
968
1013
|
* 从事件流读本回合**成功**编辑的文件(按官方观察面给的增量窗),记进本会话的账。
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
*
|
|
1014
|
+
* 被 danger-guard 拒绝或执行失败的编辑,其 tool/call 仍在事件流里——照记就会对未真正
|
|
1015
|
+
* 改动的文件(若落在独立项目根)多跑一次门禁。所以 tool/call 建 callId→{name,path},
|
|
1016
|
+
* tool/result 的 isError 标记失败 callId,只记成功的。
|
|
972
1017
|
* turn-stopping 时本回合 call+result 同处一个增量窗,单次扫描即可关联;无 callId 的事件
|
|
973
1018
|
* (异常/旧形状/测试 mock)保守直记,避免漏门禁。
|
|
974
|
-
* fork
|
|
1019
|
+
* fork 前缀:子代理会话 fork 自父会话时,
|
|
975
1020
|
* 事件流含父事件前缀,游标从 0 首读会把父会话的历史编辑一并计入、多跑门禁。
|
|
976
1021
|
* Session.inheritedEventCount(0.1.2-rc.1 起公开只读)给出 fork 切点——首读下限
|
|
977
1022
|
* 取切点而非 0,父前缀天然跳过;缺属性退回 0(旧行为,保守不漏门禁)。
|
|
@@ -979,8 +1024,22 @@ function accumulatorOf(rt, sessionId) {
|
|
|
979
1024
|
async function readEditedFiles(rt, agent, acc) {
|
|
980
1025
|
const { session } = agent;
|
|
981
1026
|
const sessionId = session.id;
|
|
982
|
-
const
|
|
983
|
-
if (
|
|
1027
|
+
const query = rt.svc.get("sessionQuery");
|
|
1028
|
+
if (query === void 0) {
|
|
1029
|
+
if (!rt.queryMissingWarned) {
|
|
1030
|
+
rt.queryMissingWarned = true;
|
|
1031
|
+
rt.log.warn("[quality-gate] sessionQuery service unavailable; edit accounting is off, so no gate will run");
|
|
1032
|
+
}
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const window = await resolveEventWindow(query, sessionId, rt.lastSeqBySession);
|
|
1036
|
+
if (window === void 0) {
|
|
1037
|
+
if (!rt.queryReadWarned) {
|
|
1038
|
+
rt.queryReadWarned = true;
|
|
1039
|
+
rt.log.warn("[quality-gate] session event read returned no window; this turn's gate is skipped (further read failures stay silent)");
|
|
1040
|
+
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
984
1043
|
rt.lastSeqBySession.set(sessionId, window.from + window.events.length);
|
|
985
1044
|
pruneToSessionBudget(rt.lastSeqBySession, sessionId);
|
|
986
1045
|
const edits = collectEdits(window.events);
|
|
@@ -1053,7 +1112,7 @@ function handleGateFailure(scope, target, problem) {
|
|
|
1053
1112
|
}
|
|
1054
1113
|
}
|
|
1055
1114
|
/** 卸载收口的读法:两次判读之间隔着 `await runGate(...)`,那期间 disposer 的 abort() 是真会
|
|
1056
|
-
*
|
|
1115
|
+
* 发生的(卸载收口由测试钉住);直接读 `gateAbort.signal.aborted` 会被控制流分析钉在前一次
|
|
1057
1116
|
* 判读的 false 上(属性窄不跨 await 复位),后一处守卫就被判成恒假。经函数取值 ⇒ 每读都按
|
|
1058
1117
|
* 声明的 boolean 算,守卫才是必要的。 */
|
|
1059
1118
|
function gateAborted(rt) {
|
|
@@ -1096,14 +1155,13 @@ async function processGate(state, left) {
|
|
|
1096
1155
|
if (!handleGateFailure(scope, gate, outcome)) await processGate(state, left.slice(1));
|
|
1097
1156
|
}
|
|
1098
1157
|
/**
|
|
1099
|
-
*
|
|
1158
|
+
* 一趟门禁批次:按总预算封顶(perGate 保底 10s 会超支)——跟踪已花时间,每门
|
|
1100
1159
|
* min(perGate, remaining)。尾段不足最小可用预算(真实 tsc/pnpm 冷启动也要数秒,几百 ms
|
|
1101
|
-
* 必超时 → 假"超时失败"
|
|
1102
|
-
* 通过(审计 MEDIUM 项)。
|
|
1160
|
+
* 必超时 → 假"超时失败"注入)时**注入未执行说明**——只 console.warn 等于静默通过。
|
|
1103
1161
|
*
|
|
1104
|
-
*
|
|
1162
|
+
* 防御收口:①整段顶层 try/catch——批次自身故障只记一条 error,绝不产生
|
|
1105
1163
|
* unhandledRejection;②批次 promise 由调用方登记进在飞账本,disposer abort 后 await 收敛
|
|
1106
|
-
* (
|
|
1164
|
+
* (dispose 必须终止并等待在飞工作)。
|
|
1107
1165
|
*/
|
|
1108
1166
|
async function runGateBatch(input) {
|
|
1109
1167
|
const { scope } = input;
|
|
@@ -1221,6 +1279,8 @@ function apply(ctx, config) {
|
|
|
1221
1279
|
gateAbort: new AbortController(),
|
|
1222
1280
|
inFlightGates: /* @__PURE__ */ new Set(),
|
|
1223
1281
|
shellWarned: false,
|
|
1282
|
+
queryMissingWarned: false,
|
|
1283
|
+
queryReadWarned: false,
|
|
1224
1284
|
disposed: false
|
|
1225
1285
|
};
|
|
1226
1286
|
svc.on("agent/turn-stopping", async (payload) => {
|
|
@@ -1245,4 +1305,4 @@ var host_default = {
|
|
|
1245
1305
|
apply
|
|
1246
1306
|
};
|
|
1247
1307
|
//#endregion
|
|
1248
|
-
export { Config, apply, host_default as default };
|
|
1308
|
+
export { Config, apply, host_default as default, shellArg };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jayyuen66/dsh-quality-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "dsh 插件:回合收口质量门禁(在 agent/turn-stopping 按项目形态跑 pnpm check / cargo check / ruff check / tsc,未过则注入修复指令开新轮)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "JayYuen",
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
"icon.svg",
|
|
13
13
|
"host.js",
|
|
14
14
|
"client.js",
|
|
15
|
-
"cordis.patch.yml"
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"README.md",
|
|
17
|
+
"README.zh-CN.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"THIRD_PARTY_NOTICES.md"
|
|
16
20
|
],
|
|
17
21
|
"type": "module",
|
|
18
22
|
"main": "host.js",
|
|
@@ -27,48 +31,51 @@
|
|
|
27
31
|
"registry": "https://registry.npmjs.org"
|
|
28
32
|
},
|
|
29
33
|
"dependencies": {
|
|
30
|
-
"@
|
|
31
|
-
"
|
|
32
|
-
"@jayyuen66/dsh-plugin-shared": "^0.1.0"
|
|
34
|
+
"@jayyuen66/dsh-plugin-shared": "^0.1.6",
|
|
35
|
+
"typescript": "7.0.2"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
|
-
"@deepseek-ai/cordis": "4.0.
|
|
36
|
-
"@deepseek-ai/dsh-agent": "0.2.
|
|
37
|
-
"@deepseek-ai/dsh-api-remotes": "0.2.
|
|
38
|
-
"@deepseek-ai/dsh-client-locale": "0.2.
|
|
39
|
-
"@deepseek-ai/dsh-client-store": "0.2.
|
|
40
|
-
"@deepseek-ai/dsh-client-ui-layout": "0.2.
|
|
41
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.
|
|
42
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.2.
|
|
43
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.
|
|
44
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-
|
|
47
|
-
"@deepseek-ai/dsh-
|
|
48
|
-
"@deepseek-ai/dsh-sandbox
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-session
|
|
52
|
-
"@deepseek-ai/dsh-session-
|
|
53
|
-
"@deepseek-ai/dsh-session-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@
|
|
38
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
39
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.2",
|
|
40
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.1-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-package-manifest": "0.2.1-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-scope": "0.2.1-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-session-query": "0.2.1-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-session-title": "0.2.1-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-settings": "0.2.1-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-shell": "0.2.1-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-subprocess": "0.2.1-rc.2",
|
|
61
|
+
"@deepseek-ai/schemastery": "3.18.5",
|
|
62
|
+
"@types/node": "26.6.5",
|
|
58
63
|
"@types/react": "19.3.0",
|
|
59
|
-
"@vitest/coverage-v8": "5.0.
|
|
64
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
60
65
|
"eslint-plugin-sonarjs": "4.2.2",
|
|
61
|
-
"oxfmt": "0.
|
|
62
|
-
"oxlint": "1.
|
|
63
|
-
"oxlint-tsgolint": "7.0.
|
|
66
|
+
"oxfmt": "0.73.0",
|
|
67
|
+
"oxlint": "1.88.0",
|
|
68
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
64
69
|
"react": "^19.3.0",
|
|
65
|
-
"rolldown": "1.2.
|
|
70
|
+
"rolldown": "1.2.14",
|
|
66
71
|
"typescript": "7.0.2",
|
|
67
|
-
"vite": "8.3.
|
|
68
|
-
"vitest": "5.0.
|
|
72
|
+
"vite": "8.3.4",
|
|
73
|
+
"vitest": "5.0.5"
|
|
69
74
|
},
|
|
70
75
|
"peerDependencies": {
|
|
71
|
-
"@deepseek-ai/dsh": "^0.2.
|
|
76
|
+
"@deepseek-ai/dsh": "^0.2.1-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
|
|
78
|
+
"@deepseek-ai/schemastery": "3.18.5"
|
|
72
79
|
},
|
|
73
80
|
"peerDependenciesMeta": {
|
|
74
81
|
"@deepseek-ai/dsh": {
|
|
@@ -77,7 +84,8 @@
|
|
|
77
84
|
},
|
|
78
85
|
"icon": "icon.svg",
|
|
79
86
|
"engines": {
|
|
80
|
-
"dsh": "
|
|
87
|
+
"dsh": "^0.2.1-rc.2",
|
|
88
|
+
"node": ">=22.12.0"
|
|
81
89
|
},
|
|
82
90
|
"dsh": {
|
|
83
91
|
"manifestVersion": 1,
|
|
@@ -93,7 +101,8 @@
|
|
|
93
101
|
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
|
|
94
102
|
"lint": "oxlint .",
|
|
95
103
|
"test": "vitest run --coverage",
|
|
96
|
-
"check": "
|
|
104
|
+
"check:pack": "node pack-check.mjs",
|
|
105
|
+
"check": "npm run --loglevel=warn typecheck && npm run --loglevel=warn lint && npm run --loglevel=warn build:client && npm run --loglevel=warn build && npm run --loglevel=warn test && npm run --loglevel=warn check:pack && npm run --loglevel=warn fmt:check",
|
|
97
106
|
"build:client": "node build-client.mjs",
|
|
98
107
|
"build": "node build-host.mjs",
|
|
99
108
|
"fmt:check": "oxfmt --check ."
|