openvisio-agent 0.14.0 → 0.15.1

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
+ 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.
62
62
 
63
63
  ```bash
64
64
  openvisio-agent watch --name ada # run in this terminal
package/bin/cli.mjs CHANGED
@@ -58,9 +58,9 @@ 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.
63
- Coding work stays on --model.
61
+ --chat-model <m> coordination model for chat, triage, and board movement.
62
+ Claude defaults to haiku. Codex uses gpt-5.6-sol for every
63
+ cycle, including coordination and messaging.
64
64
  --no-service skip the background service — just save config + print the
65
65
  watch commands to run yourself.
66
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
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,10 @@ 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
+ // Codex uses Sol for every watcher cycle. Reliability and consistent MCP/tool
219
+ // behaviour take priority over maintaining a cheaper coordination lane.
220
220
  const defaultModel = agent === 'claude' ? 'sonnet' : agent === 'codex' ? 'gpt-5.6-sol' : ''
221
- const defaultChatModel = agent === 'claude' ? 'haiku' : agent === 'codex' ? 'gpt-5.6-luna' : ''
221
+ const defaultChatModel = agent === 'claude' ? 'haiku' : agent === 'codex' ? 'gpt-5.6-sol' : ''
222
222
  const model = String(flags.model || (saved && saved.model) || defaultModel)
223
223
  const chatModel = String(flags['chat-model'] || (saved && saved.chatModel) || defaultChatModel)
224
224
 
@@ -343,17 +343,35 @@ function createCodexRunner({ mcpUrl, mcpHeaders, workdir, canCode, maxCycleMs, l
343
343
  ...(m ? ['--model', m] : []),
344
344
  ...(mcpOverride ? ['-c', mcpOverride] : []),
345
345
  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 }) }
346
+ let child = null, done = false, didCode = false, didRepoMutation = false, didMessage = false, outputText = '', jsonlBuffer = ''
347
+ const mcpCalls = new Set(), mcpErrors = new Set()
348
+ let didMcpTaskRead = false, didMcpTaskUpdate = false
349
+ const finish = (o) => {
350
+ if (done) return
351
+ done = true
352
+ clearTimeout(timer)
353
+ const calls = [...mcpCalls]
354
+ log('codex MCP calls: ' + (calls.length ? calls.join(', ') : 'none') + (mcpErrors.size ? ' (failed: ' + [...mcpErrors].join(', ') + ')' : ''))
355
+ resolve({ ...o, didCode, didRepoMutation, didMessage, didMcpTaskRead, didMcpTaskUpdate, mcpCalls: calls, outputText })
356
+ }
348
357
  const inspectLine = (line) => {
349
358
  const s = line.trim()
350
359
  if (!s) return
351
360
  if (/command_execution|file_change|apply_patch|shell_command|exec_command/i.test(s)) didCode = true
361
+ 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
362
  if (/post_message|comment_ticket/i.test(s)) didMessage = true
353
363
  try {
354
364
  const event = JSON.parse(s)
355
365
  const item = event.item ?? event
356
366
  if (item?.type === 'agent_message' && typeof item.text === 'string') outputText += ' ' + item.text
367
+ if (item?.type === 'mcp_tool_call') {
368
+ const tool = String(item.tool || item.name || item.method || 'unknown').replace(/^openvisio-team[.:/]/, '')
369
+ mcpCalls.add(tool)
370
+ if (/^(?:get_ticket|list_tasks|list_task_types)$/.test(tool)) didMcpTaskRead = true
371
+ if (tool === 'update_ticket') didMcpTaskUpdate = true
372
+ if (/post_message|comment_ticket/.test(tool)) didMessage = true
373
+ if (/fail|error/i.test(String(item.status || '')) || item.error) mcpErrors.add(tool)
374
+ }
357
375
  } catch { /* non-JSON diagnostic */ }
358
376
  if (debug) log(' · ' + s.slice(0, 220))
359
377
  }
@@ -578,7 +596,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
578
596
  }
579
597
  const ensureMcpSession = async () => {
580
598
  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)
599
+ const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.15.1' } } }, false)
582
600
  if (!res.ok) throw new Error('MCP initialize HTTP ' + res.status)
583
601
  await mcpPayload(res)
584
602
  mcpSessionId = res.headers.get('mcp-session-id') || ''
@@ -681,7 +699,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
681
699
  const prompt = (ctx.length ? ctx.join('\n') + '\n\n' : '') + baseFor(kind)
682
700
  // Chat-shaped cycles (mentions/intro) may run on the cheaper chat model; code
683
701
  // work (full/sweep) uses the main model.
684
- const useModel = kind === 'full' ? codeModel : liteModel
702
+ const useModel = agent === 'codex' ? codeModel : kind === 'full' ? codeModel : liteModel
685
703
  log('running ' + kind + ' cycle…' + (ctx.length ? ' (' + ctx.length + ' event' + (ctx.length === 1 ? '' : 's') + ')' : '') + (useModel ? ' [' + useModel + ']' : ''))
686
704
  // Live status: "working" now + a heartbeat so the UI (and its TTL) stays lit
687
705
  // through a long cycle; onTool flips it to "typing" when post_message fires.
@@ -690,13 +708,15 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
690
708
  const heartbeat = targets.length ? setInterval(() => emitStatus('working'), 9000) : null
691
709
  try {
692
710
  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.
711
+ // A zero exit is not proof of work. Assigned-ticket cycles must both use
712
+ // the task MCP and leave repository evidence. A simple `ls` no longer
713
+ // counts as completion. Do not retry a verified real blocker.
696
714
  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)
715
+ const incomplete = !result?.didRepoMutation || !result?.didMcpTaskRead || !result?.didMcpTaskUpdate
716
+ if (agent === 'codex' && kind === 'full' && result?.subtype === 'ok' && incomplete && !legitimateNoWork) {
717
+ 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)
718
+ log('coding cycle incomplete; recovery requires: ' + missing.join(', '))
719
+ 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
720
  }
701
721
  } finally {
702
722
  if (heartbeat) clearInterval(heartbeat)