openvisio-agent 0.18.4 → 0.18.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.
package/README.md CHANGED
@@ -64,6 +64,8 @@ Codex BYO agents follow the repository's normative runtime specification in `doc
64
64
 
65
65
  An `agent:mention` event only wakes the watcher; it does not grant ownership of the conversation. The actual source message is checked before any model starts. Messages redirected to another agent and unaddressed agent chatter stay silent, while a direct stand-down cancels queued/running work for that thread. Accepted coding work uses the activity indicator instead of a generic pickup message, then returns one verified result or concrete blocker in the source thread.
66
66
 
67
+ Ticket references follow the board UI: BYO agents use the project-scoped slug, such as `OVS-57`, in messages, comments, PR descriptions, blockers, and results. Numeric `project_id` and `ticket_id` values remain internal MCP arguments and are never used as human-facing ticket names. If an older backend omits the slug, the agent uses the ticket title rather than inventing one.
68
+
67
69
  Claude uses Haiku for routine coordination and Sonnet for repository work. Codex uses `gpt-5.6-sol` for every cycle, including messages, mentions, triage, board movement, MCP calls, and coding. This intentionally favors reliability and consistent tool use over the cheaper Codex tiers.
68
70
 
69
71
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
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": {
@@ -56,6 +56,7 @@ const assertions = [
56
56
  ['stand-down and redirects cancel source-thread work across runtimes', watcher.includes('cancelThread(cid, threadRoot') && watcher.includes('activeDelivery') && watcher.includes('cancelCurrent') && watcher.includes("subtype === 'canceled'")],
57
57
  ['thread cancellation is rechecked immediately before watcher delivery', watcher.includes('const newlyCancelled = suppressCancelled()') && watcher.includes('if (newlyCancelled) return newlyCancelled')],
58
58
  ['generic coding pickup messages are absent', !watcher.includes("I've picked this up and will return here with the verified result")],
59
+ ['human-facing ticket references require slugs, never database ids', watcher.includes('TICKET SLUGS, NEVER DATABASE IDS') && watcher.includes('ticketDisplaySlug(task)') && events.includes('export function ticketDisplaySlug') && !events.includes('I finished ticket #${task.id}')],
59
60
  ['rendered backend replies are checked before every guarded thread post', watcher.includes("callMcpTool('list_message_thread'") && watcher.includes('renderedAgentMessages(live') && watcher.includes('same-content-rendered')],
60
61
  ['Codex cannot race the watcher with post_message', watcher.includes("disabledMcpTools: ['post_message']") && watcher.includes('disabled_tools = [')],
61
62
  ['reply delivery keys survive reconnects', watcher.includes('deliveredReplies: [...deliveredReplies]') && watcher.includes('deliveredReplies.has(deliveryKey)')],
package/src/events.mjs CHANGED
@@ -26,6 +26,16 @@ export function taskAgentId(task) {
26
26
  return task.agent_id != null ? task.agent_id : (task.agentId != null ? task.agentId : null)
27
27
  }
28
28
 
29
+ // Backend task ids are database plumbing. Teammates identify tickets by the
30
+ // project-scoped slug shown on the board (for example, OVS-57). Never synthesize
31
+ // a slug from the numeric id: if an older backend omits it, human-facing copy
32
+ // should use the title or “the ticket” instead of inventing a reference.
33
+ export function ticketDisplaySlug(task) {
34
+ if (!task || typeof task !== 'object') return ''
35
+ const value = task.slug ?? task.ticket_slug ?? task.ticketSlug
36
+ return typeof value === 'string' ? value.trim().toUpperCase() : ''
37
+ }
38
+
29
39
  export function taskIsCompleted(task, completedTypeIds = new Set()) {
30
40
  if (!task || typeof task !== 'object') return false
31
41
  if (task.deleted_at || task.deletedAt || task.completed_at || task.completedAt || task.closed_at || task.closedAt || task.archived_at || task.archivedAt) return true
@@ -60,7 +70,9 @@ export function buildTaskCompletionReport(task, { projectId, fallbackText = '' }
60
70
  const status = String(task.type?.name || task.task_type?.name || task.status || task.state || 'review').replace(/\s+/g, ' ').trim()
61
71
  const verification = /\bVerification:\s*([^\n]{1,180})/i.exec(evidence)?.[1]?.replace(/\s+/g, ' ').trim().replace(/[.]+$/, '') || ''
62
72
  const mention = requester ? `@${requester} ` : ''
63
- const content = `${mention}I finished ticket #${task.id} “${title}” and moved it to ${status}. PR: ${prUrl}.${verification ? ` Verification: ${verification}.` : ''}`
73
+ const slug = ticketDisplaySlug(task)
74
+ const ticketLabel = slug ? `ticket ${slug} “${title}”` : `the ticket “${title}”`
75
+ const content = `${mention}I finished ${ticketLabel} and moved it to ${status}. PR: ${prUrl}.${verification ? ` Verification: ${verification}.` : ''}`
64
76
  const revision = task.updated_at ?? task.updatedAt ?? prUrl
65
77
  return { key: `${projectId ?? task.project_id ?? task.projectId ?? '?'}:${task.id}:${revision}`, content, prUrl }
66
78
  }
package/src/watch.mjs CHANGED
@@ -10,7 +10,7 @@ import { homedir } from 'node:os'
10
10
  import { join, dirname } from 'node:path'
11
11
  import { OV_DIR, DEFAULT_WORKSPACE, readConfig, writeJson, configPath, onPath, fail, ok, info, slugify, stripSlash, chmodSafe } from './lib.mjs'
12
12
  import { connectAgentWs, assertWebSocket } from './ws.mjs'
13
- import { agentStateRequest, buildTaskCompletionReport, classifyConversationTarget, codexPolicyBlock, conversationNeedsCode, mentionDedupeKeys, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted } from './events.mjs'
13
+ import { agentStateRequest, buildTaskCompletionReport, classifyConversationTarget, codexPolicyBlock, conversationNeedsCode, mentionDedupeKeys, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted, ticketDisplaySlug } from './events.mjs'
14
14
  import { createByoMemoryGraph } from './memory.mjs'
15
15
  import { repositoryHasPrPushAuthorization } from './pr-push.mjs'
16
16
  import { createMcpHttpClient } from './mcp-http.mjs'
@@ -37,6 +37,7 @@ const REPLY_DISCIPLINE = [
37
37
  ' • 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 post again. Do not send a generic pickup acknowledgement; activity shows that work is underway. The first durable task message is a verified result, a link, or a real blocker. One answer per question.',
38
38
  ' • 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.',
39
39
  ' • 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.',
40
+ ' • 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.',
40
41
  ' • 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.',
41
42
  ' • WRITE PLAINLY. Prefer short sentences, commas, periods, and colons. Avoid em dashes except when reproducing quoted text.',
42
43
  ].join('\n')
@@ -813,7 +814,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
813
814
  let lastInboxSignature = ''
814
815
  let selfAgentId = null
815
816
  const selfAliases = new Set([slug, identifier].map((value) => String(value || '').toLowerCase()).filter(Boolean))
816
- const mcpClient = createMcpHttpClient({ url: mcpUrl, apiKey, identifier, clientVersion: '0.18.4', log })
817
+ const mcpClient = createMcpHttpClient({ url: mcpUrl, apiKey, identifier, clientVersion: '0.18.5', log })
817
818
  const callMcpTool = (name, args = {}) => mcpClient.callTool(name, args)
818
819
  let mcpToolNames = null
819
820
  let mcpToolDiscoveryPromise = null
@@ -1156,7 +1157,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
1156
1157
  } else continue
1157
1158
  }
1158
1159
  memory.remember({ key: `ticket:${project.id}:${task.id}`, kind: 'ticket', state: 'assigned', summary: task.title, refs: { projectId: project.id, ticketId: task.id }, meta: { updatedAt: task.updated_at ?? task.updatedAt } })
1159
- assigned.push({ id: task.id, projectId: project.id, project: project.name, title: task.title, priority: task.priority, typeId: task.type_id ?? task.typeId, updatedAt: task.updated_at ?? task.updatedAt })
1160
+ assigned.push({ id: task.id, slug: ticketDisplaySlug(task), projectId: project.id, project: project.name, title: task.title, priority: task.priority, typeId: task.type_id ?? task.typeId, updatedAt: task.updated_at ?? task.updatedAt })
1160
1161
  }
1161
1162
  const activities = Array.isArray(activityData.activities) ? activityData.activities : Array.isArray(activityData.activity) ? activityData.activity : []
1162
1163
  const mentionNeedles = [self.name, self.identifier, self.slug, identifier].filter(Boolean).map((s) => '@' + String(s).toLowerCase())
@@ -1200,7 +1201,8 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
1200
1201
  log('backlog reconciliation found ' + assigned.length + ' assigned task(s); queueing only ticket #' + next.id)
1201
1202
  const activityChannel = await projectStatusChannel(next.projectId)
1202
1203
  pendingCompletionReports.add(`${next.projectId}:${next.id}`); trimSeen(pendingCompletionReports); persistReplay()
1203
- void drain('full', `Backlog reconciliation verified this open ticket is assigned to YOU: ${JSON.stringify(next)}. Process this ONE ticket only. This is backlog-only work with NO source message or requester thread: do NOT call post_message yourself. Call get_ticket, use list_task_types and update_ticket to move it active, complete and verify the work, open the PR, then update the ticket with evidence and move it to review or done when appropriate. The watcher will publish exactly one verified completion result to the project channel.`, activityChannel == null ? [] : [activityChannel], { projectId: next.projectId, ticketId: next.id, channelId: activityChannel })
1204
+ const humanTicketRef = next.slug ? `ticket ${next.slug}` : `the ticket “${next.title}”`
1205
+ void drain('full', `Backlog reconciliation verified ${humanTicketRef} is assigned to YOU. Internal tool identity: project_id ${next.projectId}, ticket_id ${next.id}. Process this ONE ticket only. Numeric ids are MCP arguments only and must never appear in human-facing text; use ${next.slug || 'the ticket title'} instead. This is backlog-only work with NO source message or requester thread: do NOT call post_message yourself. Call get_ticket, use list_task_types and update_ticket to move it active, complete and verify the work, open the PR, then update the ticket with evidence and move it to review or done when appropriate. The watcher will publish exactly one verified completion result to the project channel.`, activityChannel == null ? [] : [activityChannel], { projectId: next.projectId, ticketId: next.id, channelId: activityChannel })
1204
1206
  }
1205
1207
  }
1206
1208
  const inboxSignature = JSON.stringify(mentionActivity.slice(-20)).slice(0, 4000)
@@ -1483,6 +1485,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
1483
1485
  if (seenTasks.has(key)) { log(kind + ' ticket #' + ticketId + ' already queued/active — ignored'); return }
1484
1486
  seenTasks.add(key); trimSeen(seenTasks)
1485
1487
  const title = String(ticket.title || hinted.title || '')
1488
+ const ticketSlug = ticketDisplaySlug(ticket)
1486
1489
  memory.remember({ key: `ticket:${projectId}:${ticketId}`, kind: 'ticket', state: 'queued', summary: title, refs: { projectId, ticketId }, meta: { sourceEvent: kind } })
1487
1490
  const taskText = [title, ticket.description, ticket.type, ticket.kind].filter(Boolean).join(' ')
1488
1491
  const cycleKind = canCode && !coordinationOnly(taskText) ? 'full' : 'coord'
@@ -1490,7 +1493,8 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
1490
1493
  const activityChannel = await projectStatusChannel(projectId)
1491
1494
  if (activityChannel != null) sendStatus(activityChannel, 'thinking')
1492
1495
  if (cycleKind === 'full') { pendingCompletionReports.add(key); trimSeen(pendingCompletionReports); persistReplay() }
1493
- void drain(cycleKind, `Authoritative get_ticket verification confirms ticket #${ticketId} in project ${projectId} is open and assigned to YOU: ${JSON.stringify({ id: ticketId, projectId, title, description: ticket.description, priority: ticket.priority, typeId: ticket.type_id ?? ticket.typeId })}. This assignment has no source thread: do not call post_message yourself. Use list_task_types and update_ticket to move it active, complete and verify the work, open the PR when applicable, then update/move the ticket with evidence. For coding work, the watcher will publish exactly one verified completion result in the project channel.`, activityChannel == null ? [] : [activityChannel], { projectId, ticketId, channelId: activityChannel })
1496
+ const humanTicketRef = ticketSlug ? `ticket ${ticketSlug}` : `the ticket “${title}”`
1497
+ void drain(cycleKind, `Authoritative get_ticket verification confirms ${humanTicketRef} is open and assigned to YOU. Internal tool identity: project_id ${projectId}, ticket_id ${ticketId}. Numeric ids are MCP arguments only and must never appear in human-facing text; use ${ticketSlug || 'the ticket title'} instead. Ticket details: ${JSON.stringify({ slug: ticketSlug, title, description: ticket.description, priority: ticket.priority, typeId: ticket.type_id ?? ticket.typeId })}. This assignment has no source thread: do not call post_message yourself. Use list_task_types and update_ticket to move it active, complete and verify the work, open the PR when applicable, then update/move the ticket with evidence. For coding work, the watcher will publish exactly one verified completion result in the project channel.`, activityChannel == null ? [] : [activityChannel], { projectId, ticketId, channelId: activityChannel })
1494
1498
  } catch (e) {
1495
1499
  log(kind + ' ticket verification failed for #' + ticketId + ': ' + (e?.message || e))
1496
1500
  }