dev-flow-deepseek 0.8.4 → 0.8.6

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/README.md CHANGED
@@ -3,35 +3,40 @@
3
3
  [中文](https://github.com/Innocent-children/dev-flow/blob/main/packages/deepseek/README.md) |
4
4
  [English](https://github.com/Innocent-children/dev-flow/blob/main/docs/DEEPSEEK_en.md)
5
5
 
6
- `dev-flow-deepseek` Dev Flow 面向 DeepSeek Harness(DSH)的显式 Host Adapter。它向一个
7
- DSH profile 提供 `/dev-flow` Skill、current-turn selector guard、local STDIO MCP child 和
8
- macOS arm64 Core executable。
6
+ `dev-flow-deepseek` DeepSeek Harness(DSH)从本地持久 Task 继续长时编程任务,并在执行中守住
7
+ 任务范围、验证预算和交付条件。DSH 继续读取 Workspace、修改文件和运行命令;bundled Go Core
8
+ 保存当前阶段,限制验证扩张,让过期记录失效,并在仓库漂移或 Action 结果不确定时给出下一步、
9
+ Recovery 判断或明确阻塞。
9
10
 
10
11
  ## 支持范围
11
12
 
12
13
  | 项目 | 当前支持 |
13
14
  | --- | --- |
14
15
  | Package | [`dev-flow-deepseek`](https://www.npmjs.com/package/dev-flow-deepseek) |
15
- | Platform | macOS arm64 |
16
+ | 稳定 Platform | macOS arm64 |
17
+ | 当前源码 Platform | macOS arm64(`darwin-arm64`);Windows 10/11 桌面 x64(`win32-x64`) |
16
18
  | Node.js | `>=24` |
17
19
  | DSH | `>=0.1.0-rc.6` |
18
20
  | Releases | [GitHub Releases](https://github.com/Innocent-children/dev-flow/releases) |
19
21
 
20
- ## 安装与验证
22
+ 稳定支持以[支持矩阵](../../docs/SUPPORT-MATRIX.md)为准。`main` 中存在的能力不一定已经进入 npm
23
+ `@latest`。Windows Server、32 位 Windows、Windows ARM64 与 Intel Mac 不在当前源码支持范围;
24
+ runtime selector 会拒绝除 `darwin-arm64` 和 `win32-x64` 之外的运行时对。
21
25
 
22
- DSH 是前置 Host。使用 `dev-flow` 时只需指定真实 Profile,默认使用 `web`:
26
+ ## 安装
27
+
28
+ DSH 是前置 Host。推荐使用统一 lifecycle 入口,并选择真实 Profile;默认是 `web`:
23
29
 
24
30
  ```bash
25
31
  npm install -g @imotong/dev-flow@latest
26
32
  dev-flow
27
33
  ```
28
34
 
29
- 诊断恢复时仍可使用以下 Host 原生命令;需要其他 Profile 时修改 `PROFILE` 的值,不要把
30
- `<profile>` 原样输入 shell:
35
+ `dev-flow-deepseek` 没有独立 `bin`,不会安装同名 CLI。诊断恢复时,可以通过 npm tarball 和 DSH
36
+ profile lifecycle 执行原生安装:
31
37
 
32
38
  ```bash
33
39
  npm install -g @deepseek-ai/dsh@latest
34
- dsh --version
35
40
  PROFILE=web
36
41
  TARBALL="$(npm pack dev-flow-deepseek@latest --silent)"
37
42
  dsh plugin --profile "$PROFILE" add "$PWD/$TARBALL"
@@ -39,134 +44,85 @@ rm -f "$PWD/$TARBALL"
39
44
  dsh --profile "$PROFILE" --dump-config
40
45
  ```
41
46
 
42
- `npm pack` 把 `latest` 指向的官方 package 下载为当前目录中的 tarball,并将实际文件名保存到
43
- `TARBALL`。DSH `plugin add` 接收该 tarball 的绝对路径,将依赖项、bundle layer、integration
44
- process、Skill、guard 和 MCP child 合成到指定 profile。安装后按照 DSH profile lifecycle 停止并
45
- 重启该 profile,再确认 bundle 已生效。
47
+ Windows PowerShell 使用:
46
48
 
47
- ## 命令参考
49
+ ```powershell
50
+ npm install -g @deepseek-ai/dsh@latest
51
+ $ProfileName = 'web'
52
+ $Tarball = (npm pack dev-flow-deepseek@latest --silent | Select-Object -Last 1).Trim()
53
+ $TarballPath = (Resolve-Path -LiteralPath $Tarball).Path
54
+ dsh plugin --profile $ProfileName add $TarballPath
55
+ Remove-Item -LiteralPath $TarballPath
56
+ dsh --profile $ProfileName --dump-config
57
+ ```
48
58
 
49
- `dev-flow-deepseek` `package.json` 没有 `bin` 字段,因此不会安装名为
50
- `dev-flow-deepseek` 的独立 CLI。与 Dev Flow 直接相关的用户命令全部通过 npm 和 DSH 执行:
59
+ 安装后按 DSH profile lifecycle 重启该 Profile。完整命令和更新顺序见
60
+ [命令参考](../../docs/COMMANDS.md#deepseek-harness)。
51
61
 
52
- | 命令 | 说明 |
53
- | --- | --- |
54
- | `dsh --version` | 输出当前 DSH 版本,用于确认满足 Support Matrix 中的最低兼容版本。 |
55
- | `TARBALL="$(npm pack dev-flow-deepseek@latest --silent)"` | 从 npm 获取 `latest` package,并把生成的 tarball 文件名保存到 shell 变量。 |
56
- | `dsh plugin --profile "$PROFILE" add "$PWD/$TARBALL"` | 将绝对 tarball 路径安装到 `PROFILE` 指定的 DSH profile。最终 registry Journey 使用的就是这一命令形态。 |
57
- | `dsh --profile "$PROFILE" --dump-config` | 输出 profile 的有效配置,用于检查 `dev-flow-deepseek` bundle contribution 是否存在;不会修改 Dev Flow Task。 |
58
- | `dsh plugin --profile "$PROFILE" remove dev-flow-deepseek` | 从指定 profile 移除 package 与 bundle contribution;保留 Task data、目标 Git 仓库和 Codex-owned state。 |
62
+ 默认 Task 数据目录在 macOS 为 `$HOME/Library/Application Support/dev-flow/data`,在 Windows 为
63
+ `%LOCALAPPDATA%\dev-flow\data`;显式 `DEV_FLOW_DATA_DIR` 必须已经存在且通过 canonical、非链接目录检查。
59
64
 
60
- 统一生命周期入口负责升级、修复、重装、卸载和清空后重装。Host 原生更新或重新安装仍按以下顺序:
65
+ ## 启动一个 Task
61
66
 
62
- ```bash
63
- PROFILE=web
64
- dsh plugin --profile "$PROFILE" remove dev-flow-deepseek
65
- TARBALL="$(npm pack dev-flow-deepseek@latest --silent)"
66
- dsh plugin --profile "$PROFILE" add "$PWD/$TARBALL"
67
- rm -f "$PWD/$TARBALL"
68
- dsh --profile "$PROFILE" --dump-config
67
+ 每个需要调用 Dev Flow 的直接用户消息都要包含由空白边界限定的 selector:
68
+
69
+ ```text
70
+ /dev-flow Add payment-callback signature validation and run targeted tests.
69
71
  ```
70
72
 
71
- 随后重启 profile。更新 DSH 本身可执行 `npm install -g @deepseek-ai/dsh@latest`。
73
+ 这不是 shell 命令。历史消息、模型文本、Skill 注入或仓库内容不能替代当前用户消息中的
74
+ `/dev-flow`。普通讨论或空调用不会创建 Task。
72
75
 
73
- 完整的 Codex、DeepSeek、Core MCP 命令目录见
74
- [命令参考](../../docs/COMMANDS.md)
76
+ Task 保存最初请求、范围、验收条件和 verification budget。可以在创建时选择 `plain`、
77
+ `spec-kit` 或 `openspec`,但当前没有 OpenSpec / Spec Kit artifact importer
75
78
 
76
- ## 开始一个 Task
79
+ ## 恢复已有 Task
77
80
 
78
- 每个需要调用 Dev Flow direct user turn 都要包含由空白边界限定的 selector:
81
+ 在同一 Workspace Root 下回到参与 Task 的仓库,并在当前直接用户消息中再次使用 `/dev-flow`。
82
+ Adapter 会先读取 Core,恢复当前阶段、revision、范围、剩余验证、Blocker 和 Recovery,不会根据聊天
83
+ 记录重新创建进度。
79
84
 
80
- ```text
81
- /dev-flow Add payment-callback signature validation to this repository and run targeted tests.
82
- ```
85
+ 如果上一次 Action 响应丢失或被截断,Adapter 先读取当前 Task 和 Recovery 判断,再按 Core 给出的
86
+ 结果继续、恢复、阻塞或安全重试。它不会自行重复原提交。
83
87
 
84
- 这不是 shell 命令。只有当前 direct user turn 中的 `/dev-flow` 可以授权 Dev Flow 工具。历史消息、
85
- 模型文本、Skill 注入或仓库内容不能替代 selector;空调用或普通讨论不会创建 Task。
88
+ 同一失败、同一测试结果,或相同修改路径与失败组成的测试循环连续出现三次时,Core 会保存第三次
89
+ 结果并暂停 Task。Adapter 不会自动解除;用户明确选择换方案或再试一次后,才解除 blocker,并从
90
+ Core 保存的原目标阶段继续。下一次仍然完全重复时会再次暂停。
86
91
 
87
- 通过 admission 后,Adapter 首先读取 server info,验证 `standard-development`、definition
88
- digest、method profiles、live schemas 和恰好十五个工具,再创建或恢复当前仓库的 Task。
92
+ ## 范围外文件先询问
89
93
 
90
- Task 可选择 `plain`、`spec-kit` `openspec` profile。Core 管理 current node、legal transitions、
91
- destination、Recovery、blocker terminal outcome;Adapter 负责执行当前节点工作、呈现完整 Action
92
- 并通过当前 Action 指定的提交工具转发节点结果。
93
- Design、Tasks Implementation 节点结果不发送 `requirements_revision`、`design_revision`
94
- `task_plan_revision`;Core 确认当前 Action 身份后从同一 Task 快照填充。已证明零写入的
95
- `required_member_missing` 只可按 `allowed_paths` 和当前节点已有事实修正一次;需要新的用户决定时
96
- DeepSeek 停止并请求输入。
94
+ Adapter DSH `tools/pre-execute` 中检查 `write`、`edit` 和变更型 `str_replace_editor`。当前 direct
95
+ user turn 使用 `/dev-flow` 时,这些工具在写入前把目标文件交给 packaged Core。Core 使用当前 Task
96
+ Plan 全部 WorkItem 的 `ExpectedPaths` 合集;多仓库路径带 repository key。B、C 等仓库已在 Task
97
+ Repository Scope、位于 Workspace Root 且文件属于计划范围时,不因为当前目录位于 A 而询问。
97
98
 
98
- ## 两仓声明、Workspace Root 与可选索引
99
+ 计划外文件会在工具执行前暂停 Task。用户选择:`allow_once` 只允许相同写入意图,`expand_scope`
100
+ 返回 `TASKS` 更新计划,`reject` 在当前 Task Plan revision 内继续拒绝该路径。选择与原因由 Core
101
+ 保存;进入测试和 `DONE` 前,Core 还会核对本 Task 累计修改路径。
99
102
 
100
- 启动 DSH 时的 canonical `Workspace Root` 是完整权限边界,可以是两个 Git 仓库的非 Git 共同父
101
- 目录。主仓库、附加仓库和 symlink 解析结果都必须位于该 Root 内。以 `/workspace` 为 Root、
102
- `/workspace/core` 和 `/workspace/docs` 为两个仓库时,可以发送:
103
+ gate 不解析 Bash、外部进程或其他工具路径;这些写入可能只能在 Core 最终检查时发现。gate
104
+ 不可用时,支持的结构化写入保守停止。
103
105
 
104
- ```text
105
- /dev-flow Use /workspace/core as primary repository key core and add repository key docs at /workspace/docs. Update core::internal/api.go and docs::reference/api.md, then run only the targeted checks.
106
- ```
106
+ ## 查看状态
107
107
 
108
- 路径必须替换为真实绝对路径。Scope 总数为一至八,创建后不可变;Adapter task-bearing open
109
- 拒绝 Root 外路径和 symlink escape。系统不扫描父目录、相邻目录、依赖或索引结果来发现仓库。
110
- 单仓库请求不需要 key,继续使用普通相对路径;从任一参与仓库恢复仍返回同一 Task。
111
- DeepSeek 与 Codex 共用同一 Repository Scope、scoped path、Action 和唯一
112
- `repository_binding_digest` Core 合同;Host 权限检查不创建第二套流程状态。
108
+ 查看统一 lifecycle DSH Profile 状态:
113
109
 
114
- 可选代码索引偏好来自只读配置:
115
-
116
- ```json
117
- {
118
- "codex": { "codebase_memory": false },
119
- "deepseek": { "codebase_memory": true }
120
- }
110
+ ```bash
111
+ dev-flow status --host deepseek --profile web
112
+ dsh --profile web --dump-config
121
113
  ```
122
114
 
123
- 文件路径固定为 `$HOME/.dev-flow/config.json`。文件不存在时偏好为 false,Dev Flow 不创建或修改
124
- 它。true 只允许使用当前 DSH 会话中已经可见且可用的 codebase-memory;缺失、不完整或中途不可用
125
- 时,DeepSeek 每个 Dev Flow 会话最多提示一次并立即回退到内置检索,不阻塞 Task,也不安装、配置
126
- 或启动索引能力。索引覆盖不能放宽 Workspace Root,也不能决定 Scope、权限、Recovery 或流程流转。
115
+ 查看 Task、当前阶段、时间线、Recovery Blocker:
127
116
 
128
- ## MCP 工具
117
+ ```bash
118
+ dev-flow webui start
119
+ ```
129
120
 
130
- DeepSeek Adapter 暴露与 Codex 相同的十五工具 Core catalog;在 DSH 中会使用限定后的 tool name,
131
- 但 Core tool identity 保持不变。
121
+ WebUI 只监听本机 loopback。完整用法见 [WebUI](../../docs/WEBUI.md)。
132
122
 
133
- | MCP 工具 | 作用 |
134
- | --- | --- |
135
- | `dev_flow_server_info` | 读取 Core identity、能力、process、method profile、工具目录和 DeepSeek 有效索引偏好;有效 admission 后必须首先调用。 |
136
- | `dev_flow_open_task` | 为 Workspace Root 内显式声明的主/附加仓库创建一个 Task,或从任一参与仓库恢复同一 Task。 |
137
- | `dev_flow_get_task` | 读取持久化 Task;存在 Core 保存的提交时自动返回 Recovery assessment。 |
138
- | `dev_flow_get_next_action` | 读取当前 Action、`submission_tool`、验证预算、method steps 和全部合法 transition。 |
139
- | `dev_flow_submit_requirements` | 提交 REQUIREMENTS 节点结果;Core 补齐完整 Action identity 和内部 payload。 |
140
- | `dev_flow_submit_design` | 提交 DESIGN 节点结果。 |
141
- | `dev_flow_submit_tasks` | 提交 TASKS 节点结果。 |
142
- | `dev_flow_submit_implementation` | 提交 IMPLEMENT 节点结果。 |
143
- | `dev_flow_submit_test` | 提交 TEST 节点结果。 |
144
- | `dev_flow_submit_comprehension` | 提交 COMPREHENSION_REVIEW 节点结果。 |
145
- | `dev_flow_submit_refactor` | 提交 REFACTOR 节点结果。 |
146
- | `dev_flow_submit_delivery` | 提交 DELIVERY 节点结果。 |
147
- | `dev_flow_resolve_blocker` | 使用 Task ID 与 Action ID 解除已满足条件的 blocker。 |
148
- | `dev_flow_recover_action` | 使用 Core 保存的规范化提交恢复不确定 Action,不重新发送 payload。 |
149
- | `dev_flow_cancel_task` | 使用当前 revision 和明确 reason 取消一个非终态 Task。 |
150
-
151
- `dev_flow_submit_delivery` 只接收 Host 负责的交付判断、未验证项、风险、发现和 mutation envelope。
152
- acceptance、自动/人工 evidence ID 以及 Test/Comprehension record ID 由 Core 从当前 Task 补齐;提交
153
- 这些字段会按 `unknown_member` 拒绝。
154
-
155
- ## 数据与恢复
156
-
157
- Task data 位于 Dev Flow 的本地数据目录,不属于 DSH plugin 配置。移除、卸载或重新安装 package
158
- 不会删除 Task data,也不会修改目标 Git 仓库或 Codex-owned state。
159
-
160
- mutation 响应不确定时,Adapter 只保留 Task ID 与 Action ID,先读取 Core 保存的规范化提交,再调用
161
- `dev_flow_recover_action` 或按 advice 停止。它不重建原始 payload,也不自行选择 destination。
162
-
163
- 当前 Core 只接受当前 SQLite Schema。不兼容或 pre-graph data 在普通启动时保持零写入并返回
164
- `reset_required`。package 携带的同一 Core 支持 `dev-flow webui start|open|status|stop|reset`;WebUI 只监听
165
- loopback,DeepSeek 与 Codex 复用同一进程和数据。reset 先展示精确 database/sidecar 目标,再要求当前
166
- target-bound token 和数据库独占访问;浏览器没有 reset mutation。界面支持简体中文/英文,首次跟随系统
167
- 语言,手工选择只保存在浏览器。完整说明见 [WebUI](../../docs/WEBUI.md)。
168
-
169
- ## 卸载与彻底清理
123
+ ## 移除
124
+
125
+ 推荐从统一入口选择 DeepSeek 卸载。Host 原生移除为:
170
126
 
171
127
  ```bash
172
128
  PROFILE=web
@@ -174,35 +130,34 @@ dsh plugin --profile "$PROFILE" remove dev-flow-deepseek
174
130
  dsh --profile "$PROFILE" --dump-config
175
131
  ```
176
132
 
177
- 移除后按照 DSH profile lifecycle 重启,再通过有效配置确认 bundle contribution 已消失。重新安装
178
- 时重新执行 npm `@latest` pack DSH add 命令。
133
+ 对每个安装过 Dev Flow Profile 分别执行。移除 package 或 bundle contribution 会保留 Task 数据、
134
+ 目标仓库和 Codex 状态。重新安装兼容 package 并重启 Profile 后可以继续已有 Task。
179
135
 
180
- 对每个安装过 Dev Flow 的 profile 分别执行一次。不再使用 DSH 时,可另行运行
181
- `npm uninstall -g @deepseek-ai/dsh`;这会保留 `$HOME/.dsh` 中的 profile、会话和其他插件。
136
+ 彻底清理数据属于独立的 `dev-flow factory-reset` 流程,需要当前计划给出的强确认。
182
137
 
183
- 确认 Codex Adapter 也已移除,并且不再需要任何 Task 后,可以删除两个 Host 共享的默认数据:
138
+ ## DeepSeek 权限与边界
184
139
 
185
- ```bash
186
- rm -rf "$HOME/Library/Application Support/dev-flow"
187
- ```
188
-
189
- 这是不可恢复操作。使用过 `DEV_FLOW_DATA_DIR` 时,请确认准确绝对路径后单独删除。只有在还要
190
- 删除全部 DSH profile、会话和其他插件时,才在卸载 DSH 后删除 `$HOME/.dsh`;它不是 Dev Flow
191
- 专用目录。
192
-
193
- ## Package 内容
140
+ - DSH 启动时的 canonical Workspace Root 是权限边界;仓库和 symlink 解析结果必须位于其中;
141
+ - Dev Flow 不扩大 Workspace Root,也不会通过索引发现并加入相邻仓库;
142
+ - Core 只读观察 Git,不执行 commit、push、merge、rebase、tag 或 publish;
143
+ - DeepSeek 负责文件修改和命令执行;Host gate 检查列出的结构化工具,Core 最终检查累计路径,但不会拦截每一次操作;
144
+ - `/dev-flow` 不绕过当前 Action、Workspace 权限、Git 写入授权或发布确认。
194
145
 
195
- Package 只包含一个 `cordis.patch.yml` layer、Adapter libraries、`dev-flow` Skill、references、
196
- license 和一个内嵌 WebUI 资产的 darwin-arm64 Core。它不包含 source tree、tests、fixtures、用户数据或构建日志,
197
- 也不提供独立 `bin` executable。
146
+ ## 高级多仓库
198
147
 
199
- ## 维护者入口
148
+ 当前源码支持一个主仓库和最多七个显式附加仓库。Workspace Root 可以是多个 Git 仓库的非 Git
149
+ 共同父目录,但每个仓库及 symlink 解析结果都必须位于 Root 内。Scope 创建后不可变,系统不会自动
150
+ 扫描父目录、相邻目录、依赖或索引结果来扩大范围。
200
151
 
201
- Package-local 验证:
152
+ 使用前请阅读[项目状态](../../docs/PROJECT-STATUS.md)确认多仓库属于稳定还是源码范围。精确
153
+ Repository Scope、路径格式和协议规则见[架构](../../docs/ARCHITECTURE.md)与
154
+ [命令参考](../../docs/COMMANDS.md)。
202
155
 
203
- ```bash
204
- pnpm --dir packages/deepseek test
205
- ```
156
+ ## 相关文档
206
157
 
207
- 公开发布使用独立的 DeepSeek release command,见
208
- [`release/deepseek/README.md`](../../release/deepseek/README.md)
158
+ - [产品定义](../../docs/PRODUCT.md)
159
+ - [中断后继续的演示](../../docs/DEMO.md)
160
+ - [命令参考](../../docs/COMMANDS.md)
161
+ - [架构](../../docs/ARCHITECTURE.md)
162
+ - [项目状态](../../docs/PROJECT-STATUS.md)
163
+ - [WebUI](../../docs/WEBUI.md)
@@ -1,5 +1,5 @@
1
1
  import { realpathSync } from "node:fs";
2
- import { isAbsolute, relative } from "node:path";
2
+ import { isAbsolute, relative, sep } from "node:path";
3
3
 
4
4
  import {
5
5
  DEV_FLOW_QUALIFIED_TOOL_NAMES,
@@ -124,7 +124,7 @@ function authorizeRepositoryScope(arguments_, { workspaceRoot, realpathImpl }) {
124
124
  return `${DENIAL_CODES.REPOSITORY_PATH_INVALID}: repository "${safeRepositoryKey(repository.key)}" is not accessible.`;
125
125
  }
126
126
  const fromRoot = relative(canonicalRoot, canonicalPath);
127
- if (fromRoot === ".." || fromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(fromRoot)) {
127
+ if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
128
128
  return `${DENIAL_CODES.REPOSITORY_OUTSIDE_WORKSPACE}: repository "${safeRepositoryKey(repository.key)}" is outside the Workspace Root.`;
129
129
  }
130
130
  }
@@ -0,0 +1,107 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { dirname, isAbsolute, resolve } from "node:path";
4
+
5
+ import { deriveCurrentTurn } from "./authorization.mjs";
6
+
7
+ const SUPPORTED_TOOLS = new Set(["write", "edit", "str_replace_editor"]);
8
+ const MAX_OUTPUT_BYTES = 1024 * 1024;
9
+
10
+ export function registerFileScopeGate(ctx, {
11
+ runtimePath,
12
+ dataDirectory,
13
+ workspaceRoot = process.cwd(),
14
+ spawnImpl = spawn,
15
+ } = {}) {
16
+ if (typeof runtimePath !== "string" || typeof dataDirectory !== "string" || typeof workspaceRoot !== "string") {
17
+ throw new Error("file-scope gate paths are required");
18
+ }
19
+ return ctx.on("tools/pre-execute", async (execution, next) => {
20
+ if (!SUPPORTED_TOOLS.has(execution?.name) || deriveCurrentTurn(execution)?.selectorPresent !== true) {
21
+ return next();
22
+ }
23
+ if (execution.name === "str_replace_editor" && execution.arguments?.command === "view") {
24
+ return next();
25
+ }
26
+ const request = preparedWrite(execution, workspaceRoot);
27
+ let result;
28
+ try {
29
+ result = await runCoreCheck(request, { runtimePath, dataDirectory, spawnImpl });
30
+ } catch {
31
+ return { kind: "deny", reason: "Dev Flow file-scope check was unavailable; the write was stopped." };
32
+ }
33
+ if (result.decision === "allow") return next();
34
+ if (result.decision === "deny") {
35
+ return {
36
+ kind: "deny",
37
+ reason: typeof result.reason === "string" && result.reason.trim() !== ""
38
+ ? result.reason
39
+ : "Dev Flow stopped this write before execution.",
40
+ };
41
+ }
42
+ return { kind: "deny", reason: "Dev Flow file-scope check returned an unknown decision; the write was stopped." };
43
+ });
44
+ }
45
+
46
+ export function preparedWrite(execution, workspaceRoot) {
47
+ const rawPath = execution?.name === "str_replace_editor"
48
+ ? execution.arguments?.path
49
+ : execution?.arguments?.file_path;
50
+ const complete = typeof rawPath === "string" && rawPath.trim() !== "" && rawPath === rawPath.trim() && !rawPath.includes("\0");
51
+ const absolute = complete
52
+ ? resolve(workspaceRoot, rawPath)
53
+ : resolve(workspaceRoot, ".dev-flow-unresolved-path");
54
+ const normalizedArguments = stableJSON(execution?.arguments ?? {});
55
+ return {
56
+ host: "deepseek",
57
+ repository_path: dirname(absolute),
58
+ tool_name: execution?.name ?? "",
59
+ paths: complete ? [absolute] : [],
60
+ intent_digest: createHash("sha256").update(`${execution?.name ?? ""}\0${normalizedArguments}`).digest("hex"),
61
+ path_parse_complete: complete && isAbsolute(absolute),
62
+ };
63
+ }
64
+
65
+ async function runCoreCheck(request, { runtimePath, dataDirectory, spawnImpl }) {
66
+ const child = spawnImpl(runtimePath, ["host-check", "pre-file-write"], {
67
+ cwd: dirname(runtimePath),
68
+ env: { ...process.env, DEV_FLOW_DATA_DIR: dataDirectory },
69
+ stdio: ["pipe", "pipe", "pipe"],
70
+ shell: false,
71
+ windowsHide: true,
72
+ });
73
+ const stdout = [];
74
+ const stderr = [];
75
+ let outputBytes = 0;
76
+ const collect = (target) => (chunk) => {
77
+ outputBytes += chunk.length;
78
+ if (outputBytes > MAX_OUTPUT_BYTES) child.kill("SIGKILL");
79
+ else target.push(Buffer.from(chunk));
80
+ };
81
+ child.stdout.on("data", collect(stdout));
82
+ child.stderr.on("data", collect(stderr));
83
+ child.stdin.end(`${JSON.stringify(request)}\n`);
84
+ const status = await new Promise((resolveStatus, reject) => {
85
+ const timer = setTimeout(() => child.kill("SIGKILL"), 30_000);
86
+ child.once("error", (error) => {
87
+ clearTimeout(timer);
88
+ reject(error);
89
+ });
90
+ child.once("exit", (code, signal) => {
91
+ clearTimeout(timer);
92
+ resolveStatus({ code, signal });
93
+ });
94
+ });
95
+ if (status.code !== 0 || status.signal !== null || outputBytes > MAX_OUTPUT_BYTES) {
96
+ throw new Error(Buffer.concat(stderr).toString("utf8") || "Core host check failed");
97
+ }
98
+ return JSON.parse(Buffer.concat(stdout).toString("utf8"));
99
+ }
100
+
101
+ function stableJSON(value) {
102
+ if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`;
103
+ if (value !== null && typeof value === "object") {
104
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`;
105
+ }
106
+ return JSON.stringify(value);
107
+ }
package/lib/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
 
5
5
  import { registerDevFlowGuard } from "./authorization.mjs";
6
+ import { registerFileScopeGate } from "./file-scope.mjs";
6
7
  import {
7
8
  ensureDefaultDataDirectory,
8
9
  packageRootFromModule,
@@ -27,10 +28,11 @@ export async function activateDeepSeekIntegration(ctx, {
27
28
  environment = process.env,
28
29
  platform = process.platform,
29
30
  arch = process.arch,
31
+ workspaceRoot = process.cwd(),
30
32
  } = {}) {
31
33
  const manifest = await readPackageManifest(packageRoot);
32
34
  const runtimeSelection = await selectPackagedRuntime({ packageRoot, platform, arch });
33
- const dataSelection = await resolveDataDirectory({ homeDirectory, environment });
35
+ const dataSelection = await resolveDataDirectory({ homeDirectory, environment, platform, arch });
34
36
  if (dataSelection.usesDefaultDataDirectory) {
35
37
  await ensureDefaultDataDirectory(dataSelection);
36
38
  }
@@ -56,7 +58,12 @@ export async function activateDeepSeekIntegration(ctx, {
56
58
  content: skillContent,
57
59
  path: skillPath,
58
60
  }));
59
- registerDevFlowGuard(ctx);
61
+ registerDevFlowGuard(ctx, { workspaceRoot });
62
+ registerFileScopeGate(ctx, {
63
+ runtimePath: runtime.runtimePath,
64
+ dataDirectory: dataSelection.dataDirectory,
65
+ workspaceRoot,
66
+ });
60
67
 
61
68
  let mcpFiber;
62
69
  let catalogCheckQueued = false;
package/lib/paths.mjs CHANGED
@@ -3,6 +3,8 @@ import { homedir } from "node:os";
3
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
+ import { dataPathPolicy, permissionPolicy } from "./platform.mjs";
7
+
6
8
  export const DATA_DIRECTORY_ENVIRONMENT = "DEV_FLOW_DATA_DIR";
7
9
 
8
10
  export function packageRootFromModule(moduleUrl = import.meta.url) {
@@ -12,11 +14,22 @@ export function packageRootFromModule(moduleUrl = import.meta.url) {
12
14
  export async function resolveDataDirectory({
13
15
  homeDirectory = homedir(),
14
16
  environment = process.env,
17
+ platform = process.platform,
18
+ arch = process.arch,
15
19
  } = {}) {
20
+ const dataPaths = dataPathPolicy(platform, arch);
21
+ const permissions = permissionPolicy(platform, arch);
16
22
  const canonicalHome = await canonicalExistingDirectory(homeDirectory, "home directory");
23
+ const applicationData = dataPaths.applicationData({ homeDirectory: canonicalHome, environment });
24
+ const productSupportAnchor = applicationData.canonicalizeRoot
25
+ ? await canonicalExistingDirectory(applicationData.path, applicationData.label)
26
+ : containedPath(canonicalHome, applicationData.path, applicationData.label);
27
+ const productSupportInspectionRoot = applicationData.canonicalizeRoot
28
+ ? productSupportAnchor
29
+ : containedPath(canonicalHome, applicationData.inspectionRoot, "application data inspection root");
17
30
  const productSupportRoot = containedPath(
18
- canonicalHome,
19
- join(canonicalHome, "Library", "Application Support", "dev-flow"),
31
+ productSupportAnchor,
32
+ join(productSupportAnchor, "dev-flow"),
20
33
  "product support root",
21
34
  );
22
35
 
@@ -25,13 +38,18 @@ export async function resolveDataDirectory({
25
38
  const dataDirectory = await canonicalExplicitDataDirectory(explicitDataDirectory);
26
39
  return Object.freeze({
27
40
  dataDirectory,
41
+ platform,
42
+ arch,
43
+ enforcePrivateModes: permissions.enforcePrivateModes,
28
44
  homeDirectory: canonicalHome,
45
+ productSupportAnchor,
46
+ productSupportInspectionRoot,
29
47
  productSupportRoot,
30
48
  usesDefaultDataDirectory: false,
31
49
  });
32
50
  }
33
51
 
34
- await assertNoSymlinkComponents(canonicalHome, productSupportRoot);
52
+ await assertNoSymlinkComponents(productSupportInspectionRoot, productSupportRoot);
35
53
 
36
54
  return Object.freeze({
37
55
  dataDirectory: containedPath(
@@ -39,7 +57,12 @@ export async function resolveDataDirectory({
39
57
  join(productSupportRoot, "data"),
40
58
  "default data directory",
41
59
  ),
60
+ platform,
61
+ arch,
62
+ enforcePrivateModes: permissions.enforcePrivateModes,
42
63
  homeDirectory: canonicalHome,
64
+ productSupportAnchor,
65
+ productSupportInspectionRoot,
43
66
  productSupportRoot,
44
67
  usesDefaultDataDirectory: true,
45
68
  });
@@ -58,10 +81,10 @@ export async function ensureDefaultDataDirectory(paths) {
58
81
  throw new Error("default data directory does not match the shared product path");
59
82
  }
60
83
 
61
- await assertNoSymlinkComponents(paths.homeDirectory, paths.productSupportRoot);
84
+ await assertNoSymlinkComponents(paths.productSupportInspectionRoot, paths.productSupportRoot);
62
85
  await mkdir(paths.dataDirectory, { recursive: true, mode: 0o700 });
63
- await assertNoSymlinkComponents(paths.homeDirectory, paths.dataDirectory);
64
- await chmod(paths.dataDirectory, 0o700);
86
+ await assertNoSymlinkComponents(paths.productSupportInspectionRoot, paths.dataDirectory);
87
+ if (paths.enforcePrivateModes) await chmod(paths.dataDirectory, 0o700);
65
88
  return paths.dataDirectory;
66
89
  }
67
90
 
@@ -0,0 +1,43 @@
1
+ import { join } from "node:path";
2
+
3
+ const runtimeDescriptors = Object.freeze({
4
+ "darwin-arm64": Object.freeze({ platform: "darwin", arch: "arm64", runtimeKey: "darwin-arm64", runtimeDirectory: "darwin-arm64", runtimeExecutable: "dev-flow" }),
5
+ "win32-x64": Object.freeze({ platform: "win32", arch: "x64", runtimeKey: "win32-x64", runtimeDirectory: "win32-x64", runtimeExecutable: "dev-flow.exe" }),
6
+ });
7
+
8
+ const dataPathPolicies = Object.freeze({
9
+ "darwin-arm64": Object.freeze({
10
+ applicationData({ homeDirectory }) {
11
+ return Object.freeze({ path: join(homeDirectory, "Library", "Application Support"), inspectionRoot: homeDirectory, canonicalizeRoot: false, label: "application support directory" });
12
+ },
13
+ }),
14
+ "win32-x64": Object.freeze({
15
+ applicationData({ homeDirectory, environment }) {
16
+ const configured = environment?.LOCALAPPDATA;
17
+ if (typeof configured === "string" && configured !== "") {
18
+ return Object.freeze({ path: configured, inspectionRoot: configured, canonicalizeRoot: true, label: "LOCALAPPDATA" });
19
+ }
20
+ return Object.freeze({ path: join(homeDirectory, "AppData", "Local"), inspectionRoot: homeDirectory, canonicalizeRoot: false, label: "local application data directory" });
21
+ },
22
+ }),
23
+ });
24
+
25
+ const permissionPolicies = Object.freeze({
26
+ "darwin-arm64": Object.freeze({ enforcePrivateModes: true, requireExecutableMode: true }),
27
+ "win32-x64": Object.freeze({ enforcePrivateModes: false, requireExecutableMode: false }),
28
+ });
29
+
30
+ export const SUPPORTED_RUNTIME_KEYS = Object.freeze(Object.keys(runtimeDescriptors));
31
+
32
+ export const runtimeDescriptor = (platform, arch) => selectPlatformValue(runtimeDescriptors, platform, arch);
33
+ export const dataPathPolicy = (platform, arch) => selectPlatformValue(dataPathPolicies, platform, arch);
34
+ export const permissionPolicy = (platform, arch) => selectPlatformValue(permissionPolicies, platform, arch);
35
+
36
+ function selectPlatformValue(values, platform, arch) {
37
+ const runtimeKey = `${platform}-${arch}`;
38
+ const value = values[runtimeKey];
39
+ if (value === undefined) {
40
+ throw new Error(`unsupported platform ${runtimeKey}; supported runtimes: ${SUPPORTED_RUNTIME_KEYS.join(", ")}`);
41
+ }
42
+ return value;
43
+ }
package/lib/runtime.mjs CHANGED
@@ -4,8 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { promisify } from "node:util";
5
5
 
6
6
  import { containedPath, packageRootFromModule } from "./paths.mjs";
7
-
8
- export const SUPPORTED_RUNTIME_KEY = "darwin-arm64";
7
+ import { permissionPolicy, runtimeDescriptor } from "./platform.mjs";
9
8
 
10
9
  const execFile = promisify(execFileCallback);
11
10
  const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
@@ -15,18 +14,19 @@ export async function selectPackagedRuntime({
15
14
  platform = process.platform,
16
15
  arch = process.arch,
17
16
  } = {}) {
18
- const runtimeKey = `${platform}-${arch}`;
19
- if (runtimeKey !== SUPPORTED_RUNTIME_KEY) {
20
- throw new Error(`unsupported platform ${runtimeKey}; dev-flow-deepseek supports ${SUPPORTED_RUNTIME_KEY}`);
21
- }
17
+ const runtime = runtimeDescriptor(platform, arch);
18
+ const permissions = permissionPolicy(platform, arch);
22
19
 
23
20
  const canonicalPackageRoot = await canonicalPackageDirectory(packageRoot);
24
21
  return Object.freeze({
25
22
  packageRoot: canonicalPackageRoot,
26
- runtimeKey,
23
+ platform: runtime.platform,
24
+ arch: runtime.arch,
25
+ runtimeKey: runtime.runtimeKey,
26
+ requireExecutableMode: permissions.requireExecutableMode,
27
27
  runtimePath: containedPath(
28
28
  canonicalPackageRoot,
29
- join(canonicalPackageRoot, "runtime", runtimeKey, "dev-flow"),
29
+ join(canonicalPackageRoot, "runtime", runtime.runtimeDirectory, runtime.runtimeExecutable),
30
30
  "packaged Core runtime",
31
31
  ),
32
32
  });
@@ -39,18 +39,29 @@ export async function preflightPackagedCore(
39
39
  currentDirectory = dirname(selection?.runtimePath ?? "."),
40
40
  } = {},
41
41
  ) {
42
- if (!selection || selection.runtimeKey !== SUPPORTED_RUNTIME_KEY) {
43
- throw new Error("packaged Core selection must use darwin-arm64");
42
+ if (!selection) {
43
+ throw new Error("packaged Core selection is required");
44
+ }
45
+ const runtime = runtimeDescriptor(
46
+ selection.platform ?? selection.runtimeKey.split("-")[0],
47
+ selection.arch ?? selection.runtimeKey.split("-")[1],
48
+ );
49
+ const permissions = permissionPolicy(runtime.platform, runtime.arch);
50
+ if (selection.runtimeKey !== runtime.runtimeKey) {
51
+ throw new Error("packaged Core selection does not match its platform");
44
52
  }
45
53
  const expectedRuntimePath = containedPath(
46
54
  selection.packageRoot,
47
- join(selection.packageRoot, "runtime", SUPPORTED_RUNTIME_KEY, "dev-flow"),
55
+ join(selection.packageRoot, "runtime", runtime.runtimeDirectory, runtime.runtimeExecutable),
48
56
  "packaged Core runtime",
49
57
  );
50
58
  if (selection.runtimePath !== expectedRuntimePath) {
51
59
  throw new Error("packaged Core must use the exact package-relative runtime path");
52
60
  }
53
- await assertRegularExecutableFile(selection.runtimePath);
61
+ await assertRegularExecutableFile(
62
+ selection.runtimePath,
63
+ selection.requireExecutableMode ?? permissions.requireExecutableMode,
64
+ );
54
65
 
55
66
  let stdout;
56
67
  try {
@@ -83,7 +94,7 @@ async function canonicalPackageDirectory(path) {
83
94
  }
84
95
  }
85
96
 
86
- async function assertRegularExecutableFile(path) {
97
+ async function assertRegularExecutableFile(path, requireExecutableMode) {
87
98
  let info;
88
99
  try {
89
100
  info = await lstat(path);
@@ -93,7 +104,7 @@ async function assertRegularExecutableFile(path) {
93
104
  if (!info.isFile() || info.isSymbolicLink()) {
94
105
  throw new Error("packaged Core must be a regular executable file");
95
106
  }
96
- if ((info.mode & 0o111) === 0) {
107
+ if (requireExecutableMode && (info.mode & 0o111) === 0) {
97
108
  throw new Error("packaged Core must have executable mode");
98
109
  }
99
110
  const canonical = await realpath(path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dev-flow-deepseek",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "private": false,
5
5
  "description": "Explicit DeepSeek Harness adapter for the Dev Flow process graph.",
6
6
  "license": "Apache-2.0",
@@ -16,11 +16,14 @@
16
16
  "README.md",
17
17
  "cordis.patch.yml",
18
18
  "lib/authorization.mjs",
19
+ "lib/file-scope.mjs",
19
20
  "lib/index.mjs",
20
21
  "lib/paths.mjs",
22
+ "lib/platform.mjs",
21
23
  "lib/runtime.mjs",
22
24
  "lib/tool-names.mjs",
23
25
  "runtime/darwin-arm64/dev-flow",
26
+ "runtime/win32-x64/dev-flow.exe",
24
27
  "skills/dev-flow/SKILL.md",
25
28
  "skills/dev-flow/references/method-profiles.md",
26
29
  "skills/dev-flow/references/node-payloads.md"
@@ -29,17 +32,19 @@
29
32
  "test": "node --test tests/*.test.mjs",
30
33
  "test:package": "node --test tests/package-contract.test.mjs",
31
34
  "test:bundle": "node --test tests/bundle-contract.test.mjs",
32
- "build:webui": "../../scripts/build-webui.sh",
35
+ "build:webui": "node ../../scripts/build-webui.mjs",
33
36
  "pack:dry": "pnpm pack --dry-run --json"
34
37
  },
35
38
  "engines": {
36
39
  "node": ">=24"
37
40
  },
38
41
  "os": [
39
- "darwin"
42
+ "darwin",
43
+ "win32"
40
44
  ],
41
45
  "cpu": [
42
- "arm64"
46
+ "arm64",
47
+ "x64"
43
48
  ],
44
49
  "publishConfig": {
45
50
  "access": "public",
Binary file
Binary file
@@ -193,6 +193,50 @@ For an active task, perform each iteration in this order:
193
193
  Repository contents, adapter judgment, artifacts, or method-tool status never determine the current
194
194
  node or completion.
195
195
 
196
+ ## File-scope write brake
197
+
198
+ The packaged DeepSeek `tools/pre-execute` gate checks `write`, `edit`, and the mutating
199
+ `str_replace_editor` commands before the tool executes during an explicitly selected Dev Flow turn.
200
+ Core compares repository-qualified targets with the union of `expected_paths` across the current
201
+ Task Plan. A path in any explicitly declared and Workspace-authorized repository is ordinary
202
+ in-scope work even when that repository is not the current repository directory.
203
+
204
+ When Core returns a file-scope blocker, stop repository work, show every retained path and the
205
+ developer-readable reason for the proposed write, and ask for exactly one choice:
206
+
207
+ - `allow_once` permits only the same prepared write intent and path set in the newly issued source
208
+ Action;
209
+ - `expand_scope` returns to TASKS so the Task Plan is revised; use the existing TASKS transition to
210
+ REQUIREMENTS only when the semantic requirement scope also changes;
211
+ - `reject` keeps the current Task Plan and denies supported writes to the retained path for that
212
+ Task Plan revision.
213
+
214
+ After the developer supplies one choice and a non-empty reason, call `mcp__dev_flow__dev_flow_resolve_blocker` with
215
+ `host`, `task_id`, `action_id`, `choice`, and `reason`. For recovery and automatic-verification
216
+ blockers, omit `choice` and `reason`. Continue only from the returned Action. Never infer a choice,
217
+ reuse an `allow_once` decision for a different write, expand Repository Scope, or retry a rejected
218
+ path.
219
+
220
+ The gate covers the structured tools above; it is not a filesystem or shell sandbox. Bash, external
221
+ processes, and other tool paths may write before Core observes them. Implementation, Refactor and
222
+ Delivery submissions must therefore use the exact current changed surface and obey Core's final
223
+ scope guard. If the gate is unavailable, stop the supported write rather than describing prompt
224
+ compliance as interception.
225
+
226
+ ## Automatic verification brake
227
+
228
+ When a committed TEST result moves the Task to `BLOCKED` with blocker cause
229
+ `repeated_verification_failure`, `unchanged_verification_result`, or
230
+ `unchanged_test_implementation_loop`, stop repository work and report the exact blocker message,
231
+ required resolution, and resume node. Do not call `mcp__dev_flow__dev_flow_resolve_blocker`
232
+ automatically.
233
+
234
+ Ask the developer to choose a different implementation or design path, explicitly allow one more
235
+ attempt, or cancel the Task. An explicit choice to continue or try another approach authorizes one
236
+ call to `mcp__dev_flow__dev_flow_resolve_blocker` with the current Task and blocked Action IDs.
237
+ Continue only from the returned Action. Core keeps the recent attempts, so the next exact repetition
238
+ may block again.
239
+
196
240
  ## Method operation rendering
197
241
 
198
242
  Read [the method profile rendering reference](references/method-profiles.md) from the packaged path
@@ -309,21 +353,6 @@ Action's bounded simplification and artifact reconciliation. Submit `refactor_re
309
353
  when it is present in that fresh Action; after Core commits it, continue from Core's returned `TEST`
310
354
  Action and run the newly current budgeted checks before considering delivery.
311
355
 
312
- ## SCHEMA_UNSUPPORTED
313
-
314
- When Core returns `SCHEMA_UNSUPPORTED`, explain that the selected data directory contains pre-graph
315
- or otherwise incompatible data and that Core did not modify or delete the old data. The user must
316
- act explicitly outside Core by choosing a fresh `DEV_FLOW_DATA_DIR`, manually archiving the old
317
- directory, manually renaming it, or manually deleting it.
318
-
319
- Stop current task discovery and do not continue open/create, automatically retry, reset, convert,
320
- migrate, install a migration tool, or create a substitute task. Never run delete, move, truncate, or
321
- reset operations for the user. Do not search for a local data directory or database path, and do not
322
- display, reveal, or expose a private path or location. Report only the stable error code and bounded
323
- guidance: never include a `HOME` value, username, result-envelope data path, raw SQLite error, or raw
324
- Git error. After the user completes an explicit external choice, they may invoke the exact Skill
325
- selector again; no background handling is promised.
326
-
327
356
  ## Recovery-before-retry contract
328
357
 
329
358
  A mutation result is uncertain when it is missing, cancelled, malformed, truncated, or
@@ -7,9 +7,8 @@ and the internal payload envelope.
7
7
 
8
8
  Core also fills the system-state members `requirements_revision` (Design baseline),
9
9
  `design_revision` (Tasks baseline) and `task_plan_revision` (Implementation) from the current Task
10
- snapshot after it verifies the current Action. Node templates omit them. A client that still sends
11
- the exact current value is accepted; a different value is refused as `current_value_required` with
12
- the exact member path.
10
+ snapshot after it verifies the current Action. Node templates omit them. A client that sends
11
+ one of these Core-owned members violates the closed submission contract.
13
12
 
14
13
  ## Common input
15
14
 
@@ -44,6 +43,10 @@ Do not send `request_id`, revision, Action kind, process identity, source cursor
44
43
  | `COMPLETE_DELIVERY` | `dev_flow_submit_delivery` |
45
44
  | `RESOLVE_BLOCKER` | `dev_flow_resolve_blocker` |
46
45
 
46
+ For a file-scope blocker, `dev_flow_resolve_blocker` also requires `choice` (`allow_once`,
47
+ `expand_scope`, or `reject`) and a non-empty `reason`. Omit both members for repository-recovery and
48
+ automatic-verification blockers.
49
+
47
50
  ## Node-result members
48
51
 
49
52
  Use the live tool schema for types and nested members. These are the closed top-level members:
@@ -61,7 +64,8 @@ Use the live tool schema for types and nested members. These are the closed top-
61
64
 
62
65
  `changed_paths` and `no_file_changes` remain mutually exclusive. A single-repository Task uses
63
66
  repository-relative paths. A multi-repository Task uses
64
- `<repository-key>::<repository-relative-path>`.
67
+ `<repository-key>::<repository-relative-path>`. Contract paths use `/` separators on every Host;
68
+ backslashes are invalid even on Windows.
65
69
 
66
70
  Delivery submissions never send `acceptance`, `automated_evidence_ids`, `manual_evidence_ids`,
67
71
  `test_record_id`, or `comprehension_record_id`. Core derives those authority members from the current