# dsh-file-claim [![English](https://img.shields.io/badge/lang-English-blue.svg)](README.md) [![简体中文](https://img.shields.io/badge/lang-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-red.svg)](README.zh-CN.md) [![npm version](https://img.shields.io/npm/v/dsh-file-claim)](https://www.npmjs.com/package/dsh-file-claim) [![npm downloads](https://img.shields.io/npm/dm/dsh-file-claim)](https://www.npmjs.com/package/dsh-file-claim) [![CI](https://github.com/Nwflower/dsh-file-claim/actions/workflows/ci.yml/badge.svg)](https://github.com/Nwflower/dsh-file-claim/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![node](https://img.shields.io/badge/node-%3E%3D18-green.svg)](package.json) > **并行写作,永不覆盖。** > 同一工作区并行 DeepSeek Harness (DSH) 会话的文件认领/保护插件。 多个 DSH 会话并行操作同一工作区时,彼此毫无感知:两个会话可能覆盖同一文件、崩溃会话留下 陈旧状态、想改他人已占文件的会话只能干等或赌。`dsh-file-claim` 把一套久经验证的协调协议做成 原生 DSH 工具、生命周期事件与写入守卫——让并行 Agent 协作而非互相踩踏。 ```text claim_files({ paths: ["README.md"] }) # 「我来改这个文件」 write / edit ... # 写他人认领的文件会被拒绝 release_files({ paths: ["README.md"] }) # 「改完了」——等待中的 pending 编辑现在自动合并 ``` ## 目录 - [特性](#特性) - [为什么需要它](#为什么需要它) - [安装](#安装) - [快速开始](#快速开始) - [使用示例](#使用示例) - [工具](#工具) - [命令](#命令) - [写入守卫](#写入守卫) - [配置](#配置) - [审计日志](#审计日志) - [Pending 合并区](#pending-合并区) - [拦截边界](#拦截边界) - [常见问题](#常见问题) - [开发](#开发) - [相关项目](#相关项目) - [许可证](#许可证) ## 特性 - 🔒 **claim / release** —— 会话在编辑前声明对文件路径的独占认领;重复认领幂等合并,目录 认领覆盖其下所有路径,`'.'` 认领整个工作区。 - ❤️ **心跳 + stale 接管 + 孤儿自愈** —— 心跳经 agent 生命周期事件自动刷新;崩溃/强杀的会话 认领在**下一次活动时立即清除**(按进程 pid 检查),心跳间隔兜底清扫;`staleMs`(2h,针对 无 pid 的旧记录)与 `--force` 接管仍是慢速兜底。 - 🧩 **异步 pending 合并区** —— 不阻塞:会话把「改好的新内容 + git HEAD base」写入待合并区; 持有者 release 后**自动尝试** **git 三路合并**(current × base × pending),无冲突即落盘; 冲突时 `pending apply` 手动处理。 - 🛡️ **写入守卫** —— `tools/pre-execute` 拒绝写他人活跃认领文件的工具调用,附建议 (等待 / stale 后接管 / 写入 pending),并有可选的 commit 级守卫。 - ⚡ **零自动化负担** —— `agent/created` / `agent/status` 自动刷新心跳,`agent/disposed` 自动释放离开会话的全部认领。 - 📦 **纯 Host 插件、零依赖** —— 无 Browser 侧、无构建步骤,只用 `node:` 内置模块; Windows 友好。 - 🧾 **审计日志** —— 每次 claim / release / 接管 / pending 变更追加一行 JSON,供追溯与 崩溃后核对。 ## 为什么需要它 DSH 宿主无内建跨会话文件保护;505 个 `dsh-plugin` topic 仓库全量扫描**零命中** 文件认领/协调类插件。pending 合并区——现在写下改动、持有者释放后干净合并——在 agent 文件锁品类内独有。这是**填补空白而非重复造轮子**。 ### 与同类方案对比 对照 11 个 Claude Code / Codex 文件锁与协调工具(claude-code-file-locks、parallel-sessions、 guardex、agent-orchestrator、blackboard-mcp、mclaude、ruah-orch、knot 等): | 差异化 | dsh-file-claim | 同类方案 | | --- | --- | --- | | 冲突处理 | **pending 异步区 + git 三路合并**——先写入、对方释放后干净合并 | 只能等待/拒绝(「锁→写→释放」) | | 目标平台 | **DSH 原生**——身份、工具、事件、守卫、命令全集成 | Claude Code / Codex hooks;无一面向 DSH | | 平台支持 | 零依赖 Node,**Windows 友好** | Bash/jq/flock 方案偏 macOS/Linux;guardex 无原生 Windows | | 存储 | **工作区边车**——`.dshclaim` 贴在被保护文件旁(`.agentlock` 惯例) | 中央状态目录(`.coord/`、`~/.claude/…`)或 worktree 硬隔离 | | 强制层 | 工具层协作式护栏(fail-open,与品类事实标准一致) | hook 拦截/声明式锁;头部工具退化为 worktree 硬隔离 | ## 安装 ```sh dsh plugin add dsh-file-claim ``` 开发/手工验证(本地 checkout): ```sh dsh plugin --profile web add -w link:<仓库路径> ``` 要求 DSH 环境 `node >= 18`,且 `git` 在 `PATH` 中(仅三路合并时使用)。 ## 快速开始 1. **先认领,再落笔。** 要改文件?先调用 `claim_files` 声明独占认领,其他会话就不会碰它。 2. **放心写。** 自己的认领永不阻塞自己;写入被*其他*活跃会话认领的文件会被拒绝,并附带提示 (等待 / 对方 stale 后接管 / 写入 pending)。 3. **文件被占?别干等——写入 pending。** 用 `pending_write` 把改好的内容(含 git HEAD base) 放进待合并区。持有者 `release_files` 后自动三路合并(无冲突即落盘);冲突时 `pending_apply` 手动处理。 4. **写完释放。** `release_files` 清空认领、自动合并等待中的 pending 条目,并浮出需要手动 处理的条目。 ```text claim_files({ paths: ["README.md", "src/"] }) write / edit ... release_files({ paths: ["README.md"] }) ``` ## 使用示例 **两个会话,一个工作区。** 会话 A 持有 `README.md`;会话 B 也想改它: ```text // 会话 A claim_files({ paths: ["README.md"], note: "重写文档" }) write ... README.md // 允许:自己的认领 release_files({ paths: ["README.md"] }) // 会话 B —— 同时进行 who_claims({ paths: ["README.md"] }) // → 被 A 认领 write ... README.md // → 拒绝并附提示 pending_write({ path: "README.md", content: "..." }) // 异步,不阻塞 // A release 后条目自动三路合并(或浮出供手动 pending_apply) ``` **从崩溃会话恢复。** 会话 A 中途崩溃;其认领在 `staleMs`(默认 2h)后过期: ```text claim_status() # → A 显示 [stale] claim_files({ paths: ["README.md"], force: true }) # 接管 ``` ## 工具 8 个模型可见工具(身份即调用会话,无需 `--as`): | 工具 | 用途 | | --- | --- | | `claim_files` | 编辑前独占认领文件/目录(`paths`、可选 `note`、stale 接管用 `force`) | | `release_files` | 释放指定路径(`paths`)或全部(`all`) | | `who_claims` | 只读:查询路径被谁认领 | | `claim_status` | 只读:会话登记、认领、待合并区总览与最近审计 | | `pending_write` | 异步写:目标被其他活跃会话占用时,把改好的内容(+ git HEAD base)写入待合并区 | | `pending_apply` | 三路合并 `current × base × pending` 落盘;无冲突自动清除,冲突写标记 | | `pending_show` | 只读:查看某待合并条目的元信息与内容 | | `pending_drop` | 丢弃某待合并条目(不合并) | ## 命令 人工可用的斜杠命令(与上述工具同语义——模型不可用或习惯命令行时使用)。命令名后的行按 引号感知分词,含空格的路径与备注可用(`--note "多 行 备注"`)。命令执行只记入会话日志, 绝不进模型历史。 | 命令 | 用途 | | --- | --- | | `/claim ... [--note <备注>] [--force]` | 独占认领文件/目录;`--force` 接管 stale 持有者 | | `/release [... \| --all]` | 释放指定路径或全部 | | `/claim-status` | 只读:会话登记、认领与待合并区总览 | 纯逻辑核心同时提供 CLI:`node claim.mjs status | audit [n] | claim ...`——语义相同, 无需 DSH 环境。 ## 写入守卫 `tools/pre-execute` 拒绝 `write` / `edit` / `bash` / `pwsh` 调用中目标路径被**其他**活跃会话 认领的情况。拒绝信息带持有者与建议:等 `release_files`、对方 stale 后 `claim_files(force: true)` 接管、或 `pending_write` 异步写入。`read` **不拦截**——读取是观察不是修改,认领契约只保护写面。 shell 路径解析(`bash`/`pwsh`)为尽力而为:只提取**重定向目标**与**显式写命令的目标参数** (pwsh `Set-Content` / `Add-Content` / `Out-File` / `New-Item` / `Copy-Item` / `Move-Item` / `Remove-Item` / `Rename-Item`;bash `tee` / `dd of=` / `cp` / `mv` / `rm`)。**引号字面量绝不 视为写目标**——它们是数据/URL/模式,不是要写的文件;解析不出目标即放行(fail-open)。 开启 `guardCommit: true` 后,`git commit` **显式**提交其他会话活跃认领路径(`git commit -- ` 或老语法 `git commit `)也会被拒绝;提交信息(message)绝不检查,裸 `git commit`(无路径) 放行——其改动范围无法获知。 ## 配置 在 bundle(`cordis.patch.yml`)中作为插件 config 传入: | 键 | 默认 | 含义 | | --- | --- | --- | | `staleMs` | `7200000`(2h) | 心跳过期多久视为 stale | | `guard` | `true` | 设 `false` 关闭 pre-execute 写入守卫 | | `guardCommit` | `false` | 可选:额外拦截 `git commit` 显式提交其他会话活跃认领的路径 | | `heartbeatMs` | `600000`(10min) | 兜底心跳间隔 | ```yaml - insert: - id: dsh-file-claim name: dsh-file-claim config: staleMs: 3600000 # 1 小时 guardCommit: true # 同时守卫显式 git commit ``` 0.2.0 起状态以**工作区边车文件**存储——锁状态贴着被保护的文件走(对标 claude-code-file-locks 的 `.agentlock` 惯例;之所以不造命名空间目录,是因为 DSH 并没有在工作区保留任何 `.dsh/` 之类的约定目录): ```text README.md.dshclaim 认领边车(JSON:version, path, tag, note, startedAt, lastSeenAt, pid) README.md.dshpending/ 待合并边车(content / base / meta.json),与被保护文件同目录 .dsh-file-claim.audit.jsonl 工作区全局审计日志(追加式,位于仓库根) .dsh-file-claim.lock 瞬态跨进程互斥锁(仅在变更时短暂存在) ``` 建议加入 `.gitignore`: ```gitignore *.dshclaim *.dshpending/ .dsh-file-claim.audit.jsonl .dsh-file-claim.lock ``` 工作区仍存在 ≤0.1.7 的平铺 `.dsh-file-claim/` 目录(或未发布 0.2.0 的 `.dsh/dsh-file-claim/`)时,第一次 工具/命令调用会**迁移**成边车(旧目录原样保留——确认无误后可手动删除;其 `registry.json` 改名为 `registry.json.migrated` 作为幂等标记);迁移发生前的写入守卫也直接读旧注册表,升级期间保护无缝衔接。 状态跨重启保留;绝不触碰 `.git/`。 ## 审计日志 每个业务变更——claim、接管、release、pending 写/apply/drop、prune、drop——都以一行 JSON 追加到 `.dsh-file-claim.audit.jsonl`(`{ at, tag, type, paths/path, detail }`),供追溯与崩溃后核对。 心跳**刻意不记**(避免噪音)。`node claim.mjs audit [n]` 打印最近 `n` 条(默认 10); `claim_status` 恒显示最近 3 条。审计只追加、不参与也不改变认领语义;审计写入失败只会提示 警告行,不阻断操作。文件超过 1MB 自动轮转(保留最近一半 + 新条目),不会无限增长。 ## Pending 合并区 存储布局(被保护文件旁的边车目录): ```text .dshpending/content 待合并的新文件内容 .dshpending/base 写入时 git HEAD 版本(合并 base) .dshpending/meta.json { pender, claimedBy, at, baseSha } ``` 写入条件:`pending_write` 要求目标被其他会话**活跃**认领——否则应 `claim_files` 后直接写。 `base` 仅在 git HEAD 含该路径时记录;无 base 是刻意标注的不可自动合并条目。 apply 语义(`pending_apply`):用 `git merge-file` 对 `current × base × pending` 三路合并 (三个真实文件快照暂存临时目录)。无冲突 → 合并内容落盘并清除条目;有冲突 → 带冲突标记的 合并结果落盘且条目**保留**供手动解决;缺 base → 拒绝,绝不盲合;任一会话仍活跃占用 → 拒绝直至释放。 `release_files` 带解锁检查:指向被释放路径(或释放会话)的待合并条目会**自动尝试三路合并** ——无冲突即落盘并清除条目;无法自动合并(仍被占用 / 缺 base / 冲突 / 文件缺失)的条目保留, 并附 `pending_apply` / `pending_show` / `pending_drop` 手动处理提示。 ## 拦截边界 守卫是**协作式护栏**,不是强制锁:任意 shell 命令(`echo > file`、`git checkout`、脚本)、 外部编辑器、IDE/git 操作完全绕过工具栈。它把「靠 AGENTS.md 自律」升级为「工具层护栏 + 模型可见状态」,与整个品类的 fail-open 定位一致。 ## 常见问题 **bash/pwsh 写入能完全拦截吗?** 不能。只解析重定向目标与显式写命令的目标参数;任意 shell、 脚本、外部编辑器与 IDE/git 操作都绕过工具栈。这是文档化的协作边界,不是缺陷——见 [拦截边界](#拦截边界)。 **崩溃会话的认领多久清除?** 正常**立即**:每个会话记录携带进程 pid,任何会话活动 (claim / release / sync / prune)都会清扫 pid 已死的记录——崩溃/强杀在**下一次活动时即刻 清除**,无需等待。`agent/disposed` 对正常离开的会话即时释放;`staleMs`(默认 2h)只是无 pid 旧记录(如旧版本写入)的慢速兜底。 **支持多仓库并行吗?** 支持。认领根 = 会话 cwd 解析出的工作区(`workspaceRegistry`), 无工作区时回退 cwd——多仓库天然隔离。 **状态存在哪?** 工作区边车文件:每个被认领文件旁的 `<目标>.dshclaim`、每个待合并目标旁的 `<目标>.dshpending/`、仓库根的 `.dsh-file-claim.audit.jsonl`。按上面的 gitignore 规则加入 `.gitignore`;跨重启保留,绝不触碰 `.git/`。从 ≤0.1.7 升级的工作区,旧 `.dsh-file-claim/` 里的状态会在第一次工具/命令调用时迁移成边车(旧目录保留,确认后可手动删除)。 **为什么模型看不到 claim 工具?** 模型可见工具取决于部署的工具展示/限制(与所有插件工具 相同)。插件经 `ctx.tools.register` 全局注册,与官方工具包同一路径。 **pending 条目无法合并怎么办?** 条目保留并附原因(仍被占用 / 缺 base / 冲突 / 文件缺失)。 用 `pending_show` 查看、`pending_apply` / `pending_drop` 处理——绝不盲合。 **认领边车与审计文件会无限增长吗?** 不会。stale 会话的边车在心跳间隔被**自动清理**(离开会话的 记录不会累积);`.dsh-file-claim.audit.jsonl` 超 1MB 自动轮转。两者在正常使用下都保持有界。 ## 开发 ```sh npm test # node --test:claim.mjs 单测(20)+ index.mjs mock ctx 集成(13) npm pack --dry-run ``` 结构:`claim.mjs` 是零依赖纯逻辑核心(可移植,保留 CLI 入口);`index.mjs` 是唯一宿主面文件; `test/` 覆盖两者。CI 跑测试、双语 README 结构同步检查与 pack dry-run。 ## 相关项目 - [dsh-chat-import](https://github.com/Nwflower/dsh-chat-import) —— 姊妹 DSH 插件,本插件的 `session.mjs` 协调协议移植并增强自该项目。 - [awesome-dsh-plugin](https://github.com/bruc3van/awesome-dsh-plugin) —— DSH 插件生态索引 (本项目调研时扫描过 505 个仓库)。 - [@deepseek-ai/dsh](https://www.npmjs.com/package/@deepseek-ai/dsh) —— DeepSeek Harness 宿主。 ## 许可证 MIT —— 见 [LICENSE](LICENSE)。