feat: integrate Devin CLI as a third agent kind (Phase A) - #200
Open
cweaty wants to merge 10 commits into
Open
Conversation
Add DevinAdapter wrapping `devin -p --prompt-file` to stream stdout as text deltas + final_text + done, matching the bridge's AgentEvent protocol. Extends all agent-kind touch points (types, validators, runtime, CLI, capability resolution, session catalog) to support 'devin' alongside 'claude' and 'codex'. Phase A limitations (by design): no structured tool events, no session resume, no image input, hardcoded --permission-mode dangerous. Phase B will switch to `devin acp` (JSON-RPC over stdio) for structured events. Verified: typecheck 0 errors, build success, 553/556 tests pass (3 pre-existing Windows failures), adapter smoke test passes. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds Devin CLI as a third supported agentKind (Phase A) by introducing a DevinAdapter wrapper around devin -p --prompt-file and wiring 'devin' through the bridge’s type unions, validation, CLI UX strings, capability resolution, model picker, and session catalog metadata.
Changes:
- Added
DevinAdapter(Phase A) that spawnsdevin -pand streams stdout into bridgeAgentEvents. - Extended agent-kind unions/validators/runtime registry/session catalog to include
'devin'. - Centralized capability selection via
capabilityForProfile()and updated call sites;/resumenow returns a friendly “not supported” message for Devin in Phase A.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/runtime/profile-runtime.test.ts | Updates expected CLI help/error text to include devin. |
| src/session/catalog.ts | Allows session catalog entries to normalize agentId: 'devin'. |
| src/runtime/registry.ts | Accepts 'devin' in persisted process registry validation. |
| src/runtime/profile-runtime.ts | Updates bootstrap selection + user-facing messages to include devin. |
| src/runtime/locks.ts | Accepts 'devin' in runtime lock metadata validation. |
| src/config/profile-store.ts | Allows parsing agentKindFromString('devin'). |
| src/config/profile-schema.ts | Extends AgentKind union + schema validation to include devin. |
| src/config/migrate-v2.ts | Migration logic accepts registry entries with agentKind: 'devin'. |
| src/commands/index.ts | Adds Devin-aware /resume behavior and uses capabilityForProfile(). |
| src/cli/index.ts | Updates CLI --agent help text to include devin. |
| src/cli/commands/start.ts | Instantiates DevinAdapter and updates availability diagnostics. |
| src/cli/commands/service.ts | Displays “Devin CLI” for devin processes. |
| src/cli/agent-detection.ts | Detects Devin CLI via LARK_CHANNEL_DEVIN_BIN or PATH. |
| src/bot/session-catalog-identity.ts | Uses centralized capabilityForProfile() for policy evaluation context. |
| src/bot/comments.ts | Uses centralized capabilityForProfile() when handling comment mentions. |
| src/bot/channel.ts | Uses centralized capabilityForProfile() in run flow. |
| src/agent/preflight.ts | Extends LocalAgentId/diagnostic validation to include devin. |
| src/agent/models.ts | Adds DEVIN_MODELS and routes supportedModels('devin'). |
| src/agent/index.ts | Re-exports DevinAdapter. |
| src/agent/devin/adapter.ts | New Phase A adapter implementing devin -p streaming into AgentEvents. |
| src/agent/capability.ts | Adds devin capability + capabilityForProfile() helper. |
| AGENTS.md | Documents Phase A behavior/limitations and Phase B ACP upgrade plan. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+6
to
+15
| import { mergeProcessEnv, spawnProcess, type SpawnedProcessByStdio } from '../../platform/spawn'; | ||
| import { buildLarkChannelEnv, type LarkChannelEnvContext } from '../lark-channel-env'; | ||
| import { checkAgentAvailability, type AgentAvailability } from '../preflight'; | ||
| import type { | ||
| AgentAdapter, | ||
| AgentBotIdentity, | ||
| AgentEvent, | ||
| AgentRun, | ||
| AgentRunOptions, | ||
| } from '../types'; |
| // Pass the prompt via a temp file (--prompt-file) so no special | ||
| // characters ever reach the Windows cmd.exe shim — same reason the | ||
| // Claude adapter avoids argv for the prompt. | ||
| const promptFile = writePromptFile(opts.prompt); |
Comment on lines
+254
to
+257
| if (accumulated.trim()) { | ||
| yield { type: 'final_text', content: accumulated }; | ||
| } | ||
| yield { type: 'done', terminationReason: 'normal' }; |
Replace the `devin -p` plain-text wrapper with `DevinAcpAdapter` that speaks JSON-RPC 2.0 (NDJSON over stdio) with `devin acp`, following the Agent Client Protocol v1 spec. ACP events mapped to the bridge's AgentEvent protocol: - agent_message_chunk → text delta (streaming) - tool_call → tool_use (tool chips in Lark card) - tool_call_update → tool_result - stopReason → done/error Implements the full ACP lifecycle: initialize → session/new (or session/load for resume) → session/prompt → session/update streaming → session/request_permission (auto-approve) → done. New files: - src/agent/devin/acp-client.ts — minimal NDJSON JSON-RPC 2.0 client - src/agent/devin/acp-adapter.ts — DevinAcpAdapter (ACP → AgentEvent) Updated: - start.ts now uses DevinAcpAdapter instead of DevinAdapter - capability.ts: devin supportsNativeHistory = true (ACP session/load) - AGENTS.md updated with Phase B status Verified: typecheck 0 errors, build success, ACP smoke tests pass (text streaming + tool_call/tool_result events), 552/556 tests pass (4 pre-existing Windows failures). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Previously each message spawned a new `devin acp` process with a fresh session — no conversation history carried over. Now the adapter emits a `system` event with the ACP sessionId after session/new, which the bridge records in the session catalog. On the next message, the catalog entry is looked up and the sessionId is passed to the adapter, which calls `session/load` to resume the conversation. Changes: - acp-adapter.ts: emit `system` event with sessionId after session setup - run-flow.ts: handle devin in session catalog lookup (same as claude) - run-flow.ts: recordRunSessionEvent handles devin sessionId - comments.ts: session resume for devin in doc-comment flows - catalog.ts: isValidAgentEntry + assertAgentIdentity handle devin (uses sessionId like claude, not threadId like codex) Verified: two-message smoke test confirms session resume — agent remembers "42" from the first message when asked in the second. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
On Windows, `child.kill('SIGTERM')` only kills the direct child process,
not the entire process tree. `devin acp` spawns child processes (MCP
servers, telemetry, etc.) that inherit the stdout pipe and keep the
Node.js event loop alive even after the parent exits — so Ctrl+C hangs.
Fix:
- AcpClient.kill(): use `taskkill /PID <pid> /T /F` on Windows to kill
the entire process tree; fall back to SIGTERM/SIGKILL on Unix
- DevinAcpAdapter.stop(): same tree-kill approach for the stop() path
- start.ts stop(): add 10s force-exit safety net so Ctrl+C always
terminates even if bridge.disconnect() hangs
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Root cause: On Windows, `process.on('SIGINT')` does not reliably fire
when the process is launched via npm's .cmd/.ps1 wrappers in PowerShell.
The readline interface's 'SIGINT' event, however, fires reliably — but
only when created with `terminal: true`.
Fix: replace the `await new Promise(() => {})` keep-alive with a
readline interface in terminal mode that listens for 'SIGINT'. This
keeps the event loop alive AND reliably catches Ctrl+C on Windows.
Also added diagnostic logging in stop() and a 10s force-exit safety net
in case bridge.disconnect() hangs.
Verified: Ctrl+C now prints "收到 SIGINT,正在关闭..." and exits cleanly.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Two features were missing for the Devin agent that Claude and Codex
already had:
1. /resume session restore — was explicitly blocked with a placeholder
message. Now wired up:
- handleResume: lists Devin sessions from the session catalog
(filtered by scopeId+agentId+cwd, sorted by recency)
- applyResume: upserts the selected sessionId into the catalog
(same path as Claude, since both use sessionId not threadId)
- consumeResumeCandidate: validates devin candidates (must have
sessionId, same as claude)
2. Image input — run-flow.ts only passed images to codex. Now devin
also gets image paths. The ACP adapter reads image files, base64-
encodes them, and sends them as ACP image content blocks in the
session/prompt request.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Two related issues:
1. post-done-exit-timeout warning: after the event stream generator
ended, client.kill() fired taskkill via spawn (async, not awaited).
The generator returned immediately, and the run-executor's
waitForExit(2000) would fire before the process actually died,
producing a spurious warning.
2. session/load failure ("Session already open in another..."): the
next run's devin acp process tried to load the same session before
the previous process had fully exited and released the session lock,
causing a fallback to a new session (losing conversation context).
Fix: after client.kill(), await waitForChildExit(child, 10_000) before
the generator returns. This ensures the OS has reaped the process and
released any session locks before the next run starts.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The EventFanout pump breaks on terminal events (done/error), which calls generator.return(). This throws at the current yield point, skipping any code after the while loop — including the client.kill() and waitForChildExit() cleanup. Moving the kill + waitForChildExit to a finally block ensures they run even when generator.return() is called. This fixes: - post-done-exit-timeout warning (child wasn't killed before run-executor's waitForExit grace period expired) - session/load failures on subsequent runs (previous process hadn't released the session lock) Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Newer lark-cli (>=1.0.47) auto-detects the --source from environment variables (LARK_CHANNEL_HOME / HERMES_HOME / OPENCLAW_HOME) and rejects an explicit --source that doesn't match the detected environment. When bridge runs inside a Hermes/Devin environment, HERMES_HOME is set, so lark-cli detects source=hermes. But bridge hardcoded --source lark-channel, causing a validation error: "--source lark-channel does not match detected Agent environment (hermes)" Fix: try bind without --source first (let lark-cli auto-detect). Only fall back to explicit --source lark-channel for older lark-cli builds that don't support auto-detection. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…t source When the bridge runs inside a Hermes/Devin environment, HERMES_HOME is set in the outer process.env. lark-cli auto-detects the source from env vars, and HERMES_HOME takes precedence over LARK_CHANNEL_HOME, causing lark-cli to bind to the hermes workspace (wrong app) instead of the lark-channel workspace (bridge's app). Fix: in buildLarkChannelEnv, when LARK_CHANNEL_HOME is set, explicitly set HERMES_HOME and OPENCLAW_HOME to undefined. mergeProcessEnv handles undefined values by deleting the key from the merged env, so the devin acp process and lark-cli commands will not see HERMES_HOME, and lark-cli will auto-detect lark-channel as the source. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
DevinAdapter,包装devin -p --prompt-file非交互模式,将 stdout 流式输出为textdelta +final_text+done事件,完全匹配 bridge 的AgentEvent协议'claude' | 'codex'扩展为'claude' | 'codex' | 'devin'capabilityForProfile()辅助函数,集中处理三种 agent 的能力解析,替换了 4 处重复的三元表达式/resume对 devin profile 返回友好的"Phase A 暂不支持"提示Phase A 设计限制
devin -p只输出纯文本)--resume未接入)--permission-mode dangerous硬编码Phase B 将切换到
devin acp(JSON-RPC over stdio)以解锁结构化事件流、会话恢复、权限映射和图片输入。详见AGENTS.md。使用方式
改动文件
src/agent/devin/adapter.ts,AGENTS.mdTest plan
pnpm typecheck— 0 错误pnpm build— 成功pnpm test— 553 通过,3 失败(均为预先存在的 Windowsshstub 问题,与本次改动无关)devin -p启动,流式输出 "pong",发出final_text+done: normalagentKindFromString('devin')返回'devin'capabilityForProfile({ agentKind: 'devin' })返回 devin capabilitysupportedModels('devin')返回正确的模型列表--help显示devin选项Generated with Devin