mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
* fix(autofix-pr): 修复 taskId 不一致导致 monitor lock dangling
问题:createAutofixTeammate 生成 teammate UUID 作为 monitor lock 的 key,
但 registerRemoteAgentTask 内部生成的 framework taskId 是另一个 UUID。
CCR session 自然完成时框架调 clearActiveMonitor(frameworkTaskId)
guard 失败,lock 永不释放,导致后续 /autofix-pr 报 "already monitoring"。
修复(Phase 1 of remote-agent completion loop):
- monitorState 新增 updateActiveMonitor(partial) 原子更新
- callAutofixPr 在 register 后 swap lock 的 taskId 到 framework 分配的 id
- RemoteAgentTask 引入 registerCompletionHook 注册式 API(参考已有的
registerCompletionChecker 模式),在 5 个完成路径调 runCompletionHook
- autofix-pr 命令模块自己注册 cleanup hook,避免 framework 反向依赖
command 模块
测试:
- monitorState 新增 4 个测试(updateActiveMonitor 行为 + bug 复现/修复)
- launchAutofixPr 新增 3 个端到端回归测试(taskId swap + hook 触发 +
subsequent launch 不报 already monitoring)
完整分析与 Phase 2/3 改造方案见
docs/features/remote-agent-completion-analysis.md。
* feat(autofix-pr): 注册 completionChecker 用 gh CLI 探测 PR 完成
Phase 2 of remote-agent completion loop。Phase 1 修了 monitor lock
dangling,但完成信号仍然只能等 CCR session 自然 archive(timing 不可
预测,且不知道 PR 究竟有没有被修好)。Phase 2 加上主动完成探测。
实现:
- 新增 prOutcomeCheck.ts(纯决策矩阵):summariseAutofixOutcome 给定
PR 快照 + 基线 SHA 返回 completed/summary。8 个决策分支单元测试。
- 新增 prFetch.ts(spawn 层):runGhPrView 调 gh CLI,fetchPrHeadSha
在 launch 时捕获基线 SHA,checkPrAutofixOutcome 组合两者。
- AutofixPrRemoteTaskMetadata 加 initialHeadSha?: string 字段,survive
--resume。
- launchAutofixPr.ts 模块顶部 registerCompletionChecker('autofix-pr',
...),5s throttle 防 gh CLI 调用爆。callAutofixPr 启动时调
fetchPrHeadSha 传入 metadata。
决策矩阵:
MERGED → done(merged)
CLOSED 未 merge → done(closed without fix)
OPEN 无 baseline → 继续轮询
OPEN head 未变 → 继续轮询(agent 还没 push)
OPEN head 变 + CI pending → 继续轮询
OPEN head 变 + CI failure → done(surface red,user 决定 retry)
OPEN head 变 + CI success → done(clean fix)
设计:
- gh CLI 而非 Octokit:复用用户已有 auth,不引入 token 管理
- 决策与 spawn 分文件:prOutcomeCheck 纯函数易测,prFetch 单独 mock
避免 Bun mock.module 进程级污染(已在 launchAutofixPr.test 注释说明)
- 5s throttle:framework 每 1s 轮询,gh CLI subprocess 太重不能跟上
- 失败兜底:fetchPrHeadSha/checkPrAutofixOutcome 失败均不抛,returns
null/false,framework 继续走原路径
测试:
- prOutcomeCheck 9 个单测覆盖决策矩阵
- launchAutofixPr 5 个新测试:checker 注册 / fetchPrHeadSha 调用 /
initialHeadSha 传 metadata / SHA 失败仍能 launch / SHA null 处理
完整方案见 docs/features/remote-agent-completion-analysis.md。
* feat(autofix-pr): 内容回流让本地模型读到 PR 修复结果
Phase 3 of remote-agent completion loop。Phase 2 注册了 completionChecker
让框架能在 PR 合并/关闭/有 push+CI 绿时主动完成 task,但 task-notification
仍然只携带 generic 文本(""${owner}/${repo}#42 merged"")。Phase 3 让本地
模型读到远端 agent 自己产出的结构化结果(commits 列表、files 列表、CI
状态、人类可读 summary)。
实现:
- 新增 extractAutofixResultFromLog (src/commands/autofix-pr/
extractAutofixResult.ts):从 SDKMessage[] 中扫 <autofix-result> tag,
优先 hook stdout 后 fallback assistant text,latest-wins。10 个单测。
- RemoteAgentTask 新增 registerContentExtractor 注册式 API + 私有
enqueueRichRemoteNotification(参考 enqueueRemoteReviewNotification),
在 3 个 generic 完成路径(archived / completionChecker / result-driven)
先尝试 tryExtractRichContent,有内容用 rich 变体,没有走 generic。
isRemoteReview 路径不变(它走自己的 enqueueRemoteReviewNotification)。
- launchAutofixPr.ts 模块顶部 registerContentExtractor('autofix-pr',
extractAutofixResultFromLog)。initialMessage 加 <autofix-result> 输出
指令(pr-number / commits-pushed / files-changed / ci-status / summary)。
设计:
- 注册式 API(同 Phase 1 hook + Phase 2 checker):framework 不反向依赖
命令模块,所有 PR-specific 逻辑在 autofix-pr/
- latest-wins:agent 重试时只取最新 tag,旧 tag 不会污染
- truncated tag → null:开 tag 无对应闭 tag 视为不完整,走 generic
fallback
- 跨 message 不拼接:开 tag 和闭 tag 在不同 message 视为不完整(避免
误拼字符串)
- 字符串 content 不解析:assistant.message.content 为 string(非 block
array)的少见路径直接 skip,不 crash
测试:
- extractAutofixResultFromLog 10 个单测(空 log / 无 tag / hook stdout /
assistant text / hook_response subtype / 多 tag latest-wins / 截断 /
hook 后于 assistant 的优先级 / 跨 message 不拼接 / 字符串 content
graceful)
- launchAutofixPr 3 个新测试(extractor 注册 / initialMessage 含 tag
schema / extractor 真实行为)
完整方案见 docs/features/remote-agent-completion-analysis.md 第 5.3 节。
* fix(autofix-pr): extractBetween 支持 latest tag 截断时回溯到更早完整对
如果远端 agent 重试时写了完整 <autofix-result> 后又开了一个被截断的
第二个 tag, 旧实现只看 lastIndexOf(open) 然后找不到 close 就返回 null,
导致前面那个完整结果被丢弃。改为从尾向首遍历所有 open tag, 返回第一个
能配对的 open/close 对。
附带:
- docs/features/remote-agent-completion-analysis.md: 9 处裸 fenced block
补 language tag (text/http), 修复 markdownlint MD040 警告
- 同文件: 两处"三选项" → "三个选项" 符合中文量词习惯
* test(autofix-pr): 补齐 completionChecker / 边界 CI 检查覆盖率
针对 codecov patch coverage gap, 补足三块此前未走到的代码路径:
prOutcomeCheck.ts (原 96.92%, 2 lines missing):
- statusCheckRollup === undefined 路径 (与空数组分支不同, GitHub 在无
checks 配置的 PR 上直接省略字段)
- COMPLETED 状态但 conclusion 为 null/空 的 in-flight 检查归为 pending
launchAutofixPr.ts (原 58.33%, 15 lines missing):
- registerCompletionChecker arrow body: metadata 缺失早返回 / 节流窗口内
返回 null / completed=false 返回 null / completed=true 返回 summary /
initialHeadSha 透传到 checkPrAutofixOutcome
- registerCompletionHook 的 if(meta) 短路两侧: 有 metadata 时清空节流条目,
无 metadata 时仍释放 active monitor lock
所有新测试沿用现有 mock.module 与 registerXxxMock.mock.calls 拉取注册
回调的模式, 无新增依赖。prOutcomeCheck 11/11 本地通过。
* style: biome check --fix 整形 launchAutofixPr.test 新增段
---------
Co-authored-by: unraid <local@unraid.local>
Co-authored-by: Claude <noreply@anthropic.com>
429 lines
16 KiB
TypeScript
429 lines
16 KiB
TypeScript
// NOTE: subscribePR (KAIROS_GITHUB_WEBHOOKS feature) is omitted here.
|
||
// The kairos client is not fully available in this repo. The feature-gated
|
||
// call is a nice-to-have and safe to skip — teleport + registerRemoteAgentTask
|
||
// is sufficient for the core autofix flow.
|
||
|
||
import React from 'react'
|
||
import { feature } from 'bun:bundle'
|
||
import {
|
||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
logEvent,
|
||
} from '../../services/analytics/index.js'
|
||
import {
|
||
checkRemoteAgentEligibility,
|
||
formatPreconditionError,
|
||
getRemoteTaskSessionUrl,
|
||
registerCompletionChecker,
|
||
registerCompletionHook,
|
||
registerContentExtractor,
|
||
registerRemoteAgentTask,
|
||
type AutofixPrRemoteTaskMetadata,
|
||
type BackgroundRemoteSessionPrecondition,
|
||
} from '../../tasks/RemoteAgentTask/RemoteAgentTask.js'
|
||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||
import { detectCurrentRepositoryWithHost } from '../../utils/detectRepository.js'
|
||
import { teleportToRemote } from '../../utils/teleport.js'
|
||
import { AutofixProgress } from './AutofixProgress.js'
|
||
import { createAutofixTeammate } from './inProcessAgent.js'
|
||
import {
|
||
clearActiveMonitor,
|
||
getActiveMonitor,
|
||
isMonitoring,
|
||
trySetActiveMonitor,
|
||
updateActiveMonitor,
|
||
} from './monitorState.js'
|
||
import { extractAutofixResultFromLog } from './extractAutofixResult.js'
|
||
import { parseAutofixArgs } from './parseArgs.js'
|
||
import { checkPrAutofixOutcome, fetchPrHeadSha } from './prFetch.js'
|
||
import { detectAutofixSkills, formatSkillsHint } from './skillDetect.js'
|
||
|
||
// Throttle map for the completionChecker: gh CLI is called at most once per
|
||
// PR per CHECK_INTERVAL_MS, regardless of the framework's 1s poll cadence.
|
||
// Key is `${owner}/${repo}#${prNumber}`. Cleared when the completion hook
|
||
// fires so a re-launched monitor starts with a fresh budget.
|
||
const lastCheckAt = new Map<string, number>()
|
||
const CHECK_INTERVAL_MS = 5_000
|
||
|
||
function throttleKey(meta: AutofixPrRemoteTaskMetadata): string {
|
||
return `${meta.owner}/${meta.repo}#${meta.prNumber}`
|
||
}
|
||
|
||
// Register the completionChecker once at module load. The framework calls it
|
||
// on every poll tick for tasks with remoteTaskType==='autofix-pr'; throttle
|
||
// inside so we don't fire gh CLI 60×/min. Returns the summary string on
|
||
// completion (becomes the task-notification body) or null to keep polling.
|
||
registerCompletionChecker('autofix-pr', async metadata => {
|
||
const meta = metadata as AutofixPrRemoteTaskMetadata | undefined
|
||
if (!meta) return null
|
||
|
||
const key = throttleKey(meta)
|
||
const now = Date.now()
|
||
if (now - (lastCheckAt.get(key) ?? 0) < CHECK_INTERVAL_MS) return null
|
||
lastCheckAt.set(key, now)
|
||
|
||
const result = await checkPrAutofixOutcome({
|
||
owner: meta.owner,
|
||
repo: meta.repo,
|
||
prNumber: meta.prNumber,
|
||
initialHeadSha: meta.initialHeadSha,
|
||
})
|
||
return result.completed ? result.summary : null
|
||
})
|
||
|
||
// Release the singleton monitor lock when the framework transitions the
|
||
// autofix task to a terminal state. Without this, the lock — keyed by the
|
||
// framework-assigned taskId (after callAutofixPr's updateActiveMonitor swap)
|
||
// — would dangle past natural completion, blocking subsequent /autofix-pr
|
||
// invocations until the process restarts. Registered at module load; the
|
||
// framework's runCompletionHook invokes it once per terminal transition.
|
||
// Also clear the per-PR throttle entry so a re-launch starts fresh.
|
||
registerCompletionHook('autofix-pr', (taskId, metadata) => {
|
||
clearActiveMonitor(taskId)
|
||
const meta = metadata as AutofixPrRemoteTaskMetadata | undefined
|
||
if (meta) lastCheckAt.delete(throttleKey(meta))
|
||
})
|
||
|
||
// Phase 3 content return: extract the <autofix-result> tag from the session
|
||
// log so the local model sees the agent's structured outcome (commits
|
||
// pushed, files changed, CI status) inline in the completion task-
|
||
// notification — instead of just a file-path pointer. The framework falls
|
||
// back to the generic notification if extraction returns null.
|
||
registerContentExtractor('autofix-pr', log => extractAutofixResultFromLog(log))
|
||
|
||
function makeErrorText(message: string, code: string): string {
|
||
logEvent('tengu_autofix_pr_result', {
|
||
result:
|
||
'failed' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
error_code:
|
||
code as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
})
|
||
return `Autofix PR failed: ${message}`
|
||
}
|
||
|
||
export const callAutofixPr: LocalJSXCommandCall = async (
|
||
onDone,
|
||
context,
|
||
args,
|
||
) => {
|
||
try {
|
||
const parsed = parseAutofixArgs(args)
|
||
|
||
// 1. stop sub-command
|
||
if (parsed.action === 'stop') {
|
||
const m = getActiveMonitor()
|
||
if (!m) {
|
||
onDone('No active autofix monitor.', { display: 'system' })
|
||
return null
|
||
}
|
||
clearActiveMonitor()
|
||
// Honest message: the local lock is released and any in-flight
|
||
// teleport request is aborted, but a CCR session that has already
|
||
// started running on the cloud will continue until it completes or is
|
||
// cancelled from claude.ai/code.
|
||
onDone(
|
||
`Stopped local monitoring of ${m.repo}#${m.prNumber}. Any already-running remote session continues until it finishes or is cancelled from claude.ai/code.`,
|
||
{ display: 'system' },
|
||
)
|
||
return null
|
||
}
|
||
|
||
// 2. invalid
|
||
if (parsed.action === 'invalid') {
|
||
onDone(
|
||
`Invalid args: ${parsed.reason}. Use /autofix-pr <pr-number> | stop | <owner>/<repo>#<n>`,
|
||
{
|
||
display: 'system',
|
||
},
|
||
)
|
||
return null
|
||
}
|
||
|
||
// 3. freeform — not yet supported
|
||
if (parsed.action === 'freeform') {
|
||
onDone(
|
||
'Freeform prompt mode not yet supported. Use /autofix-pr <pr-number>.',
|
||
{
|
||
display: 'system',
|
||
},
|
||
)
|
||
return null
|
||
}
|
||
|
||
// 4. start. has_repo_path tracks whether the user supplied an explicit
|
||
// owner/repo via cross-repo syntax (vs relying on directory detection).
|
||
logEvent('tengu_autofix_pr_started', {
|
||
action:
|
||
'start' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
has_pr_number:
|
||
'true' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
has_repo_path: String(
|
||
!!(parsed.owner && parsed.repo),
|
||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
})
|
||
|
||
// 4.1 resolve owner/repo. Always detect cwd repo first because teleport
|
||
// takes the git source from the working directory; cross-repo args that
|
||
// don't match cwd would silently work on the wrong repo.
|
||
let detected: { host: string; owner: string; name: string } | null
|
||
try {
|
||
detected = await detectCurrentRepositoryWithHost()
|
||
} catch {
|
||
onDone(
|
||
makeErrorText(
|
||
'Cannot detect GitHub repo from current directory.',
|
||
'session_create_failed',
|
||
),
|
||
{ display: 'system' },
|
||
)
|
||
return null
|
||
}
|
||
if (!detected || detected.host !== 'github.com') {
|
||
onDone(
|
||
makeErrorText(
|
||
'Cannot detect GitHub repo from current directory.',
|
||
'session_create_failed',
|
||
),
|
||
{ display: 'system' },
|
||
)
|
||
return null
|
||
}
|
||
|
||
// Cross-repo args (owner/repo#n) must match the current working directory;
|
||
// teleport's git source is taken from cwd, so a mismatch would create a
|
||
// session against the wrong repo. Accept both as a safety check rather
|
||
// than as a real cross-repo capability — true cross-repo support requires
|
||
// a separate clone path not yet implemented here.
|
||
if (
|
||
(parsed.owner && parsed.owner !== detected.owner) ||
|
||
(parsed.repo && parsed.repo !== detected.name)
|
||
) {
|
||
onDone(
|
||
makeErrorText(
|
||
`Cross-repo autofix is not supported from this directory. Run from ${detected.owner}/${detected.name} or pass only the PR number.`,
|
||
'repo_mismatch',
|
||
),
|
||
{ display: 'system' },
|
||
)
|
||
return null
|
||
}
|
||
const owner = detected.owner
|
||
const repo = detected.name
|
||
|
||
const { prNumber } = parsed
|
||
|
||
// 4.2 singleton lock — already monitoring this exact PR
|
||
if (isMonitoring(owner, repo, prNumber)) {
|
||
logEvent('tengu_autofix_pr_result', {
|
||
result:
|
||
'success_rc' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
})
|
||
onDone(`Already monitoring ${repo}#${prNumber} in background.`, {
|
||
display: 'system',
|
||
})
|
||
return null
|
||
}
|
||
|
||
// 4.2b note: the existing-different-PR check is folded into the
|
||
// trySetActiveMonitor call below. Doing the check + set atomically there
|
||
// avoids a TOCTOU window between the read and the write under concurrent
|
||
// invocations.
|
||
|
||
// 4.3 eligibility check (tolerate no_remote_environment, surface real reasons).
|
||
// skipBundle:true matches the teleport call below — autofix needs to push
|
||
// back to GitHub, which a git bundle cannot do.
|
||
const eligibility = await checkRemoteAgentEligibility({ skipBundle: true })
|
||
if (!eligibility.eligible) {
|
||
// Discriminated union: TypeScript narrows `eligibility` here, no cast needed.
|
||
const blockers = eligibility.errors.filter(
|
||
(e: BackgroundRemoteSessionPrecondition) =>
|
||
e.type !== 'no_remote_environment',
|
||
)
|
||
if (blockers.length > 0) {
|
||
const reasons = blockers.map(formatPreconditionError).join('\n')
|
||
onDone(
|
||
makeErrorText(
|
||
`Remote agent not available:\n${reasons}`,
|
||
'session_create_failed',
|
||
),
|
||
{ display: 'system' },
|
||
)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 4.4 detect skills
|
||
const skills = detectAutofixSkills(process.cwd())
|
||
const skillsHint = formatSkillsHint(skills)
|
||
|
||
// 4.5 compose message
|
||
const target = `${owner}/${repo}#${prNumber}`
|
||
const branchName = `refs/pull/${prNumber}/head`
|
||
const initialMessage = `Auto-fix failing CI checks on PR #${prNumber} in ${owner}/${repo}.${skillsHint}
|
||
|
||
When you finish (or hit a blocker you can't recover from), output the following XML tag as your final message so the local user gets a structured summary:
|
||
|
||
<autofix-result>
|
||
<pr-number>${prNumber}</pr-number>
|
||
<commits-pushed>
|
||
<commit sha="...">commit message</commit>
|
||
</commits-pushed>
|
||
<files-changed>
|
||
<file path="...">N changes</file>
|
||
</files-changed>
|
||
<ci-status>green | red | pending | unknown</ci-status>
|
||
<summary>One-sentence summary of what was fixed or why it could not be fixed.</summary>
|
||
</autofix-result>
|
||
|
||
If no fix was needed, omit <commits-pushed> and <files-changed> and explain in <summary>. If you only attempted partial work, list the commits you did push and explain the remainder in <summary>.`
|
||
|
||
// 4.6 in-process teammate
|
||
const teammate = createAutofixTeammate(initialMessage, target)
|
||
|
||
// 4.7 acquire lock atomically BEFORE doing any awaits. This closes the
|
||
// TOCTOU race where two concurrent invocations both see active=null and
|
||
// both try to create remote sessions.
|
||
const lockAcquired = trySetActiveMonitor({
|
||
taskId: teammate.taskId,
|
||
owner,
|
||
repo,
|
||
prNumber,
|
||
abortController: teammate.abortController,
|
||
startedAt: Date.now(),
|
||
})
|
||
if (!lockAcquired) {
|
||
const existing = getActiveMonitor()
|
||
onDone(
|
||
makeErrorText(
|
||
`already monitoring ${existing?.repo}#${existing?.prNumber}. Run /autofix-pr stop first.`,
|
||
'rc_already_monitoring_other',
|
||
),
|
||
{ display: 'system' },
|
||
)
|
||
return null
|
||
}
|
||
|
||
// 4.8 teleport — wire BOTH onBundleFail and onCreateFail so HTTP-layer
|
||
// failures (4xx/5xx, expired token, invalid PR ref) reach the user with
|
||
// the upstream message instead of the generic fallback. skipBundle:true
|
||
// is required for autofix: the remote container must push back to GitHub,
|
||
// which a bundle-cloned source cannot do (teleport.tsx documents this).
|
||
// Note: refs/pull/<n>/head is not a pushable ref. We do NOT pass
|
||
// reuseOutcomeBranch — the orchestrator generates a claude/* branch and
|
||
// the user pushes/PRs from claude.ai/code.
|
||
let teleportFailMsg: string | undefined
|
||
const captureFailMsg = (msg: string) => {
|
||
teleportFailMsg = msg
|
||
}
|
||
let session: { id: string; title: string } | null = null
|
||
try {
|
||
session = await teleportToRemote({
|
||
initialMessage,
|
||
source: 'autofix_pr',
|
||
branchName,
|
||
skipBundle: true,
|
||
title: `Autofix PR: ${target}`,
|
||
useDefaultEnvironment: true,
|
||
signal: teammate.abortController.signal,
|
||
githubPr: { owner, repo, number: prNumber },
|
||
onBundleFail: captureFailMsg,
|
||
onCreateFail: captureFailMsg,
|
||
})
|
||
} catch (teleErr: unknown) {
|
||
clearActiveMonitor(teammate.taskId)
|
||
const teleMsg =
|
||
teleErr instanceof Error ? teleErr.message : String(teleErr)
|
||
onDone(makeErrorText(`teleport failed: ${teleMsg}`, 'teleport_failed'), {
|
||
display: 'system',
|
||
})
|
||
return null
|
||
}
|
||
|
||
if (!session) {
|
||
clearActiveMonitor(teammate.taskId)
|
||
onDone(
|
||
makeErrorText(
|
||
teleportFailMsg ?? 'remote session creation failed.',
|
||
'session_create_failed',
|
||
),
|
||
{ display: 'system' },
|
||
)
|
||
return null
|
||
}
|
||
|
||
// 4.8b capture PR head SHA before registering so the completionChecker
|
||
// can detect when the agent has pushed new commits. Best-effort — if gh
|
||
// is unavailable or the call fails, leave initialHeadSha undefined and
|
||
// the checker falls back to terminal-state-only completion (closed /
|
||
// merged). Don't block on this; teleport succeeded already.
|
||
const initialHeadSha =
|
||
(await fetchPrHeadSha(owner, repo, prNumber).catch(() => null)) ??
|
||
undefined
|
||
|
||
// 4.9 register task. If this throws, release the lock so the user can
|
||
// retry — the remote CCR session is already created so we surface a
|
||
// dedicated error code.
|
||
//
|
||
// After registration succeeds, swap the lock's taskId from the tentative
|
||
// teammate UUID (used to acquire the lock atomically before teleport) to
|
||
// the framework-assigned taskId. Without this swap, the framework's own
|
||
// cleanup path (clearActiveMonitor(frameworkTaskId) on natural completion)
|
||
// would no-op against a lock keyed by teammate.taskId, leaving the
|
||
// singleton lock dangling and blocking future /autofix-pr invocations.
|
||
try {
|
||
const { taskId: frameworkTaskId } = registerRemoteAgentTask({
|
||
remoteTaskType: 'autofix-pr',
|
||
session,
|
||
command: `/autofix-pr ${prNumber}`,
|
||
context,
|
||
isLongRunning: true,
|
||
remoteTaskMetadata: { owner, repo, prNumber, initialHeadSha },
|
||
})
|
||
updateActiveMonitor({ taskId: frameworkTaskId })
|
||
} catch (regErr: unknown) {
|
||
clearActiveMonitor(teammate.taskId)
|
||
const regMsg = regErr instanceof Error ? regErr.message : String(regErr)
|
||
onDone(
|
||
makeErrorText(
|
||
`task registration failed: ${regMsg}`,
|
||
'registration_failed',
|
||
),
|
||
{ display: 'system' },
|
||
)
|
||
return null
|
||
}
|
||
|
||
// 4.10 PR webhook subscription (feature-gated, non-fatal)
|
||
if (feature('KAIROS_GITHUB_WEBHOOKS')) {
|
||
// kairos client not available in this repo — skip silently
|
||
}
|
||
|
||
// 4.11 success
|
||
const sessionUrl = getRemoteTaskSessionUrl(session.id)
|
||
logEvent('tengu_autofix_pr_result', {
|
||
result:
|
||
'success_rc' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
})
|
||
// Also call onDone so callers that listen to the callback get notified.
|
||
onDone(`Autofix launched for ${target}. Track: ${sessionUrl}`, {
|
||
display: 'system',
|
||
})
|
||
// Return a React progress UI showing the completed pipeline.
|
||
// The REPL renders the returned React element inline alongside the text.
|
||
return React.createElement(AutofixProgress, {
|
||
phase: 'done',
|
||
target,
|
||
sessionUrl,
|
||
})
|
||
} catch (err: unknown) {
|
||
const msg = err instanceof Error ? err.message : String(err)
|
||
logEvent('tengu_autofix_pr_result', {
|
||
result:
|
||
'failed' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
error_code:
|
||
'exception' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
})
|
||
onDone(`Autofix PR failed: ${msg}`, { display: 'system' })
|
||
return null
|
||
}
|
||
}
|