openvisio-agent 0.3.6 → 0.4.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.
package/bin/cli.mjs CHANGED
@@ -26,7 +26,7 @@ Connect your coding agent to an OpenVisio team.
26
26
  Usage:
27
27
  openvisio-agent connect <ovs_code> --host <url> [--name "<agent>"] [--mcp-url <url>]
28
28
  openvisio-agent connect --backend <url> --key <api-key> --id <identifier> [--name "<agent>"] [--ws <wss-url>] [--mcp-url <url>]
29
- openvisio-agent watch --name <agent> [--install] [--workdir <repo>] [--debug]
29
+ openvisio-agent watch --name <agent> [--install] [--workspace <dir>] [--chat-only] [--debug]
30
30
  openvisio-agent --help | --version
31
31
 
32
32
  connect
@@ -45,14 +45,23 @@ connect --backend
45
45
  — enables real-time autonomy (task:assigned / agent:mention).
46
46
  --mcp-url <url> registers the openvisio-team MCP so the agent has tools to ACT
47
47
  on those events. Needs Node >= 21 for the WebSocket.
48
- --workdir <repo> the always-on listener may do real coding on a branch there.
48
+ --workspace <dir> where the agent works. CODE mode is ON BY DEFAULT it finds
49
+ the org's repos here (cloning any it doesn't have yet), branches,
50
+ pushes its agent/* branch and opens PRs. Defaults to
51
+ ~/openvisio-workspace; point it at an existing clones folder to
52
+ reuse those. (--workdir is an accepted alias.)
53
+ --chat-only disable code work — chat/ticket tools only.
49
54
  --no-service skip the background service — just save config + print the
50
55
  watch commands to run yourself.
51
56
 
52
57
  watch
53
- Runs the event-driven autonomy loop (reply to mentions, pick up tickets). Add
54
- --install to run it in the background on login. Add --workdir <repo> to let it do
55
- real work on a git branch (never pushes).
58
+ Runs the event-driven autonomy loop (reply to mentions, pick up tickets). CODE
59
+ mode is ON BY DEFAULT: the agent finds the org's repos in its workspace
60
+ (~/openvisio-workspace, cloning any it doesn't have yet), branches, commits, pushes
61
+ its agent/* branch, opens PRs, then circles back with the PR link and @mentions the
62
+ requester — never touching main/master, never force-pushing or merging. No per-repo
63
+ setup needed. Use --workspace <dir> to relocate it (e.g. an existing clones folder),
64
+ --chat-only to disable code work, and --install to run in the background on login.
56
65
 
57
66
  Docs: https://www.npmjs.com/package/openvisio-agent`
58
67
 
@@ -100,16 +109,19 @@ async function runConnect({ positional, flags }) {
100
109
  // A scoped MCP config (for the watcher's --strict-mcp-config) + a saved profile.
101
110
  const mcpCfg = mcpConfigPath(slug)
102
111
  writeJson(mcpCfg, { mcpServers: { 'openvisio-team': { type: 'http', url: mcpUrl, headers: { Authorization: `Bearer ${key}` } } } }, true)
103
- writeJson(configPath(slug), { host: stripSlash(host), key, mcpUrl, name, slug, mcpConfig: mcpCfg }, true)
112
+ const wsWorkdir = flags.workdir === true ? process.cwd() : flags.workdir ? String(flags.workdir) : flags.workspace ? String(flags.workspace) : ''
113
+ writeJson(configPath(slug), { host: stripSlash(host), key, mcpUrl, name, slug, mcpConfig: mcpCfg, ...(wsWorkdir ? { workspace: wsWorkdir } : {}), ...(flags['chat-only'] ? { chatOnly: true } : {}) }, true)
104
114
 
105
115
  ok(`Connected "${name}" to ${host}.`)
106
116
  info()
107
117
  info('Claude Code now has the openvisio-team tools. Run /mcp in Claude Code to confirm.')
108
118
  info()
109
- info('To let it work on its own (reply to mentions, pick up tickets):')
110
- info(` openvisio-agent watch --name ${slug} # run now, in this terminal`)
111
- info(` openvisio-agent watch --name ${slug} --install # run in the background on login`)
112
- info(` openvisio-agent watch --name ${slug} --workdir <repo> # allow real coding on a branch`)
119
+ info('To let it work on its own (reply to mentions, pick up tickets, AND do real')
120
+ info('coding it clones/branches/pushes and opens PRs out of the box):')
121
+ info(` openvisio-agent watch --name ${slug} # run now, in this terminal`)
122
+ info(` openvisio-agent watch --name ${slug} --install # background, auto-start on login`)
123
+ info(` openvisio-agent watch --name ${slug} --workspace <dir> # reuse an existing clones folder`)
124
+ info(` openvisio-agent watch --name ${slug} --chat-only # disable code work (chat/tickets only)`)
113
125
  }
114
126
 
115
127
  // Backend mode — for agents created against the OpenVisio ORG BACKEND
@@ -140,6 +152,13 @@ async function runConnectBackend({ flags }) {
140
152
  const name = (flags.name && String(flags.name)) || 'backend-agent'
141
153
  const slug = slugify(name)
142
154
 
155
+ // Code workspace preference (optional). CODE mode is on by default; this only
156
+ // pins WHERE the agent works. Persisted so the always-on `watch` reuses it.
157
+ const chatOnly = !!flags['chat-only']
158
+ const workdir = flags.workdir === true ? process.cwd()
159
+ : flags.workdir ? String(flags.workdir)
160
+ : flags.workspace ? String(flags.workspace) : ''
161
+
143
162
  // Optional real-time autonomy: --ws is the org's API-Gateway WS base (same value
144
163
  // as the frontend's NEXT_PUBLIC_BACKEND_WS_URL). --mcp-url gives the agent a tool
145
164
  // surface so WS events (task:assigned / agent:mention) can drive a Claude cycle.
@@ -157,7 +176,7 @@ async function runConnectBackend({ flags }) {
157
176
  writeJson(mcpConfig, { mcpServers: { 'openvisio-team': { type: 'http', url: mcpUrl, headers: { 'x-agent-api-key': apiKey, 'x-agent-identifier': identifier } } } }, true)
158
177
  }
159
178
 
160
- writeJson(configPath(slug), { mode: 'backend', backend, apiKey, identifier, name, slug, wsUrl, mcpUrl, mcpConfig }, true)
179
+ writeJson(configPath(slug), { mode: 'backend', backend, apiKey, identifier, name, slug, wsUrl, mcpUrl, mcpConfig, ...(workdir ? { workspace: workdir } : {}), ...(chatOnly ? { chatOnly: true } : {}) }, true)
161
180
  // A sourceable env file, matching the setup snippet OpenVisio shows.
162
181
  const envPath = join(OV_DIR, `${slug}.env`)
163
182
  mkdirSync(OV_DIR, { recursive: true })
@@ -188,7 +207,6 @@ async function runConnectBackend({ flags }) {
188
207
  // Real-time autonomy IS the point of a backend agent: install the command and a
189
208
  // background service that auto-starts on login, so a mention is always caught —
190
209
  // no terminal left open, survives reboot. Opt out with --no-service.
191
- const workdir = flags.workdir === true ? process.cwd() : (flags.workdir ? String(flags.workdir) : '')
192
210
  if (flags['no-service'] || process.platform === 'win32') {
193
211
  ensureAgentInstalled()
194
212
  info()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "description": "Connect your coding agent (Claude Code) to an OpenVisio team — MCP tools + optional autonomy — in one command. No shell scripts.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/lib.mjs CHANGED
@@ -7,6 +7,12 @@ import { join } from 'node:path'
7
7
  import { mkdirSync, writeFileSync, readFileSync, chmodSync } from 'node:fs'
8
8
 
9
9
  export const OV_DIR = join(homedir(), '.openvisio')
10
+ // The agent's default code WORKSPACE — a single dedicated root that holds the org's
11
+ // repos as subfolders. Code mode is on by default and the agent clones missing
12
+ // repos in here, so a coding assistant works out of the box with NO per-repo config
13
+ // (override with --workdir/--workspace to point at an existing clones folder). Kept
14
+ // separate from the user's own checkouts so the agent never stomps a working tree.
15
+ export const DEFAULT_WORKSPACE = join(homedir(), 'openvisio-workspace')
10
16
  const IS_WIN = process.platform === 'win32'
11
17
 
12
18
  export function fail(msg) { console.error('✖ ' + msg); process.exit(1) }
package/src/watch.mjs CHANGED
@@ -8,19 +8,76 @@ import { spawn, spawnSync } from 'node:child_process'
8
8
  import { writeFileSync, mkdirSync } from 'node:fs'
9
9
  import { homedir } from 'node:os'
10
10
  import { join, dirname } from 'node:path'
11
- import { OV_DIR, readConfig, onPath, fail, ok, info, slugify, stripSlash, chmodSafe } from './lib.mjs'
11
+ import { OV_DIR, DEFAULT_WORKSPACE, readConfig, onPath, fail, ok, info, slugify, stripSlash, chmodSafe } from './lib.mjs'
12
12
  import { connectAgentWs, assertWebSocket } from './ws.mjs'
13
13
 
14
14
  // Behaviour prompts. The openvisio-team MCP bridge requires the agent's
15
15
  // credentials as ARGUMENTS on every tool call — those are injected at runtime by
16
16
  // loopBackendWs (see credNote), NOT baked in here, so nothing needs hunting.
17
- const CYCLE = 'Run one OpenVisio autonomy cycle.'
18
- const CYCLE_FAST = 'New chat activity in OpenVisio. If a specific mention is given above, post that reply FIRST with mcp__openvisio-team__post_message. Then call poll_inbox and handle any .followUps (thread replies you are part of, even without an @mention) — ignore .tasks/.claimable. Post AT MOST ONE reply per channel: if the same person sent several nudges, answer them together in ONE post. HONESTY: your only tools are the openvisio-team chat/ticket tools — you cannot write code or touch other tools; if asked for work you have no tool for, say so plainly in one short message (or offer to file a ticket). Never invent progress. Reply in 1-3 sentences, no summary. Then stop.'
19
- const CODE_FULL = 'Run one OpenVisio autonomy cycle. Call get_marching_orders and poll_inbox. You have file + Bash tools and a git repo at your working directory FOR CODE WORK ONLY. For assigned tickets or mentions asking for real code work: FIRST "git checkout -B agent/work", make the changes with Read/Edit/Write, run tests if present, then "git add -A && git commit -m ...". NEVER git push, merge, or touch main. Then comment_ticket with a short summary + the branch name, and post a brief channel reply. Do NOT use Bash to find credentials — they are given to you. If you truly cannot (missing repo/specs), say so in one message — never fabricate.'
20
- const CODE_FAST = 'New chat activity in OpenVisio. If a specific mention is given above, post that reply FIRST with mcp__openvisio-team__post_message — do this before any Bash. Then poll_inbox and handle .followUps (thread replies you are part of, even without an @mention), AT MOST ONE reply per channel. Only if a message asks for real CODE work in your git repo, do it on a branch (checkout -B agent/work, edit, commit locally — never push/merge) and reply with the branch name. Bash is for git/tests ONLY, never for credentials. 1-3 sentences, no summary. Then stop.'
21
-
17
+ //
18
+ // Two recurring failures these prompts fix head-on: (1) the agent UNDER-READS its
19
+ // own capabilities says "I can't check a codebase" when it can and (2) it
20
+ // PROMISES work then stops, forcing the human to remind it to circle back. The
21
+ // CHARTER blocks below assert the toolbox and mandate closing the loop in-cycle.
22
+
23
+ // ── CHAT-ONLY agents (no --workdir): chat/ticket tools, no code surface. ──────
24
+ const CHAT_CHARTER = [
25
+ 'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode. Your tools are the openvisio-team chat/ticket tools (mcp__openvisio-team__*): post_message, poll_inbox, comment_ticket, react_message. You have NO file/Bash/git tools in this mode, so you cannot write code yourself.',
26
+ 'WORK ETHIC — behave like a dependable teammate: never leave a promise dangling. Either ACT now (reply, or file a ticket) or say plainly you can\'t and offer to file a ticket / tag a coding agent who can. Never invent progress. Close the loop every cycle — the human should never have to remind you to circle back.',
27
+ ].join('\n')
28
+
29
+ const CYCLE = CHAT_CHARTER + '\n\nRun one OpenVisio autonomy cycle: call poll_inbox and handle mentions + follow-ups. Reply in 1-3 sentences, @mention people by their EXACT full name, at most one reply per channel. Then stop.'
30
+ const CYCLE_FAST = CHAT_CHARTER + '\n\nNew chat activity. If a specific mention is given above, post that reply FIRST with mcp__openvisio-team__post_message. Then call poll_inbox and handle any .followUps (thread replies you are part of, even without an @mention) — ignore .tasks/.claimable. Post AT MOST ONE reply per channel: if the same person sent several nudges, answer them together in ONE post. If asked for work you have no tool for, say so plainly and offer to file a ticket. Never invent progress. Reply in 1-3 sentences, no summary. Then stop.'
31
+
32
+ // ── CODE agents (--workdir given): full file + Bash + git/gh surface. ─────────
33
+ // A stable "who you are / how you work" charter prepended to every code cycle.
34
+ const CODE_CHARTER = [
35
+ 'YOU ARE a connected CODING agent in an OpenVisio team, running ON THE USER\'S LAPTOP. You have REAL tools — use them; do NOT claim you lack a capability without checking what you actually hold. Your toolbox:',
36
+ ' • openvisio-team chat/ticket tools (mcp__openvisio-team__*) — read mentions, post messages, comment on tickets, react.',
37
+ ' • Read / Grep / Glob / Edit / Write / MultiEdit — inspect AND change code.',
38
+ ' • Bash — git (branch, commit, push a branch), gh (clone repos, open PRs), run tests/builds.',
39
+ 'YOUR WORKSPACE: your working directory is a WORKSPACE ROOT that holds the org\'s repos as subfolders. You are NOT limited to one repo — you work across ALL of them from here. For any task: find the relevant repo under the workspace; if it isn\'t cloned yet, CLONE it (gh repo clone <org>/<repo>, or git clone <url>) and then work inside that subfolder. Never make the user tell you a path or clone things for you.',
40
+ 'CAPABILITY CHECK: before you EVER answer "I can\'t do that", verify against the tools above. If a tool exists for it, DO it. To be explicit: you CAN read/inspect any of the org\'s codebases, clone a repo you don\'t have yet, work on it, create a branch, and raise a PR — say YES to these and then actually do them.',
41
+ '',
42
+ 'WORK ETHIC — how a reliable teammate behaves (this is the difference between useful and ignored):',
43
+ ' 1. CLOSE THE LOOP in THIS cycle. Never say "I\'ll do X" and stop. If you commit to something, do it NOW — the human must never have to remind you to circle back.',
44
+ ' 2. FINISH, then REPORT. Your LAST action every cycle is a status back to the requester: comment_ticket with what you did (branch + PR link + test result) AND a short channel reply that @mentions the person who asked, by their EXACT full name (a mention only links on an exact full-name match).',
45
+ ' 3. Be honest and specific. Never invent progress. If you are genuinely blocked (missing repo, unclear spec, a failing tool), say exactly what you need in one message — that IS closing the loop.',
46
+ ' 4. One reply per channel per cycle; answer several nudges together.',
47
+ ].join('\n')
48
+
49
+ const CODE_FULL = CODE_CHARTER + '\n\n' + [
50
+ 'THIS CYCLE: call get_marching_orders and poll_inbox to see assigned tickets + mentions, then act on them.',
51
+ 'For real code work (an assigned ticket, or a mention asking for changes), run the full flow end-to-end:',
52
+ ' 1. GET THE CODE: locate the target repo under your workspace root. If it\'s already a subfolder, cd in and `git pull`; if it isn\'t cloned yet, clone it into the workspace (gh repo clone <org>/<repo>, or git clone <url>) and cd in. Do this yourself — never ask the user for a path.',
53
+ ' 2. BRANCH: git checkout -B agent/<short-task-slug>. NEVER work on, commit to, or push main/master.',
54
+ ' 3. CHANGE + VERIFY: Read/Edit/Write the files; run the tests or build if the repo has them.',
55
+ ' 4. COMMIT + PUSH YOUR BRANCH: git add -A && git commit -m "…"; then git push -u origin agent/<slug>. Only ever push your own agent/* branch. Never --force, never push to main/master, never merge.',
56
+ ' 5. RAISE A PR: gh pr create --fill --base <default-branch> --head agent/<slug> (a clear title + a body summarizing the change and how you verified it). Never gh pr merge.',
57
+ ' 6. CLOSE THE LOOP: comment_ticket with the summary + PR link/branch, and post a channel reply that @mentions the requester by their exact full name.',
58
+ 'Bash is for git / gh / tests / clone ONLY — never to hunt for credentials (they are given to you above).',
59
+ ].join('\n')
60
+
61
+ const CODE_FAST = CODE_CHARTER + '\n\n' + [
62
+ 'New chat activity. If a specific mention is given above, post that reply FIRST with mcp__openvisio-team__post_message — before any Bash.',
63
+ 'Then poll_inbox and handle .followUps (thread replies you are part of, even without an @mention), at most one reply per channel.',
64
+ 'If a message asks for real CODE work, do the WHOLE job now — branch (checkout -B agent/<slug>), edit, run tests, commit, push your branch, open a PR (gh pr create), then reply with the PR link and @mention the requester by exact full name. Never push to main, never --force, never merge.',
65
+ 'Do NOT promise and stop — finish and report in THIS cycle. Reply 1-3 sentences, no summary. Then stop.',
66
+ ].join('\n')
67
+
68
+ // Bash covers git/gh/clone/tests; the deny list is where the guardrails live.
22
69
  const CODE_TOOLS = ['Read', 'Grep', 'Glob', 'Edit', 'Write', 'MultiEdit', 'TodoWrite', 'Bash', 'mcp__openvisio-team__*']
23
- const DENY_TOOLS = ['Bash(git push:*)', 'Bash(git reset --hard:*)', 'Bash(git clean:*)', 'Bash(rm:*)', 'Bash(sudo:*)', 'Bash(chmod:*)', 'Bash(curl:*)', 'Bash(wget:*)', 'Bash(npm publish:*)', 'Bash(pnpm publish:*)', 'Bash(gh pr merge:*)', 'Bash(gh repo:*)']
70
+ // Push + PR creation ARE allowed (agents raise PRs), but main/master, force-pushes,
71
+ // merges and destructive/publishing/repo-deleting commands stay blocked.
72
+ const DENY_TOOLS = [
73
+ 'Bash(git push --force:*)', 'Bash(git push -f:*)', 'Bash(git push --force-with-lease:*)',
74
+ 'Bash(git push origin main:*)', 'Bash(git push origin master:*)',
75
+ 'Bash(git push origin HEAD:main:*)', 'Bash(git push origin HEAD:master:*)',
76
+ 'Bash(git reset --hard:*)', 'Bash(git clean:*)',
77
+ 'Bash(rm:*)', 'Bash(sudo:*)', 'Bash(chmod:*)', 'Bash(curl:*)', 'Bash(wget:*)',
78
+ 'Bash(npm publish:*)', 'Bash(pnpm publish:*)',
79
+ 'Bash(gh pr merge:*)', 'Bash(gh repo delete:*)',
80
+ ]
24
81
 
25
82
  const FAST = 2500
26
83
  const SLOW = 6000
@@ -29,15 +86,29 @@ const MAX_TURNS = 15
29
86
  const SESSION_IDLE_MS = 1200000
30
87
  // A single cycle must finish within this or it's abandoned — otherwise a hung
31
88
  // cycle (e.g. an MCP tool stalling on a down bridge) would leave `busy` stuck
32
- // true forever and silently queue every later mention.
89
+ // true forever and silently queue every later mention. Code cycles get a much
90
+ // longer budget: a real clone → branch → test → push → PR flow legitimately takes
91
+ // minutes, and cutting it off mid-job is itself a "never circled back" failure.
33
92
  const MAX_CYCLE_MS = 240000
93
+ const MAX_CODE_CYCLE_MS = 900000
34
94
 
35
95
  export async function runWatch({ flags }) {
36
96
  const slug = flags.name ? slugify(String(flags.name)) : null
37
97
  const saved = slug ? readConfig(slug) : null
38
98
  const claude = String(flags.claude || onPath('claude') || 'claude')
39
99
  const mcpConfig = String(flags['mcp-config'] || (saved && saved.mcpConfig) || '')
40
- const workdir = flags.workdir === true ? process.cwd() : (flags.workdir ? String(flags.workdir) : '')
100
+ // Code mode is the DEFAULT: an agent lives on the user's laptop and should just be
101
+ // able to work across the org's repos with no per-repo setup. So `workdir` resolves
102
+ // to (in order) an explicit --workdir/--workspace, the saved workspace, or the
103
+ // shared DEFAULT_WORKSPACE — always non-empty unless the agent is opted into
104
+ // --chat-only. It's a WORKSPACE ROOT (holds repos as subfolders + clones missing
105
+ // ones), not a single repo. Created on demand so the Claude session can cwd into it.
106
+ const chatOnly = !!flags['chat-only'] || (saved && saved.chatOnly === true)
107
+ const explicitWorkdir = flags.workdir === true ? process.cwd()
108
+ : flags.workdir ? String(flags.workdir)
109
+ : flags.workspace ? String(flags.workspace) : ''
110
+ const workdir = chatOnly ? '' : (explicitWorkdir || (saved && (saved.workspace || saved.workdir)) || DEFAULT_WORKSPACE)
111
+ if (workdir) { try { mkdirSync(workdir, { recursive: true }) } catch { /* best-effort; spawn will surface a real problem */ } }
41
112
 
42
113
  // Backend agents (connect --backend) drive autonomy over a real-time WS instead
43
114
  // of REST-polling the frontend relay. Detected by the saved mode / a --ws flag.
@@ -67,6 +138,7 @@ export async function runWatch({ flags }) {
67
138
  // after MAX_TURNS or SESSION_IDLE_MS. Returns { runCycle, canCode }.
68
139
  function createCycleRunner({ claude, mcpConfig, workdir, log, debug }) {
69
140
  const canCode = !!workdir
141
+ const maxCycleMs = canCode ? MAX_CODE_CYCLE_MS : MAX_CYCLE_MS
70
142
  let child = null
71
143
  let turnsThisSession = 0
72
144
  let sessionStartedAt = 0
@@ -114,7 +186,9 @@ function createCycleRunner({ claude, mcpConfig, workdir, log, debug }) {
114
186
  })
115
187
  c.on('exit', (code) => { if (c !== child) { log('old session exited ' + code); return } log('session exited ' + code); child = null; settleTurn({ type: 'result', subtype: 'exit' }) })
116
188
  c.on('error', () => { if (c !== child) return; child = null; settleTurn({ type: 'result', subtype: 'error' }) })
117
- log('warm session started')
189
+ log('warm session started' + (canCode
190
+ ? ' [CODE mode — workspace ' + workdir + ' — finds/clones the org\'s repos here, branches, pushes, opens PRs]'
191
+ : ' [CHAT-ONLY mode (--chat-only) — chat/ticket tools only, no code work]'))
118
192
  }
119
193
 
120
194
  function runCycle(prompt) {
@@ -131,11 +205,11 @@ function createCycleRunner({ claude, mcpConfig, workdir, log, debug }) {
131
205
  // and queued mentions can proceed.
132
206
  clearCycleTimer()
133
207
  cycleTimer = setTimeout(() => {
134
- log('cycle TIMED OUT after ' + Math.round(MAX_CYCLE_MS / 1000) + 's — killing the session so the queue can proceed')
208
+ log('cycle TIMED OUT after ' + Math.round(maxCycleMs / 1000) + 's — killing the session so the queue can proceed')
135
209
  try { child && child.kill() } catch { /* already gone */ }
136
210
  child = null
137
211
  settleTurn({ type: 'result', subtype: 'timeout' })
138
- }, MAX_CYCLE_MS)
212
+ }, maxCycleMs)
139
213
  try { child.stdin.write(JSON.stringify({ type: 'user', message: { role: 'user', content: prompt } }) + '\n') }
140
214
  catch { settleTurn({ type: 'result', subtype: 'write-failed' }) }
141
215
  })
@@ -158,7 +232,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
158
232
  let busy = false
159
233
  let queued = null // 'full' | 'fast' — a cycle requested while one was running
160
234
  let handle = null
161
- let lastActivityAt = Date.now()
162
235
  // Context lines from the events themselves (the WS payload already carries the
163
236
  // channel + message / task), so the agent acts on THEM directly instead of
164
237
  // hoping poll_inbox re-surfaces the same item. Accumulated across coalesced
@@ -168,7 +241,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
168
241
  // The Mastra bridge authenticates per-CALL, not per-connection: every
169
242
  // openvisio-team tool needs agent_identifier + agent_api_key as arguments.
170
243
  // Hand them to the model up front so it never shells around hunting for them.
171
- const credNote = `AUTH: the openvisio-team (mcp__openvisio-team__*) tools REQUIRE two arguments on EVERY call — agent_identifier: "${identifier}" and agent_api_key: "${apiKey}". Include BOTH on every openvisio-team tool call (post_message, poll_inbox, react_message, comment_ticket, …). They are given to you right here: do NOT search for them, do NOT run Bash/shell/grep/cat/find, do NOT read memory just call the tools with these exact values.`
244
+ const credNote = `AUTH: the openvisio-team (mcp__openvisio-team__*) tools REQUIRE two arguments on EVERY call — agent_identifier: "${identifier}" and agent_api_key: "${apiKey}". Include BOTH on every openvisio-team tool call (post_message, poll_inbox, react_message, comment_ticket, …). They are given to you right here do NOT hunt for them (no Bash/grep/cat/find to locate credentials, no reading memory); just call the tools with these exact values. (Bash/git/gh ARE for your code work — this rule is only about not searching for these keys.)`
172
245
 
173
246
  async function drain(kind, context) {
174
247
  if (context) pending.push(context)
@@ -196,7 +269,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
196
269
 
197
270
  function onEvent(k, d) {
198
271
  const raw = d && typeof d === 'object' ? d : {}
199
- lastActivityAt = Date.now()
200
272
  if (k === 'task:assigned') {
201
273
  const t = raw.task || {}
202
274
  log('task:assigned ' + (t.id != null ? '#' + t.id + ' “' + (t.title || '') + '”' : ''))
@@ -207,34 +279,36 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
207
279
  const msg = raw.message && typeof raw.message === 'object' ? raw.message : {}
208
280
  const text = String(msg.content || msg.body || msg.text || '').replace(/\s+/g, ' ').slice(0, 600)
209
281
  const who = senderName(msg)
210
- log('agent:mention in channel ' + (cid != null ? cid : '?'))
282
+ // Reply IN THE SAME THREAD: parent is the thread root (the message's own id
283
+ // for a top-level mention, or its parent when the mention is itself a reply).
284
+ const mid = msg.id != null ? msg.id : (msg.message_id != null ? msg.message_id : null)
285
+ const parent = msg.parent_id != null ? msg.parent_id : (msg.parentId != null ? msg.parentId : null)
286
+ const threadRoot = parent != null ? parent : mid
287
+ log('agent:mention in channel ' + (cid != null ? cid : '?') + (threadRoot != null ? ' (thread ' + threadRoot + ')' : ''))
211
288
  const ctx = cid != null
212
- ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by ${who}` : ''}: "${text}". Your FIRST action must be mcp__openvisio-team__post_message with channel_id ${cid}, a 1-3 sentence reply, and the agent_identifier + agent_api_key from the AUTH line above. You already have the message here — do NOT poll_inbox to find it. After posting, you MAY poll_inbox once for other .followUps.`
289
+ ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". Reply with mcp__openvisio-team__post_message as your FIRST action arguments: channel_id ${cid}${threadRoot != null ? `, parent_id ${threadRoot} (reply IN THAT THREAD, do not start a new top-level message)` : ''}, plus agent_identifier + agent_api_key from the AUTH line above, and a 1-3 sentence reply.${who ? ` To @mention them back, write their EXACT full name "@${who}" (a mention only links when the name matches exactly — "@${who.split(' ')[0]}" alone will NOT).` : ''} You already have the message here — do NOT poll_inbox to find it.`
213
290
  : undefined
214
291
  void drain('fast', ctx)
292
+ } else if (k === 'error') {
293
+ // Surface the backend's rejection detail instead of a bare "event error".
294
+ log('error event: ' + JSON.stringify(raw).slice(0, 220))
215
295
  } else {
216
296
  log('event ' + k)
217
297
  }
218
298
  }
219
299
 
220
- // Follow-up sweep: thread replies you are part of get NO @mention (no WS event
221
- // fires), so poll the inbox on a slow cadence to catch .followUps — but ONLY
222
- // within a short window after real activity, so an idle agent costs nothing.
223
- const FOLLOWUP_MS = 60_000
224
- const ACTIVE_WINDOW_MS = 5 * 60_000
225
- const sweep = setInterval(() => {
226
- if (busy) return
227
- if (Date.now() - lastActivityAt > ACTIVE_WINDOW_MS) return // idle → don't spend a cycle
228
- void drain('fast', 'Follow-up check: call poll_inbox and reply to any NEW .followUps (thread replies you are part of, even with no @mention) or unanswered .mentions. If there is nothing new, do nothing and stop — do NOT post.')
229
- }, FOLLOWUP_MS)
230
-
300
+ // NO polling the WebSocket is the only trigger (task:assigned / agent:mention).
301
+ // A thread reply that @mentions the agent fires agent:mention and is handled
302
+ // in-thread above; genuinely un-mentioned thread activity has no WS event, so
303
+ // it's a backend concern (dispatch a thread event to participant agents), not a
304
+ // reason to poll.
231
305
  log('up — backend WS watcher on ' + wsUrl + (canCode ? ' [code: ' + workdir + ']' : ''))
232
306
  handle = connectAgentWs({ wsUrl, apiKey, identifier, onEvent, log })
233
307
 
234
308
  return new Promise(() => {
235
309
  // Run until killed. Tidy up the socket on termination so a restarting
236
310
  // service doesn't leak a half-open connection.
237
- const bye = () => { clearInterval(sweep); try { handle && handle.close() } catch { /* noop */ } process.exit(0) }
311
+ const bye = () => { try { handle && handle.close() } catch { /* noop */ } process.exit(0) }
238
312
  process.on('SIGTERM', bye)
239
313
  process.on('SIGINT', bye)
240
314
  })