openvisio-agent 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/cli.mjs +2 -2
- package/package.json +1 -1
- package/src/watch.mjs +97 -50
package/README.md
CHANGED
|
@@ -56,9 +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 uses the backend's actual tools (`list_agents`, `list_projects`, `list_tasks`, `list_task_types`, and `list_activity`) to recover assigned tasks and recent mention activity missed while offline. The same zero-model check runs every five minutes as a safety net; a model starts only when pending work exists.
|
|
60
60
|
|
|
61
|
-
Routine messages, triage, introductions, catch-up checks, and ticket movement use a
|
|
61
|
+
Routine messages, triage, introductions, catch-up checks, and ticket movement use a lower-cost model by default. Claude uses Haiku and Codex uses `gpt-5.6-terra`; Luna is intentionally not used because watcher coordination depends on reliable MCP tool use. Requests that require repository work route to the coding model: Sonnet for Claude and `gpt-5.6-sol` for Codex. Override either lane with `--chat-model` and `--model`.
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
64
|
openvisio-agent watch --name ada # run in this terminal
|
package/bin/cli.mjs
CHANGED
|
@@ -58,8 +58,8 @@ connect --backend
|
|
|
58
58
|
--model <m> coding model. Claude defaults to sonnet; Codex defaults to
|
|
59
59
|
gpt-5.6-sol; OpenCode uses its configured default.
|
|
60
60
|
Engineers can also change it live from chat: "@agent /model …".
|
|
61
|
-
--chat-model <m>
|
|
62
|
-
|
|
61
|
+
--chat-model <m> coordination model for chat, triage, and board movement.
|
|
62
|
+
Defaults: Claude haiku, Codex gpt-5.6-terra.
|
|
63
63
|
Coding work stays on --model.
|
|
64
64
|
--no-service skip the background service — just save config + print the
|
|
65
65
|
watch commands to run yourself.
|
package/package.json
CHANGED
package/src/watch.mjs
CHANGED
|
@@ -36,7 +36,7 @@ const REPLY_DISCIPLINE = [
|
|
|
36
36
|
|
|
37
37
|
// ── CHAT-ONLY agents (no --workdir): chat/ticket tools, no code surface. ──────
|
|
38
38
|
const CHAT_CHARTER = [
|
|
39
|
-
'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode.
|
|
39
|
+
'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode. Use only tools that actually appear in your openvisio-team tool list. Backend MCP commonly provides post_message, react_message, list_agents, list_projects, list_tasks, get_ticket, update_ticket, and list_activity. Some relay runtimes also provide poll_inbox or get_marching_orders. Never call a tool that is absent. You have NO file/Bash/git tools in this mode, so you cannot write code yourself.',
|
|
40
40
|
'WORK ETHIC — behave like a dependable teammate: never leave a promise dangling. Either ACT now (reply, or file a ticket) or say plainly you can\'t and offer to file a ticket / tag a coding agent who can. Never invent progress. Close the loop every cycle — the human should never have to remind you to circle back.',
|
|
41
41
|
'',
|
|
42
42
|
REPLY_DISCIPLINE,
|
|
@@ -64,7 +64,7 @@ const COORDINATE = [
|
|
|
64
64
|
// A stable "who you are / how you work" charter prepended to every code cycle.
|
|
65
65
|
const CODE_CHARTER = [
|
|
66
66
|
'YOU ARE a connected CODING agent in an OpenVisio team, running ON THE USER\'S LAPTOP. You have REAL tools — use them; do NOT claim you lack a capability without checking what you actually hold. Your toolbox:',
|
|
67
|
-
' • openvisio-team
|
|
67
|
+
' • openvisio-team tools — use the names actually present. Backend MCP provides project/task discovery through list_agents, list_projects, list_tasks, get_ticket, update_ticket, plus post_message/react_message/list_activity. Relay runtimes may additionally expose poll_inbox, get_marching_orders, or comment_ticket.',
|
|
68
68
|
' • Read / Grep / Glob / Edit / Write / MultiEdit — inspect AND change code.',
|
|
69
69
|
' • Bash — git (branch, commit, push a branch), gh (clone repos, open PRs), run tests/builds.',
|
|
70
70
|
'YOUR WORKSPACE: your working directory is a WORKSPACE ROOT that holds the org\'s repos as subfolders. Reuse existing clones and the context you already verified. Read repository AGENTS.md instructions before changing code. For any task: locate the relevant repo under the workspace; clone it only when it is genuinely absent, then work inside that subfolder. Never ask the user for a path you can discover yourself.',
|
|
@@ -72,7 +72,7 @@ const CODE_CHARTER = [
|
|
|
72
72
|
'',
|
|
73
73
|
'WORK ETHIC — how a reliable teammate behaves (this is the difference between useful and ignored):',
|
|
74
74
|
' 1. CLOSE THE LOOP in THIS cycle. Never say "I\'ll do X" and stop. If you commit to something, do it NOW — the human must never have to remind you to circle back.',
|
|
75
|
-
' 2. FINISH, then REPORT. Your LAST action every cycle is
|
|
75
|
+
' 2. FINISH, then REPORT. Your LAST action every cycle is to update/move the ticket with update_ticket when appropriate and post a short channel result with the branch, PR link, and test result. @mention the requester by their EXACT full name.',
|
|
76
76
|
' 3. Be honest and specific. Never invent progress. If you are genuinely blocked (missing repo, unclear spec, a failing tool), say exactly what you need in one message — that IS closing the loop.',
|
|
77
77
|
' 4. One reply per channel per cycle; answer several nudges together.',
|
|
78
78
|
'',
|
|
@@ -80,7 +80,7 @@ const CODE_CHARTER = [
|
|
|
80
80
|
].join('\n')
|
|
81
81
|
|
|
82
82
|
const CODE_FULL = [
|
|
83
|
-
'THIS CYCLE:
|
|
83
|
+
'THIS CYCLE: discover assigned tickets with the tools that actually exist, then act on them. If get_marching_orders/poll_inbox exist, use them. On the backend MCP, identify yourself with list_agents, call list_projects, then list_tasks for each project and keep tasks whose agent_id or nested agent.identifier is yours.',
|
|
84
84
|
'DO NOT post a promise or pre-work acknowledgement. Start the repository work immediately. Your first task/channel update must contain either a verified result (branch, commit, PR, tests) or a concrete blocker you actually encountered.',
|
|
85
85
|
'For real code work (an assigned ticket, or a mention asking for changes), run the full flow end-to-end:',
|
|
86
86
|
' 1. GET THE CODE: use verified thread/history/recall context first, locate the target repo under your workspace root, and read its AGENTS.md. Reuse an existing clone; clone only if absent. Check `git status` before changing anything and preserve unrelated user work. Update from the remote only when it is safe. Do this yourself; never ask the user for a path you can discover.',
|
|
@@ -88,7 +88,7 @@ const CODE_FULL = [
|
|
|
88
88
|
' 3. CHANGE + VERIFY: Read/Edit/Write the files; run the tests or build if the repo has them.',
|
|
89
89
|
' 4. COMMIT + PUSH YOUR BRANCH: git add -A && git commit -m "…"; then git push -u origin agent/<slug>. Only ever push your own agent/* branch. Never --force, never push to main/master, never merge.',
|
|
90
90
|
' 5. RAISE A PR: gh pr create --fill --base <default-branch> --head agent/<slug> (a clear title + a body summarizing the change and how you verified it). Never gh pr merge.',
|
|
91
|
-
' 6. CLOSE THE LOOP:
|
|
91
|
+
' 6. CLOSE THE LOOP: move/update the ticket with update_ticket when appropriate, and post a channel reply with the summary + PR link/branch that @mentions the requester by their exact full name.',
|
|
92
92
|
'Bash is for git / gh / tests / clone ONLY — never to hunt for credentials (they are given to you above).',
|
|
93
93
|
].join('\n')
|
|
94
94
|
|
|
@@ -111,7 +111,7 @@ const INTRO = [
|
|
|
111
111
|
].join('\n')
|
|
112
112
|
const SWEEP = [
|
|
113
113
|
'DAILY CATCH-UP — you may have missed items while offline. Prioritize TASKS.',
|
|
114
|
-
'
|
|
114
|
+
'Use the available task/inbox tools. If get_marching_orders/poll_inbox are absent, use list_agents + list_projects + list_tasks to find tasks assigned to your agent identity, then:',
|
|
115
115
|
' 1. For every task assigned to YOU that you have NOT started or acknowledged: acknowledge once (comment_ticket "Catching up — picking this up now"), then do the work end-to-end and report (branch/PR + a short channel note). Skip tasks assigned to other agents.',
|
|
116
116
|
' 2. Answer only the @mentions / follow-ups that were directed at YOU and that you have not already answered — at most one reply per channel. Do not reply to threads aimed at someone else.',
|
|
117
117
|
'If there is genuinely nothing outstanding, STOP silently — do NOT post a "nothing to do" message.',
|
|
@@ -215,10 +215,11 @@ export async function runWatch({ flags }) {
|
|
|
215
215
|
const workdir = chatOnly ? '' : (explicitWorkdir || (saved && (saved.workspace || saved.workdir)) || DEFAULT_WORKSPACE)
|
|
216
216
|
if (workdir) { try { mkdirSync(workdir, { recursive: true }) } catch { /* best-effort; spawn will surface a real problem */ } }
|
|
217
217
|
|
|
218
|
-
// Keep routine coordination on
|
|
219
|
-
//
|
|
218
|
+
// Keep routine coordination on a balanced tool-capable tier. Luna proved too
|
|
219
|
+
// willing to narrate instead of acting in MCP-heavy watcher cycles, so Codex
|
|
220
|
+
// defaults to Terra for coordination and reserves Sol for repository work.
|
|
220
221
|
const defaultModel = agent === 'claude' ? 'sonnet' : agent === 'codex' ? 'gpt-5.6-sol' : ''
|
|
221
|
-
const defaultChatModel = agent === 'claude' ? 'haiku' : agent === 'codex' ? 'gpt-5.6-
|
|
222
|
+
const defaultChatModel = agent === 'claude' ? 'haiku' : agent === 'codex' ? 'gpt-5.6-terra' : ''
|
|
222
223
|
const model = String(flags.model || (saved && saved.model) || defaultModel)
|
|
223
224
|
const chatModel = String(flags['chat-model'] || (saved && saved.chatModel) || defaultChatModel)
|
|
224
225
|
|
|
@@ -343,17 +344,35 @@ function createCodexRunner({ mcpUrl, mcpHeaders, workdir, canCode, maxCycleMs, l
|
|
|
343
344
|
...(m ? ['--model', m] : []),
|
|
344
345
|
...(mcpOverride ? ['-c', mcpOverride] : []),
|
|
345
346
|
full]
|
|
346
|
-
let child = null, done = false, didCode = false, didMessage = false, outputText = '', jsonlBuffer = ''
|
|
347
|
-
const
|
|
347
|
+
let child = null, done = false, didCode = false, didRepoMutation = false, didMessage = false, outputText = '', jsonlBuffer = ''
|
|
348
|
+
const mcpCalls = new Set(), mcpErrors = new Set()
|
|
349
|
+
let didMcpTaskRead = false, didMcpTaskUpdate = false
|
|
350
|
+
const finish = (o) => {
|
|
351
|
+
if (done) return
|
|
352
|
+
done = true
|
|
353
|
+
clearTimeout(timer)
|
|
354
|
+
const calls = [...mcpCalls]
|
|
355
|
+
log('codex MCP calls: ' + (calls.length ? calls.join(', ') : 'none') + (mcpErrors.size ? ' (failed: ' + [...mcpErrors].join(', ') + ')' : ''))
|
|
356
|
+
resolve({ ...o, didCode, didRepoMutation, didMessage, didMcpTaskRead, didMcpTaskUpdate, mcpCalls: calls, outputText })
|
|
357
|
+
}
|
|
348
358
|
const inspectLine = (line) => {
|
|
349
359
|
const s = line.trim()
|
|
350
360
|
if (!s) return
|
|
351
361
|
if (/command_execution|file_change|apply_patch|shell_command|exec_command/i.test(s)) didCode = true
|
|
362
|
+
if (/file_change|apply_patch/i.test(s) || /\b(?:git\s+(?:commit|push)|gh\s+pr\s+create)\b/i.test(s)) didRepoMutation = true
|
|
352
363
|
if (/post_message|comment_ticket/i.test(s)) didMessage = true
|
|
353
364
|
try {
|
|
354
365
|
const event = JSON.parse(s)
|
|
355
366
|
const item = event.item ?? event
|
|
356
367
|
if (item?.type === 'agent_message' && typeof item.text === 'string') outputText += ' ' + item.text
|
|
368
|
+
if (item?.type === 'mcp_tool_call') {
|
|
369
|
+
const tool = String(item.tool || item.name || item.method || 'unknown').replace(/^openvisio-team[.:/]/, '')
|
|
370
|
+
mcpCalls.add(tool)
|
|
371
|
+
if (/^(?:get_ticket|list_tasks|list_task_types)$/.test(tool)) didMcpTaskRead = true
|
|
372
|
+
if (tool === 'update_ticket') didMcpTaskUpdate = true
|
|
373
|
+
if (/post_message|comment_ticket/.test(tool)) didMessage = true
|
|
374
|
+
if (/fail|error/i.test(String(item.status || '')) || item.error) mcpErrors.add(tool)
|
|
375
|
+
}
|
|
357
376
|
} catch { /* non-JSON diagnostic */ }
|
|
358
377
|
if (debug) log(' · ' + s.slice(0, 220))
|
|
359
378
|
}
|
|
@@ -517,7 +536,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
517
536
|
const canCode = !!workdir
|
|
518
537
|
// The Mastra bridge authenticates per-CALL: every openvisio-team tool needs
|
|
519
538
|
// agent_identifier + agent_api_key as arguments. Hand them over up front.
|
|
520
|
-
const credNote = `AUTH: the openvisio-team tools REQUIRE two arguments on EVERY call — agent_identifier: "${identifier}" and agent_api_key: "${apiKey}". Include BOTH on every openvisio-team tool call
|
|
539
|
+
const credNote = `AUTH: the openvisio-team tools REQUIRE two arguments on EVERY call — agent_identifier: "${identifier}" and agent_api_key: "${apiKey}". Include BOTH on every openvisio-team tool call. Use ONLY names shown in the current tool list. On backend MCP, discover work with list_agents, list_projects, list_tasks, get_ticket, update_ticket, and list_activity; get_marching_orders, poll_inbox, and comment_ticket may be absent. Tools may be namespaced — call whichever names actually appear. The credentials are given here; do NOT hunt for them. Bash/git/gh ARE for code work; this rule only forbids searching for keys.`
|
|
521
540
|
// The STATIC charter + creds are the session system prompt (cached, billed once),
|
|
522
541
|
// NOT re-sent in every cycle's user message — the big token saving.
|
|
523
542
|
const systemPrompt = (canCode ? CODE_CHARTER : CHAT_CHARTER) + '\n\n' + credNote
|
|
@@ -553,6 +572,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
553
572
|
let backlogProbeBusy = false
|
|
554
573
|
let lastTaskSignature = ''
|
|
555
574
|
let lastTaskTriggeredAt = 0
|
|
575
|
+
let lastInboxSignature = ''
|
|
556
576
|
let mcpSessionId = ''
|
|
557
577
|
let mcpRpcId = 0
|
|
558
578
|
|
|
@@ -577,7 +597,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
577
597
|
}
|
|
578
598
|
const ensureMcpSession = async () => {
|
|
579
599
|
if (mcpSessionId) return
|
|
580
|
-
const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.
|
|
600
|
+
const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.15.0' } } }, false)
|
|
581
601
|
if (!res.ok) throw new Error('MCP initialize HTTP ' + res.status)
|
|
582
602
|
await mcpPayload(res)
|
|
583
603
|
mcpSessionId = res.headers.get('mcp-session-id') || ''
|
|
@@ -594,7 +614,17 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
594
614
|
}
|
|
595
615
|
const payload = await mcpPayload(res)
|
|
596
616
|
if (payload.error) throw new Error(`MCP ${name}: ${payload.error.message || 'tool error'}`)
|
|
597
|
-
|
|
617
|
+
const result = payload.result ?? payload
|
|
618
|
+
if (result?.isError) {
|
|
619
|
+
const detail = result.content?.find?.((c) => c?.type === 'text')?.text || 'tool error'
|
|
620
|
+
throw new Error(`MCP ${name}: ${detail}`)
|
|
621
|
+
}
|
|
622
|
+
return result
|
|
623
|
+
}
|
|
624
|
+
const toolData = (result) => {
|
|
625
|
+
const text = result?.content?.find?.((c) => c?.type === 'text')?.text
|
|
626
|
+
if (typeof text !== 'string') return result?.structuredContent ?? result ?? {}
|
|
627
|
+
try { return JSON.parse(text) } catch { return { text } }
|
|
598
628
|
}
|
|
599
629
|
|
|
600
630
|
// Reconcile everything that may have arrived while disconnected. This spends no
|
|
@@ -603,27 +633,53 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
603
633
|
if (!mcpUrl || backlogProbeBusy || busy) return
|
|
604
634
|
backlogProbeBusy = true
|
|
605
635
|
try {
|
|
606
|
-
const
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
636
|
+
const agentsData = toolData(await callMcpTool('list_agents'))
|
|
637
|
+
const agents = Array.isArray(agentsData.agents) ? agentsData.agents : []
|
|
638
|
+
const self = agents.find((a) => String(a.identifier || a.slug || '') === identifier)
|
|
639
|
+
if (!self?.id) throw new Error('list_agents did not return this BYO agent')
|
|
640
|
+
const projectsData = toolData(await callMcpTool('list_projects'))
|
|
641
|
+
const projects = Array.isArray(projectsData.projects) ? projectsData.projects : []
|
|
642
|
+
const assigned = []
|
|
643
|
+
const mentionActivity = []
|
|
644
|
+
for (const project of projects) {
|
|
645
|
+
const [tasksData, typesData, activityData] = await Promise.all([
|
|
646
|
+
callMcpTool('list_tasks', { project_id: project.id }).then(toolData),
|
|
647
|
+
callMcpTool('list_task_types', { project_id: project.id }).then(toolData),
|
|
648
|
+
callMcpTool('list_activity', { project_id: project.id }).then(toolData),
|
|
649
|
+
])
|
|
650
|
+
const doneIds = new Set((Array.isArray(typesData.types) ? typesData.types : Array.isArray(typesData.task_types) ? typesData.task_types : Array.isArray(typesData.taskTypes) ? typesData.taskTypes : []).filter((t) => /^(?:done|complete|completed|closed)$/i.test(String(t.name || '').trim())).map((t) => Number(t.id)))
|
|
651
|
+
for (const task of Array.isArray(tasksData.tasks) ? tasksData.tasks : []) {
|
|
652
|
+
const taskAgentId = Number(task.agent_id ?? task.agentId ?? task.agent?.id)
|
|
653
|
+
const taskIdent = String(task.agent?.identifier ?? task.agent?.slug ?? '')
|
|
654
|
+
if (!task.deleted_at && !doneIds.has(Number(task.type_id ?? task.typeId)) && (taskAgentId === Number(self.id) || taskIdent === identifier)) {
|
|
655
|
+
assigned.push({ id: task.id, projectId: project.id, project: project.name, title: task.title, priority: task.priority, typeId: task.type_id ?? task.typeId })
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const activities = Array.isArray(activityData.activities) ? activityData.activities : Array.isArray(activityData.activity) ? activityData.activity : []
|
|
659
|
+
const mentionNeedles = [self.name, self.identifier, self.slug, identifier].filter(Boolean).map((s) => '@' + String(s).toLowerCase())
|
|
660
|
+
for (const item of activities) {
|
|
661
|
+
const text = JSON.stringify(item)
|
|
662
|
+
const lower = text.toLowerCase()
|
|
663
|
+
if (mentionNeedles.some((needle) => lower.includes(needle)) && /message|mention|channel/i.test(text)) mentionActivity.push({ projectId: project.id, project: project.name, activity: item })
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (!assigned.length) lastTaskSignature = ''
|
|
667
|
+
else {
|
|
668
|
+
const signature = JSON.stringify(assigned)
|
|
613
669
|
const retryDue = Date.now() - lastTaskTriggeredAt > 30 * 60 * 1000
|
|
614
670
|
if (signature !== lastTaskSignature || retryDue) {
|
|
615
671
|
lastTaskSignature = signature
|
|
616
672
|
lastTaskTriggeredAt = Date.now()
|
|
617
|
-
log('backlog reconciliation found
|
|
618
|
-
void drain('full',
|
|
673
|
+
log('backlog reconciliation found ' + assigned.length + ' assigned task(s) -> coding cycle')
|
|
674
|
+
void drain('full', `Backlog reconciliation verified these open tasks are assigned to YOU: ${JSON.stringify(assigned)}. Start with the highest-priority/oldest one immediately. Do not call nonexistent get_marching_orders or poll_inbox tools, and do not merely acknowledge. Use get_ticket if more detail is needed, move it to the active column with update_ticket, complete the repository work, verify it, open the PR, move the ticket to done when appropriate, and report evidence.`)
|
|
619
675
|
}
|
|
620
676
|
}
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
log('backlog reconciliation found
|
|
625
|
-
void drain('fast',
|
|
626
|
-
}
|
|
677
|
+
const inboxSignature = JSON.stringify(mentionActivity.slice(-20)).slice(0, 4000)
|
|
678
|
+
if (mentionActivity.length && inboxSignature !== lastInboxSignature) {
|
|
679
|
+
lastInboxSignature = inboxSignature
|
|
680
|
+
log('backlog reconciliation found mention activity -> reply cycle')
|
|
681
|
+
void drain('fast', `Recent project activity contains these messages mentioning YOU: ${inboxSignature}. Handle each still-unanswered mention once using the channel/message ids in the activity. Do not call nonexistent poll_inbox or get_marching_orders tools. Skip anything already answered by you.`)
|
|
682
|
+
} else if (!mentionActivity.length) lastInboxSignature = ''
|
|
627
683
|
} catch (e) {
|
|
628
684
|
mcpSessionId = ''
|
|
629
685
|
log('backlog reconciliation failed: ' + (e && e.message ? e.message : e))
|
|
@@ -653,13 +709,15 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
653
709
|
const heartbeat = targets.length ? setInterval(() => emitStatus('working'), 9000) : null
|
|
654
710
|
try {
|
|
655
711
|
const result = await runCycle(prompt, useModel)
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
// Do not retry a real blocker
|
|
712
|
+
// A zero exit is not proof of work. Assigned-ticket cycles must both use
|
|
713
|
+
// the task MCP and leave repository evidence. A simple `ls` no longer
|
|
714
|
+
// counts as completion. Do not retry a verified real blocker.
|
|
659
715
|
const legitimateNoWork = /\b(?:no (?:open |assigned |pending )?tasks?|not assigned to (?:me|you)|assigned to (?:another|someone else)|blocked|cannot|can't|missing|need access|permission|unclear)\b/i.test(result?.outputText || '')
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
716
|
+
const incomplete = !result?.didRepoMutation || !result?.didMcpTaskRead || !result?.didMcpTaskUpdate
|
|
717
|
+
if (agent === 'codex' && kind === 'full' && result?.subtype === 'ok' && incomplete && !legitimateNoWork) {
|
|
718
|
+
const missing = [!result.didMcpTaskRead && 'read the ticket through get_ticket/list_tasks', !result.didRepoMutation && 'perform and verify the repository change', !result.didMcpTaskUpdate && 'update the ticket through update_ticket'].filter(Boolean)
|
|
719
|
+
log('coding cycle incomplete; recovery requires: ' + missing.join(', '))
|
|
720
|
+
await runCycle(`The assigned task is NOT complete. Missing evidence: ${missing.join('; ')}. Do not post another acknowledgement or plan. Resume now. First use get_ticket/list_tasks and list_task_types, then do the repository work, verify it, commit and push an agent/* branch, open the PR, call update_ticket with the correct board column, and post exactly one result update with evidence. If a real blocker appears, report it once.`, codeModel)
|
|
663
721
|
}
|
|
664
722
|
} finally {
|
|
665
723
|
if (heartbeat) clearInterval(heartbeat)
|
|
@@ -802,8 +860,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
802
860
|
log('up — backend WS watcher on ' + wsUrl + (canCode ? ' [code: ' + workdir + ']' : ''))
|
|
803
861
|
handle = connectAgentWs({ wsUrl, apiKey, identifier, onEvent, onConnect: () => { setTimeout(() => void reconcileBacklog(), 250) }, log })
|
|
804
862
|
|
|
805
|
-
|
|
806
|
-
let introTimer = null, sweepStartTimer = null, sweepTimer = null, taskProbeStartTimer = null, taskProbeTimer = null
|
|
863
|
+
let introTimer = null, taskProbeStartTimer = null, taskProbeTimer = null
|
|
807
864
|
if (mcpConfig || mcpUrl) {
|
|
808
865
|
// Workspace ethics: a one-time hello the FIRST time this agent ever connects.
|
|
809
866
|
const introMarker = join(OV_DIR, 'intro-' + slugify(identifier) + '.done')
|
|
@@ -812,17 +869,9 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
812
869
|
log('first connection — introducing self to the workspace')
|
|
813
870
|
introTimer = setTimeout(() => void drain('intro'), 5000) // let the socket subscribe first
|
|
814
871
|
}
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
if (claimStartupSweep(identifier)) {
|
|
819
|
-
sweepStartTimer = setTimeout(() => { log('startup catch-up sweep'); void drain('sweep', SWEEP) }, 12_000)
|
|
820
|
-
} else {
|
|
821
|
-
log('startup catch-up sweep skipped (ran within the last 6h)')
|
|
822
|
-
}
|
|
823
|
-
sweepTimer = setInterval(() => { log('daily catch-up sweep'); void drain('sweep', SWEEP) }, DAY_MS)
|
|
824
|
-
// Cheap reliability net: no model runs unless the MCP result actually contains
|
|
825
|
-
// assigned work. This also catches assignments created while the socket was down.
|
|
872
|
+
// Reconciliation replaces the old model-driven startup/daily sweep. It uses
|
|
873
|
+
// supported MCP tools directly, stays silent when empty, and hands verified
|
|
874
|
+
// coding tasks to the full lane instead of letting Luna announce it cannot code.
|
|
826
875
|
taskProbeStartTimer = setTimeout(() => void reconcileBacklog(), 3_000)
|
|
827
876
|
taskProbeTimer = setInterval(() => void reconcileBacklog(), 5 * 60 * 1000)
|
|
828
877
|
} else {
|
|
@@ -834,8 +883,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
834
883
|
// service doesn't leak a half-open connection or a dangling interval.
|
|
835
884
|
const bye = () => {
|
|
836
885
|
if (introTimer) clearTimeout(introTimer)
|
|
837
|
-
if (sweepStartTimer) clearTimeout(sweepStartTimer)
|
|
838
|
-
if (sweepTimer) clearInterval(sweepTimer)
|
|
839
886
|
if (taskProbeStartTimer) clearTimeout(taskProbeStartTimer)
|
|
840
887
|
if (taskProbeTimer) clearInterval(taskProbeTimer)
|
|
841
888
|
try { handle && handle.close() } catch { /* noop */ }
|