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/);
+ },
+);