openvisio-agent 0.13.0 → 0.14.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 +1 -1
- package/package.json +1 -1
- package/src/watch.mjs +69 -42
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ 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
|
|
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
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`.
|
|
62
62
|
|
package/package.json
CHANGED
package/src/watch.mjs
CHANGED
|
@@ -36,7 +36,7 @@ const REPLY_DISCIPLINE = [
|
|
|
36
36
|
|
|
37
37
|
// ── CHAT-ONLY agents (no --workdir): chat/ticket tools, no code surface. ──────
|
|
38
38
|
const CHAT_CHARTER = [
|
|
39
|
-
'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode.
|
|
39
|
+
'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 commonly provides post_message, react_message, list_agents, list_projects, list_tasks, get_ticket, update_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.',
|
|
40
40
|
'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.',
|
|
41
41
|
'',
|
|
42
42
|
REPLY_DISCIPLINE,
|
|
@@ -64,7 +64,7 @@ const COORDINATE = [
|
|
|
64
64
|
// A stable "who you are / how you work" charter prepended to every code cycle.
|
|
65
65
|
const CODE_CHARTER = [
|
|
66
66
|
'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:',
|
|
67
|
-
' • openvisio-team
|
|
67
|
+
' • 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, plus post_message/react_message/list_activity. Relay runtimes may additionally expose poll_inbox, get_marching_orders, or comment_ticket.',
|
|
68
68
|
' • Read / Grep / Glob / Edit / Write / MultiEdit — inspect AND change code.',
|
|
69
69
|
' • Bash — git (branch, commit, push a branch), gh (clone repos, open PRs), run tests/builds.',
|
|
70
70
|
'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.',
|
|
@@ -72,7 +72,7 @@ const CODE_CHARTER = [
|
|
|
72
72
|
'',
|
|
73
73
|
'WORK ETHIC — how a reliable teammate behaves (this is the difference between useful and ignored):',
|
|
74
74
|
' 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.',
|
|
75
|
-
' 2. FINISH, then REPORT. Your LAST action every cycle is
|
|
75
|
+
' 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
76
|
' 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.',
|
|
77
77
|
' 4. One reply per channel per cycle; answer several nudges together.',
|
|
78
78
|
'',
|
|
@@ -80,7 +80,7 @@ const CODE_CHARTER = [
|
|
|
80
80
|
].join('\n')
|
|
81
81
|
|
|
82
82
|
const CODE_FULL = [
|
|
83
|
-
'THIS CYCLE:
|
|
83
|
+
'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.',
|
|
84
84
|
'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.',
|
|
85
85
|
'For real code work (an assigned ticket, or a mention asking for changes), run the full flow end-to-end:',
|
|
86
86
|
' 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.',
|
|
@@ -88,7 +88,7 @@ const CODE_FULL = [
|
|
|
88
88
|
' 3. CHANGE + VERIFY: Read/Edit/Write the files; run the tests or build if the repo has them.',
|
|
89
89
|
' 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.',
|
|
90
90
|
' 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.',
|
|
91
|
-
' 6. CLOSE THE LOOP:
|
|
91
|
+
' 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
92
|
'Bash is for git / gh / tests / clone ONLY — never to hunt for credentials (they are given to you above).',
|
|
93
93
|
].join('\n')
|
|
94
94
|
|
|
@@ -111,7 +111,7 @@ const INTRO = [
|
|
|
111
111
|
].join('\n')
|
|
112
112
|
const SWEEP = [
|
|
113
113
|
'DAILY CATCH-UP — you may have missed items while offline. Prioritize TASKS.',
|
|
114
|
-
'
|
|
114
|
+
'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:',
|
|
115
115
|
' 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.',
|
|
116
116
|
' 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.',
|
|
117
117
|
'If there is genuinely nothing outstanding, STOP silently — do NOT post a "nothing to do" message.',
|
|
@@ -517,7 +517,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
517
517
|
const canCode = !!workdir
|
|
518
518
|
// The Mastra bridge authenticates per-CALL: every openvisio-team tool needs
|
|
519
519
|
// agent_identifier + agent_api_key as arguments. Hand them over up front.
|
|
520
|
-
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
|
|
520
|
+
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 work with list_agents, list_projects, list_tasks, get_ticket, update_ticket, and list_activity; get_marching_orders, poll_inbox, and comment_ticket may be absent. 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.`
|
|
521
521
|
// The STATIC charter + creds are the session system prompt (cached, billed once),
|
|
522
522
|
// NOT re-sent in every cycle's user message — the big token saving.
|
|
523
523
|
const systemPrompt = (canCode ? CODE_CHARTER : CHAT_CHARTER) + '\n\n' + credNote
|
|
@@ -553,6 +553,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
553
553
|
let backlogProbeBusy = false
|
|
554
554
|
let lastTaskSignature = ''
|
|
555
555
|
let lastTaskTriggeredAt = 0
|
|
556
|
+
let lastInboxSignature = ''
|
|
556
557
|
let mcpSessionId = ''
|
|
557
558
|
let mcpRpcId = 0
|
|
558
559
|
|
|
@@ -577,7 +578,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
577
578
|
}
|
|
578
579
|
const ensureMcpSession = async () => {
|
|
579
580
|
if (mcpSessionId) return
|
|
580
|
-
const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.
|
|
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)
|
|
581
582
|
if (!res.ok) throw new Error('MCP initialize HTTP ' + res.status)
|
|
582
583
|
await mcpPayload(res)
|
|
583
584
|
mcpSessionId = res.headers.get('mcp-session-id') || ''
|
|
@@ -594,7 +595,17 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
594
595
|
}
|
|
595
596
|
const payload = await mcpPayload(res)
|
|
596
597
|
if (payload.error) throw new Error(`MCP ${name}: ${payload.error.message || 'tool error'}`)
|
|
597
|
-
|
|
598
|
+
const result = payload.result ?? payload
|
|
599
|
+
if (result?.isError) {
|
|
600
|
+
const detail = result.content?.find?.((c) => c?.type === 'text')?.text || 'tool error'
|
|
601
|
+
throw new Error(`MCP ${name}: ${detail}`)
|
|
602
|
+
}
|
|
603
|
+
return result
|
|
604
|
+
}
|
|
605
|
+
const toolData = (result) => {
|
|
606
|
+
const text = result?.content?.find?.((c) => c?.type === 'text')?.text
|
|
607
|
+
if (typeof text !== 'string') return result?.structuredContent ?? result ?? {}
|
|
608
|
+
try { return JSON.parse(text) } catch { return { text } }
|
|
598
609
|
}
|
|
599
610
|
|
|
600
611
|
// Reconcile everything that may have arrived while disconnected. This spends no
|
|
@@ -603,27 +614,53 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
603
614
|
if (!mcpUrl || backlogProbeBusy || busy) return
|
|
604
615
|
backlogProbeBusy = true
|
|
605
616
|
try {
|
|
606
|
-
const
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
617
|
+
const agentsData = toolData(await callMcpTool('list_agents'))
|
|
618
|
+
const agents = Array.isArray(agentsData.agents) ? agentsData.agents : []
|
|
619
|
+
const self = agents.find((a) => String(a.identifier || a.slug || '') === identifier)
|
|
620
|
+
if (!self?.id) throw new Error('list_agents did not return this BYO agent')
|
|
621
|
+
const projectsData = toolData(await callMcpTool('list_projects'))
|
|
622
|
+
const projects = Array.isArray(projectsData.projects) ? projectsData.projects : []
|
|
623
|
+
const assigned = []
|
|
624
|
+
const mentionActivity = []
|
|
625
|
+
for (const project of projects) {
|
|
626
|
+
const [tasksData, typesData, activityData] = await Promise.all([
|
|
627
|
+
callMcpTool('list_tasks', { project_id: project.id }).then(toolData),
|
|
628
|
+
callMcpTool('list_task_types', { project_id: project.id }).then(toolData),
|
|
629
|
+
callMcpTool('list_activity', { project_id: project.id }).then(toolData),
|
|
630
|
+
])
|
|
631
|
+
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)))
|
|
632
|
+
for (const task of Array.isArray(tasksData.tasks) ? tasksData.tasks : []) {
|
|
633
|
+
const taskAgentId = Number(task.agent_id ?? task.agentId ?? task.agent?.id)
|
|
634
|
+
const taskIdent = String(task.agent?.identifier ?? task.agent?.slug ?? '')
|
|
635
|
+
if (!task.deleted_at && !doneIds.has(Number(task.type_id ?? task.typeId)) && (taskAgentId === Number(self.id) || taskIdent === identifier)) {
|
|
636
|
+
assigned.push({ id: task.id, projectId: project.id, project: project.name, title: task.title, priority: task.priority, typeId: task.type_id ?? task.typeId })
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const activities = Array.isArray(activityData.activities) ? activityData.activities : Array.isArray(activityData.activity) ? activityData.activity : []
|
|
640
|
+
const mentionNeedles = [self.name, self.identifier, self.slug, identifier].filter(Boolean).map((s) => '@' + String(s).toLowerCase())
|
|
641
|
+
for (const item of activities) {
|
|
642
|
+
const text = JSON.stringify(item)
|
|
643
|
+
const lower = text.toLowerCase()
|
|
644
|
+
if (mentionNeedles.some((needle) => lower.includes(needle)) && /message|mention|channel/i.test(text)) mentionActivity.push({ projectId: project.id, project: project.name, activity: item })
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (!assigned.length) lastTaskSignature = ''
|
|
648
|
+
else {
|
|
649
|
+
const signature = JSON.stringify(assigned)
|
|
613
650
|
const retryDue = Date.now() - lastTaskTriggeredAt > 30 * 60 * 1000
|
|
614
651
|
if (signature !== lastTaskSignature || retryDue) {
|
|
615
652
|
lastTaskSignature = signature
|
|
616
653
|
lastTaskTriggeredAt = Date.now()
|
|
617
|
-
log('backlog reconciliation found
|
|
618
|
-
void drain('full',
|
|
654
|
+
log('backlog reconciliation found ' + assigned.length + ' assigned task(s) -> coding cycle')
|
|
655
|
+
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.`)
|
|
619
656
|
}
|
|
620
657
|
}
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
log('backlog reconciliation found
|
|
625
|
-
void drain('fast',
|
|
626
|
-
}
|
|
658
|
+
const inboxSignature = JSON.stringify(mentionActivity.slice(-20)).slice(0, 4000)
|
|
659
|
+
if (mentionActivity.length && inboxSignature !== lastInboxSignature) {
|
|
660
|
+
lastInboxSignature = inboxSignature
|
|
661
|
+
log('backlog reconciliation found mention activity -> reply cycle')
|
|
662
|
+
void drain('fast', `Recent project activity contains these messages mentioning YOU: ${inboxSignature}. Handle each still-unanswered mention once using the channel/message ids in the activity. Do not call nonexistent poll_inbox or get_marching_orders tools. Skip anything already answered by you.`)
|
|
663
|
+
} else if (!mentionActivity.length) lastInboxSignature = ''
|
|
627
664
|
} catch (e) {
|
|
628
665
|
mcpSessionId = ''
|
|
629
666
|
log('backlog reconciliation failed: ' + (e && e.message ? e.message : e))
|
|
@@ -656,9 +693,10 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
656
693
|
// Codex can exit successfully after posting only "I'll do it". For coding
|
|
657
694
|
// cycles, treat that as incomplete and give it one focused recovery turn.
|
|
658
695
|
// Do not retry a real blocker, which would only create duplicate noise.
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
696
|
+
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)
|
|
662
700
|
}
|
|
663
701
|
} finally {
|
|
664
702
|
if (heartbeat) clearInterval(heartbeat)
|
|
@@ -801,8 +839,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
801
839
|
log('up — backend WS watcher on ' + wsUrl + (canCode ? ' [code: ' + workdir + ']' : ''))
|
|
802
840
|
handle = connectAgentWs({ wsUrl, apiKey, identifier, onEvent, onConnect: () => { setTimeout(() => void reconcileBacklog(), 250) }, log })
|
|
803
841
|
|
|
804
|
-
|
|
805
|
-
let introTimer = null, sweepStartTimer = null, sweepTimer = null, taskProbeStartTimer = null, taskProbeTimer = null
|
|
842
|
+
let introTimer = null, taskProbeStartTimer = null, taskProbeTimer = null
|
|
806
843
|
if (mcpConfig || mcpUrl) {
|
|
807
844
|
// Workspace ethics: a one-time hello the FIRST time this agent ever connects.
|
|
808
845
|
const introMarker = join(OV_DIR, 'intro-' + slugify(identifier) + '.done')
|
|
@@ -811,17 +848,9 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
811
848
|
log('first connection — introducing self to the workspace')
|
|
812
849
|
introTimer = setTimeout(() => void drain('intro'), 5000) // let the socket subscribe first
|
|
813
850
|
}
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
if (claimStartupSweep(identifier)) {
|
|
818
|
-
sweepStartTimer = setTimeout(() => { log('startup catch-up sweep'); void drain('sweep', SWEEP) }, 12_000)
|
|
819
|
-
} else {
|
|
820
|
-
log('startup catch-up sweep skipped (ran within the last 6h)')
|
|
821
|
-
}
|
|
822
|
-
sweepTimer = setInterval(() => { log('daily catch-up sweep'); void drain('sweep', SWEEP) }, DAY_MS)
|
|
823
|
-
// Cheap reliability net: no model runs unless the MCP result actually contains
|
|
824
|
-
// assigned work. This also catches assignments created while the socket was down.
|
|
851
|
+
// Reconciliation replaces the old model-driven startup/daily sweep. It uses
|
|
852
|
+
// supported MCP tools directly, stays silent when empty, and hands verified
|
|
853
|
+
// coding tasks to the full lane instead of letting Luna announce it cannot code.
|
|
825
854
|
taskProbeStartTimer = setTimeout(() => void reconcileBacklog(), 3_000)
|
|
826
855
|
taskProbeTimer = setInterval(() => void reconcileBacklog(), 5 * 60 * 1000)
|
|
827
856
|
} else {
|
|
@@ -833,8 +862,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
833
862
|
// service doesn't leak a half-open connection or a dangling interval.
|
|
834
863
|
const bye = () => {
|
|
835
864
|
if (introTimer) clearTimeout(introTimer)
|
|
836
|
-
if (sweepStartTimer) clearTimeout(sweepStartTimer)
|
|
837
|
-
if (sweepTimer) clearInterval(sweepTimer)
|
|
838
865
|
if (taskProbeStartTimer) clearTimeout(taskProbeStartTimer)
|
|
839
866
|
if (taskProbeTimer) clearInterval(taskProbeTimer)
|
|
840
867
|
try { handle && handle.close() } catch { /* noop */ }
|