openvisio-agent 0.18.2 → 0.18.3

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
@@ -56,9 +56,9 @@ Then `watch --name ada` auto-detects the backend agent and runs a WebSocket loop
56
56
 
57
57
  Runs the **autonomy loop** — the agent replies to @mentions and picks up tickets on its own. It cheaply polls an inbox endpoint (no model spend when idle) and pokes a single warm Claude Code session only when something new arrives.
58
58
 
59
- Backend/BYO watchers reconcile immediately whenever the process starts or the WebSocket connects. A direct MCP session uses the backend's actual tools (`list_agents`, `list_projects`, `list_tasks`, `list_task_types`, and `list_activity`) to recover assigned tasks and recent mention activity missed while offline. The same zero-model check runs every five minutes as a safety net; a model starts only when pending work exists.
59
+ Backend/BYO watchers reconcile immediately whenever the process starts or the WebSocket connects. A direct MCP session discovers and caches the backend's actual `tools/list` response, then uses available tools such as `list_agents`, `list_projects`, `list_tasks`, `list_task_types`, and `list_activity` to recover assigned tasks and recent mention activity missed while offline. Optional actions such as ticket comments are used only when advertised; their absence cannot strand a completed ticket in a retry loop. The same zero-model check runs every five minutes as a safety net; a model starts only when pending work exists.
60
60
 
61
- The backend MCP may be stateful or stateless. A successful initialize response without `Mcp-Session-Id` is accepted as stateless, so OpenCode agents do not stop with “MCP initialize returned no session id.” Each OpenCode lane keeps its MCP identity in a private per-agent config directory while the repository is supplied separately with `--dir`; stale workspace configuration therefore cannot swap one agent's credentials for another's. The generated remote configuration sends the agent headers directly, disables OAuth probing, and backend cycles never request relay-only `get_marching_orders`, `poll_inbox`, or `get_resource` tools.
61
+ The backend MCP may be stateful or stateless. A successful initialize response without `Mcp-Session-Id` is accepted as stateless, so OpenCode agents do not stop with “MCP initialize returned no session id.” Each OpenCode lane keeps its MCP identity in a private per-agent config directory while the repository is supplied separately with `--dir`; stale workspace configuration therefore cannot swap one agent's credentials for another's. The generated remote configuration sends the agent headers directly, disables OAuth probing, and backend cycles never request relay-only inbox calls or MCP resource-discovery tools in place of team actions.
62
62
 
63
63
  Codex BYO agents follow the repository's normative runtime specification in `docs/CODEX_BYO_AGENT_SPEC.md`: one WebSocket identity, independent Sol reply/work lanes, authoritative `get_ticket` verification for assignments, REST-backed in-app activity, persistent replay suppression, and runtime evidence gates before completion. Maintainers must run `npm run certify` before publishing.
64
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
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": {
@@ -41,7 +41,7 @@ const assertions = [
41
41
  ['assigned coding completion is posted by the watcher', watcher.includes('announceTaskCompletion') && watcher.includes('postMessageOnce({ key: `completion:${report.key}`')],
42
42
  ['completion requires review/done state and PR evidence', watcher.includes('buildTaskCompletionReport') && watcher.includes('completion report deferred')],
43
43
  ['completion delivery survives reconnect and deduplicates', watcher.includes('pendingCompletionReports: [...pendingCompletionReports]') && watcher.includes('reportedCompletions: [...reportedCompletions]') && watcher.includes('reportedCompletions.has(report.key)')],
44
- ['verified completion is persisted as one ticket comment', watcher.includes("callMcpTool('comment_ticket', { project_id: projectId, ticket_id: ticketId, text: report.content })") && watcher.includes('reportedTaskComments: [...reportedTaskComments]') && watcher.includes('reportedTaskComments.has(report.key)')],
44
+ ['optional ticket comments cannot block verified completion', watcher.includes("callOptionalMcpTool('comment_ticket', { project_id: projectId, ticket_id: ticketId, text: report.content })") && watcher.includes('comment_ticket is not exposed; completing ticket') && watcher.includes('reportedTaskComments: [...reportedTaskComments]') && watcher.includes('reportedTaskComments.has(report.key)')],
45
45
  ['ticket comments cannot masquerade as channel completion', watcher.includes('didChannelMessage') && watcher.includes("mcpCalls.includes('post_message')")],
46
46
  ['single-watcher acquisition is atomic and fails closed', watcher.includes("openSync(lockPath, 'wx')") && watcher.includes('Could not acquire the single-watcher lock')],
47
47
  ['websocket and activity mention delivery share a replay guard', watcher.includes('markMentionHandled(activityMessage, activityChannelId)') && watcher.includes('markMentionHandled(msg, cid)') && watcher.includes('recentMentionSignatures')],
@@ -69,12 +69,14 @@ const assertions = [
69
69
  ['OpenCode emits structured events for runtime evidence', watcher.includes("'--format', 'json'") && watcher.includes('opencodeEventEvidence(event)')],
70
70
  ['OpenCode API-key MCP disables OAuth probing', opencodeConfig.includes("oauth: false") && opencodeConfig.includes('timeout: 15_000')],
71
71
  ['OpenCode identities use isolated configs outside the code workspace', watcher.includes('opencodeRuntimeLayout({ cfgKey, workdir })') && watcher.includes("'--dir', workspace") && watcher.includes('OPENCODE_CONFIG: opencodeConfigPath') && watcher.includes('OPENCODE_CONFIG_CONTENT: JSON.stringify(opencodeConfig)')],
72
- ['OpenCode backend prompts forbid relay-only tools', watcher.includes('BACKEND MCP RULE') && watcher.includes('get_marching_orders, poll_inbox, and get_resource are relay-only')],
72
+ ['OpenCode backend prompts forbid relay and resource-discovery tools', watcher.includes('BACKEND MCP RULE') && watcher.includes('get_marching_orders, poll_inbox, get_resource, list_mcp_resources, list_mcp_resource_templates')],
73
73
  ['OpenCode tool failures retain sanitized diagnostics', events.includes('toolError: toolError.replace') && watcher.includes("opencode tool '") && watcher.includes("split(redactKey).join('[redacted]')")],
74
74
  ['OpenCode acknowledgements cannot satisfy coding completion', watcher.includes("agent === 'codex' || agent === 'opencode'") && watcher.includes('releaseTaskForRetry(activeTaskRef, prompt)')],
75
75
  ['backend MCP accepts stateless initialize responses', mcpHttp.includes("mode = () => !initialized ? 'uninitialized' : sessionId ? 'stateful' : 'stateless'") && !watcher.includes('MCP initialize returned no session id')],
76
76
  ['MCP initialize is shared across concurrent startup probes', mcpHttp.includes('if (initializePromise) return initializePromise')],
77
77
  ['stateless tool errors do not cause initialize loops', mcpHttp.includes('if (hadSession && !retried')],
78
+ ['backend MCP tools are discovered and cached', mcpHttp.includes("method: 'tools/list'") && mcpHttp.includes('if (!refresh && toolsCache)') && watcher.includes('discoverMcpTools')],
79
+ ['missing optional MCP tools use compatibility fallbacks', watcher.includes("reason: 'not-advertised'") && watcher.includes('recording the blocker in the ticket description')],
78
80
  ['backend introduction is watcher-owned for every runtime', watcher.includes('void announceIntroduction().then((delivered)')],
79
81
  ['Codex policy rejection is captured from stderr', watcher.includes("stdio: ['ignore', 'pipe', 'pipe']") && watcher.includes('forwardDiagnostic(d)') && watcher.includes('inspectDiagnostic(incoming)')],
80
82
  ['Codex recoverable subprocess diagnostics are not surfaced as activity', watcher.includes('shouldSuppressCodexDiagnostic(line)') && watcher.includes("forwardDiagnostic('', true)") && watcher.includes('RECOVER DEAD COMMAND SESSIONS')],
package/src/mcp-http.mjs CHANGED
@@ -14,6 +14,8 @@ export function createMcpHttpClient({ url, apiKey, identifier, clientVersion, fe
14
14
  let sessionId = ''
15
15
  let rpcId = 0
16
16
  let initializePromise = null
17
+ let toolsPromise = null
18
+ let toolsCache = null
17
19
 
18
20
  const post = (message, withSession = true) => fetchImpl(url, {
19
21
  method: 'POST',
@@ -27,7 +29,7 @@ export function createMcpHttpClient({ url, apiKey, identifier, clientVersion, fe
27
29
  body: JSON.stringify(message),
28
30
  })
29
31
 
30
- const reset = () => { initialized = false; sessionId = '' }
32
+ const reset = () => { initialized = false; sessionId = ''; toolsCache = null }
31
33
 
32
34
  const initialize = () => {
33
35
  if (initialized) return Promise.resolve()
@@ -80,6 +82,35 @@ export function createMcpHttpClient({ url, apiKey, identifier, clientVersion, fe
80
82
  return result
81
83
  }
82
84
 
85
+ const requestTools = async (retried = false) => {
86
+ await initialize()
87
+ const hadSession = !!sessionId
88
+ const res = await post({ jsonrpc: '2.0', id: ++rpcId, method: 'tools/list', params: {} })
89
+ if (!res.ok) {
90
+ if (hadSession && !retried && [400, 404, 409, 410].includes(res.status)) {
91
+ reset()
92
+ return requestTools(true)
93
+ }
94
+ throw new Error(`MCP tools/list HTTP ${res.status}`)
95
+ }
96
+ const payload = await parsePayload(res)
97
+ if (payload.error) throw new Error(`MCP tools/list: ${payload.error.message || 'protocol error'}`)
98
+ const tools = payload.result?.tools ?? payload.tools
99
+ if (!Array.isArray(tools)) throw new Error('MCP tools/list returned no tool array')
100
+ toolsCache = tools
101
+ return tools
102
+ }
103
+
104
+ // Capability discovery is shared and cached. BYO runtimes use it before
105
+ // optional actions so a backend deployment that lacks one tool cannot trap a
106
+ // completed ticket in a permanent retry loop.
107
+ const listTools = (refresh = false) => {
108
+ if (!refresh && toolsCache) return Promise.resolve(toolsCache)
109
+ if (toolsPromise) return toolsPromise
110
+ toolsPromise = requestTools().finally(() => { toolsPromise = null })
111
+ return toolsPromise
112
+ }
113
+
83
114
  const mode = () => !initialized ? 'uninitialized' : sessionId ? 'stateful' : 'stateless'
84
- return { callTool, initialize, reset, mode }
115
+ return { callTool, listTools, initialize, reset, mode }
85
116
  }
package/src/watch.mjs CHANGED
@@ -42,7 +42,7 @@ const REPLY_DISCIPLINE = [
42
42
 
43
43
  // ── CHAT-ONLY agents (no --workdir): chat/ticket tools, no code surface. ──────
44
44
  const CHAT_CHARTER = [
45
- '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, comment_ticket, and list_activity. 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.',
45
+ '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.',
46
46
  '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.',
47
47
  '',
48
48
  REPLY_DISCIPLINE,
@@ -70,7 +70,7 @@ const COORDINATE = [
70
70
  // A stable "who you are / how you work" charter prepended to every code cycle.
71
71
  const CODE_CHARTER = [
72
72
  '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:',
73
- ' • openvisio-team tools — use the names actually present. Backend MCP provides project/task discovery through list_agents, list_projects, list_tasks, get_ticket, update_ticket, and comment_ticket, plus post_message/react_message/list_activity. Relay runtimes may additionally expose poll_inbox or get_marching_orders.',
73
+ ' • 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.',
74
74
  ' • Read / Grep / Glob / Edit / Write / MultiEdit — inspect AND change code.',
75
75
  ' • Bash — git (branch, commit, push a branch), gh (clone repos, open PRs), run tests/builds.',
76
76
  '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.',
@@ -95,7 +95,7 @@ const CODE_FULL = [
95
95
  ' 3. CHANGE + VERIFY: Read/Edit/Write the files; run the tests or build if the repo has them.',
96
96
  ' 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.',
97
97
  ' 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.',
98
- ' 6. CLOSE THE LOOP: use comment_ticket for a concrete ticket-scoped blocker or clarification, then move/update the ticket with update_ticket. The watcher adds one evidence-verified final ticket comment after handoff. Reply with the summary + PR link in a source thread explicitly supplied by the event. For backlog-only tickets, do not call post_message yourself; the watcher sends one verified project-channel completion message and deduplicates it across reconnects.',
98
+ ' 6. CLOSE THE LOOP: move/update the ticket with update_ticket. Use a ticket-comment tool for a blocker or clarification only when that tool actually appears; otherwise keep the blocker in the ticket update and let the watcher deliver the visible channel result. Reply with the summary + PR link in a source thread explicitly supplied by the event. For backlog-only tickets, do not call post_message yourself; the watcher sends one verified project-channel completion message and deduplicates it across reconnects.',
99
99
  'Bash is for git / gh / tests / clone ONLY — never to hunt for credentials (they are given to you above).',
100
100
  ].join('\n')
101
101
 
@@ -119,7 +119,7 @@ const INTRO = [
119
119
  const SWEEP = [
120
120
  'DAILY CATCH-UP — you may have missed items while offline. Prioritize TASKS.',
121
121
  'Use the available task/inbox tools. If get_marching_orders/poll_inbox are absent, use list_agents + list_projects + list_tasks to find tasks assigned to your agent identity, then:',
122
- ' 1. For every task assigned to YOU that you have NOT started or acknowledged: acknowledge once (comment_ticket "Catching up picking this up now"), then do the work end-to-end and report (branch/PR + a short channel note). Skip tasks assigned to other agents.',
122
+ ' 1. For every task assigned to YOU that you have NOT started: begin the work without inventing an acknowledgement tool. If a ticket-comment tool is present you may acknowledge once; otherwise use update_ticket and report through an actual source channel only when one is supplied. Then do the work end-to-end. Skip tasks assigned to other agents.',
123
123
  ' 2. Answer only the @mentions / follow-ups that were directed at YOU and that you have not already answered — at most one reply per channel. Do not reply to threads aimed at someone else.',
124
124
  'If there is genuinely nothing outstanding, STOP silently — do NOT post a "nothing to do" message.',
125
125
  ].join('\n')
@@ -687,7 +687,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
687
687
  const canCode = !!workdir
688
688
  // The Mastra bridge authenticates per-CALL: every openvisio-team tool needs
689
689
  // agent_identifier + agent_api_key as arguments. Hand them over up front.
690
- const credNote = `AUTH: the 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. Use ONLY names shown in the current tool list. On backend MCP, discover and update work with list_agents, list_projects, list_tasks, list_task_types, get_ticket, update_ticket, comment_ticket, list_channels, list_message_thread, and list_activity as applicable. get_marching_orders, poll_inbox, and get_resource are relay-only and are NOT available here; never call them. Tools may be namespaced — call whichever names actually appear. The credentials are given here; do NOT hunt for them. Bash/git/gh ARE for code work; this rule only forbids searching for keys.`
690
+ const credNote = `AUTH: the 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. Use ONLY names shown in the current tool list. On backend MCP, discover and update work with list_agents, list_projects, list_tasks, list_task_types, get_ticket, update_ticket, list_channels, list_message_thread, and list_activity as applicable. Ticket comments are optional: never invent or call comment_ticket unless that exact tool appears. get_marching_orders, poll_inbox, get_resource, list_mcp_resources, and list_mcp_resource_templates are NOT team-action tools here; never call them. Tools may be namespaced — call whichever names actually appear. The credentials are given here; do NOT hunt for them. Bash/git/gh ARE for code work; this rule only forbids searching for keys.`
691
691
  // The STATIC charter + creds are the session system prompt (cached, billed once),
692
692
  // NOT re-sent in every cycle's user message — the big token saving.
693
693
  const systemPrompt = (canCode ? CODE_CHARTER : CHAT_CHARTER) + '\n\n' + credNote
@@ -704,7 +704,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
704
704
  const codexPushGuide = agent === 'codex' && canCode
705
705
  ? '\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.'
706
706
  : ''
707
- const backendToolRule = 'BACKEND MCP RULE: the event and watcher already provide the work source. Never call get_marching_orders, poll_inbox, get_resource, or other relay-only tools; they are not exposed by the backend MCP. 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.'
707
+ 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.'
708
708
  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
709
709
  const fastPrompt = (canCode ? CODE_FAST : CYCLE_FAST) + '\n\n' + backendToolRule
710
710
  const coordinatePrompt = COORDINATE + '\n\n' + backendToolRule
@@ -787,8 +787,46 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
787
787
  let lastTaskTriggeredAt = 0
788
788
  let lastInboxSignature = ''
789
789
  let selfAgentId = null
790
- const mcpClient = createMcpHttpClient({ url: mcpUrl, apiKey, identifier, clientVersion: '0.18.2', log })
790
+ const mcpClient = createMcpHttpClient({ url: mcpUrl, apiKey, identifier, clientVersion: '0.18.3', log })
791
791
  const callMcpTool = (name, args = {}) => mcpClient.callTool(name, args)
792
+ let mcpToolNames = null
793
+ let mcpToolDiscoveryPromise = null
794
+ let mcpToolDiscoveryWarned = false
795
+ const discoverMcpTools = (refresh = false) => {
796
+ if (!refresh && mcpToolNames) return Promise.resolve(mcpToolNames)
797
+ if (mcpToolDiscoveryPromise) return mcpToolDiscoveryPromise
798
+ mcpToolDiscoveryPromise = mcpClient.listTools(refresh).then((tools) => {
799
+ mcpToolNames = new Set(tools.map((tool) => String(tool?.name || '')).filter(Boolean))
800
+ const required = ['list_agents', 'list_projects', 'list_tasks', 'get_ticket', 'update_ticket', 'post_message']
801
+ const missing = required.filter((name) => !mcpToolNames.has(name))
802
+ log(`MCP tools ready (${mcpToolNames.size})${missing.length ? '; missing core tools: ' + missing.join(', ') : ''}`)
803
+ return mcpToolNames
804
+ }).finally(() => { mcpToolDiscoveryPromise = null })
805
+ return mcpToolDiscoveryPromise
806
+ }
807
+ const mcpSupports = async (name) => {
808
+ try { return (await discoverMcpTools()).has(name) }
809
+ catch (e) {
810
+ if (!mcpToolDiscoveryWarned) {
811
+ mcpToolDiscoveryWarned = true
812
+ log('MCP tool discovery failed; optional actions will use compatibility fallbacks: ' + (e?.message || e))
813
+ }
814
+ return null
815
+ }
816
+ }
817
+ const callOptionalMcpTool = async (name, args) => {
818
+ const supported = await mcpSupports(name)
819
+ if (supported === false) return { called: false, reason: 'not-advertised' }
820
+ try { return { called: true, result: await callMcpTool(name, args) } }
821
+ catch (e) {
822
+ if (/\b(?:unknown|missing|unsupported) tool\b|\btool\b.*\bnot found\b/i.test(String(e?.message || e))) {
823
+ try { await discoverMcpTools(true) } catch { /* the original error is enough */ }
824
+ return { called: false, reason: 'not-available' }
825
+ }
826
+ throw e
827
+ }
828
+ }
829
+ if (mcpUrl) void discoverMcpTools().catch(() => {})
792
830
  const toolData = (result) => {
793
831
  const text = result?.content?.find?.((c) => c?.type === 'text')?.text
794
832
  if (typeof text !== 'string') return result?.structuredContent ?? result ?? {}
@@ -892,13 +930,18 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
892
930
  refs: { projectId, ticketId },
893
931
  meta: { reportKey: report.key, prUrl: report.prUrl },
894
932
  })
895
- // Ticket comments are now a backend first-class surface. The watcher owns the
896
- // final comment so every runtime (Claude, Codex, OpenCode) closes the ticket
897
- // loop consistently, and the persisted report key prevents reconnect repeats.
933
+ // Ticket comments are optional across backend deployments. Prefer the tool
934
+ // when advertised, but never let its absence block the verified channel
935
+ // handoff or trap the ticket in reconnect retries.
898
936
  if (!reportedTaskComments.has(report.key)) {
899
- await callMcpTool('comment_ticket', { project_id: projectId, ticket_id: ticketId, text: report.content })
937
+ try {
938
+ const comment = await callOptionalMcpTool('comment_ticket', { project_id: projectId, ticket_id: ticketId, text: report.content })
939
+ if (comment.called) log('posted verified ticket comment for #' + ticketId)
940
+ else log('comment_ticket is not exposed; completing ticket #' + ticketId + ' through its verified state and project-channel report')
941
+ } catch (e) {
942
+ log('optional ticket comment failed for #' + ticketId + ': ' + (e?.message || e) + '; continuing with the verified project-channel report')
943
+ }
900
944
  reportedTaskComments.add(report.key); trimSeen(reportedTaskComments); persistReplay()
901
- log('posted verified ticket comment for #' + ticketId)
902
945
  }
903
946
  if (reportedCompletions.has(report.key)) {
904
947
  pendingCompletionReports.delete(taskKey)
@@ -959,12 +1002,10 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
959
1002
  refs: { projectId, ticketId, ...(Number.isFinite(channelId) ? { channelId } : {}) },
960
1003
  })
961
1004
  try {
962
- await callMcpTool('comment_ticket', { project_id: projectId, ticket_id: ticketId, text: ticketNotice })
963
- delivered = true
964
- return
965
- } catch (e) {
966
- log('comment_ticket failed for blocker; falling back to the ticket description for #' + ticketId)
967
- }
1005
+ const comment = await callOptionalMcpTool('comment_ticket', { project_id: projectId, ticket_id: ticketId, text: ticketNotice })
1006
+ if (comment.called) { delivered = true; return }
1007
+ log('comment_ticket is not exposed; recording the blocker in the ticket description for #' + ticketId)
1008
+ } catch (e) { log('optional ticket comment failed; recording the blocker in the ticket description for #' + ticketId) }
968
1009
  const current = toolData(await callMcpTool('get_ticket', { project_id: projectId, ticket_id: ticketId }))
969
1010
  const ticket = current.ticket ?? current.task ?? current
970
1011
  const description = String(ticket.description || '')
@@ -1446,8 +1487,8 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
1446
1487
  const replyDelivery = guardedDelivery(codingMention ? 'result' : 'reply', !codingMention)
1447
1488
  const ctx = cid != null
1448
1489
  ? replyDelivery
1449
- ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. ${codingMention ? 'Complete the repository work and verification first.' : 'Answer the request.'} Do NOT call post_message; it is intentionally unavailable. Return only the final 1-3 sentence reply as your final answer. The watcher will read the real thread, check its persistent memory graph, and render that answer at most once.${who ? ` To mention the requester, use their exact full name "@${who}".` : ''} The complete message is already here; do not call get_resource, get_marching_orders, or poll_inbox.`
1450
- : `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. ${codingMention ? 'This is repository work: complete the coding flow first, then send' : 'Send'} EXACTLY ONE reply with post_message: 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. Compose the whole answer, then post it ONCE. Do not post a first reply and then a revised version. FIRST read the recent messages in this thread: if you already answered this, or another agent was the one addressed, do NOT post at all. Be sure of your answer before sending.${who ? ` To @mention them back, write their EXACT full name "@${who}". A mention only links when the name matches exactly.` : ''} The complete message is already here. Do not call get_resource, get_marching_orders, or poll_inbox, and after your single reply, STOP.`
1490
+ ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. ${codingMention ? 'Complete the repository work and verification first.' : 'Answer the request.'} Do NOT call post_message; it is intentionally unavailable. Return only the final 1-3 sentence reply as your final answer. The watcher will read the real thread, check its persistent memory graph, and render that answer at most once.${who ? ` To mention the requester, use their exact full name "@${who}".` : ''} The complete message is already here; do not call get_resource, get_marching_orders, poll_inbox, list_mcp_resources, or list_mcp_resource_templates.`
1491
+ : `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. ${codingMention ? 'This is repository work: complete the coding flow first, then send' : 'Send'} EXACTLY ONE reply with post_message: 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. Compose the whole answer, then post it ONCE. Do not post a first reply and then a revised version. FIRST read the recent messages in this thread: if you already answered this, or another agent was the one addressed, do NOT post at all. Be sure of your answer before sending.${who ? ` To @mention them back, write their EXACT full name "@${who}". A mention only links when the name matches exactly.` : ''} The complete message is already here. Do not call get_resource, get_marching_orders, poll_inbox, list_mcp_resources, or list_mcp_resource_templates, and after your single reply, STOP.`
1451
1492
  : undefined
1452
1493
  if (codingMention) {
1453
1494
  if (agent === 'codex' && cid != null) {