openvisio-agent 0.8.0 → 0.9.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/watch.mjs +45 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Connect your coding agent (Claude Code) to an OpenVisio team — MCP tools + optional autonomy — in one command. No shell scripts.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/watch.mjs CHANGED
@@ -34,16 +34,19 @@ const REPLY_DISCIPLINE = [
34
34
 
35
35
  // ── CHAT-ONLY agents (no --workdir): chat/ticket tools, no code surface. ──────
36
36
  const CHAT_CHARTER = [
37
- 'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode. Your tools are the openvisio-team chat/ticket tools (mcp__openvisio-team__*): post_message, poll_inbox, comment_ticket, react_message. You have NO file/Bash/git tools in this mode, so you cannot write code yourself.',
37
+ 'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode. Your tools are the openvisio-team chat/ticket tools post_message, poll_inbox, comment_ticket, react_message (your runtime may namespace them, e.g. mcp__openvisio-team__post_message or openvisio-team_post_message — call whatever names appear in your tool list). You have NO file/Bash/git tools in this mode, so you cannot write code yourself.',
38
38
  '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.',
39
39
  '',
40
40
  REPLY_DISCIPLINE,
41
41
  ].join('\n')
42
42
 
43
- const CYCLE = CHAT_CHARTER + '\n\nRun one OpenVisio autonomy cycle: call poll_inbox and handle mentions + follow-ups. Reply in 1-3 sentences, @mention people by their EXACT full name, at most one reply per channel. Then stop.'
44
- const CYCLE_FAST = CHAT_CHARTER + '\n\n' + [
43
+ // NOTE: the CHARTER (who you are + reply discipline) is NOT prepended here it is
44
+ // passed ONCE as the session's system prompt (cacheable, not re-billed every cycle).
45
+ // These bases are the small per-cycle instructions only. See createCycleRunner.
46
+ const CYCLE = 'Run one OpenVisio autonomy cycle: call poll_inbox and handle mentions + follow-ups. Reply in 1-3 sentences, @mention people by their EXACT full name, at most one reply per channel. Then stop.'
47
+ const CYCLE_FAST = [
45
48
  'New chat activity. Do EXACTLY ONE of these:',
46
- ' • IF a specific mention/message FOR YOU is given above: reply to THAT ONE message exactly once with mcp__openvisio-team__post_message, then STOP. Do NOT call poll_inbox and do NOT answer anything else this cycle — you already have the message; polling would make you re-answer it and double-post.',
49
+ ' • IF a specific mention/message FOR YOU is given above: reply to THAT ONE message exactly once with post_message, then STOP. Do NOT call poll_inbox and do NOT answer anything else this cycle — you already have the message; polling would make you re-answer it and double-post.',
47
50
  ' • IF NO specific mention is given above: call poll_inbox and reply only to items truly directed at YOU (a question to you, or a reply to your own message) — SKIP chatter aimed at someone else / another agent, ignore .tasks/.claimable, at most one reply per channel.',
48
51
  'Post ONE message total for the thing you are answering — compose it fully, then send once. Never send a reply and then a "better" version; never repeat a reply you already sent. Be sure of your answer before sending. If asked for work you have no tool for, say so plainly and offer to file a ticket. 1-3 sentences, no summary. Then stop.',
49
52
  ].join('\n')
@@ -52,7 +55,7 @@ const CYCLE_FAST = CHAT_CHARTER + '\n\n' + [
52
55
  // A stable "who you are / how you work" charter prepended to every code cycle.
53
56
  const CODE_CHARTER = [
54
57
  '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:',
55
- ' • openvisio-team chat/ticket tools (mcp__openvisio-team__*) — read mentions, post messages, comment on tickets, react.',
58
+ ' • openvisio-team chat/ticket tools — read mentions, post messages, comment on tickets, react (post_message / poll_inbox / comment_ticket / react_message; your runtime may namespace them — use whatever names appear in your tool list).',
56
59
  ' • Read / Grep / Glob / Edit / Write / MultiEdit — inspect AND change code.',
57
60
  ' • Bash — git (branch, commit, push a branch), gh (clone repos, open PRs), run tests/builds.',
58
61
  'YOUR WORKSPACE: your working directory is a WORKSPACE ROOT that holds the org\'s repos as subfolders. You are NOT limited to one repo — you work across ALL of them from here. For any task: find the relevant repo under the workspace; if it isn\'t cloned yet, CLONE it (gh repo clone <org>/<repo>, or git clone <url>) and then work inside that subfolder. Never make the user tell you a path or clone things for you.',
@@ -67,7 +70,7 @@ const CODE_CHARTER = [
67
70
  REPLY_DISCIPLINE,
68
71
  ].join('\n')
69
72
 
70
- const CODE_FULL = CODE_CHARTER + '\n\n' + [
73
+ const CODE_FULL = [
71
74
  'THIS CYCLE: call get_marching_orders and poll_inbox to see assigned tickets + mentions, then act on them.',
72
75
  'ACKNOWLEDGE ONCE: for a task assigned to you that you have NOT already acknowledged, post a SINGLE one-line comment_ticket ("On it — picking this up now") before you start. First check the ticket/thread — if you already acknowledged it on an earlier cycle, skip this and just keep working. Then report only when you have the result.',
73
76
  'For real code work (an assigned ticket, or a mention asking for changes), run the full flow end-to-end:',
@@ -80,9 +83,9 @@ const CODE_FULL = CODE_CHARTER + '\n\n' + [
80
83
  'Bash is for git / gh / tests / clone ONLY — never to hunt for credentials (they are given to you above).',
81
84
  ].join('\n')
82
85
 
83
- const CODE_FAST = CODE_CHARTER + '\n\n' + [
86
+ const CODE_FAST = [
84
87
  'New chat activity. Do EXACTLY ONE of these:',
85
- ' • IF a specific mention/message FOR YOU is given above: reply to THAT ONE message exactly once with mcp__openvisio-team__post_message, then STOP. Do NOT call poll_inbox and do NOT answer anything else this cycle — polling would re-surface the same message and make you double-post.',
88
+ ' • IF a specific mention/message FOR YOU is given above: reply to THAT ONE message exactly once with post_message, then STOP. Do NOT call poll_inbox and do NOT answer anything else this cycle — polling would re-surface the same message and make you double-post.',
86
89
  ' • IF NO specific mention is given above: call poll_inbox and reply only to items directed at YOU (asks you something, or responds to your own message) — SKIP chatter aimed at someone else / another agent; at most one reply per channel.',
87
90
  'If the message asks YOU for real CODE work, do the WHOLE job now — branch (checkout -B agent/<slug>), edit, run tests, commit, push your branch, open a PR (gh pr create), then reply once with the PR link and @mention the requester by exact full name. Never push to main, never --force, never merge.',
88
91
  'Post ONE message for the thing you are answering — send it once; never a reply then a "better" version; never repeat a reply you already sent; be sure before you send. Do NOT promise and stop — finish and report in THIS cycle. 1-3 sentences, no summary. Then stop.',
@@ -122,7 +125,12 @@ const DENY_TOOLS = [
122
125
  const FAST = 2500
123
126
  const SLOW = 6000
124
127
  const IDLE_AFTER = 60000
125
- const MAX_TURNS = 15
128
+ // Recycle the warm session after ONE cycle. Autonomy cycles are independent (a
129
+ // mention → a reply, a task → its work), so keeping a session across cycles just
130
+ // re-bills the entire prior history on every new cycle — the #1 token sink. Fresh
131
+ // per cycle keeps each cycle's cost to its own work (the static charter/creds ride
132
+ // in the cached system prompt, so a fresh spawn is cheap).
133
+ const MAX_TURNS = 1
126
134
  const SESSION_IDLE_MS = 1200000
127
135
  // A single cycle must finish within this or it's abandoned — otherwise a hung
128
136
  // cycle (e.g. an MCP tool stalling on a down bridge) would leave `busy` stuck
@@ -227,7 +235,7 @@ export async function runWatch({ flags }) {
227
235
  // The openvisio-team MCP is declared in an `opencode.json` written into the run cwd
228
236
  // (opencode reads it from there). `--auto` approves tool use non-interactively.
229
237
  // Same { runCycle, canCode } contract as the Claude runner.
230
- function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, maxCycleMs, log, debug, model }) {
238
+ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, maxCycleMs, log, debug, model, systemPrompt }) {
231
239
  // opencode reads opencode.json from its CWD: the code workspace, or a dedicated
232
240
  // per-agent dir for chat-only agents.
233
241
  const cwd = workdir || join(OV_DIR, 'opencode-' + (cfgKey || 'agent'))
@@ -253,7 +261,10 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
253
261
  return new Promise((resolve) => {
254
262
  ensureConfig()
255
263
  const m = cycleModel || model
256
- const args = ['run', prompt, '--auto', ...(m ? ['--model', m] : [])]
264
+ // opencode has no system-prompt flag; each run is a fresh process, so fold the
265
+ // charter/creds into the message (still not re-accumulated across cycles).
266
+ const full = systemPrompt ? systemPrompt + '\n\n' + prompt : prompt
267
+ const args = ['run', full, '--auto', ...(m ? ['--model', m] : [])]
257
268
  let child = null, done = false
258
269
  const finish = (o) => { if (done) return; done = true; clearTimeout(timer); resolve(o) }
259
270
  const timer = setTimeout(() => {
@@ -276,12 +287,12 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
276
287
  // ── Claude Code warm-session cycle runner (shared by the REST + WS loops) ─────
277
288
  // One persistent stream-json session, poked with a prompt per cycle. Recycled
278
289
  // after MAX_TURNS or SESSION_IDLE_MS. Returns { runCycle, canCode }.
279
- function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfig, workdir, log, debug, model, onTool }) {
290
+ function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfig, workdir, log, debug, model, onTool, systemPrompt }) {
280
291
  const canCode = !!workdir
281
292
  const maxCycleMs = canCode ? MAX_CODE_CYCLE_MS : MAX_CYCLE_MS
282
293
  // opencode drives cycles differently — a headless `opencode run` per cycle rather
283
294
  // than a persistent stream-json session. Same { runCycle, canCode } contract.
284
- if (agent === 'opencode') return createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, maxCycleMs, log, debug, model })
295
+ if (agent === 'opencode') return createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, maxCycleMs, log, debug, model, systemPrompt })
285
296
  let child = null
286
297
  // The model the CURRENT session was spawned with. runCycle can pass a different
287
298
  // model per cycle (cheap for chat, stronger for code) — a change recycles the
@@ -312,7 +323,9 @@ function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfi
312
323
  function ensureSession() {
313
324
  if (child && !child.killed) return
314
325
  if (!mcpConfig) { log('WARNING: no MCP config — the agent can react to events but has no tools to act. Re-connect with --mcp-url.') }
315
- const base = ['-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', '--strict-mcp-config', '--mcp-config', mcpConfig, ...(sessionModel ? ['--model', sessionModel] : [])]
326
+ // The charter + creds ride in the system prompt (cacheable not re-billed each
327
+ // cycle), leaving only the small per-cycle instruction in the user message.
328
+ const base = ['-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', '--strict-mcp-config', '--mcp-config', mcpConfig, ...(sessionModel ? ['--model', sessionModel] : []), ...(systemPrompt ? ['--append-system-prompt', systemPrompt] : [])]
316
329
  const args = canCode ? [...base, '--allowedTools', ...CODE_TOOLS, '--disallowedTools', ...DENY_TOOLS] : [...base, '--allowedTools', 'mcp__openvisio-team__*']
317
330
  const c = spawn(claude, args, { cwd: workdir || undefined, stdio: ['pipe', 'pipe', 'inherit'] })
318
331
  child = c
@@ -387,9 +400,16 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
387
400
  // agent:status broadcast (thinking → working → typing → done).
388
401
  const statusTargets = new Set()
389
402
  const emitStatus = (state) => { for (const c of statusTargets) { try { handle && handle.sendStatus(c, state) } catch { /* best-effort */ } } }
390
- const { runCycle, canCode } = createCycleRunner({
403
+ const canCode = !!workdir
404
+ // The Mastra bridge authenticates per-CALL: every openvisio-team tool needs
405
+ // agent_identifier + agent_api_key as arguments. Hand them over up front.
406
+ 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 (post_message, poll_inbox, react_message, comment_ticket, get_marching_orders, …). These tools may appear in your tool list namespaced (mcp__openvisio-team__post_message on Claude Code, openvisio-team_post_message on opencode) — call whichever names you actually see. The credentials are given to you right here — do NOT hunt for them (no Bash/grep/cat/find, no reading memory); just call the tools with these exact values. (Bash/git/gh ARE for your code work — this rule is only about not searching for these keys.)`
407
+ // The STATIC charter + creds are the session system prompt (cached, billed once),
408
+ // NOT re-sent in every cycle's user message — the big token saving.
409
+ const systemPrompt = (canCode ? CODE_CHARTER : CHAT_CHARTER) + '\n\n' + credNote
410
+ const { runCycle } = createCycleRunner({
391
411
  claude, agent, mcpUrl, mcpHeaders: { 'x-agent-api-key': apiKey, 'x-agent-identifier': identifier },
392
- cfgKey: identifier, mcpConfig, workdir, log, debug, model,
412
+ cfgKey: identifier, mcpConfig, workdir, log, debug, model, systemPrompt,
393
413
  // The moment the agent calls post_message it is about to speak → "typing".
394
414
  onTool: (name) => { if (/post_message/.test(name)) emitStatus('typing') },
395
415
  })
@@ -417,11 +437,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
417
437
  // events and drained into the next cycle's prompt.
418
438
  const pending = []
419
439
 
420
- // The Mastra bridge authenticates per-CALL, not per-connection: every
421
- // openvisio-team tool needs agent_identifier + agent_api_key as arguments.
422
- // Hand them to the model up front so it never shells around hunting for them.
423
- const credNote = `AUTH: the openvisio-team (mcp__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 (post_message, poll_inbox, react_message, comment_ticket, …). They are given to you right here — do NOT hunt for them (no Bash/grep/cat/find to locate credentials, no reading memory); just call the tools with these exact values. (Bash/git/gh ARE for your code work — this rule is only about not searching for these keys.)`
424
-
425
440
  // Higher rank wins when coalescing cycles requested while one is running.
426
441
  const RANK = { fast: 0, intro: 1, sweep: 2, full: 3 }
427
442
  const baseFor = (kind) => kind === 'intro' ? INTRO : (kind === 'full' || kind === 'sweep') ? fullPrompt : fastPrompt
@@ -431,7 +446,9 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
431
446
  if (busy) { queued = (RANK[kind] ?? 0) >= (RANK[queued] ?? 0) ? kind : queued; log('busy — queued a ' + kind + ' follow-up cycle'); return }
432
447
  busy = true
433
448
  const ctx = pending.splice(0) // take everything accumulated so far
434
- const prompt = credNote + '\n\n' + (ctx.length ? ctx.join('\n') + '\n\n' : '') + baseFor(kind)
449
+ // credNote + charter live in the cached system prompt now the per-cycle
450
+ // message is just the event context + the small base instruction.
451
+ const prompt = (ctx.length ? ctx.join('\n') + '\n\n' : '') + baseFor(kind)
435
452
  // Chat-shaped cycles (mentions/intro) may run on the cheaper chat model; code
436
453
  // work (full/sweep) uses the main model.
437
454
  const useModel = (kind === 'fast' || kind === 'intro') ? liteModel : codeModel
@@ -552,7 +569,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
552
569
  // takes it to working → typing → done).
553
570
  if (cid != null) { statusTargets.add(cid); try { handle && handle.sendStatus(cid, 'thinking') } catch { /* best-effort */ } }
554
571
  const ctx = cid != null
555
- ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. Send EXACTLY ONE reply with mcp__openvisio-team__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/"better" one. 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 — "@${who.split(' ')[0]}" alone will NOT).` : ''} You ALREADY have the message here — do NOT poll_inbox, and after your single reply, STOP.`
572
+ ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. 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/"better" one. 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 — "@${who.split(' ')[0]}" alone will NOT).` : ''} You ALREADY have the message here — do NOT poll_inbox, and after your single reply, STOP.`
556
573
  : undefined
557
574
  void drain('fast', ctx)
558
575
  } else if (k === 'error') {
@@ -609,7 +626,11 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
609
626
  function loop({ host, key, slug, claude, agent, mcpConfig, mcpUrl, workdir, model, chatModel, debug }) {
610
627
  const log = (m) => process.stdout.write('[warm ' + new Date().toISOString() + '] ' + m + '\n')
611
628
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
612
- const { runCycle, canCode } = createCycleRunner({ claude, agent, mcpUrl, mcpHeaders: { Authorization: 'Bearer ' + key }, cfgKey: slug, mcpConfig, workdir, log, debug, model })
629
+ const canCode = !!workdir
630
+ // Static charter as the cached system prompt (this loop's MCP authenticates via a
631
+ // Bearer header, so no per-call creds are needed in the prompt).
632
+ const systemPrompt = canCode ? CODE_CHARTER : CHAT_CHARTER
633
+ const { runCycle } = createCycleRunner({ claude, agent, mcpUrl, mcpHeaders: { Authorization: 'Bearer ' + key }, cfgKey: slug, mcpConfig, workdir, log, debug, model, systemPrompt })
613
634
  const fullPrompt = canCode ? CODE_FULL : CYCLE
614
635
  const fastPrompt = canCode ? CODE_FAST : CYCLE_FAST
615
636
  const liteModel = chatModel || model // cheaper model for chat/quick cycles