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 +1 -1
- package/src/events.mjs +41 -11
- package/src/opencode-config.mjs +23 -1
- package/src/watch.mjs +3 -2
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
if (!value || !aliases.size) return false
|
|
479
|
+
if (!value || !(selfAliases || []).some((alias) => cleanAlias(alias))) return false
|
|
450
480
|
|
|
451
|
-
const
|
|
452
|
-
const self =
|
|
481
|
+
const parsed = conversationMentions(value, selfAliases)
|
|
482
|
+
const self = parsed.self[0]
|
|
453
483
|
if (!self) return false
|
|
454
|
-
const later =
|
|
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()
|
package/src/opencode-config.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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.
|