dev-flow-deepseek 0.8.8 → 0.8.9

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
@@ -7,10 +7,9 @@
7
7
  [中文](https://github.com/Innocent-children/dev-flow/blob/main/packages/deepseek/README.md) |
8
8
  [English](https://github.com/Innocent-children/dev-flow/blob/main/docs/DEEPSEEK_en.md)
9
9
 
10
- `dev-flow-deepseek` 让 DeepSeek Harness(DSH)从本地持久 Task 继续长时编程任务,并在执行中守住
11
- 任务范围、验证预算和交付条件。DSH 继续读取 Workspace、修改文件和运行命令;bundled Go Core
12
- 保存当前阶段,限制验证扩张,让过期记录失效,并在仓库漂移或 Action 结果不确定时给出下一步、
13
- Recovery 判断或明确阻塞。
10
+ `dev-flow-deepseek` 让 DeepSeek Harness(DSH)在独立工作树中运行一个持久 Core Task。普通开发请求
11
+ 先只读评估,不调用 Dev Flow;后续精确确认才授权创建工作树和重启会话。Core 随后通过只读 Git
12
+ 观察计算当前 Task surface,DSH 继续负责用户授权的 fetch、branch、worktree、文件修改和命令执行。
14
13
 
15
14
  ## 支持范围
16
15
 
@@ -68,23 +67,34 @@ dsh --profile $ProfileName --dump-config
68
67
 
69
68
  ## 启动一个 Task
70
69
 
71
- 每个需要调用 Dev Flow 的直接用户消息都要包含由空白边界限定的 selector:
70
+ 先把开发请求正常发给 DSH。Adapter 只读检查候选实现、调用关系、测试、配置和 Git 状态,返回
71
+ `small|standard|large|uncertain`、已经找到的影响面、未知项和建议;这一轮不调用 Core、不修改 Git、
72
+ 不运行测试,也不创建 Task。即使第一条消息已经包含 `/dev-flow`,新请求也不能跳过评估和确认。
73
+
74
+ 选择 Dev Flow 后,逐仓确认 remote、base branch 和新的 target branch。确认消息必须使用 Adapter
75
+ 显示的精确形式,例如:
72
76
 
73
77
  ```text
74
- /dev-flow Add payment-callback signature validation and run targeted tests.
78
+ /dev-flow confirm-worktree
79
+ repository=primary;remote=origin;base=main;target=feature/payment-callback-signature
75
80
  ```
76
81
 
77
- 这不是 shell 命令。历史消息、模型文本、Skill 注入或仓库内容不能替代当前用户消息中的
78
- `/dev-flow`。普通讨论或空调用不会创建 Task。
82
+ WorkspaceCoordinator 随后精确 fetch 选定 remote/base,冻结 commit,并从该 commit 创建干净、独立、
83
+ 具名分支的 worktree。源 checkout 可以 dirty,但 staged、tracked dirty 和 untracked 内容都不会复制。
84
+ fetch、分支校验或 worktree 创建失败时不会创建 Core Task。
79
85
 
80
- Task 保存最初请求、范围、验收条件和 verification budget。可以在创建时选择 `plain`、
81
- `spec-kit` `openspec`,但当前没有 OpenSpec / Spec Kit artifact importer。
86
+ DSH Workspace Root 在进程启动时固定,因此 Adapter 不会扩大当前 Root。它返回由 command、argv 和
87
+ cwd 分开的 relaunch 信息;新会话从隔离 workspace 启动,使用返回的
88
+ `/dev-flow resume-worktree launch=<launch_id>` 消费 receipt,复核 branch、HEAD 和 clean 状态,然后
89
+ 才创建 Task。新 Task 保存最初请求、范围、验收条件和 method profile,不在分析前冻结最终
90
+ verification budget;profile 可以是 `plain`、`spec-kit` 或 `openspec`。
82
91
 
83
92
  ## 恢复已有 Task
84
93
 
85
- 在同一 Workspace Root 下回到参与 Task 的仓库,并在当前直接用户消息中再次使用 `/dev-flow`。
86
- Adapter 会先读取 Core,恢复当前阶段、revision、范围、剩余验证、Blocker 和 Recovery,不会根据聊天
87
- 记录重新创建进度。
94
+ 回到 Task 原来绑定的同一 worktree 实例,并在当前直接用户消息中再次使用 `/dev-flow`。Adapter 会先
95
+ 读取 Core,恢复当前阶段、revision、范围、剩余验证、Blocker 和 Recovery,不会根据聊天记录重新创建
96
+ 进度。原 worktree 丢失或被替换时进入 `WORKSPACE_UNAVAILABLE`;同路径重建目录或同名 branch 不能冒充
97
+ 原实例。此时只能恢复原实例,或明确 abandon Task。
88
98
 
89
99
  如果上一次 Action 响应丢失或被截断,Adapter 先读取当前 Task 和 Recovery 判断,再按 Core 给出的
90
100
  结果继续、恢复、阻塞或安全重试。它不会自行重复原提交。
@@ -93,6 +103,24 @@ Adapter 会先读取 Core,恢复当前阶段、revision、范围、剩余验
93
103
  结果并暂停 Task。Adapter 不会自动解除;用户明确选择换方案或再试一次后,才解除 blocker,并从
94
104
  Core 保存的原目标阶段继续。下一次仍然完全重复时会再次暂停。
95
105
 
106
+ ## 验证投入和修改后复核
107
+
108
+ Adapter 在 TASKS 完成需求、设计、工作拆分、影响面和现有测试结构分析后,才保存初始
109
+ `verification_plan`,包括计划检查及理由、预计自动命令数、完整套件预期和测试代码预期。小改动先用
110
+ 离当前 diff 最近的定向检查;剩余额度不能作为扩大到 package、module 或全仓库的理由。
111
+
112
+ 额度不足时不直接结束 Task,也不先运行额外命令。Adapter 使用当前 TEST Action 的
113
+ `verification_budget_increased`,用 `new_impact`、`new_risk`、`verification_failure` 或
114
+ `verification_gap` 说明具体事实,只增加当前需要的检查、命令或权限。Core 保存原因和调整前后预算并
115
+ 留在 TEST。为了更全面、提高信心、保险起见或“还有预算”都不能支持增加。
116
+
117
+ 每次完整套件前都重新判断广泛影响、定向/包级检查是否足够、待补的具体风险和仓库当前检查点要求,
118
+ 本次理由写入 `full_suite_reason`,小修复后不自动沿用旧理由。测试代码只为稳定行为、公共合同、重要
119
+ 失败路径或真实回归保留;一次性 README 词语要求只做一次文本搜索。
120
+
121
+ 普通实现后的复核只看当前 diff、直接或间接影响和验收所需路径。修复复核发现后只检查原问题、相关
122
+ 回归和对应定向检查,不重启全仓库审计。显式 code review 阶段只读,交付完整发现后等待单独修复授权。
123
+
96
124
  ## 范围外文件先询问
97
125
 
98
126
  Adapter 在 DSH `tools/pre-execute` 中检查 `write`、`edit` 和变更型 `str_replace_editor`。当前 direct
@@ -102,7 +130,7 @@ Repository Scope、位于 Workspace Root 且文件属于计划范围时,不因
102
130
 
103
131
  计划外文件会在工具执行前暂停 Task。用户选择:`allow_once` 只允许相同写入意图,`expand_scope`
104
132
  返回 `TASKS` 更新计划,`reject` 在当前 Task Plan revision 内继续拒绝该路径。选择与原因由 Core
105
- 保存;进入测试和 `DONE` 前,Core 还会核对本 Task 累计修改路径。
133
+ 保存;Core 根据 frozen base、commit、index、worktree 和 untracked 内容重新计算当前 Task surface。
106
134
 
107
135
  该 gate 不解析 Bash、外部进程或其他工具路径;这些写入可能只能在 Core 最终检查时发现。gate
108
136
  不可用时,支持的结构化写入保守停止。
@@ -142,16 +170,32 @@ dsh --profile "$PROFILE" --dump-config
142
170
  ## DeepSeek 权限与边界
143
171
 
144
172
  - DSH 启动时的 canonical Workspace Root 是权限边界;仓库和 symlink 解析结果必须位于其中;
145
- - Dev Flow 不扩大 Workspace Root,也不会通过索引发现并加入相邻仓库;
146
- - Core 只读观察 Git,不执行 commit、push、merge、rebase、tag 或 publish
147
- - DeepSeek 负责文件修改和命令执行;Host gate 检查列出的结构化工具,Core 最终检查累计路径,但不会拦截每一次操作;
173
+ - Dev Flow 不扩大 Workspace Root,也不会通过索引发现并加入相邻仓库;需要 sibling worktree 时重启到
174
+ Coordinator 返回的新 Root
175
+ - Core 只读观察 Git,不执行 fetch、branch、worktree、commit、push、merge、rebase、tag 或 publish;
176
+ - DeepSeek 负责用户确认后的 fetch、branch、worktree、文件修改和命令执行;Host gate 检查列出的
177
+ 结构化工具,Core 计算当前 Task surface,但不会拦截每一次操作;
148
178
  - `/dev-flow` 不绕过当前 Action、Workspace 权限、Git 写入授权或发布确认。
149
179
 
180
+ `DONE` 和 `CANCELLED` 只结束 Core Task并释放 claim,不会 commit、push、创建 PR、handoff 或删除
181
+ worktree/branch。终态会显示 remote/base/frozen commit、task branch/HEAD、路径、clean 状态、当前改动和
182
+ 验证结果。worktree 删除与 branch 删除需要两次独立授权;active、dirty、未推送或状态不确定的资源不
183
+ 自动清理。
184
+
185
+ DeepSeek 的清理仍由 WorkspaceCoordinator 执行。它先在不删除资源的 `prepare_cleanup` 操作中核对
186
+ receipt、终态 Task 和同 Git group 的源 checkout,并返回从该源 checkout 重启 DSH 的 descriptor,避免
187
+ 原地删除当前 Workspace Root。重启后,第一次独立确认只删除 receipt 拥有、Core 已终态、clean 且
188
+ terminal HEAD 已精确推送的 worktree,并保留 task branch。第二次独立确认使用非 force 的
189
+ `git branch -d` 删除 branch;未合并、仍被 worktree 使用、HEAD/remote 不一致或 Core 读取失败都会
190
+ 保留资源。源 checkout 路径只在调用中使用,不写入 receipt。
191
+
150
192
  ## 高级多仓库
151
193
 
152
- 当前源码支持一个主仓库和最多七个显式附加仓库。Workspace Root 可以是多个 Git 仓库的非 Git
153
- 共同父目录,但每个仓库及 symlink 解析结果都必须位于 Root 内。Scope 创建后不可变,系统不会自动
154
- 扫描父目录、相邻目录、依赖或索引结果来扩大范围。
194
+ 当前源码支持一个主仓库和最多七个显式附加仓库。每个仓库都要分别确认 remote、base branch 和唯一
195
+ target branch;只有全部仓库完成 fetch、独立 worktree 创建和验证后,才一次创建一个 Core Task。
196
+ 新 DSH 会话使用 Coordinator 返回的非 Git 共同 Workspace Root,所有 worktree 和 symlink 解析结果
197
+ 都必须位于该 Root 内。任一仓库失败时不能只使用部分 Scope、退回共享 checkout 或留下部分 Core
198
+ claim。Scope 创建后不可变,系统不会扫描父目录、相邻目录、依赖或索引结果扩大范围。
155
199
 
156
200
  使用前请阅读[项目状态](../../docs/PROJECT-STATUS.md)确认多仓库属于稳定还是源码范围。精确
157
201
  Repository Scope、路径格式和协议规则见[架构](../../docs/ARCHITECTURE.md)与
@@ -48,13 +48,27 @@ export function deriveCurrentTurn(execution) {
48
48
  }
49
49
  }
50
50
 
51
+ export function currentDirectUserText(execution) {
52
+ const turn = deriveCurrentTurn(execution);
53
+ if (turn === undefined) return "";
54
+ const ids = new Set(turn.directUserMessageIds);
55
+ return execution.agent.session.events
56
+ .filter((event) => event?.type === "user/message"
57
+ && ids.has(event.data?.id)
58
+ && event.data?.source?.kind === "user")
59
+ .flatMap((event) => event.data.content ?? [])
60
+ .filter((block) => block?.type === "text" && typeof block.text === "string")
61
+ .map((block) => block.text)
62
+ .join("\n");
63
+ }
64
+
51
65
  export function authorizeDevFlowExecution(execution, {
52
66
  workspaceRoot = process.cwd(),
53
67
  realpathImpl = realpathSync,
54
68
  } = {}) {
55
69
  if (!isDevFlowNamespaceTool(execution?.name)) return undefined;
56
70
  if (!isExpectedDevFlowTool(execution.name)) {
57
- return `${DENIAL_CODES.UNEXPECTED_TOOL}: the Dev Flow namespace permits only the six contracted tools.`;
71
+ return `${DENIAL_CODES.UNEXPECTED_TOOL}: the Dev Flow namespace permits only the contracted Core tools.`;
58
72
  }
59
73
  if (execution.agent === undefined) {
60
74
  return `${DENIAL_CODES.NO_AGENT}: ${selectorInstruction}.`;
package/lib/index.mjs CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  resolveDataDirectory,
11
11
  } from "./paths.mjs";
12
12
  import { preflightPackagedCore, selectPackagedRuntime } from "./runtime.mjs";
13
+ import { registerWorkspaceCoordinator } from "./workspace-tool.mjs";
13
14
  import {
14
15
  DEV_FLOW_SERVER_NAME,
15
16
  assertQualifiedToolCatalog,
@@ -18,8 +19,8 @@ import {
18
19
  export const name = "dev-flow-deepseek";
19
20
  export const inject = ["skills", "tools"];
20
21
 
21
- export async function apply(ctx) {
22
- await activateDeepSeekIntegration(ctx);
22
+ export async function apply(ctx, options = {}) {
23
+ await activateDeepSeekIntegration(ctx, options);
23
24
  }
24
25
 
25
26
  export async function activateDeepSeekIntegration(ctx, {
@@ -46,10 +47,10 @@ export async function activateDeepSeekIntegration(ctx, {
46
47
 
47
48
  ctx.skills.register(Object.freeze({
48
49
  name: "dev-flow",
49
- description: "Use the Dev Flow graph Core for the current explicit development request.",
50
- whenToUse: "Use only for a current direct user turn containing /dev-flow.",
50
+ description: "Assess bounded development requests, then use Dev Flow only after the developer confirms an isolated worktree launch.",
51
+ whenToUse: "Use for new development-request suitability assessment, explicit worktree confirmation, or an explicit Dev Flow Task resume.",
51
52
  invocation: Object.freeze({
52
- modelInvocable: false,
53
+ modelInvocable: true,
53
54
  userInvocable: true,
54
55
  }),
55
56
  source: "bundled",
@@ -64,6 +65,10 @@ export async function activateDeepSeekIntegration(ctx, {
64
65
  dataDirectory: dataSelection.dataDirectory,
65
66
  workspaceRoot,
66
67
  });
68
+ registerWorkspaceCoordinator(ctx, {
69
+ dataDirectory: dataSelection.dataDirectory,
70
+ workspaceRoot,
71
+ });
67
72
 
68
73
  let mcpFiber;
69
74
  let catalogCheckQueued = false;
@@ -0,0 +1,163 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, lstat, mkdir, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
3
+ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+
5
+ const MAX_RECEIPT_BYTES = 256 * 1024;
6
+ const receiptStatuses = new Set(["confirmed", "fetching", "fetched", "provisioning", "provisioned", "consumed", "cleaned", "failed", "uncertain"]);
7
+ const repositoryStatuses = new Set(["confirmed", "fetching", "fetched", "provisioning", "provisioned", "consumed", "worktree_removed", "branch_removed", "failed", "uncertain"]);
8
+
9
+ // One launch envelope groups the per-repository provisioning facts needed for
10
+ // all-or-nothing Core admission. It is an operation receipt, not a Task cursor.
11
+
12
+ export function provisioningReceiptPath(dataDirectory, launchID) {
13
+ assertLaunchID(launchID);
14
+ const root = provisioningReceiptDirectory(dataDirectory);
15
+ return containedPath(root, join(root, `${launchID}.json`), "provisioning receipt");
16
+ }
17
+
18
+ export async function writeProvisioningReceipt(dataDirectory, receipt) {
19
+ const validated = validateProvisioningReceipt(receipt);
20
+ const root = provisioningReceiptDirectory(dataDirectory);
21
+ await rejectSymlinkComponents(dataDirectory, root);
22
+ await mkdir(root, { recursive: true, mode: 0o700 });
23
+ await rejectSymlinkComponents(dataDirectory, root);
24
+ const target = provisioningReceiptPath(dataDirectory, validated.launch_id);
25
+ const temporary = join(root, `.${basename(target)}.${process.pid}.${randomUUID()}.tmp`);
26
+ try {
27
+ await writeFile(temporary, `${JSON.stringify(validated, null, 2)}\n`, { mode: 0o600, flag: "wx" });
28
+ await rename(temporary, target);
29
+ if (process.platform !== "win32") await chmod(target, 0o600);
30
+ } catch (error) {
31
+ await unlink(temporary).catch(() => {});
32
+ throw error;
33
+ }
34
+ return validated;
35
+ }
36
+
37
+ export async function readProvisioningReceipt(dataDirectory, launchID) {
38
+ const target = provisioningReceiptPath(dataDirectory, launchID);
39
+ let info;
40
+ try {
41
+ info = await lstat(target);
42
+ } catch (error) {
43
+ if (error?.code === "ENOENT") return null;
44
+ throw error;
45
+ }
46
+ if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_RECEIPT_BYTES) {
47
+ throw new Error("provisioning receipt must be a bounded regular file");
48
+ }
49
+ if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
50
+ throw new Error("provisioning receipt permissions are too broad");
51
+ }
52
+ const canonical = await realpath(target);
53
+ if (canonical !== resolve(target)) throw new Error("provisioning receipt must be canonical");
54
+ let value;
55
+ try {
56
+ value = JSON.parse(await readFile(target, "utf8"));
57
+ } catch (error) {
58
+ throw new Error("provisioning receipt is invalid", { cause: error });
59
+ }
60
+ return validateProvisioningReceipt(value);
61
+ }
62
+
63
+ export function validateProvisioningReceipt(value) {
64
+ assertExactKeys(value, [
65
+ "launch_id", "host", "request_digest", "profile", "workspace_root",
66
+ "operation_status", "repositories", "created_at", "updated_at",
67
+ ], "provisioning receipt");
68
+ assertLaunchID(value.launch_id);
69
+ if (value.host !== "deepseek") throw new Error("provisioning receipt host is invalid");
70
+ assertDigest(value.request_digest, "request digest");
71
+ if (typeof value.profile !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(value.profile)) {
72
+ throw new Error("provisioning receipt profile is invalid");
73
+ }
74
+ if (typeof value.workspace_root !== "string" || !isAbsolute(value.workspace_root)) {
75
+ throw new Error("provisioning receipt workspace root is invalid");
76
+ }
77
+ if (!receiptStatuses.has(value.operation_status)) throw new Error("provisioning receipt status is invalid");
78
+ if (!Array.isArray(value.repositories) || value.repositories.length < 1 || value.repositories.length > 8) {
79
+ throw new Error("provisioning receipt repository count is invalid");
80
+ }
81
+ const keys = new Set();
82
+ const paths = new Set();
83
+ for (const repository of value.repositories) {
84
+ validateRepositoryReceipt(repository);
85
+ if (keys.has(repository.repository_key) || paths.has(repository.worktree_path)) {
86
+ throw new Error("provisioning receipt repositories must be unique");
87
+ }
88
+ keys.add(repository.repository_key);
89
+ paths.add(repository.worktree_path);
90
+ }
91
+ if (!Number.isFinite(Date.parse(value.created_at)) || !Number.isFinite(Date.parse(value.updated_at))) {
92
+ throw new Error("provisioning receipt timestamps are invalid");
93
+ }
94
+ return structuredClone(value);
95
+ }
96
+
97
+ function validateRepositoryReceipt(value) {
98
+ assertExactKeys(value, [
99
+ "source_repository_identity", "repository_key", "remote_name",
100
+ "base_branch", "target_branch", "fetched_commit", "worktree_path", "operation_status", "created_at",
101
+ ], "provisioning repository receipt");
102
+ assertDigest(value.source_repository_identity, "source repository identity");
103
+ if (typeof value.repository_key !== "string" || !/^[a-z0-9][a-z0-9._-]{0,127}$/u.test(value.repository_key)) {
104
+ throw new Error("provisioning repository key is invalid");
105
+ }
106
+ for (const field of ["worktree_path"]) {
107
+ if (typeof value[field] !== "string" || !isAbsolute(value[field])) throw new Error(`provisioning ${field} is invalid`);
108
+ }
109
+ for (const field of ["remote_name", "base_branch", "target_branch"]) {
110
+ if (typeof value[field] !== "string" || value[field] === "" || value[field].length > 255 || /[\0\r\n;=]/u.test(value[field])) {
111
+ throw new Error(`provisioning ${field} is invalid`);
112
+ }
113
+ }
114
+ if (value.fetched_commit !== null && (typeof value.fetched_commit !== "string" || !/^[0-9a-f]{40,64}$/u.test(value.fetched_commit))) {
115
+ throw new Error("provisioning fetched commit is invalid");
116
+ }
117
+ if (!repositoryStatuses.has(value.operation_status)) throw new Error("provisioning repository status is invalid");
118
+ if (!Number.isFinite(Date.parse(value.created_at))) throw new Error("provisioning repository timestamp is invalid");
119
+ }
120
+
121
+ function provisioningReceiptDirectory(dataDirectory) {
122
+ const root = resolve(dataDirectory);
123
+ return containedPath(root, join(root, "host-operations", "deepseek", "provisioning"), "provisioning receipt directory");
124
+ }
125
+
126
+ function assertLaunchID(value) {
127
+ if (typeof value !== "string" || !/^[0-9a-f-]{36}$/u.test(value)) throw new Error("provisioning launch identity is invalid");
128
+ }
129
+
130
+ function assertDigest(value, label) {
131
+ if (typeof value !== "string" || !/^[0-9a-f]{64}$/u.test(value)) throw new Error(`${label} is invalid`);
132
+ }
133
+
134
+ function assertExactKeys(value, expected, label) {
135
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
136
+ if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...expected].sort())) throw new Error(`${label} fields are invalid`);
137
+ }
138
+
139
+ function containedPath(root, candidate, label) {
140
+ const canonicalRoot = resolve(root);
141
+ const canonicalCandidate = resolve(candidate);
142
+ const offset = relative(canonicalRoot, canonicalCandidate);
143
+ if (offset === ".." || offset.startsWith(`..${sep}`) || isAbsolute(offset)) {
144
+ throw new Error(`${label} escapes its owned root`);
145
+ }
146
+ return canonicalCandidate;
147
+ }
148
+
149
+ async function rejectSymlinkComponents(root, candidate) {
150
+ const canonicalRoot = resolve(root);
151
+ const target = containedPath(canonicalRoot, candidate, "receipt path");
152
+ const parts = relative(canonicalRoot, target).split(sep).filter(Boolean);
153
+ let current = canonicalRoot;
154
+ for (const part of parts) {
155
+ current = join(current, part);
156
+ try {
157
+ if ((await lstat(current)).isSymbolicLink()) throw new Error(`receipt path contains a symbolic link: ${current}`);
158
+ } catch (error) {
159
+ if (error?.code === "ENOENT") return;
160
+ throw error;
161
+ }
162
+ }
163
+ }
@@ -14,9 +14,11 @@ export const DEV_FLOW_RAW_TOOL_NAMES = Object.freeze([
14
14
  "dev_flow_submit_comprehension",
15
15
  "dev_flow_submit_refactor",
16
16
  "dev_flow_submit_delivery",
17
+ "dev_flow_prepare_task_relocation",
17
18
  "dev_flow_resolve_blocker",
18
19
  "dev_flow_recover_action",
19
20
  "dev_flow_cancel_task",
21
+ "dev_flow_abandon_task",
20
22
  ]);
21
23
 
22
24
  export const DEV_FLOW_QUALIFIED_TOOL_NAMES = Object.freeze(