openvisio-agent 0.18.8 → 0.18.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.18.8",
3
+ "version": "0.18.9",
4
4
  "description": "Connect Claude Code, Codex, or OpenCode to an OpenVisio team — MCP tools + optional autonomy — in one command.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/events.mjs CHANGED
@@ -389,6 +389,34 @@ export function renderedAgentMessages(value, identity = {}) {
389
389
 
390
390
  const cleanAlias = (value) => String(value || '').trim().toLowerCase().replace(/^@/, '')
391
391
 
392
+ const escapeRegex = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
393
+
394
+ // Mentions may use a display name containing spaces (for example,
395
+ // `@Alex Morgan`). The generic handle matcher sees only `@Alex`, so find this
396
+ // agent's known aliases first and discard overlapping generic matches. This
397
+ // keeps recipient checks exact without reducing a full display name to an
398
+ // ambiguous first name.
399
+ function conversationMentions(value, selfAliases) {
400
+ const text = String(value || '')
401
+ const aliases = [...new Set((selfAliases || []).map(cleanAlias).filter(Boolean))]
402
+ .sort((a, b) => b.length - a.length)
403
+ const self = []
404
+ for (const alias of aliases) {
405
+ // A sentence-ending period is punctuation, while a period followed by a
406
+ // handle character is still part of an alias such as `alex.morgan`.
407
+ const pattern = new RegExp(`@${escapeRegex(alias)}(?![a-z0-9_-]|\\.(?=[a-z0-9]))`, 'gi')
408
+ for (const match of text.matchAll(pattern)) {
409
+ const index = match.index ?? 0
410
+ const end = index + match[0].length
411
+ if (!self.some((item) => index >= item.index && end <= item.end)) self.push({ name: alias, index, end })
412
+ }
413
+ }
414
+ const other = [...text.matchAll(/@([a-z0-9](?:[a-z0-9_.-]*[a-z0-9_-])?)(?=$|[^a-z0-9_.-]|\.(?:\s|$))/gi)]
415
+ .map((match) => ({ name: cleanAlias(match[1]), index: match.index ?? 0, end: (match.index ?? 0) + match[0].length }))
416
+ .filter((mention) => !self.some((item) => mention.index >= item.index && mention.end <= item.end))
417
+ return { self, other, all: [...self, ...other].sort((a, b) => a.index - b.index) }
418
+ }
419
+
392
420
  export function messageSenderIsAgent(message) {
393
421
  const m = message && typeof message === 'object' ? message : {}
394
422
  if (m.senderAgent || m.sender_agent || m.agent || m.agent_id != null || m.sender_agent_id != null || m.senderAgentId != null) return true
@@ -404,11 +432,7 @@ export function messageSenderIsAgent(message) {
404
432
  export function classifyConversationTarget(message, selfAliases) {
405
433
  const m = message && typeof message === 'object' ? message : {}
406
434
  const text = String(m.content ?? m.body ?? m.text ?? m.message ?? '').replace(/\s+/g, ' ').trim()
407
- const aliases = new Set((selfAliases || []).map(cleanAlias).filter(Boolean))
408
- const mentions = [...text.matchAll(/@([a-z0-9](?:[a-z0-9_.-]*[a-z0-9_-])?)/gi)]
409
- .map((match) => ({ name: cleanAlias(match[1]), index: match.index ?? 0, end: (match.index ?? 0) + match[0].length }))
410
- const selfMentions = mentions.filter((mention) => aliases.has(mention.name))
411
- const otherMentions = mentions.filter((mention) => !aliases.has(mention.name))
435
+ const { self: selfMentions, other: otherMentions, all: mentions } = conversationMentions(text, selfAliases)
412
436
  const explicitSelf = selfMentions.length > 0
413
437
 
414
438
  if (messageSenderIsAgent(m) && !explicitSelf) {
@@ -426,7 +450,14 @@ export function classifyConversationTarget(message, selfAliases) {
426
450
  if (explicitSelf && requestTargetsLaterAgent(text, selfAliases)) {
427
451
  return { action: 'ignore', reason: 'redirected-to-later-agent', text, explicitSelf, otherMentions: otherMentions.map((item) => item.name) }
428
452
  }
429
- return { action: 'handle', reason: explicitSelf ? 'explicit-self-recipient' : 'eligible-follow-up', text, explicitSelf, otherMentions: otherMentions.map((item) => item.name) }
453
+ // A transport-level `agent:mention` can be emitted for every new message in a
454
+ // thread whose root once mentioned this agent. With no mention or explicit
455
+ // stand-down in the current message, ownership is ambiguous. Fail closed
456
+ // instead of making every agent that ever joined the thread answer it.
457
+ if (!explicitSelf) {
458
+ return { action: 'ignore', reason: 'unaddressed-thread-activity', text, explicitSelf, otherMentions: otherMentions.map((item) => item.name) }
459
+ }
460
+ return { action: 'handle', reason: 'explicit-self-recipient', text, explicitSelf, otherMentions: otherMentions.map((item) => item.name) }
430
461
  }
431
462
 
432
463
  // Require an actionable repository verb and a concrete code/repository object.
@@ -445,13 +476,12 @@ export function conversationNeedsCode(value) {
445
476
  // model starts, while keeping explicitly shared requests addressed to both.
446
477
  export function requestTargetsLaterAgent(text, selfAliases) {
447
478
  const value = String(text || '')
448
- const aliases = new Set((selfAliases || []).map((v) => String(v || '').toLowerCase().replace(/^@/, '')).filter(Boolean))
449
- if (!value || !aliases.size) return false
479
+ if (!value || !(selfAliases || []).some((alias) => cleanAlias(alias))) return false
450
480
 
451
- const mentions = [...value.matchAll(/@([a-z0-9](?:[a-z0-9_.-]*[a-z0-9_-])?)/gi)].map((match) => ({ name: match[1].toLowerCase(), index: match.index ?? 0, end: (match.index ?? 0) + match[0].length }))
452
- const self = mentions.find((mention) => aliases.has(mention.name))
481
+ const parsed = conversationMentions(value, selfAliases)
482
+ const self = parsed.self[0]
453
483
  if (!self) return false
454
- const later = mentions.find((mention) => mention.index > self.index && !aliases.has(mention.name))
484
+ const later = parsed.other.find((mention) => mention.index > self.index)
455
485
  if (!later) return false
456
486
 
457
487
  const bridge = value.slice(self.end, later.index).toLowerCase()
@@ -13,7 +13,29 @@ export function opencodeRuntimeLayout({ cfgKey, workdir, baseDir = OV_DIR }) {
13
13
 
14
14
  export function buildOpencodeConfig({ mcpUrl, mcpHeaders, canCode = true }) {
15
15
  if (!mcpUrl && canCode) return null
16
- const replyPermissions = { '*': 'deny', 'openvisio-team_*': 'allow' }
16
+ // Keep chat/reply runs genuinely MCP-only. `--auto` should honor a wildcard
17
+ // deny, but some OpenCode releases have still exposed built-ins through a
18
+ // merged/default agent configuration. Explicit denials prevent a reply cycle
19
+ // from invoking local grep (and hitting its 64 KiB JSON-record limit), reading
20
+ // the workspace, editing files, or delegating before the MCP allow-list wins.
21
+ const replyPermissions = {
22
+ '*': 'deny',
23
+ read: 'deny',
24
+ edit: 'deny',
25
+ glob: 'deny',
26
+ grep: 'deny',
27
+ list: 'deny',
28
+ bash: 'deny',
29
+ task: 'deny',
30
+ external_directory: 'deny',
31
+ todowrite: 'deny',
32
+ webfetch: 'deny',
33
+ websearch: 'deny',
34
+ lsp: 'deny',
35
+ skill: 'deny',
36
+ question: 'deny',
37
+ 'openvisio-team_*': 'allow',
38
+ }
17
39
  return {
18
40
  $schema: 'https://opencode.ai/config.json',
19
41
  ...(!canCode ? {
package/src/watch.mjs CHANGED
@@ -39,6 +39,7 @@ const REPLY_DISCIPLINE = [
39
39
  ' • NO DUPLICATES OR PICKUP NOISE. Before you post, scan the recent thread/channel for what YOU already said. If you already replied to this exact request, do NOT repeat the same message. Do not send a generic pickup acknowledgement; activity shows that work is underway. For longer code work, you may send at most one concrete progress update after work has actually begun, but that update NEVER completes the cycle: keep using tools, then send one distinct verified result or real blocker. One final answer per question.',
40
40
  ' • BE SURE BEFORE YOU SPEAK. Do not claim something is possible, done, or broken until you have actually verified it — call the tool, read the code, check the real state. Never assert then contradict yourself. If you are unsure, verify FIRST, then give ONE clear, final answer instead of thinking out loud across several messages.',
41
41
  ' • USE RECALL, NEVER INVENT IT. Before answering a context-dependent question, search the visible thread and use any available history, search, docs, or recall tools. Reuse verified context instead of asking the user to repeat it. If no record exists, say plainly "I don\'t have a record of that". Never fabricate past events, conversations, results, links, PR numbers, deploy URLs, or figures.',
42
+ ' • LOOK UP ASSIGNED WORK. If someone says they assigned you a task, asks which task is yours, or asks for its status, check the live board yourself with list_agents + list_projects + list_tasks and then get_ticket as needed. Match assignments to your authenticated agent identity. Do not ask the teammate for a project slug, ticket slug, or numeric id before trying those MCP tools; ask only if the live lookup fails or returns genuinely ambiguous matches.',
42
43
  ' • TICKET SLUGS, NEVER DATABASE IDS. In every human-facing channel message, ticket comment, PR description, summary, blocker, and result, reference a ticket by the exact project-scoped slug returned by get_ticket/list_tasks (for example, `OVS-57`). Numeric project_id and ticket_id values are internal MCP arguments only: never write `#57`, `ticket 57`, or expose a database id to teammates. If the backend omits the slug, use the ticket title or say “the ticket”; do not invent a slug.',
43
44
  ' • CLOSE CONCERNS. Never leave a concern, direct question, correction, or blocker addressed to you without a clear response. Acknowledge the concern, act if you can, then report the verified result. If blocked, name the blocker and the exact next action or owner in one message.',
44
45
  ' • WRITE PLAINLY. Prefer short sentences, commas, periods, and colons. Avoid em dashes except when reproducing quoted text.',
@@ -46,7 +47,7 @@ const REPLY_DISCIPLINE = [
46
47
 
47
48
  // ── CHAT-ONLY agents (no --workdir): chat/ticket tools, no code surface. ──────
48
49
  const CHAT_CHARTER = [
49
- 'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode. Use only tools that actually appear in your openvisio-team tool list. Backend MCP provides post_message, react_message, list_agents, list_projects, list_tasks, get_ticket, update_ticket, and list_activity. A ticket-comment tool is optional and must not be assumed. Some relay runtimes also provide poll_inbox or get_marching_orders. Never call a tool that is absent. You have NO file/Bash/git tools in this mode, so you cannot write code yourself.',
50
+ 'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode. Use only tools that actually appear in your openvisio-team tool list. Backend MCP provides post_message, react_message, list_agents, list_projects, list_tasks, get_ticket, update_ticket, and list_activity. A ticket-comment tool is optional and must not be assumed. Some relay runtimes also provide poll_inbox or get_marching_orders. Never call a tool that is absent. You have NO local read, grep, glob, list, file, Bash, git, web, or delegation tools in this mode; use the OpenVisio MCP for live team state and do not inspect the local workspace.',
50
51
  '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.',
51
52
  '',
52
53
  REPLY_DISCIPLINE,
@@ -789,7 +790,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
789
790
  const fullPrompt = (canCode ? CODE_FULL + codexPushGuide : 'Handle the supplied verified backend ticket with the available OpenVisio tools. Update or comment on the ticket as requested, do not claim repository work in chat-only mode, and stop after the verified action.') + '\n\n' + backendToolRule
790
791
  const fastPrompt = (canCode ? CODE_FAST : CYCLE_FAST) + '\n\n' + backendToolRule
791
792
  const coordinatePrompt = COORDINATE + '\n\n' + backendToolRule
792
- const guardedReplyPrompt = 'WATCHER-DELIVERED REPLY: the watcher has already verified that the current source message is addressed to you. Do not call post_message or any inbox/resource-discovery tool. Return only one natural, context-specific reply of 1-3 sentences. Do not echo the request, announce a plan, or add a generic acknowledgement. If the supplied context says the work was completed, lead with the verified result; if it is blocked, name only the real blocker and next action.' + '\n\n' + backendToolRule
793
+ const guardedReplyPrompt = 'WATCHER-DELIVERED REPLY: the watcher has already verified that the current source message is addressed to you. Do not call post_message, relay inbox tools, or MCP resource APIs. OpenVisio team-state tools are allowed: when the answer depends on live projects, assignments, tickets, or status, call list_agents/list_projects/list_tasks/get_ticket yourself before answering and never ask the teammate for a slug that those tools can resolve. Return only one natural, context-specific reply of 1-3 sentences. Do not echo the request, announce a plan, or add a generic acknowledgement. If the supplied context says the work was completed, lead with the verified result; if it is blocked, name only the real blocker and next action.' + '\n\n' + backendToolRule
793
794
  // Live model state — changeable at runtime by the in-chat `/model` command.
794
795
  // codeModel drives full/sweep cycles; chatModel (if set) the lighter fast/intro
795
796
  // ones, so routine chatter can run cheaper than real code work.