@jayyuen66/dsh-quality-gate 0.1.2 → 0.1.4

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 CHANGED
@@ -3,6 +3,97 @@
3
3
  格式遵循 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),版本号遵循
4
4
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html)。最新版本在最上。
5
5
 
6
+ ## 0.1.4
7
+
8
+ 这一版收窄回合门禁的作用域:子包自己的 workspace 门禁不再被外层伞工作区顶掉,改单仓的回合不再跑整棵树。
9
+
10
+ ### Fixed
11
+
12
+ - **单仓安装态下两份 zod 的 `ZodType` 不兼容**:宿主投影包自带 zod 4.4.3,本包 catalog 是
13
+ 4.6.7;`editLedgerProjection.stateSchema` 经宿主声明的类型做一次窄桥接,运行期仍交给宿主
14
+ `safeParse`,不改依赖版本。
15
+ - **子包自己的 workspace 门禁被外层伞工作区顶掉。** 编辑一个自带 `pnpm-workspace.yaml` 且声明了 `check` 的子包时,根探测会继续上溯到外层伞工作区,把「改一个仓」放大成「整棵树跑一遍 `pnpm check`」:回合收口的预算被单条命令吃光,改本仓的回合反而反复超时。现在子包自己产得出 `pnpm check` 时就地执行、不再上溯;外层升级只在子包自己产不出门禁时发生。`test/host-integrity.test.ts` 新增一条伞工作区用例钉住这一面。
16
+ - **脚本内部的 exit 126/127 被误报成「门禁工具缺失」。** 126/127 是整条命令的退出码,不是「工具没装」的充分证据:`pnpm check` 的 check 脚本里缺命令时 pnpm 同样以 127 退出(实测 `pnpm run check` 对不存在的脚本命令回 `[ELIFECYCLE] Command failed with exit code 127`),tsc 的脚本也可能因缺 devDependency 回 127。原先这两档一律按 not-run 注入「command not found (tool not installed) — the gate did not run」,项目自己的坏脚本被当成环境问题放掉。现在只有 stderr **点名门禁命令自己的首 token**(`sh: pnpm: command not found`、`command not found: tsc`、Windows 的 `'pnpm' is not recognized`)才判工具不可用,其余 126/127 按代码失败上报并附证据;126 同理只在点名「不可执行」时判 not-run。`test/host.test.ts` 与 `test/host-integrity.test.ts` 的 126/127 用例补上点名证据,并新增一条「脚本内 127 仍报代码失败」的用例。
17
+
18
+ ## 0.1.3
19
+
20
+ 这一版把判定面与构建/CI 面一起收口:ruff 不再对只写了 `pyproject.toml` 的工程发门禁,卡片保存期间的编辑不再被静默丢弃,记忆反馈的文案与来源一致,Windows 与发布链的静默降级补上判据。宿主线抬到 `0.2.1-rc.3`。
21
+
22
+ ### Fixed
23
+
24
+ - **只写了 `pyproject.toml` 的 Python 工程会被发 `ruff check .`。** `pyproject.toml` 是 Python 的元数据文件,不是「本项目用 ruff」的声明;原先只要它在场就产门禁,于是把 lint 交给别的工具的工程会被宿主 PATH 上的 ruff 按默认规则判一遍,再经同一个注入通道当成真实缺陷。现在 ruff 门禁要求根上有 `ruff.toml`,或 `pyproject.toml` 里真有 `[tool.ruff]` 一族段(段头按行首字面匹配,带引号的键与整表数组一并认);两者都没有就不发。拿不到 `pyproject.toml` 内容时按「没有信息」处理,与读不到同一方向。
25
+ - **卡片保存期间的编辑会被静默丢弃。** 保存请求在途时输入框只按 `writable` 禁用,用户仍能继续改;而写入成功后无条件清空待保存集合,把点保存之后产生的新改动一起抹掉。现在只剔除本次真正写下的键,在途期间的新改动留在待保存面里。
26
+ - **记忆反馈的文案承诺了实现没做的事。** 文案说清空 key 就不启用记忆反馈,实际还有环境变量里的网关地址兜底、引用名也会回落到默认值;照着改,代码型门禁失败的证据仍会发出去。文案改成如实说明两条来源。
27
+ - **`test/` 下三处机器侧门禁的静默放行**:动态 `import()` 吃裸文件路径(Windows 上抛 `ERR_UNSUPPORTED_ESM_URL_SCHEME`)改走 `pathToFileURL().href`;产物清单为空时两侧都折成同一个空串、被判「新鲜」,改为当场抛错;显式设了 `DSH_PROFILE_PACKAGE_JSON` 却指向不存在的文件时静默跳过,改为判红。
28
+ - **四个工作流里的判断面缺陷**:`ci.yml` 的 `find … -print -quit` 在零匹配时得到空串、多匹配时静默取任意一个,改为「恰好一个 tarball」并点名实际个数;`publish.yml` 的重复发布检查把包名与依赖名写死在 URL 里(改名后会查到 404 而当成首发放行),改为从 `package.json` 读,并在版本低于 registry `latest` 时告警(只告警:从旧分支发版会把 `latest` 回退,但半吊子的预发布序比较不该一票否决发布);静态 token 的形态校验把 `exit 1` 降级为告警,OIDC 主线不该被兜底凭据的形态卡死;`scorecard.yml` 的 job 级判据用 `github.ref_name` 比默认分支名,同名 tag 上会误判,改为比完整 ref。
29
+ - **`ci.yml` 的 `-ignore SC2016` 是全局豁免**:一行调用同时校验四份工作流,`codeql.yml` 与 `scorecard.yml` 没有 run 脚本体,却也继承了这条豁免——它们将来真写出的同类缺陷会被一起吞掉。改成按文件分两次调用。
30
+ - **`codeql.yml` 与 `scorecard.yml` 缺并发控制与权限声明**:前者补 `actions: read`(本仓是 public,官方只在私有仓要求它;显式声明是为了可见性变化时不静默缺权限),后者补按事件名分组的 `concurrency`,让四个入口不再互相覆盖同一份 analysis 结果。
31
+
32
+ - **`test/schema-coverage.ts` 的 host schema 解析器**:括号匹配不跳过字符串、模板与注释,`Schema.object(` 只取首个块,扩展名只收 `.ts`,包根靠 `testDir/..` 硬推且读盘无兜底。这些偏差都会让「宿主字段全集」偏少而门禁假绿。解析器改成字符串/注释感知、遍历全部 `Schema.object`、按前缀识别扩展名、向上找 `package.json`、读不到时判红;新增 `test/schema-coverage.test.ts` 钉住这一面。
33
+ - **记忆反馈的目标面与正文收口**:网关地址必须是合法的 `http(s)` URL(半截串、非 http(s) scheme 均按「未配置」处理),凭据只发 https 或回环地址;`x-tdai-service-id` 形状不合规时回落默认服务名;命令、根与错误文本在写入记忆前折成单行,避免一条反馈被拆成多段。
34
+ - **编辑累加器的容量契约对任意入参成立**:`maxFiles` 为 NaN、0、负数或小数时回落到默认值;同一文件被反复编辑会刷新到最近位置,避免最该检查的文件第一个被淘汰;host 与累加器共用同一份编辑工具名单。
35
+ - **消息模板插值只认自有键**:`{constructor}` / `{toString}` 这类原型链名字不再渲染成函数源码,未提供的名字留空串。
36
+
37
+ ### Changed
38
+
39
+ - **`ruff` 门禁的可发条件**(见 Fixed 第一条)是**判定规则**的变化:只有 `pyproject.toml` 的 Python 工程从此不再自动获得 ruff 门禁。两份 README 的命令表同步。
40
+ - **宿主线抬到 `0.2.1-rc.3`**:`engines.dsh`、peer/dev 依赖、overrides 与 README 徽标/正文同值。
41
+ - **构建编排统一进 shared 的 `lib/build-*`**:三个构建脚本只剩薄配置层,产物仍落 `dist/`;CI/CodeQL/Scorecard/Dependabot 与 Windows 矩阵补齐。
42
+
43
+ ### Build and CI fixes
44
+
45
+ 这一版同时清掉构建侧与 CI 侧的缺陷:一批只在 Windows 上才会红的判据、两个构建脚本里没有任何门看得见的静默降级,以及发布链路缺的一道检查。
46
+
47
+ #### Fixed
48
+
49
+ - **`host.ts` 里三处用 `/` 拼子项的地方改成 `path.join`。** `findManifestRoot` 探测 `MANIFESTS`、`workspaceRootUpgrade` 探测 `pnpm-workspace.yaml`、`probeProjectRoot` 过滤 manifest 与判断 `tsconfig.json`,原先都用模板串拼出 `dir/manifest`。`dir` 是宿主给的真实路径,在 Windows 上这种混形让 `existsSync` 恒假——于是所有 manifest 都探测不到、项目根一路回落到父目录,`gate-detect` 的判据整片失效而命令照常退出 0。同一处 `declaredScriptsOf` 读 `package.json` 也改成 `path.join`。新增 `TSCONFIG_MANIFEST` 常量,避免同一个文件名在文件里出现两次。
50
+ - **`collapseStorePaths` 的折叠原先完全静默。** 正则只认 `/` 分隔的「内容寻址 store 段」(`node_modules/` 下那层带「名@版_哈希」的目录),既没有归一反斜杠,也没有折掉前缀里的 `../`。没命中时它原样放行,于是 `client.js` 会带着「版本 + 本机 store 哈希」进公共产物,而两道新鲜度门禁比的都是同一台机器上的内存构建与磁盘文件——没有任何门能看见。现在先归一分隔符,再把可选的前缀与 store 段整段折成一层,折完仍检出 store 段就抛错停止构建。
51
+ - **`writeArtifact` 的临时文件名只用 pid。** 同一进程里并发写两个产物会撞名、各自截断对方。临时名加上 `randomUUID()`;`catch` 里清理临时文件的 `rm` 包进内层 try/catch,清理失败不再取代并吞掉真正的写失败(Windows 上 `EBUSY` / `EPERM` 会让清理自己抛)。
52
+ - **客户端产物的 `external` 由 `id.startsWith("react/")` 改成精确集合 `REACT_RUNTIME_MODULES`。** 外壳的模块表按精确串匹配,前缀放行会给产物留下模块表里没有的键——加载期报 `not in the module table`,整张卡片静默不渲染。集合与宿主客户端侧的 `PLATFORM_MODULES` 同源,只收宿主真给得出的四个说明符。
53
+ - **两处注释与实现不符。** 构建脚本的头注释说「react 及其子路径都要外部化」并给了「内联子路径会出现第二份 React」的理由——那个理由不成立,`react/jsx-runtime` 内联后自身仍 `require("react")`;`ci.yml` 里「门禁没跑过就别留下产物。」那行原先挂在上传覆盖率报告上方,与它描述的步骤错位。
54
+ - **`test/host-integrity.test.ts`、`test/host.test.ts`、`test/gateway-feedback.test.ts` 的 POSIX 假设。** 新增的 Windows 矩阵会让这些判据整片红:临时目录原先写成字面量 `/tmp/qg-shallow-…`,改成 `path.join(tmpdir(), …)`;匹配 tsc 命令的正则 `/bin\/tsc --noEmit$/u` 在 Windows 上拿到的渲染是 `'C:\…\bin\tsc' --noEmit`(带引号、反斜杠),改成 `/bin[\\/]tsc'? --noEmit$/u`;`deriveProjectKey` 的注入补上 `sep: "/"`,让它与字形的来源同源——路径字形与 `resolve` 不来自同一次调用时,`path.win32` 会把 `/tmp/x/proj` 解析成带盘符的串,字面量替换一次都不命中。
55
+
56
+ #### Added
57
+
58
+ - **`tsconfig.build-scripts.json` 与 `typecheck` 里的一次额外 `tsc`。** 两个构建脚本头部的 `// @ts-check` 只在编辑器里生效:它们不在任何 `tsc` program 的 `include` 里,CI 因此对这一面没有任何门。新 tsconfig 继承共享基线、打开 `allowJs` / `checkJs`、`noEmit`,只收两个构建脚本;`typecheck` 追加 `&& tsc --noEmit -p tsconfig.build-scripts.json`。变异实测:往 `build-client.mjs` 塞一行 `probe.toFixed("nope")` 会红,删掉即绿。
59
+ - **`tsconfig.build-scripts.json` 补齐之后暴露并修掉的类型缺陷**:`isRecord` 一律改成类型谓词(`@returns {raw is Record<string, unknown>}`),`asRecord` 改成守卫分支而不是类型断言(断言会同时撞 `typescript(no-unsafe-type-assertion)` 与 `eslint(no-inline-comments)`);`invokedDirectly()` 先取 `process.argv.at(1)` 再判 `undefined`,不再把可能不存在的值直接交给 `realpathSync`;读 `package.json` 的 `name` 用 `const nameKey = "name"` 索引——点号写法被 `noPropertyAccessFromIndexSignature` 拒绝,索引写法又被 `dot-notation` 要求改成点号,两侧只能靠一次具名取值同时满足。
60
+ - **`.gitattributes` 钉 `* text=auto eol=lf`。** Windows runner 上 Git for Windows 的机器级 `core.autocrlf=true` 会把工作树换成 CRLF,而 `oxfmt` 的 `endOfLine` 默认是 `lf`——`fmt:check` 只在 windows 那一格把每个文件都判成未格式化,内容一字不差。
61
+ - **`test/dist-size.test.ts` 的预算改成按实测锚定**:`host.js` 70 500 B(实测 61 110 B)、`client.js` 23 500 B(实测 20 222 B),取实测上浮约 15% 再向上取整到 500 B。注释写明本仓没有 `build-types.mjs`,所以只量两项。
62
+ - **`package.json` 的 `keywords`**,以及两份 README 各新增一节「shared 的版本下限 / Shared version floor」。
63
+
64
+ #### Changed
65
+
66
+ - **`publish.yml` 补回 `pnpm run check`。** 发布由 `workflow_dispatch` 手动触发,可以从任意分支派发;CI 在 `main` 上的绿不代表被派发的那个 ref 是绿的,而发布前少这一道检查等于把「发布物能装」与「发布物正确」当成一件事。现在它在 `pnpm install --frozen-lockfile` 之后、`发布` 之前跑。
67
+ - **`ci.yml` 的覆盖率上传取 `if-no-files-found: warn` 而不是 `error`。** 这一步在 `if: always()` 下跑,而 `coverage/` 由 vitest 产出——typecheck / lint / build 任一先红时目录根本不存在,此时判红会盖住真正的失败原因,还把后面的步骤一起中断。
68
+ - **`ci.yml` 里新增的 Windows 矩阵与多平台并发组的形状**:组键带 `event_name`,取消只给 `push` 与 `pull_request`;gate 的检查名带 `matrix.os`;attest 拆成独立 job 并补齐 `artifact-metadata: write`;install-probe 收窄到 `contents: read`;所有 `actions/checkout` 带 `persist-credentials: false`;第三方 action 与 `actionlint` 全部钉到 commit SHA / 校验和。
69
+
70
+ #### Not changed, deliberately
71
+
72
+ - **除 ruff 的可发条件外,四条命令的白名单与优先级、`not-run` 与 `code-failure` 的分界、注入配额、预算分摊、默认设置值与双语文案内容未变。**
73
+ - **覆盖率阈值仍由 `@jayyuen66/dsh-plugin-shared/config/vitest.base` 单源给出(100%)**,`coverageInclude` 仍只覆盖 host 半。
74
+
75
+ #### Tests
76
+
77
+ - `pnpm run check` 通过:typecheck 三个 tsconfig、`oxlint` 0 warning / 0 error、两个 rolldown 构建、`vitest run --coverage` 四项 100%、`oxfmt --check`、`check:pack` 的 tarball 体检。
78
+ - 新增的门禁都做过变异实测:`tsconfig.build-scripts.json` 那条塞错误即红、删掉即绿;`collapseStorePaths` 的残留检查拿含 `.pnpm` 的行喂进去会抛,四种真实形态(无前缀 / `../` 前缀 / 反斜杠 / 普通相对段)折叠结果逐个断言。
79
+ - Windows 兼容那三条用渲染出的两种字形直接验证判据:POSIX 形态的 `<root>/node_modules/.bin/tsc --noEmit` 与 win32 形态的带盘符、带反斜杠、带引号那一种,旧正则只命中前者,新正则两个都命中。
80
+
81
+ ## 0.1.2
82
+
83
+ `tsc` 改走本包依赖、凭据只发 https,并修掉三处「命令照绿而门禁没在工作」的静默失效。
84
+
85
+ ### Fixed
86
+
87
+ - **事件读面缺失或读失败时整条门禁静默失活、零日志。** 改为按成因 warn-once,并补「同一条只说一次」的断言。
88
+ - **每回合让宿主按默认 `projectionMode="all"` 白算整份 session 投影**:显式传 `"none"`。
89
+ - **`workspace` 上溯会把子包自己的 `tsc` / `ruff` 门禁吃掉**:只在外层真能产出门禁时才升级,两份 README 同步。
90
+ - **`npx tsc` 在项目未装 `typescript` 时会下载并执行已弃用的 stub 包**:`typescript` 进 `dependencies`,门禁改为解析真实入口(工程本地优先、其次本包依赖),`npx` 与 stub 的识别文案删除;命令拼接补引号处理。
91
+ - **记忆网关允许明文 `http` 并照发 `Bearer`**:只允许 `https` 或回环地址,其它地址在解析凭据之前按未配置拒绝并说明理由。
92
+
93
+ ### Changed
94
+
95
+ - `vitest.config.ts` 写明客户端半边不计覆盖率的理由(共享基线要求的例外说明)。
96
+
6
97
  ## 0.1.1
7
98
 
8
99
  宿主生态与工具链跟到发布线,发布面的判据补严。**门禁判定规则、四条命令白名单、注入文案与
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 JayYuen
3
+ Copyright (c) 2026 JayYuen666
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,104 +1,190 @@
1
- # @jayyuen66/dsh-quality-gate
1
+ <p align="center">
2
+ <img src="https://cdn.jsdelivr.net/gh/JayYuen666/dsh-quality-gate@main/icon.svg" width="96" height="96" alt="dsh-quality-gate logo" />
3
+ </p>
4
+
5
+ <h1 align="center">dsh-quality-gate</h1>
6
+
7
+ <p align="center"><em>At turn end it runs the checks the project itself declares; a check that never ran is never recorded as passed.</em></p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@jayyuen66/dsh-quality-gate"><img src="https://img.shields.io/npm/v/@jayyuen66/dsh-quality-gate?style=flat-square&logo=npm&logoColor=white" alt="npm version" /></a>
11
+ <a href="https://www.npmjs.com/package/@jayyuen66/dsh-quality-gate"><img src="https://img.shields.io/npm/d18m/@jayyuen66/dsh-quality-gate?style=flat-square" alt="npm downloads (total)" /></a>
12
+ <a href="https://github.com/JayYuen666/dsh-quality-gate/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/JayYuen666/dsh-quality-gate/ci.yml?branch=main&style=flat-square&label=ci" alt="ci" /></a>
13
+ <img src="https://img.shields.io/badge/license-MIT-4c1?style=flat-square" alt="MIT" />
14
+ <img src="https://img.shields.io/badge/node-%3E%3D22.12-4c1?style=flat-square&logo=nodedotjs&logoColor=white" alt="node" />
15
+ <img src="https://img.shields.io/badge/dsh-%5E0.2.1--rc.3-658EFF?style=flat-square" alt="dsh" />
16
+ <a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/powered_by_dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white" alt="powered by dsh" /></a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <img src="https://skillicons.dev/icons?i=typescript,nodejs,react,vitest,githubactions" alt="tech stack" />
21
+ </p>
22
+
23
+ <p align="center">
24
+ <a href="https://github.com/JayYuen666"><img src="https://img.shields.io/badge/GitHub-JayYuen666-181717?style=flat-square&logo=github&logoColor=white" alt="GitHub" /></a>
25
+ <a href="mailto:cqwkkj@gmail.com"><img src="https://img.shields.io/badge/Email-cqwkkj@gmail.com-EA4335?style=flat-square&logo=gmail&logoColor=white" alt="Email" /></a>
26
+ <a href="https://twitter.com/JayYuen666"><img src="https://img.shields.io/badge/Twitter-X%2FJayYuen666-000000?style=flat-square&logo=x&logoColor=white" alt="Twitter/X" /></a>
27
+ <a href="./README.zh-CN.md"><img src="https://img.shields.io/badge/lang-简体中文-658EFF?style=flat-square" alt="简体中文" /></a>
28
+ </p>
29
+
30
+ **A [dsh](https://github.com/deepseek-ai/deepseek-harness) plugin: a turn-end quality gate — when the turn edited code or config files, the checks the project itself declares run before the turn closes; a failure goes back as a repair instruction that opens a new turn, under a per-session quota and a total budget.**
31
+
32
+ ## <img src="https://api.iconify.design/lucide/file-text.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Contents
33
+
34
+ - [What it does](#what-it-does)
35
+ - [Install](#install)
36
+ - [Verify it is live](#verify-it-is-live)
37
+ - [What it does not provide](#what-it-does-not-provide)
38
+ - [Enabling it in dsh](#enabling-it-in-dsh)
39
+ - [The four gate commands](#the-four-gate-commands)
40
+ - [Settings](#settings)
41
+ - [Public interface](#public-interface)
42
+ - [Data and privacy](#data-and-privacy)
43
+ - [Known boundaries](#known-boundaries)
44
+ - [FAQ](#faq)
45
+ - [Development](#development)
46
+ - [License](#license)
47
+
48
+ ## <img src="https://api.iconify.design/lucide/shield.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> What it does
49
+
50
+ - **Turn-end, not in-flight.** The hook is the host event `agent/turn-stopping`: read this session's **successful** edits → walk each file up to the nearest manifest root → detect the gate → run it. A failure is injected back via `agent.followup`; the host still checks the inbox after turn-stopping, so a repair instruction opens a new turn.
51
+ - **One run has exactly three outcomes**: `pass` (it really finished with exit 0), `code-failure` (it ran and reported code errors), `not-run` (it never ran). A check that did not truly complete is never recorded as passed; and "could not run" is never conflated with "code error" — the former injects "please confirm manually", writes no memory and never reaches the lesson bus as `gate-failure`.
52
+ - **Only commands the project itself declares.** One of four whitelist commands per root; without a declaration the gate is simply not issued — better no gate than an invented command that fails deterministically (see "The four gate commands").
53
+ - **Runaway protection.** Repair injections are quota'd per session across turns; once spent, one user-visible release note is injected and auto-repair stops. The total gate time is budget-capped; a timeout is injected as "could not run" instead of passing silently.
54
+ - **It does not edit code, does not block dangerous commands (that is danger-guard's job) and does not write any file.**
55
+
56
+ ## <img src="https://api.iconify.design/lucide/package.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Install
2
57
 
3
- [中文](./README.zh-CN.md)
58
+ ```sh
59
+ dsh plugin --profile web add @jayyuen66/dsh-quality-gate
60
+ ```
61
+
62
+ Remove with `dsh plugin --profile web remove @jayyuen66/dsh-quality-gate`. The package is on the public npm registry, so installation needs no credentials.
63
+
64
+ - **Requires Node.js `>= 22.12`.** The artifacts use newer runtime faces such as `AbortSignal.any`.
65
+ - **Requires a host `dsh` on the `0.2.1-rc.3` line.** The requirement the host reads is `engines.dsh` (`^0.2.1-rc.3`), and it **decides alone**; pre-release versions participate in range comparison, and `0.3.0` and later need the explicit, risk-accepting grant `dsh plugin --profile <p> allow-version <pkg>@<ver> --dsh-version <exact> --accept-risk`.The support window rolls: this package is verified against the three most recently released host versions at publish time and re-verified once the window moves, so the version number in this bullet is not a standing promise.
66
+ - **The runtime dependency `@jayyuen66/dsh-plugin-shared` must already be on the same registry**, otherwise the whole set 404s.
67
+ - **`typescript` is a real `dependencies` entry**: the fallback entry for the `tsc` gate — the project's own install wins, then the copy shipped with this package; no network action at runtime, never an `npx` auto-download.
68
+ - **`zod` is also a real `dependencies` entry**: the edit-ledger projection state and the settings card's value/draft faces use it for shape validation, versioned through the umbrella catalog. The client artifact keeps `require("zod")` externalized via the host's `PLATFORM_MODULES`.
69
+ - **`@deepseek-ai/schemastery` is resolved from the host, not installed by this package**: only the host's fork resolves `.volatile()` fields into live references; a public schemastery turns "edit a setting" into "restart to apply". Two validator functions of `@deepseek-ai/dsh-credentials` are value imports, so that package sits in `peerDependencies` — no `@deepseek-ai/*` under `dependencies` is a publishing contract (`test/publish-manifest.test.ts` enforces it: a host package there materializes a second copy in the consumer's profile, next to the running host).
70
+ - Installing a local directory needs pnpm ≥ 12.3.0 (12.0.0 has a known bug in that path).
71
+
72
+ ### <img src="https://api.iconify.design/lucide/arrow-down-to-line.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Shared version floor
73
+
74
+ If your profile pins `@jayyuen66/dsh-plugin-shared` with a pnpm `overrides` entry, that pin must be at least the version this package declares. A lower pin installs a copy without the names this package imports at runtime, and the host then reports `N entries did not activate` with `does not provide an export named ...` at boot — the package installs fine and never loads, and no per-repository CI run can see it.
75
+
76
+ ## <img src="https://api.iconify.design/lucide/circle-check.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Verify it is live
77
+
78
+ 1. **The layer is mounted.** `dsh --profile web --dump-config` ends with `# == @jayyuen66/dsh-quality-gate` plus `- id: quality-gate`.
79
+ - That command is the only accepted "in the layer stack" signal, and it **is not read-only** — it rewrites the profile's root assembly file. Read the layer lines from stdout **and** check stderr separately: unmatched patch targets only appear on stderr, so the exit code alone proves nothing. Accept both shapes: `# == <layer>` and `# == <layer>, patched by <path>`.
80
+ 2. **The card renders.** The plugin list on the settings page shows "quality-gate turn-end quality gate"; change a value, Save goes from disabled to enabled, and the write lands.
81
+ 3. **The gate really runs.** In a project it recognizes (say a TS project with `package.json` + `tsconfig.json` at the root), have the agent edit a code file and end the turn: the gate command runs in that root, and on failure the model receives a repair instruction prefixed `[quality-gate]`, so the turn continues instead of closing. Host log lines with the same prefix carry the command and root. If nothing changes at all, the reading is **the layer is not mounted**, not "the rules are loose".
82
+ 4. **Reading "could not run".** Environment-class failures (sandbox denial, missing tool, exhausted budget) inject a "please confirm manually" notice, not a repair instruction — that is not evidence of a code error; do not change code because of it.
83
+
84
+ ## <img src="https://api.iconify.design/lucide/circle-slash.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> What it does not provide
85
+
86
+ - **No model tools.** The whole surface is the `agent/turn-stopping` listener; there is no `gate_status` tool to call.
87
+ - **No HTTP routes and no Remote namespace.** The settings card consumes the official `configForms` form and writes through the host's channel — unlike siblings with their own data plane, this package's only data plane is settings.
88
+ - **No CLI subcommand.**
89
+ - **No file writes, no telemetry**; the only outbound request is the memory gateway you explicitly enable (see "Data and privacy").
90
+ - Optional consumption: the `lessonLoop` bus (lesson reports), `credentials` (key resolution), `sandboxPolicy` (per-session policy), `sessionQuery` (event reads), `shell` (execution). All are optional `ctx.get` reads — a missing one only removes that capability; the plugin still loads.
91
+
92
+ ## <img src="https://api.iconify.design/lucide/plug.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Enabling it in dsh
93
+
94
+ - Bundle form: `dsh.bundle.patch` in `package.json` points at `cordis.patch.yml`, whose single registration line is `- id: quality-gate` with `name` set to the bare package name (the host scans `dsh.client` only on bare-name entries); `dsh plugin --profile web add/remove` maintains that line.
95
+ - The host half hard-depends only on `settings` (`default.inject = ["settings"]`); precedence is card runtime value > the line's `config:` > schema built-in defaults.
96
+ - The client half (`dsh.client.platform = web`, `immediately: true`) injects a card into the `plugins.bundle.config` slot of the plugin page. Two identifiers, deliberately different sources: the slot key is the **bundle package name** `@jayyuen66/dsh-quality-gate`, while the settings namespace and `configForms.get()` use the **bare entry id** `quality-gate` — mix them up and the card never renders.
97
+ - Saving on the card is explicitly staged: edits stay in local pending state until Save writes them **serially**, key by key, consuming the `Promise<boolean>` acceptance bit — the first `false` stops the loop, shows `saveRejected` and keeps the unwritten changes; a rejecting write shows `saveFailed`. Inputs stay editable during a save, and edits made in flight stay in the pending set instead of being wiped by it; Revert discards the pending edits. Clearing an input means unset, restoring that field's schema default.
98
+ - Two deployment values have no row on the card and go on the registration line's `config:` only, applying on restart: `maxRootDepth` (root search depth) and `gateStdoutMaxBytes` (evidence capture cap); defaults in the settings table.
99
+
100
+ ## <img src="https://api.iconify.design/lucide/terminal.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> The four gate commands
101
+
102
+ The whitelist is locked at type level (the `GateCommand` union in `lib/gate-detect.ts`): only these four can reach the execution surface; at most one per root, priority workspace > Rust > Python > bare TS.
4
103
 
5
- ### What it does
104
+ | Command | When it is issued |
105
+ | --- | --- |
106
+ | `pnpm check` | The root has both `pnpm-workspace.yaml` and `package.json`, and the root scripts declare a non-empty `check`. Otherwise it falls back to the next candidate |
107
+ | `cargo check` | The root has a `Cargo.toml` |
108
+ | `ruff check .` | The root has a `ruff.toml`, or a `pyproject.toml` with a `[tool.ruff]` section. A bare `pyproject.toml` does not count — it is Python metadata, not a ruff opt-in |
109
+ | `tsc --noEmit` | The root has both `package.json` and `tsconfig.json`; without `tsconfig.json` it is not a TS project |
6
110
 
7
- - A turn-end quality gate: when the current turn edited code or config files, the checks that the project itself declares run before the turn closes; a failure opens a new turn via `agent.followup` and injects repair instructions into the model.
8
- - One run has exactly three outcomes: `pass` (it really finished with exit 0), `code-failure` (it ran and reported code errors), `not-run` (it never ran). A check that did not truly complete is never recorded as passed.
9
- - It does not edit code, does not block dangerous commands (that is danger-guard's job) and does not write any file.
111
+ - `tsc` is only a logical name in the command: the entry is resolved at runtime — the project's own `typescript` first, then the copy shipped with this package, finally `tsc` on PATH (reported as not-run by the "tool unavailable" exit); never an `npx` auto-download.
112
+ - Root detection: from each edited file, up to `maxRootDepth` (default 10) levels to the nearest manifest root; a package that is **itself** a workspace root yielding `pnpm check` keeps its own root (a change in one package must not fan out into the whole umbrella workspace), and otherwise a monorepo package is upgraded to the workspace root holding `pnpm-workspace.yaml` — but 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).
113
+ - Budget and concurrency: total budget `gateBudgetMs`; each root gets the smaller of `max(10s, budget / root count)` and the time actually left; a tail with under 5s left does not start and is injected as "did not run". Gates run sequentially and only one failure is injected at a time.
114
+ - Evidence caps: 64 KiB per stream stays in memory (the tail is kept; the first error is the root cause), and when truncated a bounded 4 KiB head excerpt is read back from the spill file; the per-session edit accumulator holds 500 files, evicting oldest-first with a warning.
10
115
 
11
- ### When it runs, and when it does not
116
+ ## <img src="https://api.iconify.design/lucide/sliders-horizontal.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Settings
12
117
 
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.
14
- - Edits counted: `edit` / `write` / `str_replace_editor` writes; `view` is read-only and does not count.
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).
16
- - Skipped when:
17
- - `enabled: false`; the turn has no checkable edit (only md/txt/images/`.sh`, or edits that were denied or failed).
18
- - Subagent sessions (`origin === "subagent"` or `delegationDepth > 0`); the inbox already has pending items (the gate does not talk over the user).
19
- - The root yields no gate; the turn signal is aborted or the plugin is uninstalled.
20
- - With no shell service in the host it warns once and passes through; plan mode is not exempt — a planning turn that touched code is gated the same way.
118
+ The namespace `quality-gate` is the settings entry id; the fields projected into the form are exactly the `.volatile()` ones.
21
119
 
22
- ### Install
120
+ | Field | Type | Default | What it does |
121
+ | --- | --- | --- | --- |
122
+ | `enabled` | boolean | `true` | Master switch; off means no turn-end verification at all |
123
+ | `maxInjectsPerTurn` | natural, 1–5 | `2` | Repair-injection quota, accumulated per session across turns |
124
+ | `gateBudgetMs` | natural, 10000–600000 | `300000` | Total gate budget; a timeout is injected as "could not run" instead of passing silently |
125
+ | `memoryFeedback` | boolean | `false` | Additionally persist code-level failures to the memory gateway |
126
+ | `memoryGatewayUrl` | string | `""` (unset) | Gateway base URL; the setting wins, empty falls back to the env `TDAI_GATEWAY_URL` |
127
+ | `memoryGatewayKeyRef` | string (credential ref name) | `TDAI_GATEWAY_KEY` | Ref name of the gateway key; the value lives in the host credential service, never in settings |
128
+ | `maxRootDepth` | natural | `10` | Max levels of upward root search (deployment value, no row on the card) |
129
+ | `gateStdoutMaxBytes` | natural | `65536` | Evidence capture cap per stream (deployment value, no row on the card) |
130
+
131
+ Settings persist in the host's settings file under the dsh data directory; the card's save semantics are in "Enabling it in dsh".
132
+
133
+ ## <img src="https://api.iconify.design/lucide/network.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Public interface
134
+
135
+ - **No HTTP surface**: this package registers no routes or endpoints and offers no interface for external calls.
136
+ - **To the host**: the settings namespace `quality-gate` is registered implicitly (namespace = entry id; editable fields are the `.volatile()` ones of the exported `Config`; there is no `settings.register` call), and the schema and the registration line's `config` share that one `Config`. The page policy is declared `auto: false` — this package ships its own card, so the host must not generate a second, automatic form.
137
+ - **Injected followups** are `role: "user"` with `source: { kind: "plugin:quality-gate" }` and an id built as the prefix `quality-gate-` plus a UUID.
138
+ - **The text injected into the model comes in four kinds**, every message carrying a `[quality-gate]` prefix plus command and root context: a repair instruction; a "could not run" notice (with the budget-starved skip list embedded); a release note once the quota is spent.
139
+ - **Logging** goes through the host's named logger (the `info` / `warn` / `error` face), falling back to `console` only when that service is absent.
140
+ - **Language**: host-side text follows the official `locale` settings preference, defaulting to Chinese when unregistered and taking effect on the next turn; the card's own text goes through the official client locale, and a language switch re-renders the slot with no page reload.
141
+ - **To the bus** (when lesson-loop is installed): `report({ source: "quality-gate", category: "gate-failure" | "gate-not-run", cwd, sessionId, turn, signature: the command string, detail: the full evidence, evidence: { root, command, kind } })`. When the bus is absent the report is simply dropped and the gate keeps working; the `memoryFeedback` switch does not affect bus reporting.
142
+ - **Optional outbound call**: a `POST /v2/conversation/add` to `memoryGatewayUrl`, under a hard 5s timeout. Headers carry a Bearer token and `x-tdai-service-id` (env `TDAI_SERVICE_ID` overrides, default `default`); the body is `session_id: "dsh-agent"` plus one `assistant` message, whose `agent_id` is the cwd's tail directory name with an 8-hex sha256 prefix (same derivation as lesson-loop's project buckets).
143
+
144
+ ## <img src="https://api.iconify.design/lucide/lock.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Data and privacy
145
+
146
+ | Aspect | Facts |
147
+ | --- | --- |
148
+ | Zero outbound by default | `memoryFeedback` off plus an empty URL are a double lock — with either empty nothing is pushed and the credential store is not even queried |
149
+ | What leaves the machine | When enabled, only the command, the project root and the full error text (single-lined, untruncated) — sent to **your own** gateway |
150
+ | URL and credential safety | Credentials go to `https` or loopback addresses only (`localhost`, `127.0.0.0/8`, IPv6 loopback); plain `http` to a non-loopback host is refused as unconfigured, with the reason stated. A base URL that does not parse as a legal `http(s)` URL counts as unconfigured too |
151
+ | The key | The value lives in the host credential service (a credentials file under `$DSH_HOME`); settings store only the ref name, and a ref name that is not a valid POSIX variable name counts as unconfigured |
152
+ | Failure handling | Push failures only warn, at most once per load, and never block the turn |
153
+ | Disk reads | Manifest presence, the root `package.json` scripts, `pyproject.toml` section headers, the head of a spilled truncated file; no file writes, no telemetry |
154
+ | Memory | Session ledgers (edit accounting / injection quota / event cursors) live in process memory and are cleared when the plugin unloads |
155
+
156
+ ## <img src="https://api.iconify.design/lucide/triangle-alert.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Known boundaries
157
+
158
+ - **The whitelist has exactly four commands and cannot be extended via settings.** Roots checked by other tools (`go test`, `mypy`, `eslint`, …) get no gate.
159
+ - **Only successful edits count**, and only writes through `edit` / `write` / `str_replace_editor` (`view` is read-only). Files outside the extension whitelist (md/txt/images/`.sh`) are not counted — none of the gate commands can validate them, and a `.sh` would only drag the root resolution somewhere else and produce a false failure.
160
+ - **Out-of-process subagents (ACP / claude-code / codex) emit no dsh events**, so they are outside the gate by construction; in-process subagents are skipped and their edits are verified by the parent session's next turn.
161
+ - **Never talks over the user**: no injection when the inbox has pending items, the turn signal is aborted, or the plugin is unloading. Plan mode is not exempt — a planning turn that touched code is gated the same way.
162
+ - **When evidence is truncated with no spill file, the first error may already be gone**; the text says the evidence is incomplete instead of pretending otherwise.
163
+ - **When the sandbox policy cannot be resolved, no gate runs at all** (injected as not-run); it never falls back to the deployment-level policy — that would run the gate outside the sandbox, get it denied, and produce a false repair instruction.
164
+
165
+ ## <img src="https://api.iconify.design/lucide/circle-help.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> FAQ
166
+
167
+ | Question | Answer |
168
+ | --- | --- |
169
+ | Why did my pnpm project not run `pnpm check`? | Its root `package.json` declares no non-empty `check` script — a deliberate trade: a false failure costs more than a missed check |
170
+ | The gate said "could not run" — is my code broken? | No. Sandbox policy denial, a runner that will not start, a timeout, a cancellation, an exhausted budget, and exit 126/127 **whose stderr names the gate command itself** (tool not installed) all land here. The injected text explicitly says **do not change code because of it**, and it is reported separately as `gate-not-run`. A 126/127 raised *inside* your own `check` script is a code failure and is reported as such |
171
+ | Can it loop forever? | Once the quota is spent it injects one user-visible release note, stops auto-repair, and only counts again after a fully clean pass |
172
+ | Stop it temporarily? | Turn off `enabled` on the card, or run `dsh plugin --profile web remove @jayyuen66/dsh-quality-gate` |
173
+ | Which `tsc` is used? | The project's own install first, then the `typescript` shipped with this package, and PATH only as the last resort — never an `npx` download |
174
+ | What about code edited by subagents? | In-process subagents are covered by the parent session's next turn; out-of-process ones emit no dsh events, so they are outside the gate by construction |
175
+
176
+ ## <img src="https://api.iconify.design/lucide/wrench.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Development
23
177
 
24
178
  ```sh
25
- dsh plugin --profile web add @jayyuen66/dsh-quality-gate
179
+ pnpm install
180
+ pnpm run check # typecheck (three tsconfigs) → lint → client build → host build → tests (with coverage) → tarball inspection → format
26
181
  ```
27
182
 
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.
30
- - The runtime dependency `@jayyuen66/dsh-plugin-shared` must already be on the same registry, otherwise the whole set 404s.
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".
32
- - Installing a local directory needs pnpm ≥ 12.3.0 (12.0.0 has a known bug in that path).
183
+ - Coverage is measured over `host.ts` and `lib/**` only, with all four thresholds at 100%. `src/` is not untested: `test/client-card.test.ts` drives the card wiring and both locale dictionaries headlessly (`ui-messages.ts` is fully executed), but most of `client-entry.ts` is the render surface, which needs a DOM harness this package deliberately omits (`react-dom`/`jsdom` are not pulled in). Counting all of `src/` would turn the thresholds into a permanent red, so they are pinned to the surface that is fully verified.
184
+ - `test/publish-manifest.test.ts` enforces the publishing shape: entry points resolve, no `@deepseek-ai/*` under `dependencies`, no dates or local paths in the artifacts, with comments and documents scanned the same way. `test/dist-size.test.ts` guards the byte budget of both artifacts; `check:pack` inspects a real `pnpm pack` tarball.
185
+
186
+ ## <img src="https://api.iconify.design/lucide/scale.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> License
187
+
188
+ MIT. Source at [github.com/JayYuen666/dsh-quality-gate](https://github.com/JayYuen666/dsh-quality-gate).
33
189
 
34
- ### Enabling it in dsh
35
-
36
- - Loading is driven by this package's `cordis.patch.yml` (pointed at by `dsh.bundle.patch` in `package.json`), which inserts one line `- id: quality-gate` with `name` set to this package; `dsh plugin --profile web add/remove` maintains that line.
37
- - The host half hard-depends only on `settings` (`default.inject = ["settings"]`); `shell` / `sandboxPolicy` / `lessonLoop` / `credentials` / `sessionQuery` are all optional `ctx.get` reads, so a missing one never stops the plugin from loading.
38
- - Precedence: card runtime value > the line's `config:` > built-in default.
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`).
40
- - Saving on the card is staged: edits stay in local pending state until Save writes them key by key into settings.
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.
42
-
43
- ### The commands it runs
44
-
45
- Four whitelist commands are locked at type level (only these can reach the execution surface); one per root, priority workspace > Rust > Python > bare TS:
46
-
47
- - `pnpm check`: the root has both `pnpm-workspace.yaml` and `package.json`, and the root scripts declare a non-empty `check`. Without that declaration it falls back to the next candidate — better no gate than inventing a command that fails deterministically.
48
- - `cargo check` (`Cargo.toml`), `ruff check .` (`pyproject.toml` or `ruff.toml`), `npx tsc --noEmit` (`package.json` + `tsconfig.json`; without `tsconfig.json` it is not a TS project).
49
- - Budget and concurrency: the total budget is `gateBudgetMs`; each root gets the smaller of `max(10s, budget/root count)` and the time actually left.
50
- - When fewer than 5s remain the rest do not start and are injected as "did not run".
51
- - Gates run sequentially and only one failure is injected at a time.
52
- - Evidence caps: 64 KiB per stream stays in memory, and when truncated a bounded 4 KiB head excerpt is read back from the spill file (the first error is the root cause); the per-session edit accumulator holds 500 files, evicting oldest-first with a warning.
53
-
54
- ### Settings
55
-
56
- - `enabled`: boolean, default `true`. Off means no turn-end verification at all.
57
- - `maxInjectsPerTurn`: 1–5, default `2`. Repair-injection quota, accumulated per session across turns.
58
- - `gateBudgetMs`: 10000–600000, default `300000`. A timeout injects a failure instead of passing silently.
59
- - `memoryFeedback`: boolean, default `false`. Whether code-level failures are additionally persisted to the memory gateway.
60
- - `memoryGatewayUrl`: string, default empty = disabled; the setting wins, and when empty the env `TDAI_GATEWAY_URL` is the fallback.
61
- - `memoryGatewayKeyRef`: string (a credential ref name), default `TDAI_GATEWAY_KEY`; only the ref name is stored in settings.
62
- - Two further fields are non-volatile deployment values: the card has no row for them, they go on the registration line's `config:` only (cordis passes plain values, so a change applies on restart).
63
- - `maxRootDepth` defaults to `10` - that is the "max 10 levels" root search; `gateStdoutMaxBytes` defaults to `65536` - that is the "64 KiB per stream" evidence cap.
64
-
65
- ### Public surface
66
-
67
- - No HTTP surface: this package registers no routes or endpoints and offers no interface for external calls.
68
- - To the host: the settings namespace `quality-gate` is registered implicitly under 0.1.7 - the namespace IS the entry id, and the editable fields are the `.volatile()` ones of the `Config` the entry exports (no `settings.register` call is left).
69
- - The schema and the line `config` still share that one `Config`.
70
- - Only the page policy is declared: in the `svc.inject(["settings"], …)` child fiber it calls `settings.configure({ auto: false }, svc.fiber)` - this package ships its own card, so the host must not generate a second, automatic form.
71
- - Injected followups are `role: "user"` with `source: { kind: "plugin:quality-gate" }` and an id built as the prefix `quality-gate-` plus a UUID.
72
- - Logging goes through the host's named logger (`ctx.logger("quality-gate")`, of which this package uses the `info` / `warn` / `error` face), falling back to `console` only when that service is absent.
73
- - The text injected into the model comes in four kinds; every message carries a `[quality-gate]` prefix plus command and root context:
74
- - A repair instruction.
75
- - A "could not run" notice.
76
- - A release note once the quota is spent.
77
- - The list of gates skipped for lack of budget, embedded inside the "could not run" notice.
78
- - Both languages ship with the package; the choice follows the official `locale` settings namespace preference and defaults to Chinese when that namespace is not registered, taking effect on the next turn.
79
- - The card's own text goes through the official client locale runtime: `ctx.locale.register(NS, UI_MESSAGES)` hands over the dictionary and `ctx.locale.bind(NS)` yields the getter, so a language switch re-renders the slot with no page reload.
80
- - To the bus: `ctx.get("lessonLoop")?.report({ source: "quality-gate", category: "gate-failure" | "gate-not-run", cwd, sessionId, turn, signature: the command string, detail: the full evidence, evidence: { root, command, kind } })`.
81
- - Optional outbound call: a `POST /v2/conversation/add` to the address in `memoryGatewayUrl`, under a hard 5s timeout.
82
- - Headers: a Bearer token and `x-tdai-service-id` (env overridable, default `default`).
83
- - Body: `session_id: "dsh-agent"` plus one `assistant` message, whose `agent_id` is the cwd's tail directory name with an 8-hex sha256 prefix (same derivation as lesson-loop's project buckets).
84
-
85
- ### Data and privacy
86
-
87
- - Zero outbound traffic by default: `memoryFeedback` off plus an empty URL are a double lock — with either empty nothing is pushed and the credential store is not even queried.
88
- - When enabled, the payload is only the command, the project root and the full (untruncated) error text, sent to the gateway you configured.
89
- - Failures only warn, at most once per load, and never block the turn.
90
- - The secret itself lives in the host credential service (a credentials file under `$DSH_HOME`); a ref name that is not a valid POSIX variable name counts as unconfigured.
91
- - The relation to lesson-loop is **optional**: if the bus is absent the report is simply dropped and the gate keeps working; installing it only adds lesson persistence, and the `memoryFeedback` switch does not affect bus reporting.
92
- - This package only reads the disk (manifest presence, the root `package.json` scripts, the head of a spilled truncated file); it writes no file and collects no telemetry.
93
-
94
- ### FAQ
95
-
96
- - Why did my pnpm project not run `pnpm check`? Its root `package.json` declares no non-empty `check` script — a deliberate trade: a false failure costs more than a missed check.
97
- - The gate said "could not run" — is my code broken? No.
98
- - Sandbox policy denial, a runner that will not start, a timeout, a cancellation, exit 126/127, `npx` fetching a stub package and an exhausted budget all land here.
99
- - The injected text explicitly says "do not change code because of it", nothing is written to memory, and it is reported separately as `gate-not-run`.
100
- - Can it loop forever? Once the quota is spent it injects one user-visible release note, stops auto-repair, and only counts again after a fully clean pass.
101
- - To stop it temporarily: turn off `enabled` on the card, or run `dsh plugin --profile web remove @jayyuen66/dsh-quality-gate`.
102
- - What about code edited by subagents? In-process subagents are skipped and their edits get verified by the parent session's next turn.
103
- - Out-of-process ones (ACP/claude-code/codex) emit no dsh events, so they are outside the gate by construction.
104
- - Licensed MIT; the repository is in `repository.url` of `package.json`.
190
+ The mechanism is ported from ECC (MIT, © Affaan Mustafa) — turn-end checks the project itself declares, and edit accounting. [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md) records the upstream origin, what this package built on top of it, and the runtime-dependency boundary.