openvisio-agent 0.18.6 → 0.18.7
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 +2 -2
- package/package.json +1 -1
- package/scripts/certify.mjs +4 -2
- package/src/events.mjs +141 -3
- package/src/watch.mjs +104 -72
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ npx -y openvisio-agent@latest connect --backend https://api.your-org.example/dev
|
|
|
50
50
|
- `--ws <wss-url>` — the org's API-Gateway WebSocket base (the same value the frontend uses as `NEXT_PUBLIC_BACKEND_WS_URL`).
|
|
51
51
|
- `--mcp-url <url>` — registers the `openvisio-team` MCP (authed with the agent header pair) so the agent has tools to **act** on the events.
|
|
52
52
|
|
|
53
|
-
Then `watch --name ada` auto-detects the backend agent and runs a WebSocket loop instead of polling: it connects with `?api_key=&identifier=`, keeps the connection warm with keepalives, reconnects with backoff, and
|
|
53
|
+
Then `watch --name ada` auto-detects the backend agent and runs a WebSocket loop instead of polling: it connects with `?api_key=&identifier=`, keeps the connection warm with keepalives, reconnects with backoff, and starts one work cycle per `task:assigned` / `agent:mention` (a burst of mentions coalesces into a follow-up). A work cycle can contain many assistant, tool, and message actions; emitting a progress message does not end it. Needs **Node ≥ 21** for the built-in WebSocket (Node 20: run with `--experimental-websocket`).
|
|
54
54
|
|
|
55
55
|
### `watch --name <agent>`
|
|
56
56
|
|
|
@@ -62,7 +62,7 @@ The backend MCP may be stateful or stateless. A successful initialize response w
|
|
|
62
62
|
|
|
63
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.
|
|
64
64
|
|
|
65
|
-
An `agent:mention` event only wakes the watcher; it does not grant ownership of the conversation. The actual source message is checked before any model starts. Messages redirected to another agent and unaddressed agent chatter stay silent, while a direct stand-down cancels queued/running work for that thread. Accepted coding work uses the activity indicator instead of a generic pickup message
|
|
65
|
+
An `agent:mention` event only wakes the watcher; it does not grant ownership of the conversation. The actual source message is checked before any model starts. Messages redirected to another agent and unaddressed agent chatter stay silent, while a direct stand-down cancels queued/running work for that thread. Accepted coding work uses the activity indicator instead of a generic pickup message. Claude and OpenCode may add one concrete progress update after work begins, but must continue and post a distinct verified result or blocker afterward; Codex keeps cancellation-safe delivery watcher-owned and renders the verified final answer once.
|
|
66
66
|
|
|
67
67
|
Ticket references follow the board UI: BYO agents use the project-scoped slug, such as `OVS-57`, in messages, comments, PR descriptions, blockers, and results. Numeric `project_id` and `ticket_id` values remain internal MCP arguments and are never used as human-facing ticket names. If an older backend omits the slug, the agent uses the ticket title rather than inventing one.
|
|
68
68
|
|
package/package.json
CHANGED
package/scripts/certify.mjs
CHANGED
|
@@ -47,7 +47,7 @@ const assertions = [
|
|
|
47
47
|
['completion requires review/done state and PR evidence', watcher.includes('buildTaskCompletionReport') && watcher.includes('completion report deferred')],
|
|
48
48
|
['completion delivery survives reconnect and deduplicates', watcher.includes('pendingCompletionReports: [...pendingCompletionReports]') && watcher.includes('reportedCompletions: [...reportedCompletions]') && watcher.includes('reportedCompletions.has(report.key)')],
|
|
49
49
|
['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)')],
|
|
50
|
-
['ticket comments cannot masquerade as channel completion',
|
|
50
|
+
['ticket comments cannot masquerade as channel completion', events.includes("didChannelMessage: completed && mcpTool === 'post_message'") && watcher.includes('result.didChannelMessage')],
|
|
51
51
|
['single-watcher acquisition is atomic and fails closed', watcher.includes("openSync(lockPath, 'wx')") && watcher.includes('Could not acquire the single-watcher lock')],
|
|
52
52
|
['websocket and activity mention delivery share a replay guard', watcher.includes('markMentionHandled(activityMessage, activityChannelId)') && watcher.includes('markMentionHandled(msg, cid)') && watcher.includes('recentMentionSignatures')],
|
|
53
53
|
['reconciled mentions reuse the guarded websocket delivery path', watcher.includes("onEvent('agent:mention'") && watcher.includes('_mentionAlreadyMarked: true')],
|
|
@@ -87,7 +87,9 @@ const assertions = [
|
|
|
87
87
|
['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)')],
|
|
88
88
|
['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')],
|
|
89
89
|
['OpenCode tool failures retain sanitized diagnostics', events.includes('toolError: toolError.replace') && watcher.includes("opencode tool '") && watcher.includes("split(redactKey).join('[redacted]')")],
|
|
90
|
-
['
|
|
90
|
+
['all runtime acknowledgements cannot satisfy coding completion', watcher.includes('missingRuntimeWorkEvidence(result') && watcher.includes('claudeEventEvidence(o, turnToolUses)') && watcher.includes('releaseTaskForRetry(activeTaskRef, prompt)')],
|
|
91
|
+
['Codex prose cannot masquerade as repository evidence', watcher.includes('codexEventEvidence(event)') && events.includes("item.type === 'agent_message'") && events.includes("event.type === 'item.completed'")],
|
|
92
|
+
['coding recovery retains source context and requires a final result', watcher.includes('ORIGINAL REQUEST AND ROUTING CONTEXT') && events.includes('resultMessageRequired && !result?.didResultMessage') && events.includes("outputText: second?.outputText || first?.outputText || ''")],
|
|
91
93
|
['backend MCP accepts stateless initialize responses', mcpHttp.includes("mode = () => !initialized ? 'uninitialized' : sessionId ? 'stateful' : 'stateless'") && !watcher.includes('MCP initialize returned no session id')],
|
|
92
94
|
['MCP initialize is shared across concurrent startup probes', mcpHttp.includes('if (initializePromise) return initializePromise')],
|
|
93
95
|
['stateless tool errors do not cause initialize loops', mcpHttp.includes('if (hadSession && !retried')],
|
package/src/events.mjs
CHANGED
|
@@ -56,7 +56,21 @@ export function taskIsAwaitingReview(task, reviewTypeIds = new Set()) {
|
|
|
56
56
|
return /\b(?:review|test|testing|qa|quality\s+assurance|verification|approval)\b/i.test(state)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
function personDisplayName(person) {
|
|
60
|
+
if (!person || typeof person !== 'object') return ''
|
|
61
|
+
return (`${person.first_name || person.firstName || ''} ${person.last_name || person.lastName || ''}`.trim() || person.name || '').trim()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The human responsible for a BYO agent is the person who added it, not the
|
|
65
|
+
* creator of whichever ticket it happens to complete. Backend deployments have
|
|
66
|
+
* exposed that relationship under both association and transport-style names. */
|
|
67
|
+
export function agentAddedByName(agent) {
|
|
68
|
+
if (!agent || typeof agent !== 'object') return ''
|
|
69
|
+
const addedBy = agent.agentCreator || agent.agent_creator || agent.createdByUser || agent.created_by_user || agent.owner || agent.user
|
|
70
|
+
return personDisplayName(addedBy)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildTaskCompletionReport(task, { projectId, fallbackText = '', recipientName = '' } = {}) {
|
|
60
74
|
if (!task || typeof task !== 'object' || task.id == null) return null
|
|
61
75
|
if (!taskIsCompleted(task) && !taskIsAwaitingReview(task)) return null
|
|
62
76
|
|
|
@@ -64,8 +78,7 @@ export function buildTaskCompletionReport(task, { projectId, fallbackText = '' }
|
|
|
64
78
|
const prUrl = /https:\/\/github\.com\/[^\s)\]}>]+\/pull\/\d+/i.exec(evidence)?.[0]?.replace(/[.,;:]+$/, '') || ''
|
|
65
79
|
if (!prUrl) return null
|
|
66
80
|
|
|
67
|
-
const
|
|
68
|
-
const requester = (`${creator.first_name || creator.firstName || ''} ${creator.last_name || creator.lastName || ''}`.trim() || creator.name || '').trim()
|
|
81
|
+
const requester = String(recipientName || '').trim()
|
|
69
82
|
const title = String(task.title || 'Untitled task').replace(/\s+/g, ' ').trim().slice(0, 180)
|
|
70
83
|
const status = String(task.type?.name || task.task_type?.name || task.status || task.state || 'review').replace(/\s+/g, ' ').trim()
|
|
71
84
|
const verification = /\bVerification:\s*([^\n]{1,180})/i.exec(evidence)?.[1]?.replace(/\s+/g, ' ').trim().replace(/[.]+$/, '') || ''
|
|
@@ -131,6 +144,131 @@ export function opencodeEventEvidence(event) {
|
|
|
131
144
|
}
|
|
132
145
|
}
|
|
133
146
|
|
|
147
|
+
// Codex emits started/completed JSONL records for the same item. Trust only a
|
|
148
|
+
// completed, non-failed record for mutation and MCP evidence. In particular,
|
|
149
|
+
// words such as "apply_patch" inside an agent_message are just prose.
|
|
150
|
+
export function codexEventEvidence(event) {
|
|
151
|
+
if (!event || typeof event !== 'object') return {}
|
|
152
|
+
const item = event.item ?? event
|
|
153
|
+
if (!item || typeof item !== 'object') return {}
|
|
154
|
+
if (item.type === 'agent_message') return { outputText: typeof item.text === 'string' ? item.text : '' }
|
|
155
|
+
|
|
156
|
+
const status = String(item.status ?? '').toLowerCase()
|
|
157
|
+
const completed = event.type === 'item.completed' || /^(?:completed|success|succeeded|ok)$/.test(status)
|
|
158
|
+
const failed = event.type === 'item.failed' || /(?:fail|error|denied|rejected)/.test(status) || item.error != null || Number(item.exit_code) > 0
|
|
159
|
+
const itemType = String(item.type ?? '')
|
|
160
|
+
if (/^(?:command_execution|file_change|apply_patch|shell_command|exec_command)$/.test(itemType)) {
|
|
161
|
+
const command = String(item.command ?? item.input?.command ?? item.input?.cmd ?? '')
|
|
162
|
+
return {
|
|
163
|
+
failed,
|
|
164
|
+
completed: completed && !failed,
|
|
165
|
+
didCode: completed && !failed,
|
|
166
|
+
didRepoMutation: completed && !failed && (/^(?:file_change|apply_patch)$/.test(itemType) || /\b(?:git\s+(?:commit|push)|gh\s+pr\s+create|openvisio-agent\s+push-pr-branch)\b/i.test(command)),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (itemType !== 'mcp_tool_call') return {}
|
|
170
|
+
|
|
171
|
+
const rawTool = String(item.tool ?? item.name ?? item.method ?? 'unknown')
|
|
172
|
+
const prefixed = /^(?:mcp__)?openvisio(?:-team|_team)(?:__|[_.:/-])(.+)$/i.exec(rawTool)
|
|
173
|
+
const mcpTool = String(prefixed?.[1] ?? rawTool).replace(/[-.]/g, '_')
|
|
174
|
+
const succeeded = completed && !failed
|
|
175
|
+
return {
|
|
176
|
+
mcpTool,
|
|
177
|
+
failed,
|
|
178
|
+
completed: succeeded,
|
|
179
|
+
didCode: succeeded && /^(?:create_codebase_branch|create_codebase_commit|write_codebase_file|create_pull_request)$/.test(mcpTool),
|
|
180
|
+
didRepoMutation: succeeded && /^(?:create_codebase_branch|create_codebase_commit|write_codebase_file|create_pull_request)$/.test(mcpTool),
|
|
181
|
+
didMcpTaskRead: succeeded && /^(?:get_ticket|list_tasks|list_task_types)$/.test(mcpTool),
|
|
182
|
+
didMcpTaskUpdate: succeeded && mcpTool === 'update_ticket',
|
|
183
|
+
didMessage: succeeded && /^(?:post_message|comment_ticket)$/.test(mcpTool),
|
|
184
|
+
didChannelMessage: succeeded && mcpTool === 'post_message',
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Claude's stream-json protocol separates tool_use blocks from their later
|
|
189
|
+
// tool_result blocks. Keep the pending uses supplied by the caller and reduce
|
|
190
|
+
// only completed results to work evidence; announcing a tool call is not proof
|
|
191
|
+
// that the action succeeded.
|
|
192
|
+
export function claudeEventEvidence(event, pendingToolUses = new Map()) {
|
|
193
|
+
if (!event || typeof event !== 'object') return {}
|
|
194
|
+
const content = Array.isArray(event.message?.content) ? event.message.content : []
|
|
195
|
+
const output = []
|
|
196
|
+
const toolCalls = []
|
|
197
|
+
const toolResults = []
|
|
198
|
+
|
|
199
|
+
if (event.type === 'assistant') {
|
|
200
|
+
for (const block of content) {
|
|
201
|
+
if (block?.type === 'text' && typeof block.text === 'string' && block.text.trim()) output.push(block.text.trim())
|
|
202
|
+
if (block?.type !== 'tool_use' || !block.name) continue
|
|
203
|
+
const id = String(block.id ?? block.tool_use_id ?? '')
|
|
204
|
+
const use = { name: String(block.name), input: block.input && typeof block.input === 'object' ? block.input : {} }
|
|
205
|
+
if (id) pendingToolUses.set(id, use)
|
|
206
|
+
toolCalls.push(use.name)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (event.type === 'user') {
|
|
211
|
+
for (const block of content) {
|
|
212
|
+
if (block?.type !== 'tool_result') continue
|
|
213
|
+
const id = String(block.tool_use_id ?? block.id ?? '')
|
|
214
|
+
const use = id ? pendingToolUses.get(id) : null
|
|
215
|
+
if (!use) continue
|
|
216
|
+
pendingToolUses.delete(id)
|
|
217
|
+
const failed = block.is_error === true
|
|
218
|
+
toolResults.push(opencodeEventEvidence({
|
|
219
|
+
type: 'tool_use',
|
|
220
|
+
part: {
|
|
221
|
+
type: 'tool',
|
|
222
|
+
tool: use.name,
|
|
223
|
+
state: {
|
|
224
|
+
status: failed ? 'error' : 'completed',
|
|
225
|
+
input: use.input,
|
|
226
|
+
...(failed ? { error: block.content ?? 'Claude tool call failed' } : {}),
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
}))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
...(output.length ? { outputText: output.join(' ') } : {}),
|
|
235
|
+
...(toolCalls.length ? { toolCalls } : {}),
|
|
236
|
+
...(toolResults.length ? { toolResults } : {}),
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function missingRuntimeWorkEvidence(result, { ticketCycle = false, resultMessageRequired = false } = {}) {
|
|
241
|
+
const missing = [
|
|
242
|
+
ticketCycle && !result?.didMcpTaskRead && 'read the ticket through get_ticket/list_tasks',
|
|
243
|
+
!result?.didRepoMutation && 'perform and verify the repository change',
|
|
244
|
+
ticketCycle && !result?.didMcpTaskUpdate && 'update the ticket through update_ticket',
|
|
245
|
+
resultMessageRequired && !result?.didResultMessage && 'post a final result after the repository work',
|
|
246
|
+
].filter(Boolean)
|
|
247
|
+
if (result?.mcpErrors?.length) missing.push('resolve failed MCP calls: ' + result.mcpErrors.join(', '))
|
|
248
|
+
return missing
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function combineRuntimeWorkEvidence(first, second) {
|
|
252
|
+
return {
|
|
253
|
+
...second,
|
|
254
|
+
didCode: !!first?.didCode || !!second?.didCode,
|
|
255
|
+
didRepoMutation: !!first?.didRepoMutation || !!second?.didRepoMutation,
|
|
256
|
+
didMessage: !!first?.didMessage || !!second?.didMessage,
|
|
257
|
+
didChannelMessage: !!first?.didChannelMessage || !!second?.didChannelMessage,
|
|
258
|
+
// A message in a continuation comes after repository work completed in the
|
|
259
|
+
// first turn, even when that continuation itself performs no new mutation.
|
|
260
|
+
didResultMessage: !!first?.didResultMessage || !!second?.didResultMessage || (!!first?.didRepoMutation && !!second?.didChannelMessage),
|
|
261
|
+
didMcpTaskRead: !!first?.didMcpTaskRead || !!second?.didMcpTaskRead,
|
|
262
|
+
didMcpTaskUpdate: !!first?.didMcpTaskUpdate || !!second?.didMcpTaskUpdate,
|
|
263
|
+
mcpCalls: [...new Set([...(first?.mcpCalls || []), ...(second?.mcpCalls || [])])],
|
|
264
|
+
// A focused continuation gets the chance to clear an earlier transient MCP
|
|
265
|
+
// failure. Only failures still present in the continuation remain blockers.
|
|
266
|
+
mcpErrors: second?.mcpErrors || [],
|
|
267
|
+
// Never deliver a stale promise together with a later verified result.
|
|
268
|
+
outputText: second?.outputText || first?.outputText || '',
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
134
272
|
export function agentStateRequest(backend, channelId, state, apiKey, identifier) {
|
|
135
273
|
if (!['thinking', 'working', 'typing'].includes(state)) throw new Error('invalid agent state')
|
|
136
274
|
const id = Number(channelId)
|
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 { agentStateRequest, buildTaskCompletionReport, classifyConversationTarget, codexPolicyBlock, conversationNeedsCode, mentionDedupeKeys, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted, ticketDisplaySlug } from './events.mjs'
|
|
13
|
+
import { agentAddedByName, agentStateRequest, buildTaskCompletionReport, claudeEventEvidence, classifyConversationTarget, codexEventEvidence, codexPolicyBlock, combineRuntimeWorkEvidence, conversationNeedsCode, mentionDedupeKeys, missingRuntimeWorkEvidence, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted, ticketDisplaySlug } from './events.mjs'
|
|
14
14
|
import { createByoMemoryGraph } from './memory.mjs'
|
|
15
15
|
import { repositoryHasPrPushAuthorization } from './pr-push.mjs'
|
|
16
16
|
import { createMcpHttpClient } from './mcp-http.mjs'
|
|
@@ -34,7 +34,7 @@ const REPLY_DISCIPLINE = [
|
|
|
34
34
|
' • FIRST-PERSON VOICE. Speak as yourself: use “I”, “I\'m”, and “my”. Never refer to yourself by your agent name or in the third person, and never restate your own name in introductions, acknowledgements, progress, blockers, or results. The app already shows who sent the message. Sound like a warm, accountable teammate, not a status bot.',
|
|
35
35
|
' • IS IT FOR YOU? Act ONLY on messages addressed to YOU — an @mention of your exact name, a direct question to you, or a reply to something YOU said or did. If a DIFFERENT agent or person was @mentioned or asked to do something, STAY OUT: do not answer for them and do not pick up their task. When it is not yours, posting nothing is the correct move.',
|
|
36
36
|
' • EVENT NAMES ARE NOT OWNERSHIP. A transport may wake you for activity in a thread you once joined. Trust only the watcher\'s verified recipient decision for the current source message; never infer that every thread update is yours.',
|
|
37
|
-
' • NO DUPLICATES OR PICKUP NOISE. Before you post, scan the recent thread/channel for what YOU already said. If you already replied to this exact request, do NOT
|
|
37
|
+
' • NO DUPLICATES OR PICKUP NOISE. Before you post, scan the recent thread/channel for what YOU already said. If you already replied to this exact request, do NOT repeat the same message. Do not send a generic pickup acknowledgement; activity shows that work is underway. For longer code work, you may send at most one concrete progress update after work has actually begun, but that update NEVER completes the cycle: keep using tools, then send one distinct verified result or real blocker. One final answer per question.',
|
|
38
38
|
' • BE SURE BEFORE YOU SPEAK. Do not claim something is possible, done, or broken until you have actually verified it — call the tool, read the code, check the real state. Never assert then contradict yourself. If you are unsure, verify FIRST, then give ONE clear, final answer instead of thinking out loud across several messages.',
|
|
39
39
|
' • USE RECALL, NEVER INVENT IT. Before answering a context-dependent question, search the visible thread and use any available history, search, docs, or recall tools. Reuse verified context instead of asking the user to repeat it. If no record exists, say plainly "I don\'t have a record of that". Never fabricate past events, conversations, results, links, PR numbers, deploy URLs, or figures.',
|
|
40
40
|
' • TICKET SLUGS, NEVER DATABASE IDS. In every human-facing channel message, ticket comment, PR description, summary, blocker, and result, reference a ticket by the exact project-scoped slug returned by get_ticket/list_tasks (for example, `OVS-57`). Numeric project_id and ticket_id values are internal MCP arguments only: never write `#57`, `ticket 57`, or expose a database id to teammates. If the backend omits the slug, use the ticket title or say “the ticket”; do not invent a slug.',
|
|
@@ -79,10 +79,10 @@ const CODE_CHARTER = [
|
|
|
79
79
|
'CAPABILITY CHECK: before you EVER answer "I can\'t do that", verify against the tools above. If a tool exists for it, DO it. To be explicit: you CAN read/inspect any of the org\'s codebases, clone a repo you don\'t have yet, work on it, create a branch, and raise a PR — say YES to these and then actually do them.',
|
|
80
80
|
'',
|
|
81
81
|
'WORK ETHIC — how a reliable teammate behaves (this is the difference between useful and ignored):',
|
|
82
|
-
' 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.',
|
|
82
|
+
' 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. Sending an intent or progress message is not a stop condition: continue using tools and send the verified result or blocker afterward.',
|
|
83
83
|
' 2. FINISH, then REPORT. Always update/move the ticket with update_ticket. Reply in a supplied human source thread when one exists. For backlog-assigned work, do not call post_message yourself: the watcher publishes one evidence-verified result in the project channel after the PR and ticket handoff are confirmed.',
|
|
84
84
|
' 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.',
|
|
85
|
-
' 4. One reply per
|
|
85
|
+
' 4. One final reply per request; answer several nudges together. A single concrete progress update is allowed during longer work, but it must be followed by the final result or blocker in the same cycle.',
|
|
86
86
|
' 5. RECOVER DEAD COMMAND SESSIONS. If write_stdin reports “Unknown process id”, that command session has already exited. Never poll the same process id again. Start a fresh exec_command when more work is required, then continue the task and verify the final state.',
|
|
87
87
|
'',
|
|
88
88
|
REPLY_DISCIPLINE,
|
|
@@ -106,7 +106,7 @@ const CODE_FAST = [
|
|
|
106
106
|
' • 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.',
|
|
107
107
|
' • 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.',
|
|
108
108
|
'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.',
|
|
109
|
-
'
|
|
109
|
+
'For a non-code question, post ONE answer and stop. For code work, do not post a generic pickup message. You may post one concrete progress update after work starts, but keep working after it; then post one distinct final result with the PR/test evidence or a real blocker. Never repeat the same message. The final update should be 1-3 sentences.',
|
|
110
110
|
].join('\n')
|
|
111
111
|
|
|
112
112
|
// ── Workspace-ethics cycles (both chat-only + code agents) ───────────────────
|
|
@@ -328,7 +328,7 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
328
328
|
// JSON mode is the evidence boundary. Formatted stdout only tells us that
|
|
329
329
|
// OpenCode exited; raw events tell us which tools actually completed.
|
|
330
330
|
const args = ['run', full, '--auto', '--format', 'json', '--dir', workspace, ...(m ? ['--model', m] : [])]
|
|
331
|
-
let child = null, done = false, didCode = false, didRepoMutation = false, didMessage = false, didChannelMessage = false, didMcpTaskRead = false, didMcpTaskUpdate = false
|
|
331
|
+
let child = null, done = false, didCode = false, didRepoMutation = false, didMessage = false, didChannelMessage = false, didResultMessage = false, didMcpTaskRead = false, didMcpTaskUpdate = false
|
|
332
332
|
let cancel = null
|
|
333
333
|
let outputText = '', jsonlBuffer = ''
|
|
334
334
|
const mcpCalls = new Set(), mcpErrors = new Set(), runtimeErrors = new Set()
|
|
@@ -339,7 +339,7 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
339
339
|
clearTimeout(timer)
|
|
340
340
|
const calls = [...mcpCalls]
|
|
341
341
|
log('opencode MCP calls: ' + (calls.length ? calls.join(', ') : 'none') + (mcpErrors.size ? ' (failed: ' + [...mcpErrors].join(', ') + ')' : ''))
|
|
342
|
-
resolve({ ...o, didCode, didRepoMutation, didMessage, didChannelMessage, didMcpTaskRead, didMcpTaskUpdate, mcpCalls: calls, mcpErrors: [...mcpErrors], outputText })
|
|
342
|
+
resolve({ ...o, didCode, didRepoMutation, didMessage, didChannelMessage, didResultMessage, didMcpTaskRead, didMcpTaskUpdate, mcpCalls: calls, mcpErrors: [...mcpErrors], outputText })
|
|
343
343
|
}
|
|
344
344
|
const inspectLine = (line) => {
|
|
345
345
|
const value = String(line || '').trim()
|
|
@@ -367,6 +367,8 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
367
367
|
if (evidence.failed) mcpErrors.add(evidence.mcpTool)
|
|
368
368
|
else if (evidence.completed) mcpErrors.delete(evidence.mcpTool)
|
|
369
369
|
}
|
|
370
|
+
const repositoryWasChanged = didRepoMutation
|
|
371
|
+
if (evidence.didChannelMessage && repositoryWasChanged) didResultMessage = true
|
|
370
372
|
didCode ||= !!evidence.didCode
|
|
371
373
|
didRepoMutation ||= !!evidence.didRepoMutation
|
|
372
374
|
didMessage ||= !!evidence.didMessage
|
|
@@ -446,7 +448,7 @@ function createCodexRunner({ mcpUrl, mcpHeaders, workdir, canCode, maxCycleMs, l
|
|
|
446
448
|
...(m ? ['--model', m] : []),
|
|
447
449
|
...(mcpOverride ? ['-c', mcpOverride] : []),
|
|
448
450
|
full]
|
|
449
|
-
let child = null, done = false, didCode = false, didRepoMutation = false, didMessage = false, didChannelMessage = false, outputText = '', jsonlBuffer = '', stderrBuffer = '', stderrLineBuffer = ''
|
|
451
|
+
let child = null, done = false, didCode = false, didRepoMutation = false, didMessage = false, didChannelMessage = false, didResultMessage = false, outputText = '', jsonlBuffer = '', stderrBuffer = '', stderrLineBuffer = ''
|
|
450
452
|
let cancel = null
|
|
451
453
|
let policyBlock = null
|
|
452
454
|
const mcpCalls = new Set(), mcpErrors = new Set()
|
|
@@ -458,7 +460,7 @@ function createCodexRunner({ mcpUrl, mcpHeaders, workdir, canCode, maxCycleMs, l
|
|
|
458
460
|
clearTimeout(timer)
|
|
459
461
|
const calls = [...mcpCalls]
|
|
460
462
|
log('codex MCP calls: ' + (calls.length ? calls.join(', ') : 'none') + (mcpErrors.size ? ' (failed: ' + [...mcpErrors].join(', ') + ')' : ''))
|
|
461
|
-
resolve({ ...o, didCode, didRepoMutation, didMessage, didChannelMessage, didMcpTaskRead, didMcpTaskUpdate, mcpCalls: calls, mcpErrors: [...mcpErrors], outputText, policyBlock })
|
|
463
|
+
resolve({ ...o, didCode, didRepoMutation, didMessage, didChannelMessage, didResultMessage, didMcpTaskRead, didMcpTaskUpdate, mcpCalls: calls, mcpErrors: [...mcpErrors], outputText, policyBlock })
|
|
462
464
|
}
|
|
463
465
|
const inspectDiagnostic = (value) => {
|
|
464
466
|
const s = String(value || '')
|
|
@@ -483,24 +485,23 @@ function createCodexRunner({ mcpUrl, mcpHeaders, workdir, canCode, maxCycleMs, l
|
|
|
483
485
|
const s = line.trim()
|
|
484
486
|
if (!s) return
|
|
485
487
|
policyBlock = codexPolicyBlock(s) || policyBlock
|
|
486
|
-
if (/command_execution|file_change|apply_patch|shell_command|exec_command/i.test(s)) didCode = true
|
|
487
|
-
if (/file_change|apply_patch/i.test(s) || /\b(?:git\s+(?:commit|push)|gh\s+pr\s+create|openvisio-agent\s+push-pr-branch)\b/i.test(s)) didRepoMutation = true
|
|
488
|
-
if (/post_message|comment_ticket/i.test(s)) didMessage = true
|
|
489
488
|
try {
|
|
490
489
|
const event = JSON.parse(s)
|
|
491
|
-
const
|
|
492
|
-
if (
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (
|
|
498
|
-
if (tool === 'update_ticket') didMcpTaskUpdate = true
|
|
499
|
-
if (/^(?:create_codebase_branch|create_codebase_commit|write_codebase_file|create_pull_request)$/.test(tool)) { didCode = true; didRepoMutation = true }
|
|
500
|
-
if (/post_message|comment_ticket/.test(tool)) didMessage = true
|
|
501
|
-
if (/post_message/.test(tool)) didChannelMessage = true
|
|
502
|
-
if (/fail|error/i.test(String(item.status || '')) || item.error) mcpErrors.add(tool)
|
|
490
|
+
const evidence = codexEventEvidence(event)
|
|
491
|
+
if (evidence.outputText) outputText += ' ' + evidence.outputText
|
|
492
|
+
if (evidence.mcpTool) {
|
|
493
|
+
mcpCalls.add(evidence.mcpTool)
|
|
494
|
+
try { onTool && onTool(evidence.mcpTool) } catch { /* activity is best-effort */ }
|
|
495
|
+
if (evidence.failed) mcpErrors.add(evidence.mcpTool)
|
|
496
|
+
else if (evidence.completed) mcpErrors.delete(evidence.mcpTool)
|
|
503
497
|
}
|
|
498
|
+
if (evidence.didChannelMessage && didRepoMutation) didResultMessage = true
|
|
499
|
+
didCode ||= !!evidence.didCode
|
|
500
|
+
didRepoMutation ||= !!evidence.didRepoMutation
|
|
501
|
+
didMessage ||= !!evidence.didMessage
|
|
502
|
+
didChannelMessage ||= !!evidence.didChannelMessage
|
|
503
|
+
didMcpTaskRead ||= !!evidence.didMcpTaskRead
|
|
504
|
+
didMcpTaskUpdate ||= !!evidence.didMcpTaskUpdate
|
|
504
505
|
} catch { /* non-JSON diagnostic */ }
|
|
505
506
|
if (debug) log(' · ' + s.slice(0, 220))
|
|
506
507
|
}
|
|
@@ -567,13 +568,35 @@ function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfi
|
|
|
567
568
|
let resolveTurn = null
|
|
568
569
|
let cycleTimer = null
|
|
569
570
|
let turnToolCalls = new Set()
|
|
571
|
+
let turnToolUses = new Map()
|
|
572
|
+
let turnMcpErrors = new Set()
|
|
573
|
+
let turnDidCode = false
|
|
574
|
+
let turnDidRepoMutation = false
|
|
575
|
+
let turnDidMessage = false
|
|
576
|
+
let turnDidChannelMessage = false
|
|
577
|
+
let turnDidResultMessage = false
|
|
578
|
+
let turnDidMcpTaskRead = false
|
|
579
|
+
let turnDidMcpTaskUpdate = false
|
|
580
|
+
let turnOutputText = ''
|
|
570
581
|
const clearCycleTimer = () => { if (cycleTimer) { clearTimeout(cycleTimer); cycleTimer = null } }
|
|
571
582
|
const settleTurn = (o) => {
|
|
572
583
|
clearCycleTimer()
|
|
573
584
|
const r = resolveTurn
|
|
574
585
|
resolveTurn = null
|
|
575
586
|
const mcpCalls = [...turnToolCalls]
|
|
576
|
-
if (r) r({
|
|
587
|
+
if (r) r({
|
|
588
|
+
...o,
|
|
589
|
+
mcpCalls,
|
|
590
|
+
mcpErrors: [...turnMcpErrors],
|
|
591
|
+
didCode: turnDidCode,
|
|
592
|
+
didRepoMutation: turnDidRepoMutation,
|
|
593
|
+
didMessage: turnDidMessage,
|
|
594
|
+
didChannelMessage: turnDidChannelMessage,
|
|
595
|
+
didResultMessage: turnDidResultMessage,
|
|
596
|
+
didMcpTaskRead: turnDidMcpTaskRead,
|
|
597
|
+
didMcpTaskUpdate: turnDidMcpTaskUpdate,
|
|
598
|
+
outputText: turnOutputText.trim(),
|
|
599
|
+
})
|
|
577
600
|
}
|
|
578
601
|
|
|
579
602
|
// --debug: surface what the cycle actually does (tool calls, tool errors, text)
|
|
@@ -612,12 +635,25 @@ function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfi
|
|
|
612
635
|
if (!line.trim()) continue
|
|
613
636
|
let o; try { o = JSON.parse(line) } catch { continue }
|
|
614
637
|
if (debug) logEvent(o)
|
|
615
|
-
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
638
|
+
const evidence = claudeEventEvidence(o, turnToolUses)
|
|
639
|
+
if (evidence.outputText) turnOutputText += (turnOutputText ? ' ' : '') + evidence.outputText
|
|
640
|
+
for (const name of evidence.toolCalls || []) {
|
|
641
|
+
turnToolCalls.add(String(name).replace(/^mcp__openvisio-team__/, ''))
|
|
642
|
+
try { onTool && onTool(name) } catch { /* status is best-effort */ }
|
|
643
|
+
}
|
|
644
|
+
for (const fact of evidence.toolResults || []) {
|
|
645
|
+
if (fact.mcpTool) {
|
|
646
|
+
turnToolCalls.add(fact.mcpTool)
|
|
647
|
+
if (fact.failed) turnMcpErrors.add(fact.mcpTool)
|
|
648
|
+
else if (fact.completed) turnMcpErrors.delete(fact.mcpTool)
|
|
620
649
|
}
|
|
650
|
+
if (fact.didChannelMessage && turnDidRepoMutation) turnDidResultMessage = true
|
|
651
|
+
turnDidCode ||= !!fact.didCode
|
|
652
|
+
turnDidRepoMutation ||= !!fact.didRepoMutation
|
|
653
|
+
turnDidMessage ||= !!fact.didMessage
|
|
654
|
+
turnDidChannelMessage ||= !!fact.didChannelMessage
|
|
655
|
+
turnDidMcpTaskRead ||= !!fact.didMcpTaskRead
|
|
656
|
+
turnDidMcpTaskUpdate ||= !!fact.didMcpTaskUpdate
|
|
621
657
|
}
|
|
622
658
|
if (o.type === 'result') {
|
|
623
659
|
log('cycle done (' + (o.subtype || 'ok') + (o.is_error ? ' · ERROR' : '') + ')')
|
|
@@ -655,6 +691,16 @@ function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfi
|
|
|
655
691
|
ensureSession()
|
|
656
692
|
turnsThisSession++
|
|
657
693
|
turnToolCalls = new Set()
|
|
694
|
+
turnToolUses = new Map()
|
|
695
|
+
turnMcpErrors = new Set()
|
|
696
|
+
turnDidCode = false
|
|
697
|
+
turnDidRepoMutation = false
|
|
698
|
+
turnDidMessage = false
|
|
699
|
+
turnDidChannelMessage = false
|
|
700
|
+
turnDidResultMessage = false
|
|
701
|
+
turnDidMcpTaskRead = false
|
|
702
|
+
turnDidMcpTaskUpdate = false
|
|
703
|
+
turnOutputText = ''
|
|
658
704
|
resolveTurn = resolve
|
|
659
705
|
// Backstop: abandon a cycle that never returns a result so `busy` is released
|
|
660
706
|
// and queued mentions can proceed.
|
|
@@ -813,7 +859,15 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
813
859
|
let lastTaskTriggeredAt = 0
|
|
814
860
|
let lastInboxSignature = ''
|
|
815
861
|
let selfAgentId = null
|
|
862
|
+
let completionRecipientName = ''
|
|
816
863
|
const selfAliases = new Set([slug, identifier].map((value) => String(value || '').toLowerCase()).filter(Boolean))
|
|
864
|
+
const rememberSelfAgent = (self) => {
|
|
865
|
+
if (!self || typeof self !== 'object') return
|
|
866
|
+
if (self.id != null) selfAgentId = Number(self.id)
|
|
867
|
+
for (const alias of [self.name, self.identifier, self.slug]) if (alias) selfAliases.add(String(alias).toLowerCase())
|
|
868
|
+
const addedBy = agentAddedByName(self)
|
|
869
|
+
if (addedBy) completionRecipientName = addedBy
|
|
870
|
+
}
|
|
817
871
|
const mcpClient = createMcpHttpClient({ url: mcpUrl, apiKey, identifier, clientVersion: '0.18.5', log })
|
|
818
872
|
const callMcpTool = (name, args = {}) => mcpClient.callTool(name, args)
|
|
819
873
|
let mcpToolNames = null
|
|
@@ -978,7 +1032,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
978
1032
|
const taskKey = `${projectId}:${ticketId}`
|
|
979
1033
|
const current = toolData(await callMcpTool('get_ticket', { project_id: projectId, ticket_id: ticketId }))
|
|
980
1034
|
const ticket = current.ticket ?? current.task ?? current
|
|
981
|
-
const report = buildTaskCompletionReport(ticket, { projectId, fallbackText: result.outputText })
|
|
1035
|
+
const report = buildTaskCompletionReport(ticket, { projectId, fallbackText: result.outputText, recipientName: completionRecipientName })
|
|
982
1036
|
if (!report) return false
|
|
983
1037
|
const memoryKey = `ticket:${projectId}:${ticketId}`
|
|
984
1038
|
memory.remember({
|
|
@@ -1112,8 +1166,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1112
1166
|
const agents = Array.isArray(agentsData.agents) ? agentsData.agents : []
|
|
1113
1167
|
const self = agents.find((a) => String(a.identifier || a.slug || '') === identifier)
|
|
1114
1168
|
if (!self?.id) throw new Error('list_agents did not return this BYO agent')
|
|
1115
|
-
|
|
1116
|
-
for (const alias of [self.name, self.identifier, self.slug]) if (alias) selfAliases.add(String(alias).toLowerCase())
|
|
1169
|
+
rememberSelfAgent(self)
|
|
1117
1170
|
const projectsData = toolData(await callMcpTool('list_projects'))
|
|
1118
1171
|
const projects = Array.isArray(projectsData.projects) ? projectsData.projects : []
|
|
1119
1172
|
const assigned = []
|
|
@@ -1232,33 +1285,9 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1232
1285
|
const baseFor = (kind, delivery) => delivery?.watcherOwned
|
|
1233
1286
|
? (kind === 'full' ? fullPrompt + '\n\n' + guardedReplyPrompt : guardedReplyPrompt)
|
|
1234
1287
|
: kind === 'intro' ? INTRO : kind === 'full' ? fullPrompt : (kind === 'coord' || kind === 'sweep') ? coordinatePrompt : fastPrompt
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
|
|
1238
|
-
const evidenceGatedRuntime = agent === 'codex' || agent === 'opencode'
|
|
1239
|
-
const missingWorkEvidence = (result, ticketCycle) => {
|
|
1240
|
-
const missing = [
|
|
1241
|
-
ticketCycle && !result?.didMcpTaskRead && 'read the ticket through get_ticket/list_tasks',
|
|
1242
|
-
!result?.didRepoMutation && 'perform and verify the repository change',
|
|
1243
|
-
ticketCycle && !result?.didMcpTaskUpdate && 'update the ticket through update_ticket',
|
|
1244
|
-
].filter(Boolean)
|
|
1245
|
-
if (result?.mcpErrors?.length) missing.push('resolve failed MCP calls: ' + result.mcpErrors.join(', '))
|
|
1246
|
-
return missing
|
|
1247
|
-
}
|
|
1248
|
-
const combineWorkEvidence = (first, second) => ({
|
|
1249
|
-
...second,
|
|
1250
|
-
didCode: !!first?.didCode || !!second?.didCode,
|
|
1251
|
-
didRepoMutation: !!first?.didRepoMutation || !!second?.didRepoMutation,
|
|
1252
|
-
didMessage: !!first?.didMessage || !!second?.didMessage,
|
|
1253
|
-
didChannelMessage: !!first?.didChannelMessage || !!second?.didChannelMessage,
|
|
1254
|
-
didMcpTaskRead: !!first?.didMcpTaskRead || !!second?.didMcpTaskRead,
|
|
1255
|
-
didMcpTaskUpdate: !!first?.didMcpTaskUpdate || !!second?.didMcpTaskUpdate,
|
|
1256
|
-
mcpCalls: [...new Set([...(first?.mcpCalls || []), ...(second?.mcpCalls || [])])],
|
|
1257
|
-
// A focused recovery is allowed to clear an earlier MCP failure. Only calls
|
|
1258
|
-
// still failing in the recovery remain blockers.
|
|
1259
|
-
mcpErrors: second?.mcpErrors || [],
|
|
1260
|
-
outputText: [first?.outputText, second?.outputText].filter(Boolean).join(' '),
|
|
1261
|
-
})
|
|
1288
|
+
// Every coding runtime now exposes structured action evidence. A normal CLI
|
|
1289
|
+
// result or assistant prose never closes a work cycle by itself.
|
|
1290
|
+
const cycleSucceeded = (result) => !!result && !result.is_error && [undefined, null, '', 'ok', 'success'].includes(result.subtype)
|
|
1262
1291
|
const releaseTaskForRetry = (taskRef, prompt) => {
|
|
1263
1292
|
const directProject = Number(taskRef?.projectId)
|
|
1264
1293
|
const directTicket = Number(taskRef?.ticketId)
|
|
@@ -1323,9 +1352,10 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1323
1352
|
catch (e) { log('failed to publish policy blocker: ' + (e?.message || e)) }
|
|
1324
1353
|
return
|
|
1325
1354
|
}
|
|
1326
|
-
if (kind === 'full' &&
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1355
|
+
if (kind === 'full' && !cycleSucceeded(result)) {
|
|
1356
|
+
const outcome = result?.subtype || 'an unknown runtime error'
|
|
1357
|
+
const notice = `I'm blocked because the coding cycle ended with ${outcome}. I'm not claiming completion, and I've left the ticket open for retry.`
|
|
1358
|
+
log('WORK_CYCLE_BLOCKED ' + outcome + '; publishing blocker')
|
|
1329
1359
|
try { await publishBlocker({ prompt, taskRef: activeTaskRef, notice }) }
|
|
1330
1360
|
catch (e) { log('failed to publish cycle blocker: ' + (e?.message || e)) }
|
|
1331
1361
|
releaseTaskForRetry(activeTaskRef, prompt)
|
|
@@ -1341,13 +1371,15 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1341
1371
|
// Model prose never proves success or a blocker. Full cycles must produce
|
|
1342
1372
|
// runtime-observed ticket reads, repository evidence, and ticket updates.
|
|
1343
1373
|
const ticketCycle = !!activeTaskRef || /ticket\s+#?\d+.*?project\s+\d+/i.test(prompt)
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1374
|
+
const resultMessageRequired = kind === 'full' && !!delivery && !delivery.watcherOwned
|
|
1375
|
+
const missing = missingRuntimeWorkEvidence(result, { ticketCycle, resultMessageRequired })
|
|
1376
|
+
if (kind === 'full' && cycleSucceeded(result) && missing.length) {
|
|
1346
1377
|
log(agent + ' coding cycle incomplete; recovery requires: ' + missing.join(', '))
|
|
1347
|
-
const
|
|
1348
|
-
const
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1378
|
+
const recoveryPrompt = `CONTINUE THE SAME OPENVISIO REQUEST. Your earlier output did not complete it. Missing runtime evidence: ${missing.join('; ')}. An intent or progress message is not completion. Continue the actual work now, verify it, and then post one distinct final result or real blocker in the supplied source thread; a final result after an earlier progress message is explicitly allowed and required. Do not repeat the progress message. ${agent === 'codex' ? 'Prefer the available OpenVisio create_codebase_branch/create_codebase_commit/create_pull_request tools. Only when that linked-codebase flow is unavailable, run openvisio-agent push-pr-branch; never retry a rejected direct git push.' : ''}\n\nORIGINAL REQUEST AND ROUTING CONTEXT:\n${prompt}`
|
|
1379
|
+
const recovery = await runners.work.runCycle(recoveryPrompt, codeModel, delivery?.watcherOwned ? { disabledMcpTools: ['post_message'] } : {})
|
|
1380
|
+
const recoveredResult = combineRuntimeWorkEvidence(result, recovery)
|
|
1381
|
+
const recoveryMissing = missingRuntimeWorkEvidence(recoveredResult, { ticketCycle, resultMessageRequired })
|
|
1382
|
+
if (!cycleSucceeded(recovery) || recoveryMissing.length) {
|
|
1351
1383
|
if (recovery?.subtype === 'blocked' && recovery?.policyBlock) {
|
|
1352
1384
|
try { await reportPolicyBlock(prompt, activeTaskRef, recovery.policyBlock) }
|
|
1353
1385
|
catch (e) { log('failed to publish recovery policy blocker: ' + (e?.message || e)) }
|
|
@@ -1446,8 +1478,8 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1446
1478
|
if (selfAgentId == null) {
|
|
1447
1479
|
const agentsData = toolData(await callMcpTool('list_agents'))
|
|
1448
1480
|
const self = (Array.isArray(agentsData.agents) ? agentsData.agents : []).find((a) => String(a.identifier || a.slug || '') === identifier)
|
|
1449
|
-
selfAgentId =
|
|
1450
|
-
|
|
1481
|
+
selfAgentId = null
|
|
1482
|
+
rememberSelfAgent(self)
|
|
1451
1483
|
}
|
|
1452
1484
|
const ticketData = toolData(await callMcpTool('get_ticket', { project_id: projectId, ticket_id: ticketId }))
|
|
1453
1485
|
const ticket = ticketData.ticket ?? ticketData.task ?? ticketData
|
|
@@ -1582,7 +1614,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1582
1614
|
const ctx = cid != null
|
|
1583
1615
|
? replyDelivery?.watcherOwned
|
|
1584
1616
|
? `The watcher verified this source message is addressed to you in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". ${codingMention ? 'Complete the concrete repository work and verification first.' : 'Answer the request directly.'} Do NOT call post_message; it is intentionally unavailable. Return only the final reply as your final answer. The watcher will re-check the live thread and render it 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.`
|
|
1585
|
-
: `The watcher verified this source message is addressed to you in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". ${codingMention ?
|
|
1617
|
+
: `The watcher verified this source message is addressed to you in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". ${codingMention ? `This is concrete repository work. Start the coding flow now. You may send at most one concrete progress update after work begins, but do not stop there: continue the work and then send one distinct final result with evidence or a real blocker. Use post_message for the final result even if you already sent a progress message` : '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. Never repeat the same message.${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.${codingMention ? ' Stop only after the work and final result/blocker.' : ' After your single reply, STOP.'}`
|
|
1586
1618
|
: undefined
|
|
1587
1619
|
if (codingMention) {
|
|
1588
1620
|
// Activity indicators make accepted work visible. Do not add a canned
|