agentworkshop 0.7.3 → 0.7.5

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 (130) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/B9Yie5HN.js +2 -0
  3. package/.output/public/_nuxt/CN7Zh8GQ.js +1 -0
  4. package/.output/public/_nuxt/CdSgfcYR.js +1 -0
  5. package/.output/public/_nuxt/CdfAP_IH2.js +1 -0
  6. package/.output/public/_nuxt/ChnfyQHz.js +1 -0
  7. package/.output/public/_nuxt/{BkKYf8xY.js → CyygA0q4.js} +4 -4
  8. package/.output/public/_nuxt/{Bz40vyZS.js → DFnGFEav.js} +7 -7
  9. package/.output/public/_nuxt/Xaf_HIR3.js +1 -0
  10. package/.output/public/_nuxt/ZugRk0CM2.js +4 -0
  11. package/.output/public/_nuxt/builds/latest.json +1 -1
  12. package/.output/public/_nuxt/builds/meta/55c10854-ee0e-4be4-924b-a1b0e100fe11.json +1 -0
  13. package/.output/public/_nuxt/{default.iGJy9dP-.css → default.D17kXlD6.css} +1 -1
  14. package/.output/public/_nuxt/monitor.CmwpEUCI.css +1 -0
  15. package/.output/public/_nuxt/yIKY_V1P.js +1 -0
  16. package/.output/server/chunks/_/nitro.mjs +1131 -920
  17. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  18. package/.output/server/chunks/build/{TownView-BkHxMiwZ.mjs → TownView-Bl8Gkcxi.mjs} +3 -3
  19. package/.output/server/chunks/build/{TownView-BkHxMiwZ.mjs.map → TownView-Bl8Gkcxi.mjs.map} +1 -1
  20. package/.output/server/chunks/build/{_id_-Bna1_JuB.mjs → _id_-CWLFGZjV.mjs} +2 -2
  21. package/.output/server/chunks/build/{_id_-Bna1_JuB.mjs.map → _id_-CWLFGZjV.mjs.map} +1 -1
  22. package/.output/server/chunks/build/_id_-GnRJPykX.mjs +1 -1
  23. package/.output/server/chunks/build/{_wsId_-CzLDGT_V.mjs → _wsId_-BhQMjyHG.mjs} +3 -4
  24. package/.output/server/chunks/build/{_wsId_-CzLDGT_V.mjs.map → _wsId_-BhQMjyHG.mjs.map} +1 -1
  25. package/.output/server/chunks/build/{daq-CmeM4u5u.mjs → daq-BOgpn9sl.mjs} +4 -5
  26. package/.output/server/chunks/build/daq-BOgpn9sl.mjs.map +1 -0
  27. package/.output/server/chunks/build/dcw-BTM5YU-B.mjs +1 -1
  28. package/.output/server/chunks/build/{default-Dt6xLdpd.mjs → default-CKJWhkot.mjs} +60 -17
  29. package/.output/server/chunks/build/{default-Dt6xLdpd.mjs.map → default-CKJWhkot.mjs.map} +1 -1
  30. package/.output/server/chunks/build/{logs-CUQt1z7D.mjs → logs-Dyk0I2I9.mjs} +3 -4
  31. package/.output/server/chunks/build/logs-Dyk0I2I9.mjs.map +1 -0
  32. package/.output/server/chunks/build/{monitor-yNoV98rf.mjs → monitor-KW8WLOJd.mjs} +34 -33
  33. package/.output/server/chunks/build/{monitor-yNoV98rf.mjs.map → monitor-KW8WLOJd.mjs.map} +1 -1
  34. package/.output/server/chunks/build/{town-BN4vQPb8.mjs → town-_4tEEEVW.mjs} +3 -4
  35. package/.output/server/chunks/build/town-_4tEEEVW.mjs.map +1 -0
  36. package/.output/server/chunks/build/useDaqStream-hwkhJHFU.mjs +1 -1
  37. package/.output/server/chunks/build/useDcwStream-Bdew_yrr.mjs +1 -1
  38. package/.output/server/chunks/build/{useWorkshopWs-xcdeiSmJ.mjs → useWorkshopWs-B3j5wIAK.mjs} +177 -3
  39. package/.output/server/chunks/build/useWorkshopWs-B3j5wIAK.mjs.map +1 -0
  40. package/.output/server/chunks/build/{workshop-CLhwe5eo.mjs → workshop-k0XZTFKg.mjs} +2 -3
  41. package/.output/server/chunks/build/workshop-k0XZTFKg.mjs.map +1 -0
  42. package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
  43. package/.output/server/chunks/routes/api/workshop/channels.post.mjs +1 -1
  44. package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +32 -0
  45. package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs.map +1 -0
  46. package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +56 -0
  47. package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs.map +1 -0
  48. package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
  49. package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
  50. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
  51. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
  52. package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
  53. package/.output/server/chunks/routes/renderer.mjs +2 -2
  54. package/.output/server/chunks/virtual/entry.mjs +17 -11
  55. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  56. package/.output/server/package.json +1 -1
  57. package/README.md +435 -434
  58. package/app/components/AppHeader.vue +138 -0
  59. package/app/composables/workshop/useWorkshopWs.ts +4 -0
  60. package/app/pages/monitor.vue +17 -1
  61. package/app/stores/workshop/hitl.ts +80 -0
  62. package/bin/aw.mjs +0 -0
  63. package/cli/commands/stop.mjs +108 -0
  64. package/cli/commands/tui.mjs +19 -0
  65. package/config.yml +134 -134
  66. package/docs/tui.md +43 -0
  67. package/i18n/locales/en.ts +3 -0
  68. package/i18n/locales/zh-CN.ts +3 -0
  69. package/package.json +140 -144
  70. package/scripts/_spike-pitui.mjs +116 -0
  71. package/scripts/e2e-aw-tui-global.mjs +277 -0
  72. package/scripts/e2e-hitl-live.mjs +323 -0
  73. package/scripts/e2e-line-live.mjs +144 -0
  74. package/scripts/test-hitl-registry.ts +112 -0
  75. package/scripts/test-tui-commands.mjs +148 -0
  76. package/scripts/test-tui-reducers.mjs +72 -0
  77. package/scripts/tsconfig.json +14 -0
  78. package/scripts/tui-smoke.mjs +100 -0
  79. package/server/api/workshop/hitl/pending.get.ts +28 -0
  80. package/server/api/workshop/hitl/respond.post.ts +67 -0
  81. package/server/api/workshop/ws.ts +30 -1
  82. package/server/data/daqs.json +39 -17
  83. package/server/data/dcw-lines.json +14 -0
  84. package/server/data/dcw-products.json +35 -0
  85. package/server/data/dcw-recipes.json +140 -0
  86. package/server/data/dcw-rollback.json +111 -1
  87. package/server/data/dcw-runs.json +135 -90
  88. package/server/data/dcw-writes.json +120 -0
  89. package/server/data/dcws.json +400 -275
  90. package/server/data/line-runs.json +11 -1
  91. package/server/plugins/workshop.ts +7 -0
  92. package/server/services/workshop/agents/harness-terminal.ts +92 -25
  93. package/server/services/workshop/agents/hitl-registry.ts +181 -0
  94. package/server/services/workshop/agents/tool-approvals.ts +15 -0
  95. package/server/services/workshop/scene-events.ts +19 -9
  96. package/shared/workshop-protocol.ts +37 -0
  97. package/tui/README.md +74 -0
  98. package/tui/aw-tui.mjs +406 -0
  99. package/tui/commands/index.mjs +287 -0
  100. package/tui/components/chat-log.mjs +42 -0
  101. package/tui/components/hitl-card.mjs +36 -0
  102. package/tui/components/monitor-pane.mjs +39 -0
  103. package/tui/components/root.mjs +53 -0
  104. package/tui/components/status-bar.mjs +33 -0
  105. package/tui/lib/api.mjs +82 -0
  106. package/tui/lib/config.mjs +58 -0
  107. package/tui/lib/pickers.mjs +102 -0
  108. package/tui/lib/reducers.mjs +218 -0
  109. package/tui/lib/state.mjs +86 -0
  110. package/tui/lib/virtual-terminal.mjs +48 -0
  111. package/tui/lib/ws.mjs +124 -0
  112. package/tui/theme.mjs +38 -0
  113. package/.output/public/_nuxt/B7V9IfTL.js +0 -1
  114. package/.output/public/_nuxt/BE5QyG5J2.js +0 -1
  115. package/.output/public/_nuxt/BuKI8z-5.js +0 -1
  116. package/.output/public/_nuxt/D0C5Q0di.js +0 -1
  117. package/.output/public/_nuxt/DC-QMEk1.js +0 -2
  118. package/.output/public/_nuxt/DEkQ0qQ02.js +0 -1
  119. package/.output/public/_nuxt/builds/meta/c6c01cdd-762e-4fb8-86e7-753c5185b856.json +0 -1
  120. package/.output/public/_nuxt/dt2tKE95.js +0 -4
  121. package/.output/public/_nuxt/i_MKObHo.js +0 -1
  122. package/.output/public/_nuxt/monitor.41fCD6TB.css +0 -1
  123. package/.output/public/_nuxt/tpDMVY5w.js +0 -1
  124. package/.output/server/chunks/build/connection-Dc0fn6yd.mjs +0 -120
  125. package/.output/server/chunks/build/connection-Dc0fn6yd.mjs.map +0 -1
  126. package/.output/server/chunks/build/daq-CmeM4u5u.mjs.map +0 -1
  127. package/.output/server/chunks/build/logs-CUQt1z7D.mjs.map +0 -1
  128. package/.output/server/chunks/build/town-BN4vQPb8.mjs.map +0 -1
  129. package/.output/server/chunks/build/useWorkshopWs-xcdeiSmJ.mjs.map +0 -1
  130. package/.output/server/chunks/build/workshop-CLhwe5eo.mjs.map +0 -1
@@ -0,0 +1,116 @@
1
+ // Phase 0 spike:pi-tui(@earendil-works/pi-tui)无 TTY 可行性验证。
2
+ // 用虚拟终端(Terminal 接口最小实现)驱动真实 TUI 渲染 + Editor 输入,
3
+ // 全部断言通过 ⇒ 框架机制可用,交互层(真实终端 raw-mode/VT)由人工在
4
+ // Git Bash / Windows Terminal 各跑一次 `node scripts/_spike-pitui.mjs --live` 终验。
5
+ import { TuiMainScreen, Text, Editor, ProcessTerminal } from '@earendil-works/pi-tui'
6
+
7
+ const isLive = process.argv.includes('--live')
8
+
9
+ /** 虚拟终端:捕获写入、可注入输入(Terminal 接口最小 duck-typing 实现) */
10
+ class VirtualTerminal {
11
+ constructor(cols = 100, rows = 30) {
12
+ this.columns = cols
13
+ this.rows = rows
14
+ this.frames = []
15
+ this.inputHandler = null
16
+ this.kittyProtocolActive = false
17
+ }
18
+
19
+ start(onInput) { this.inputHandler = onInput }
20
+ stop() {}
21
+ async drainInput() {}
22
+ write(data) { this.frames.push(data) }
23
+ moveBy() {}
24
+ hideCursor() {}
25
+ showCursor() {}
26
+ clearLine() {}
27
+ clearFromCursor() {}
28
+ clearScreen() {}
29
+ setTitle() {}
30
+ setProgress() {}
31
+ /** 测试注入按键(模拟 raw-mode 字节流) */
32
+ emitInput(data) { this.inputHandler?.(data) }
33
+ /** 全部输出拼接(strip ANSI 后可断言) */
34
+ output() { return this.frames.join('') }
35
+ }
36
+
37
+ // eslint-disable-next-line no-control-regex
38
+ const stripAnsi = s => s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '')
39
+
40
+ // ---- 主题(与 tui/theme.mjs 同源的极简令牌;spike 内联验证结构) ----
41
+ const theme = {
42
+ borderColor: s => `\x1b[38;2;53;224;160m${s}\x1b[0m`,
43
+ selectList: {
44
+ selectedPrefix: s => `› ${s}`,
45
+ selectedText: s => `\x1b[38;2;65;200;244m${s}\x1b[0m`,
46
+ description: s => `\x1b[2m${s}\x1b[0m`,
47
+ scrollInfo: s => `\x1b[2m${s}\x1b[0m`,
48
+ noMatch: s => s,
49
+ },
50
+ }
51
+
52
+ let failures = 0
53
+ function check(name, cond) {
54
+ console.log(`${cond ? ' ✓' : ' ✗ FAIL'} ${name}`)
55
+ if (!cond) failures++
56
+ }
57
+
58
+ if (isLive) {
59
+ // 真实终端交互验证:输入字符 + 回车;Ctrl+C 退出
60
+ const tui = new TuiMainScreen(new ProcessTerminal())
61
+ tui.addChild(new Text('pi-tui live spike: 输入任意文字后回车,Ctrl+C 退出'))
62
+ const editor = new Editor(tui, theme)
63
+ editor.onSubmit = text => tui.addChild(new Text(`[submit] ${text}`))
64
+ tui.addChild(editor)
65
+ tui.setFocus(editor)
66
+ tui.start()
67
+ console.log('live spike running…')
68
+ }
69
+ else await (async () => {
70
+ // ---- headless 验证 ----
71
+ console.log('[spike] pi-tui headless 渲染验证')
72
+ const vt = new VirtualTerminal()
73
+ const tui = new TuiMainScreen(vt)
74
+ tui.addChild(new Text('AgentWorkShop TUI boot ok'))
75
+ tui.addChild(new Text('频道: daq-产线A · 2 agents · HITL 待处理 1'))
76
+ const editor = new Editor(tui, theme)
77
+ const submitted = []
78
+ editor.onSubmit = text => submitted.push(text)
79
+ tui.addChild(editor)
80
+ tui.setFocus(editor)
81
+ tui.start()
82
+ await new Promise(r => setTimeout(r, 150))
83
+
84
+ const out1 = stripAnsi(vt.output())
85
+ check('TUI 实例化并启动(tui.start 无异常)', true)
86
+ check('Text 组件渲染出现在输出帧', out1.includes('AgentWorkShop TUI boot ok'))
87
+ check('中文与多行文本渲染', out1.includes('频道: daq-产线A'))
88
+ check('Editor 边框渲染(borderColor 令牌生效)', vt.output().includes('\x1b[38;2;53;224;160m'))
89
+
90
+ // Editor 输入:/help 回车 → onSubmit
91
+ vt.frames.length = 0
92
+ for (const ch of '/help') vt.emitInput(ch)
93
+ await new Promise(r => setTimeout(r, 50))
94
+ check('编辑器回显输入文本', stripAnsi(vt.output()).includes('/help'))
95
+ vt.emitInput('\r')
96
+ await new Promise(r => setTimeout(r, 50))
97
+ check('回车触发 onSubmit', submitted.length === 1 && submitted[0] === '/help')
98
+
99
+ // 光标移动 + 退格 + 历史(编辑器核心机制)
100
+ vt.emitInput('abc')
101
+ vt.emitInput('\x7f') // backspace
102
+ check('退格删除字符', editor.getText() === 'ab')
103
+ vt.emitInput('\x15') // Ctrl+U 清行
104
+ check('Ctrl+U 清空输入行', editor.getText() === '')
105
+
106
+ // 动态 addChild(流式追加语义)
107
+ const before = stripAnsi(vt.output()).length
108
+ tui.addChild(new Text('agent.delta 流块追加'))
109
+ tui.requestRender(true)
110
+ await new Promise(r => setTimeout(r, 80))
111
+ check('动态 addChild + requestRender 增量渲染', stripAnsi(vt.output()).length > before && stripAnsi(vt.output()).includes('agent.delta 流块追加'))
112
+
113
+ tui.stop()
114
+ console.log(failures === 0 ? '\n[spike] GO —— pi-tui 机制全部可用(真实终端交互请另跑 --live 人工终验)' : `\n[spike] NO-GO —— ${failures} 项失败`)
115
+ process.exit(failures === 0 ? 0 : 1)
116
+ })()
@@ -0,0 +1,277 @@
1
+ /**
2
+ * 全局 `aw tui` 完整 E2E(用户真实路径:npm i -g agentworkshop → aw tui):
3
+ * node scripts/e2e-aw-tui-global.mjs [--base http://127.0.0.1:3000]
4
+ *
5
+ * 前置:全局已安装 agentworkshop@0.7.4(npm root -g 可解析);dev/prod 服务运行。
6
+ * 场景:加载【全局包】的 tui 入口(非仓库源码)→ 建频道(内联 omp lead)→
7
+ * 成员列表 → 任务下发(触发 omp spawn)→ /monitor 独立监控接入 →
8
+ * /send 注入 ask → /hitl 定位作答 → REST 确认落定 → 终端帧证实答案
9
+ * 回传 agent → /task 正式任务 + /tasks 列表 → /monitor off → 清理。
10
+ */
11
+ import { execSync } from 'node:child_process'
12
+ import { existsSync } from 'node:fs'
13
+ import { pathToFileURL } from 'node:url'
14
+ import { join } from 'node:path'
15
+
16
+ const BASE = (() => {
17
+ const i = process.argv.indexOf('--base')
18
+ return i > 0 ? process.argv[i + 1] : 'http://127.0.0.1:3000'
19
+ })()
20
+ // --repo:加载仓库源码的 tui(开发验证新交互);默认加载全局安装包(用户真实路径)
21
+ const USE_REPO = process.argv.includes('--repo')
22
+ const WS_BASE = BASE.replace(/^http/, 'ws')
23
+ const TAG = Date.now().toString(36)
24
+ const ASK_INSTRUCTION = 'Use the ask tool NOW to ask me: Deploy? with options go and abort. After I answer, call complete_task with the deliverable set to my answer.'
25
+
26
+ let failures = 0
27
+ let passed = 0
28
+ const check = (name, ok, detail = '') => {
29
+ console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
30
+ if (ok) passed += 1
31
+ else failures += 1
32
+ }
33
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
34
+
35
+ async function waitUntil(name, cond, timeoutMs = 300_000, intervalMs = 1000) {
36
+ const deadline = Date.now() + timeoutMs
37
+ let last = null
38
+ while (Date.now() < deadline) {
39
+ try {
40
+ last = await cond()
41
+ if (last) return last
42
+ }
43
+ catch (e) { last = e }
44
+ await sleep(intervalMs)
45
+ }
46
+ throw new Error(`waitUntil timeout: ${name} (last=${String(last).slice(0, 200)})`)
47
+ }
48
+
49
+ const api = async (method, path, { body, token } = {}) => {
50
+ const headers = { 'content-type': 'application/json' }
51
+ if (token) headers.authorization = `Bearer ${token}`
52
+ const res = await fetch(`${BASE}${path}`, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined })
53
+ return { status: res.status, ...(await res.json().catch(() => ({}))) }
54
+ }
55
+
56
+ /** 终端 WS(答案回传验证用) */
57
+ function openTerm(agentId, channelId, token) {
58
+ const messages = []
59
+ const ws = new WebSocket(`${WS_BASE}/api/system/monitor/terminal/ws?agentId=${agentId}&channelId=${channelId}&token=${encodeURIComponent(token)}`)
60
+ ws.addEventListener('message', (ev) => {
61
+ try {
62
+ messages.push(JSON.parse(ev.data))
63
+ }
64
+ catch { /* ignore */ }
65
+ })
66
+ return { ws, messages, frames: () => messages.filter(m => m.type === 'term.frames').flatMap(m => m.frames) }
67
+ }
68
+
69
+ /** 解析 TUI 入口:--repo = 仓库源码;默认 = 全局安装包(用户 `aw tui` 的真实加载路径) */
70
+ function tuiEntry() {
71
+ if (USE_REPO) {
72
+ const entry = join(process.cwd(), 'tui', 'aw-tui.mjs')
73
+ return { entry, ok: existsSync(entry), repo: true }
74
+ }
75
+ const root = execSync('npm root -g', { encoding: 'utf8' }).trim()
76
+ const entry = join(root, 'agentworkshop', 'tui', 'aw-tui.mjs')
77
+ return { entry, ok: existsSync(entry), repo: false }
78
+ }
79
+
80
+ async function main() {
81
+ console.log(`\n━━━ 全局 aw tui 完整 E2E @ ${BASE} ━━━`)
82
+
83
+ // 0. 入口存在(全局包 = 0.7.4 发布包含 tui/;repo = 源码)
84
+ const g = tuiEntry()
85
+ check(`0.1 TUI 入口存在(${g.repo ? '仓库源码' : '全局包'})`, g.ok, g.entry)
86
+ if (!g.ok) process.exit(1)
87
+
88
+ // 登录
89
+ const reg = await api('POST', '/api/users/register', {
90
+ body: { email: `awtui-${TAG}@test.local`, password: 'Passw0rd!123', name: `awtui-${TAG}` },
91
+ })
92
+ const token = reg.data?.token
93
+ if (!token) throw new Error(`注册失败: ${JSON.stringify(reg).slice(0, 160)}`)
94
+
95
+ // 0.2 预建一个频道(启动频道选择器需要至少一个可选项;全局包模式同样受益)
96
+ const bootCh = (await api('POST', '/api/workshop/channels', {
97
+ body: { name: `awtui-boot-${TAG}`, description: '启动选择器测试频道' },
98
+ token,
99
+ })).data
100
+ check('0.2 预建启动频道', Boolean(bootCh?.channelId), `id=${bootCh?.channelId?.slice(0, 8)}`)
101
+
102
+ // 1. 加载 TUI,headless 驱动
103
+ const mod = await import(pathToFileURL(g.entry).href)
104
+ await mod.main(['--headless', '--url', BASE, '--token', token, '--channel', ''])
105
+ const vt = globalThis.__tuiSmokeTerminal
106
+ const type = async (text) => {
107
+ for (const c of text) vt.emitInput(c)
108
+ await sleep(50)
109
+ vt.emitInput('\r')
110
+ await sleep(400)
111
+ }
112
+ const waitTui = async (needle, timeoutMs = 300_000) => {
113
+ const deadline = Date.now() + timeoutMs
114
+ while (Date.now() < deadline) {
115
+ if (vt.text().includes(needle)) return true
116
+ await sleep(500)
117
+ }
118
+ return false
119
+ }
120
+ check('1.1 TUI 启动就绪', await waitTui('TUI 已就绪', 30_000), g.entry.slice(-60))
121
+
122
+ // 1.2 [新交互] 启动频道选择器:↑↓ + Enter 选择预建频道;Esc 自动进第一个
123
+ if (USE_REPO) {
124
+ check('1.2 启动弹出频道选择器', await waitTui('选择要进入的频道', 20_000))
125
+ vt.emitInput('\x1b[B') // 下移一次,选中第二个(证明方向键导航)
126
+ await sleep(200)
127
+ vt.emitInput('\r')
128
+ check('1.3 Enter 进入所选频道', await waitTui('已切换到频道「', 30_000))
129
+ }
130
+ else {
131
+ check('1.2 (全局包)自动接入频道', await waitTui('已切换到频道「', 30_000))
132
+ }
133
+
134
+ // 2. Channel 操作:列表 + 创建(内联 omp lead)+ 自动切换
135
+ await type('/channels')
136
+ check('2.1 /channels 列出频道', await waitTui('个:', 15_000))
137
+ const channelName = `awtui-e2e-${TAG}`
138
+ await type(`/channel new ${channelName} --lead 领航员 全局tui端到端频道`)
139
+ check('2.2 /channel new 创建成功', await waitTui('✔ 频道已创建', 30_000))
140
+ check('2.3 自动切换到新频道', await waitTui(`已切换到频道「${channelName}」`, 30_000))
141
+
142
+ // 2.4 [新交互] /channel use 无参 → 频道选择器 → 方向键导航到目标频道 → Enter
143
+ // (SelectList 的可打印字符过滤不生效,导航用方向键;序号按 REST 顺序计算)
144
+ if (USE_REPO) {
145
+ await type('/channel use')
146
+ check('2.4 /channel use 弹出频道选择器', await waitTui('选择要进入的频道', 20_000))
147
+ const order = (await api('GET', '/api/workshop/channels', { token })).data ?? []
148
+ const downTimes = Math.max(0, order.findIndex(c => c.name === channelName))
149
+ for (let i = 0; i < downTimes; i++) {
150
+ vt.emitInput('\x1b[B')
151
+ await sleep(150)
152
+ }
153
+ await sleep(200)
154
+ vt.emitInput('\r')
155
+ await sleep(800)
156
+ check('2.5 方向键选择后频道正确', await waitTui(`已切换到频道「${channelName}」`, 20_000))
157
+ }
158
+
159
+ // 3. Agent 操作:成员列表出现内联建的 omp lead(选择器切换后刷新有竞态,轮询重发)
160
+ {
161
+ const ok = await waitUntil('3.1 /agents 出现 lead 成员', async () => {
162
+ await type('/agents')
163
+ return vt.text().includes('领航员(lead)') ? true : null
164
+ }, 30_000).catch(() => null)
165
+ check('3.1 /agents 出现 lead 成员', ok === true)
166
+ }
167
+
168
+ // 4. 任务下发:普通文本 → lead(触发 omp spawn)
169
+ await type('请保持待命,等待操作员通过终端下达进一步指令。')
170
+ console.log(' … 等待 omp spawn(冷启动可达 2-3 分钟)…')
171
+ const ch = (await api('GET', '/api/workshop/channels', { token })).data?.find?.(c => c.name === channelName)
172
+ ?? (await api('GET', '/api/workshop/channels', { token })).data?.at(-1)
173
+ const channelId = ch?.id
174
+ const members = channelId ? (await api('GET', `/api/workshop/channels/${channelId}/agents`, { token })).data ?? [] : []
175
+ const lead = members.find(a => a.role === 'lead')
176
+ check('4.1 频道/lead 实例可查(REST 交叉核对)', Boolean(lead?.id), `lead=${lead?.id?.slice(0, 8)}`)
177
+
178
+ const spawnOk = await waitUntil('4.2 omp 进程 spawn', async () => {
179
+ const mon = await api('GET', '/api/system/monitor', { token })
180
+ return (mon.data?.processes ?? []).find(p => p.harness === 'omp' && p.alive && p.agentId === lead?.id) ? true : null
181
+ }, 300_000).catch(() => null)
182
+ check('4.2 omp 进程 spawn(REST 监控)', spawnOk === true)
183
+
184
+ // 5. 独立 monitor:/monitor 领航员 → 终端面板接入
185
+ await type('/monitor 领航员')
186
+ check('5.1 /monitor 面板开启', await waitTui('监控已开启:领航员', 20_000))
187
+ const termConnected = await waitTui('已接入 领航员', 120_000)
188
+ check('5.2 监控面板接入 omp 会话(term.init)', termConnected)
189
+
190
+ // 6. HITL:等首回合结束 → /send 注入 ask → /hitl 定位作答
191
+ const idleOk = await waitUntil('6.1 lead 首回合结束(idle)', async () => {
192
+ const terms = await api('GET', `/api/workshop/channels/${channelId}/terminals`, { token })
193
+ const mine = (terms.data ?? []).find(t2 => t2.agentId === lead?.id)
194
+ return mine && mine.running === false ? true : null
195
+ }, 300_000).catch(() => null)
196
+ check('6.1 lead 空闲(可注入)', idleOk === true)
197
+ await sleep(1500)
198
+ await type(`/send 领航员 ${ASK_INSTRUCTION}`)
199
+ check('6.2 /send 直发回执', await waitTui('✔ 已直发 领航员', 30_000))
200
+
201
+ const item = await waitUntil('6.3 HITL 待办到达(REST Deploy?)', async () => {
202
+ const p = await api('GET', `/api/workshop/hitl/pending?channelId=${channelId}`, { token })
203
+ return (p.data?.items ?? []).find(i => String(i.title).includes('Deploy?')) ?? null
204
+ }, 300_000).catch(() => null)
205
+ check('6.3 omp ask → 全局待办(Deploy?)', Boolean(item), item ? `method=${item.method} options=${JSON.stringify(item.options)}` : 'timeout')
206
+
207
+ await type('/hitl')
208
+ check('6.4 /hitl 列表渲染', await waitTui('待人工处理', 20_000))
209
+ const row = await waitUntil('6.5 列表出现 Deploy? 行', () => {
210
+ const m = vt.text().match(/(\d+)\. \[omp-dialog\] Deploy\?/)
211
+ return m ? m[1] : null
212
+ }, 60_000).catch(() => null)
213
+ check('6.5 目标待办行定位', Boolean(row), `序号=${row}`)
214
+ await type(`/hitl ${row}`)
215
+ check('6.6 进入作答卡', await waitTui('HITL 作答', 20_000))
216
+ await type('1')
217
+ check('6.7 应答提交回执(选项 1 = go)', await waitTui('应答已提交', 30_000))
218
+ if (failures > 0) console.log('[diag 6.7 tail]', JSON.stringify(vt.text().slice(-700)))
219
+
220
+ const cleared = await waitUntil('6.8 REST 待办不再含 Deploy?', async () => {
221
+ const p = await api('GET', `/api/workshop/hitl/pending?channelId=${channelId}`, { token })
222
+ return ((p.data?.items ?? []).every(i => !String(i.title).includes('Deploy?'))) ? true : null
223
+ }, 60_000).catch(() => null)
224
+ check('6.8 应答写入服务端(待办落定)', cleared === true)
225
+
226
+ // 6.9 答案回传 agent 铁证:ask 工具结果含 go(独立终端 WS 帧)
227
+ const term = openTerm(lead.id, channelId, token)
228
+ const askFed = await waitUntil('6.9 ask 工具收到 go(终端帧)', () => {
229
+ const list = term.frames().filter(f => f.frame.type === 'tool_execution_end' && String(f.frame.toolName) === 'ask')
230
+ const lastAsk = list.at(-1)
231
+ return lastAsk ? String(lastAsk.frame.result ?? '').toLowerCase().includes('go') : null
232
+ }, 120_000).catch(() => null)
233
+ check('6.9 闭环:答案回传 agent(ask 结果含 go)', Boolean(askFed))
234
+
235
+ // 6.10-6.14 [新交互] Tab 目标选择器 + 通信/任务分流(仓库源码特性)
236
+ if (USE_REPO) {
237
+ vt.emitInput('\t')
238
+ check('6.10 Tab 呼出目标选择器', await waitTui('选择对话目标', 20_000))
239
+ vt.emitInput('\x1b[B')
240
+ await sleep(200)
241
+ vt.emitInput('\r')
242
+ check('6.11 选成员 → 通信模式 + 自动开监控', await waitTui('监控已开启:领航员', 20_000) && await waitTui('@领航员(通信)', 20_000))
243
+ await type('你在吗?看到请回应')
244
+ check('6.12 普通文本按通信直发目标', await waitTui('通信消息已发 → @领航员', 30_000))
245
+ vt.emitInput('\t')
246
+ await waitTui('选择对话目标', 20_000)
247
+ vt.emitInput('\r')
248
+ await sleep(400)
249
+ check('6.13 选频道 → 重置任务模式', await waitTui('频道(任务)', 20_000))
250
+ await type('/msg 心跳检查,请确认收到')
251
+ check('6.14 /msg 通信消息(目标重置后路由 lead)', await waitTui('通信消息已发 → @', 30_000))
252
+ }
253
+
254
+ // 7. 正式任务 + 任务列表
255
+ await type('/task 汇报当前待命状态')
256
+ check('7.1 /task 正式任务提交', await waitTui('✔ 任务已发布', 30_000))
257
+ await type('/tasks')
258
+ check('7.2 /tasks 列表出现任务', await waitTui('汇报当前待命状态', 30_000))
259
+
260
+ // 8. monitor 关闭 + 清理
261
+ await type('/monitor off')
262
+ check('8.1 /monitor off 关闭', await waitTui('监控面板已关闭', 20_000))
263
+ await api('DELETE', `/api/workshop/channels/${channelId}?purge=1`, { token }).catch(() => {})
264
+ await api('DELETE', `/api/workshop/channels/${bootCh.channelId}?purge=1`, { token }).catch(() => {})
265
+
266
+ console.log(`\n━━━ 结果: ${passed} passed / ${failures} failed ━━━`)
267
+ console.log('(经 /quit 退出全局 TUI 进程)')
268
+ if (failures > 0) process.exit(1)
269
+ await type('/quit')
270
+ console.log('[warn] /quit 未按预期退出,兜底退出')
271
+ process.exit(0)
272
+ }
273
+
274
+ main().catch((err) => {
275
+ console.error('FATAL', err.message)
276
+ process.exit(1)
277
+ })