openvisio-agent 0.16.1 → 0.16.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
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/events.mjs CHANGED
@@ -26,6 +26,14 @@ 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
+ export function taskIsCompleted(task, completedTypeIds = new Set()) {
30
+ if (!task || typeof task !== 'object') return false
31
+ if (task.deleted_at || task.deletedAt || task.completed_at || task.completedAt || task.closed_at || task.closedAt || task.archived_at || task.archivedAt) return true
32
+ if (completedTypeIds.has(Number(task.type_id ?? task.typeId ?? task.status_id ?? task.statusId))) return true
33
+ const state = [task.status, task.state, task.type?.name, task.task_type?.name].filter(Boolean).join(' ')
34
+ return /\b(?:done|complete|completed|closed|cancelled|canceled|archived|resolved)\b/i.test(state)
35
+ }
36
+
29
37
  // A mention event means this agent's name appeared somewhere, not necessarily
30
38
  // that the request was addressed to it. Reject a later-agent hand-off before a
31
39
  // model starts, while keeping explicitly shared requests addressed to both.
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 { requestTargetsLaterAgent, taskAgentId, taskFromEvent } from './events.mjs'
13
+ import { requestTargetsLaterAgent, taskAgentId, taskFromEvent, taskIsCompleted } from './events.mjs'
14
14
 
15
15
  // Behaviour prompts. The openvisio-team MCP bridge requires the agent's
16
16
  // credentials as ARGUMENTS on every tool call — those are injected at runtime by
@@ -73,7 +73,7 @@ const CODE_CHARTER = [
73
73
  '',
74
74
  'WORK ETHIC — how a reliable teammate behaves (this is the difference between useful and ignored):',
75
75
  ' 1. CLOSE THE LOOP in THIS cycle. Never say "I\'ll do X" and stop. If you commit to something, do it NOW — the human must never have to remind you to circle back.',
76
- ' 2. FINISH, then REPORT. Your LAST action every cycle is to update/move the ticket with update_ticket when appropriate and post a short channel result with the branch, PR link, and test result. @mention the requester by their EXACT full name.',
76
+ ' 2. FINISH, then REPORT. Always update/move the ticket with update_ticket. Post a channel result ONLY when this cycle includes a specific source channel/thread from a human request, and reply in that thread. Backlog-only work has no implied audience: update the ticket with evidence and do not announce it in an unrelated channel.',
77
77
  ' 3. Be honest and specific. Never invent progress. If you are genuinely blocked (missing repo, unclear spec, a failing tool), say exactly what you need in one message — that IS closing the loop.',
78
78
  ' 4. One reply per channel per cycle; answer several nudges together.',
79
79
  '',
@@ -89,7 +89,7 @@ const CODE_FULL = [
89
89
  ' 3. CHANGE + VERIFY: Read/Edit/Write the files; run the tests or build if the repo has them.',
90
90
  ' 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.',
91
91
  ' 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.',
92
- ' 6. CLOSE THE LOOP: move/update the ticket with update_ticket when appropriate, and post a channel reply with the summary + PR link/branch that @mentions the requester by their exact full name.',
92
+ ' 6. CLOSE THE LOOP: move/update the ticket with update_ticket. Reply with the summary + PR link only in a source thread explicitly supplied by the event. For backlog-only tickets, do not post_message or seek a channel to announce completion.',
93
93
  'Bash is for git / gh / tests / clone ONLY — never to hunt for credentials (they are given to you above).',
94
94
  ].join('\n')
95
95
 
@@ -577,7 +577,15 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
577
577
  // Mentions we've already reacted to (by message id) — the backend can re-deliver
578
578
  // an agent:mention (reconnect replay, dup fan-out), which otherwise makes the
579
579
  // agent reply to the SAME message twice.
580
- const seenMentions = new Set()
580
+ const replayPath = join(OV_DIR, 'watch-' + slug + '-replay.json')
581
+ let replayState = {}
582
+ try { replayState = JSON.parse(readFileSync(replayPath, 'utf8')) } catch { /* first run */ }
583
+ const seenMentions = new Set(Array.isArray(replayState.seenMentions) ? replayState.seenMentions : [])
584
+ const seenActivities = new Set(Array.isArray(replayState.seenActivities) ? replayState.seenActivities : [])
585
+ const trimSeen = (set) => { while (set.size > 500) set.delete(set.values().next().value) }
586
+ const persistReplay = () => {
587
+ try { writeJson(replayPath, { seenMentions: [...seenMentions], seenActivities: [...seenActivities] }, true) } catch { /* best-effort */ }
588
+ }
581
589
  // Context lines from the events themselves (the WS payload already carries the
582
590
  // channel + message / task), so the agent acts on THEM directly instead of
583
591
  // hoping poll_inbox re-surfaces the same item. Accumulated across coalesced
@@ -610,7 +618,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
610
618
  }
611
619
  const ensureMcpSession = async () => {
612
620
  if (mcpSessionId) return
613
- const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.16.1' } } }, false)
621
+ const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.16.2' } } }, false)
614
622
  if (!res.ok) throw new Error('MCP initialize HTTP ' + res.status)
615
623
  await mcpPayload(res)
616
624
  mcpSessionId = res.headers.get('mcp-session-id') || ''
@@ -660,11 +668,11 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
660
668
  callMcpTool('list_task_types', { project_id: project.id }).then(toolData),
661
669
  callMcpTool('list_activity', { project_id: project.id }).then(toolData),
662
670
  ])
663
- const doneIds = new Set((Array.isArray(typesData.types) ? typesData.types : Array.isArray(typesData.task_types) ? typesData.task_types : Array.isArray(typesData.taskTypes) ? typesData.taskTypes : []).filter((t) => /^(?:done|complete|completed|closed)$/i.test(String(t.name || '').trim())).map((t) => Number(t.id)))
671
+ const doneIds = new Set((Array.isArray(typesData.types) ? typesData.types : Array.isArray(typesData.task_types) ? typesData.task_types : Array.isArray(typesData.taskTypes) ? typesData.taskTypes : []).filter((t) => /\b(?:done|complete|completed|closed|cancelled|canceled|archived|resolved)\b/i.test(String(t.name || ''))).map((t) => Number(t.id)))
664
672
  for (const task of Array.isArray(tasksData.tasks) ? tasksData.tasks : []) {
665
673
  const taskAgentId = Number(task.agent_id ?? task.agentId ?? task.agent?.id)
666
674
  const taskIdent = String(task.agent?.identifier ?? task.agent?.slug ?? '')
667
- if (!task.deleted_at && !doneIds.has(Number(task.type_id ?? task.typeId)) && (taskAgentId === Number(self.id) || taskIdent === identifier)) {
675
+ if (!taskIsCompleted(task, doneIds) && (taskAgentId === Number(self.id) || taskIdent === identifier)) {
668
676
  assigned.push({ id: task.id, projectId: project.id, project: project.name, title: task.title, priority: task.priority, typeId: task.type_id ?? task.typeId })
669
677
  }
670
678
  }
@@ -673,9 +681,14 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
673
681
  for (const item of activities) {
674
682
  const text = JSON.stringify(item)
675
683
  const lower = text.toLowerCase()
676
- if (mentionNeedles.some((needle) => lower.includes(needle)) && /message|mention|channel/i.test(text)) mentionActivity.push({ projectId: project.id, project: project.name, activity: item })
684
+ const activityKey = String(project.id) + ':' + String(item.id ?? item.message_id ?? item.messageId ?? text.slice(0, 500))
685
+ if (!seenActivities.has(activityKey) && mentionNeedles.some((needle) => lower.includes(needle)) && /message|mention|channel/i.test(text)) {
686
+ seenActivities.add(activityKey); trimSeen(seenActivities)
687
+ mentionActivity.push({ projectId: project.id, project: project.name, activity: item })
688
+ }
677
689
  }
678
690
  }
691
+ if (mentionActivity.length) persistReplay()
679
692
  if (!assigned.length) lastTaskSignature = ''
680
693
  else {
681
694
  const signature = JSON.stringify(assigned)
@@ -684,7 +697,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
684
697
  lastTaskSignature = signature
685
698
  lastTaskTriggeredAt = Date.now()
686
699
  log('backlog reconciliation found ' + assigned.length + ' assigned task(s) -> coding cycle')
687
- void drain('full', `Backlog reconciliation verified these open tasks are assigned to YOU: ${JSON.stringify(assigned)}. Start with the highest-priority/oldest one immediately. Do not call nonexistent get_marching_orders or poll_inbox tools, and do not merely acknowledge. Use get_ticket if more detail is needed, move it to the active column with update_ticket, complete the repository work, verify it, open the PR, move the ticket to done when appropriate, and report evidence.`)
700
+ void drain('full', `Backlog reconciliation verified these open tasks are assigned to YOU: ${JSON.stringify(assigned)}. Start with the highest-priority/oldest one immediately. This is backlog-only work with NO source message or requester thread: do NOT post_message and do not announce progress or completion in any channel. Use get_ticket if more detail is needed, move it to the active column with update_ticket, complete the repository work, verify it, open the PR, then update the ticket with the evidence and move it to done when appropriate.`)
688
701
  }
689
702
  }
690
703
  const inboxSignature = JSON.stringify(mentionActivity.slice(-20)).slice(0, 4000)
@@ -827,7 +840,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
827
840
  // when the payload carries no id.
828
841
  const dedupeKey = mid != null ? 'id:' + mid : 'sig:' + (cid != null ? cid : '?') + '|' + text.slice(0, 100)
829
842
  if (seenMentions.has(dedupeKey)) { log('agent:mention (dup) — skipped'); return }
830
- seenMentions.add(dedupeKey); if (seenMentions.size > 500) seenMentions.clear()
843
+ seenMentions.add(dedupeKey); trimSeen(seenMentions); persistReplay()
831
844
  if (requestTargetsLaterAgent(text, [slug, identifier])) {
832
845
  log('agent:mention addressed to a later-mentioned agent — skipped')
833
846
  return