openvisio-agent 0.18.1 → 0.18.3
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 +3 -1
- package/package.json +1 -1
- package/scripts/certify.mjs +17 -1
- package/src/events.mjs +3 -0
- package/src/mcp-http.mjs +116 -0
- package/src/opencode-config.mjs +29 -0
- package/src/watch.mjs +94 -84
package/README.md
CHANGED
|
@@ -56,7 +56,9 @@ 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 discovers and caches the backend's actual `tools/list` response, then uses available tools such as `list_agents`, `list_projects`, `list_tasks`, `list_task_types`, and `list_activity` to recover assigned tasks and recent mention activity missed while offline. Optional actions such as ticket comments are used only when advertised; their absence cannot strand a completed ticket in a retry loop. The same zero-model check runs every five minutes as a safety net; a model starts only when pending work exists.
|
|
60
|
+
|
|
61
|
+
The backend MCP may be stateful or stateless. A successful initialize response without `Mcp-Session-Id` is accepted as stateless, so OpenCode agents do not stop with “MCP initialize returned no session id.” Each OpenCode lane keeps its MCP identity in a private per-agent config directory while the repository is supplied separately with `--dir`; stale workspace configuration therefore cannot swap one agent's credentials for another's. The generated remote configuration sends the agent headers directly, disables OAuth probing, and backend cycles never request relay-only inbox calls or MCP resource-discovery tools in place of team actions.
|
|
60
62
|
|
|
61
63
|
Codex BYO agents follow the repository's normative runtime specification in `docs/CODEX_BYO_AGENT_SPEC.md`: one WebSocket identity, independent Sol reply/work lanes, authoritative `get_ticket` verification for assignments, REST-backed in-app activity, persistent replay suppression, and runtime evidence gates before completion. Maintainers must run `npm run certify` before publishing.
|
|
62
64
|
|
package/package.json
CHANGED
package/scripts/certify.mjs
CHANGED
|
@@ -23,11 +23,14 @@ run('diff whitespace check', 'git', ['diff', '--check'], repo)
|
|
|
23
23
|
const watcher = readFileSync(join(root, 'src', 'watch.mjs'), 'utf8')
|
|
24
24
|
const events = readFileSync(join(root, 'src', 'events.mjs'), 'utf8')
|
|
25
25
|
const memory = readFileSync(join(root, 'src', 'memory.mjs'), 'utf8')
|
|
26
|
+
const mcpHttp = readFileSync(join(root, 'src', 'mcp-http.mjs'), 'utf8')
|
|
27
|
+
const opencodeConfig = readFileSync(join(root, 'src', 'opencode-config.mjs'), 'utf8')
|
|
26
28
|
const prPush = readFileSync(join(root, 'src', 'pr-push.mjs'), 'utf8')
|
|
27
29
|
const cli = readFileSync(join(root, 'bin', 'cli.mjs'), 'utf8')
|
|
28
30
|
const websocket = readFileSync(join(root, 'src', 'ws.mjs'), 'utf8')
|
|
29
31
|
const activityHook = readFileSync(join(repo, 'frontend', 'hooks', 'useAgentActivity.ts'), 'utf8')
|
|
30
32
|
const taskHook = readFileSync(join(repo, 'frontend', 'hooks', 'useBackendTasks.ts'), 'utf8')
|
|
33
|
+
const liveTasks = readFileSync(join(repo, 'frontend', 'lib', 'collab', 'liveTasks.ts'), 'utf8')
|
|
31
34
|
const spec = readFileSync(join(repo, 'docs', 'CODEX_BYO_AGENT_SPEC.md'), 'utf8')
|
|
32
35
|
|
|
33
36
|
const assertions = [
|
|
@@ -38,7 +41,7 @@ const assertions = [
|
|
|
38
41
|
['assigned coding completion is posted by the watcher', watcher.includes('announceTaskCompletion') && watcher.includes('postMessageOnce({ key: `completion:${report.key}`')],
|
|
39
42
|
['completion requires review/done state and PR evidence', watcher.includes('buildTaskCompletionReport') && watcher.includes('completion report deferred')],
|
|
40
43
|
['completion delivery survives reconnect and deduplicates', watcher.includes('pendingCompletionReports: [...pendingCompletionReports]') && watcher.includes('reportedCompletions: [...reportedCompletions]') && watcher.includes('reportedCompletions.has(report.key)')],
|
|
41
|
-
['
|
|
44
|
+
['optional ticket comments cannot block verified completion', watcher.includes("callOptionalMcpTool('comment_ticket', { project_id: projectId, ticket_id: ticketId, text: report.content })") && watcher.includes('comment_ticket is not exposed; completing ticket') && watcher.includes('reportedTaskComments: [...reportedTaskComments]') && watcher.includes('reportedTaskComments.has(report.key)')],
|
|
42
45
|
['ticket comments cannot masquerade as channel completion', watcher.includes('didChannelMessage') && watcher.includes("mcpCalls.includes('post_message')")],
|
|
43
46
|
['single-watcher acquisition is atomic and fails closed', watcher.includes("openSync(lockPath, 'wx')") && watcher.includes('Could not acquire the single-watcher lock')],
|
|
44
47
|
['websocket and activity mention delivery share a replay guard', watcher.includes('markMentionHandled(activityMessage, activityChannelId)') && watcher.includes('markMentionHandled(msg, cid)') && watcher.includes('recentMentionSignatures')],
|
|
@@ -59,9 +62,22 @@ const assertions = [
|
|
|
59
62
|
['frontend consumes typing event', activityHook.includes("'channel:agent:typing'")],
|
|
60
63
|
['frontend activity TTL distinguishes work from typing', activityHook.includes('thinking: 6_000') && activityHook.includes('typing: 5_000') && activityHook.includes('working: 30_000')],
|
|
61
64
|
['frontend consumes documented task comment events', taskHook.includes("'task:comment':") && taskHook.includes("'task:comment_updated':") && taskHook.includes("'task:comment_deleted':") && taskHook.includes("'task:comment_reacted':")],
|
|
65
|
+
['ticket comments load only on demand', !taskHook.includes('for (const task of tasks)') && taskHook.includes('loadedCommentIds.current.has(taskId)')],
|
|
66
|
+
['concurrent ticket comment loads share one request', taskHook.includes('commentRequests.current.get(requestKey)') && taskHook.includes('commentRequests.current.set(requestKey, request)')],
|
|
67
|
+
['backend ticket slugs render uppercase', liveTasks.includes("t.slug?.trim().toUpperCase()")],
|
|
62
68
|
['completion has an evidence failure gate', watcher.includes('WORK_CYCLE_FAILED')],
|
|
63
69
|
['OpenCode emits structured events for runtime evidence', watcher.includes("'--format', 'json'") && watcher.includes('opencodeEventEvidence(event)')],
|
|
70
|
+
['OpenCode API-key MCP disables OAuth probing', opencodeConfig.includes("oauth: false") && opencodeConfig.includes('timeout: 15_000')],
|
|
71
|
+
['OpenCode identities use isolated configs outside the code workspace', watcher.includes('opencodeRuntimeLayout({ cfgKey, workdir })') && watcher.includes("'--dir', workspace") && watcher.includes('OPENCODE_CONFIG: opencodeConfigPath') && watcher.includes('OPENCODE_CONFIG_CONTENT: JSON.stringify(opencodeConfig)')],
|
|
72
|
+
['OpenCode backend prompts forbid relay and resource-discovery tools', watcher.includes('BACKEND MCP RULE') && watcher.includes('get_marching_orders, poll_inbox, get_resource, list_mcp_resources, list_mcp_resource_templates')],
|
|
73
|
+
['OpenCode tool failures retain sanitized diagnostics', events.includes('toolError: toolError.replace') && watcher.includes("opencode tool '") && watcher.includes("split(redactKey).join('[redacted]')")],
|
|
64
74
|
['OpenCode acknowledgements cannot satisfy coding completion', watcher.includes("agent === 'codex' || agent === 'opencode'") && watcher.includes('releaseTaskForRetry(activeTaskRef, prompt)')],
|
|
75
|
+
['backend MCP accepts stateless initialize responses', mcpHttp.includes("mode = () => !initialized ? 'uninitialized' : sessionId ? 'stateful' : 'stateless'") && !watcher.includes('MCP initialize returned no session id')],
|
|
76
|
+
['MCP initialize is shared across concurrent startup probes', mcpHttp.includes('if (initializePromise) return initializePromise')],
|
|
77
|
+
['stateless tool errors do not cause initialize loops', mcpHttp.includes('if (hadSession && !retried')],
|
|
78
|
+
['backend MCP tools are discovered and cached', mcpHttp.includes("method: 'tools/list'") && mcpHttp.includes('if (!refresh && toolsCache)') && watcher.includes('discoverMcpTools')],
|
|
79
|
+
['missing optional MCP tools use compatibility fallbacks', watcher.includes("reason: 'not-advertised'") && watcher.includes('recording the blocker in the ticket description')],
|
|
80
|
+
['backend introduction is watcher-owned for every runtime', watcher.includes('void announceIntroduction().then((delivered)')],
|
|
65
81
|
['Codex policy rejection is captured from stderr', watcher.includes("stdio: ['ignore', 'pipe', 'pipe']") && watcher.includes('forwardDiagnostic(d)') && watcher.includes('inspectDiagnostic(incoming)')],
|
|
66
82
|
['Codex recoverable subprocess diagnostics are not surfaced as activity', watcher.includes('shouldSuppressCodexDiagnostic(line)') && watcher.includes("forwardDiagnostic('', true)") && watcher.includes('RECOVER DEAD COMMAND SESSIONS')],
|
|
67
83
|
['policy rejection cannot be logged as successful', watcher.includes("subtype = policyBlock ? 'blocked'")],
|
package/src/events.mjs
CHANGED
|
@@ -89,6 +89,8 @@ export function opencodeEventEvidence(event) {
|
|
|
89
89
|
const status = String(state.status ?? part.status ?? '').toLowerCase()
|
|
90
90
|
const failed = /error|failed|denied|rejected/.test(status) || state.error != null || part.error != null
|
|
91
91
|
const completed = !failed && (!status || /completed|success|succeeded|ok/.test(status))
|
|
92
|
+
const rawToolError = state.error ?? part.error
|
|
93
|
+
const toolError = rawToolError == null ? '' : (typeof rawToolError === 'string' ? rawToolError : JSON.stringify(rawToolError))
|
|
92
94
|
const input = state.input && typeof state.input === 'object' ? state.input : (part.input && typeof part.input === 'object' ? part.input : {})
|
|
93
95
|
const command = String(input.command ?? input.cmd ?? '')
|
|
94
96
|
|
|
@@ -107,6 +109,7 @@ export function opencodeEventEvidence(event) {
|
|
|
107
109
|
...(mcpTool ? { mcpTool } : {}),
|
|
108
110
|
failed,
|
|
109
111
|
completed,
|
|
112
|
+
...(failed && toolError ? { toolError: toolError.replace(/\s+/g, ' ').slice(0, 500) } : {}),
|
|
110
113
|
didCode: completed && (mutationTool || bashTool || codebaseMutation),
|
|
111
114
|
didRepoMutation: completed && (mutationTool || codebaseMutation || (bashTool && commandMutation)),
|
|
112
115
|
didMcpTaskRead: completed && /^(?:get_ticket|list_tasks|list_task_types)$/.test(mcpTool),
|
package/src/mcp-http.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const parsePayload = async (res) => {
|
|
2
|
+
const body = await res.text()
|
|
3
|
+
const data = body.split(/\r?\n/).filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trim()).pop()
|
|
4
|
+
try { return JSON.parse(data || body || '{}') }
|
|
5
|
+
catch { throw new Error('MCP returned a non-JSON response') }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Streamable HTTP permits both stateful servers (initialize returns
|
|
9
|
+
// Mcp-Session-Id) and stateless servers (no session header). The backend agent
|
|
10
|
+
// MCP is deployed in both forms, so absence of a session id is a transport mode,
|
|
11
|
+
// not an initialization failure.
|
|
12
|
+
export function createMcpHttpClient({ url, apiKey, identifier, clientVersion, fetchImpl = fetch, log = () => {} }) {
|
|
13
|
+
let initialized = false
|
|
14
|
+
let sessionId = ''
|
|
15
|
+
let rpcId = 0
|
|
16
|
+
let initializePromise = null
|
|
17
|
+
let toolsPromise = null
|
|
18
|
+
let toolsCache = null
|
|
19
|
+
|
|
20
|
+
const post = (message, withSession = true) => fetchImpl(url, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
'content-type': 'application/json',
|
|
24
|
+
accept: 'application/json, text/event-stream',
|
|
25
|
+
'x-agent-api-key': apiKey,
|
|
26
|
+
'x-agent-identifier': identifier,
|
|
27
|
+
...(withSession && sessionId ? { 'mcp-session-id': sessionId } : {}),
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify(message),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const reset = () => { initialized = false; sessionId = ''; toolsCache = null }
|
|
33
|
+
|
|
34
|
+
const initialize = () => {
|
|
35
|
+
if (initialized) return Promise.resolve()
|
|
36
|
+
if (initializePromise) return initializePromise
|
|
37
|
+
initializePromise = (async () => {
|
|
38
|
+
const res = await post({
|
|
39
|
+
jsonrpc: '2.0',
|
|
40
|
+
id: ++rpcId,
|
|
41
|
+
method: 'initialize',
|
|
42
|
+
params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: clientVersion } },
|
|
43
|
+
}, false)
|
|
44
|
+
if (!res.ok) throw new Error('MCP initialize HTTP ' + res.status)
|
|
45
|
+
const payload = await parsePayload(res)
|
|
46
|
+
if (payload.error) throw new Error(`MCP initialize: ${payload.error.message || 'protocol error'}`)
|
|
47
|
+
sessionId = res.headers.get('mcp-session-id') || ''
|
|
48
|
+
|
|
49
|
+
const ready = await post({ jsonrpc: '2.0', method: 'notifications/initialized' })
|
|
50
|
+
if (!ready.ok) { reset(); throw new Error('MCP initialized HTTP ' + ready.status) }
|
|
51
|
+
initialized = true
|
|
52
|
+
if (!sessionId) log('MCP initialized in stateless mode (no session id required)')
|
|
53
|
+
})().finally(() => { initializePromise = null })
|
|
54
|
+
return initializePromise
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const callTool = async (name, args = {}, retried = false) => {
|
|
58
|
+
await initialize()
|
|
59
|
+
const hadSession = !!sessionId
|
|
60
|
+
const res = await post({
|
|
61
|
+
jsonrpc: '2.0',
|
|
62
|
+
id: ++rpcId,
|
|
63
|
+
method: 'tools/call',
|
|
64
|
+
params: { name, arguments: { ...args, agent_api_key: apiKey, agent_identifier: identifier } },
|
|
65
|
+
})
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
// Only a stateful transport can have an expired session. A stateless 4xx
|
|
68
|
+
// belongs to the tool request itself and must not trigger an initialize loop.
|
|
69
|
+
if (hadSession && !retried && [400, 404, 409, 410].includes(res.status)) {
|
|
70
|
+
reset()
|
|
71
|
+
return callTool(name, args, true)
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`MCP ${name} HTTP ${res.status}`)
|
|
74
|
+
}
|
|
75
|
+
const payload = await parsePayload(res)
|
|
76
|
+
if (payload.error) throw new Error(`MCP ${name}: ${payload.error.message || 'tool error'}`)
|
|
77
|
+
const result = payload.result ?? payload
|
|
78
|
+
if (result?.isError) {
|
|
79
|
+
const detail = String(result.content?.find?.((item) => item?.type === 'text')?.text || 'tool error').split(apiKey).join('[redacted]')
|
|
80
|
+
throw new Error(`MCP ${name}: ${detail}`)
|
|
81
|
+
}
|
|
82
|
+
return result
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const requestTools = async (retried = false) => {
|
|
86
|
+
await initialize()
|
|
87
|
+
const hadSession = !!sessionId
|
|
88
|
+
const res = await post({ jsonrpc: '2.0', id: ++rpcId, method: 'tools/list', params: {} })
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
if (hadSession && !retried && [400, 404, 409, 410].includes(res.status)) {
|
|
91
|
+
reset()
|
|
92
|
+
return requestTools(true)
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`MCP tools/list HTTP ${res.status}`)
|
|
95
|
+
}
|
|
96
|
+
const payload = await parsePayload(res)
|
|
97
|
+
if (payload.error) throw new Error(`MCP tools/list: ${payload.error.message || 'protocol error'}`)
|
|
98
|
+
const tools = payload.result?.tools ?? payload.tools
|
|
99
|
+
if (!Array.isArray(tools)) throw new Error('MCP tools/list returned no tool array')
|
|
100
|
+
toolsCache = tools
|
|
101
|
+
return tools
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Capability discovery is shared and cached. BYO runtimes use it before
|
|
105
|
+
// optional actions so a backend deployment that lacks one tool cannot trap a
|
|
106
|
+
// completed ticket in a permanent retry loop.
|
|
107
|
+
const listTools = (refresh = false) => {
|
|
108
|
+
if (!refresh && toolsCache) return Promise.resolve(toolsCache)
|
|
109
|
+
if (toolsPromise) return toolsPromise
|
|
110
|
+
toolsPromise = requestTools().finally(() => { toolsPromise = null })
|
|
111
|
+
return toolsPromise
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const mode = () => !initialized ? 'uninitialized' : sessionId ? 'stateful' : 'stateless'
|
|
115
|
+
return { callTool, listTools, initialize, reset, mode }
|
|
116
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { OV_DIR, slugify } from './lib.mjs'
|
|
3
|
+
|
|
4
|
+
export function opencodeRuntimeLayout({ cfgKey, workdir, baseDir = OV_DIR }) {
|
|
5
|
+
const agentKey = slugify(String(cfgKey || 'agent')) || 'agent'
|
|
6
|
+
const configDir = join(baseDir, 'opencode-' + agentKey)
|
|
7
|
+
return {
|
|
8
|
+
configDir,
|
|
9
|
+
configPath: join(configDir, 'opencode.json'),
|
|
10
|
+
workspace: workdir || configDir,
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildOpencodeConfig({ mcpUrl, mcpHeaders }) {
|
|
15
|
+
if (!mcpUrl) return null
|
|
16
|
+
return {
|
|
17
|
+
$schema: 'https://opencode.ai/config.json',
|
|
18
|
+
mcp: {
|
|
19
|
+
'openvisio-team': {
|
|
20
|
+
type: 'remote',
|
|
21
|
+
url: mcpUrl,
|
|
22
|
+
enabled: true,
|
|
23
|
+
oauth: false,
|
|
24
|
+
timeout: 15_000,
|
|
25
|
+
...(mcpHeaders && Object.keys(mcpHeaders).length ? { headers: mcpHeaders } : {}),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/watch.mjs
CHANGED
|
@@ -13,6 +13,8 @@ import { connectAgentWs, assertWebSocket } from './ws.mjs'
|
|
|
13
13
|
import { agentStateRequest, buildTaskCompletionReport, codexPolicyBlock, mentionDedupeKeys, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, requestTargetsLaterAgent, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted } from './events.mjs'
|
|
14
14
|
import { createByoMemoryGraph } from './memory.mjs'
|
|
15
15
|
import { repositoryHasPrPushAuthorization } from './pr-push.mjs'
|
|
16
|
+
import { createMcpHttpClient } from './mcp-http.mjs'
|
|
17
|
+
import { buildOpencodeConfig, opencodeRuntimeLayout } from './opencode-config.mjs'
|
|
16
18
|
|
|
17
19
|
// Behaviour prompts. The openvisio-team MCP bridge requires the agent's
|
|
18
20
|
// credentials as ARGUMENTS on every tool call — those are injected at runtime by
|
|
@@ -40,7 +42,7 @@ const REPLY_DISCIPLINE = [
|
|
|
40
42
|
|
|
41
43
|
// ── CHAT-ONLY agents (no --workdir): chat/ticket tools, no code surface. ──────
|
|
42
44
|
const CHAT_CHARTER = [
|
|
43
|
-
'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 provides post_message, react_message, list_agents, list_projects, list_tasks, get_ticket, update_ticket,
|
|
45
|
+
'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 provides post_message, react_message, list_agents, list_projects, list_tasks, get_ticket, update_ticket, and list_activity. A ticket-comment tool is optional and must not be assumed. 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.',
|
|
44
46
|
'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.',
|
|
45
47
|
'',
|
|
46
48
|
REPLY_DISCIPLINE,
|
|
@@ -68,7 +70,7 @@ const COORDINATE = [
|
|
|
68
70
|
// A stable "who you are / how you work" charter prepended to every code cycle.
|
|
69
71
|
const CODE_CHARTER = [
|
|
70
72
|
'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:',
|
|
71
|
-
' • openvisio-team tools — use the names actually present. Backend MCP provides project/task discovery through list_agents, list_projects, list_tasks, get_ticket,
|
|
73
|
+
' • openvisio-team tools — use the names actually present. Backend MCP provides project/task discovery through list_agents, list_projects, list_tasks, get_ticket, and update_ticket, plus post_message/react_message/list_activity. Ticket comments are optional: use a comment tool only when it appears in the current tool list. Relay runtimes may additionally expose poll_inbox or get_marching_orders.',
|
|
72
74
|
' • Read / Grep / Glob / Edit / Write / MultiEdit — inspect AND change code.',
|
|
73
75
|
' • Bash — git (branch, commit, push a branch), gh (clone repos, open PRs), run tests/builds.',
|
|
74
76
|
'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.',
|
|
@@ -93,7 +95,7 @@ const CODE_FULL = [
|
|
|
93
95
|
' 3. CHANGE + VERIFY: Read/Edit/Write the files; run the tests or build if the repo has them.',
|
|
94
96
|
' 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.',
|
|
95
97
|
' 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.',
|
|
96
|
-
' 6. CLOSE THE LOOP:
|
|
98
|
+
' 6. CLOSE THE LOOP: move/update the ticket with update_ticket. Use a ticket-comment tool for a blocker or clarification only when that tool actually appears; otherwise keep the blocker in the ticket update and let the watcher deliver the visible channel result. Reply with the summary + PR link in a source thread explicitly supplied by the event. For backlog-only tickets, do not call post_message yourself; the watcher sends one verified project-channel completion message and deduplicates it across reconnects.',
|
|
97
99
|
'Bash is for git / gh / tests / clone ONLY — never to hunt for credentials (they are given to you above).',
|
|
98
100
|
].join('\n')
|
|
99
101
|
|
|
@@ -117,7 +119,7 @@ const INTRO = [
|
|
|
117
119
|
const SWEEP = [
|
|
118
120
|
'DAILY CATCH-UP — you may have missed items while offline. Prioritize TASKS.',
|
|
119
121
|
'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:',
|
|
120
|
-
' 1. For every task assigned to YOU that you have NOT started
|
|
122
|
+
' 1. For every task assigned to YOU that you have NOT started: begin the work without inventing an acknowledgement tool. If a ticket-comment tool is present you may acknowledge once; otherwise use update_ticket and report through an actual source channel only when one is supplied. Then do the work end-to-end. Skip tasks assigned to other agents.',
|
|
121
123
|
' 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.',
|
|
122
124
|
'If there is genuinely nothing outstanding, STOP silently — do NOT post a "nothing to do" message.',
|
|
123
125
|
].join('\n')
|
|
@@ -288,25 +290,25 @@ export async function runWatch({ flags }) {
|
|
|
288
290
|
// ── opencode cycle runner ─────────────────────────────────────────────────────
|
|
289
291
|
// opencode (opencode.ai) has no persistent stream-json protocol like Claude Code,
|
|
290
292
|
// so each cycle is a headless `opencode run <prompt> --auto [--model provider/model]`.
|
|
291
|
-
// The openvisio-team MCP is declared in
|
|
292
|
-
//
|
|
293
|
+
// The openvisio-team MCP is declared in a private per-agent config. The actual
|
|
294
|
+
// code workspace is passed through --dir, so two agents never overwrite each
|
|
295
|
+
// other's MCP identity even when they work in the same repository.
|
|
296
|
+
// `--auto` approves tool use non-interactively.
|
|
293
297
|
// Same { runCycle, canCode } contract as the Claude runner.
|
|
294
298
|
function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, maxCycleMs, log, debug, model, onTool, systemPrompt }) {
|
|
295
|
-
|
|
296
|
-
// per-agent dir for chat-only agents.
|
|
297
|
-
const cwd = workdir || join(OV_DIR, 'opencode-' + (cfgKey || 'agent'))
|
|
299
|
+
const { configDir, configPath: opencodeConfigPath, workspace } = opencodeRuntimeLayout({ cfgKey, workdir })
|
|
298
300
|
const bin = onPath('opencode') || 'opencode'
|
|
301
|
+
const redactKey = String(mcpHeaders?.['x-agent-api-key'] || '')
|
|
302
|
+
const opencodeConfig = buildOpencodeConfig({ mcpUrl, mcpHeaders })
|
|
299
303
|
let configured = false
|
|
300
304
|
const ensureConfig = () => {
|
|
301
305
|
if (configured) return
|
|
302
306
|
configured = true
|
|
303
307
|
try {
|
|
304
|
-
mkdirSync(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
mcp: { 'openvisio-team': { type: 'remote', url: mcpUrl, enabled: true, ...(mcpHeaders && Object.keys(mcpHeaders).length ? { headers: mcpHeaders } : {}) } },
|
|
309
|
-
}, true)
|
|
308
|
+
mkdirSync(configDir, { recursive: true })
|
|
309
|
+
mkdirSync(workspace, { recursive: true })
|
|
310
|
+
if (opencodeConfig) {
|
|
311
|
+
writeJson(opencodeConfigPath, opencodeConfig, true)
|
|
310
312
|
} else {
|
|
311
313
|
log('WARNING: no --mcp-url — opencode has no openvisio-team tools to act with. Re-connect with --mcp-url.')
|
|
312
314
|
}
|
|
@@ -322,7 +324,7 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
322
324
|
const full = systemPrompt ? systemPrompt + '\n\n' + prompt : prompt
|
|
323
325
|
// JSON mode is the evidence boundary. Formatted stdout only tells us that
|
|
324
326
|
// OpenCode exited; raw events tell us which tools actually completed.
|
|
325
|
-
const args = ['run', full, '--auto', '--format', 'json', ...(m ? ['--model', m] : [])]
|
|
327
|
+
const args = ['run', full, '--auto', '--format', 'json', '--dir', workspace, ...(m ? ['--model', m] : [])]
|
|
326
328
|
let child = null, done = false, didCode = false, didRepoMutation = false, didMessage = false, didChannelMessage = false, didMcpTaskRead = false, didMcpTaskUpdate = false
|
|
327
329
|
let outputText = '', jsonlBuffer = ''
|
|
328
330
|
const mcpCalls = new Set(), mcpErrors = new Set(), runtimeErrors = new Set()
|
|
@@ -350,6 +352,10 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
350
352
|
if (evidence.runtimeError) runtimeErrors.add(evidence.runtimeError)
|
|
351
353
|
if (!evidence.tool) return
|
|
352
354
|
if (debug) log(' → tool ' + evidence.tool + (evidence.failed ? ' (failed)' : evidence.completed ? ' (completed)' : ''))
|
|
355
|
+
if (evidence.failed && evidence.toolError) {
|
|
356
|
+
const safeError = (redactKey ? String(evidence.toolError).split(redactKey).join('[redacted]') : String(evidence.toolError)).replace(/\s+/g, ' ').slice(0, 240)
|
|
357
|
+
log(' ✗ opencode tool ' + evidence.tool + ': ' + safeError)
|
|
358
|
+
}
|
|
353
359
|
try { onTool && onTool(evidence.tool) } catch { /* activity is best-effort */ }
|
|
354
360
|
if (evidence.mcpTool) {
|
|
355
361
|
mcpCalls.add(evidence.mcpTool)
|
|
@@ -370,7 +376,18 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
370
376
|
}, maxCycleMs)
|
|
371
377
|
log('running opencode cycle…' + (m ? ' [' + m + ']' : ''))
|
|
372
378
|
try {
|
|
373
|
-
child = spawn(bin, args, {
|
|
379
|
+
child = spawn(bin, args, {
|
|
380
|
+
cwd: configDir,
|
|
381
|
+
env: {
|
|
382
|
+
...process.env,
|
|
383
|
+
OPENCODE_CONFIG: opencodeConfigPath,
|
|
384
|
+
// Inline config has higher precedence than a project opencode.json.
|
|
385
|
+
// This prevents a stale generated workspace file from replacing this
|
|
386
|
+
// agent's URL or credentials while keeping normal project settings.
|
|
387
|
+
...(opencodeConfig ? { OPENCODE_CONFIG_CONTENT: JSON.stringify(opencodeConfig) } : {}),
|
|
388
|
+
},
|
|
389
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
390
|
+
})
|
|
374
391
|
child.stdout?.on('data', (data) => {
|
|
375
392
|
jsonlBuffer += String(data)
|
|
376
393
|
const lines = jsonlBuffer.split('\n')
|
|
@@ -389,7 +406,7 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
389
406
|
})
|
|
390
407
|
}
|
|
391
408
|
|
|
392
|
-
log('opencode runner ready' + (model ? ' [model ' + model + ']' : '') + (canCode ? ' [CODE workspace ' +
|
|
409
|
+
log('opencode runner ready' + (model ? ' [model ' + model + ']' : '') + (canCode ? ' [CODE workspace ' + workspace + '; isolated cfg ' + configDir + ']' : ' [CHAT-ONLY; isolated cfg ' + configDir + ']'))
|
|
393
410
|
return { runCycle, canCode }
|
|
394
411
|
}
|
|
395
412
|
|
|
@@ -670,7 +687,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
670
687
|
const canCode = !!workdir
|
|
671
688
|
// The Mastra bridge authenticates per-CALL: every openvisio-team tool needs
|
|
672
689
|
// agent_identifier + agent_api_key as arguments. Hand them over up front.
|
|
673
|
-
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 and update work with list_agents, list_projects, list_tasks, get_ticket, update_ticket,
|
|
690
|
+
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 and update work with list_agents, list_projects, list_tasks, list_task_types, get_ticket, update_ticket, list_channels, list_message_thread, and list_activity as applicable. Ticket comments are optional: never invent or call comment_ticket unless that exact tool appears. get_marching_orders, poll_inbox, get_resource, list_mcp_resources, and list_mcp_resource_templates are NOT team-action tools here; never call them. 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.`
|
|
674
691
|
// The STATIC charter + creds are the session system prompt (cached, billed once),
|
|
675
692
|
// NOT re-sent in every cycle's user message — the big token saving.
|
|
676
693
|
const systemPrompt = (canCode ? CODE_CHARTER : CHAT_CHARTER) + '\n\n' + credNote
|
|
@@ -687,8 +704,10 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
687
704
|
const codexPushGuide = agent === 'codex' && canCode
|
|
688
705
|
? '\n\nCODEX PR DELIVERY: first inspect the OpenVisio MCP tools. When list_codebases, create_codebase_branch, create_codebase_commit (or write_codebase_file), and create_pull_request are available, use that authenticated linked-codebase flow to create the agent/* branch, publish the verified changed files, and open the PR. This is the preferred path and requires no local git push. If those tools are unavailable for the repository, do not run git push directly. From the repository run `openvisio-agent push-pr-branch`. It is a user-authorized constrained fallback that can only push HEAD to the matching agent/* branch on the exact authorized origin. If it reports OPENVISIO_PR_PUSH_AUTH_REQUIRED, do not retry or route around it. Report the one-time command `openvisio-agent authorize-pr-push` as the blocker.'
|
|
689
706
|
: ''
|
|
690
|
-
const
|
|
691
|
-
const
|
|
707
|
+
const backendToolRule = 'BACKEND MCP RULE: the event and watcher already provide the work source. Never call get_marching_orders, poll_inbox, get_resource, list_mcp_resources, list_mcp_resource_templates, or other relay/resource-discovery tools; they are not backend team actions. Use only names present in the current openvisio-team tool list. For discovery use list_agents, list_projects, list_tasks, list_task_types, list_activity, get_ticket, and list_channels as applicable. Never call comment_ticket unless that exact optional tool is present.'
|
|
708
|
+
const fullPrompt = (canCode ? CODE_FULL + codexPushGuide : 'Handle the supplied verified backend ticket with the available OpenVisio tools. Update or comment on the ticket as requested, do not claim repository work in chat-only mode, and stop after the verified action.') + '\n\n' + backendToolRule
|
|
709
|
+
const fastPrompt = (canCode ? CODE_FAST : CYCLE_FAST) + '\n\n' + backendToolRule
|
|
710
|
+
const coordinatePrompt = COORDINATE + '\n\n' + backendToolRule
|
|
692
711
|
// Live model state — changeable at runtime by the in-chat `/model` command.
|
|
693
712
|
// codeModel drives full/sweep cycles; chatModel (if set) the lighter fast/intro
|
|
694
713
|
// ones, so routine chatter can run cheaper than real code work.
|
|
@@ -768,54 +787,46 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
768
787
|
let lastTaskTriggeredAt = 0
|
|
769
788
|
let lastInboxSignature = ''
|
|
770
789
|
let selfAgentId = null
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
...(withSession && mcpSessionId ? { 'mcp-session-id': mcpSessionId } : {}),
|
|
788
|
-
},
|
|
789
|
-
body: JSON.stringify(message),
|
|
790
|
-
})
|
|
791
|
-
return res
|
|
792
|
-
}
|
|
793
|
-
const ensureMcpSession = async () => {
|
|
794
|
-
if (mcpSessionId) return
|
|
795
|
-
const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.18.1' } } }, false)
|
|
796
|
-
if (!res.ok) throw new Error('MCP initialize HTTP ' + res.status)
|
|
797
|
-
await mcpPayload(res)
|
|
798
|
-
mcpSessionId = res.headers.get('mcp-session-id') || ''
|
|
799
|
-
if (!mcpSessionId) throw new Error('MCP initialize returned no session id')
|
|
800
|
-
const ready = await mcpPost({ jsonrpc: '2.0', method: 'notifications/initialized' })
|
|
801
|
-
if (!ready.ok) throw new Error('MCP initialized HTTP ' + ready.status)
|
|
790
|
+
const mcpClient = createMcpHttpClient({ url: mcpUrl, apiKey, identifier, clientVersion: '0.18.3', log })
|
|
791
|
+
const callMcpTool = (name, args = {}) => mcpClient.callTool(name, args)
|
|
792
|
+
let mcpToolNames = null
|
|
793
|
+
let mcpToolDiscoveryPromise = null
|
|
794
|
+
let mcpToolDiscoveryWarned = false
|
|
795
|
+
const discoverMcpTools = (refresh = false) => {
|
|
796
|
+
if (!refresh && mcpToolNames) return Promise.resolve(mcpToolNames)
|
|
797
|
+
if (mcpToolDiscoveryPromise) return mcpToolDiscoveryPromise
|
|
798
|
+
mcpToolDiscoveryPromise = mcpClient.listTools(refresh).then((tools) => {
|
|
799
|
+
mcpToolNames = new Set(tools.map((tool) => String(tool?.name || '')).filter(Boolean))
|
|
800
|
+
const required = ['list_agents', 'list_projects', 'list_tasks', 'get_ticket', 'update_ticket', 'post_message']
|
|
801
|
+
const missing = required.filter((name) => !mcpToolNames.has(name))
|
|
802
|
+
log(`MCP tools ready (${mcpToolNames.size})${missing.length ? '; missing core tools: ' + missing.join(', ') : ''}`)
|
|
803
|
+
return mcpToolNames
|
|
804
|
+
}).finally(() => { mcpToolDiscoveryPromise = null })
|
|
805
|
+
return mcpToolDiscoveryPromise
|
|
802
806
|
}
|
|
803
|
-
const
|
|
804
|
-
await
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
807
|
+
const mcpSupports = async (name) => {
|
|
808
|
+
try { return (await discoverMcpTools()).has(name) }
|
|
809
|
+
catch (e) {
|
|
810
|
+
if (!mcpToolDiscoveryWarned) {
|
|
811
|
+
mcpToolDiscoveryWarned = true
|
|
812
|
+
log('MCP tool discovery failed; optional actions will use compatibility fallbacks: ' + (e?.message || e))
|
|
813
|
+
}
|
|
814
|
+
return null
|
|
809
815
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
816
|
+
}
|
|
817
|
+
const callOptionalMcpTool = async (name, args) => {
|
|
818
|
+
const supported = await mcpSupports(name)
|
|
819
|
+
if (supported === false) return { called: false, reason: 'not-advertised' }
|
|
820
|
+
try { return { called: true, result: await callMcpTool(name, args) } }
|
|
821
|
+
catch (e) {
|
|
822
|
+
if (/\b(?:unknown|missing|unsupported) tool\b|\btool\b.*\bnot found\b/i.test(String(e?.message || e))) {
|
|
823
|
+
try { await discoverMcpTools(true) } catch { /* the original error is enough */ }
|
|
824
|
+
return { called: false, reason: 'not-available' }
|
|
825
|
+
}
|
|
826
|
+
throw e
|
|
816
827
|
}
|
|
817
|
-
return result
|
|
818
828
|
}
|
|
829
|
+
if (mcpUrl) void discoverMcpTools().catch(() => {})
|
|
819
830
|
const toolData = (result) => {
|
|
820
831
|
const text = result?.content?.find?.((c) => c?.type === 'text')?.text
|
|
821
832
|
if (typeof text !== 'string') return result?.structuredContent ?? result ?? {}
|
|
@@ -919,13 +930,18 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
919
930
|
refs: { projectId, ticketId },
|
|
920
931
|
meta: { reportKey: report.key, prUrl: report.prUrl },
|
|
921
932
|
})
|
|
922
|
-
// Ticket comments are
|
|
923
|
-
//
|
|
924
|
-
//
|
|
933
|
+
// Ticket comments are optional across backend deployments. Prefer the tool
|
|
934
|
+
// when advertised, but never let its absence block the verified channel
|
|
935
|
+
// handoff or trap the ticket in reconnect retries.
|
|
925
936
|
if (!reportedTaskComments.has(report.key)) {
|
|
926
|
-
|
|
937
|
+
try {
|
|
938
|
+
const comment = await callOptionalMcpTool('comment_ticket', { project_id: projectId, ticket_id: ticketId, text: report.content })
|
|
939
|
+
if (comment.called) log('posted verified ticket comment for #' + ticketId)
|
|
940
|
+
else log('comment_ticket is not exposed; completing ticket #' + ticketId + ' through its verified state and project-channel report')
|
|
941
|
+
} catch (e) {
|
|
942
|
+
log('optional ticket comment failed for #' + ticketId + ': ' + (e?.message || e) + '; continuing with the verified project-channel report')
|
|
943
|
+
}
|
|
927
944
|
reportedTaskComments.add(report.key); trimSeen(reportedTaskComments); persistReplay()
|
|
928
|
-
log('posted verified ticket comment for #' + ticketId)
|
|
929
945
|
}
|
|
930
946
|
if (reportedCompletions.has(report.key)) {
|
|
931
947
|
pendingCompletionReports.delete(taskKey)
|
|
@@ -986,12 +1002,10 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
986
1002
|
refs: { projectId, ticketId, ...(Number.isFinite(channelId) ? { channelId } : {}) },
|
|
987
1003
|
})
|
|
988
1004
|
try {
|
|
989
|
-
await
|
|
990
|
-
delivered = true
|
|
991
|
-
|
|
992
|
-
} catch (e) {
|
|
993
|
-
log('comment_ticket failed for blocker; falling back to the ticket description for #' + ticketId)
|
|
994
|
-
}
|
|
1005
|
+
const comment = await callOptionalMcpTool('comment_ticket', { project_id: projectId, ticket_id: ticketId, text: ticketNotice })
|
|
1006
|
+
if (comment.called) { delivered = true; return }
|
|
1007
|
+
log('comment_ticket is not exposed; recording the blocker in the ticket description for #' + ticketId)
|
|
1008
|
+
} catch (e) { log('optional ticket comment failed; recording the blocker in the ticket description for #' + ticketId) }
|
|
995
1009
|
const current = toolData(await callMcpTool('get_ticket', { project_id: projectId, ticket_id: ticketId }))
|
|
996
1010
|
const ticket = current.ticket ?? current.task ?? current
|
|
997
1011
|
const description = String(ticket.description || '')
|
|
@@ -1144,14 +1158,14 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1144
1158
|
}
|
|
1145
1159
|
} else if (!mentionActivity.length) lastInboxSignature = ''
|
|
1146
1160
|
} catch (e) {
|
|
1147
|
-
|
|
1161
|
+
mcpClient.reset()
|
|
1148
1162
|
log('backlog reconciliation failed: ' + (e && e.message ? e.message : e))
|
|
1149
1163
|
} finally { backlogProbeBusy = false }
|
|
1150
1164
|
}
|
|
1151
1165
|
|
|
1152
1166
|
// Higher rank wins when coalescing cycles requested while one is running.
|
|
1153
1167
|
const RANK = { fast: 0, intro: 1, coord: 2, sweep: 2, full: 3 }
|
|
1154
|
-
const baseFor = (kind) => kind === 'intro' ? INTRO : kind === 'full' ? fullPrompt : (kind === 'coord' || kind === 'sweep') ?
|
|
1168
|
+
const baseFor = (kind) => kind === 'intro' ? INTRO : kind === 'full' ? fullPrompt : (kind === 'coord' || kind === 'sweep') ? coordinatePrompt : fastPrompt
|
|
1155
1169
|
// Codex and OpenCode expose structured action streams. Require runtime facts
|
|
1156
1170
|
// from those streams before accepting a full coding cycle. Claude's evidence
|
|
1157
1171
|
// shape is different and remains on its existing completion path.
|
|
@@ -1473,8 +1487,8 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1473
1487
|
const replyDelivery = guardedDelivery(codingMention ? 'result' : 'reply', !codingMention)
|
|
1474
1488
|
const ctx = cid != null
|
|
1475
1489
|
? replyDelivery
|
|
1476
|
-
? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. ${codingMention ? 'Complete the repository work and verification first.' : 'Answer the request.'} Do NOT call post_message; it is intentionally unavailable. Return only the final 1-3 sentence reply as your final answer. The watcher will read the real thread, check its persistent memory graph, and render that answer at most once.${who ? ` To mention the requester, use their exact full name "@${who}".` : ''}
|
|
1477
|
-
: `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. ${codingMention ? 'This is repository work: complete the coding flow first, then send' : '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 version. 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.` : ''}
|
|
1490
|
+
? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. ${codingMention ? 'Complete the repository work and verification first.' : 'Answer the request.'} Do NOT call post_message; it is intentionally unavailable. Return only the final 1-3 sentence reply as your final answer. The watcher will read the real thread, check its persistent memory graph, and render that answer at most once.${who ? ` To mention the requester, use their exact full name "@${who}".` : ''} The complete message is already here; do not call get_resource, get_marching_orders, poll_inbox, list_mcp_resources, or list_mcp_resource_templates.`
|
|
1491
|
+
: `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. ${codingMention ? 'This is repository work: complete the coding flow first, then send' : '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 version. 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.` : ''} The complete message is already here. Do not call get_resource, get_marching_orders, poll_inbox, list_mcp_resources, or list_mcp_resource_templates, and after your single reply, STOP.`
|
|
1478
1492
|
: undefined
|
|
1479
1493
|
if (codingMention) {
|
|
1480
1494
|
if (agent === 'codex' && cid != null) {
|
|
@@ -1509,12 +1523,8 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1509
1523
|
// Workspace ethics: a one-time hello the FIRST time this agent ever connects.
|
|
1510
1524
|
const introMarker = join(OV_DIR, 'intro-' + slugify(identifier) + '.done')
|
|
1511
1525
|
if (!existsSync(introMarker)) {
|
|
1512
|
-
if (agent !== 'codex') {
|
|
1513
|
-
try { mkdirSync(OV_DIR, { recursive: true }); writeFileSync(introMarker, new Date().toISOString() + '\n') } catch { /* best-effort */ }
|
|
1514
|
-
}
|
|
1515
1526
|
log('first connection — introducing self to the workspace')
|
|
1516
1527
|
introTimer = setTimeout(() => {
|
|
1517
|
-
if (agent !== 'codex') { void drain('intro'); return }
|
|
1518
1528
|
void announceIntroduction().then((delivered) => {
|
|
1519
1529
|
if (!delivered) return
|
|
1520
1530
|
try { mkdirSync(OV_DIR, { recursive: true }); writeFileSync(introMarker, new Date().toISOString() + '\n') } catch { /* best-effort */ }
|