From 64a6610c4e87e4cbd1cc93f513220ecea47b5e6a Mon Sep 17 00:00:00 2001 From: mindcarver Date: Wed, 30 Sep 2026 10:00:48 +0800 Subject: [PATCH] feat(dsh): mount continuity plugin via host-level user patch layer (#102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 挂载方式从复制 agent preset 改为 $DSH_HOME/cordis.patch.yml 的 insert 块, 覆盖全部 profile 与 preset(含 Electron desktop),消除 preset 拷贝漂移 - 纠正消息 provenance 改为 { kind: plugin, plugin: thinloop-continuity }, 对齐官方 dsh-hooks-codex Stop 桥 - verify-install 新增 DSH 只读挂载检查(无 CLI 探测),未挂载保持 MANUAL - 三处文档同步宿主级挂载与验证方式;版本推进 0.17.1 并新增发布说明 - 测试:挂载/未挂载/DSH_HOME/foreign-row fixture 用例与插件结构断言 --- .claude-plugin/marketplace.json | 4 +- .claude-plugin/plugin.json | 2 +- .codebuddy-plugin/marketplace.json | 4 +- .codebuddy-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- .dsh-plugin/README.md | 74 ++++++++++---- .dsh-plugin/continuity.mjs | 9 +- .zcode-plugin/plugin.json | 2 +- README.md | 2 +- config/platform-capabilities.json | 14 ++- docs/installation.md | 37 +++++-- docs/releases/v0.17.1.md | 37 +++++++ docs/verification.md | 17 +++- marketplace.json | 2 +- scripts/verify-install.mjs | 124 ++++++++++++++++++++++-- tests/platform-capabilities.test.mjs | 138 ++++++++++++++++++++++++++- tests/plugin-compatibility.test.mjs | 22 +++++ 17 files changed, 433 insertions(+), 59 deletions(-) create mode 100644 docs/releases/v0.17.1.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1753060..7dc7c77 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Thinloop skills and continuity hooks for Issue-backed discovery, read-only project-status navigation, multi-Issue project DAG decomposition and execution, evidence-backed reengineering and delivery, and approved skill evolution.", - "version": "0.17.0" + "version": "0.17.1" }, "plugins": [ { "name": "thinloop", "source": ".", "description": "Discovery, Web UIUX, architecture, Next project-status navigation, Project DAG decomposition, Execute READY-wave orchestration, Reengineering gates, QuickDev delivery, maintenance, knowledge, and evolution skills with continuity checks; Project itself remains non-executing.", - "version": "0.17.0" + "version": "0.17.1" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index f65c00a..57cfe8c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "thinloop", - "version": "0.17.0", + "version": "0.17.1", "description": "Simplify complex development with Issue-backed discovery, risk-adaptive Web experience and architecture design, non-executing multi-Issue project decomposition into dependency DAGs, bounded READY-wave execution, read-only project-status navigation, project-scale refactoring and cross-stack reimplementation, autonomous single-Issue PR delivery, evidence-backed maintenance, concise experiential knowledge, and human-approved skill evolution.", "author": { "name": "mindcarver" diff --git a/.codebuddy-plugin/marketplace.json b/.codebuddy-plugin/marketplace.json index 1753060..7dc7c77 100644 --- a/.codebuddy-plugin/marketplace.json +++ b/.codebuddy-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Thinloop skills and continuity hooks for Issue-backed discovery, read-only project-status navigation, multi-Issue project DAG decomposition and execution, evidence-backed reengineering and delivery, and approved skill evolution.", - "version": "0.17.0" + "version": "0.17.1" }, "plugins": [ { "name": "thinloop", "source": ".", "description": "Discovery, Web UIUX, architecture, Next project-status navigation, Project DAG decomposition, Execute READY-wave orchestration, Reengineering gates, QuickDev delivery, maintenance, knowledge, and evolution skills with continuity checks; Project itself remains non-executing.", - "version": "0.17.0" + "version": "0.17.1" } ] } diff --git a/.codebuddy-plugin/plugin.json b/.codebuddy-plugin/plugin.json index cb3bcf2..85e3614 100644 --- a/.codebuddy-plugin/plugin.json +++ b/.codebuddy-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "thinloop", - "version": "0.17.0", + "version": "0.17.1", "description": "Simplify complex development with Issue-backed discovery, risk-adaptive Web experience and architecture design, non-executing multi-Issue project decomposition into dependency DAGs, bounded READY-wave execution, read-only project-status navigation, project-scale refactoring and cross-stack reimplementation, autonomous single-Issue PR delivery, evidence-backed maintenance, concise experiential knowledge, and human-approved skill evolution.", "author": { "name": "mindcarver" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index e1e7ab3..8f496bd 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "thinloop", - "version": "0.17.0", + "version": "0.17.1", "description": "Simplify complex development with Issue-backed discovery, risk-adaptive Web experience and architecture design, non-executing multi-Issue project decomposition into dependency DAGs, bounded READY-wave execution, read-only project-status navigation, project-scale refactoring and cross-stack reimplementation, autonomous single-Issue PR delivery, evidence-backed maintenance, concise experiential knowledge, and human-approved skill evolution.", "author": { "name": "mindcarver" diff --git a/.dsh-plugin/README.md b/.dsh-plugin/README.md index a53756d..e4745b9 100644 --- a/.dsh-plugin/README.md +++ b/.dsh-plugin/README.md @@ -16,35 +16,73 @@ SCD 管理但不可恢复,就 `agent.steer(...)` 一条纠正消息,让 Agen `PreCompact` 的一半不在此移植;压缩后由 DSH 自身的 `AGENTS.md` 基线机制 重新注入指令。 +## 挂载层:宿主级用户 patch,而非 agent preset + +推荐把插件挂载到 **home 级用户 patch 层** `$DSH_HOME/cordis.patch.yml` +(默认 `~/.dsh/cordis.patch.yml`)。理由: + +- DSH 的 profile 组合顺序是 bundle patch → profile 自身 + `cordis.patch.yml` → home 级 `$DSH_HOME/cordis.patch.yml` → `--patch` + overlay。desktop(Electron)宿主通过同一个 `runProfile` 组合代码启动 + desktop profile,因此 home 级一行的效果覆盖 **所有 profile、所有 agent + preset**(standard、ptc、cordis 与用户自建 preset),无需复制任何 preset。 +- `agent/turn-stopping` 是按 Agent 作用域派发的 scoped 事件,而 DSH 的 + scope 事件过滤规则对 **未打标签的监听器始终放行**(祖先作用域可以观察 + 后代活动)。宿主层注册的 `ctx.on("agent/turn-stopping", ...)` 能收到每个 + Agent 的停止事件——官方 `dsh-hooks-codex` / `dsh-hooks-claude-code` 桥 + 插件正是以同样的挂载形态实现各自的 `Stop` 语义。 +- 不复制 preset 就不会随 DSH 升级漂移:shipped preset 属于部署,升级会 + 覆盖它;任何 preset 拷贝都会与新版 `standard` 逐渐失配。 + +需要把闸门限制到单个 preset 时,才把同样的行写进该 preset 的 +`agent.cordis.yml`(用户自建 preset 位于 `$DSH_HOME/.agent-presets//`)。 +注意 preset 是整份会话组合的拷贝,DSH 升级后需要人工同步,且与宿主级挂载 +同时启用会对同一停止事件各 steer 一次;两者选其一。 + ## 安装 -1. 确认 `.dsh-plugin/` 与 `hooks/` 都留在同一份 Thinloop 检出内,插件通过 +1. 确认 `.dsh-plugin/` 与 `hooks/` 都留在同一份 Thinloop 检出内:插件通过 相对路径导入 `../hooks/validate-state.mjs`,两处必须同时存在。 -2. 找到要启用的 Agent preset 的 `agent.cordis.yml`(Agent 平面组合,而非 - 宿主 `cordis.patch.yml`;生命周期事件在 Agent 作用域派发)。系统内置 - preset 位于 DSH 的 `config/agent-presets//agent.cordis.yml`。 -3. 在该组合文件的顶层列表追加一行: +2. 把十二个 Skill 链接到 `$DSH_HOME/skills`(见 + [docs/installation.md](../docs/installation.md) 的统一链接脚本)。 +3. 在 `$DSH_HOME/cordis.patch.yml` 顶层列表追加一个 `insert` 块(文件为 + `[]` 时替换为下面内容;已有其他条目时保留它们): ```yaml - - id: thinloop-continuity - name: /绝对路径/thinloop/.dsh-plugin/continuity.mjs + - insert: + - id: thinloop-continuity + name: file:///绝对路径/thinloop/.dsh-plugin/continuity.mjs ``` - `name` 既可以是包名,也可以是与组合文件目录相对的路径(如 - `./thinloop/.dsh-plugin/continuity.mjs`),或 `file:///` 绝对 URL; - Windows 下请优先使用 `file:///D:/path/to/thinloop/.dsh-plugin/continuity.mjs` - 形式,避免把盘符误解为 URL scheme。 -4. 重启或新建 DSH 会话,让新组合生效。 + 注意 `cordis.patch.yml` 是 **patch 层**:新增条目必须用 `insert` 列表, + 直接写 `- id: thinloop-continuity` 裸行会被当作按 id 更新既有条目,组合 + 时报 `entry "thinloop-continuity" not found`。裸行形式只适用于 + `cordis.yml` 组合文件(例如 agent preset 的 `agent.cordis.yml`)。 + `name` 既可以是包名,也可以是 `file:///` 绝对 URL;macOS/Linux 也可写 + 绝对路径(如 `/Users/me/thinloop/.dsh-plugin/continuity.mjs`)。Windows + 下请使用 `file:///D:/path/to/thinloop/.dsh-plugin/continuity.mjs` 形式, + 避免把盘符误解为 URL scheme。profile 自身的 + `$DSH_HOME/profiles//cordis.patch.yml` 是等效挂载点(同样用 + `insert` 块),只影响该 profile。 +4. 重启 DSH 或新建会话,让新组合生效(home 级与 profile 级 patch 在启动时 + 应用;配置了 `patchReload: live` 的 profile 会被 watcher 热加载)。 ## 验证 - 静态检查:`node --check .dsh-plugin/continuity.mjs`;共享校验器由 - `tests/validate-state.test.mjs` 覆盖。 -- 运行时验证为手动项:新会话写入一份 `managed_by` 为 `scd-quickdev` / - `scd-discovery` 但缺少章节的 `.scd/tasks/current.md`,确认 Agent 停止前被 - 纠正消息打断、补齐状态后才允许停下;没有 SCD 状态文件时应静默不干预。 - 本仓库的统一只读检查器(`scripts/verify-install.mjs`)没有可依赖的 DSH - CLI,无法自动验证插件挂载,因此该项保持 `MANUAL`。 + `tests/validate-state.test.mjs` 覆盖,插件结构由 + `tests/plugin-compatibility.test.mjs` 覆盖。 +- 组合检查(只读,不启动会话):`dsh --profile web --dump-config` 输出的 + 组合树应包含 `thinloop-continuity` 行。`desktop` profile 由 Electron 独占 + 管理,CLI 拒绝对它做 config-dump,但 home 级层对它同样生效。 +- 统一只读检查器:`node scripts/verify-install.mjs --platform dsh` 读取 + `$DSH_HOME/cordis.patch.yml` 与 `$DSH_HOME/profiles/*/cordis.patch.yml`, + 挂载行指向当前源码的 `.dsh-plugin/continuity.mjs` 时 `hooks` 检查为 + `PASS`;未挂载时保持 `MANUAL`(skills-only 仍是受支持安装形态)。 +- 运行时行为:临时目录写入一份 `managed_by` 为 `scd-quickdev` 但缺章节的 + `.scd/tasks/current.md`,在该目录运行 + `dsh --profile headless "简单任务"`,确认 Agent 停止前被纠正消息打断、 + 补齐状态后才允许停下;没有 SCD 状态文件时应静默不干预。 ## 与 Claude Code / WorkBuddy / ZCode 的差异 diff --git a/.dsh-plugin/continuity.mjs b/.dsh-plugin/continuity.mjs index 98ffb4d..fd31744 100644 --- a/.dsh-plugin/continuity.mjs +++ b/.dsh-plugin/continuity.mjs @@ -26,8 +26,11 @@ import { RELATIVE_STATE_PATH, validateState } from "../hooks/validate-state.mjs" export const name = "thinloop-continuity"; export const inject = []; -// A minimal user message matching the runtime `UserMessage` shape -// ({ id, role: 'user', content: [{ type: 'text', text }], source: { kind: 'user' } }). +// A corrective user-role message matching the runtime `UserMessage` shape +// ({ id, role: 'user', content: [{ type: 'text', text }], source }). Provenance +// is the plugin itself — the same `MessageSourceMap['plugin']` shape the +// official `dsh-hooks-codex` Stop bridge steers with — so the transcript keeps +// attributing the correction to Thinloop instead of spoofing the human user. function correctiveMessage(text) { return { id: `thinloop-continuity-${Date.now().toString(36)}-${Math.random() @@ -35,7 +38,7 @@ function correctiveMessage(text) { .slice(2)}`, role: "user", content: [{ type: "text", text }], - source: { kind: "user" }, + source: { kind: "plugin", plugin: "thinloop-continuity" }, }; } diff --git a/.zcode-plugin/plugin.json b/.zcode-plugin/plugin.json index 168c6bd..628e6ae 100644 --- a/.zcode-plugin/plugin.json +++ b/.zcode-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "thinloop", - "version": "0.17.0", + "version": "0.17.1", "description": "Simplify complex development with Issue-backed discovery, risk-adaptive Web experience and architecture design, non-executing multi-Issue project decomposition into dependency DAGs, bounded READY-wave execution, read-only project-status navigation, project-scale refactoring and cross-stack reimplementation, autonomous single-Issue PR delivery, evidence-backed maintenance, concise experiential knowledge, and human-approved skill evolution.", "author": { "name": "mindcarver" diff --git a/README.md b/README.md index d02a5d0..5836215 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

- v0.17.0 + v0.17.1   ISSUE-DRIVEN   diff --git a/config/platform-capabilities.json b/config/platform-capabilities.json index 4d9f25a..ff24ca1 100644 --- a/config/platform-capabilities.json +++ b/config/platform-capabilities.json @@ -134,7 +134,7 @@ "environment": "DSH_HOME", "suffix": "skills" }, - "summary": "把十二个 Skill 链接到 `~/.dsh/skills`", + "summary": "把十二个 Skill 链接到 `~/.dsh/skills`,并在 `$DSH_HOME/cordis.patch.yml` 挂载 `.dsh-plugin/continuity.mjs`(宿主级 Cordis 插件行)", "takesEffect": "新会话(filesystem provider 的 watcher 会自动失效并更新目录)" }, "capabilities": { @@ -143,13 +143,21 @@ { "event": "agent/turn-stopping", "source": ".dsh-plugin/continuity.mjs", - "kind": "cordis-plugin" + "kind": "cordis-plugin", + "mount": { + "layer": "host-user-patch", + "row": "thinloop-continuity", + "patchFiles": [ + "cordis.patch.yml", + "profiles/*/cordis.patch.yml" + ] + } } ] }, "verification": { "mode": "skill-links", - "summary": "十二个 Skill 链接均指向当前源码;新会话的 skill 工具可发现 `scd-next`、`scd-execute`、`scd-project` 与 `scd-quickdev`" + "summary": "十二个 Skill 链接均指向当前源码;新会话的 skill 工具可发现 `scd-next`、`scd-execute`、`scd-project` 与 `scd-quickdev`;只读检查器核对 home 级与 profile 级 `cordis.patch.yml` 中的挂载行,已挂载为 `PASS`,未挂载为 `MANUAL`" } }, { diff --git a/docs/installation.md b/docs/installation.md index a5f54a5..bd05721 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -11,16 +11,16 @@ | Pi | 把十二个 Skill 链接到 `~/.pi/agent/skills` | 新会话或执行 `/reload` | | CodeWhale | 把十二个 Skill 链接到 `~/.codewhale/skills` | 新会话 | | Reasonix | 把十二个 Skill 链接到 `~/.reasonix/skills` | 新会话 | -| DeepSeek Harness | 把十二个 Skill 链接到 `~/.dsh/skills` | 新会话(filesystem provider 的 watcher 会自动失效并更新目录) | +| DeepSeek Harness | 把十二个 Skill 链接到 `~/.dsh/skills`,并在 `$DSH_HOME/cordis.patch.yml` 挂载 `.dsh-plugin/continuity.mjs`(宿主级 Cordis 插件行) | 新会话(filesystem provider 的 watcher 会自动失效并更新目录) | | Claude Code | 安装完整插件 | 更新后重启或重新加载插件 | | WorkBuddy | 安装完整插件 | 更新后重启 WorkBuddy | | ZCode | 安装完整插件 | 更新后新建会话 | Skill 链接随源码仓库更新,但默认不启用连续性 Hook;Claude Code、WorkBuddy -和 ZCode 的完整插件会额外启用各自支持的 Hook,DeepSeek Harness 则通过手动 -挂载 `.dsh-plugin/continuity.mjs` 插件启用 `agent/turn-stopping` 连续性闸门。 -不要在同一个 Agent 中同时安装完整插件和个人 Skill 链接,以免重复暴露同名 -能力。 +和 ZCode 的完整插件会额外启用各自支持的 Hook,DeepSeek Harness 则通过把 +`.dsh-plugin/continuity.mjs` 挂载到 DSH 的 home 级用户 patch 层启用 +`agent/turn-stopping` 连续性闸门。不要在同一个 Agent 中同时安装完整插件和 +个人 Skill 链接,以免重复暴露同名能力。 ## Codex、OpenCode、Pi、CodeWhale、Reasonix 与 DeepSeek Harness @@ -141,10 +141,22 @@ skill 工具即可发现并加载 `scd-next`、`scd-execute`、`scd-project`、 「JSON Hook 清单 + 子进程处理程序」的声明式 Hook,但提供可编程的 Cordis 插件生命周期事件系统:Thinloop 通过 `.dsh-plugin/continuity.mjs` 插件注册 `agent/turn-stopping`(`Stop` 的等价物)监听器,在状态不可恢复时 -`agent.steer(...)` 让 Agent 继续补齐,而不是在不可恢复的状态上停下。该插件 -需手动挂载到 Agent preset 的 `agent.cordis.yml`(详见 -`.dsh-plugin/README.md`);DSH 未暴露第三方可用的压缩前否决点,压缩后仍由 -`AGENTS.md` 基线机制重新注入指令。 +`agent.steer(...)` 让 Agent 继续补齐,而不是在不可恢复的状态上停下。启用 +方式是把挂载块写进 **home 级用户 patch 层** `$DSH_HOME/cordis.patch.yml` +(等效挂载点为各 profile 的 `$DSH_HOME/profiles//cordis.patch.yml`): + +```yaml +- insert: + - id: thinloop-continuity + name: file:///绝对路径/thinloop/.dsh-plugin/continuity.mjs +``` + +patch 层新增条目必须用 `insert` 列表;裸行会被当作按 id 更新既有条目而报 +`entry "thinloop-continuity" not found`。该层对所有 profile 生效(含 +Electron desktop 宿主与全部 agent preset),无需复制 preset;挂载细节、事件 +作用域依据与 Windows 路径注意事项见 +[`.dsh-plugin/README.md`](../.dsh-plugin/README.md)。DSH 未暴露第三方可用的 +压缩前否决点,压缩后仍由 `AGENTS.md` 基线机制重新注入指令。 ## Evolve 权威源码 @@ -306,6 +318,13 @@ codebuddy plugin update thinloop@thinloop --scope user 页面证据、高风险确认、main 核验和精确清理门继续有效。评分器保留 unknown, 完整交付协议与真实模型证据分开记录;每次交付核对 ZCode、Claude Code、Codex, 同版本载荷漂移也需修复。范围和测量限制见 [`v0.17.0`](./releases/v0.17.0.md)。 +- 升级到 v0.17.1:DSH 连续性插件的挂载方式改为宿主级用户 patch 层——在 + `$DSH_HOME/cordis.patch.yml` 以 `insert` 列表追加 `thinloop-continuity` + 条目即可对全部 profile 与 preset 生效,不再复制 agent preset;纠正消息的 + 来源标记改为 `plugin`。`verify-install.mjs --platform dsh` 会读取 home 级 + 与 profile 级 patch 文件核对挂载,未挂载保持 `MANUAL`。升级后重启 DSH 使 + 新组合生效,并移除旧的 `standard-thinloop` 之类 preset 拷贝,避免双重 + 纠正。范围见 [`v0.17.1`](./releases/v0.17.1.md)。 - 若从 v0.6.x 升级,另确认旧 `scd-dev-loop` 已消失。 更新后可以在 Thinloop 源码仓库运行只读检查器: diff --git a/docs/releases/v0.17.1.md b/docs/releases/v0.17.1.md new file mode 100644 index 0000000..687965b --- /dev/null +++ b/docs/releases/v0.17.1.md @@ -0,0 +1,37 @@ +# Thinloop v0.17.1 + +## 范围 + +- DeepSeek Harness 连续性插件的挂载方式改为宿主级用户 patch 层:在 + `$DSH_HOME/cordis.patch.yml`(或单个 profile 的 + `$DSH_HOME/profiles//cordis.patch.yml`)以 `insert` 列表追加 + `thinloop-continuity` 条目,即可对全部 profile 与 agent preset 生效, + 包括 Electron desktop 宿主;不再要求复制 agent preset 的 + `agent.cordis.yml`,避免随 DSH 升级漂移。 +- 依据:DSH 的 scope 事件过滤对未打标签的宿主监听器始终放行,宿主层注册的 + `agent/turn-stopping` 监听器可收到每个 Agent 的停止事件,与官方 + `dsh-hooks-codex` / `dsh-hooks-claude-code` 桥的挂载形态一致;desktop 宿主 + 经 `runProfile` 使用与 CLI 相同的 profile 组合代码,home 级层必然叠加。 +- `.dsh-plugin/continuity.mjs` 的纠正消息 `source` 从 `{ kind: "user" }` 改为 + `{ kind: "plugin", plugin: "thinloop-continuity" }`,与官方桥的 Stop 处理 + 一致,transcript 不再把纠正归因于人类用户。 +- `verify-install.mjs --platform dsh` 新增只读挂载检查:扫描 home 级与 + profile 级 `cordis.patch.yml`,挂载行指向当前源码时 `hooks` 报 `PASS`, + 未挂载保持 `MANUAL`(skills-only 仍是受支持安装形态);不运行任何 CLI + 探测。 +- `.dsh-plugin/README.md`、`docs/installation.md`、`docs/verification.md` 同步 + 新挂载与验证方式;插件版本、市场清单、README 与升级入口统一为 0.17.1。 + +## 验证与边界 + +- fixture 测试覆盖挂载与未挂载两态的检查器输出,插件结构测试覆盖导出形状 + 与 `plugin` 来源标记。 +- 真实行为验收使用 `dsh --profile headless`:存在 SCD 管理但不可恢复的 + `.scd/tasks/current.md` 时 Agent 停止前被纠正消息打断并补齐;无状态文件时 + 静默不干预。 +- `dsh --profile web --dump-config` 提供只读组合核对;`desktop` profile 由 + Electron 独占管理,CLI 拒绝对它做 config-dump,home 级挂载需重启 desktop + 宿主后生效,该项为运行时手动核验。 +- 挂载行指向源码检出内的 `.dsh-plugin/continuity.mjs`,仓库更新即生效,无 + 载荷拷贝;本补丁不改变其他平台的安装与验证协议,也不表示模型效果或完整 + 交付评测有变化。 diff --git a/docs/verification.md b/docs/verification.md index 7860542..3ac0ef4 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -72,7 +72,7 @@ node evals/knowledge/runner/run.mjs --mode full | Pi | 十二个 Skill 链接均指向当前源码;Pi RPC `get_commands` 可发现十二个 `/skill:scd-*` 命令 | | CodeWhale | 十二个 Skill 链接均指向当前源码;`codewhale doctor --json` 确认全局 Skill 根、数量且跳过实时 API 探测 | | Reasonix | 十二个 Skill 链接均指向当前源码;新会话可通过 `/scd-next`、`/scd-execute`、`/scd-project` 与 `/scd-quickdev` 调用 | -| DeepSeek Harness | 十二个 Skill 链接均指向当前源码;新会话的 skill 工具可发现 `scd-next`、`scd-execute`、`scd-project` 与 `scd-quickdev` | +| DeepSeek Harness | 十二个 Skill 链接均指向当前源码;新会话的 skill 工具可发现 `scd-next`、`scd-execute`、`scd-project` 与 `scd-quickdev`;只读检查器核对 home 级与 profile 级 `cordis.patch.yml` 中的挂载行,已挂载为 `PASS`,未挂载为 `MANUAL` | | Claude Code | `claude plugin list --json` 提供版本、enabled 与安装路径;检查器从该路径核对十二个 Skill 和两个 Hook,包括 `scd-next` 与 `scd-execute` | | WorkBuddy | 不验证:WorkBuddy 无可靠只读 CLI 探测;已取消插件页核验要求 | | ZCode | `zcode plugins list --json` 提供 enabled、version、rootPath、skillCount 与 hookDetails;检查完整 Skill/Hook 载荷和两个可运行 Hook | @@ -170,9 +170,16 @@ DeepSeek Harness 没有可依赖的 CLI 或插件列表命令,安装链接通 阻断不是声明式子进程 Hook,而是可编程的 Cordis 插件:Thinloop 通过 `.dsh-plugin/continuity.mjs` 注册 `agent/turn-stopping` 监听器,在 `.scd/tasks/current.md` 属于 SCD 管理但不可恢复时 `agent.steer(...)` 让 Agent -继续补齐。挂载与运行时行为需手动核验(无只读 CLI 探测,统一检查器将其记为 -`MANUAL`):新会话写入一份缺章节的状态文件,确认 Agent 停止前被打断、补齐 -后才允许停下;DSH 未暴露第三方可用的压缩前否决点,压缩后仍由 DSH 自身的 -`AGENTS.md` 机制重新注入指令基线。 +继续补齐。插件以宿主级用户 patch 层挂载:统一检查器只读扫描 +`$DSH_HOME/cordis.patch.yml` 与 `$DSH_HOME/profiles/*/cordis.patch.yml`, +挂载行指向当前源码的 `.dsh-plugin/continuity.mjs` 时 `hooks` 检查为 `PASS`; +未挂载时保持 `MANUAL`(skills-only 安装仍是受支持形态)。运行时行为需在 +真实会话核验:临时目录写入一份缺章节的状态文件,在该目录运行 +`dsh --profile headless "简单任务"`,确认 Agent 停止前被打断、补齐后才允许 +停下;DSH 未暴露第三方可用的压缩前否决点,压缩后仍由 DSH 自身的 +`AGENTS.md` 机制重新注入指令基线。`dsh --profile web --dump-config` 可做 +只读组合核对(组合树应包含 `thinloop-continuity` 行);`desktop` profile +由 Electron 独占管理,CLI 拒绝对它做 config-dump,但 home 级挂载层对它 +同样生效,重启后应用。 完整评测方法、历史证据和限制见 [EVALUATION.md](../EVALUATION.md)。 diff --git a/marketplace.json b/marketplace.json index e51f171..40175ed 100644 --- a/marketplace.json +++ b/marketplace.json @@ -6,7 +6,7 @@ "name": "thinloop", "source": ".", "description": "Discovery, Web UIUX, architecture, Next project-status navigation, Project DAG decomposition, Execute READY-wave orchestration, Reengineering gates, QuickDev delivery, maintenance, knowledge, and evolution skills with continuity checks; Project itself remains non-executing.", - "version": "0.17.0" + "version": "0.17.1" } ] } diff --git a/scripts/verify-install.mjs b/scripts/verify-install.mjs index cef2a8b..4030bd5 100644 --- a/scripts/verify-install.mjs +++ b/scripts/verify-install.mjs @@ -4,7 +4,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { isDeepStrictEqual } from "node:util"; const SCRIPT_ROOT = path.resolve( @@ -173,9 +173,30 @@ function validateRegistry(registry) { throw new Error(`Platform ${platform.id} has an unsafe runtime probe`); } } + const hookMounts = hookMountDescriptors(platform); + if (hookMounts.length > 0 && platform.id !== "dsh") { + throw new Error(`Platform ${platform.id} must not declare a hook mount`); + } + for (const mount of hookMounts) { + if ( + mount.row !== "thinloop-continuity" || + !Array.isArray(mount.patchFiles) || + JSON.stringify(mount.patchFiles) !== + JSON.stringify(["cordis.patch.yml", "profiles/*/cordis.patch.yml"]) + ) { + throw new Error(`Platform ${platform.id} has an unsafe hook mount`); + } + } } } +/** Mount descriptors declared on a platform's cordis-plugin hooks, if any. */ +function hookMountDescriptors(platform) { + return platform.capabilities.hooks + .map((hook) => hook.mount) + .filter((mount) => mount !== undefined); +} + function makeCheck(name, status, detail) { return { name, status, detail }; } @@ -323,19 +344,102 @@ function inspectSkillLinks(platform, expected, homeDir, environment) { : "source version cannot be attributed until every link is valid", ), ); - checks.push( - makeCheck( - "hooks", - platform.capabilities.hooks.length === 0 ? "PASS" : "MANUAL", - platform.capabilities.hooks.length === 0 - ? "not supported by this installation mode" - : `${platform.capabilities.hooks.length} continuity hook (Cordis plugin) — verify in a real session; no read-only CLI probe exists`, - ), - ); + if (hookMountDescriptors(platform).length > 0) { + checks.push( + inspectHookMount(platform, expected, { homeDir, environment }), + ); + } else { + checks.push( + makeCheck( + "hooks", + platform.capabilities.hooks.length === 0 ? "PASS" : "MANUAL", + platform.capabilities.hooks.length === 0 + ? "not supported by this installation mode" + : `${platform.capabilities.hooks.length} continuity hook (Cordis plugin) — verify in a real session; no read-only CLI probe exists`, + ), + ); + } return platformResult(platform, checks); } +/** + * Read-only inspection of a Cordis-plugin host mount: the DSH user patch layers + * (`$DSH_HOME/cordis.patch.yml` plus every profile's own `cordis.patch.yml`) + * are scanned for a row naming the source checkout's hook handler. A mounted + * row proves the composition inserts the plugin; no CLI probe is run. An + * absent row stays MANUAL because a skills-only install remains a supported + * state, not a confirmed failure. + */ +function inspectHookMount(platform, expected, context) { + const mount = hookMountDescriptors(platform)[0]; + const dshHome = context.environment.DSH_HOME + ? path.resolve(context.environment.DSH_HOME) + : path.join(context.homeDir, ".dsh"); + const handlerPath = resolveFrom( + expected.sourceRoot, + platform.capabilities.hookHandler, + ); + const handlerUrl = pathToFileURL(handlerPath).href; + + const candidates = []; + for (const pattern of mount.patchFiles) { + const segments = pattern.split("/"); + if (segments.includes("*")) { + const wildcardIndex = segments.indexOf("*"); + const anchor = path.join(dshHome, ...segments.slice(0, wildcardIndex)); + let entries; + try { + entries = fs.readdirSync(anchor, { withFileTypes: true }); + } catch (error) { + if (error?.code !== "ENOENT") throw error; + continue; + } + for (const entry of entries) { + // `node_modules` beside the profiles holds bundle patches owned by the + // installation, not user mount rows. + if (!entry.isDirectory() || entry.name === "node_modules") continue; + candidates.push( + path.join(anchor, entry.name, ...segments.slice(wildcardIndex + 1)), + ); + } + } else { + candidates.push(path.join(dshHome, ...segments)); + } + } + + const mountedIn = []; + const unreadable = []; + for (const candidate of candidates) { + let text; + try { + text = fs.readFileSync(candidate, "utf8"); + } catch (error) { + if (error?.code === "ENOENT") continue; + unreadable.push(`${candidate}: ${error.message}`); + continue; + } + if (text.includes(handlerPath) || text.includes(handlerUrl)) { + mountedIn.push(candidate); + } + } + + if (mountedIn.length > 0) { + return makeCheck( + "hooks", + "PASS", + `${platform.capabilities.hooks.length}/${platform.capabilities.hooks.length} Cordis plugin mounted via ${mountedIn.join(", ")}; loaded at profile boot (restart applies profile patches)`, + ); + } + return makeCheck( + "hooks", + "MANUAL", + unreadable.length > 0 + ? `plugin mount unknown (${unreadable.join("; ")}); mount per .dsh-plugin/README.md` + : `plugin not mounted in any ${mount.patchFiles.join(" or ")} under ${dshHome}; skills-only install remains supported — mount per .dsh-plugin/README.md`, + ); +} + function defaultRunCommand(command, { homeDir, environment } = {}) { const [executable, ...args] = command; return spawnSync(executable, args, { diff --git a/tests/platform-capabilities.test.mjs b/tests/platform-capabilities.test.mjs index 51458d6..b6bbb01 100644 --- a/tests/platform-capabilities.test.mjs +++ b/tests/platform-capabilities.test.mjs @@ -4,7 +4,7 @@ import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; import test from "node:test"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { formatText, @@ -902,6 +902,142 @@ test("checker can target DeepSeek Harness without probing its CLI", () => { } }); +test("DeepSeek Harness hook mount check passes when the home patch names the source handler", () => { + const homeDir = makeFixture(); + try { + linkSkills(homeDir, "dsh"); + const dshHome = path.join(homeDir, ".dsh"); + fs.mkdirSync(dshHome, { recursive: true }); + fs.writeFileSync( + path.join(dshHome, "cordis.patch.yml"), + `- id: thinloop-continuity\n name: ${pathToFileURL( + path.join(root, ".dsh-plugin", "continuity.mjs"), + ).href}\n`, + ); + const report = inspectInstallations({ + registryPath, + sourceRoot: root, + homeDir, + environment: {}, + platformId: "dsh", + runCommand: () => { + throw new Error("targeted DeepSeek Harness verification must not run a CLI probe"); + }, + }); + + assert.equal(report.exitCode, 0); + assert.equal(report.results[0].status, "PASS"); + const hooks = report.results[0].checks.find( + (check) => check.name === "hooks", + ); + assert.equal(hooks.status, "PASS"); + assert.match(hooks.detail, new RegExp(escapeRegex("cordis.patch.yml"))); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + } +}); + +test("DeepSeek Harness hook mount check accepts a profile patch with an absolute path row", () => { + const homeDir = makeFixture(); + try { + linkSkills(homeDir, "dsh"); + const profileDir = path.join(homeDir, ".dsh", "profiles", "desktop"); + fs.mkdirSync(profileDir, { recursive: true }); + fs.writeFileSync( + path.join(profileDir, "cordis.patch.yml"), + `- id: thinloop-continuity\n name: ${path.join(root, ".dsh-plugin", "continuity.mjs")}\n`, + ); + const report = inspectInstallations({ + registryPath, + sourceRoot: root, + homeDir, + environment: {}, + platformId: "dsh", + }); + + assert.equal(report.results[0].status, "PASS"); + assert.match( + report.results[0].checks.find((check) => check.name === "hooks").detail, + new RegExp(escapeRegex(path.join("profiles", "desktop"))), + ); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + } +}); + +test("DeepSeek Harness hook mount check ignores bundle patches and foreign rows", () => { + const homeDir = makeFixture(); + try { + linkSkills(homeDir, "dsh"); + const dshHome = path.join(homeDir, ".dsh"); + const profileDir = path.join(dshHome, "profiles", "web"); + fs.mkdirSync(profileDir, { recursive: true }); + fs.writeFileSync( + path.join(profileDir, "cordis.patch.yml"), + "- id: other-plugin\n name: '@deepseek-ai/dsh-something'\n", + ); + const bundlePatch = path.join( + dshHome, + "profiles", + "node_modules", + "dsh-bundle", + ); + fs.mkdirSync(bundlePatch, { recursive: true }); + fs.writeFileSync( + path.join(bundlePatch, "cordis.patch.yml"), + `- id: thinloop-continuity\n name: ${path.join( + homeDir, + "elsewhere", + ".dsh-plugin", + "continuity.mjs", + )}\n`, + ); + const report = inspectInstallations({ + registryPath, + sourceRoot: root, + homeDir, + environment: {}, + platformId: "dsh", + }); + + assert.equal(report.results[0].status, "MANUAL"); + assert.equal( + report.results[0].checks.find((check) => check.name === "hooks").status, + "MANUAL", + ); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + } +}); + +test("DeepSeek Harness hook mount honors DSH_HOME", () => { + const homeDir = makeFixture(); + const dshHomeDir = makeFixture(); + try { + const dshHome = path.join(dshHomeDir, "harness-home"); + linkSkills(homeDir, "dsh", expectedSkills, { DSH_HOME: dshHome }); + fs.mkdirSync(dshHome, { recursive: true }); + fs.writeFileSync( + path.join(dshHome, "cordis.patch.yml"), + `- id: thinloop-continuity\n name: ${pathToFileURL( + path.join(root, ".dsh-plugin", "continuity.mjs"), + ).href}\n`, + ); + const report = inspectInstallations({ + registryPath, + sourceRoot: root, + homeDir, + environment: { DSH_HOME: dshHome }, + platformId: "dsh", + }); + + assert.equal(report.results[0].status, "PASS"); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + fs.rmSync(dshHomeDir, { recursive: true, force: true }); + } +}); + test("checker targets CodeWhale and rejects a mismatched runtime Skill root", () => { const homeDir = makeFixture(); const environment = { diff --git a/tests/plugin-compatibility.test.mjs b/tests/plugin-compatibility.test.mjs index 08e72a8..a59412c 100644 --- a/tests/plugin-compatibility.test.mjs +++ b/tests/plugin-compatibility.test.mjs @@ -188,3 +188,25 @@ test("shared skills recognize both repository instruction conventions", () => { assert.match(maintenance, /`AGENTS\.md`、`CLAUDE\.md`/); assert.match(reengineering, /`AGENTS\.md`、`CLAUDE\.md`/); }); + +test( + "DeepSeek Harness continuity plugin keeps the Cordis plugin contract", + { skip: process.platform === "win32" }, + async () => { + const pluginUrl = new URL("../.dsh-plugin/continuity.mjs", import.meta.url); + const plugin = await import(pluginUrl.href); + + assert.equal(plugin.name, "thinloop-continuity"); + assert.deepEqual(plugin.inject, []); + assert.equal(typeof plugin.apply, "function"); + + const source = fs.readFileSync(pluginUrl, "utf8"); + // Steered corrections must attribute to the plugin, matching the official + // dsh-hooks-codex Stop bridge, instead of spoofing the human user. + assert.match(source, /kind: "plugin", plugin: "thinloop-continuity"/); + assert.doesNotMatch(source, /kind: "user"/); + // The gate must share one validator with the subprocess hooks. + assert.match(source, /from "\.\.\/hooks\/validate-state\.mjs"/); + assert.match(source, /agent\/turn-stopping/); + }, +);