openvisio-agent 0.18.4 → 0.18.6

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.6",
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)')],
@@ -98,7 +99,8 @@ const assertions = [
98
99
  ['policy rejection cannot be logged as successful', watcher.includes("subtype = policyBlock ? 'blocked'")],
99
100
  ['policy-blocked tickets are persisted and paused', watcher.includes('blockedTasks: [...blockedTasks]') && watcher.includes('WORK_CYCLE_BLOCKED')],
100
101
  ['repository push authorization automatically resumes the paused ticket', watcher.includes('blockedTaskRepos: [...blockedTaskRepos]') && watcher.includes('repositoryHasPrPushAuthorization')],
101
- ['Codex prefers linked-codebase MCP PR delivery', watcher.includes('CODEX PR DELIVERY') && watcher.includes('create_codebase_branch/create_codebase_commit/create_pull_request') && events.includes('const codebaseMutation')],
102
+ ['BYO coding prefers an existing local repository', watcher.includes('A usable local clone is your primary code surface') && watcher.includes('do not use remote codebase tools') && watcher.includes('Remote codebase tools are a fallback only when the repository cannot be obtained locally')],
103
+ ['Codex publishes local branches with the constrained helper', watcher.includes('CODEX PR DELIVERY') && watcher.includes('openvisio-agent push-pr-branch') && watcher.includes('Use list_codebases/create_codebase_branch/create_codebase_commit/create_pull_request only as a fallback') && events.includes('const codebaseMutation')],
102
104
  ['private PR pushes use an explicit constrained helper', cli.includes("cmd === 'authorize-pr-push'") && cli.includes("cmd === 'push-pr-branch'") && prPush.includes("'push', '-u', 'origin', destination")],
103
105
  ['PR push helper rejects protected/alternate/force targets by construction', prPush.includes("/^agent\\/") && prPush.includes('entry?.root === root && entry?.remote === remote') && prPush.includes('accepts no force, remote, or ref args')],
104
106
  ['Codex recognizes helper authorization as a blocker', events.includes('OPENVISIO_PR_PUSH_AUTH_REQUIRED') && watcher.includes("block?.kind === 'pr-push-authorization-required'")],
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')
@@ -74,7 +75,7 @@ const CODE_CHARTER = [
74
75
  ' • openvisio-team tools — use the names actually present. Backend MCP provides project/task discovery through list_agents, list_projects, list_tasks, get_ticket, and update_ticket, plus post_message/react_message/list_activity. Ticket comments are optional: use a comment tool only when it appears in the current tool list. Relay runtimes may additionally expose poll_inbox or get_marching_orders.',
75
76
  ' • Read / Grep / Glob / Edit / Write / MultiEdit — inspect AND change code.',
76
77
  ' • Bash — git (branch, commit, push a branch), gh (clone repos, open PRs), run tests/builds.',
77
- 'YOUR WORKSPACE: your working directory is a WORKSPACE ROOT that holds the org\'s repos as subfolders. Reuse existing clones and the context you already verified. Read repository AGENTS.md instructions before changing code. For any task: locate the relevant repo under the workspace; clone it only when it is genuinely absent, then work inside that subfolder. Never ask the user for a path you can discover yourself.',
78
+ 'YOUR WORKSPACE: your working directory is a WORKSPACE ROOT that holds the org\'s repos as subfolders. Reuse existing clones and the context you already verified. A usable local clone is your primary code surface: inspect, search, edit, branch, test, and commit with the local file and Bash/git tools. Do not call remote codebase read/write/branch/commit tools for a repository that already exists locally. Read repository AGENTS.md instructions before changing code. For any task: locate the relevant repo under the workspace; clone it only when it is genuinely absent, then work inside that subfolder. Never ask the user for a path you can discover yourself. Remote codebase tools are a fallback only when the repository cannot be obtained locally.',
78
79
  '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.',
79
80
  '',
80
81
  'WORK ETHIC — how a reliable teammate behaves (this is the difference between useful and ignored):',
@@ -91,7 +92,7 @@ const CODE_FULL = [
91
92
  'THIS CYCLE: discover assigned tickets with the tools that actually exist, then act on them. If get_marching_orders/poll_inbox exist, use them. On the backend MCP, identify yourself with list_agents, call list_projects, then list_tasks for each project and keep tasks whose agent_id or nested agent.identifier is yours.',
92
93
  'DO NOT post a promise or pre-work acknowledgement. Start the repository work immediately. Your first task/channel update must contain either a verified result (branch, commit, PR, tests) or a concrete blocker you actually encountered.',
93
94
  'For real code work (an assigned ticket, or a mention asking for changes), run the full flow end-to-end:',
94
- ' 1. GET THE CODE: use verified thread/history/recall context first, locate the target repo under your workspace root, and read its AGENTS.md. Reuse an existing clone; clone only if absent. Check `git status` before changing anything and preserve unrelated user work. Update from the remote only when it is safe. Do this yourself; never ask the user for a path you can discover.',
95
+ ' 1. GET THE CODE: use verified thread/history/recall context first, locate the target repo under your workspace root, and read its AGENTS.md. Reuse an existing clone; clone only if absent. When the repo exists locally, use Read/Grep/Glob and local git for all code discovery and changes; do not use remote codebase tools. Check `git status` before changing anything and preserve unrelated user work. Update from the remote only when it is safe. Do this yourself; never ask the user for a path you can discover.',
95
96
  ' 2. BRANCH: git checkout -B agent/<short-task-slug>. NEVER work on, commit to, or push main/master.',
96
97
  ' 3. CHANGE + VERIFY: Read/Edit/Write the files; run the tests or build if the repo has them.',
97
98
  ' 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.',
@@ -726,7 +727,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
726
727
  reply: createCycleRunner({ ...runnerOptions, cfgKey: identifier + '-reply', onTool: (name) => { if (/post_message/.test(name)) emitLaneStatus('reply', 'typing') } }),
727
728
  }
728
729
  const codexPushGuide = agent === 'codex' && canCode
729
- ? '\n\nCODEX PR DELIVERY: first inspect the OpenVisio MCP tools. When list_codebases, create_codebase_branch, create_codebase_commit (or write_codebase_file), and create_pull_request are available, use that authenticated linked-codebase flow to create the agent/* branch, publish the verified changed files, and open the PR. This is the preferred path and requires no local git push. If those tools are unavailable for the repository, do not run git push directly. From the repository run `openvisio-agent push-pr-branch`. It is a user-authorized constrained fallback that can only push HEAD to the matching agent/* branch on the exact authorized origin. If it reports OPENVISIO_PR_PUSH_AUTH_REQUIRED, do not retry or route around it. Report the one-time command `openvisio-agent authorize-pr-push` as the blocker.'
730
+ ? '\n\nCODEX PR DELIVERY: when the repository exists in the local workspace, use that clone for branch creation, edits, tests, and commits; do not inspect or mutate it through linked-codebase MCP tools. To publish the local agent/* branch, run `openvisio-agent push-pr-branch` from the repository, then open the PR with `gh pr create`. The helper can only push HEAD to the matching agent/* branch on the exact authorized origin. If it reports OPENVISIO_PR_PUSH_AUTH_REQUIRED, do not retry or route around it. Report the one-time command `openvisio-agent authorize-pr-push` as the blocker. Use list_codebases/create_codebase_branch/create_codebase_commit/create_pull_request only as a fallback when the repository cannot be obtained locally.'
730
731
  : ''
731
732
  const backendToolRule = 'BACKEND MCP RULE: the event and watcher already provide the work source. Never call get_marching_orders, poll_inbox, get_resource, list_mcp_resources, list_mcp_resource_templates, or other relay/resource-discovery tools; they are not backend team actions. Use only names present in the current openvisio-team tool list. For discovery use list_agents, list_projects, list_tasks, list_task_types, list_activity, get_ticket, and list_channels as applicable. Never call comment_ticket unless that exact optional tool is present.'
732
733
  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
@@ -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
  }