openvisio-agent 0.14.0 → 0.15.0

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
@@ -58,7 +58,7 @@ Runs the **autonomy loop** — the agent replies to @mentions and picks up ticke
58
58
 
59
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.
60
60
 
61
- Routine messages, triage, introductions, catch-up checks, and ticket movement use a lightweight model by default. Claude uses Haiku and Codex uses `gpt-5.6-luna`. Requests that clearly require repository work route to the coding model: Sonnet for Claude and `gpt-5.6-sol` for Codex. Override either lane with `--chat-model` and `--model`.
61
+ Routine messages, triage, introductions, catch-up checks, and ticket movement use a lower-cost model by default. Claude uses Haiku and Codex uses `gpt-5.6-terra`; Luna is intentionally not used because watcher coordination depends on reliable MCP tool use. Requests that require repository work route to the coding model: Sonnet for Claude and `gpt-5.6-sol` for Codex. Override either lane with `--chat-model` and `--model`.
62
62
 
63
63
  ```bash
64
64
  openvisio-agent watch --name ada # run in this terminal
package/bin/cli.mjs CHANGED
@@ -58,8 +58,8 @@ connect --backend
58
58
  --model <m> coding model. Claude defaults to sonnet; Codex defaults to
59
59
  gpt-5.6-sol; OpenCode uses its configured default.
60
60
  Engineers can also change it live from chat: "@agent /model …".
61
- --chat-model <m> lightweight coordination model for chat, triage, and board
62
- movement. Defaults: Claude haiku, Codex gpt-5.6-luna.
61
+ --chat-model <m> coordination model for chat, triage, and board movement.
62
+ Defaults: Claude haiku, Codex gpt-5.6-terra.
63
63
  Coding work stays on --model.
64
64
  --no-service skip the background service — just save config + print the
65
65
  watch commands to run yourself.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
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/watch.mjs CHANGED
@@ -215,10 +215,11 @@ export async function runWatch({ flags }) {
215
215
  const workdir = chatOnly ? '' : (explicitWorkdir || (saved && (saved.workspace || saved.workdir)) || DEFAULT_WORKSPACE)
216
216
  if (workdir) { try { mkdirSync(workdir, { recursive: true }) } catch { /* best-effort; spawn will surface a real problem */ } }
217
217
 
218
- // Keep routine coordination on the cheapest capable tier. Reserve the stronger
219
- // default for coding cycles. Explicit flags and saved choices always win.
218
+ // Keep routine coordination on a balanced tool-capable tier. Luna proved too
219
+ // willing to narrate instead of acting in MCP-heavy watcher cycles, so Codex
220
+ // defaults to Terra for coordination and reserves Sol for repository work.
220
221
  const defaultModel = agent === 'claude' ? 'sonnet' : agent === 'codex' ? 'gpt-5.6-sol' : ''
221
- const defaultChatModel = agent === 'claude' ? 'haiku' : agent === 'codex' ? 'gpt-5.6-luna' : ''
222
+ const defaultChatModel = agent === 'claude' ? 'haiku' : agent === 'codex' ? 'gpt-5.6-terra' : ''
222
223
  const model = String(flags.model || (saved && saved.model) || defaultModel)
223
224
  const chatModel = String(flags['chat-model'] || (saved && saved.chatModel) || defaultChatModel)
224
225
 
@@ -343,17 +344,35 @@ function createCodexRunner({ mcpUrl, mcpHeaders, workdir, canCode, maxCycleMs, l
343
344
  ...(m ? ['--model', m] : []),
344
345
  ...(mcpOverride ? ['-c', mcpOverride] : []),
345
346
  full]
346
- let child = null, done = false, didCode = false, didMessage = false, outputText = '', jsonlBuffer = ''
347
- const finish = (o) => { if (done) return; done = true; clearTimeout(timer); resolve({ ...o, didCode, didMessage, outputText }) }
347
+ let child = null, done = false, didCode = false, didRepoMutation = false, didMessage = false, outputText = '', jsonlBuffer = ''
348
+ const mcpCalls = new Set(), mcpErrors = new Set()
349
+ let didMcpTaskRead = false, didMcpTaskUpdate = false
350
+ const finish = (o) => {
351
+ if (done) return
352
+ done = true
353
+ clearTimeout(timer)
354
+ const calls = [...mcpCalls]
355
+ log('codex MCP calls: ' + (calls.length ? calls.join(', ') : 'none') + (mcpErrors.size ? ' (failed: ' + [...mcpErrors].join(', ') + ')' : ''))
356
+ resolve({ ...o, didCode, didRepoMutation, didMessage, didMcpTaskRead, didMcpTaskUpdate, mcpCalls: calls, outputText })
357
+ }
348
358
  const inspectLine = (line) => {
349
359
  const s = line.trim()
350
360
  if (!s) return
351
361
  if (/command_execution|file_change|apply_patch|shell_command|exec_command/i.test(s)) didCode = true
362
+ if (/file_change|apply_patch/i.test(s) || /\b(?:git\s+(?:commit|push)|gh\s+pr\s+create)\b/i.test(s)) didRepoMutation = true
352
363
  if (/post_message|comment_ticket/i.test(s)) didMessage = true
353
364
  try {
354
365
  const event = JSON.parse(s)
355
366
  const item = event.item ?? event
356
367
  if (item?.type === 'agent_message' && typeof item.text === 'string') outputText += ' ' + item.text
368
+ if (item?.type === 'mcp_tool_call') {
369
+ const tool = String(item.tool || item.name || item.method || 'unknown').replace(/^openvisio-team[.:/]/, '')
370
+ mcpCalls.add(tool)
371
+ if (/^(?:get_ticket|list_tasks|list_task_types)$/.test(tool)) didMcpTaskRead = true
372
+ if (tool === 'update_ticket') didMcpTaskUpdate = true
373
+ if (/post_message|comment_ticket/.test(tool)) didMessage = true
374
+ if (/fail|error/i.test(String(item.status || '')) || item.error) mcpErrors.add(tool)
375
+ }
357
376
  } catch { /* non-JSON diagnostic */ }
358
377
  if (debug) log(' · ' + s.slice(0, 220))
359
378
  }
@@ -578,7 +597,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
578
597
  }
579
598
  const ensureMcpSession = async () => {
580
599
  if (mcpSessionId) return
581
- const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.14.0' } } }, false)
600
+ const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.15.0' } } }, false)
582
601
  if (!res.ok) throw new Error('MCP initialize HTTP ' + res.status)
583
602
  await mcpPayload(res)
584
603
  mcpSessionId = res.headers.get('mcp-session-id') || ''
@@ -690,13 +709,15 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
690
709
  const heartbeat = targets.length ? setInterval(() => emitStatus('working'), 9000) : null
691
710
  try {
692
711
  const result = await runCycle(prompt, useModel)
693
- // Codex can exit successfully after posting only "I'll do it". For coding
694
- // cycles, treat that as incomplete and give it one focused recovery turn.
695
- // Do not retry a real blocker, which would only create duplicate noise.
712
+ // A zero exit is not proof of work. Assigned-ticket cycles must both use
713
+ // the task MCP and leave repository evidence. A simple `ls` no longer
714
+ // counts as completion. Do not retry a verified real blocker.
696
715
  const legitimateNoWork = /\b(?:no (?:open |assigned |pending )?tasks?|not assigned to (?:me|you)|assigned to (?:another|someone else)|blocked|cannot|can't|missing|need access|permission|unclear)\b/i.test(result?.outputText || '')
697
- if (agent === 'codex' && kind === 'full' && result?.subtype === 'ok' && !result.didCode && !legitimateNoWork) {
698
- log('coding cycle exited without repository action; running one completion recovery')
699
- await runCycle('You exited the assigned coding task without performing repository work. Do not post another acknowledgement or plan. Resume the same task now: inspect the workspace, make the required changes, verify them, commit and push an agent/* branch, open the PR, then post exactly one result update with evidence. If a real blocker appears, report that blocker once.', codeModel)
716
+ const incomplete = !result?.didRepoMutation || !result?.didMcpTaskRead || !result?.didMcpTaskUpdate
717
+ if (agent === 'codex' && kind === 'full' && result?.subtype === 'ok' && incomplete && !legitimateNoWork) {
718
+ const missing = [!result.didMcpTaskRead && 'read the ticket through get_ticket/list_tasks', !result.didRepoMutation && 'perform and verify the repository change', !result.didMcpTaskUpdate && 'update the ticket through update_ticket'].filter(Boolean)
719
+ log('coding cycle incomplete; recovery requires: ' + missing.join(', '))
720
+ await runCycle(`The assigned task is NOT complete. Missing evidence: ${missing.join('; ')}. Do not post another acknowledgement or plan. Resume now. First use get_ticket/list_tasks and list_task_types, then do the repository work, verify it, commit and push an agent/* branch, open the PR, call update_ticket with the correct board column, and post exactly one result update with evidence. If a real blocker appears, report it once.`, codeModel)
700
721
  }
701
722
  } finally {
702
723
  if (heartbeat) clearInterval(heartbeat)