botmux 2.33.0 → 2.34.0

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 (281) hide show
  1. package/README.en.md +12 -1
  2. package/README.md +45 -1
  3. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  4. package/dist/adapters/cli/claude-code.js +11 -0
  5. package/dist/adapters/cli/claude-code.js.map +1 -1
  6. package/dist/cli/bots-list-output.d.ts +21 -0
  7. package/dist/cli/bots-list-output.d.ts.map +1 -0
  8. package/dist/cli/bots-list-output.js +23 -0
  9. package/dist/cli/bots-list-output.js.map +1 -0
  10. package/dist/cli/workflow.d.ts +13 -0
  11. package/dist/cli/workflow.d.ts.map +1 -0
  12. package/dist/cli/workflow.js +781 -0
  13. package/dist/cli/workflow.js.map +1 -0
  14. package/dist/cli.js +69 -14
  15. package/dist/cli.js.map +1 -1
  16. package/dist/core/command-handler.d.ts.map +1 -1
  17. package/dist/core/command-handler.js +219 -6
  18. package/dist/core/command-handler.js.map +1 -1
  19. package/dist/core/session-manager.d.ts +6 -1
  20. package/dist/core/session-manager.d.ts.map +1 -1
  21. package/dist/core/session-manager.js +22 -12
  22. package/dist/core/session-manager.js.map +1 -1
  23. package/dist/core/worker-pool.d.ts +13 -0
  24. package/dist/core/worker-pool.d.ts.map +1 -1
  25. package/dist/core/worker-pool.js +100 -6
  26. package/dist/core/worker-pool.js.map +1 -1
  27. package/dist/daemon.d.ts +3 -0
  28. package/dist/daemon.d.ts.map +1 -1
  29. package/dist/daemon.js +884 -3
  30. package/dist/daemon.js.map +1 -1
  31. package/dist/dashboard/auth.d.ts +36 -0
  32. package/dist/dashboard/auth.d.ts.map +1 -1
  33. package/dist/dashboard/auth.js +22 -0
  34. package/dist/dashboard/auth.js.map +1 -1
  35. package/dist/dashboard/web/app.js +20 -1
  36. package/dist/dashboard/web/app.js.map +1 -1
  37. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  38. package/dist/dashboard/web/i18n.js +356 -0
  39. package/dist/dashboard/web/i18n.js.map +1 -1
  40. package/dist/dashboard/web/workflow-catalog.d.ts +2 -0
  41. package/dist/dashboard/web/workflow-catalog.d.ts.map +1 -0
  42. package/dist/dashboard/web/workflow-catalog.js +323 -0
  43. package/dist/dashboard/web/workflow-catalog.js.map +1 -0
  44. package/dist/dashboard/web/workflows.d.ts +2 -0
  45. package/dist/dashboard/web/workflows.d.ts.map +1 -0
  46. package/dist/dashboard/web/workflows.js +1618 -0
  47. package/dist/dashboard/web/workflows.js.map +1 -0
  48. package/dist/dashboard/workflow-api.d.ts +23 -0
  49. package/dist/dashboard/workflow-api.d.ts.map +1 -0
  50. package/dist/dashboard/workflow-api.js +463 -0
  51. package/dist/dashboard/workflow-api.js.map +1 -0
  52. package/dist/dashboard-web/app.js +494 -199
  53. package/dist/dashboard-web/index.html +1 -0
  54. package/dist/dashboard-web/style.css +160 -6
  55. package/dist/dashboard-web/terminal-replay.html +227 -0
  56. package/dist/dashboard.js +29 -12
  57. package/dist/dashboard.js.map +1 -1
  58. package/dist/i18n/en.d.ts.map +1 -1
  59. package/dist/i18n/en.js +12 -0
  60. package/dist/i18n/en.js.map +1 -1
  61. package/dist/i18n/zh.d.ts.map +1 -1
  62. package/dist/i18n/zh.js +12 -0
  63. package/dist/i18n/zh.js.map +1 -1
  64. package/dist/im/lark/card-handler.d.ts +3 -0
  65. package/dist/im/lark/card-handler.d.ts.map +1 -1
  66. package/dist/im/lark/card-handler.js +27 -1
  67. package/dist/im/lark/card-handler.js.map +1 -1
  68. package/dist/im/lark/client.d.ts +19 -2
  69. package/dist/im/lark/client.d.ts.map +1 -1
  70. package/dist/im/lark/client.js +21 -2
  71. package/dist/im/lark/client.js.map +1 -1
  72. package/dist/im/lark/workflow-card-handler.d.ts +50 -0
  73. package/dist/im/lark/workflow-card-handler.d.ts.map +1 -0
  74. package/dist/im/lark/workflow-card-handler.js +152 -0
  75. package/dist/im/lark/workflow-card-handler.js.map +1 -0
  76. package/dist/im/lark/workflow-cards.d.ts +46 -0
  77. package/dist/im/lark/workflow-cards.d.ts.map +1 -0
  78. package/dist/im/lark/workflow-cards.js +226 -0
  79. package/dist/im/lark/workflow-cards.js.map +1 -0
  80. package/dist/im/lark/workflow-progress-card.d.ts +76 -0
  81. package/dist/im/lark/workflow-progress-card.d.ts.map +1 -0
  82. package/dist/im/lark/workflow-progress-card.js +279 -0
  83. package/dist/im/lark/workflow-progress-card.js.map +1 -0
  84. package/dist/im/lark/workflow-slash-command.d.ts +92 -0
  85. package/dist/im/lark/workflow-slash-command.d.ts.map +1 -0
  86. package/dist/im/lark/workflow-slash-command.js +185 -0
  87. package/dist/im/lark/workflow-slash-command.js.map +1 -0
  88. package/dist/services/group-creator.d.ts.map +1 -1
  89. package/dist/services/group-creator.js +17 -4
  90. package/dist/services/group-creator.js.map +1 -1
  91. package/dist/services/groups-store.d.ts +11 -0
  92. package/dist/services/groups-store.d.ts.map +1 -1
  93. package/dist/services/groups-store.js +26 -0
  94. package/dist/services/groups-store.js.map +1 -1
  95. package/dist/services/jsonl-cursor.d.ts +12 -0
  96. package/dist/services/jsonl-cursor.d.ts.map +1 -0
  97. package/dist/services/jsonl-cursor.js +45 -0
  98. package/dist/services/jsonl-cursor.js.map +1 -0
  99. package/dist/services/schedule-store.d.ts +35 -0
  100. package/dist/services/schedule-store.d.ts.map +1 -1
  101. package/dist/services/schedule-store.js +108 -1
  102. package/dist/services/schedule-store.js.map +1 -1
  103. package/dist/skills/definitions.d.ts.map +1 -1
  104. package/dist/skills/definitions.js +399 -0
  105. package/dist/skills/definitions.js.map +1 -1
  106. package/dist/types.d.ts +4 -0
  107. package/dist/types.d.ts.map +1 -1
  108. package/dist/utils/cli-usage-limit.d.ts.map +1 -1
  109. package/dist/utils/cli-usage-limit.js +4 -0
  110. package/dist/utils/cli-usage-limit.js.map +1 -1
  111. package/dist/worker.js +118 -14
  112. package/dist/worker.js.map +1 -1
  113. package/dist/workflows/attempt-resume.d.ts +114 -0
  114. package/dist/workflows/attempt-resume.d.ts.map +1 -0
  115. package/dist/workflows/attempt-resume.js +385 -0
  116. package/dist/workflows/attempt-resume.js.map +1 -0
  117. package/dist/workflows/attempt-terminal.d.ts +21 -0
  118. package/dist/workflows/attempt-terminal.d.ts.map +1 -0
  119. package/dist/workflows/attempt-terminal.js +7 -0
  120. package/dist/workflows/attempt-terminal.js.map +1 -0
  121. package/dist/workflows/blob.d.ts +27 -0
  122. package/dist/workflows/blob.d.ts.map +1 -0
  123. package/dist/workflows/blob.js +39 -0
  124. package/dist/workflows/blob.js.map +1 -0
  125. package/dist/workflows/cancel-run.d.ts +45 -0
  126. package/dist/workflows/cancel-run.d.ts.map +1 -0
  127. package/dist/workflows/cancel-run.js +99 -0
  128. package/dist/workflows/cancel-run.js.map +1 -0
  129. package/dist/workflows/cancel.d.ts +111 -0
  130. package/dist/workflows/cancel.d.ts.map +1 -0
  131. package/dist/workflows/cancel.js +120 -0
  132. package/dist/workflows/cancel.js.map +1 -0
  133. package/dist/workflows/catalog.d.ts +60 -0
  134. package/dist/workflows/catalog.d.ts.map +1 -0
  135. package/dist/workflows/catalog.js +119 -0
  136. package/dist/workflows/catalog.js.map +1 -0
  137. package/dist/workflows/cold-attach.d.ts +30 -0
  138. package/dist/workflows/cold-attach.d.ts.map +1 -0
  139. package/dist/workflows/cold-attach.js +40 -0
  140. package/dist/workflows/cold-attach.js.map +1 -0
  141. package/dist/workflows/cold-scan.d.ts +21 -0
  142. package/dist/workflows/cold-scan.d.ts.map +1 -0
  143. package/dist/workflows/cold-scan.js +70 -0
  144. package/dist/workflows/cold-scan.js.map +1 -0
  145. package/dist/workflows/daemon-spawn.d.ts +117 -0
  146. package/dist/workflows/daemon-spawn.d.ts.map +1 -0
  147. package/dist/workflows/daemon-spawn.js +551 -0
  148. package/dist/workflows/daemon-spawn.js.map +1 -0
  149. package/dist/workflows/definition.d.ts +1309 -0
  150. package/dist/workflows/definition.d.ts.map +1 -0
  151. package/dist/workflows/definition.js +334 -0
  152. package/dist/workflows/definition.js.map +1 -0
  153. package/dist/workflows/effect-input.d.ts +4 -0
  154. package/dist/workflows/effect-input.d.ts.map +1 -0
  155. package/dist/workflows/effect-input.js +18 -0
  156. package/dist/workflows/effect-input.js.map +1 -0
  157. package/dist/workflows/events/append.d.ts +77 -0
  158. package/dist/workflows/events/append.d.ts.map +1 -0
  159. package/dist/workflows/events/append.js +214 -0
  160. package/dist/workflows/events/append.js.map +1 -0
  161. package/dist/workflows/events/idempotency.d.ts +77 -0
  162. package/dist/workflows/events/idempotency.d.ts.map +1 -0
  163. package/dist/workflows/events/idempotency.js +116 -0
  164. package/dist/workflows/events/idempotency.js.map +1 -0
  165. package/dist/workflows/events/index.d.ts +7 -0
  166. package/dist/workflows/events/index.d.ts.map +1 -0
  167. package/dist/workflows/events/index.js +7 -0
  168. package/dist/workflows/events/index.js.map +1 -0
  169. package/dist/workflows/events/payloads.d.ts +917 -0
  170. package/dist/workflows/events/payloads.d.ts.map +1 -0
  171. package/dist/workflows/events/payloads.js +337 -0
  172. package/dist/workflows/events/payloads.js.map +1 -0
  173. package/dist/workflows/events/replay.d.ts +238 -0
  174. package/dist/workflows/events/replay.d.ts.map +1 -0
  175. package/dist/workflows/events/replay.js +608 -0
  176. package/dist/workflows/events/replay.js.map +1 -0
  177. package/dist/workflows/events/schema.d.ts +5242 -0
  178. package/dist/workflows/events/schema.d.ts.map +1 -0
  179. package/dist/workflows/events/schema.js +295 -0
  180. package/dist/workflows/events/schema.js.map +1 -0
  181. package/dist/workflows/events/types.d.ts +34 -0
  182. package/dist/workflows/events/types.d.ts.map +1 -0
  183. package/dist/workflows/events/types.js +2 -0
  184. package/dist/workflows/events/types.js.map +1 -0
  185. package/dist/workflows/fanout.d.ts +36 -0
  186. package/dist/workflows/fanout.d.ts.map +1 -0
  187. package/dist/workflows/fanout.js +114 -0
  188. package/dist/workflows/fanout.js.map +1 -0
  189. package/dist/workflows/hostExecutors/botmux-schedule.d.ts +41 -0
  190. package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -0
  191. package/dist/workflows/hostExecutors/botmux-schedule.js +121 -0
  192. package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -0
  193. package/dist/workflows/hostExecutors/feishu-im.d.ts +12 -0
  194. package/dist/workflows/hostExecutors/feishu-im.d.ts.map +1 -0
  195. package/dist/workflows/hostExecutors/feishu-im.js +49 -0
  196. package/dist/workflows/hostExecutors/feishu-im.js.map +1 -0
  197. package/dist/workflows/hostExecutors/feishu-reply.d.ts +24 -0
  198. package/dist/workflows/hostExecutors/feishu-reply.d.ts.map +1 -0
  199. package/dist/workflows/hostExecutors/feishu-reply.js +88 -0
  200. package/dist/workflows/hostExecutors/feishu-reply.js.map +1 -0
  201. package/dist/workflows/hostExecutors/feishu-send.d.ts +23 -0
  202. package/dist/workflows/hostExecutors/feishu-send.d.ts.map +1 -0
  203. package/dist/workflows/hostExecutors/feishu-send.js +124 -0
  204. package/dist/workflows/hostExecutors/feishu-send.js.map +1 -0
  205. package/dist/workflows/hostExecutors/index.d.ts +8 -0
  206. package/dist/workflows/hostExecutors/index.d.ts.map +1 -0
  207. package/dist/workflows/hostExecutors/index.js +8 -0
  208. package/dist/workflows/hostExecutors/index.js.map +1 -0
  209. package/dist/workflows/hostExecutors/protocol.d.ts +42 -0
  210. package/dist/workflows/hostExecutors/protocol.d.ts.map +1 -0
  211. package/dist/workflows/hostExecutors/protocol.js +181 -0
  212. package/dist/workflows/hostExecutors/protocol.js.map +1 -0
  213. package/dist/workflows/hostExecutors/registry.d.ts +10 -0
  214. package/dist/workflows/hostExecutors/registry.d.ts.map +1 -0
  215. package/dist/workflows/hostExecutors/registry.js +36 -0
  216. package/dist/workflows/hostExecutors/registry.js.map +1 -0
  217. package/dist/workflows/hostExecutors/types.d.ts +78 -0
  218. package/dist/workflows/hostExecutors/types.d.ts.map +1 -0
  219. package/dist/workflows/hostExecutors/types.js +2 -0
  220. package/dist/workflows/hostExecutors/types.js.map +1 -0
  221. package/dist/workflows/loader.d.ts +16 -0
  222. package/dist/workflows/loader.d.ts.map +1 -0
  223. package/dist/workflows/loader.js +56 -0
  224. package/dist/workflows/loader.js.map +1 -0
  225. package/dist/workflows/loop.d.ts +50 -0
  226. package/dist/workflows/loop.d.ts.map +1 -0
  227. package/dist/workflows/loop.js +350 -0
  228. package/dist/workflows/loop.js.map +1 -0
  229. package/dist/workflows/ops-projection.d.ts +168 -0
  230. package/dist/workflows/ops-projection.d.ts.map +1 -0
  231. package/dist/workflows/ops-projection.js +707 -0
  232. package/dist/workflows/ops-projection.js.map +1 -0
  233. package/dist/workflows/orchestrator.d.ts +107 -0
  234. package/dist/workflows/orchestrator.d.ts.map +1 -0
  235. package/dist/workflows/orchestrator.js +197 -0
  236. package/dist/workflows/orchestrator.js.map +1 -0
  237. package/dist/workflows/output-binding.d.ts +70 -0
  238. package/dist/workflows/output-binding.d.ts.map +1 -0
  239. package/dist/workflows/output-binding.js +265 -0
  240. package/dist/workflows/output-binding.js.map +1 -0
  241. package/dist/workflows/params.d.ts +61 -0
  242. package/dist/workflows/params.d.ts.map +1 -0
  243. package/dist/workflows/params.js +195 -0
  244. package/dist/workflows/params.js.map +1 -0
  245. package/dist/workflows/resume.d.ts +263 -0
  246. package/dist/workflows/resume.d.ts.map +1 -0
  247. package/dist/workflows/resume.js +808 -0
  248. package/dist/workflows/resume.js.map +1 -0
  249. package/dist/workflows/run-id.d.ts +2 -0
  250. package/dist/workflows/run-id.d.ts.map +1 -0
  251. package/dist/workflows/run-id.js +7 -0
  252. package/dist/workflows/run-id.js.map +1 -0
  253. package/dist/workflows/run-init.d.ts +48 -0
  254. package/dist/workflows/run-init.d.ts.map +1 -0
  255. package/dist/workflows/run-init.js +99 -0
  256. package/dist/workflows/run-init.js.map +1 -0
  257. package/dist/workflows/runs-dir.d.ts +4 -0
  258. package/dist/workflows/runs-dir.d.ts.map +1 -0
  259. package/dist/workflows/runs-dir.js +15 -0
  260. package/dist/workflows/runs-dir.js.map +1 -0
  261. package/dist/workflows/runtime.d.ts +211 -0
  262. package/dist/workflows/runtime.d.ts.map +1 -0
  263. package/dist/workflows/runtime.js +594 -0
  264. package/dist/workflows/runtime.js.map +1 -0
  265. package/dist/workflows/spawn-bot.d.ts +165 -0
  266. package/dist/workflows/spawn-bot.d.ts.map +1 -0
  267. package/dist/workflows/spawn-bot.js +215 -0
  268. package/dist/workflows/spawn-bot.js.map +1 -0
  269. package/dist/workflows/system.d.ts +49 -0
  270. package/dist/workflows/system.d.ts.map +1 -0
  271. package/dist/workflows/system.js +48 -0
  272. package/dist/workflows/system.js.map +1 -0
  273. package/dist/workflows/trigger-run.d.ts +70 -0
  274. package/dist/workflows/trigger-run.d.ts.map +1 -0
  275. package/dist/workflows/trigger-run.js +88 -0
  276. package/dist/workflows/trigger-run.js.map +1 -0
  277. package/dist/workflows/wait.d.ts +120 -0
  278. package/dist/workflows/wait.d.ts.map +1 -0
  279. package/dist/workflows/wait.js +181 -0
  280. package/dist/workflows/wait.js.map +1 -0
  281. package/package.json +3 -3
@@ -0,0 +1,781 @@
1
+ /**
2
+ * `botmux workflow <sub>` CLI subcommand handlers.
3
+ *
4
+ * v0 offline-runner: load a workflow definition, drive `runLoop` against
5
+ * a stub spawn, and print events to stdout. No daemon / no IM
6
+ * integration — used for smoke-testing the orchestrator end-to-end.
7
+ *
8
+ * The on-daemon path (with lark fan-out, real worker spawn) lives in
9
+ * the `/workflow run` Skill (Slice E-2). This module deliberately
10
+ * keeps the CLI route the simplest possible smoke test.
11
+ */
12
+ import { promises as fs } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { ZodError } from 'zod';
15
+ import { EventLog } from '../workflows/events/append.js';
16
+ import { replay } from '../workflows/events/replay.js';
17
+ import { parseWorkflowDefinition } from '../workflows/definition.js';
18
+ import { loadWorkflowDefinition } from '../workflows/loader.js';
19
+ import { coerceWorkflowParams, ParamCoerceFailure, } from '../workflows/params.js';
20
+ import { runLoop } from '../workflows/loop.js';
21
+ import { mintWorkflowRunId } from '../workflows/run-id.js';
22
+ import { createRun } from '../workflows/run-init.js';
23
+ import { getRunsDir, runDir } from '../workflows/runs-dir.js';
24
+ import { createDefaultHostExecutorRegistry, createDefaultProviderReconcilers, } from '../workflows/hostExecutors/registry.js';
25
+ import { loadEffectInputSidecar } from '../workflows/effect-input.js';
26
+ import { cancelWorkflowRun, isTerminalRunStatus, } from '../workflows/cancel-run.js';
27
+ import { createStubSpawnFn, } from '../workflows/spawn-bot.js';
28
+ import { eventSeqFromId, extractEventContext, listRuns, } from '../workflows/ops-projection.js';
29
+ // Local arg parsers — mirror cli.ts shape; deliberately not exported.
30
+ function argValue(args, ...flags) {
31
+ for (let i = 0; i < args.length; i++) {
32
+ const a = args[i];
33
+ for (const f of flags) {
34
+ if (a === f && i + 1 < args.length)
35
+ return args[i + 1];
36
+ if (a.startsWith(f + '='))
37
+ return a.slice(f.length + 1);
38
+ }
39
+ }
40
+ return undefined;
41
+ }
42
+ function positionals(args) {
43
+ const out = [];
44
+ for (let i = 0; i < args.length; i++) {
45
+ const a = args[i];
46
+ if (a.startsWith('--')) {
47
+ if (!a.includes('=') && i + 1 < args.length)
48
+ i++;
49
+ continue;
50
+ }
51
+ out.push(a);
52
+ }
53
+ return out;
54
+ }
55
+ export async function cmdWorkflow(sub, rest) {
56
+ switch (sub) {
57
+ case 'run':
58
+ await cmdWorkflowRun(rest);
59
+ return;
60
+ case 'resume':
61
+ await cmdWorkflowResume(rest);
62
+ return;
63
+ case 'cancel':
64
+ await cmdWorkflowCancel(rest);
65
+ return;
66
+ case 'ls':
67
+ case 'list':
68
+ await cmdWorkflowLs(rest);
69
+ return;
70
+ case 'tail':
71
+ await cmdWorkflowTail(rest);
72
+ return;
73
+ case 'validate':
74
+ await cmdWorkflowValidate(rest);
75
+ return;
76
+ case 'show':
77
+ await cmdWorkflowShow(rest);
78
+ return;
79
+ case 'help':
80
+ case '':
81
+ case undefined:
82
+ printHelp();
83
+ return;
84
+ default:
85
+ console.error(`未知子命令: workflow ${sub}`);
86
+ printHelp();
87
+ process.exit(1);
88
+ }
89
+ }
90
+ function printHelp() {
91
+ console.log(`用法: botmux workflow <run|resume|cancel|ls|tail|validate|show> [...]
92
+
93
+ 子命令:
94
+ run <id> [--param key=value ...] [--param-json key=<json> ...] [--run-id <id>] [--bot-resolver echo]
95
+ 离线驱动 workflow(stub spawn)。事件 / 状态打到 stdout。
96
+ humanGate 节点跑到 'awaiting-wait' 即退出(CLI 离线场景下没有审批入口)。
97
+ --param 适合标量(string/number/boolean);--param-json 适合 object/array
98
+ 或希望严格保留 JSON 类型的值,例如 --param-json users='["a","b"]'。
99
+ 未声明的 param 名会被拒;type 不匹配 / 缺 required 会清晰报错。
100
+
101
+ resume <runId>
102
+ 从磁盘 runDir 冷恢复一个已有 run。R0 recovery 先收 dangling effect,
103
+ 之后 orchestrator 继续推进;遇到 humanGate 只输出 awaiting-wait,
104
+ 不伪造审批;run 已 terminal 则直接打摘要,零事件写入。
105
+ CLI 不会 spawn 新 subagent —— 现有 in-flight subagent 会被标记
106
+ WorkerCrashed/manual 并由 orchestrator 终结 run。
107
+
108
+ cancel <runId> [--reason <text>]
109
+ 写入 run-level cancelRequested 并驱动 cancel recovery。terminal run
110
+ 直接 no-op;不会发 IM 通知或重发审批卡。
111
+
112
+ ls [--all] [--status running,failed,...] [--wide] [--json]
113
+ 列出 runsDir 下所有 run。默认仅 non-terminal;--all 全列;--status
114
+ 支持逗号多选;--wide 增加 failedNodeId/chatId/larkAppId;--json
115
+ 输出完整 JSON 行。
116
+
117
+ tail <runId> [--from <seq>] [--follow] [--json]
118
+ 打印 run 的事件简表(seq / type / node / activity / errorCode)。
119
+ 默认 history-only;--follow 才轮询 events.ndjson 增量。--from 默认 1。
120
+
121
+ validate <path>
122
+ 校验 workflow.json 文件。成功打印 workflowId / node 数;失败打印
123
+ JSON parse、Zod issue path + message,或 graph invariant 错误。
124
+
125
+ show <runId>
126
+ replay 当前 run 的事件,打印 Snapshot 摘要 JSON(含 nodes/dangling 等)。
127
+
128
+ 环境变量:
129
+ BOTMUX_WORKFLOW_RUNS_DIR=<path> 覆盖 runs 根目录(默认 ~/.botmux/workflow-runs)
130
+ `);
131
+ }
132
+ // ─── run ──────────────────────────────────────────────────────────────────
133
+ async function cmdWorkflowRun(rest) {
134
+ const id = positionals(rest)[0];
135
+ if (!id) {
136
+ console.error('用法: botmux workflow run <id> [--param key=value ...] [--param-json key=<json> ...]');
137
+ process.exit(1);
138
+ }
139
+ const runId = argValue(rest, '--run-id') ?? mintWorkflowRunId(id);
140
+ const rawParams = collectRawParams(rest);
141
+ const def = await loadWorkflowDefinition(id).catch((err) => {
142
+ console.error(err.message);
143
+ process.exit(1);
144
+ });
145
+ // unreachable after process.exit, but TS doesn't know
146
+ if (!def)
147
+ return;
148
+ let params;
149
+ try {
150
+ params = coerceWorkflowParams(def, rawParams);
151
+ }
152
+ catch (err) {
153
+ if (err instanceof ParamCoerceFailure) {
154
+ console.error('参数校验失败:');
155
+ for (const issue of err.issues) {
156
+ console.error(`- ${issue.message}`);
157
+ }
158
+ }
159
+ else {
160
+ console.error(`参数校验失败:${err instanceof Error ? err.message : String(err)}`);
161
+ }
162
+ process.exit(1);
163
+ }
164
+ // Bootstrap the in-memory bot registry so hostExecutors like
165
+ // feishu-send can resolve `larkAppId` → Lark client. IM path inherits
166
+ // the daemon's already-registered bots; the standalone CLI doesn't.
167
+ try {
168
+ const { registerBot, loadBotConfigs } = await import('../bot-registry.js');
169
+ for (const cfg of loadBotConfigs())
170
+ registerBot(cfg);
171
+ }
172
+ catch {
173
+ // Missing/invalid bots.json is fine — workflows that don't touch
174
+ // Feishu still run; the host executor will surface a clear
175
+ // "Bot not registered" error if one does.
176
+ }
177
+ const log = new EventLog(runId, getRunsDir());
178
+ const botResolver = () => ({});
179
+ const spawnSubagent = createStubSpawnFn(echoHandler);
180
+ console.log(`workflow=${id} runId=${runId} params=${JSON.stringify(params)}`);
181
+ console.log(`runsDir=${getRunsDir()}`);
182
+ await createRun(log, { def, params, initiator: 'cli', botResolver });
183
+ console.log('runCreated, runStarted');
184
+ const ctx = {
185
+ log,
186
+ def,
187
+ spawnSubagent,
188
+ hostExecutors: createDefaultHostExecutorRegistry(),
189
+ reconcilers: createDefaultProviderReconcilers(),
190
+ loadEffectInput: (activityId, attemptId) => loadEffectInputSidecar(log, activityId, attemptId),
191
+ };
192
+ const result = await runLoop(ctx, { maxTicks: 200 });
193
+ console.log(`\nloop stopped: ${result.reason} after ${result.ticks} tick(s)`);
194
+ console.log(`run.status=${result.lastSnapshot.run.status}`);
195
+ console.log(`events: ${result.lastSnapshot.lastSeq}`);
196
+ if (result.reason === 'awaiting-wait') {
197
+ console.log(`awaiting-wait on: ${result.lastSnapshot.danglingWaits.join(', ')}`);
198
+ console.log(`(CLI 离线模式没有审批入口;从 IM 用 /workflow run 跑能拿到审批卡)`);
199
+ }
200
+ if (result.reason === 'terminal' && result.lastSnapshot.run.output) {
201
+ console.log(`output: ${result.lastSnapshot.run.output.outputHash}`);
202
+ }
203
+ }
204
+ const echoHandler = (input) => ({
205
+ echo: input.prompt.slice(0, 200),
206
+ bot: input.botName,
207
+ activityId: input.activityId,
208
+ });
209
+ // ─── resume ───────────────────────────────────────────────────────────────
210
+ /**
211
+ * R1 cold resume — pick up an existing run from its on-disk runDir.
212
+ *
213
+ * Contract (codex-loopy review 2026-05-20):
214
+ * - Replay first. If the run is already terminal, print summary and
215
+ * write zero events.
216
+ * - Do NOT call `createRun` / write `runStarted` — those are mint-time
217
+ * events. Resume just attaches a fresh ctx to the existing log.
218
+ * - `spawnSubagent` is a no-throw failure stub: returns
219
+ * `WorkerCrashed/manual` so any subagent dispatch the orchestrator
220
+ * decides to do during resume lands as a recorded `activityFailed`
221
+ * (NOT a thrown JS error that would crash the CLI). manual class
222
+ * prevents R0 from auto-retrying.
223
+ * - hostExecutors / reconcilers / loadEffectInput are wired so the
224
+ * recovery phase can settle dangling side-effects via reconciler.
225
+ *
226
+ * Out of scope for R1: daemon-startup scan, watcher rebuild, real worker
227
+ * reattach, dashboard surface.
228
+ */
229
+ async function cmdWorkflowResume(rest) {
230
+ const runId = positionals(rest)[0];
231
+ if (!runId) {
232
+ console.error('用法: botmux workflow resume <runId>');
233
+ process.exit(1);
234
+ }
235
+ const runsDir = getRunsDir();
236
+ const dir = runDir(runId, runsDir);
237
+ const workflowJsonPath = join(dir, 'workflow.json');
238
+ let defRaw;
239
+ try {
240
+ defRaw = await fs.readFile(workflowJsonPath, 'utf-8');
241
+ }
242
+ catch (err) {
243
+ if (err.code === 'ENOENT') {
244
+ console.error(`找不到 runDir 的 workflow.json:${workflowJsonPath}`);
245
+ console.error(`(runsDir=${runsDir};用 BOTMUX_WORKFLOW_RUNS_DIR 覆盖)`);
246
+ }
247
+ else {
248
+ console.error(`读取 ${workflowJsonPath} 失败:${err.message}`);
249
+ }
250
+ process.exit(1);
251
+ }
252
+ let def;
253
+ try {
254
+ def = parseWorkflowDefinition(JSON.parse(defRaw));
255
+ }
256
+ catch (err) {
257
+ console.error(`解析 ${workflowJsonPath} 失败:${err.message}`);
258
+ process.exit(1);
259
+ return;
260
+ }
261
+ // Same as run: load bots so feishu host executors can resolve larkAppId.
262
+ try {
263
+ const { registerBot, loadBotConfigs } = await import('../bot-registry.js');
264
+ for (const cfg of loadBotConfigs())
265
+ registerBot(cfg);
266
+ }
267
+ catch {
268
+ // bots.json missing/invalid is fine — workflows that don't touch IM
269
+ // still resume; IM-touching steps will surface a clear error.
270
+ }
271
+ const log = new EventLog(runId, runsDir);
272
+ const events = await log.readAll();
273
+ if (events.length === 0) {
274
+ console.error(`runId=${runId} 没找到任何事件 (runsDir=${runsDir})`);
275
+ process.exit(1);
276
+ }
277
+ const { replay } = await import('../workflows/events/replay.js');
278
+ const initialSnap = replay(events);
279
+ console.log(`workflow=${def.workflowId} runId=${runId}`);
280
+ console.log(`runsDir=${runsDir}`);
281
+ // ── Terminal short-circuit ────────────────────────────────────────────
282
+ // Per codex review: replay first; if the run already finished, print
283
+ // summary and DON'T enter runLoop (no new events written).
284
+ if (initialSnap.run.status === 'succeeded' ||
285
+ initialSnap.run.status === 'failed' ||
286
+ initialSnap.run.status === 'cancelled') {
287
+ console.log(`\nrun.status=${initialSnap.run.status} (terminal — nothing to resume)`);
288
+ console.log(`events: ${initialSnap.lastSeq}`);
289
+ if (initialSnap.run.output) {
290
+ console.log(`output: ${initialSnap.run.output.outputHash}`);
291
+ }
292
+ if (initialSnap.run.status !== 'succeeded') {
293
+ process.exit(1);
294
+ }
295
+ return;
296
+ }
297
+ const spawnSubagent = async (input) => ({
298
+ kind: 'failure',
299
+ errorCode: 'WorkerCrashed',
300
+ errorClass: 'manual',
301
+ errorMessage: `subagent '${input.botName}' (node=${input.nodeId}, activity=${input.activityId}) ` +
302
+ `is not resumable via 'botmux workflow resume' — CLI does not spawn workers. ` +
303
+ `Use IM /workflow run for full execution, or restart the run.`,
304
+ });
305
+ const ctx = {
306
+ log,
307
+ def: def,
308
+ spawnSubagent,
309
+ hostExecutors: createDefaultHostExecutorRegistry(),
310
+ reconcilers: createDefaultProviderReconcilers(),
311
+ loadEffectInput: (activityId, attemptId) => loadEffectInputSidecar(log, activityId, attemptId),
312
+ };
313
+ const result = await runLoop(ctx, { maxTicks: 200 });
314
+ console.log(`\nloop stopped: ${result.reason} after ${result.ticks} tick(s)`);
315
+ console.log(`run.status=${result.lastSnapshot.run.status}`);
316
+ console.log(`events: ${result.lastSnapshot.lastSeq}`);
317
+ if (result.reason === 'awaiting-wait') {
318
+ console.log(`awaiting-wait on: ${result.lastSnapshot.danglingWaits.join(', ')}`);
319
+ console.log(`(CLI resume 不发卡;从 IM 用 /workflow run 进的话审批入口在那边)`);
320
+ }
321
+ if (result.reason === 'no-progress') {
322
+ if (result.lastSnapshot.danglingEffectAttempted.length > 0) {
323
+ console.log(`dangling effects: ${result.lastSnapshot.danglingEffectAttempted.join(', ')}`);
324
+ }
325
+ const danglingNonEffect = result.lastSnapshot.danglingActivities.filter((a) => !result.lastSnapshot.danglingEffectAttempted.includes(a));
326
+ if (danglingNonEffect.length > 0) {
327
+ console.log(`dangling activities (non-effect): ${danglingNonEffect.join(', ')}`);
328
+ }
329
+ }
330
+ if (result.reason === 'terminal' && result.lastSnapshot.run.output) {
331
+ console.log(`output: ${result.lastSnapshot.run.output.outputHash}`);
332
+ }
333
+ // Non-zero exit when the run did not resolve to a clean terminal/awaiting.
334
+ if (result.reason !== 'terminal' &&
335
+ result.reason !== 'awaiting-wait') {
336
+ process.exit(1);
337
+ }
338
+ if (result.reason === 'terminal' && result.lastSnapshot.run.status !== 'succeeded') {
339
+ process.exit(1);
340
+ }
341
+ }
342
+ // ─── cancel ───────────────────────────────────────────────────────────────
343
+ async function cmdWorkflowCancel(rest) {
344
+ const runId = positionals(rest)[0];
345
+ if (!runId) {
346
+ console.error('用法: botmux workflow cancel <runId> [--reason <text>]');
347
+ process.exit(1);
348
+ }
349
+ const reason = argValue(rest, '--reason') ?? 'cancelled via botmux workflow cancel';
350
+ const runsDir = getRunsDir();
351
+ const log = new EventLog(runId, runsDir);
352
+ const def = await loadRunWorkflowDefinition(runId, runsDir);
353
+ let snapshot = replay(await readExistingRunEvents(log, runsDir, runId));
354
+ console.log(`workflow=${def.workflowId} runId=${runId}`);
355
+ console.log(`runsDir=${runsDir}`);
356
+ if (isTerminalRunStatus(snapshot.run.status)) {
357
+ console.log(`\nrun.status=${snapshot.run.status} (terminal — nothing to cancel)`);
358
+ console.log(`events: ${snapshot.lastSeq}`);
359
+ return;
360
+ }
361
+ const ctx = workflowCliRuntimeContext(log, def, cliResumeSpawnSubagent);
362
+ const result = await cancelWorkflowRun({
363
+ ctx,
364
+ reason,
365
+ by: 'cli',
366
+ actor: 'human',
367
+ maxTicks: 200,
368
+ });
369
+ snapshot = result.snapshot;
370
+ if (result.cancelEventId) {
371
+ console.log(result.cancelAlreadyRequested
372
+ ? `cancel already requested: ${result.cancelEventId}`
373
+ : `cancelRequested: ${result.cancelEventId}`);
374
+ }
375
+ console.log(`\nloop stopped: ${result.loopResult?.reason ?? 'terminal'} ` +
376
+ `after ${result.loopResult?.ticks ?? 0} tick(s)`);
377
+ console.log(`run.status=${snapshot.run.status}`);
378
+ console.log(`events: ${snapshot.lastSeq}`);
379
+ if (snapshot.danglingCancels.length > 0) {
380
+ console.log(`dangling cancels: ${snapshot.danglingCancels.join(', ')}`);
381
+ }
382
+ if (snapshot.danglingEffectAttempted.length > 0) {
383
+ console.log(`dangling effects: ${snapshot.danglingEffectAttempted.join(', ')}`);
384
+ }
385
+ if (snapshot.danglingWaits.length > 0) {
386
+ console.log(`dangling waits: ${snapshot.danglingWaits.join(', ')}`);
387
+ }
388
+ if (snapshot.run.status !== 'cancelled') {
389
+ process.exit(1);
390
+ }
391
+ }
392
+ function workflowCliRuntimeContext(log, def, spawnSubagent) {
393
+ return {
394
+ log,
395
+ def,
396
+ spawnSubagent,
397
+ hostExecutors: createDefaultHostExecutorRegistry(),
398
+ reconcilers: createDefaultProviderReconcilers(),
399
+ loadEffectInput: (activityId, attemptId) => loadEffectInputSidecar(log, activityId, attemptId),
400
+ };
401
+ }
402
+ const cliResumeSpawnSubagent = async (input) => ({
403
+ kind: 'failure',
404
+ errorCode: 'WorkerCrashed',
405
+ errorClass: 'manual',
406
+ errorMessage: `subagent '${input.botName}' (node=${input.nodeId}, activity=${input.activityId}) ` +
407
+ `is not resumable via 'botmux workflow resume' — CLI does not spawn workers. ` +
408
+ `Use IM /workflow run for full execution, or restart the run.`,
409
+ });
410
+ async function loadRunWorkflowDefinition(runId, runsDir = getRunsDir()) {
411
+ const workflowJsonPath = join(runDir(runId, runsDir), 'workflow.json');
412
+ let defRaw;
413
+ try {
414
+ defRaw = await fs.readFile(workflowJsonPath, 'utf-8');
415
+ }
416
+ catch (err) {
417
+ if (err.code === 'ENOENT') {
418
+ console.error(`找不到 runDir 的 workflow.json:${workflowJsonPath}`);
419
+ console.error(`(runsDir=${runsDir};用 BOTMUX_WORKFLOW_RUNS_DIR 覆盖)`);
420
+ }
421
+ else {
422
+ console.error(`读取 ${workflowJsonPath} 失败:${err.message}`);
423
+ }
424
+ process.exit(1);
425
+ }
426
+ try {
427
+ return parseWorkflowDefinition(JSON.parse(defRaw));
428
+ }
429
+ catch (err) {
430
+ console.error(`解析 ${workflowJsonPath} 失败:${err.message}`);
431
+ process.exit(1);
432
+ }
433
+ }
434
+ async function readExistingRunEvents(log, runsDir, runId) {
435
+ const events = await log.readAll();
436
+ if (events.length === 0) {
437
+ console.error(`runId=${runId} 没找到任何事件 (runsDir=${runsDir})`);
438
+ process.exit(1);
439
+ }
440
+ return events;
441
+ }
442
+ /**
443
+ * Parse CLI args into raw param inputs. Each `--param key=value` carries a
444
+ * plain string (type coercion happens in `coerceWorkflowParams` against the
445
+ * workflow's `params` schema); each `--param-json key=<json>` carries a
446
+ * parsed JSON value, which is the only way to thread `object` / `array`
447
+ * params (or numbers / booleans you'd rather not stringify) into a run.
448
+ *
449
+ * Both flags accept the `--flag value` and `--flag=value` forms.
450
+ */
451
+ function collectRawParams(rest) {
452
+ const out = {};
453
+ const ingestStringKV = (kv) => {
454
+ const eq = kv.indexOf('=');
455
+ if (eq <= 0) {
456
+ console.error(`--param 期望 key=value,收到 "${kv}"`);
457
+ process.exit(1);
458
+ }
459
+ out[kv.slice(0, eq)] = { kind: 'string', value: kv.slice(eq + 1) };
460
+ };
461
+ const ingestJsonKV = (kv) => {
462
+ const eq = kv.indexOf('=');
463
+ if (eq <= 0) {
464
+ console.error(`--param-json 期望 key=<json>,收到 "${kv}"`);
465
+ process.exit(1);
466
+ }
467
+ const key = kv.slice(0, eq);
468
+ const jsonText = kv.slice(eq + 1);
469
+ try {
470
+ out[key] = { kind: 'json', value: JSON.parse(jsonText) };
471
+ }
472
+ catch (err) {
473
+ console.error(`--param-json ${key} 的 JSON 解析失败:` +
474
+ (err instanceof Error ? err.message : String(err)));
475
+ process.exit(1);
476
+ }
477
+ };
478
+ for (let i = 0; i < rest.length; i++) {
479
+ if (rest[i] === '--param' && i + 1 < rest.length) {
480
+ ingestStringKV(rest[i + 1]);
481
+ i++;
482
+ }
483
+ else if (rest[i]?.startsWith('--param=')) {
484
+ ingestStringKV(rest[i].slice('--param='.length));
485
+ }
486
+ else if (rest[i] === '--param-json' && i + 1 < rest.length) {
487
+ ingestJsonKV(rest[i + 1]);
488
+ i++;
489
+ }
490
+ else if (rest[i]?.startsWith('--param-json=')) {
491
+ ingestJsonKV(rest[i].slice('--param-json='.length));
492
+ }
493
+ }
494
+ return out;
495
+ }
496
+ // ─── validate ─────────────────────────────────────────────────────────────
497
+ async function cmdWorkflowValidate(rest) {
498
+ const path = positionals(rest)[0];
499
+ if (!path) {
500
+ console.error('用法: botmux workflow validate <path>');
501
+ process.exit(1);
502
+ }
503
+ let rawText;
504
+ try {
505
+ rawText = await fs.readFile(path, 'utf-8');
506
+ }
507
+ catch (err) {
508
+ console.error(`读取 ${path} 失败:${err.message}`);
509
+ process.exit(1);
510
+ }
511
+ let raw;
512
+ try {
513
+ raw = JSON.parse(rawText);
514
+ }
515
+ catch (err) {
516
+ console.error(`解析 JSON 失败:${err.message}`);
517
+ process.exit(1);
518
+ }
519
+ try {
520
+ const def = parseWorkflowDefinition(raw);
521
+ console.log(`workflow valid: ${def.workflowId} ` +
522
+ `(version=${def.version}, nodes=${Object.keys(def.nodes).length})`);
523
+ }
524
+ catch (err) {
525
+ console.error(`workflow invalid: ${path}`);
526
+ if (err instanceof ZodError) {
527
+ for (const issue of err.issues) {
528
+ const p = issue.path.length ? issue.path.join('.') : '<root>';
529
+ console.error(`- ${p}: ${issue.message}`);
530
+ }
531
+ }
532
+ else {
533
+ console.error(`- ${err instanceof Error ? err.message : String(err)}`);
534
+ }
535
+ process.exit(1);
536
+ }
537
+ }
538
+ // ─── ls ───────────────────────────────────────────────────────────────────
539
+ /**
540
+ * `botmux workflow ls` — operator surface for "what's running on disk?"
541
+ *
542
+ * Read-only: walks runsDir/<runId>/events.ndjson, replays each, projects a
543
+ * row. By default lists only non-terminal runs (the typical operator
544
+ * question: "what's still hot?"). Terminal runs are useful for triage
545
+ * and stay one `--all` flag away.
546
+ *
547
+ * Output:
548
+ * - default: aligned table on stdout. Column set tuned to fit ~120
549
+ * cols: runId | workflowId | status | lastSeq | dEf/dAct/dWait | updatedAt
550
+ * - `--wide`: appends failedNodeId / chatId / larkAppId.
551
+ * - `--json`: one JSON object per line (machine-parseable).
552
+ *
553
+ * Filters:
554
+ * - `--all`: include terminal (succeeded/failed/cancelled).
555
+ * - `--status running,failed`: comma-separated set; overrides `--all`.
556
+ */
557
+ async function cmdWorkflowLs(rest) {
558
+ const all = rest.includes('--all');
559
+ const wide = rest.includes('--wide');
560
+ const json = rest.includes('--json');
561
+ const statusFilter = argValue(rest, '--status');
562
+ const wantStatuses = statusFilter
563
+ ? new Set(statusFilter.split(',').map((s) => s.trim()).filter(Boolean))
564
+ : undefined;
565
+ const runsDir = getRunsDir();
566
+ let rows;
567
+ try {
568
+ rows = await listRuns(runsDir, {
569
+ all,
570
+ statuses: wantStatuses,
571
+ // chat-binding columns are only printed in --wide or --json; skip the
572
+ // extra fs op otherwise.
573
+ includeBinding: wide || json,
574
+ });
575
+ }
576
+ catch (err) {
577
+ console.error(`读取 ${runsDir} 失败:${err.message}`);
578
+ process.exit(1);
579
+ }
580
+ if (json) {
581
+ for (const r of rows)
582
+ console.log(JSON.stringify(r));
583
+ return;
584
+ }
585
+ if (rows.length === 0) {
586
+ console.log('(no runs match)');
587
+ return;
588
+ }
589
+ const headers = wide
590
+ ? ['RUN_ID', 'WORKFLOW', 'STATUS', 'LAST_SEQ', 'dEf/dAct/dWait', 'UPDATED', 'FAILED_NODE', 'CHAT_ID', 'LARK_APP']
591
+ : ['RUN_ID', 'WORKFLOW', 'STATUS', 'LAST_SEQ', 'dEf/dAct/dWait', 'UPDATED'];
592
+ const rowCells = rows.map((r) => {
593
+ const dangling = `${r.dEf}/${r.dAct}/${r.dWait}`;
594
+ const updated = new Date(r.updatedAt).toISOString().slice(0, 19).replace('T', ' ');
595
+ const base = [r.runId, r.workflowId, r.status, String(r.lastSeq), dangling, updated];
596
+ if (!wide)
597
+ return base;
598
+ return [...base, r.failedNodeId ?? '-', r.chatId ?? '-', r.larkAppId ?? '-'];
599
+ });
600
+ const widths = headers.map((h, i) => Math.max(h.length, ...rowCells.map((row) => row[i].length)));
601
+ const pad = (s, w) => s + ' '.repeat(w - s.length);
602
+ console.log(headers.map((h, i) => pad(h, widths[i])).join(' '));
603
+ for (const cells of rowCells) {
604
+ console.log(cells.map((c, i) => pad(c, widths[i])).join(' '));
605
+ }
606
+ }
607
+ // ─── tail ─────────────────────────────────────────────────────────────────
608
+ /**
609
+ * `botmux workflow tail <runId>` — operator surface for "show me the
610
+ * event stream of this run".
611
+ *
612
+ * Default mode is history-only (codex review 2026-05-20): print every
613
+ * event from `--from` (default 1) and exit. CLI defaults that hang are
614
+ * a footgun for scripts and tests — `--follow` is the opt-in that turns
615
+ * on the watch loop.
616
+ *
617
+ * Follow strategy: poll `fs.stat` on the events.ndjson file at 200ms
618
+ * cadence and incrementally read new bytes from the recorded offset.
619
+ * NDJSON makes the boundary handling trivial — we only emit on `\n`.
620
+ * Truncation / rotation isn't supported here (events.ndjson is
621
+ * append-only by design); if the file shrinks we surface a warning.
622
+ */
623
+ async function cmdWorkflowTail(rest) {
624
+ const runId = positionals(rest)[0];
625
+ if (!runId) {
626
+ console.error('用法: botmux workflow tail <runId> [--from <seq>] [--follow] [--json]');
627
+ process.exit(1);
628
+ }
629
+ const fromArg = argValue(rest, '--from');
630
+ const fromSeq = fromArg ? Number(fromArg) : 1;
631
+ if (!Number.isFinite(fromSeq) || fromSeq < 1) {
632
+ console.error(`--from 必须是 >=1 的整数,收到 "${fromArg}"`);
633
+ process.exit(1);
634
+ }
635
+ const follow = rest.includes('--follow') || rest.includes('-f');
636
+ const json = rest.includes('--json');
637
+ const runsDir = getRunsDir();
638
+ const eventsPath = join(runsDir, runId, 'events.ndjson');
639
+ const log = new EventLog(runId, runsDir);
640
+ // Capture the watch starting offset BEFORE readAll so that any event
641
+ // appended between readAll and the first stat is still picked up by
642
+ // the watch loop (lastSeq dedups any overlap). Codex review (O1
643
+ // medium #1): if we stat AFTER readAll, a race-window event lands
644
+ // past readAll's view but inside the offset, and follow silently
645
+ // skips it forever.
646
+ let followOffset = 0;
647
+ if (follow) {
648
+ try {
649
+ followOffset = (await fs.stat(eventsPath)).size;
650
+ }
651
+ catch {
652
+ // events.ndjson must exist if readAll below succeeds; defensive
653
+ // fallback keeps offset 0 so the watch re-reads the whole file
654
+ // and lastSeq still dedups.
655
+ followOffset = 0;
656
+ }
657
+ }
658
+ let initial;
659
+ try {
660
+ initial = await log.readAll();
661
+ }
662
+ catch (err) {
663
+ console.error(`读取 ${eventsPath} 失败:${err.message}`);
664
+ process.exit(1);
665
+ }
666
+ if (initial.length === 0) {
667
+ console.error(`runId=${runId} 没找到任何事件 (runsDir=${runsDir})`);
668
+ process.exit(1);
669
+ }
670
+ for (const ev of initial) {
671
+ const seq = eventSeqFromId(ev.eventId);
672
+ if (seq < fromSeq)
673
+ continue;
674
+ printEventLine(ev, json);
675
+ }
676
+ if (!follow)
677
+ return;
678
+ // Watch loop. Resume from `followOffset` (captured pre-readAll); parse
679
+ // incrementally by line. Stop on Ctrl-C; until then we never resolve.
680
+ let offset = followOffset;
681
+ let lastSeq = eventSeqFromId(initial[initial.length - 1].eventId);
682
+ let buffer = '';
683
+ process.on('SIGINT', () => process.exit(0));
684
+ while (true) {
685
+ await new Promise((r) => setTimeout(r, 200));
686
+ const stat = await fs.stat(eventsPath).catch(() => null);
687
+ if (!stat)
688
+ continue;
689
+ if (stat.size < offset) {
690
+ console.error(`(events.ndjson 大小回退 ${offset} → ${stat.size},停止 tail)`);
691
+ return;
692
+ }
693
+ if (stat.size === offset)
694
+ continue;
695
+ const fd = await fs.open(eventsPath, 'r');
696
+ try {
697
+ const chunk = Buffer.alloc(stat.size - offset);
698
+ await fd.read(chunk, 0, chunk.length, offset);
699
+ offset = stat.size;
700
+ buffer += chunk.toString('utf-8');
701
+ }
702
+ finally {
703
+ await fd.close();
704
+ }
705
+ let nl;
706
+ while ((nl = buffer.indexOf('\n')) >= 0) {
707
+ const line = buffer.slice(0, nl);
708
+ buffer = buffer.slice(nl + 1);
709
+ if (!line.trim())
710
+ continue;
711
+ let ev;
712
+ try {
713
+ ev = JSON.parse(line);
714
+ }
715
+ catch {
716
+ continue;
717
+ }
718
+ if (typeof ev?.eventId !== 'string')
719
+ continue;
720
+ const seq = eventSeqFromId(ev.eventId);
721
+ if (seq <= lastSeq)
722
+ continue;
723
+ lastSeq = seq;
724
+ if (seq < fromSeq)
725
+ continue;
726
+ printEventLine(ev, json);
727
+ }
728
+ }
729
+ }
730
+ function printEventLine(ev, json) {
731
+ if (json) {
732
+ console.log(JSON.stringify(ev));
733
+ return;
734
+ }
735
+ const e = ev;
736
+ const seq = String(eventSeqFromId(e.eventId)).padStart(4);
737
+ const type = e.type.padEnd(22);
738
+ const ctx = extractEventContext(e.payload);
739
+ const parts = [];
740
+ if (ctx.nodeId)
741
+ parts.push('node=' + ctx.nodeId);
742
+ if (ctx.activityId)
743
+ parts.push('act=' + ctx.activityId);
744
+ const where = parts.join(' ');
745
+ const err = ctx.errorCode ? ' err=' + ctx.errorCode : '';
746
+ console.log(seq + ' ' + type + ' ' + where + err);
747
+ }
748
+ // ─── show ─────────────────────────────────────────────────────────────────
749
+ async function cmdWorkflowShow(rest) {
750
+ const runId = positionals(rest)[0];
751
+ if (!runId) {
752
+ console.error('用法: botmux workflow show <runId>');
753
+ process.exit(1);
754
+ }
755
+ const { replay } = await import('../workflows/events/replay.js');
756
+ const log = new EventLog(runId, getRunsDir());
757
+ const events = await log.readAll();
758
+ if (events.length === 0) {
759
+ console.error(`runId=${runId} 没找到任何事件 (runsDir=${getRunsDir()})`);
760
+ process.exit(1);
761
+ }
762
+ const snap = replay(events);
763
+ console.log(JSON.stringify({
764
+ runId,
765
+ workflowId: snap.run.workflowId,
766
+ revisionId: snap.run.revisionId,
767
+ status: snap.run.status,
768
+ lastSeq: snap.lastSeq,
769
+ nodes: [...snap.nodes.entries()].map(([id, n]) => ({
770
+ id,
771
+ status: n.status,
772
+ retryCount: n.retryCount,
773
+ })),
774
+ danglingActivities: snap.danglingActivities,
775
+ danglingWaits: snap.danglingWaits,
776
+ }, null, 2));
777
+ // `parseWorkflowDefinition` re-exported here only so the bundler keeps it
778
+ // alongside loader (some smoke tests dlopen the helpers directly).
779
+ void parseWorkflowDefinition;
780
+ }
781
+ //# sourceMappingURL=workflow.js.map