@metagptx/deepflow-suite-cli 0.0.0-stage → 0.1.1

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.
Files changed (36) hide show
  1. package/README.md +48 -2
  2. package/bin/deepflow-suite.js +77 -0
  3. package/bundle/deepflow-release.json +185 -0
  4. package/bundle/skills/deepflow-suite/SKILL.md +79 -0
  5. package/bundle/skills/deepflow-suite/manifest.yaml +20 -0
  6. package/bundle/skills/df-agentctrl/SKILL.md +143 -0
  7. package/bundle/skills/df-agentctrl/assets/agent.json +8 -0
  8. package/bundle/skills/df-agentctrl/references/maintenance.md +68 -0
  9. package/bundle/skills/df-agentctrl/scripts/to_snapshot.py +26 -0
  10. package/bundle/skills/df-agentrt/SKILL.md +191 -0
  11. package/bundle/skills/df-agentrt-repo/SKILL.md +83 -0
  12. package/bundle/skills/df-task/SKILL.md +177 -0
  13. package/bundle/skills/df-task/assets/payload-examples/adjudication.json +5 -0
  14. package/bundle/skills/df-task/assets/payload-examples/artifact_published.json +6 -0
  15. package/bundle/skills/df-task/assets/payload-examples/help_requested.json +5 -0
  16. package/bundle/skills/df-task/assets/payload-examples/run_outcome.json +5 -0
  17. package/bundle/skills/df-task/assets/payload-examples/run_terminated.json +4 -0
  18. package/bundle/skills/df-task/assets/payload-examples/summary.json +4 -0
  19. package/bundle/skills/df-task/references/fact-types.md +48 -0
  20. package/bundle/skills/df-task/skill_test.go +201 -0
  21. package/bundle/skills/df-taskdriver/SKILL.md +126 -0
  22. package/bundle/skills/df-workbench/SKILL.md +64 -0
  23. package/bundle/skills/df-workbench/manifest.yaml +5 -0
  24. package/manifest.json +68 -0
  25. package/package.json +28 -4
  26. package/scripts/build-package.mjs +12 -0
  27. package/skills/deepflow-suite/SKILL.md +79 -0
  28. package/skills/deepflow-suite/manifest.yaml +20 -0
  29. package/src/bundle-installer.mjs +189 -0
  30. package/src/bundle.mjs +128 -0
  31. package/src/cli-install.mjs +140 -0
  32. package/src/commands/doctor.mjs +35 -0
  33. package/src/commands/install.mjs +32 -0
  34. package/src/constants.mjs +30 -0
  35. package/src/linker.mjs +96 -0
  36. package/src/manifest.mjs +72 -0
@@ -0,0 +1,201 @@
1
+ package skill
2
+
3
+ import (
4
+ "encoding/json"
5
+ "os"
6
+ "path/filepath"
7
+ "strings"
8
+ "testing"
9
+
10
+ "github.com/deepflow/deepflow-task/internal/authz"
11
+ "github.com/deepflow/deepflow-task/internal/model"
12
+ "github.com/deepflow/deepflow-task/internal/validate"
13
+ )
14
+
15
+ func readFile(t *testing.T, path string) string {
16
+ t.Helper()
17
+ data, err := os.ReadFile(path)
18
+ if err != nil {
19
+ t.Fatal(err)
20
+ }
21
+ return string(data)
22
+ }
23
+
24
+ func TestSkillHasFrontmatter(t *testing.T) {
25
+ content := readFile(t, "SKILL.md")
26
+ if !strings.HasPrefix(content, "---\n") {
27
+ t.Fatal("SKILL.md must start with YAML frontmatter")
28
+ }
29
+ frontmatter, _, found := strings.Cut(strings.TrimPrefix(content, "---\n"), "\n---")
30
+ if !found {
31
+ t.Fatal("frontmatter is not closed")
32
+ }
33
+ for _, key := range []string{"name:", "description:"} {
34
+ if !strings.Contains(frontmatter, key) {
35
+ t.Errorf("frontmatter is missing %s", key)
36
+ }
37
+ }
38
+ if !strings.Contains(frontmatter, "name: df-task") {
39
+ t.Errorf("frontmatter name must be df-task, got %q", frontmatter)
40
+ }
41
+ }
42
+
43
+ func TestSkillReferenceDocumentsEveryFactType(t *testing.T) {
44
+ // **十六种全要,不是只要智能体写得了的那九种。** 这份 reference 的作用是让智能体
45
+ // 读日志时认得每一条事实 —— 只遍历两个 Run 身份的可写集合,把「## 外部输入」
46
+ // 或「## 推进器写的」整张表删掉照样全绿,而 `external_result` 与 `invocation_ended`
47
+ // 是三条触发事实里的两条,认不出它们,读日志就读漏了最关键的两种。
48
+ reference := readFile(t, filepath.Join("references", "fact-types.md"))
49
+ for _, factType := range model.FactTypes() {
50
+ if !strings.Contains(reference, string(factType)) {
51
+ t.Errorf("references/fact-types.md 没有记 %s", factType)
52
+ }
53
+ }
54
+ // 「一共十六种」这个数字也要钉住。不钉的话,将来加第十六种类型时 Task 10 的
55
+ // `len(payloadSchemas) != len(model.FactTypes())` 会红,而这份文档会悄悄过期。
56
+ // **两个条件要各给自己的报错。** 合成一句 `if A || B` 的话,「有人把文档里那句话
57
+ // 删了而类型数还是 15」这一路会红出「文档写的是十六种,而 FactTypes() 有 15 种」
58
+ // —— 一句不含任何矛盾的话,看的人不知道该改文档还是改代码。而这一路恰好是最
59
+ // 常见的那一路:改文档比加第十六种类型常见得多。
60
+ if len(model.FactTypes()) != 16 {
61
+ t.Errorf("model.FactTypes() 有 %d 种,而 fact-types.md 与本断言都按十六种写的 —— 加了新类型要同时改这两处", len(model.FactTypes()))
62
+ }
63
+ if !strings.Contains(reference, "一共十六种事实") {
64
+ t.Error("fact-types.md 里找不到「一共十六种事实」这句 —— 这个数字是文档过期的唯一机械信号,不要删")
65
+ }
66
+ }
67
+
68
+ func TestSkillCommandTablesMatchTheAuthorizationMatrix(t *testing.T) {
69
+ // 两个方向都要验。只验「不该出现的没出现」,那么往调度一节里插一行
70
+ // `--type run_outcome` 照样全绿,而任务 Run 读到它就会去写,然后拿一个 403;
71
+ // 只验「该出现的出现了」,那么删掉一整行命令也照样全绿,而 Agent 在 Run 里
72
+ // 只有这份文档可读 —— 少一行,那条事实它就永远不会写。
73
+ content := readFile(t, "SKILL.md")
74
+ sections := map[model.ActorKind]string{
75
+ model.ActorTaskRun: "## 任务 Run 可以写什么",
76
+ model.ActorSchedulerRun: "## 调度 Run 可以写什么",
77
+ }
78
+ for kind, heading := range sections {
79
+ // strings.Cut 返回三个值:before、after、found。这里要的是标题**之后**那段,
80
+ // 所以第一个返回值丢掉 —— 接成两个变量编译就过不去。
81
+ _, afterHeading, found := strings.Cut(content, heading)
82
+ if !found {
83
+ t.Fatalf("SKILL.md is missing the %q section", heading)
84
+ }
85
+ // 截到下一个 `## ` 为止。两节各自只谈自己那个身份,所以「职责边界」一节里
86
+ // 那句「写一条 summary 或 help_requested 说明情况」不会被算进调度一节。
87
+ section, _, _ := strings.Cut(afterHeading, "\n## ")
88
+ for _, factType := range model.FactTypes() {
89
+ // 正向只认表格那一列的形状 `| ` + 类型名 + `` ` ``。用裸的 Contains 会被
90
+ // 同一节的正文喂饱 —— 「`run_outcome` 只是你的主张」「读日志时你看到的是
91
+ // `dispatch_intent`」这些句子里都有类型名,于是删掉对应那行命令照样全绿,
92
+ // 而 Agent 在 Run 里只有这份文档可读,少一行那条事实它就永远不会写。
93
+ inTable := strings.Contains(section, "| `"+string(factType)+"`")
94
+ // 反向要宽:正文里出现一个写不了的类型,同样会把 Agent 带去拿 403。
95
+ mentioned := strings.Contains(section, string(factType))
96
+ switch {
97
+ case authz.CanWrite(kind, factType) && !inTable:
98
+ t.Errorf("%s 的命令表里没有 %s,而这个身份写得了它", heading, factType)
99
+ case !authz.CanWrite(kind, factType) && mentioned:
100
+ t.Errorf("%s 一节提了 %s,而这个身份写它会拿 403", heading, factType)
101
+ }
102
+ }
103
+ }
104
+ // 上面那两节里没有推进器的身份,所以唤起生命周期靠这一条兜:整份文档里都不许
105
+ // 出现「让 Agent 用 append 写它」的样子。
106
+ for _, factType := range authz.WritableBy(model.ActorDriver) {
107
+ if strings.Contains(content, string(factType)+" --file") {
108
+ t.Errorf("SKILL.md shows an agent writing %s; only the driver writes the invocation lifecycle", factType)
109
+ }
110
+ }
111
+ }
112
+
113
+ func TestSkillStatesTheEnvironmentContract(t *testing.T) {
114
+ content := readFile(t, "SKILL.md")
115
+ // DF_TASK_ID 和 DF_RUN_ID 也在这份清单里:SKILL.md 的示例命令用了它们,
116
+ // 而它们由 runtime 注入、不由本服务定义。示例里出现却没说明来源,
117
+ // Agent 就会以为要自己编一个。
118
+ for _, needle := range []string{"DF_TASK_BASE_URL", "DF_TASK_ACTOR_KIND", "DF_TASK_ACTOR_ID", "DF_TASK_ID", "DF_RUN_ID", "--json"} {
119
+ if !strings.Contains(content, needle) {
120
+ t.Errorf("SKILL.md does not mention %s", needle)
121
+ }
122
+ }
123
+ }
124
+
125
+ // 推进器拉起的 Run 里没有人可问。环境门禁若在 Run 内也要求询问,调度 Agent
126
+ // 每一轮都会停下不读事实,而推进器照常记下执行结束——多轮执行就此静默断掉。
127
+ func TestSkillResolvesTheRunEnvironmentWithoutAsking(t *testing.T) {
128
+ content := readFile(t, "SKILL.md")
129
+ for _, needle := range []string{"### Run 内的目标环境", "http://deepflowtask.deepflow.svc.cluster.local:8090` 即 `dev`", "不询问"} {
130
+ if !strings.Contains(content, needle) {
131
+ t.Errorf("SKILL.md does not state %q", needle)
132
+ }
133
+ }
134
+ }
135
+
136
+ func TestSkillStatesFactsAreAppendOnly(t *testing.T) {
137
+ content := readFile(t, "SKILL.md")
138
+ if !strings.Contains(content, "只追加") {
139
+ t.Fatal("SKILL.md must state that facts are append-only")
140
+ }
141
+ }
142
+
143
+ func TestPayloadExamplesPassServerValidation(t *testing.T) {
144
+ entries, err := os.ReadDir(filepath.Join("assets", "payload-examples"))
145
+ if err != nil {
146
+ t.Fatal(err)
147
+ }
148
+ if len(entries) == 0 {
149
+ t.Fatal("there must be at least one payload example")
150
+ }
151
+ checked := 0
152
+ for _, entry := range entries {
153
+ // 只看 .json。这台机器上到处都是 .DS_Store,不过滤的话它会被当成
154
+ // 一个叫 ".DS_Store" 的事实类型,测试在一个与代码无关的原因上变红。
155
+ if !strings.HasSuffix(entry.Name(), ".json") {
156
+ continue
157
+ }
158
+ checked++
159
+ name := strings.TrimSuffix(entry.Name(), ".json")
160
+ factType := model.FactType(name)
161
+ if !model.KnownFactType(factType) {
162
+ t.Errorf("%s is not a fact type", entry.Name())
163
+ continue
164
+ }
165
+ raw := readFile(t, filepath.Join("assets", "payload-examples", entry.Name()))
166
+ if !json.Valid([]byte(raw)) {
167
+ t.Errorf("%s is not valid JSON", entry.Name())
168
+ continue
169
+ }
170
+ if err := validate.Payload(factType, []byte(raw)); err != nil {
171
+ t.Errorf("%s would be rejected by the server: %v", entry.Name(), err)
172
+ }
173
+ }
174
+ if checked == 0 {
175
+ t.Fatal("the directory has entries but none of them is a .json example")
176
+ }
177
+ }
178
+
179
+ func TestEveryHandWrittenPayloadHasAnExample(t *testing.T) {
180
+ // 凡是 SKILL.md 让智能体自己写 payload 的事实类型,都要有一份能抄的样例。
181
+ // 语法糖那三种(dispatch / wait / handoff)的字段由 CLI 填,不需要样例。
182
+ // **这里两个身份都要走。** 只走任务 Run 的话,调度 Run 那两种手写类型就露在外面:
183
+ // `adjudication` 的 `subject` 是全服务唯一一个数字字段,没样例的调度智能体会写成
184
+ // 字符串然后吃一个 422;而日志只追加,它既改不了那条,也没有别的地方能学对。
185
+ sugar := map[model.FactType]bool{
186
+ model.FactDispatchIntent: true,
187
+ model.FactWaitingReason: true,
188
+ model.FactHandoffToHuman: true,
189
+ }
190
+ for _, kind := range []model.ActorKind{model.ActorTaskRun, model.ActorSchedulerRun} {
191
+ for _, factType := range authz.WritableBy(kind) {
192
+ if sugar[factType] {
193
+ continue
194
+ }
195
+ path := filepath.Join("assets", "payload-examples", string(factType)+".json")
196
+ if _, err := os.Stat(path); err != nil {
197
+ t.Errorf("%s 缺样例,而 %s 要手写这份 payload:%v", factType, kind, err)
198
+ }
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: df-taskdriver
3
+ description: 使用df-taskdriver CLI诊断DeepFlow任务推进器的运行状态,回答某个Task为什么没有被推进、当前哪些Task正在被处理、实例是否就绪。适用于任务卡住时的排查与部署后的健康确认,不负责创建Task、写入事实或决定下一步动作。
4
+ ---
5
+
6
+ # DeepFlow Task Driver
7
+
8
+ 通过`df-taskdriver`查看一个正在运行的driver实例。运行命令前确认CLI已在PATH中;实例地址由`DF_TASKDRIVER_BASE_URL`或`--base-url`提供,默认`http://127.0.0.1:8081`。本Skill不安装二进制,也不启动服务。
9
+
10
+ ## 目标环境门禁与地址
11
+
12
+ 每次调用 driver 前,必须先确认用户要操作的目标环境。用户没有明确说 `local`、本地、`dev`、开发环境或 `prod`、生产环境时,先询问:
13
+
14
+ ```text
15
+ 请指定目标环境:local(本地)、dev(开发环境)还是 prod(生产环境)?
16
+ ```
17
+
18
+ 环境一旦选定,本轮所有服务调用都必须保持在同一环境;不能因为连接失败就切换到另一环境,也不能把当前机器所在位置当成目标环境。
19
+
20
+ | 目标环境 | 本地执行时的地址 | 在 dev-k8s 内执行时的地址 |
21
+ | --- | --- | --- |
22
+ | `local` | `http://127.0.0.1:8081` | 不适用 |
23
+ | `dev` | `https://deepflowtaskdriver.dev.deepwisdomai.com` | `http://deepflowtaskdriver.deepflow.svc.cluster.local:8081` |
24
+ | `prod` | 尚未配置 | 尚未配置 |
25
+
26
+ 地址列描述的是同一目标环境的两种访问方式:本地 CLI 使用 dev Ingress,集群内 Agent 使用 dev Service DNS。确认环境后,可以用用户明确提供的 `--base-url` 覆盖该环境的默认地址,但该地址必须属于已选环境;如果地址与环境冲突,先停止并澄清,不能用它绕过环境门禁。`prod` 目前没有地址,任何 prod 操作都必须停止并说明尚未接入。
27
+
28
+ 本地操作 dev 时示例:
29
+
30
+ ```bash
31
+ df-taskdriver --base-url https://deepflowtaskdriver.dev.deepwisdomai.com task <task-id>
32
+ ```
33
+
34
+ 执行任何诊断前先请求选定地址的 `GET /healthz`。健康检查失败时报告该环境不可达,不得静默改用 localhost 或其他环境。
35
+
36
+ ```bash
37
+ df-taskdriver help
38
+ df-taskdriver task <task-id>
39
+ ```
40
+
41
+ 若命令不存在,说明缺少CLI部署:本项目源码可用时运行`make install-cli`,否则向调用方说明需要安装`df-taskdriver`并提供实例地址。driver的地址不是事实服务的地址,也不是runtime的地址。
42
+
43
+ ## driver做什么,不做什么
44
+
45
+ driver不决定任何业务动作。它只做两件事:发现某个Task有未被覆盖的触发事实时,起一个调度Run;看到调度智能体写下的`dispatch_intent`时,把它兑现成一次runtime唤起。**下一步做什么由调度智能体决定,写在事实日志里**——所以「Task停住了」有两种完全不同的原因,排查的第一步就是区分它们:
46
+
47
+ - driver还欠着事(有在飞唤起、有未兑现意图、或谓词成立但还没跑到),问题在driver或它的上游依赖。
48
+ - driver无未了之事,那是调度智能体没有下发新意图、或者事实压根没写进日志。这时候查driver是白费功夫。
49
+
50
+ ## Task metadata 的路由作用
51
+
52
+ driver 会读取 Task metadata 里的 source 和 query_keys,把它作为软范围提示传给 Agent Control,
53
+ 并将完整 metadata 透传给新建的 Runtime Session。driver CLI 没有修改或筛选 metadata 的命令,
54
+ 也不会把它当成权限检查;没有 metadata 的旧Task仍按原有流程推进。
55
+
56
+ 如果某个Task因为范围内找不到Agent而没有推进,先用事实服务查看 Task metadata,再用同一范围
57
+ 检查控制面目录:
58
+
59
+ df-task get <task-id> --json
60
+ df-agentctrl list \
61
+ --metadata-source <source> \
62
+ --metadata-query-key <query-key> \
63
+ --json
64
+
65
+ query_keys 可以包含多个标识;"*" 表示显式共享。没有业务上下文时不要凭空添加过滤条件,
66
+ 否则可能把本来可用的全量目录缩小。
67
+
68
+ **只要实例答了 200**,`task`子命令的最后一行就会直接说明属于哪一种,**三句话必有一句**,不会出现「最后一行什么也没说」的情况:
69
+
70
+ - `驱动这边还有未了之事(见上方在飞唤起/未兑现意图),先看它们卡在哪一步`⇒ 第一种,往上翻看在飞唤起和未兑现意图。
71
+ - `驱动无未了之事,继续查上游(事实是否写入、调度智能体是否下发)`⇒ 第二种,查上游。
72
+ - `驱动这边没有未了之事,谓词已成立,下一拍就会起一轮调度——这一刻不用做什么`⇒ 两者都不是:driver 手上是干净的,而且它**已经决定要起一轮了**,只是还没跑到那一拍。刚往Task写完一条`user_message`时看到的就是这一句。这一刻什么都不用做,隔一个扫描间隔再看;如果反复看到同一句而水位线一直不动,那才是故障,按第一种往下查。
73
+
74
+ 这三句只在人类输出、而且实例真的答了 200 的时候出现。`--json`走的是另一条路——响应体原样印出,没有这一行;用法错误、`limit`越界、非 2xx、响应体解析不了这几种情况下最后一行是报错本身,退出码也不是 0,其中最常撞上的是非 2xx(最后一行形如`读取失败: ...`)。看见这些就照下面「输出与错误」那一节读退出码,别在那里找这三句。
75
+
76
+ 第三句是刻意从第二句里拆出来的:它和第二句都是「驱动无未了之事」,但结论相反——一句叫你去查上游,一句叫你什么都别做。早先两句版本会把这种最常见的正常状态归到第一句,而那句让人「往上翻看在飞唤起」,上方偏偏什么都没印,等于把人指向一段不存在的输出。
77
+
78
+ ## 排查一个不动的Task
79
+
80
+ ```bash
81
+ df-taskdriver task <task-id>
82
+ df-taskdriver task <task-id> 20 # 每页 20 条读;整条日志照样会读完
83
+ df-taskdriver --json task <task-id> # 给脚本用
84
+ df-taskdriver task <task-id> --json # 等价,两个位置都认
85
+ ```
86
+
87
+ 第二个位置参数是 `limit`,透传成 `/debug/tasks/{id}?limit=`,必须是正整数(`0` 和负数会被 CLI 当场拒掉,退出 2,请求不会发出去)。**它是页大小,不是上限**:无论传多少,实例都会翻页把整条日志读完再折叠,所以**只要事实服务正常推进 `after_seq`**,这里的结论就是看全了日志得出的,和推进循环一致。传小值的唯一效果是把一次读拆成更多次上游请求——它不是「日志太长时用来少读点」的旋钮,那样用只会更慢。不传就用默认页大小。响应里的 `facts` 和 `max_seq` 是**这次读到并折叠了多少**:正常情况下就等于整条日志,而一个比预期小的数正是「这次没读全」的信号——上游不推进 `after_seq` 的时候翻页会提前收手、返回一个前缀,而那个前缀折出来的结论是会错的。排障时先看这两个数,因为你怀疑的上游恰恰是最可能干这件事的那一个。
88
+
89
+ 输出第一行是 Task ID,第二行是结论,其余是证据。按下面的顺序读:
90
+
91
+ 1. **结论行(第二行)**。`不会调度`后面跟着谓词给出的理由,这就是答案本身。
92
+ 2. **水位线 / 最大触发事实 / 本轮锚点**。谓词成立的条件是「最大触发事实的seq超过水位线」。水位线是某一轮调度声明「我已经看到这里」的位置,锚点是那一轮幂等键的推导来源——两者印在一起是为了让人看出它们不是一个东西。
93
+ 3. **在飞唤起**。有调度Run在飞时谓词必然不成立,这是正常的等待而不是故障。标着`[待重放]`的是「起了但结果未知」,下一轮对账会用同一把派生键重新POST,不需要人工干预。
94
+ 4. **失败预算**。标着`已耗尽`时driver主动停手等人:确认上游问题已修好后,往该Task写一条新的`user_message`即可恢复——它的seq高于当前水位线,谓词立刻重新成立。不要试图通过重启driver来恢复,重启不改变任何事实。
95
+ 5. **未兑现意图**。有意图却一直没有对应的在飞唤起,说明兑现路径在失败,看`attempt_failed`事实和driver日志。
96
+
97
+ ## 其他子命令
98
+
99
+ ```bash
100
+ df-taskdriver health # 进程还在吗
101
+ df-taskdriver ready # 能不能干活,未就绪时退出码 1
102
+ df-taskdriver locks # 当前哪些 Task 正在被处理、被谁
103
+ df-taskdriver metrics [过滤子串] # 抓取指标
104
+ ```
105
+
106
+ `health`和`ready`是两回事:连不上Redis的实例**不就绪但仍然存活**,不该被重启——Redis恢复后下一拍自己就接上了。
107
+
108
+ `locks`里`其他实例`的持有者是正常的横向扩容现象。锁不续期,所以TTL在一轮还活着时过期、两个实例同时处理一个Task也是允许的:唤起键从日志事实推导。runtime 只有在已部署 `0004_session_idempotency.sql` 和支持 `Idempotency-Key` 的新 handler 时,Session 与 Invocation 幂等键才会把重复请求收敛到同一组资源;旧版本不具备这一保证。可运行 `scripts/accept-runtime-contract.sh`(或追加 `-race`)验证真实 HTTP+SQLite 恢复闭环。指标里`lock_expired_before_release`持续出现说明`LockTTL`配得比真实轮次时长短,仍需调参。
109
+
110
+ ## 输出与错误
111
+
112
+ 自动处理响应用`--json`:它把实例返回的响应体原样印出来(只重新缩进),不经过CLI自己的结构体。所以新实例多返回的字段不会被吞掉,CLI比实例旧一个版本时`--json`依然完整——而人类输出只显示这一版CLI认识的字段。`--json`写在子命令前后都认:`df-taskdriver --json task <task-id>` 和 `df-taskdriver task <task-id> --json` 完全等价。两种写法都支持是刻意的——技术设计 2.7 举的例子是后置形式,而它恰好是 Go `flag` 包默认解析不到的位置(解析在第一个位置参数处就停了),于是 `--json` 会被当成位置参数悄悄吞掉:`locks` 照样印人类文本并退出 0,脚本拿到的是「实例给了肯定回答」而不是它要的 JSON。
113
+
114
+ `metrics`接受`--json`但忽略它:`/metrics`是Prometheus文本,没有第二种渲染可选。接受而不报错是刻意的,这样脚本可以给所有子命令统一加上`--json`。
115
+
116
+ - 退出0:实例给出肯定回答。
117
+ - 退出1:实例回答了但是否定的(未就绪、读取失败、指标注册表没接上)。**非2xx的响应体解析不了也算这一档**——多半是反向代理的错误页带着真实状态码,而状态码本身就是那个否定回答。
118
+ - 退出2:**三种情况**——连不上实例(先核对`--base-url`)、用法错误(参数个数或`limit`越界)、以及**2xx**但响应体不是合法JSON(多半是反向代理返回的错误页披着200:此时状态行是唯一的肯定依据,而它已经不可信了)。后两种情况下响应体照样会印出来,看见它才是发现代理挡在前面的途径。
119
+
120
+ **最后那一档不适用于`metrics`。** `/metrics`回的是Prometheus文本,它本来就不是JSON——对它做「响应体不是合法JSON就退出2」的检查,等于这个子命令永远退出2。所以`metrics`只按状态码算退出码:2xx是0,其余是1(连不上实例仍然是2——那一档跟响应体无关)。代价是一个披着200的代理错误页在`metrics`这一档会被当成肯定回答;这是没办法的事,Prometheus文本没有「合法性」可验(一份空的抓取结果和一个空的错误页在字节上没有区别),而真正的抓取端`/metrics`本身会由Prometheus自己的`up`指标兜住。其余四个真的发请求的子命令(`health` / `ready` / `locks` / `task`)都吃这一档;`help` 不发请求,所以六个子命令里只有 `metrics` 是豁免的那一个。
121
+
122
+ 区分1和2很重要:1说明实例活着且在正常表达状态,2说明根本没拿到回答(或者问法不对)。运维脚本把2当重试、把1当报警。
123
+
124
+ **这套退出码与`df-agentctrl`的映射不同,不要套用。** 两边都用0/1/2,差别在于谁落到哪一档:ctrl 把「连不上」「HTTP≥400」「响应体不是合法JSON」全折进1(它的`doRequest`对这三种一律返回error),2只留给用法错误、未知子命令和base-url不合法;driver 把「连不上」和「2xx但响应体不是JSON」拆出去给2,1只留给「实例回答了但是否定的」。所以一段照着ctrl写的脚本搬过来,会把 driver 的「没问到」当成「问到了个否」。driver 要这么拆,是因为它的正常状态里就包含「上游全挂但我活着」:那种状态下`ready`必须是0而`task`必须是1,而「根本没拿到回答」必须和这两个都分得开——脚本对2重试、对1报警。
125
+
126
+ 本Skill的所有命令都是只读的GET,不会改变任何状态。需要往Task写事实、创建Task或查询事实日志,用事实服务自己的接口,不要指望在这里做。
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: df-workbench
3
+ description: 面向普通用户的 DeepFlow Workbench 入口。使用 df-workbench 获取当前用户和工作区配置,创建 Task、发送后续对话、读取 Task 事实并反馈可见进度;不负责 driver、Agent Control 或 Runtime 的开发者排障,也不执行本地安装。
4
+ ---
5
+
6
+ # df-workbench
7
+
8
+ `df-workbench` 是普通用户的轻量入口。它保持与 Workbench 人工操作一致:先确定当前身份和工作区,再通过 Workbench 的用户 API 创建任务和发送消息。不要直接调用数据库、`df-task` 的写接口或伪造 `DF_TASK_ACTOR_*` 身份。
9
+
10
+ ## 环境和身份
11
+
12
+ 用户必须先明确目标环境:`local`、`dev` 或 `prod`。本轮不要因为连接失败自动切换环境。
13
+
14
+ CLI 配置:
15
+
16
+ ```text
17
+ DF_WORKBENCH_BASE_URL Workbench 根地址;默认 http://127.0.0.1:8793
18
+ DF_WORKBENCH_TOKEN OIDC 环境使用的 personal token
19
+ DF_WORKBENCH_WORKSPACE_ID 可选,覆盖当前工作区
20
+ ```
21
+
22
+ 运行前先调用:
23
+
24
+ ```bash
25
+ df-workbench me --json
26
+ df-workbench config --json
27
+ ```
28
+
29
+ `me` 返回当前用户和可访问工作区;`config` 返回认证模式、当前工作区、Task 写入能力和 Workbench UI 能力。不要打印 Token、Cookie 或其他凭据。
30
+
31
+ local 环境只允许从本机回环地址访问;OIDC 环境使用 Workbench personal token。Token 应通过公司内部配置或 Skill 平台提供,不在命令参数中传递。
32
+
33
+ ## 用户任务流程
34
+
35
+ 创建 Task:
36
+
37
+ ```bash
38
+ df-workbench task create --message '请检查登录页按钮溢出问题' --json
39
+ ```
40
+
41
+ 继续对话:
42
+
43
+ ```bash
44
+ df-workbench task send <task-id> --message '补充检查 Safari 浏览器' --json
45
+ ```
46
+
47
+ 读取任务和事实:
48
+
49
+ ```bash
50
+ df-workbench task get <task-id> --json
51
+ df-workbench task facts <task-id> --json
52
+ ```
53
+
54
+ 创建和发送必须使用稳定的 `--idempotency-key`。如果请求超时或返回未知结果,不要换一个 key 盲目重试;沿用原 key 核对结果。
55
+
56
+ ## 用户反馈边界
57
+
58
+ 可以根据 `task facts` 反馈已经写入的用户消息、Agent 回应、等待原因、交接和产物事实。默认将内部事实整理成用户可理解的里程碑,不展示内部游标、锁、Session 或 Invocation 细节。
59
+
60
+ 如果事实不足以解释任务为什么没有继续推进,只能如实反馈当前可见事实,并说明需要 DeepFlow 开发者排查。不要自行调用 `df-taskdriver`、`df-agentctrl` 或 `df-agentrt`,也不要把“没有新事实”描述成“已经定位故障”。
61
+
62
+ ## 缺少依赖
63
+
64
+ 如果 `df-workbench` CLI 或本 Skill 缺失,提示用户从公司内部 Skill 共享平台安装对应项目,不执行 `pnpm`、`go install`、`make install-cli`、`git clone` 或本地 Skill 目录复制。
@@ -0,0 +1,5 @@
1
+ id: df-workbench
2
+ version: 1.0.0
3
+ provides:
4
+ skill: df-workbench
5
+ cli: df-workbench
package/manifest.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "distribution": "deepflow-suite",
3
+ "version": "0.1.1",
4
+ "skills": [
5
+ {
6
+ "name": "deepflow-suite",
7
+ "source": "tools/deepflow-cli/skills/deepflow-suite",
8
+ "packageSource": "skills/deepflow-suite"
9
+ },
10
+ {
11
+ "name": "df-workbench",
12
+ "source": "deepflow-workbench/skills/df-workbench",
13
+ "packageSource": "skills/df-workbench"
14
+ },
15
+ {
16
+ "name": "df-task",
17
+ "source": "deepflow-task/skills/df-task",
18
+ "packageSource": "skills/df-task"
19
+ },
20
+ {
21
+ "name": "df-taskdriver",
22
+ "source": "deepflow-task-driver/skills/df-taskdriver",
23
+ "packageSource": "skills/df-taskdriver"
24
+ },
25
+ {
26
+ "name": "df-agentctrl",
27
+ "source": "deepflow-agent-ctrl/skills/df-agentctrl",
28
+ "packageSource": "skills/df-agentctrl"
29
+ },
30
+ {
31
+ "name": "df-agentrt",
32
+ "source": "deepflow-agent-runtime/skills/df-agentrt",
33
+ "packageSource": "skills/df-agentrt"
34
+ },
35
+ {
36
+ "name": "df-agentrt-repo",
37
+ "source": "deepflow-agent-runtime/skills/df-agentrt-repo",
38
+ "packageSource": "skills/df-agentrt-repo"
39
+ }
40
+ ],
41
+ "clis": [
42
+ {
43
+ "name": "df-workbench",
44
+ "project": "deepflow-workbench",
45
+ "install": "project"
46
+ },
47
+ {
48
+ "name": "df-task",
49
+ "project": "deepflow-task",
50
+ "install": "make install-cli"
51
+ },
52
+ {
53
+ "name": "df-taskdriver",
54
+ "project": "deepflow-task-driver",
55
+ "install": "make install-cli"
56
+ },
57
+ {
58
+ "name": "df-agentctrl",
59
+ "project": "deepflow-agent-ctrl",
60
+ "install": "make install-cli"
61
+ },
62
+ {
63
+ "name": "df-agentrt",
64
+ "project": "deepflow-agent-runtime",
65
+ "install": "make build"
66
+ }
67
+ ]
68
+ }
package/package.json CHANGED
@@ -1,6 +1,30 @@
1
1
  {
2
2
  "name": "@metagptx/deepflow-suite-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Agent-friendly installer and distribution CLI for the DeepFlow suite.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "bin": {
8
+ "deepflow-suite": "./bin/deepflow-suite.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "bundle/",
13
+ "manifest.json",
14
+ "skills/",
15
+ "src/",
16
+ "scripts/",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "engines": {
23
+ "node": ">=22.13.0"
24
+ },
25
+ "scripts": {
26
+ "test": "node --test tests/*.test.mjs",
27
+ "prepack": "node scripts/build-package.mjs",
28
+ "pack:dry-run": "npm pack --dry-run"
29
+ }
30
+ }
@@ -0,0 +1,12 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { assembleBundle } from "../src/bundle.mjs";
5
+
6
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const workspaceRoot = path.resolve(packageRoot, "../..");
8
+ await assembleBundle({
9
+ workspaceRoot,
10
+ outputDir: path.join(packageRoot, "bundle"),
11
+ manifestPath: path.join(packageRoot, "manifest.json"),
12
+ });
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: deepflow-suite
3
+ description: DeepFlow 统一入口。常规任务默认使用 df-workbench;只有用户明确要求或 Workbench 事实无法解释阻塞时,才按顺序检查 Task、Task Driver、Agent Control 和 Agent Runtime。
4
+ ---
5
+
6
+ # DeepFlow Suite
7
+
8
+ 这是 DeepFlow 的统一用户和开发者入口。它开放完整的 DeepFlow 能力,但不默认调用整条链路:普通任务先走 `df-workbench`,只有确实需要解释推进、Agent 或 Runtime 问题时才升级到其他项目 CLI。
9
+
10
+ ## 依赖
11
+
12
+ 本 Skill 依赖以下项目级 Skill 和 CLI:
13
+
14
+ - `df-workbench`:用户身份、工作区、Workbench 配置和任务事实;
15
+ - `df-task`:Task 原始事实和事实落库核对;
16
+ - `df-taskdriver`:任务推进和唤起兑现诊断;
17
+ - `df-agentctrl`:Agent 定义、版本和可分配目录;
18
+ - `df-agentrt`:Session、Invocation、transcript 和运行现场。
19
+ - `df-agentrt-repo`:在 Runtime 会话中发现和追加代码仓库。
20
+
21
+ 这些依赖由 `deepflow-suite` 安装器统一安装。不要因为某个依赖缺失就自行复制 Skill、克隆仓库或切换安装源。
22
+
23
+ ## 环境门禁
24
+
25
+ 用户没有明确说 `local`、本地、`dev`、开发环境或 `prod`、生产环境时,先询问:
26
+
27
+ ```text
28
+ 请指定目标环境:local(本地)、dev(开发环境)还是 prod(生产环境)?
29
+ ```
30
+
31
+ 环境一旦选定,本轮所有 Workbench、Task、Driver、Agent Control 和 Runtime 请求保持在同一环境。连接失败不能静默切换环境。
32
+
33
+ ## 默认路由
34
+
35
+ ### 普通任务
36
+
37
+ 创建任务、继续对话、查看任务和读取可见进度,只调用 `df-workbench`:
38
+
39
+ ```bash
40
+ df-workbench me --json
41
+ df-workbench config --json
42
+ df-workbench task create --message '<用户原始请求>' --json
43
+ df-workbench task send <task-id> --message '<补充内容>' --json
44
+ df-workbench task get <task-id> --json
45
+ df-workbench task facts <task-id> --json
46
+ ```
47
+
48
+ 不要在普通用户入口直接调用 `df-task` 写接口,不要伪造 `DF_TASK_ACTOR_*`,不要自行创建 Run 或选择执行 Agent。
49
+
50
+ ### 深度排障
51
+
52
+ 用户询问“任务为什么卡住”“为什么没有执行”“查看 Session/Invocation”“检查 Driver/Agent/Runtime”时,按以下顺序只读检查:
53
+
54
+ ```text
55
+ df-workbench
56
+ → df-task
57
+ → df-taskdriver
58
+ → df-agentctrl
59
+ → df-agentrt
60
+ ```
61
+
62
+ 前一步事实不足以解释问题时,才进入下一步。不要把某个服务可达描述成整条链路成功,也不要把未知状态描述成完成或失败。
63
+
64
+ ## 写操作安全
65
+
66
+ - 普通排障优先只读;
67
+ - 取消、释放 Session、修复孤儿执行、启停或回退 Agent 前必须获得明确确认;
68
+ - 不直接修改 Task 事实;
69
+ - 不因诊断失败而重建 Session、Invocation 或切换环境;
70
+ - 所有服务请求保持在用户选定的环境。
71
+
72
+ ## 输出边界
73
+
74
+ 回复时区分:
75
+
76
+ 1. 已由事实确认的结果;
77
+ 2. Driver、Agent Control 或 Runtime 返回的诊断;
78
+ 3. 仍无法确认的未知状态;
79
+ 4. 需要用户或服务维护者采取的下一步。
@@ -0,0 +1,20 @@
1
+ id: deepflow-suite
2
+ version: 0.1.1
3
+ kind: composite-skill
4
+ requires:
5
+ skills:
6
+ - df-workbench
7
+ - df-task
8
+ - df-taskdriver
9
+ - df-agentctrl
10
+ - df-agentrt
11
+ - df-agentrt-repo
12
+ cli:
13
+ - df-workbench
14
+ - df-task
15
+ - df-taskdriver
16
+ - df-agentctrl
17
+ - df-agentrt
18
+ resolution:
19
+ managed_by: deepflow-suite
20
+ install_missing_dependencies: false