botmux 2.93.0 → 2.94.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 (227) hide show
  1. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  2. package/dist/adapters/cli/claude-code.js +6 -1
  3. package/dist/adapters/cli/claude-code.js.map +1 -1
  4. package/dist/bot-registry.d.ts +10 -1
  5. package/dist/bot-registry.d.ts.map +1 -1
  6. package/dist/bot-registry.js +2 -0
  7. package/dist/bot-registry.js.map +1 -1
  8. package/dist/cli/dashboard-endpoint.d.ts +1 -1
  9. package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
  10. package/dist/cli/dashboard-endpoint.js +3 -0
  11. package/dist/cli/dashboard-endpoint.js.map +1 -1
  12. package/dist/cli/workflow.d.ts.map +1 -1
  13. package/dist/cli/workflow.js +11 -4
  14. package/dist/cli/workflow.js.map +1 -1
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +133 -1
  17. package/dist/cli.js.map +1 -1
  18. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  19. package/dist/core/dashboard-ipc-server.js +3 -0
  20. package/dist/core/dashboard-ipc-server.js.map +1 -1
  21. package/dist/core/inherit-peer.d.ts +5 -0
  22. package/dist/core/inherit-peer.d.ts.map +1 -1
  23. package/dist/core/inherit-peer.js +3 -0
  24. package/dist/core/inherit-peer.js.map +1 -1
  25. package/dist/core/trigger-session.d.ts +10 -0
  26. package/dist/core/trigger-session.d.ts.map +1 -1
  27. package/dist/core/trigger-session.js +22 -2
  28. package/dist/core/trigger-session.js.map +1 -1
  29. package/dist/core/worker-pool.d.ts +6 -0
  30. package/dist/core/worker-pool.d.ts.map +1 -1
  31. package/dist/core/worker-pool.js +49 -6
  32. package/dist/core/worker-pool.js.map +1 -1
  33. package/dist/daemon.d.ts.map +1 -1
  34. package/dist/daemon.js +297 -13
  35. package/dist/daemon.js.map +1 -1
  36. package/dist/dashboard/auth.d.ts.map +1 -1
  37. package/dist/dashboard/auth.js +7 -1
  38. package/dist/dashboard/auth.js.map +1 -1
  39. package/dist/dashboard/bot-payload.d.ts +1 -0
  40. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  41. package/dist/dashboard/bot-payload.js +1 -0
  42. package/dist/dashboard/bot-payload.js.map +1 -1
  43. package/dist/dashboard/federation-spoke-api.d.ts +31 -0
  44. package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
  45. package/dist/dashboard/federation-spoke-api.js +36 -14
  46. package/dist/dashboard/federation-spoke-api.js.map +1 -1
  47. package/dist/dashboard/v3-runs-api.d.ts +26 -0
  48. package/dist/dashboard/v3-runs-api.d.ts.map +1 -0
  49. package/dist/dashboard/v3-runs-api.js +67 -0
  50. package/dist/dashboard/v3-runs-api.js.map +1 -0
  51. package/dist/dashboard/web/app.js +21 -9
  52. package/dist/dashboard/web/app.js.map +1 -1
  53. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  54. package/dist/dashboard/web/bot-defaults.js +24 -1
  55. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  56. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  57. package/dist/dashboard/web/i18n.js +12 -4
  58. package/dist/dashboard/web/i18n.js.map +1 -1
  59. package/dist/dashboard/web/legacy-workflow-link.d.ts +2 -0
  60. package/dist/dashboard/web/legacy-workflow-link.d.ts.map +1 -0
  61. package/dist/dashboard/web/legacy-workflow-link.js +12 -0
  62. package/dist/dashboard/web/legacy-workflow-link.js.map +1 -0
  63. package/dist/dashboard/web/v3-terminal.d.ts +11 -0
  64. package/dist/dashboard/web/v3-terminal.d.ts.map +1 -0
  65. package/dist/dashboard/web/v3-terminal.js +143 -0
  66. package/dist/dashboard/web/v3-terminal.js.map +1 -0
  67. package/dist/dashboard/web/v3.d.ts +2 -0
  68. package/dist/dashboard/web/v3.d.ts.map +1 -0
  69. package/dist/dashboard/web/v3.js +564 -0
  70. package/dist/dashboard/web/v3.js.map +1 -0
  71. package/dist/dashboard/web/workflows.d.ts.map +1 -1
  72. package/dist/dashboard/web/workflows.js +12 -8
  73. package/dist/dashboard/web/workflows.js.map +1 -1
  74. package/dist/dashboard-web/app.js +787 -699
  75. package/dist/dashboard-web/index.html +1 -1
  76. package/dist/dashboard-web/style.css +223 -0
  77. package/dist/dashboard.js +61 -40
  78. package/dist/dashboard.js.map +1 -1
  79. package/dist/i18n/en.d.ts.map +1 -1
  80. package/dist/i18n/en.js +6 -3
  81. package/dist/i18n/en.js.map +1 -1
  82. package/dist/i18n/zh.d.ts.map +1 -1
  83. package/dist/i18n/zh.js +6 -3
  84. package/dist/i18n/zh.js.map +1 -1
  85. package/dist/im/lark/card-handler.d.ts +12 -0
  86. package/dist/im/lark/card-handler.d.ts.map +1 -1
  87. package/dist/im/lark/card-handler.js +52 -2
  88. package/dist/im/lark/card-handler.js.map +1 -1
  89. package/dist/im/lark/doc-comment.d.ts +20 -0
  90. package/dist/im/lark/doc-comment.d.ts.map +1 -1
  91. package/dist/im/lark/doc-comment.js +23 -0
  92. package/dist/im/lark/doc-comment.js.map +1 -1
  93. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  94. package/dist/im/lark/event-dispatcher.js +13 -9
  95. package/dist/im/lark/event-dispatcher.js.map +1 -1
  96. package/dist/im/lark/v3-blocked-card-handler.d.ts +28 -0
  97. package/dist/im/lark/v3-blocked-card-handler.d.ts.map +1 -0
  98. package/dist/im/lark/v3-blocked-card-handler.js +134 -0
  99. package/dist/im/lark/v3-blocked-card-handler.js.map +1 -0
  100. package/dist/im/lark/v3-blocked-card.d.ts +75 -0
  101. package/dist/im/lark/v3-blocked-card.d.ts.map +1 -0
  102. package/dist/im/lark/v3-blocked-card.js +206 -0
  103. package/dist/im/lark/v3-blocked-card.js.map +1 -0
  104. package/dist/im/lark/v3-gate-card-handler.d.ts +31 -0
  105. package/dist/im/lark/v3-gate-card-handler.d.ts.map +1 -0
  106. package/dist/im/lark/v3-gate-card-handler.js +110 -0
  107. package/dist/im/lark/v3-gate-card-handler.js.map +1 -0
  108. package/dist/im/lark/v3-gate-card.d.ts +47 -0
  109. package/dist/im/lark/v3-gate-card.d.ts.map +1 -0
  110. package/dist/im/lark/v3-gate-card.js +119 -0
  111. package/dist/im/lark/v3-gate-card.js.map +1 -0
  112. package/dist/im/lark/v3-loop-grant-card-handler.d.ts +28 -0
  113. package/dist/im/lark/v3-loop-grant-card-handler.d.ts.map +1 -0
  114. package/dist/im/lark/v3-loop-grant-card-handler.js +90 -0
  115. package/dist/im/lark/v3-loop-grant-card-handler.js.map +1 -0
  116. package/dist/im/lark/v3-loop-grant-card.d.ts +46 -0
  117. package/dist/im/lark/v3-loop-grant-card.d.ts.map +1 -0
  118. package/dist/im/lark/v3-loop-grant-card.js +119 -0
  119. package/dist/im/lark/v3-loop-grant-card.js.map +1 -0
  120. package/dist/im/lark/v3-revisit-grant-card-handler.d.ts +29 -0
  121. package/dist/im/lark/v3-revisit-grant-card-handler.d.ts.map +1 -0
  122. package/dist/im/lark/v3-revisit-grant-card-handler.js +96 -0
  123. package/dist/im/lark/v3-revisit-grant-card-handler.js.map +1 -0
  124. package/dist/im/lark/v3-revisit-grant-card.d.ts +52 -0
  125. package/dist/im/lark/v3-revisit-grant-card.d.ts.map +1 -0
  126. package/dist/im/lark/v3-revisit-grant-card.js +123 -0
  127. package/dist/im/lark/v3-revisit-grant-card.js.map +1 -0
  128. package/dist/im/lark/workflow-cards.d.ts.map +1 -1
  129. package/dist/im/lark/workflow-cards.js +5 -1
  130. package/dist/im/lark/workflow-cards.js.map +1 -1
  131. package/dist/im/lark/workflow-progress-card.d.ts +1 -1
  132. package/dist/im/lark/workflow-progress-card.js +1 -1
  133. package/dist/im/lark/workflow-slash-command.d.ts +28 -0
  134. package/dist/im/lark/workflow-slash-command.d.ts.map +1 -1
  135. package/dist/im/lark/workflow-slash-command.js +61 -5
  136. package/dist/im/lark/workflow-slash-command.js.map +1 -1
  137. package/dist/platform/bind.d.ts.map +1 -1
  138. package/dist/platform/bind.js +17 -3
  139. package/dist/platform/bind.js.map +1 -1
  140. package/dist/services/card-prefs-store.d.ts +4 -0
  141. package/dist/services/card-prefs-store.d.ts.map +1 -1
  142. package/dist/services/card-prefs-store.js +19 -0
  143. package/dist/services/card-prefs-store.js.map +1 -1
  144. package/dist/services/group-creator.d.ts.map +1 -1
  145. package/dist/services/group-creator.js +18 -2
  146. package/dist/services/group-creator.js.map +1 -1
  147. package/dist/skills/definitions.d.ts.map +1 -1
  148. package/dist/skills/definitions.js +208 -4
  149. package/dist/skills/definitions.js.map +1 -1
  150. package/dist/worker.js +79 -13
  151. package/dist/worker.js.map +1 -1
  152. package/dist/workflows/definition.d.ts +80 -80
  153. package/dist/workflows/events/payloads.d.ts +4 -4
  154. package/dist/workflows/events/schema.d.ts +16 -16
  155. package/dist/workflows/v3/architect.d.ts +37 -0
  156. package/dist/workflows/v3/architect.d.ts.map +1 -0
  157. package/dist/workflows/v3/architect.js +226 -0
  158. package/dist/workflows/v3/architect.js.map +1 -0
  159. package/dist/workflows/v3/bot-resolve.d.ts +21 -0
  160. package/dist/workflows/v3/bot-resolve.d.ts.map +1 -0
  161. package/dist/workflows/v3/bot-resolve.js +47 -0
  162. package/dist/workflows/v3/bot-resolve.js.map +1 -0
  163. package/dist/workflows/v3/cli-run.d.ts +24 -0
  164. package/dist/workflows/v3/cli-run.d.ts.map +1 -0
  165. package/dist/workflows/v3/cli-run.js +265 -0
  166. package/dist/workflows/v3/cli-run.js.map +1 -0
  167. package/dist/workflows/v3/contract.d.ts +302 -0
  168. package/dist/workflows/v3/contract.d.ts.map +1 -0
  169. package/dist/workflows/v3/contract.js +91 -0
  170. package/dist/workflows/v3/contract.js.map +1 -0
  171. package/dist/workflows/v3/daemon-run.d.ts +311 -0
  172. package/dist/workflows/v3/daemon-run.d.ts.map +1 -0
  173. package/dist/workflows/v3/daemon-run.js +775 -0
  174. package/dist/workflows/v3/daemon-run.js.map +1 -0
  175. package/dist/workflows/v3/dag.d.ts +332 -0
  176. package/dist/workflows/v3/dag.d.ts.map +1 -0
  177. package/dist/workflows/v3/dag.js +1040 -0
  178. package/dist/workflows/v3/dag.js.map +1 -0
  179. package/dist/workflows/v3/ephemeral-pool.d.ts +29 -0
  180. package/dist/workflows/v3/ephemeral-pool.d.ts.map +1 -0
  181. package/dist/workflows/v3/ephemeral-pool.js +331 -0
  182. package/dist/workflows/v3/ephemeral-pool.js.map +1 -0
  183. package/dist/workflows/v3/grill-state.d.ts +92 -0
  184. package/dist/workflows/v3/grill-state.d.ts.map +1 -0
  185. package/dist/workflows/v3/grill-state.js +151 -0
  186. package/dist/workflows/v3/grill-state.js.map +1 -0
  187. package/dist/workflows/v3/host.d.ts +88 -0
  188. package/dist/workflows/v3/host.d.ts.map +1 -0
  189. package/dist/workflows/v3/host.js +349 -0
  190. package/dist/workflows/v3/host.js.map +1 -0
  191. package/dist/workflows/v3/human-gate.d.ts +85 -0
  192. package/dist/workflows/v3/human-gate.d.ts.map +1 -0
  193. package/dist/workflows/v3/human-gate.js +153 -0
  194. package/dist/workflows/v3/human-gate.js.map +1 -0
  195. package/dist/workflows/v3/journal.d.ts +207 -0
  196. package/dist/workflows/v3/journal.d.ts.map +1 -0
  197. package/dist/workflows/v3/journal.js +74 -0
  198. package/dist/workflows/v3/journal.js.map +1 -0
  199. package/dist/workflows/v3/manifest.d.ts +17 -0
  200. package/dist/workflows/v3/manifest.d.ts.map +1 -0
  201. package/dist/workflows/v3/manifest.js +266 -0
  202. package/dist/workflows/v3/manifest.js.map +1 -0
  203. package/dist/workflows/v3/ops-projection.d.ts +110 -0
  204. package/dist/workflows/v3/ops-projection.d.ts.map +1 -0
  205. package/dist/workflows/v3/ops-projection.js +0 -0
  206. package/dist/workflows/v3/ops-projection.js.map +1 -0
  207. package/dist/workflows/v3/orchestrator.d.ts +168 -0
  208. package/dist/workflows/v3/orchestrator.d.ts.map +1 -0
  209. package/dist/workflows/v3/orchestrator.js +417 -0
  210. package/dist/workflows/v3/orchestrator.js.map +1 -0
  211. package/dist/workflows/v3/runtime.d.ts +193 -0
  212. package/dist/workflows/v3/runtime.d.ts.map +1 -0
  213. package/dist/workflows/v3/runtime.js +1284 -0
  214. package/dist/workflows/v3/runtime.js.map +1 -0
  215. package/dist/workflows/v3/spec.d.ts +32 -0
  216. package/dist/workflows/v3/spec.d.ts.map +1 -0
  217. package/dist/workflows/v3/spec.js +173 -0
  218. package/dist/workflows/v3/spec.js.map +1 -0
  219. package/dist/workflows/v3/state.d.ts +74 -0
  220. package/dist/workflows/v3/state.d.ts.map +1 -0
  221. package/dist/workflows/v3/state.js +353 -0
  222. package/dist/workflows/v3/state.js.map +1 -0
  223. package/package.json +1 -1
  224. package/dist/dashboard/web/workflow-catalog.d.ts +0 -2
  225. package/dist/dashboard/web/workflow-catalog.d.ts.map +0 -1
  226. package/dist/dashboard/web/workflow-catalog.js +0 -323
  227. package/dist/dashboard/web/workflow-catalog.js.map +0 -1
package/dist/daemon.js CHANGED
@@ -40,7 +40,7 @@ import { buildQuotaExhaustedCard, buildRepoSelectCard, buildStreamingCard, getCl
40
40
  import { RECEIVED_REACTION_EMOJI_TYPE } from './core/pending-response.js';
41
41
  import { t as tr, botLocale, localeForBot } from './i18n/index.js';
42
42
  import { createCliAdapterSync } from './adapters/cli/registry.js';
43
- import { initWorkerPool, setActiveSessionsRegistry, forkWorker, killWorker, reapOrphanWorkers, scheduleCardPatch, setCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, ensureCliEnv, writableTerminalLinkFor, } from './core/worker-pool.js';
43
+ import { initWorkerPool, setActiveSessionsRegistry, forkWorker, killWorker, reapOrphanWorkers, scheduleCardPatch, setCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, ensureCliEnv, sweepGlobalBotmuxSkills, writableTerminalLinkFor, } from './core/worker-pool.js';
44
44
  import { ipcRoute, jsonRes, readJsonBody, setBotName, setLarkAppId, startIpcServer, setWorkflowRunner } from './core/dashboard-ipc-server.js';
45
45
  import { saveFrozenCards } from './services/frozen-card-store.js';
46
46
  import { DAEMON_COMMANDS, SESSIONLESS_DAEMON_COMMANDS, resolvePassthroughCommands, resolveAdapterDefaultPassthroughCommands, handleCommand, handleCardCommand, handleTermLinkCommand, parseSlashCommandInvocation, parseForceTopicInvocation } from './core/command-handler.js';
@@ -54,8 +54,18 @@ import { beginReplyTargetTurn, fallbackTurnId, resolveSessionReplyTarget, syncRe
54
54
  import { sweepOrphanSandboxes } from './adapters/backend/sandbox.js';
55
55
  import { sweepIdleWorkers, DEFAULT_MAX_LIVE_WORKERS } from './core/idle-worker-sweeper.js';
56
56
  import { handleCardAction } from './im/lark/card-handler.js';
57
- import { executeWorkflowCommand, parseWorkflowCommand, resolveBotSnapshot, } from './im/lark/workflow-slash-command.js';
57
+ import { executeWorkflowCommand, parseWorkflowCommand, parseWorkflowGrillTrigger, buildWorkflowGrillPrompt, resolveBotSnapshot, WORKFLOW_USAGE, WORKFLOW_V2_RENAME_NOTICE, } from './im/lark/workflow-slash-command.js';
58
58
  import { workflowRunDetailUrl } from './im/lark/workflow-cards.js';
59
+ import { createV3GateRunner, requestV3Retry, requestV3LoopGrant } from './workflows/v3/daemon-run.js';
60
+ import { buildV3GateCard } from './im/lark/v3-gate-card.js';
61
+ import { buildV3BlockedCard } from './im/lark/v3-blocked-card.js';
62
+ import { buildV3LoopGrantCard } from './im/lark/v3-loop-grant-card.js';
63
+ import { buildV3RevisitGrantCard } from './im/lark/v3-revisit-grant-card.js';
64
+ import { isValidRunId as isValidV3RunId } from './workflows/v3/ops-projection.js';
65
+ import { readRunChatBinding as readV3RunChatBinding, defaultBaseDir as v3DefaultBaseDir } from './workflows/v3/grill-state.js';
66
+ /** This daemon process's bot larkAppId (set in startDaemon). Used to scope v3
67
+ * humanGate cold-attach + start to runs this bot owns (codex blocker #1). */
68
+ let selfV3LarkAppId;
59
69
  import { buildWorkflowStartingCard, buildWorkflowProgressCard, buildAttemptDeeplinkEnricher, } from './im/lark/workflow-progress-card.js';
60
70
  import { EventLog as WorkflowEventLog } from './workflows/events/append.js';
61
71
  import { replay as replayWorkflow } from './workflows/events/replay.js';
@@ -1089,6 +1099,13 @@ function workflowSpawnFn() {
1089
1099
  return createDaemonSpawnFn(daemonDeps);
1090
1100
  }
1091
1101
  async function handleWorkflowCommandIfAny(content, anchor, chatId, larkAppId, initiator) {
1102
+ // 旧 `/workflow run|cancel` 软降级:在执行前**先**发改名提示,让迁移指引第一时间
1103
+ // 到达用户(codex review:先发优于后发)。从原始 content 判定而非 parse 结果,
1104
+ // 这样连 `/workflow run`(缺 id)这类 invalid legacy 也能收到提示(codex review)。
1105
+ // 仅匹配 legacy 的 run|cancel(executeWorkflowCommand 必然 handle),不误伤 /template。
1106
+ if (/^\/workflow\s+(run|cancel)\b/.test(content.trim())) {
1107
+ await sessionReply(anchor, WORKFLOW_V2_RENAME_NOTICE, 'text', larkAppId);
1108
+ }
1092
1109
  // Captured by the `onRunCreated` closure so the trailing text reply can be
1093
1110
  // suppressed when the run-level progress card already landed. Codex
1094
1111
  // round 1 medium: "single self-updating tile" promise breaks if we also
@@ -1265,6 +1282,96 @@ function fireSessionlessCommandDetached(cmd, anchor, message, larkAppId) {
1265
1282
  void handleCommand(cmd, anchor, message, commandDeps, larkAppId).catch((err) => logger.error(`[sessionless ${cmd}] ${anchor.substring(0, 12)} failed: ${err?.message ?? err}`));
1266
1283
  }
1267
1284
  // Dependencies passed to card-handler
1285
+ // v3 humanGate run-controller: drives daemon-side v3 runs, posts/​re-posts
1286
+ // approval cards to the run's bound topic, and re-arms pending gates on startup
1287
+ // (cold-attach). postCard / notifyTerminal use the daemon's Lark sender; the
1288
+ // run logic + in-flight guard live in createV3GateRunner.
1289
+ const v3GateRunner = createV3GateRunner({
1290
+ postCard: async (binding, gate, runId) => {
1291
+ const card = buildV3GateCard({
1292
+ runId,
1293
+ waitId: gate.waitId,
1294
+ nodeId: gate.nodeId,
1295
+ prompt: gate.prompt,
1296
+ options: gate.options,
1297
+ approveOptions: gate.approveOptions,
1298
+ approvers: gate.approvers,
1299
+ });
1300
+ // codex blocker #3: never silently skip — a missing rootMessageId would
1301
+ // leave the run stuck at awaitingGate forever. Reply in-thread when we have
1302
+ // an anchor; otherwise post to the chat directly.
1303
+ if (binding.rootMessageId) {
1304
+ await sessionReply(binding.rootMessageId, card, 'interactive', binding.larkAppId);
1305
+ }
1306
+ else {
1307
+ await sendMessage(binding.larkAppId, binding.chatId, card, 'interactive');
1308
+ }
1309
+ },
1310
+ postBlockedCard: async (binding, info, runId) => {
1311
+ const card = buildV3BlockedCard({
1312
+ runId,
1313
+ nodeId: info.nodeId,
1314
+ attemptId: info.attemptId,
1315
+ errorClass: info.errorClass,
1316
+ errorCode: info.errorCode,
1317
+ message: info.message,
1318
+ // human-ask 受阻 → 渲染问题 + 选项按钮卡(替代纯重试卡)。
1319
+ ...(info.ask ? { ask: info.ask } : {}),
1320
+ });
1321
+ if (binding.rootMessageId) {
1322
+ await sessionReply(binding.rootMessageId, card, 'interactive', binding.larkAppId);
1323
+ }
1324
+ else {
1325
+ await sendMessage(binding.larkAppId, binding.chatId, card, 'interactive');
1326
+ }
1327
+ },
1328
+ postLoopGrantCard: async (binding, info, runId) => {
1329
+ const card = buildV3LoopGrantCard({
1330
+ runId,
1331
+ loopId: info.loopId,
1332
+ iteration: info.iteration,
1333
+ maxIterations: info.maxIterations,
1334
+ granted: info.granted,
1335
+ detail: info.detail,
1336
+ });
1337
+ if (binding.rootMessageId) {
1338
+ await sessionReply(binding.rootMessageId, card, 'interactive', binding.larkAppId);
1339
+ }
1340
+ else {
1341
+ await sendMessage(binding.larkAppId, binding.chatId, card, 'interactive');
1342
+ }
1343
+ },
1344
+ postRevisitGrantCard: async (binding, info, runId) => {
1345
+ const card = buildV3RevisitGrantCard({
1346
+ runId,
1347
+ sourceNodeId: info.sourceNodeId,
1348
+ toNodeId: info.toNodeId,
1349
+ tier: info.tier,
1350
+ attemptId: info.attemptId,
1351
+ detail: info.detail,
1352
+ });
1353
+ if (binding.rootMessageId) {
1354
+ await sessionReply(binding.rootMessageId, card, 'interactive', binding.larkAppId);
1355
+ }
1356
+ else {
1357
+ await sendMessage(binding.larkAppId, binding.chatId, card, 'interactive');
1358
+ }
1359
+ },
1360
+ notifyTerminal: async (binding, runId, outcome) => {
1361
+ if (!binding?.rootMessageId)
1362
+ return;
1363
+ const msg = outcome.runStatus === 'succeeded'
1364
+ ? `✅ v3 workflow \`${runId}\` 跑完了`
1365
+ : outcome.runStatus === 'blocked'
1366
+ // Fallback only — the blocked path normally posts a retry/grant card instead.
1367
+ ? `⏸️ v3 workflow \`${runId}\` 受阻${outcome.blockedNodeId ? `(节点 ${outcome.blockedNodeId})` : ''},可 \`botmux workflow retry ${runId}\` 重试(loop 耗尽则 \`botmux workflow grant ${runId}\` 追加一轮)`
1368
+ : `❌ v3 workflow \`${runId}\` 失败${outcome.failedNodeId ? `(节点 ${outcome.failedNodeId})` : ''}`;
1369
+ await sessionReply(binding.rootMessageId, msg, 'text', binding.larkAppId).catch(() => { });
1370
+ },
1371
+ onError: (runId, err) => {
1372
+ logger.warn(`[v3:${runId}] drive failed: ${err instanceof Error ? err.message : String(err)}`);
1373
+ },
1374
+ });
1268
1375
  const cardDeps = {
1269
1376
  activeSessions,
1270
1377
  sessionReply,
@@ -1274,6 +1381,24 @@ const cardDeps = {
1274
1381
  logger.warn(`[workflow:${runId}] re-entry after approval failed: ${err instanceof Error ? err.message : String(err)}`);
1275
1382
  });
1276
1383
  },
1384
+ v3GateDeps: {
1385
+ driveRun: (runId) => v3GateRunner.driveDetached(runId),
1386
+ // 审批权限:复用 canOperate(话题 owner / allowedUsers / oncall)。无 binding(corrupt /
1387
+ // 非 grill 出生的旧卡)→ **拒**(codex follow-up:合法卡一定有 binding,缺失即可疑).
1388
+ canResolve: (binding, operatorOpenId) => binding ? canOperate(binding.larkAppId, binding.chatId, operatorOpenId) : false,
1389
+ },
1390
+ v3BlockedDeps: {
1391
+ driveRun: (runId) => v3GateRunner.driveDetached(runId),
1392
+ canResolve: (binding, operatorOpenId) => binding ? canOperate(binding.larkAppId, binding.chatId, operatorOpenId) : false,
1393
+ },
1394
+ v3LoopGrantDeps: {
1395
+ driveRun: (runId) => v3GateRunner.driveDetached(runId),
1396
+ canResolve: (binding, operatorOpenId) => binding ? canOperate(binding.larkAppId, binding.chatId, operatorOpenId) : false,
1397
+ },
1398
+ v3RevisitGrantDeps: {
1399
+ driveRun: (runId) => v3GateRunner.driveDetached(runId),
1400
+ canResolve: (binding, operatorOpenId) => binding ? canOperate(binding.larkAppId, binding.chatId, operatorOpenId) : false,
1401
+ },
1277
1402
  };
1278
1403
  function dashboardWaitStatus(error) {
1279
1404
  switch (error.error) {
@@ -1308,6 +1433,99 @@ for (const [path, resolution] of [
1308
1433
  return jsonRes(res, 200, result);
1309
1434
  });
1310
1435
  }
1436
+ // v3 humanGate: start a daemon-driven run (grill `approve-dag` 后的主入口). Same
1437
+ // 127.0.0.1 ipcRoute posture as the v0.2 approve/reject mutations (dashboard
1438
+ // proxies authed external calls). Fire-and-forget: the runner drives the run +
1439
+ // posts gate cards; the caller polls /api/v3/runs/:id for status.
1440
+ ipcRoute('POST', '/api/v3/runs/:runId/start', async (_req, res, params) => {
1441
+ const runId = params.runId;
1442
+ if (!isValidV3RunId(runId))
1443
+ return jsonRes(res, 400, { ok: false, error: 'bad_run_id' });
1444
+ // Owner check (codex blocker #1): only the daemon owning this run's bot may
1445
+ // start it — otherwise the wrong daemon drives + posts cards with its client.
1446
+ const binding = readV3RunChatBinding(join(v3DefaultBaseDir(), runId));
1447
+ if (!binding)
1448
+ return jsonRes(res, 404, { ok: false, error: 'unknown_run_or_no_binding' });
1449
+ if (selfV3LarkAppId && binding.larkAppId !== selfV3LarkAppId) {
1450
+ return jsonRes(res, 409, { ok: false, error: 'wrong_daemon', ownerLarkAppId: binding.larkAppId });
1451
+ }
1452
+ v3GateRunner.driveDetached(runId);
1453
+ return jsonRes(res, 202, { ok: true, runId });
1454
+ });
1455
+ // v3 blocked retry: append the retry intent + re-drive. Same owner posture as
1456
+ // /start. Body: { nodeId? } (defaults to the run's blockedNodeId).
1457
+ ipcRoute('POST', '/api/v3/runs/:runId/retry', async (req, res, params) => {
1458
+ const runId = params.runId;
1459
+ if (!isValidV3RunId(runId))
1460
+ return jsonRes(res, 400, { ok: false, error: 'bad_run_id' });
1461
+ const binding = readV3RunChatBinding(join(v3DefaultBaseDir(), runId));
1462
+ if (!binding)
1463
+ return jsonRes(res, 404, { ok: false, error: 'unknown_run_or_no_binding' });
1464
+ if (selfV3LarkAppId && binding.larkAppId !== selfV3LarkAppId) {
1465
+ return jsonRes(res, 409, { ok: false, error: 'wrong_daemon', ownerLarkAppId: binding.larkAppId });
1466
+ }
1467
+ let body = {};
1468
+ try {
1469
+ body = await readJsonBody(req);
1470
+ }
1471
+ catch {
1472
+ /* empty body is fine — retry the blockedNodeId */
1473
+ }
1474
+ const nodeId = typeof body.nodeId === 'string' && body.nodeId ? body.nodeId : undefined;
1475
+ let outcome;
1476
+ try {
1477
+ outcome = requestV3Retry(v3DefaultBaseDir(), runId, { nodeId });
1478
+ }
1479
+ catch (err) {
1480
+ return jsonRes(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
1481
+ }
1482
+ if (outcome.kind === 'stale-run') {
1483
+ return jsonRes(res, 409, {
1484
+ ok: false,
1485
+ error: outcome.reason === 'missing' ? 'unknown_run'
1486
+ : outcome.reason === 'loop-node' ? 'loop_node_use_grant'
1487
+ : 'not_blocked',
1488
+ });
1489
+ }
1490
+ // requested / already-requested → make sure the run is moving.
1491
+ v3GateRunner.driveDetached(runId);
1492
+ return jsonRes(res, 202, { ok: true, runId, ...outcome });
1493
+ });
1494
+ // v3 loop grant: append one extra iteration for an exhausted loop + re-drive.
1495
+ // Same owner posture as /retry. Body: { loopId? } (defaults to the run's
1496
+ // blocked loop).
1497
+ ipcRoute('POST', '/api/v3/runs/:runId/grant', async (req, res, params) => {
1498
+ const runId = params.runId;
1499
+ if (!isValidV3RunId(runId))
1500
+ return jsonRes(res, 400, { ok: false, error: 'bad_run_id' });
1501
+ const binding = readV3RunChatBinding(join(v3DefaultBaseDir(), runId));
1502
+ if (!binding)
1503
+ return jsonRes(res, 404, { ok: false, error: 'unknown_run_or_no_binding' });
1504
+ if (selfV3LarkAppId && binding.larkAppId !== selfV3LarkAppId) {
1505
+ return jsonRes(res, 409, { ok: false, error: 'wrong_daemon', ownerLarkAppId: binding.larkAppId });
1506
+ }
1507
+ let body = {};
1508
+ try {
1509
+ body = await readJsonBody(req);
1510
+ }
1511
+ catch {
1512
+ /* empty body is fine — grant the blocked loop */
1513
+ }
1514
+ const loopId = typeof body.loopId === 'string' && body.loopId ? body.loopId : undefined;
1515
+ let outcome;
1516
+ try {
1517
+ outcome = requestV3LoopGrant(v3DefaultBaseDir(), runId, { loopId, by: 'cli' });
1518
+ }
1519
+ catch (err) {
1520
+ return jsonRes(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
1521
+ }
1522
+ if (outcome.kind === 'stale-run') {
1523
+ return jsonRes(res, 409, { ok: false, error: outcome.reason === 'missing' ? 'unknown_run' : 'not_exhausted' });
1524
+ }
1525
+ // granted / already-granted → make sure the run is moving.
1526
+ v3GateRunner.driveDetached(runId);
1527
+ return jsonRes(res, 202, { ok: true, runId, ...outcome });
1528
+ });
1311
1529
  function attemptResumeStatus(error) {
1312
1530
  switch (error.error) {
1313
1531
  case 'bad_run_id':
@@ -1421,7 +1639,7 @@ ipcRoute('POST', '/api/workflows/definitions/:id/run', async (req, res, params)
1421
1639
  }
1422
1640
  }
1423
1641
  // Convert JSON-channel params (decoded values) into the shared RawParamInput
1424
- // map. String-channel coercion stays on the IM `/workflow run` path.
1642
+ // map. String-channel coercion stays on the IM `/template run` path.
1425
1643
  const rawParams = {};
1426
1644
  for (const [k, v] of Object.entries(body.params ?? {})) {
1427
1645
  rawParams[k] = { kind: 'json', value: v };
@@ -1699,7 +1917,11 @@ async function resolvePinnedWorkingDir(ctx) {
1699
1917
  oncallEntry = await maybeAutoBindDefaultOncall(ctx.larkAppId, ctx.chatId, ctx.chatType);
1700
1918
  }
1701
1919
  const inheritedFrom = !oncallEntry
1702
- ? findInheritablePeer({ scope: ctx.scope, anchor: ctx.anchor, chatId: ctx.chatId, chatType: ctx.chatType, selfAppId: ctx.larkAppId })
1920
+ ? findInheritablePeer({
1921
+ scope: ctx.scope, anchor: ctx.anchor, chatId: ctx.chatId, chatType: ctx.chatType,
1922
+ selfAppId: ctx.larkAppId,
1923
+ botToBotSameDir: getBot(ctx.larkAppId).config.botToBotSameDir !== false,
1924
+ })
1703
1925
  : null;
1704
1926
  const botDefaultWorkingDir = (!oncallEntry && !inheritedFrom)
1705
1927
  ? resolveBotDefaultWorkingDir(ctx.larkAppId)
@@ -1868,13 +2090,32 @@ async function handleNewTopic(data, ctx) {
1868
2090
  msgType: parsed.msgType,
1869
2091
  content,
1870
2092
  });
1871
- if (parseWorkflowCommand(cmdContent)) {
2093
+ // v3 即兴 grill:`/workflow [new] <目标>`。daemon 不拷问——把目标包成触发
2094
+ // botmux-workflow skill 的 prompt(改写 content,promptContent 随后从 content
2095
+ // 构造),fall-through 到正常 session 创建,让本话题 agent 接管整条链路。
2096
+ // run|cancel 不在此命中(归 v2 legacy,由下面 handleWorkflowCommandIfAny 处理)。
2097
+ const newTopicGrill = parseWorkflowGrillTrigger(cmdContent);
2098
+ if (newTopicGrill) {
1872
2099
  if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, '/workflow')) {
1873
2100
  return;
1874
2101
  }
2102
+ if (newTopicGrill.kind === 'usage') {
2103
+ await sessionReply(anchor, WORKFLOW_USAGE, 'text', larkAppId);
2104
+ return;
2105
+ }
2106
+ content = buildWorkflowGrillPrompt(newTopicGrill.goal);
2107
+ // 保留原 cmdContent("/workflow new …")供 title/日志;/workflow 非注册命令,
2108
+ // 下面的 parseSlashCommandInvocation 会让它落到正常 spawn 路径。
1875
2109
  }
1876
- if (await handleWorkflowCommandIfAny(cmdContent, anchor, chatId, larkAppId, senderOpenId)) {
1877
- return;
2110
+ else {
2111
+ if (parseWorkflowCommand(cmdContent)) {
2112
+ if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, '/template')) {
2113
+ return;
2114
+ }
2115
+ }
2116
+ if (await handleWorkflowCommandIfAny(cmdContent, anchor, chatId, larkAppId, senderOpenId)) {
2117
+ return;
2118
+ }
1878
2119
  }
1879
2120
  // Intercept daemon commands in new topics (no session needed for some commands)
1880
2121
  const invocation = parseSlashCommandInvocation(cmdContent);
@@ -2390,7 +2631,9 @@ async function handleThreadReply(data, ctx) {
2390
2631
  const botSenderPrefix = isForeignBot
2391
2632
  ? `${tr('daemon.foreign_bot_mention_prefix', { botName: foreignBotName }, localeForBot(larkAppId))}\n`
2392
2633
  : '';
2393
- const promptContent = buildQuoteHint(parsed, scope, anchor, localeForBot(larkAppId)) + botSenderPrefix + parsed.content;
2634
+ // `let` (not const): the v3 grill gate below may replace this with a
2635
+ // skill-trigger prompt when the user sends `/workflow [new] <目标>` mid-thread.
2636
+ let promptContent = buildQuoteHint(parsed, scope, anchor, localeForBot(larkAppId)) + botSenderPrefix + parsed.content;
2394
2637
  const existingHookSession = activeSessions.get(sessionKey(anchor, larkAppId));
2395
2638
  emitHookEvent('thread.reply', {
2396
2639
  larkAppId,
@@ -2458,13 +2701,30 @@ async function handleThreadReply(data, ctx) {
2458
2701
  return;
2459
2702
  }
2460
2703
  }
2461
- if (parseWorkflowCommand(cmdContent)) {
2704
+ // v3 即兴 grill(thread 内):`/workflow [new] <目标>` → 把目标包成触发
2705
+ // botmux-workflow skill 的 prompt 覆盖 promptContent,fall-through 到下面正常
2706
+ // 转发逻辑,让现有/新建的 agent 接管。run|cancel 归 v2 legacy(走 else)。
2707
+ const threadGrill = parseWorkflowGrillTrigger(cmdContent);
2708
+ if (threadGrill) {
2462
2709
  if (await replyGrantRestrictionIfNeeded(larkAppId, threadChatId, threadSenderOpenId, anchor, '/workflow')) {
2463
2710
  return;
2464
2711
  }
2712
+ if (threadGrill.kind === 'usage') {
2713
+ await sessionReply(anchor, WORKFLOW_USAGE, 'text', larkAppId);
2714
+ return;
2715
+ }
2716
+ promptContent = buildWorkflowGrillPrompt(threadGrill.goal);
2717
+ // fall through to normal forwarding with the rewritten promptContent
2465
2718
  }
2466
- if (await handleWorkflowCommandIfAny(cmdContent, anchor, threadChatId, larkAppId, threadSenderOpenId)) {
2467
- return;
2719
+ else {
2720
+ if (parseWorkflowCommand(cmdContent)) {
2721
+ if (await replyGrantRestrictionIfNeeded(larkAppId, threadChatId, threadSenderOpenId, anchor, '/template')) {
2722
+ return;
2723
+ }
2724
+ }
2725
+ if (await handleWorkflowCommandIfAny(cmdContent, anchor, threadChatId, larkAppId, threadSenderOpenId)) {
2726
+ return;
2727
+ }
2468
2728
  }
2469
2729
  // Intercept daemon commands
2470
2730
  const invocation = parseSlashCommandInvocation(cmdContent);
@@ -2593,10 +2853,13 @@ async function handleThreadReply(data, ctx) {
2593
2853
  // 自定义回复拦截:该话题有未结的 ask 时,把这条文字当答案,走 submitCustomReply
2594
2854
  // settle 掉 ask(替代选项语义),不再当作新一轮指令喂给 CLI。此时发起 ask 的 CLI
2595
2855
  // 正阻塞等结果,回什么都得先等 ask 结束,故无副作用。仅拦截纯文字(slash 命令 /
2596
- // 回调 URL / workflow 已在上方各自 return,可用来中止)。答复权限 = canTalk,由
2856
+ // 回调 URL 已在上方 return,可用来中止)。答复权限 = canTalk,由
2597
2857
  // broker 在 submitCustomReply 内按注入的 canTalkChecker 判定:非授权人返回
2598
2858
  // 'unauthorized',这里 fall through 到正常路由。卡片由 broker.onSettle 自动 PATCH。
2599
- if (threadSenderOpenId && threadChatId) {
2859
+ // `!threadGrill`:grill goal 分支只改写 promptContent 后 fall-through(不 return),
2860
+ // cmdContent 仍是字面量 `/workflow new <目标>`;若不排除,待回答 ask 会把它当答案吞掉,
2861
+ // grill 永远不启动。grill 必须穿过拦截器走正常转发。
2862
+ if (threadSenderOpenId && threadChatId && !threadGrill) {
2600
2863
  const askReplyText = cmdContent.trim();
2601
2864
  if (askReplyText) {
2602
2865
  const pendingAsk = findPendingAskByAnchor({ larkAppId, chatId: threadChatId, anchor });
@@ -3184,6 +3447,7 @@ export async function startDaemon(botIndex) {
3184
3447
  // also needs its own copy for SessionRow.botName.
3185
3448
  setBotName(cfg.larkAppId);
3186
3449
  setLarkAppId(cfg.larkAppId);
3450
+ selfV3LarkAppId = cfg.larkAppId; // scope v3 humanGate cold-attach / start to this bot
3187
3451
  // Bind dashboard IPC HTTP server BEFORE publishing the registry descriptor.
3188
3452
  // Otherwise the dashboard process can race-fetch the IPC port from the
3189
3453
  // descriptor and hit ECONNREFUSED before we're listening — that left every
@@ -3365,6 +3629,20 @@ export async function startDaemon(botIndex) {
3365
3629
  reapOrphanWorkers();
3366
3630
  // Restore active sessions from previous run
3367
3631
  await restoreActiveSessions(activeSessions);
3632
+ // Second global-skills sweep, AFTER restore has settled. The early
3633
+ // cleanupGlobalBotmuxSkillsOnce() pass (in the startup ensureCliEnv above)
3634
+ // runs before any restart overlap settles, so an outgoing old-build daemon
3635
+ // (pre `--plugin-dir` migration) can re-create ~/.claude/skills/botmux-* a few
3636
+ // ms after we cleaned it — leaving it to leak into the user's standalone
3637
+ // `claude` until the *next* restart. Re-sweeping here, once this daemon's own
3638
+ // spawns and the handoff window are done, catches that leak on the same
3639
+ // startup. Idempotent & best-effort — never blocks startup.
3640
+ try {
3641
+ sweepGlobalBotmuxSkills();
3642
+ }
3643
+ catch (err) {
3644
+ logger.warn(`[skills] post-restore global sweep failed: ${err instanceof Error ? err.message : String(err)}`);
3645
+ }
3368
3646
  // 文档订阅恢复:重启后订阅可能已失效,给仍活跃的会话重订阅;会话没恢复
3369
3647
  // (已关/丢失)的订阅则退订 + 清表,避免「命中订阅但无会话」的孤儿。
3370
3648
  await restoreDocSubscriptions(activeSessions);
@@ -3406,6 +3684,12 @@ export async function startDaemon(botIndex) {
3406
3684
  }, 120_000);
3407
3685
  sandboxReconcileTimer.unref?.();
3408
3686
  await attachColdWorkflowRuns(cfg.larkAppId);
3687
+ // v3 humanGate cold-attach: re-post pending gate cards + resume healed gates
3688
+ // for runs OWNED BY THIS BOT (codex blocker #1 — owner filter, mirrors
3689
+ // attachColdWorkflowRuns(cfg.larkAppId)). Best-effort; never blocks startup.
3690
+ await v3GateRunner.coldAttach(cfg.larkAppId).catch((err) => {
3691
+ logger.warn(`[v3] cold-attach failed; continuing daemon startup: ${err instanceof Error ? err.message : String(err)}`);
3692
+ });
3409
3693
  // Start scheduler in every daemon. Each daemon owns exactly one bot, so
3410
3694
  // each filters to only execute tasks whose `larkAppId` matches its bot
3411
3695
  // (unmatched tasks are handled by the owning bot's daemon instead; a