openvisio-agent 0.18.13 → 0.18.14
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/bin/cli.mjs +0 -0
- package/package.json +1 -1
- package/scripts/certify.mjs +2 -0
- package/src/events.mjs +17 -0
- package/src/watch.mjs +94 -15
package/bin/cli.mjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/scripts/certify.mjs
CHANGED
|
@@ -92,6 +92,8 @@ const assertions = [
|
|
|
92
92
|
['bare-agent posts have a live conversation guard', bareRun.includes('const canPostMessage') && bareRun.includes('{ canPostMessage }') && bareRuntime.includes('reply suppressed because the live thread was redirected, cancelled, or already answered')],
|
|
93
93
|
['quick replies preserve distinct top-level conversations', quickReply.includes('m.parentId ?? m.messageId') && quickReply.includes('...(parentId ? { parentId } : {})')],
|
|
94
94
|
['completion has an evidence failure gate', watcher.includes('WORK_CYCLE_FAILED')],
|
|
95
|
+
['failed evidence revisions persist and suppress self-triggered retries', watcher.includes('failedTaskVersions: [...failedTaskVersions]') && watcher.includes("failedTaskVersions.set(key, 'pending')") && watcher.includes('failedTaskRevisionIsCurrent(failedRevision, ticket)') && watcher.includes('ticket paused until its revision changes')],
|
|
96
|
+
['assigned coding tickets prefer their prepared local worktree', watcher.includes('findTicketWorktree(workdir, ticketId)') && watcher.includes('A prepared local git worktree for this ticket exists') && watcher.includes('Do not call list_codebases, codebase_tree, or get_codebase')],
|
|
95
97
|
['OpenCode emits structured events for runtime evidence', watcher.includes("'--format', 'json'") && watcher.includes('opencodeEventEvidence(event)')],
|
|
96
98
|
['OpenCode API-key MCP disables OAuth probing', opencodeConfig.includes("oauth: false") && opencodeConfig.includes('timeout: 15_000')],
|
|
97
99
|
['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)')],
|
package/src/events.mjs
CHANGED
|
@@ -36,6 +36,23 @@ export function ticketDisplaySlug(task) {
|
|
|
36
36
|
return typeof value === 'string' ? value.trim().toUpperCase() : ''
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Failed coding cycles are paused at a concrete backend revision. The watcher
|
|
40
|
+
// may write its own blocker onto the ticket after the failure, so a temporary
|
|
41
|
+
// `pending` marker must also count as current until that write is observed.
|
|
42
|
+
export function taskRevision(task) {
|
|
43
|
+
if (!task || typeof task !== 'object') return ''
|
|
44
|
+
const value = task.updated_at ?? task.updatedAt ?? task.modified_at ?? task.modifiedAt
|
|
45
|
+
return value == null ? '' : String(value).trim()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function failedTaskRevisionIsCurrent(failedRevision, task) {
|
|
49
|
+
const failed = String(failedRevision || '').trim()
|
|
50
|
+
if (!failed) return false
|
|
51
|
+
if (failed === 'pending') return true
|
|
52
|
+
const current = taskRevision(task)
|
|
53
|
+
return !current || current === failed
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
export function taskIsCompleted(task, completedTypeIds = new Set()) {
|
|
40
57
|
if (!task || typeof task !== 'object') return false
|
|
41
58
|
if (task.deleted_at || task.deletedAt || task.completed_at || task.completedAt || task.closed_at || task.closedAt || task.archived_at || task.archivedAt) return true
|
package/src/watch.mjs
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
// than written to disk from a pasted heredoc.
|
|
6
6
|
|
|
7
7
|
import { spawn, spawnSync } from 'node:child_process'
|
|
8
|
-
import { closeSync, writeFileSync, mkdirSync, existsSync, openSync, readFileSync, unlinkSync } from 'node:fs'
|
|
8
|
+
import { closeSync, writeFileSync, mkdirSync, existsSync, openSync, readFileSync, readdirSync, unlinkSync } from 'node:fs'
|
|
9
9
|
import { homedir } from 'node:os'
|
|
10
10
|
import { join, dirname } from 'node:path'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import { OV_DIR, DEFAULT_WORKSPACE, readConfig, writeJson, configPath, onPath, fail, ok, info, slugify, stripSlash, chmodSafe } from './lib.mjs'
|
|
13
13
|
import { connectAgentWs, assertWebSocket } from './ws.mjs'
|
|
14
|
-
import { agentAddedByName, agentStateRequest, buildTaskCompletionReport, claudeEventEvidence, classifyConversationTarget, codexEventEvidence, codexPolicyBlock, combineRuntimeWorkEvidence, conversationNeedsCode, mentionDedupeKeys, missingRuntimeWorkEvidence, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted, taskIsCoordinationOnly, ticketDisplaySlug } from './events.mjs'
|
|
14
|
+
import { agentAddedByName, agentStateRequest, buildTaskCompletionReport, claudeEventEvidence, classifyConversationTarget, codexEventEvidence, codexPolicyBlock, combineRuntimeWorkEvidence, conversationNeedsCode, failedTaskRevisionIsCurrent, mentionDedupeKeys, missingRuntimeWorkEvidence, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted, taskIsCoordinationOnly, taskRevision, ticketDisplaySlug } from './events.mjs'
|
|
15
15
|
import { createByoMemoryGraph } from './memory.mjs'
|
|
16
16
|
import { repositoryHasPrPushAuthorization } from './pr-push.mjs'
|
|
17
17
|
import { createMcpHttpClient } from './mcp-http.mjs'
|
|
@@ -20,6 +20,20 @@ import { buildCodexMcpOverride } from './codex-config.mjs'
|
|
|
20
20
|
import { createCycleQueue } from './cycle-queue.mjs'
|
|
21
21
|
import { modelProcessOptions, stopModelProcess } from './process-lifecycle.mjs'
|
|
22
22
|
|
|
23
|
+
function findTicketWorktree(workspaceRoot, ticketId) {
|
|
24
|
+
if (!workspaceRoot || ticketId == null) return ''
|
|
25
|
+
try {
|
|
26
|
+
if (existsSync(join(workspaceRoot, '.git'))) return workspaceRoot
|
|
27
|
+
const suffix = `-ticket-${String(ticketId).toLowerCase()}`
|
|
28
|
+
for (const entry of readdirSync(workspaceRoot, { withFileTypes: true })) {
|
|
29
|
+
if (!entry.isDirectory() || !entry.name.toLowerCase().endsWith(suffix)) continue
|
|
30
|
+
const candidate = join(workspaceRoot, entry.name)
|
|
31
|
+
if (existsSync(join(candidate, '.git'))) return candidate
|
|
32
|
+
}
|
|
33
|
+
} catch { /* workspace discovery is best-effort */ }
|
|
34
|
+
return ''
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
// Behaviour prompts. The openvisio-team MCP bridge requires the agent's
|
|
24
38
|
// credentials as ARGUMENTS on every tool call — those are injected at runtime by
|
|
25
39
|
// loopBackendWs (see credNote), NOT baked in here, so nothing needs hunting.
|
|
@@ -817,7 +831,8 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
817
831
|
const queues = Object.fromEntries(['work', 'reply'].map((laneName) => [laneName, createCycleQueue({
|
|
818
832
|
run: (item) => executeCycle(item.kind, item.context, item.targetChannels, item.taskRef, item.delivery),
|
|
819
833
|
onError: (error, item) => {
|
|
820
|
-
|
|
834
|
+
pauseFailedTask(item.taskRef)
|
|
835
|
+
void finalizeFailedTaskPause(item.taskRef)
|
|
821
836
|
log(laneName + ' cycle failed: ' + (error?.message || error))
|
|
822
837
|
},
|
|
823
838
|
})]))
|
|
@@ -842,6 +857,10 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
842
857
|
const pendingCompletionReports = new Set(Array.isArray(replayState.pendingCompletionReports) ? replayState.pendingCompletionReports : [])
|
|
843
858
|
const reportedCompletions = new Set(Array.isArray(replayState.reportedCompletions) ? replayState.reportedCompletions : [])
|
|
844
859
|
const reportedTaskComments = new Set(Array.isArray(replayState.reportedTaskComments) ? replayState.reportedTaskComments : [])
|
|
860
|
+
// Evidence-gate failures are held at the ticket revision that produced them.
|
|
861
|
+
// This is distinct from a policy block: any later human ticket change resumes
|
|
862
|
+
// the work, but reconnects and the watcher's own blocker update do not.
|
|
863
|
+
const failedTaskVersions = new Map(Array.isArray(replayState.failedTaskVersions) ? replayState.failedTaskVersions : [])
|
|
845
864
|
// A policy-blocked task stays paused across reconnects. It is released only
|
|
846
865
|
// after the ticket itself carries explicit authorization or is completed/
|
|
847
866
|
// unassigned. This prevents a 30-minute reconciliation retry from repeatedly
|
|
@@ -849,6 +868,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
849
868
|
const blockedTasks = new Set(Array.isArray(replayState.blockedTasks) ? replayState.blockedTasks : [])
|
|
850
869
|
const blockedTaskRepos = new Map(Array.isArray(replayState.blockedTaskRepos) ? replayState.blockedTaskRepos : [])
|
|
851
870
|
const trimSeen = (set) => { while (set.size > 500) set.delete(set.values().next().value) }
|
|
871
|
+
const trimMap = (map) => { while (map.size > 500) map.delete(map.keys().next().value) }
|
|
852
872
|
const MENTION_SIGNATURE_TTL_MS = 10 * 60 * 1000
|
|
853
873
|
const pruneMentionSignatures = () => {
|
|
854
874
|
const cutoff = Date.now() - MENTION_SIGNATURE_TTL_MS
|
|
@@ -863,6 +883,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
863
883
|
recentMentionSignatures: [...recentMentionSignatures],
|
|
864
884
|
seenActivities: [...seenActivities],
|
|
865
885
|
deliveredReplies: [...deliveredReplies],
|
|
886
|
+
failedTaskVersions: [...failedTaskVersions],
|
|
866
887
|
blockedTasks: [...blockedTasks],
|
|
867
888
|
blockedTaskRepos: [...blockedTaskRepos],
|
|
868
889
|
pendingCompletionReports: [...pendingCompletionReports],
|
|
@@ -871,6 +892,49 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
871
892
|
}, true)
|
|
872
893
|
} catch { /* best-effort */ }
|
|
873
894
|
}
|
|
895
|
+
const pauseFailedTask = (taskRef) => {
|
|
896
|
+
const projectId = Number(taskRef?.projectId)
|
|
897
|
+
const ticketId = Number(taskRef?.ticketId)
|
|
898
|
+
if (!Number.isFinite(projectId) || !Number.isFinite(ticketId)) return
|
|
899
|
+
const key = `${projectId}:${ticketId}`
|
|
900
|
+
// Persist before publishing the blocker. Its update_ticket event can arrive
|
|
901
|
+
// while publishBlocker is awaiting the backend response.
|
|
902
|
+
failedTaskVersions.set(key, 'pending')
|
|
903
|
+
trimMap(failedTaskVersions)
|
|
904
|
+
seenTasks.add(key)
|
|
905
|
+
trimSeen(seenTasks)
|
|
906
|
+
lastTaskSignature = ''
|
|
907
|
+
persistReplay()
|
|
908
|
+
}
|
|
909
|
+
const finalizeFailedTaskPause = async (taskRef) => {
|
|
910
|
+
const projectId = Number(taskRef?.projectId)
|
|
911
|
+
const ticketId = Number(taskRef?.ticketId)
|
|
912
|
+
if (!Number.isFinite(projectId) || !Number.isFinite(ticketId)) return
|
|
913
|
+
const key = `${projectId}:${ticketId}`
|
|
914
|
+
try {
|
|
915
|
+
const current = toolData(await callMcpTool('get_ticket', { project_id: projectId, ticket_id: ticketId }))
|
|
916
|
+
const ticket = current.ticket ?? current.task ?? current
|
|
917
|
+
failedTaskVersions.set(key, taskRevision(ticket) || 'pending')
|
|
918
|
+
trimMap(failedTaskVersions)
|
|
919
|
+
persistReplay()
|
|
920
|
+
} catch (e) {
|
|
921
|
+
log('could not snapshot failed ticket revision for #' + ticketId + ': ' + (e?.message || e) + '; keeping it paused')
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const clearFailedTask = (key) => {
|
|
925
|
+
if (!failedTaskVersions.delete(key)) return false
|
|
926
|
+
persistReplay()
|
|
927
|
+
return true
|
|
928
|
+
}
|
|
929
|
+
const failedTaskIsPaused = (key, ticket) => {
|
|
930
|
+
if (!failedTaskVersions.has(key)) return false
|
|
931
|
+
const failedRevision = failedTaskVersions.get(key)
|
|
932
|
+
if (failedTaskRevisionIsCurrent(failedRevision, ticket)) return true
|
|
933
|
+
failedTaskVersions.delete(key)
|
|
934
|
+
seenTasks.delete(key)
|
|
935
|
+
persistReplay()
|
|
936
|
+
return false
|
|
937
|
+
}
|
|
874
938
|
const markMentionHandled = (message, channelId) => {
|
|
875
939
|
pruneMentionSignatures()
|
|
876
940
|
const { idKey, signatureKey } = mentionDedupeKeys(message, channelId)
|
|
@@ -1229,12 +1293,18 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1229
1293
|
catch (e) { log('completion report retry failed for ticket #' + task.id + ': ' + (e?.message || e)) }
|
|
1230
1294
|
}
|
|
1231
1295
|
if (blockedTasks.delete(taskKey)) { blockedTaskRepos.delete(taskKey); persistReplay() }
|
|
1296
|
+
clearFailedTask(taskKey)
|
|
1232
1297
|
// Release the in-flight de-dupe key at handoff. If a reviewer moves
|
|
1233
1298
|
// the ticket back to an actionable column, that update must start a
|
|
1234
1299
|
// fresh work cycle.
|
|
1235
1300
|
seenTasks.delete(taskKey)
|
|
1236
1301
|
continue
|
|
1237
1302
|
}
|
|
1303
|
+
if (failedTaskIsPaused(taskKey, task)) {
|
|
1304
|
+
seenTasks.add(taskKey)
|
|
1305
|
+
log('backlog ticket #' + task.id + ' is paused at its failed revision; waiting for a ticket change')
|
|
1306
|
+
continue
|
|
1307
|
+
}
|
|
1238
1308
|
if (blockedTasks.has(taskKey)) {
|
|
1239
1309
|
const blockedRepo = blockedTaskRepos.get(taskKey)
|
|
1240
1310
|
const helperAuthorized = blockedRepo && repositoryHasPrPushAuthorization({ cwd: blockedRepo })
|
|
@@ -1399,11 +1469,12 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1399
1469
|
}
|
|
1400
1470
|
if (!cycleSucceeded(result)) {
|
|
1401
1471
|
const outcome = result?.subtype || 'an unknown runtime error'
|
|
1402
|
-
const notice = `I'm blocked because the ${kind === 'full' ? 'coding' : 'reply'} cycle ended with ${outcome}. I'm not claiming completion.${activeTaskRef ? " I've
|
|
1472
|
+
const notice = `I'm blocked because the ${kind === 'full' ? 'coding' : 'reply'} cycle ended with ${outcome}. I'm not claiming completion.${activeTaskRef ? " I've paused this revision until the ticket changes." : ''}`
|
|
1403
1473
|
log('WORK_CYCLE_BLOCKED ' + outcome + '; publishing blocker')
|
|
1474
|
+
if (activeTaskRef) pauseFailedTask(activeTaskRef)
|
|
1404
1475
|
try { await publishBlocker({ prompt, taskRef: activeTaskRef, delivery, notice }) }
|
|
1405
1476
|
catch (e) { log('failed to publish cycle blocker: ' + (e?.message || e)) }
|
|
1406
|
-
|
|
1477
|
+
finally { if (activeTaskRef) await finalizeFailedTaskPause(activeTaskRef) }
|
|
1407
1478
|
return
|
|
1408
1479
|
}
|
|
1409
1480
|
if (kind !== 'full' && result?.mcpErrors?.length) {
|
|
@@ -1431,12 +1502,13 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1431
1502
|
catch (e) { log('failed to publish recovery policy blocker: ' + (e?.message || e)) }
|
|
1432
1503
|
} else {
|
|
1433
1504
|
const unresolved = recoveryMissing.length ? recoveryMissing : [`the recovery cycle ended with ${recovery?.subtype || 'an unknown error'}`]
|
|
1434
|
-
const notice = `I'm blocked after one recovery attempt. Missing required evidence: ${unresolved.join('; ')}. I've
|
|
1505
|
+
const notice = `I'm blocked after one recovery attempt. Missing required evidence: ${unresolved.join('; ')}. I've paused this revision until the ticket changes, and I'm not claiming completion.`
|
|
1506
|
+
if (activeTaskRef) pauseFailedTask(activeTaskRef)
|
|
1435
1507
|
try { await publishBlocker({ prompt, taskRef: activeTaskRef, delivery, notice }) }
|
|
1436
1508
|
catch (e) { log('failed to publish recovery blocker: ' + (e?.message || e)) }
|
|
1509
|
+
finally { if (activeTaskRef) await finalizeFailedTaskPause(activeTaskRef) }
|
|
1437
1510
|
}
|
|
1438
|
-
|
|
1439
|
-
log('WORK_CYCLE_FAILED evidence gate still incomplete after one recovery; ticket retained for retry')
|
|
1511
|
+
log('WORK_CYCLE_FAILED evidence gate still incomplete after one recovery; ticket paused until its revision changes')
|
|
1440
1512
|
return
|
|
1441
1513
|
}
|
|
1442
1514
|
completionResult = recoveredResult
|
|
@@ -1445,12 +1517,10 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1445
1517
|
try {
|
|
1446
1518
|
const delivered = await announceTaskCompletion(activeTaskRef, completionResult)
|
|
1447
1519
|
if (!delivered) {
|
|
1448
|
-
|
|
1449
|
-
log('completion report deferred for ticket #' + activeTaskRef.ticketId + '; waiting for verified review/done state and PR evidence; ticket remains retryable')
|
|
1520
|
+
log('completion report deferred for ticket #' + activeTaskRef.ticketId + '; waiting for verified review/done state and PR evidence without rerunning repository work')
|
|
1450
1521
|
}
|
|
1451
1522
|
} catch (e) {
|
|
1452
|
-
|
|
1453
|
-
log('completion report failed for ticket #' + activeTaskRef.ticketId + ': ' + (e?.message || e) + '; retained for reconnect retry')
|
|
1523
|
+
log('completion report failed for ticket #' + activeTaskRef.ticketId + ': ' + (e?.message || e) + '; retained for delivery retry without rerunning repository work')
|
|
1454
1524
|
}
|
|
1455
1525
|
}
|
|
1456
1526
|
if (delivery?.watcherOwned) {
|
|
@@ -1536,7 +1606,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1536
1606
|
})
|
|
1537
1607
|
}
|
|
1538
1608
|
memory.remember({ key: `ticket:${projectId}:${ticketId}`, kind: 'ticket', state: 'unassigned', summary: ticket.title, refs: { projectId, ticketId } })
|
|
1539
|
-
blockedTasks.delete(key); blockedTaskRepos.delete(key); pendingCompletionReports.delete(key); persistReplay(); seenTasks.delete(key); log(kind + ' ticket #' + ticketId + ' is not assigned to this agent — ignored'); return
|
|
1609
|
+
blockedTasks.delete(key); blockedTaskRepos.delete(key); pendingCompletionReports.delete(key); failedTaskVersions.delete(key); persistReplay(); seenTasks.delete(key); log(kind + ' ticket #' + ticketId + ' is not assigned to this agent — ignored'); return
|
|
1540
1610
|
}
|
|
1541
1611
|
if (taskIsCompleted(ticket) || taskIsAwaitingReview(ticket)) {
|
|
1542
1612
|
memory.remember({ key: `ticket:${projectId}:${ticketId}`, kind: 'ticket', state: 'handoff', summary: ticket.title, refs: { projectId, ticketId } })
|
|
@@ -1545,10 +1615,17 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1545
1615
|
try { await announceTaskCompletion({ projectId, ticketId, channelId: activityChannel }) }
|
|
1546
1616
|
catch (e) { log('completion report failed for ticket #' + ticketId + ': ' + (e?.message || e) + '; retained for reconnect retry') }
|
|
1547
1617
|
}
|
|
1548
|
-
blockedTasks.delete(key); blockedTaskRepos.delete(key); persistReplay(); seenTasks.delete(key)
|
|
1618
|
+
blockedTasks.delete(key); blockedTaskRepos.delete(key); failedTaskVersions.delete(key); persistReplay(); seenTasks.delete(key)
|
|
1549
1619
|
log(kind + ' ticket #' + ticketId + ' is already complete or awaiting review — ignored')
|
|
1550
1620
|
return
|
|
1551
1621
|
}
|
|
1622
|
+
const hadFailedRevision = failedTaskVersions.has(key)
|
|
1623
|
+
if (failedTaskIsPaused(key, ticket)) {
|
|
1624
|
+
seenTasks.add(key)
|
|
1625
|
+
log(kind + ' ticket #' + ticketId + ' is paused at its failed revision; waiting for a ticket change')
|
|
1626
|
+
return
|
|
1627
|
+
}
|
|
1628
|
+
if (hadFailedRevision) log(kind + ' ticket #' + ticketId + ' changed since its failed cycle — resuming once')
|
|
1552
1629
|
if (blockedTasks.has(key)) {
|
|
1553
1630
|
const blockedRepo = blockedTaskRepos.get(key)
|
|
1554
1631
|
const helperAuthorized = blockedRepo && repositoryHasPrPushAuthorization({ cwd: blockedRepo })
|
|
@@ -1567,12 +1644,14 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1567
1644
|
memory.remember({ key: `ticket:${projectId}:${ticketId}`, kind: 'ticket', state: 'queued', summary: title, refs: { projectId, ticketId }, meta: { sourceEvent: kind } })
|
|
1568
1645
|
const taskText = [title, ticket.description, ticket.type, ticket.kind].filter(Boolean).join(' ')
|
|
1569
1646
|
const cycleKind = canCode && !taskIsCoordinationOnly(taskText) ? 'full' : 'coord'
|
|
1647
|
+
const preparedWorktree = cycleKind === 'full' ? findTicketWorktree(workdir, ticketId) : ''
|
|
1570
1648
|
log(kind + ' verified ticket #' + ticketId + ' “' + title + '” -> ' + cycleKind + ' lane')
|
|
1571
1649
|
const activityChannel = await projectStatusChannel(projectId)
|
|
1572
1650
|
if (activityChannel != null) sendStatus(activityChannel, 'thinking')
|
|
1573
1651
|
if (cycleKind === 'full') { pendingCompletionReports.add(key); trimSeen(pendingCompletionReports); persistReplay() }
|
|
1574
1652
|
const humanTicketRef = ticketSlug ? `ticket ${ticketSlug}` : `the ticket “${title}”`
|
|
1575
|
-
|
|
1653
|
+
const workspaceDirective = preparedWorktree ? ` A prepared local git worktree for this ticket exists at ${JSON.stringify(preparedWorktree)}. Start repository inspection and edits there. Do not call list_codebases, codebase_tree, or get_codebase while this local worktree is available.` : ''
|
|
1654
|
+
void drain(cycleKind, `Authoritative get_ticket verification confirms ${humanTicketRef} is open and assigned to YOU. Internal tool identity: project_id ${projectId}, ticket_id ${ticketId}. For every get_ticket call pass exactly { project_id: ${projectId}, ticket_id: ${ticketId} }; for list_task_types pass exactly { project_id: ${projectId} }. Numeric ids are MCP arguments only and must never appear in human-facing text; use ${ticketSlug || 'the ticket title'} instead. Ticket details: ${JSON.stringify({ slug: ticketSlug, title, description: ticket.description, priority: ticket.priority, typeId: ticket.type_id ?? ticket.typeId })}.${workspaceDirective} This assignment has no source thread: do not call post_message yourself. Use list_task_types and update_ticket to move it active, complete and verify the work, open the PR when applicable, then update/move the ticket with evidence. For coding work, the watcher will publish exactly one verified completion result in the project channel.`, activityChannel == null ? [] : [activityChannel], { projectId, ticketId, channelId: activityChannel })
|
|
1576
1655
|
} catch (e) {
|
|
1577
1656
|
log(kind + ' ticket verification failed for #' + ticketId + ': ' + (e?.message || e))
|
|
1578
1657
|
}
|