@metagptx/deepflow-suite-cli 0.3.1 → 0.3.3

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.
@@ -2,13 +2,13 @@
2
2
  "schemaVersion": 1,
3
3
  "distribution": "deepflow-suite",
4
4
  "package": "@metagptx/deepflow-suite-cli",
5
- "version": "0.3.1",
5
+ "version": "0.3.3",
6
6
  "platform": "any",
7
7
  "arch": "any",
8
8
  "cli": {
9
9
  "name": "deepflow-suite",
10
10
  "package": "@metagptx/deepflow-suite-cli",
11
- "version": "0.3.1",
11
+ "version": "0.3.3",
12
12
  "command": "deepflow-suite"
13
13
  },
14
14
  "skills": [
@@ -80,8 +80,8 @@
80
80
  },
81
81
  {
82
82
  "path": "references/fact-types.md",
83
- "size": 3974,
84
- "sha256": "5dd10c73e652d018ce20617ec8a5e1becb55fb935f15a366831050063a8989fc"
83
+ "size": 6174,
84
+ "sha256": "efab99cb46ad24100e834efd3e7ed4ab5e5004bb4b32320cff08ac14d5fa0070"
85
85
  },
86
86
  {
87
87
  "path": "references/query-scope.md",
@@ -95,8 +95,8 @@
95
95
  },
96
96
  {
97
97
  "path": "SKILL.md",
98
- "size": 13393,
99
- "sha256": "cd1ffc54bf884f02e540ddd4b75c805ed868e32d298d20abcecb3c8af51feb8b"
98
+ "size": 14374,
99
+ "sha256": "357d805b8260d0e67a46a7edaef4f443c7be58dec075bfc2f176a83e88ebb5d1"
100
100
  }
101
101
  ]
102
102
  },
@@ -173,8 +173,8 @@
173
173
  },
174
174
  {
175
175
  "path": "SKILL.md",
176
- "size": 11934,
177
- "sha256": "8710e7ec3da45b8363006bc09befa79b07e8edde4f8f46d98841b8761d1ea228"
176
+ "size": 12201,
177
+ "sha256": "6af6181c7094ca4217578c8d13938ecfd3df006dacdef6446c30b00fcd8ddfec"
178
178
  }
179
179
  ]
180
180
  },
@@ -184,8 +184,8 @@
184
184
  "files": [
185
185
  {
186
186
  "path": "SKILL.md",
187
- "size": 3492,
188
- "sha256": "f04a22e7967727217003071b7ad7c9268fde9058af4e1f8d3de22e28e18ac718"
187
+ "size": 3962,
188
+ "sha256": "1b8d4b372f04a78430b96c4a39661a83903c1aefc777a9da10600277497ec205"
189
189
  }
190
190
  ]
191
191
  }
@@ -12,22 +12,20 @@ CLI 由 `@metagptx/deepflow-suite-cli` 统一安装,本 Skill 不携带 CLI
12
12
  你运行在 **DeepFlow Agent Runtime** 上。这是一个把「会话 + 多轮消息 + 代码仓库」
13
13
  管起来的服务,你现在执行的是其中一轮。
14
14
 
15
- ## 先读这条:任务简报里的命令清单大半是假的
15
+ ## 执行环境与工具范围
16
16
 
17
- 执行引擎是 multica 的 daemon,它拼简报时**默认自己是完整的 multica 产品**——
18
- issue、chat channel、squad、附件、workspace、project,简报里会提到这些。
19
- **这里一个都没有。**
20
-
21
- 简报里那份 `multica` 命令手册,只有一条在本环境真实可用:
17
+ 执行引擎的自动简报按完整 multica 产品生成,其中的 issue、chat channel、squad、
18
+ 附件、workspace、project 等操作不适用于本 Runtime。当前 `multica` shim 只支持:
22
19
 
23
20
  ```bash
24
21
  multica repo checkout <URL>
25
22
  ```
26
23
 
27
- 其余的 `multica` 子命令敲下去都会被明确拒绝,stderr 上会告诉你这里没有这个东西、
28
- 以及改做什么。那些拒绝信息是给你看的,照着做就行,**不要换个写法重试**。
24
+ 这限定的是 `multica` 子命令,不限制普通 Git、shell 或其他已提供的工具。
25
+ 简报中的“先用 checkout 获取代码”不是唯一入口要求;已授权仓库可通过普通 Git
26
+ 取得或复用工作副本,也可使用托管检出,不设固定的工具调用顺序。
29
27
 
30
- 你自己的能力在 `df-agentrt` 上。Runtime 镜像通过标准 PATH 提供 `df-task`、`df-agentctrl`、`df-agentrt` 和 `multica` shim,普通 shell 与登录 shell 均应可直接调用。命令缺失或 `--help` 失败属于执行环境异常:报告并结束,不要在 Run 中安装、编译 CLI 或猜绝对路径反复尝试。
28
+ Runtime 镜像通过标准 PATH 提供 `df-task`、`df-agentctrl`、`df-agentrt` 和 `multica` shim,普通 shell 与登录 shell 均应可直接调用。命令缺失或 `--help` 失败表示对应能力存在执行环境缺口;不在 Run 中安装、编译 CLI 或猜绝对路径反复尝试。保留具体失败与可用能力,是否阻塞当前目标由执行者结合执行定义和任务判断,不能把某个可替代工具不可用直接当作整个任务无法继续。
31
29
 
32
30
  Skill 内容通过 provider 的原生目录交付,以当前会话的 Skill 目录清单为准。清单中的 `r0` 等是根目录别名,打开前先展开,不能拼成 `skills/r0/...`。Codex 的 Runtime Skills 在 `$CODEX_HOME/skills/<slug>/SKILL.md`;Claude 在任务工作目录的 `.claude/skills/<slug>/SKILL.md`。支持文件相对该 Skill 目录读取。`$DEEPFLOW_WORKDIR` 表示任务工作目录,不是通用 Skill 根目录。
33
31
 
@@ -64,11 +62,11 @@ Run 中 `df-agentrt` 只使用部署注入的 `MULTICA_SERVER_URL`,忽略 `AGE
64
62
 
65
63
  文件仍依赖所属 Worker 工作目录,不是归档或永久可用承诺;未发布或仍在生成的文件不能宣称已交付。
66
64
 
67
- ## 工作目录:跨轮复用,检出是同级目录
65
+ ## 工作目录与工作副本
68
66
 
69
67
  - 同一个会话的多轮**复用同一个工作目录**,上一轮留下的文件这一轮还在。
70
- - `repo add` / `repo checkout` 检出的仓库,落在**你当前工作目录的同级**,不在里面。
71
- - 目录名取自 URL 的最后一段,**不一定等于应用名**。不要猜,用命令打印出来的绝对路径。
68
+ - 托管 `repo add` / `repo checkout` 检出的仓库位于任务工作目录的同级;用命令返回的绝对路径访问,不猜目录名。这不是所有 Git 工作副本的目录规则。
69
+ - 普通 Git 获取的工作副本放在本 Session 拥有的工作区内;保留已有改动、提交和分支,不把 daemon 缓存或其他 Session 的目录当作自己的工作副本。需要通过文件通道交付的普通文件应位于可读取的任务工作目录内,代码交付使用远端固定引用。
72
70
 
73
71
  ## release 之后工作目录的生命周期
74
72
 
@@ -160,10 +158,8 @@ release 会停止会话接收新轮次并改变生命周期状态(当前不会
160
158
  | 0 | 成功 | —— |
161
159
  | 1 | 请求发出去了,但失败了 | 看 stderr,它写清了原因 |
162
160
  | 2 | 命令行写错了,一个请求都没发 | 改命令 |
163
- | 3 | 环境/部署不对 | **不是你的错,重试没用**,在回复里说明 |
161
+ | 3 | 对应命令所需环境/部署不满足 | 报告具体缺口;不靠改参数反复执行同一命令,不据此推断其他授权方式也不可用 |
164
162
 
165
- ## 添加代码仓库
163
+ ## 仓库发现与托管检出
166
164
 
167
- `repo list` / `search` / `add` 的详细用法、`--ref` 的限制、以及三个容易做错的地方,
168
- 见 `df-agentrt-repo` skill。一句话版本:**不要自己拼 `multica repo checkout` 去检出刚加的
169
- 仓库**,daemon 的白名单按 URL 逐字符匹配,你手敲的那个字符串未必是服务端存下来的那个。
165
+ 需要按应用名发现仓库、查询或追加 Session 仓库配置、使用托管检出时,见 `df-agentrt-repo`。该能力不是普通 Git 操作的前置条件。
@@ -1,87 +1,60 @@
1
1
  ---
2
2
  name: df-agentrt-repo
3
- description: 任务进行中发现需要另一个代码仓库时使用。查看本会话已有的仓库、按应用名搜索仓库、把新仓库加进来并当场检出。
3
+ description: 在 Runtime 会话中查询已配置仓库、按应用名发现仓库,或追加仓库配置并托管检出时使用。
4
4
  ---
5
5
 
6
- # 中途添加代码仓库
6
+ # Runtime 仓库发现与托管检出
7
7
 
8
- ## CLI 依赖
9
-
10
- CLI 由 `@metagptx/deepflow-suite-cli` 统一安装,本 Skill 不携带 CLI 实现。使用前应已完成 Suite 安装;缺少命令或版本不匹配时,用 `deepflow-suite doctor --json` 核对。Run 内缺少工具属于执行环境异常,不临时安装或编译。
11
-
12
- 会话创建时定下的配置里,只有**代码仓库**这一项是可以在任务进行中追加的。
13
-
14
- 你可能在分析到一半时才发现答案不在手上这几个仓库里——这时不需要中断任务回报、
15
- 等人重建会话,用 `df-agentrt` 自己把仓库加进来,一个回合内就能继续。
16
-
17
- `df-agentrt` 已经在 PATH 上,不需要定位路径。
8
+ 本 Skill 说明 Runtime 的仓库登记与托管检出能力。已知授权仓库及所需 ref 时,也可通过普通 Git 获取或复用工作副本,不要求先查询、登记或托管检出。按当前任务需要选择能力,不设固定调用顺序。
18
9
 
19
- ## 已经有哪些仓库
10
+ 仓库列表、应用搜索结果和登记成功都不增加任务授权;只操作任务范围内的相关仓库。工作目录与复用边界见 `df-agentrt`,Git 认证、提交和 MR 操作见已加载的 Git 能力。
20
11
 
21
- ```bash
22
- df-agentrt repo list
23
- ```
24
-
25
- 每行一个仓库:URL、`@分支`(如果固定了分支)、括号里是说明。
26
- **先跑这个**——需要的仓库很可能已经在列表里,那就只差一个检出:
27
-
28
- ```bash
29
- multica repo checkout <上面列出的 URL>
30
- ```
12
+ ## CLI 依赖
31
13
 
32
- ## 找仓库
14
+ CLI 由 `@metagptx/deepflow-suite-cli` 统一安装,本 Skill 不携带 CLI 实现。缺少命令或版本不匹配时,用 `deepflow-suite doctor --json` 核对。Run 内不临时安装或编译 CLI。
33
15
 
34
- 知道应用叫什么、不知道代码在哪:
16
+ ## 按需查询与发现
35
17
 
36
- ```bash
37
- df-agentrt repo search 结算服务
38
- ```
18
+ `df-agentrt repo list` 查询本 Session 已配置的仓库。每行包含 URL、`@ref`(若已指定)和说明;列表不证明工作副本已存在或远端已刷新。
39
19
 
40
- 输出三列,用 Tab 分隔:**应用代码**、应用名、仓库地址。第一列是下一步要用的参数。
20
+ 知道应用名、不知道仓库地址时,可用 `df-agentrt repo search <应用名>`。输出为应用代码、应用名和仓库地址,以 Tab 分隔。搜索无结果只表示此次未匹配到应用;可调整关键词,已有准确地址时无需搜索。
41
21
 
42
- 搜不到不是失败。换一个更短的词再试,或者你要是本来就知道 clone 地址,跳过这步。
22
+ ## 登记与托管检出
43
23
 
44
- ## 加进来
24
+ 向本 Session 追加仓库并检出:
45
25
 
46
26
  ```bash
47
27
  df-agentrt repo add <应用代码或 clone 地址> [...]
48
28
  ```
49
29
 
50
- 参数里带 `://` 或 `@` 的当作 clone 地址,其余当作应用代码去查。两种可以混在一条命令里。
51
-
52
- 要固定分支:
30
+ 参数含 `://` 或 `@` 时按 clone 地址处理,其余按应用代码查询。`--ref` 仅支持单个 clone 地址,可指定任务所需的分支、tag 或提交:
53
31
 
54
32
  ```bash
55
33
  df-agentrt repo add https://git.example.com/g/settle --ref release/2026-09
56
34
  ```
57
35
 
58
- `--ref` 一次只能配一个 clone 地址。多个仓库共用一个分支名几乎都是写错了,
59
- 真要这么做就分成几条命令。
36
+ 对已配置的仓库单独执行托管检出时,使用配置中的 URL,并保留所需 ref:
60
37
 
61
- **这条命令会把仓库检出,不需要再跑别的。** stdout 上每行是一个检出目录的绝对路径,
62
- 直接拿去读文件。
63
-
64
- ## 三件容易做错的事
65
-
66
- **不要自己拼 `multica repo checkout` 去检出刚加的仓库。** daemon 的白名单按 URL
67
- **逐字符**匹配,你手敲的地址少一个 `.git`、多一个尾斜杠,就是另一个仓库,
68
- 会得到「repo is not configured」——而 `repo list` 里明明有它。`repo add` 用的是服务端存下来的
69
- 那个字符串,所以它检出得了,你重新敲一遍的不一定。
38
+ ```bash
39
+ multica repo checkout <已配置的URL> [--ref <所需ref>]
40
+ ```
70
41
 
71
- **仓库只能加不能删。** 当前回合可能正在某个仓库里工作,删掉就把这个回合弄坏了。
42
+ 成功时使用工具返回的工作副本绝对路径。缓存命中或检出成功都不能证明最新远端 ref 已刷新,仍需核对所需版本与实际起始 SHA。
72
43
 
73
- **重复添加是安全的。** 已经有了就不会重复加,但仍然会做一次检出——
74
- 「在列表里」和「在磁盘上」是两回事。
44
+ ### 操作语义
75
45
 
76
- ## 命令失败了怎么办
46
+ - `repo add` 包含登记和检出两个动作。登记已成功但检出失败时,配置仍然保留,不能把整体失败当作登记回滚。
47
+ - 重复添加不重复登记,但仍会执行检出,不是无副作用的查询或重试。已有工作副本时先核对实际目录与改动,不为切换传输方式重新检出。
48
+ - 托管检出按已保存的 URL 精确匹配。使用 `repo list` 或登记结果中的原始 URL;手工改变 `.git`、尾斜杠或 SSH/HTTPS 可能不匹配。`repo add` 已完成检出时,无需再调用 checkout。
49
+ - Session 仓库配置只支持追加,不提供移除接口;这描述的是配置能力,不是所有本地文件操作的规则。
77
50
 
78
- stderr 上的话是给你看的,照着做:
51
+ ## 失败含义
79
52
 
80
- - 说「retrying will not help」——环境问题,不是参数问题,别换个写法再试,直接在回复里说明;
81
- - 说「remain configured」——仓库加上了,只是检出没成功,可以用 `multica repo checkout <URL>` 重试;
82
- - 其余的错误信息来自服务端,已经写清楚了原因和下一步。
53
+ 根据失败发生的位置解释结果,不把某条命令失败直接当作仓库或整个任务不可用:
83
54
 
84
- ## 加完之后
55
+ - `retrying will not help` 表示该命令缺少所需执行环境;换参数不会修复该缺口,不代表普通 Git 或其他已有能力也不可用。
56
+ - `remain configured` 表示登记保留、检出未成功;按实际错误判断是否适合重试,不能无条件再次检出已有工作副本。
57
+ - Runtime 仓库配置刷新或查询超时,不证明 GitLab 不可达、仓库不存在、凭据缺失或缓存未命中。已知授权仓库和所需 ref 时,普通 Git 不依赖这条托管检出链路。
58
+ - SSH 主机校验或公钥认证失败,表示对应 SSH 连接未成功;Token 与 SSH 私钥是不同凭据,传输方式和凭据绑定按 Git 能力说明处理。
85
59
 
86
- 检出目录和你当前的工作目录是同级的。用 `repo add` 打印出来的绝对路径访问,
87
- 不要猜目录名——仓库目录名来自 URL 最后一段,不一定等于应用名。
60
+ 保留具体失败操作、已完成动作、实际目录及未确认项。是否采用其他授权方法、续办或请求人工协助,由执行者结合任务与执行定义判断;不通过扩大仓库范围、切换账号或访问其他 Session 的工作副本绕过限制。
@@ -31,6 +31,12 @@ metadata 的 `source`、`query_keys` 用于可选软范围过滤,`data` 由业
31
31
 
32
32
  托管 Run 中 `df-task` 由 Runtime 镜像提供。命令缺失或 `--help` 无法执行时,报告执行环境异常并结束;不要在 Run 中安装、编译 CLI 或猜测绝对路径来继续。非托管的本地开发可按项目 README 安装 CLI。**注意不要把 runtime 的地址填成 Task 服务的地址** —— `df-agentrt` 和 `df-task` 是两个服务、两个端口、两套授权。
33
33
 
34
+ ## 创建来源与人工参与请求
35
+
36
+ 涉及人的协作时,从 `get` 的不可变 `creator.kind/id` 核对创建来源;owner、当前成员、经办人及正文不能代替。读取来源未知时保持未知。是否选人、何时求助、授予什么责任,由当前 Agent 定义和本次授权判断。
37
+
38
+ 普通 `handoff --ask` 只写公开交接,不请求成员加入。需要实际人工参与时,读取[人工参与请求与回执](references/fact-types.md#请求实际人工参与),使用 `handoff --participation ensure_member --assignee <真实账号>`;明确授予该人主经办责任时才附加 `--task-assignee`。请求写入成功不等于成员、通知或分配已生效;按请求事实序号核对平台回执,部分成功、失败与未知分别保留。旧 CLI 缺少这些参数时,只有在已加载协议且平台支持的前提下,才可用通用 `append` 提交同一载荷,不能猜字段或把公开答复当作实际邀请。
39
+
34
40
  ## 读日志
35
41
 
36
42
  先确定本轮要作出的判断,再按需选择事实;不要求每轮先通读全部日志。当前描述与版本用上文的 `get` 读取,事实用于补足决策依据。
@@ -6,8 +6,8 @@
6
6
 
7
7
  | 类型 | 谁写 | 必填 | 可选 |
8
8
  | --- | --- | --- | --- |
9
- | `user_message` | 人 | `text` | `wake_policy`、`agent_ids` |
10
- | `external_result` | 外部系统 | `source`、`ref`、`result` | —— |
9
+ | `user_message` | 人或 bot | `text` | `wake_policy`、`agent_ids` |
10
+ | `external_result` | 外部系统或 bot | `source`、`ref`、`result` | —— |
11
11
 
12
12
  `wake_policy` 由产品端写入,缺失时按 `default` 处理。`passive` 表示多人对话里人和人之间的讨论,它**不触发**调度,被别的事件唤醒时也只能当作上下文,不能单独据此派活。`agent_ids` 只是人提到的 Agent,作为提示;派给谁仍由调度根据全局事实决定。
13
13
 
@@ -32,7 +32,7 @@
32
32
  | --- | --- | --- | --- |
33
33
  | `dispatch_intent` | `agent`、`instruction`、`mode`;`mode=message` 时还必须有 `target_run_id` | `agent_version`(Driver 读取的正整数版本) | `new_run` 必须省略 `target_run_id`,由 Driver 根据 `task_id + intent_seq` 派生逻辑 Run;`message` 指向已有逻辑 Run并复用原 Session/AgentData,不切换版本。简写 `df-task dispatch` 不支持版本/幂等键,需固定时用 `append` |
34
34
  | `waiting_reason` | `waiting_on`、`reason` | `waiting_run_id`(实际执行 ID) | 给人和给下一轮调度的上下文,**不是推进器的门** |
35
- | `handoff_to_human` | `ask` | `assignee` | 交球和派活是同一条事实,区别只在 `ask` 的内容 |
35
+ | `handoff_to_human` | `ask` | `assignee`、`participation`、`task_assignee` | 交球和派活是同一条事实,区别只在 `ask` 的内容 |
36
36
  | `adjudication` | `subject`、`verdict` | `reason` | `subject` 是被裁决那条事实的**序号**(数字,不是字符串) |
37
37
  | `run_terminated` | `run_id`、`confirmed_status` | —— | 已经停完并确认之后记录的既成事实 |
38
38
 
@@ -48,3 +48,29 @@
48
48
  | `attempt_failed` | `watermark`、`reason` | `intent_seq` | 推进失败 |
49
49
 
50
50
  **「有 started 没有 accepted」是合法的中间状态**,含义是「正在启动,结果未知」。读日志时不要把它当成异常 —— 推进器会靠幂等键重投把它推进到 accepted。`run_kind` 只能是 `scheduler` 或 `task`;只有调度唤起带 `input_watermark`。
51
+
52
+ ## 请求实际人工参与
53
+
54
+ 公开 handoff 本身不等于邀请。平台支持时,scheduler_run 可用以下命令写入实际参与请求;账号须已由调用方核验,`ask` 向接收人说明具体协助事项和相关成果。
55
+
56
+ ```bash
57
+ # 邀请并定向通知;保留现有经办人
58
+ df-task handoff "$DF_TASK_ID" --participation ensure_member \
59
+ --assignee "$human_id" --ask "$handoff_text" --json
60
+ # 同时明确授予同一人主经办责任
61
+ df-task handoff "$DF_TASK_ID" --participation ensure_member \
62
+ --assignee "$human_id" --task-assignee "$assignment_json" \
63
+ --ask "$handoff_text" --json
64
+ ```
65
+
66
+ `assignment_json` 是 `{"kind":"human","id":"与 assignee 相同的真实账号"}`。省略该参数不改经办人,普通答复、补信息与审核不得因定向交接自动改派。通用 `append --type handoff_to_human --file <JSON>` 可提交同一协议:`ask`、`assignee`、`participation: "ensure_member"`,以及明确需要时的 `task_assignee`;不新增另一套操作语义。
67
+
68
+ 命令成功只确认请求事实已写入。读取 `external_result`,以 `source="human_participation"`、`ref="请求事实序号"` 关联回执;`details.request_seq` 应与该序号一致:
69
+
70
+ - `succeeded`:核对 `member_confirmed`、`member_id`、`notification_id`;请求分配时再核对 `task_assignee`,并读回当前成员与经办人。
71
+ - `failed`:保留错误码及部分成功字段。成员已加入或通知已生成,不代表分配也成功;不得把失败请求当作已交接全部责任。
72
+ - `unknown` 或尚无回执:结果未确认,后续核对同一请求;平台恢复会沿原请求兑现,不因暂未确认而重复邀请。后续真实成功回执可补充先前未知结果,按实际操作及当前状态判断。
73
+
74
+ 通知记录生成不证明人已阅读、回复或批准。邀请、经办人设置、人的实际处理和审核意见是不同事实;本协议不决定何时求助、选择谁或是否接受审核结果。
75
+
76
+ 创建身份从 `df-task get` 的不可变 `creator` 读取,不能从 owner、成员人数或任意 metadata 推断为 bot。未知身份保持未知。
package/manifest.json CHANGED
@@ -56,5 +56,6 @@
56
56
  "project": "deepflow-agent-runtime",
57
57
  "entry": "cli/df-agentrt.mjs"
58
58
  }
59
- ]
59
+ ],
60
+ "version": "0.3.3"
60
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metagptx/deepflow-suite-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Agent-friendly installer and distribution CLI for the DeepFlow suite.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1 +1 @@
1
- {"version":"0.3.1","project":"deepflow-agent-ctrl","sourceCommit":"10e745773559f4712b04ab61915a018995e30630","sourceDirty":false}
1
+ {"version":"0.3.3","project":"deepflow-agent-ctrl","sourceCommit":"dee1a88615b09bd979562902cebc631f81b1859b","sourceDirty":false}
@@ -1 +1 @@
1
- {"version":"0.3.1","project":"deepflow-agent-runtime","sourceCommit":"420ebb91ed4cf92ca81eb8c295cc111f94b9da2c","sourceDirty":false}
1
+ {"version":"0.3.3","project":"deepflow-agent-runtime","sourceCommit":"592af8311ec783961a98e47377340121b78f2a68","sourceDirty":false}
@@ -3,7 +3,7 @@ export const help = `df-task — DeepFlow Task fact log CLI
3
3
  Usage:
4
4
  df-task ls [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
5
5
  df-task get <task-id> [--fields <paths> --json] [--metadata-source <source>] [--metadata-query-key <key>]
6
- df-task create --title <text> [--description <text>] [--message <text>] [--metadata <json>]
6
+ df-task create --title <text> [--description <text>] [--message <text>] [--metadata <json>] [--idempotency-key <key>]
7
7
  df-task facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
8
8
  df-task facts <task-id> --all --output-file <path> --json [--max-pages <n>] [--max-bytes <n>]
9
9
  df-task append <task-id> --type <type> --file payload.json [--idempotency-key <k>]
@@ -14,7 +14,7 @@ Usage:
14
14
  Scheduling shorthands (each one is a wrapper around append):
15
15
  df-task dispatch <task-id> --agent <name> --instruction <text> [--run <run-id>]
16
16
  df-task wait <task-id> --on <what> --reason <text> [--waiting-run <run-id>]
17
- df-task handoff <task-id> --ask <text> [--assignee <who>]
17
+ df-task handoff <task-id> --ask <text> [--assignee <who>] [--participation ensure_member] [--task-assignee <json>]
18
18
 
19
19
  Global options (before or after the command):
20
20
  --base-url <url> Service URL (or DF_TASK_BASE_URL)
@@ -23,7 +23,7 @@ Global options (before or after the command):
23
23
 
24
24
  Environment:
25
25
  DF_TASK_BASE_URL Service URL, default http://127.0.0.1:8090
26
- DF_TASK_ACTOR_KIND Write identity: human | external | task_run | scheduler_run | driver
26
+ DF_TASK_ACTOR_KIND Write identity: human | bot | external | task_run | scheduler_run | driver
27
27
  DF_TASK_ACTOR_ID Write identity id, e.g. the current run id
28
28
 
29
29
  Facts are append-only. The API has no way to change or remove a fact, so neither does this CLI.
@@ -54,13 +54,14 @@ export const commands = {
54
54
  "writes": false
55
55
  },
56
56
  "create": {
57
- "usage": "create --title <text> [--description <text>] [--message <text>] [--metadata <json>]",
57
+ "usage": "create --title <text> [--description <text>] [--message <text>] [--metadata <json>] [--idempotency-key <key>]",
58
58
  "description": "Create a task, optionally with its first user message in the same transaction.",
59
59
  "flags": [
60
60
  "--title",
61
61
  "--description",
62
62
  "--message",
63
- "--metadata"
63
+ "--metadata",
64
+ "--idempotency-key"
64
65
  ],
65
66
  "required": [
66
67
  "--title"
@@ -173,11 +174,14 @@ export const commands = {
173
174
  "writes": true
174
175
  },
175
176
  "handoff": {
176
- "usage": "handoff <task-id> --ask <text> [--assignee <who>]",
177
+ "usage": "handoff <task-id> --ask <text> [--assignee <who>] [--participation ensure_member] [--task-assignee <json>]",
177
178
  "description": "Append a handoff to a human. Handing the ball back and asking a human to do a step are the same fact.",
178
179
  "flags": [
179
180
  "--ask",
180
- "--assignee"
181
+ "--assignee",
182
+ "--participation",
183
+ "--task-assignee",
184
+ "--idempotency-key"
181
185
  ],
182
186
  "required": [
183
187
  "--ask"
@@ -58,8 +58,21 @@ export function validate(o, env) {
58
58
  }
59
59
  if (spec.writes) {
60
60
  if (!env.DF_TASK_ACTOR_KIND?.trim() || !env.DF_TASK_ACTOR_ID?.trim()) throw Error('writing a fact requires DF_TASK_ACTOR_KIND and DF_TASK_ACTOR_ID');
61
+ if (o.command === 'create') {
62
+ const key = o.values['--idempotency-key'] || '';
63
+ if (key.length > 200 || /[^\x21-\x7e]/.test(key) || (env.DF_TASK_ACTOR_KIND === 'bot' && key.length < 8)) throw Error('creation idempotency key must be at most 200 visible ASCII bytes; bot creation requires at least 8 bytes');
64
+ }
61
65
  if (!actorKinds.includes(env.DF_TASK_ACTOR_KIND)) throw Error(`unknown DF_TASK_ACTOR_KIND ${env.DF_TASK_ACTOR_KIND}`);
62
- if (o.command === 'metadata' && env.DF_TASK_ACTOR_KIND !== 'human') throw Error('metadata requires DF_TASK_ACTOR_KIND=human');
66
+ if (o.command === 'metadata' && !['human','bot'].includes(env.DF_TASK_ACTOR_KIND)) throw Error('metadata requires DF_TASK_ACTOR_KIND=human or bot');
67
+ }
68
+ if (o.command === 'handoff' && ('--participation' in o.values || '--task-assignee' in o.values)) {
69
+ const target = o.values['--assignee'];
70
+ if (o.values['--participation'] !== 'ensure_member') throw Error('--participation must be ensure_member for member operations');
71
+ if (!target || target.trim() !== target || Buffer.byteLength(target) > 200) throw Error('member operations require a non-empty --assignee of at most 200 bytes without surrounding whitespace');
72
+ if ('--task-assignee' in o.values) {
73
+ const assignment = object(o.values['--task-assignee'], '--task-assignee must be a JSON object');
74
+ if (Object.keys(assignment).length !== 2 || assignment.kind !== 'human' || assignment.id !== target) throw Error('--task-assignee must identify the same human as --assignee');
75
+ }
63
76
  }
64
77
  validateReadOptions(o);
65
78
  }
@@ -95,8 +108,8 @@ export async function requestFor(o) {
95
108
  let type, payload;
96
109
  if (c === 'dispatch') { type = 'dispatch_intent'; payload = { agent: v['--agent'], instruction: v['--instruction'], mode: v['--run'] ? 'message' : 'new_run', ...(v['--run'] ? { target_run_id: v['--run'] } : {}) }; }
97
110
  if (c === 'wait') { type = 'waiting_reason'; payload = { waiting_on: v['--on'], reason: v['--reason'], ...(v['--waiting-run'] ? { waiting_run_id: v['--waiting-run'] } : {}) }; }
98
- if (c === 'handoff') { type = 'handoff_to_human'; payload = { ask: v['--ask'], ...(v['--assignee'] ? { assignee: v['--assignee'] } : {}) }; }
99
- return post(`${path}/facts`, { type, payload });
111
+ if (c === 'handoff') { type = 'handoff_to_human'; payload = { ask: v['--ask'], ...(v['--assignee'] ? { assignee: v['--assignee'] } : {}), ...(v['--participation'] ? { participation: v['--participation'] } : {}), ...(v['--task-assignee'] ? { task_assignee: object(v['--task-assignee'], '--task-assignee must be a JSON object') } : {}) }; }
112
+ return post(`${path}/facts`, { type, payload, ...(v['--idempotency-key'] ? {idempotency_key:v['--idempotency-key']} : {}) });
100
113
  }
101
114
  function human(command, data, stdout) {
102
115
  const page = JSON.parse(data, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value) || {};
@@ -149,6 +162,7 @@ export async function run(args, { env = process.env, stdout = process.stdout, st
149
162
  const request = async (options, limit = 0) => {
150
163
  const req = options === o ? initialRequest : await requestFor(options);
151
164
  const headers = req.body ? { 'Content-Type': 'application/json' } : {};
165
+ if (o.command === 'create' && o.values['--idempotency-key']) headers['Idempotency-Key'] = o.values['--idempotency-key'];
152
166
  if (req.method !== 'GET') { headers['X-DF-Actor-Kind'] = env.DF_TASK_ACTOR_KIND; headers['X-DF-Actor-Id'] = env.DF_TASK_ACTOR_ID; }
153
167
  const response = await fetch(base.replace(/\/+$/, '') + req.path, { method: req.method, body: req.body, headers, signal: AbortSignal.timeout(30000) });
154
168
  const data = await readResponse(response, limit);
@@ -1,3 +1,3 @@
1
1
  // Generated by generate-protocol.mjs from internal/model/fact.go.
2
2
  export const factTypes = ["task_lifecycle","user_message","external_result","artifact_published","summary","help_requested","run_outcome","dispatch_intent","waiting_reason","handoff_to_human","adjudication","run_terminated","invocation_started","invocation_accepted","invocation_ended","attempt_failed"];
3
- export const actorKinds = ["human","external","task_run","scheduler_run","driver"];
3
+ export const actorKinds = ["human","bot","external","task_run","scheduler_run","driver"];
@@ -18,7 +18,7 @@ export function validateReadOptions(o) {
18
18
  const v = o.values;
19
19
  if ('--fields' in v) {
20
20
  if (!o.json) throw Error('--fields requires --json');
21
- const roots = o.command === 'get' ? ['id', 'title', 'description', 'description_version', 'metadata', 'created_at', 'updated_at'] : ['seq', 'type', 'actor_kind', 'actor_id', 'payload', 'idempotency_key', 'created_at'];
21
+ const roots = o.command === 'get' ? ['id', 'title', 'description', 'description_version', 'creator', 'metadata', 'created_at', 'updated_at'] : ['seq', 'type', 'actor_kind', 'actor_id', 'payload', 'idempotency_key', 'created_at'];
22
22
  const paths = v['--fields'].split(',');
23
23
  paths.forEach((field, i) => {
24
24
  const parts = field.split('.');
@@ -1 +1 @@
1
- {"version":"0.3.1","project":"deepflow-task","sourceCommit":"a5b9064298158aad2fef2a67f0baa0a638c89f4b","sourceDirty":false}
1
+ {"version":"0.3.3","project":"deepflow-task","sourceCommit":"cc53e86834b19e8a4fe73f9e9a08d76853d51e51","sourceDirty":false}
@@ -1 +1 @@
1
- {"version":"0.3.1","project":"deepflow-task-driver","sourceCommit":"c05efdc13dfd0d7a9043007f7421dbd1ac6daab2","sourceDirty":false}
1
+ {"version":"0.3.3","project":"deepflow-task-driver","sourceCommit":"c05efdc13dfd0d7a9043007f7421dbd1ac6daab2","sourceDirty":false}
@@ -48,6 +48,7 @@ export function parseArgs(argv, env = process.env) {
48
48
  after: "",
49
49
  limit: "",
50
50
  scope: "mine",
51
+ offset: "",
51
52
  squadId: undefined,
52
53
  file: undefined,
53
54
  attachmentIds: [],
@@ -60,6 +61,7 @@ export function parseArgs(argv, env = process.env) {
60
61
  options.json = true;
61
62
  continue;
62
63
  }
64
+ if(argument==='--receipt-only'){options.receiptOnly=true;continue;}
63
65
  if (argument === "--help" || argument === "-h") {
64
66
  options.help = true;
65
67
  continue;
@@ -78,19 +80,27 @@ export function parseArgs(argv, env = process.env) {
78
80
  "--after",
79
81
  "--limit",
80
82
  "--scope",
83
+ "--offset",
81
84
  "--squad-id",
82
85
  "--file",
83
86
  "--attachment-id",
84
87
  "--member-id",
85
- "--expected-owner",
86
- "--owner-id",
88
+ "--assignee-id",
89
+ "--expected-assignee",
87
90
  "--participant-id",
91
+ "--creation-mode",
92
+ "--request-seq",
93
+ "--followup-context",
88
94
  ].includes(name)
89
95
  ) {
90
96
  const parsed =
91
97
  inline === undefined ? valueAfter(argv, index, name) : inline;
92
98
  if (inline === undefined) index += 1;
93
99
  if (name === "--base-url") options.baseUrl = parsed;
100
+ if (name === "--creation-mode") {
101
+ if(!['atomic','staged'].includes(parsed))throw new UsageError('--creation-mode 需为 atomic 或 staged。');
102
+ options.creationMode=parsed;
103
+ }
94
104
  if (name === "--workspace") options.workspace = parsed;
95
105
  if (name === "--expected-actor") {
96
106
  if (!parsed.trim() || parsed.length > 160 || /[\r\n]/.test(parsed))
@@ -103,12 +113,30 @@ export function parseArgs(argv, env = process.env) {
103
113
  if (name === "--after") options.after = parsed;
104
114
  if (name === "--limit") options.limit = parsed;
105
115
  if (name === "--scope") options.scope = parsed;
116
+ if (name === "--offset") options.offset = parsed;
106
117
  if (name === "--squad-id") options.squadId = parsed;
107
118
  if (name === "--file") options.file = parsed;
108
119
  if (name === "--attachment-id") options.attachmentIds.push(parsed);
109
120
  if (name === "--member-id") options.memberId = parsed;
110
- if (name === "--expected-owner") options.expectedOwner = parsed;
111
- if (name === "--owner-id") options.ownerId = parsed;
121
+ if (name === "--request-seq") options.requestSeq=parsed;
122
+ if (name === "--followup-context") {
123
+ let value;try{value=JSON.parse(parsed);}catch{throw new UsageError('--followup-context 需要原提醒关联 JSON。');}
124
+ if(!value||Array.isArray(value)||Object.keys(value).length!==4||Object.keys(value).some(k=>!['task_instance_id','recipient_id','request_seq','version'].includes(k))||
125
+ typeof value.task_instance_id!=='string'||!value.task_instance_id||value.task_instance_id.length>2048||/[\u0000-\u001f\u007f]/.test(value.task_instance_id)||
126
+ typeof value.recipient_id!=='string'||!/^[\w-]{1,160}$/.test(value.recipient_id)||!Number.isSafeInteger(value.request_seq)||value.request_seq<1||typeof value.version!=='string'||!/^[a-f0-9]{64}$/.test(value.version))throw new UsageError('--followup-context 原提醒关联无效。');
127
+ options.followupContext=value;
128
+ }
129
+ if (name === "--assignee-id") options.assigneeId = parsed;
130
+ if (name === "--expected-assignee") {
131
+ let baseline;
132
+ try { baseline=JSON.parse(parsed); } catch { throw new UsageError('--expected-assignee 需要 JSON 原值基线。'); }
133
+ const v=baseline?.value;
134
+ if(!baseline || Array.isArray(baseline) || typeof baseline.present!=='boolean' ||
135
+ Object.keys(baseline).some(k=>!['present','value'].includes(k)) ||
136
+ (baseline.present ? !Object.hasOwn(baseline,'value') || v!==null && (!v || !['human','agent'].includes(v.kind) || !/^[\w-]{1,100}$/.test(v.id||'') || Object.keys(v).some(k=>!['kind','id'].includes(k))) : Object.hasOwn(baseline,'value')))
137
+ throw new UsageError('--expected-assignee 原值基线无效。');
138
+ options.expectedAssignee=baseline;
139
+ }
112
140
  if (name === "--participant-id") options.participantIds.push(parsed);
113
141
  continue;
114
142
  }
@@ -200,6 +228,7 @@ async function request(options, path, requestOptions = {}, context = {}) {
200
228
  const baseUrl = normalizeBaseUrl(options.baseUrl);
201
229
  const url = new URL(path, `${baseUrl}/`);
202
230
  const headers = requestHeaders(options, context, requestOptions.key);
231
+ if(requestOptions.receiptOnly)headers['X-Deepflow-Message-Receipt']='lookup';
203
232
  if (requestOptions.body !== undefined)
204
233
  headers["Content-Type"] = "application/json";
205
234
  let response;
@@ -241,7 +270,7 @@ async function request(options, path, requestOptions = {}, context = {}) {
241
270
  return data;
242
271
  }
243
272
 
244
- async function currentContext(options) {
273
+ async function currentContext(options,requireWorkspace=true) {
245
274
  const me = await request(
246
275
  options,
247
276
  "/api/v1/me",
@@ -249,12 +278,12 @@ async function currentContext(options) {
249
278
  options.workspace ? { workspace: options.workspace } : {},
250
279
  );
251
280
  const workspace = options.workspace || me.workspaceId;
252
- if (!workspace)
281
+ if (!workspace&&requireWorkspace)
253
282
  throw new RequestError("Workbench 没有返回当前工作区。", {
254
283
  code: "workspace_missing",
255
284
  });
256
285
  if (
257
- Array.isArray(me.workspaces) &&
286
+ workspace && Array.isArray(me.workspaces) &&
258
287
  !me.workspaces.some((item) => item.id === workspace)
259
288
  )
260
289
  throw new RequestError(`当前用户无权访问工作区:${workspace}`, {
@@ -277,6 +306,8 @@ async function currentContext(options) {
277
306
 
278
307
  async function loadConfig(options) {
279
308
  const context = await currentContext(options);
309
+ if(context.me.user.principalKind==='bot')return {baseUrl:normalizeBaseUrl(options.baseUrl),identity:context.me.user,workspaces:context.me.workspaces,
310
+ workspace:context.me.workspaces.find(w=>w.id===context.workspace),authentication:context.me.authentication,capabilities:context.me.capabilities};
280
311
  const [platform, auth, readiness, capabilities] = await Promise.all([
281
312
  request(options, "/api/v1/platform-state"),
282
313
  request(options, "/api/v1/auth/config"),
@@ -451,18 +482,21 @@ function help() {
451
482
  df-workbench squad create --file <配置.json> [--idempotency-key <key>] [--json]
452
483
  df-workbench squad update <squad-id> --file <配置.json> [--idempotency-key <key>] [--json]
453
484
  df-workbench workspace members [--json]
454
- df-workbench task create --message <消息> [--title <标题>] [--squad-id <squad-id>] [--owner-id <user-id>] [--participant-id <user-id>] [--idempotency-key <key>]
485
+ df-workbench task create --message <消息> [--title <标题>] [--squad-id <squad-id>] [--assignee-id <user-id>] [--creation-mode <atomic|staged>] [--participant-id <user-id>] [--idempotency-key <key>]
486
+ df-workbench task assign <task-id> --member-id <user-id> [--expected-assignee <JSON基线>] [--idempotency-key <key>]
455
487
  df-workbench task member-options <task-id>
456
488
  df-workbench task invite <task-id> --member-id <user-id> [--idempotency-key <key>]
457
- df-workbench task transfer-owner <task-id> --member-id <user-id> --expected-owner <user-id> [--idempotency-key <key>]
458
489
  df-workbench task send <task-id> --message <消息> [--idempotency-key <key>]
490
+ df-workbench task followup-context <task-id> --member-id <user-id> [--request-seq <seq>]
491
+ df-workbench task delivery-context <task-id> --member-id <user-id>
459
492
  df-workbench task lookup <DF-number> [--json]
460
493
  df-workbench task get <task-id> [--json]
461
494
  df-workbench task facts <task-id> [--after <seq>] [--limit <n>] [--json]
462
- df-workbench task list [--scope <mine|workspace>] [--limit <n>] [--json]
495
+ df-workbench task list [--scope <mine|workspace>] [--limit <n>] [--offset <n>] [--json]
463
496
  df-workbench attachment upload --file <文件> [--idempotency-key <key>] [--json]
464
497
 
465
498
  task create/send 可重复传入 --attachment-id <id>,每次最多 10 个。
499
+ task send 可携带 --followup-context <原提醒JSON>;--receipt-only 仅核对原幂等键的消息回执,不创建新消息。
466
500
 
467
501
  全局配置:
468
502
  DF_WORKBENCH_BASE_URL Workbench 根地址,默认 http://127.0.0.1:8793
@@ -481,11 +515,13 @@ async function run(options) {
481
515
  process.stdout.write(`${help()}\n`);
482
516
  return;
483
517
  }
484
- if ((options.ownerId !== undefined || options.participantIds.length) && !(command === "task" && subject === "create")) throw new UsageError("--owner-id 和 --participant-id 仅用于 task create。");
485
- if (options.ownerId !== undefined && !/^[\w-]{1,100}$/.test(options.ownerId)) throw new UsageError("初始负责人 ID 无效。");
486
- if ((options.memberId !== undefined && !(command === "task" && ["invite","transfer-owner"].includes(subject))) ||
487
- (options.expectedOwner !== undefined && !(command === "task" && subject === "transfer-owner")))
488
- throw new UsageError("成员和负责人参数只用于对应的任务成员命令。");
518
+ if ((options.assigneeId !== undefined || options.participantIds.length) && !(command === "task" && subject === "create")) throw new UsageError("--assignee-id 和 --participant-id 仅用于 task create。");
519
+ if (options.assigneeId !== undefined && !/^[\w-]{1,100}$/.test(options.assigneeId)) throw new UsageError("初始负责人 ID 无效。");
520
+ if (options.memberId !== undefined && !(command === "task" && ["invite","assign","followup-context","delivery-context"].includes(subject))) throw new UsageError("--member-id 仅用于邀请、指定负责人或核对提醒对象。");
521
+ if(options.followupContext && !(command==='task'&&subject==='send'))throw new UsageError('--followup-context 仅用于向原任务回复。');
522
+ if(options.receiptOnly && !(command==='task'&&subject==='send'))throw new UsageError('--receipt-only 仅用于核对原消息回执。');
523
+ if(options.requestSeq!==undefined && !(command==='task'&&subject==='followup-context'))throw new UsageError('--request-seq 仅用于核对原提醒。');
524
+
489
525
  if (
490
526
  options.squadId !== undefined &&
491
527
  !(command === "task" && subject === "create")
@@ -553,12 +589,12 @@ async function run(options) {
553
589
  return;
554
590
  }
555
591
  if (command === "me") {
556
- const context = await currentContext(options);
592
+ const context = await currentContext(options,false);
557
593
  writeResult(
558
594
  context.me,
559
595
  options,
560
596
  (value) =>
561
- `用户: ${value.user.name} (${value.user.id})\n工作区: ${value.workspaceId}`,
597
+ `身份: ${value.user.principalKind||"human"} · ${value.user.name} (${value.user.id})\n工作区: ${value.workspaceId}`,
562
598
  );
563
599
  return;
564
600
  }
@@ -579,17 +615,50 @@ async function run(options) {
579
615
  writeResult(value,options,value => value.members.map(member=>`${member.userId} ${member.name || ""}`).join("\n"));return;
580
616
  }
581
617
  if (command !== "task") throw new UsageError(`不支持的命令:${command}`);
582
- if (["member-options", "invite", "transfer-owner"].includes(subject)) {
618
+ if (subject === 'transfer-owner') throw new UsageError('此任务操作已停用。');
619
+ if(subject==='capabilities') {
620
+ const context=await currentContext(options),value=await request(options,'/api/v1/task-ui/capabilities',{},context);
621
+ writeResult(value,options,value=>JSON.stringify(value));return;
622
+ }
623
+ if(['followup-context','delivery-context'].includes(subject)) {
624
+ const taskId=rest[0];
625
+ if(!/^[\w-]{1,160}$/.test(taskId||'')||!/^[\w-]{1,160}$/.test(options.memberId||''))throw new UsageError('需要任务 ID 和 --member-id 提醒对象。');
626
+ const context=await currentContext(options),query=new URLSearchParams({recipient_id:options.memberId});
627
+ if(options.requestSeq!==undefined)query.set('request_seq',String(integerOption(options.requestSeq,'--request-seq',0,1,Number.MAX_SAFE_INTEGER)));
628
+ const value=await request(options,`/api/v1/task-ui/tasks/${encodeURIComponent(taskId)}/${subject}?${query}`,{},context);
629
+ if(value?.taskId!==taskId||value.recipientId!==options.memberId||typeof value.taskInstanceId!=='string'||!value.taskInstanceId||!Number.isSafeInteger(value.requestSeq)||value.requestSeq<1||!/^[a-f0-9]{64}$/.test(value.version||''))throw new RequestError('原提醒关联回执不匹配。',{code:'task_lookup_mismatch'});
630
+ if(subject==='delivery-context'&&(value.kind!=='delivery'||!Array.isArray(value.artifacts)||!value.artifacts.length||!Number.isSafeInteger(value.startedSeq)||!Number.isSafeInteger(value.endedSeq)))throw new RequestError('交付版本回执不匹配。',{code:'task_lookup_mismatch'});
631
+ writeResult(value,options,value=>`原请求 #${value.requestSeq}:${value.text}`);return;
632
+ }
633
+ if (["member-options", "invite", "assign"].includes(subject)) {
583
634
  const taskId = rest[0];
584
635
  if (!taskId || !/^[\w-]{1,160}$/.test(taskId)) throw new UsageError("需要有效的 Task ID。");
585
636
  const writes = subject !== "member-options";
586
637
  if (writes && !/^[\w-]{1,100}$/.test(options.memberId || "")) throw new UsageError("需要 --member-id 指定有效的 DF 用户 ID。");
587
- if (subject === "transfer-owner" && !/^[\w-]{1,100}$/.test(options.expectedOwner || "")) throw new UsageError("需要 --expected-owner 指定当前负责人 ID。");
588
638
  const context = await currentContext(options);
589
- const path = subject === "member-options" ? `/api/v1/task-ui/tasks/${encodeURIComponent(taskId)}/property-options`
590
- : `/api/v1/tasks/${encodeURIComponent(taskId)}/${subject === "invite" ? "member-invitations" : "owner-transfer"}`;
591
- const value = await request(options, path, writes ? {method:"POST", key:idempotencyKey(options), body:subject === "invite"
592
- ? {invitee_ids:[options.memberId]} : {new_owner_id:options.memberId,expected_owner_id:options.expectedOwner}} : {}, context);
639
+ let value;
640
+ if(subject==='assign') {
641
+ const current=(await request(options,`/api/v1/tasks/${encodeURIComponent(taskId)}`,{},context)).task;
642
+ if(current?.id!==taskId||current.metadata?.data?.workspace_id!==context.workspace)throw new RequestError('任务范围未确认。',{code:'task_metadata_mismatch'});
643
+ const presentation=current.metadata.data.presentation||{},key=idempotencyKey(options);
644
+ const assignee=Object.hasOwn(presentation,'assignee')?{present:true,value:presentation.assignee}:{present:false};
645
+ if(options.expectedAssignee) {
646
+ const expected=options.expectedAssignee;
647
+ const same=expected.present===assignee.present && (!expected.present || expected.value===null&&assignee.value===null || expected.value?.kind===assignee.value?.kind&&expected.value?.id===assignee.value?.id);
648
+ const alreadyAssigned=presentation.assignee?.kind==='human' && presentation.assignee.id===options.memberId;
649
+ if(!same&&!alreadyAssigned)throw new RequestError('负责人已变化,请重新核对分配要求。',{code:'task_assignee_changed',status:409});
650
+ }
651
+ // The original baseline is part of the idempotent command, including after
652
+ // a successful write whose response was lost.
653
+ const baselineAssignee=options.expectedAssignee || assignee;
654
+ value=await request(options,`/api/v1/tasks/${encodeURIComponent(taskId)}/presentation`,{method:'POST',key,body:{changes:{assignee:{kind:'human',id:options.memberId}},baseline:{assignee:baselineAssignee}}},context);
655
+ const saved=value?.task,data=saved?.metadata?.data;
656
+ if(saved?.id!==taskId||data?.workspace_id!==context.workspace||data.presentation?.assignee?.kind!=='human'||data.presentation.assignee.id!==options.memberId)
657
+ throw new RequestError('负责人保存回执未确认,请保留原幂等键核对。',{code:'task_metadata_mismatch',outcome:'unknown',taskId,idempotencyKey:key});
658
+ } else {
659
+ const path=subject==='member-options'?`/api/v1/task-ui/tasks/${encodeURIComponent(taskId)}/property-options`:`/api/v1/tasks/${encodeURIComponent(taskId)}/member-invitations`;
660
+ value=await request(options,path,writes?{method:'POST',key:idempotencyKey(options),body:{invitee_ids:[options.memberId]}}:{},context);
661
+ }
593
662
  writeResult(value, options, value => JSON.stringify(value));
594
663
  return;
595
664
  }
@@ -599,17 +668,24 @@ async function run(options) {
599
668
  options.title,
600
669
  options.squadId,
601
670
  );
602
- if (options.ownerId) {
603
- if (![options.ownerId,...options.participantIds].every(id=>/^[\w-]{1,100}$/.test(id))) throw new UsageError("初始负责人和参与人 ID 无效。");
604
- body.initial_owner_id=options.ownerId;
605
- body.first_message="";
671
+ if (options.assigneeId) {
672
+ if (![options.assigneeId,...options.participantIds].every(id=>/^[\w-]{1,100}$/.test(id))) throw new UsageError("初始负责人和参与人 ID 无效。");
673
+ if(options.creationMode==='atomic') {body.initial_presentation={assignee:{kind:'human',id:options.assigneeId}};body.first_message=messageValue(options.message);}
674
+ else {body.initial_assignee_id=options.assigneeId;body.first_message="";}
606
675
  body.invitee_ids=[...new Set(options.participantIds)].sort();
607
- } else if(options.participantIds.length) throw new UsageError("--participant-id 需要同时指定 --owner-id。");
608
- if (options.attachmentIds.length && !options.ownerId)
676
+ } else if(options.participantIds.length) throw new UsageError("--participant-id 需要同时指定 --assignee-id。");
677
+ if (options.attachmentIds.length && (!options.assigneeId || options.creationMode==='atomic'))
609
678
  body.attachment_ids = options.attachmentIds;
610
679
  const context = await currentContext(options);
680
+ if(context.me.user.principalKind==='bot') {
681
+ if(options.assigneeId||options.squadId||options.attachmentIds.length)throw new UsageError('Bot 创建任务不支持指定人工负责人、小队或初始附件;附件可在创建后通过 task send 提交。');
682
+ delete body.conversation;
683
+ body.title ||= Array.from(body.description.split('\n')[0]).slice(0,80).join('');
684
+ }
611
685
  const key = idempotencyKey(options);
612
- let value = await request(
686
+ let atomicCreation=options.creationMode==='atomic';
687
+ let value;
688
+ try {value = await request(
613
689
  options,
614
690
  "/api/v1/tasks",
615
691
  {
@@ -618,7 +694,17 @@ async function run(options) {
618
694
  body,
619
695
  },
620
696
  context,
621
- );
697
+ );} catch(error) {
698
+ // A checked validation rejection has not applied the staged command. An
699
+ // idempotency conflict can also be an atomic receipt from before upgrade.
700
+ // Reuse the original key; never fall back after an unknown write outcome.
701
+ const rejected=error instanceof RequestError && (error.status===422 && error.code==='invalid_input' || error.status===409 && error.code==='idempotency_conflict');
702
+ if(!options.assigneeId || atomicCreation || !rejected)throw error;
703
+ const atomicBody={...body,first_message:messageValue(options.message),initial_presentation:{assignee:{kind:'human',id:options.assigneeId}},...(options.attachmentIds.length?{attachment_ids:options.attachmentIds}:{})};
704
+ delete atomicBody.initial_assignee_id;
705
+ value=await request(options,'/api/v1/tasks',{method:'POST',key,body:atomicBody},context);
706
+ atomicCreation=true;
707
+ }
622
708
  if (
623
709
  body.squad_id &&
624
710
  value?.task?.metadata?.data?.squad?.squadId !== body.squad_id
@@ -632,10 +718,13 @@ async function run(options) {
632
718
  idempotencyKey: key,
633
719
  },
634
720
  );
635
- if (options.ownerId) {
721
+ if (options.assigneeId) {
636
722
  const task=value?.task,data=task?.metadata?.data;
637
- if (!task?.id || data?.workspace_id!==context.workspace || data.owner_id!==options.ownerId || data.created_by!==context.actorId ||
638
- ![context.actorId,options.ownerId,...options.participantIds].every(id=>data.members?.includes(id))) throw new RequestError("已创建的任务初始负责人或成员回执不匹配,请保留原幂等键核对,不要重建。",{code:"task_metadata_mismatch",outcome:"unknown",taskId:task?.id,idempotencyKey:key});
723
+ if (!task?.id || data?.workspace_id!==context.workspace || (data.presentation?.assignee?.kind!=='human'||data.presentation.assignee.id!==options.assigneeId) || data.created_by!==context.actorId ||
724
+ ![context.actorId,options.assigneeId,...options.participantIds].every(id=>data.members?.includes(id))) throw new RequestError("已创建的任务初始负责人或成员回执不匹配,请保留原幂等键核对,不要重建。",{code:"task_metadata_mismatch",outcome:"unknown",taskId:task?.id,idempotencyKey:key});
725
+ // Atomic creation freezes the initial message and squad in the server's
726
+ // creation receipt; only the staged protocol needs a second message.
727
+ if(!atomicCreation) {
639
728
  // Current squad edits must never change a retry of the original message.
640
729
  const agentId=data.initial_message_context?.agentId;
641
730
  if (!agentId) throw new RequestError("任务已创建,但创建时的小队快照没有可接收首条消息的伙伴;请保留原任务与幂等键核对。",{code:"initial_agent_unavailable",outcome:"unknown",taskId:task.id,idempotencyKey:key});
@@ -648,6 +737,7 @@ async function run(options) {
648
737
  }
649
738
  if (!Number.isSafeInteger(first?.fact?.seq) || first.fact.task_id!==task.id) throw new RequestError("初始消息回执不匹配;保留原任务与幂等键重试。",{code:"initial_message_unconfirmed",outcome:"unknown",taskId:task.id,idempotencyKey:key});
650
739
  value={...value,first_fact:first.fact,first_fact_seq:first.fact.seq};
740
+ }
651
741
  }
652
742
  writeResult(value, options, taskSummary);
653
743
  return;
@@ -663,8 +753,10 @@ async function run(options) {
663
753
  {
664
754
  method: "POST",
665
755
  key,
756
+ receiptOnly:options.receiptOnly===true,
666
757
  body: {
667
758
  text: messageValue(options.message),
759
+ ...(options.followupContext?{followup_context:options.followupContext}:{}),
668
760
  ...(options.attachmentIds.length
669
761
  ? { attachment_ids: options.attachmentIds }
670
762
  : {}),
@@ -750,6 +842,7 @@ async function run(options) {
750
842
  `/api/v1/tasks?${new URLSearchParams({
751
843
  scope: options.scope,
752
844
  limit: String(limit),
845
+ offset: String(integerOption(options.offset, "--offset", 0, 0, 1048576)),
753
846
  include_state: "1",
754
847
  })}`,
755
848
  {},
@@ -1 +1 @@
1
- {"version":"0.3.1","project":"deepflow-workbench","sourceCommit":"b83963f15f4ed187164464dddbfd2ea460f52971","sourceDirty":false}
1
+ {"version":"0.3.3","project":"deepflow-workbench","sourceCommit":"adfb558b1c3532b338b5a8b0d5c11a43739e4ca6","sourceDirty":false}