| name | popola-loom |
| version | 1.6.2 |
| description | PopolaLoom — 跨 CLI 元编排器。当用户要把任务派发给 Cursor / Claude / Codex / Kimi / Copilot 等 agent CLI 并跨终端持久化运行 (spawn → trace task_id → attach in)、查看任务状态、批量调度多 agent、需要 HITL 确认 / Lark 通知,或要查看 daemon 进程健康时使用本 Skill。提供 popola CLI (8+ root verb 含 dispatch / list / status / attach / cancel / probe / init / skill / doctor / update) + popolaloom-mcp stdio + Lark 双向通道。 |
| metadata | {"surfaces":["cli","ide","mcp"],"requires":{"bins":["popola"],"pythonVersion":">=3.11"},"cliHelp":"popola --help"} |
| tier | 1 |
| token_estimate | 3400 |
| last_updated | 2026-05-21 |
PopolaLoom Skill
v0.9.0 GA stable surface — 自 v0.9.0 起 CLI verb / flag spelling / daemon RPC path / --json schema / popolad.toml section name 全部锁入 SemVer(详见 docs/API_STABILITY.md)。Workflow 6/7/8 涵盖的 --cli=cursor-cloud REST + Cloud HITL γ MCP + popola relay 全部 stable;Workflow 9 (popola cloud runs) 在 v0.9.0 仍标 experimental(API_STABILITY §3.1)。v1.6.0 single-path self-hosted breaking changes:popola cloud worker {start,debug} 不再接受 --pool / --pool-name;--cloud-target=self-hosted --auth-mode=rest 退 2(改用隐式默认 --auth-mode=session-jwt,需先 agent login);--allow-fallback 对 self-hosted 是 no-op + WARN。Managed cloud + 本地 CLI 行为不变。v0.7.x → v0.9.0 升级走 docs/MIGRATION_v07_to_v09.md。
What is PopolaLoom?
PopolaLoom 是 DevolaFlow 之上的本机常驻"织机式 (loom) / 编织者 (weaver)"元编排器:把"跨 agent CLI 派发 + 持久化进程总线 + Lark + IDE 三通道 HITL"做成开发者桌面的 sidecar 服务(per init_popola_loom.md L1 — "纺织机 / 编织者"定位)。它通过常驻 popolad daemon、ArkTower 任务池(vendored)、LangGraph 子图与 9-verb MCP 桥,在 Cursor / Claude Code / Codex CLI / Kimi CLI / GitHub Copilot CLI 上空提供"派发任务 → 拿到 task_id → 任意终端 attach 进去看实时事件流 → 必要时 HITL 中断求人类回答 → 终态后 Lark 通知"的一等公民支持。关键差异:DevolaFlow 是单 agent 任务质量框架(per-task gates / convergence loops),PopolaLoom 是它在多任务 / 多 CLI 维度的横切伴侣 — 把 N 个 DevolaFlow 实例织进同一张运行图。
When to use this Skill
调用 popola 系列 verb 当用户表述匹配下列意图:
- "派发 / 起 / spawn 一个任务给 Cursor / Claude / Codex / Kimi / Copilot" →
popola dispatch <prompt> --cli=<name>
- "看一下后台跑着的所有任务 / list my running agents / 我有哪些任务在跑" →
popola list (默认仅非终态; --all 含终态)
- "这个任务现在跑到哪了 / what's the status of <task_id>" →
popola status <task_id> (含 state / pid / exit_code / latest_event_index / arktower_task_id)
- "我要 attach 进去 / 看一下实时输出 / 流一下事件 / follow this task" →
popola attach <task_id> --follow (SSE / NDJSON 流,Ctrl-C 退 attach 但任务继续跑)
- "取消 / 停 / kill 这个任务" →
popola cancel <task_id> (SIGTERM → 5 秒 grace → SIGKILL 升级,发 task.canceled)
- "确认 daemon / popolad / 后台进程是否在跑" →
popola probe 或 popola popolad status
- "把 PopolaLoom 装到我的 IDE 里 / register skill / install for cursor" →
popola init [cursor|claude|codex|copilot|all] (Stage S2/S3)
- "把 daemon 启动 / 起个后台进程" →
popola popolad start (foreground 加 --foreground)
- "评估一下 PopolaLoom 自己的 self-bootstrap 健康 / 跑一下 nines" →
popola eval run --output /tmp/nines.toml
- "我想要任务完成 / 失败 / 取消时收到 Lark 通知" → 设置
LARK_HITL_TARGET_OPEN_ID + LARK_NOTIFY_ON_COMPLETED=1 (v0.4.1+)
- "需要 HITL 暂停 / 让我点确认再继续" → 任务在 LangGraph
interrupt() 处暂停,Lark 卡片到达后用户点"通过 / 拒绝"恢复
- "诊断 PopolaLoom 整体健康 / 自检 / 一下子看完所有子系统" →
popola doctor (v0.5.0+, Stage S4)
如果用户只是问"DevolaFlow 怎么写一个 task agent"或"如何写一个 prompt",那是 DevolaFlow / 通用编程问题,不要走 popola — PopolaLoom 只在跨 CLI 派发 / 持久化 / 多任务调度场景介入。
Ambiguity Resolution Protocol
When the user asks vaguely to "dispatch/run a cloud task" and target/model/thinking depth/special modes are missing, ask option-group questions before calling popola_submit. Dimensions: target (local cursor | local claude | local codex | cursor-cloud managed | cursor-cloud self-hosted), model (adapter default plus known models such as composer-2.5, composer-2-5, sonnet, gpt-5.5, opus, gemini — v1.6.1: legacy composer-2 / composer-2-fast removed upstream), thinking_depth (cursor output_format, claude max_turns, codex sandbox, cursor-cloud effort/path-B), and special_modes (auto_create_pr, work_on_current_branch, skip_reviewer_request, cwd_flag).
AskQuestion templates (copy/paste shape): {"question":"Dispatch target?","options":["local cursor","local claude","local codex","cursor-cloud managed","cursor-cloud self-hosted"]}, then ask model, thinking_depth, and multi-select special_modes. Construct extra from the answers and call popola_submit with only cli, prompt, cwd, and extra; MCP schema remains free-form for extra. Precise user intent may skip Q&A and call popola dispatch --wizard or explicit flags.
Quick reference — common commands
| Command | Purpose | Example |
|---|
popola dispatch <prompt> --cli=<name> | 派发任务到指定 agent CLI | popola dispatch "fix the bug in foo.py" --cli=cursor |
popola dispatch <prompt> --wizard | v1.1.1 option-group Q&A: target/model/thinking_depth/special_modes | popola dispatch "refactor X" --wizard |
popola dispatch ... --cli=cursor-cloud | 派发任务到 Cursor Cloud Agents REST(远端跑,不占本机 subprocess) | 见下文 Workflow 6 |
popola cloud worker {debug,start,status,handoff,dispatch} | 启动 / 查看本机 self-hosted Cursor worker;dispatch 直接走 popolad 路由到当前 workspace worker | 见下文 Workflow 10 |
Cloud Agent 调 popolaloom_cloud_hitl_request MCP 工具 | 云端任务遇高风险决策时 deferer 给真人审批走 Lark(v0.8.7+,Enterprise/γ 模式) | 见下文 Workflow 7 |
popola dispatch ... --wait --timeout=120 | 派发并阻塞到终态(默认 60s) | popola dispatch "..." --cli=claude --wait |
popola dispatch ... --cli-flag KEY=VAL | 透传 adapter 专属参数(可重复;JSON 值自动解析)(v0.2.0+,详见 Workflow 4) | popola dispatch "..." --cli=cursor --cli-flag output_format=stream-json |
popola dispatch --replay <handoff_id> | 按之前写下的 envelope 重派发(v0.7.3+) | popola dispatch --replay cursor-fix-bug-foo-py-3a7f9c1d |
popola handoff list [--json] | 列出 active envelope(按 mtime 倒排,v0.7.2+) | popola handoff list |
popola handoff show <handoff_id> [--json] | 打印 active envelope 内容(默认 raw Markdown) | popola handoff show cursor-fix-bug-foo-py-3a7f9c1d |
popola handoff archive <handoff_id> <task_id> | 复制到 <archive_root>/<task_id>/<id>.md(D4 双层) | popola handoff archive <handoff_id> cursor-23e74ec18917 |
popola list | 列出非终态任务(v0.8.6+ 默认含 runtime 列:local/cloud) | popola list |
popola list --no-runtime | v0.8.6+ 隐藏 runtime 列(escape hatch;--json 输出不受影响) | popola list --no-runtime |
popola list --all | 含已完成 / 失败 / 取消的所有任务 | popola list --all |
popola status <task_id> | 单任务全字段状态(JSON 加 --json) | popola status cursor-23e74ec18917 |
popola attach <task_id> --follow | 跟随 SSE 事件流(默认 follow=true;v0.8.6+ 云任务额外 ingest Cursor SSE) | popola attach cursor-23e74ec18917 --follow |
popola attach <id> --follow --no-stream | v0.8.6+ 强制 legacy poll-only 路径(escape hatch) | popola attach <id> --follow --no-stream |
popola attach <task_id> --no-follow | 一次性 dump 已有事件(不阻塞) | popola attach <id> --no-follow |
popola cancel <task_id> | SIGTERM → 5 秒 grace → SIGKILL | popola cancel cursor-23e74ec18917 |
popola probe | 轻量 daemon 健康(pid / uptime / active) | popola probe |
popola list-cli | 列已注册 CLI adapter + 是否在 PATH | popola list-cli |
popola popolad start | 启动 daemon(detach 默认;--foreground 前台) | popola popolad start |
popola popolad status | daemon socket / pid / probe 三检 | popola popolad status |
popola popolad stop | SIGTERM 5s → SIGKILL,清 pid + sock | popola popolad stop |
popola eval run --output PATH | 跑 8-dim PopolaLoom-nines self-eval | popola eval run -o /tmp/nines.toml |
popola init | 自动检测 IDE 并装 SKILL.md | popola init |
popola init cursor --global | 装到 ~/.cursor/skills/popola-loom/ | popola init cursor --global |
popola init local --mode=core | 仅 scaffold .local/ 工作区 | popola init local --mode=core |
popola init --list | 打印检测到的 IDE + 安装路径 | popola init --list |
popola doctor | 五项综合自检(v0.5.0+,Stage S4) | popola doctor |
popola skill upgrade --target=cursor | 用 wheel 内最新 SKILL.md 覆盖装机版(Stage S4) | popola skill upgrade --target=cursor |
popola update | v1.4.0+ 一条命令完成 pip 升级 + 双 scope skill 刷新 + doctor(install.sh update 的 Python 等价) | popola update |
popola update --dry-run --json | 预演计划:解析 pip spec、target × scope 矩阵、doctor 探针,不动 pip / 不写文件 | popola update --dry-run --json |
popola update --from=pypi --version=1.4.0 | 锁版本到 PyPI(待 BL-v0.9.x-PyPI 落地) | popola update --from=pypi --version=1.4.0 |
popola version | 打印 popolaloom <version> | popola version |
Workflows
Workflow 1 — Dispatch + trace (single CLI)
最常见的"我有一个长任务,要丢到后台让 Cursor 跑,结果出来再回来看"流程,5 步:
- 启动 daemon(每台机器只启一次,
systemd-run / 系统重启后丢了再启):
popola popolad start
- 派发任务(拿到
task_id,立刻退 shell 也没问题):
popola dispatch "refactor module X for clarity, add tests" --cli=cursor --cwd ~/proj
- 看后台都跑着啥:
popola list
- 回来 attach 看实时输出(在另一台终端 / 另一个 SSH session 里也行,事件总线是持久化的):
popola attach cursor-23e74ec18917 --follow
- 任务终态后取消订阅 / 看最终事件 / 决定是否取消:
popola status cursor-23e74ec18917
popola cancel cursor-23e74ec18917
Workflow 2 — Multi-CLI handoff (relay)
跨 agent CLI 接力(设计阶段 Cursor 出方案 → 实现阶段 Claude 写代码 → 验证阶段 Codex 跑测试),3 步:
- 首发任务到 Cursor,等其落 handoff envelope:
popola dispatch "design API for ETag cache layer; emit handoff at .local/.agent/handoff/" \
--cli=cursor --wait
- 从 handoff envelope 读出后继任务 prompt(约定路径
.local/.agent/handoff/<id>.yaml)。
- 接力派发到 Claude(携 envelope 摘要作为 prompt 上下文):
popola dispatch "$(yq '.next_prompt' .local/.agent/handoff/<id>.yaml)" --cli=claude
Note: v0.4.0 的 popola_relay MCP primitive 已就位(per mcp/tools.py),但 CLI 直接 verb (popola relay) 仍是 v0.6.0 项;当前用 shell 编排即可。
Workflow 3 — HITL pause + Lark approval
LangGraph interrupt() 节点阻塞任务、Lark 卡片到人、人点确认后任务恢复,5 步:
- 派发一个含 HITL 节点的任务(adapter 内部检测到危险操作时调
interrupt(prompt="ok to delete prod table?")):
export LARK_HITL_TARGET_OPEN_ID=ou_xxx
popola dispatch "drop the staging.users table" --cli=cursor
- 任务卡在
interrupt(),daemon 经 lark-cli 推送一张交互卡到 LARK_HITL_TARGET_OPEN_ID(per lark/notifier.py + lark/card_templates.py)。
- 用户在飞书 App 里点"通过"或"拒绝" —
lark-cli event consume listener (per lark/listener.py) 捕获 button event。
LarkSupervisor 把 reply 写回 LangGraph state,任务恢复执行(state 切回 running,事件总线发 state.resumed)。
- 用
popola attach <task_id> --follow 在终端同步看到 resume 后的输出,或等 LARK_NOTIFY_ON_COMPLETED=1 后 Lark 卡通知终态。
Workflow 4 — Adapter-specific arg passthrough (--cli-flag)
每个 agent CLI 有自己的可选参数(cursor 的 --output-format / --session-id、claude 的 --max-turns / --session-id、codex 的 --sandbox 三档);PopolaLoom 用统一的 --cli-flag KEY=VAL 选项透传,daemon 把 KEY=VAL 收进 extra dict 后由各 adapter 的 build_command 拼成最终 argv。Value 优先按 JSON 解析(true / 123 / "foo"),解析失败 fall back 到字符串(output_format=text 等同 output_format="text"),出处 cli/main.py:_parse_cli_flags(R-012 落地)。
支持的 KEY(按 adapter 列;多余 / 未识别的 KEY 会被 adapter 静默忽略):
| Adapter | KEY | 类型 | 含义 / 落点 argv |
|---|
cursor | output_format | str | text(默认)/ stream-json,落到 --output-format <val>(白名单校验,违规直接 ValueError,No Silent Failures) |
cursor | cwd_flag | bool | true 时把 --cwd <cwd> 注入 argv;默认 false(让 supervisor 通过 Popen(cwd=...) 控制) |
cursor | session_id | str | 追加 --session-id <chatId>,与 cursor-agent create-chat 预生成的 chat 复用 |
claude | session_id | str (UUID) | 追加 --session-id <UUID>,"先分配 ID 再 spawn" 形态 |
claude | max_turns | int | 追加 --max-turns <n>,限对话轮数(防长任务失控) |
codex | sandbox | str | 三档之一: read-only / workspace-write / danger-full-access,落到 --sandbox <val>(白名单校验) |
3 个常见用法:
- Cursor
stream-json 输出(让 supervisor 端 NDJSON 解析器逐行消费 cursor 的工具调用事件):
popola dispatch "design caching layer" --cli=cursor \
--cli-flag output_format=stream-json
- Claude 先生成会话 UUID 再 spawn(PopolaLoom 派发器的标准做法 — 让
task_id 与 session_id 一一对应,便于 attach / handoff 复用):
SESSION=$(python -c "import uuid;print(uuid.uuid4())")
popola dispatch "refactor module X" --cli=claude \
--cli-flag session_id="$SESSION" \
--cli-flag max_turns=10
- Codex 限制 sandbox 到只读模式(适合 design-only / review-only 派发,防 codex 不小心写文件):
popola dispatch "review src/foo.py for bugs" --cli=codex \
--cli-flag sandbox=read-only
Tip:--cli-flag 可重复多次(典型用法是 cursor 的 output_format=stream-json + session_id=<chatId> 同时给)。Value 含空格 / 等号时用 shell 引号 + JSON 字符串:--cli-flag 'cmd_args="--foo --bar"'(注意 popolaloom 的 cursor adapter 当前不透传任意 cmd_args;需要 cursor-agent 自定义 flag 时走 popolaloom._vendored 二开或等 v0.6+ 的 --passthrough 项)。
Workflow 5 — Self-eval (PopolaLoom-nines)
跑 8 维度自评、写 TOML 报告,2 步:
- 跑评估(不需要 daemon,从
~/.popola/events/ 走 NDJSON 收据):
popola eval run --output /tmp/nines.toml
- 看维度权重 / debug 单维度:
popola eval show --json
Workflow 6 — Cloud Agent dispatch (--cli=cursor-cloud, v0.8.5+ / SSE v0.8.6+; stable since v0.9.0; single-path self-hosted since v1.6.0)
v0.9.0 GA:本 Workflow 涉及的 CLI verb (popola dispatch --cli=cursor-cloud) + --cli-flag keys + popola list runtime 列 + popola attach --no-stream flag 全部进 v0.9.x stable surface。仅 cloud.sse.* 子事件类型 payload shape 仍 experimental(API_STABILITY §3.4)。需要纯云端项目脚手架走 popola init --target=cloud-only(v0.9.0+,Q-D-4 偏离默认);要 copy-paste-ready 上手脚本走 cloud-quickstart.sh。
v1.6.0 single-path self-hosted(feedback_for_v1.5.2.md 6 项强约束):--cloud-target=self-hosted 走且仅走 Path-B JWT 直连(StartBackgroundComposerFromSnapshot)。--auth-mode=session-jwt 是 self-hosted 的隐式默认(不传即生效;显式传 --auth-mode=rest 会被拒绝退 2);不再支持 --pool worker;--allow-fallback 对 self-hosted 是 no-op + WARN;self-hosted 不会做 GitHub/GitLab 校验;每次派发完毕 stdout 多打印一行 view: https://cursor.com/agents/<bcId>。Managed Cloud (--cloud-target=cursor-managed) 仍走稳定的 Path-A REST,行为不变。
云端 Background Agent:不走本机 subprocess,而是经 daemon 侧 Supervisor 调用 Cursor Cloud Agents(managed 走 Path-A REST POST /v1/agents、self-hosted 走 Path-B Connect-RPC StartBackgroundComposerFromSnapshot),任务出现在浏览器里的 Cloud Agents UI(仪表盘入口例如 https://cursor.com/dashboard/cloud-agents,任务列表亦可从 https://cursor.com/agents 跳转)。daemon 侧的 Supervisor 检测到 CLOUD_BUILD_COMMAND_MARKER sentinel 就走 _spawn_cloud(),再按 cloud_target 选 Path-A / Path-B 分支 + cloud poller 线程对齐状态事件。
先决:
- Managed cloud:非空
CURSOR_API_KEY 环境变量 — HTTP Basic:username=api_key、password= 空串(适配器读环境变量,CloudCursorAdapter.is_available() 亦以此为准)。
- Self-hosted (v1.6.0 单路径):
agent login 生成的 ~/.config/cursor/auth.json(Path-B JWT 直连用;不需要 CURSOR_API_KEY)。同时本机需要先用 popola cloud worker start --worker-dir <repo> 注册一个 self-hosted worker。
派发命令形态 1 — managed cloud(Path-A REST,行为同 v0.9.0),与工作区 Decision matrix Q6 对齐:默认 autoCreatePR=false,需要的话用 flag 打开:
export CURSOR_API_KEY="cr_..."
popola dispatch "implement smoke-test stub in README" \
--cli=cursor-cloud \
--cloud-target=cursor-managed \
--cwd ~/src/myrepo \
--cli-flag repo_url=https://github.com/acme/monorepo \
--cli-flag starting_ref=main \
--cli-flag model=composer-2.5 \
--cli-flag auto_create_pr=false
派发命令形态 2 — self-hosted(v1.6.0 单路径 Path-B JWT,构成 Workflow 12 的实际形态):
agent login
popola cloud worker start --worker-dir "$(pwd)" --management-addr 127.0.0.1:39231
popola dispatch "ship the v1.6.0 release notes" \
--cloud-target=self-hosted --worker-name=popolaloom-myrepo-deadbeef \
--cwd "$(pwd)" \
--cli-flag repo_url=https://github.com/acme/myrepo \
--model=gpt-5.5 --preset=grind \
--no-auto-branch --no-auto-create-pr --work-on-current-branch
注意 self-hosted 形态:
- 不需要
--cli=cursor-cloud(--cloud-target=self-hosted 自动设 --cli=cursor-cloud)。
- 不需要
--auth-mode=session-jwt(v1.6.0 单路径默认;显式 --auth-mode=rest 退 2)。
- 不需要
CURSOR_API_KEY(Path-B 走 JWT)。
- 不会 校验 GitHub/GitLab —— worker 已经持有 workspace clone。
view: URL 是 web-side 观测入口,可点开看 agent 运行状态、读 workspace 内容、跟实时输出。
支持的 --cli-flag extra keys(传给 cursor_cloud.CursorCloudAdapter / REST):
| Key | 说明 |
|---|
repo_url | Git HTTPS 克隆地址(或与 pr_url 二选一) |
pr_url | 直接基于已有 PR URL 派发(与 repo_url 二选一) |
starting_ref | branch / tag,默认 "main" |
model | 云端模型 id,默认 "default"(Cursor Auto;v0.10.0 起从 "composer-2" bump 到 "default",v1.6.1 文档对齐)。可选当前 ids:composer-2.5、composer-2-5、sonnet、gpt-5.5、opus、gemini(见 GET /v1/models) |
auto_create_pr | bool,默认 false |
work_on_current_branch | bool,默认 false |
skip_reviewer_request | bool,auto_create_pr=true 时可选 |
env_vars | dict[str,str] JSON blob(透传到 payload envVars) |
use_private_worker | bool,显式请求 Cursor REST usePrivateWorker=true |
labels | dict[str,str] JSON blob,自托管 / 本地 worker 路由 labels |
worker_name / machine_name / pool_name | 非空字符串 convenience keys;分别合并为 labels.worker / labels.machine / labels.pool,并自动启用 use_private_worker |
timeout_s | HTTP 单次请求超时 float |
api_key | 覆盖环境变量里的 key(一般由测试/DI 用;生产不推荐写进 envelope) |
若设置了 labels 或任一 convenience key,use_private_worker 会自动变为 true;显式传 use_private_worker=false 同时又设置路由 label 会被拒绝,避免误以为已请求自托管 worker 路由。
与本地 cursor subprocess 的差异(一眼):popola list / popola status 会带出 runtime="cloud"、cursor_agent_id(通常 bc-*)、cursor_run_id、cloud_phase。v0.8.6+ 起 popola list 默认渲染 runtime 列(在 task_id 与 cli 之间,local/cloud),加 --no-runtime 可隐藏。取消:popola cancel 在云路径走 cancel_run REST,不是 SIGTERM。HITL:若云端任务需要通过 Popola daemon 请人拍板走 Lark,可走 cloud 通道:POST /hitl/cloud/request → block GET /hitl/cloud/wait/{hitl_id} → Lark 侧照旧 first-responder wins → POST /hitl/cloud/answer/{hitl_id}。
SSE 实时拉取(v0.8.6+):popola attach <task_id> --follow 在 runtime=cloud 任务上默认启用 SSE:在 daemon /attach_stream 之外另起一个后台线程 pump Cursor 的 GET /v1/agents/{id}/runs/{run_id}/stream,把 cloud.sse.{assistant,tool_call,result,status,parse_error,stream_expired,dedup_drop} 事件流入同一渲染器。每条 envelope 携带 (task_id, run_id, stream_session_id, sse_id, seq) 五元组用于幂等去重。自动 fallback 到 poll:遇到 CursorCloudStreamExpiredError(HTTP 410 stream_expired)/ httpx.ReadError / httpx.ConnectError / httpx.TimeoutException / 缺 CURSOR_API_KEY / 主线程 Ctrl-C 时,SSE 线程优雅退出并 append 一条 cloud.sse.fallback_to_poll 边界事件 + stderr 一行 [cloud sse] ... 提示(No-Silent-Failures),既有 poll-driven 视图继续工作。强制 legacy 路径:popola attach <id> --follow --no-stream。容忍漂移:cloud_poller 仍是 cloud_phase 的唯一写入者(state SoT 锁),SSE 是 append-only 旁路;SSE-side stream:running 与 poller-side cloud_phase=CREATING 之间最长容忍 ≤3 s 不一致(interval_s + 1s,默认 interval_s=2s)。
云端错误提示(v0.8.6+):v0.8.6 在 cursor_cloud.py 内嵌 16 条 _ERROR_CATALOG 条目,按 (error.code → error.message regex → HTTP status) 优先级派发到 10 个新 CursorCloud*Error 子类,每个都带双语 .hint_en / .hint_zh(每条 ≤2 句、含 ≥1 个 dashboard URL)+ 稳定 .cli_exit 退出码。覆盖:401 unauthorized / api_key_not_found、403 plan_required / role_forbidden / feature_unavailable、404 agent_not_found / run_not_found、409 agent_busy / agent_archived / run_not_cancellable、410 stream_expired、422 三类 GitHub-App 集成错(RepoAllowlistError / GithubAppMissingError / GithubAppPermissionError)、400/422 validation_error、429 rate_limit_exceeded(v0.8.8 完整重试)、5xx internal_error/upstream_error。完整提示文本与重试矩阵见研究备忘 .local/research/v0.8.6_sse/422-error-catalog.md §3(.local/ 仅本地存在,已 gitignore)。
Opt-in quota smoke:导出 CURSOR_API_KEY 后跑 pytest tests/real_cursor_cloud/ -m real_cursor_cloud,否则该目录四项 case 仅 skipped(见 pytest marker real_cursor_cloud)。
出处:.local/research/v0.8.5_cloud_agent/research.md(Option α)+ 00-decision-matrix-zh.md §7;v0.8.6 SSE / 422 / state SoT 设计:.local/research/v0.8.6_sse/{sse-event-schema,state-source-of-truth,422-error-catalog}.md(local-only research notes)。
Workflow 7 — Cloud HITL approval via MCP tool (γ mode, v0.8.7+)
Tier:Enterprise / Self-Hosted。本 workflow 让 Cursor Cloud Agent 在云端跑任务时,遇到高风险决策(删数据、上线变更、生产部署等)能 deferer 给真人 — 经 Lark 卡片走人审批,结果回传给 Cloud Agent 继续执行。先决条件、详细架构与安装步骤详见 docs/USER_GUIDE.md#cloud-hitl-enterprise--self-hosted。
架构(γ — Worker stdio MCP 一等公民模式):
Cursor Cloud Agent (云端) ──tool_call──▶ Self-Hosted Worker
│
│ spawns / pipes stdio
▼
popolaloom-mcp
│
│ HTTP RPC (loopback / VPC)
▼
popolad ──▶ popola_hitl (SQLite)
│
│ outbound HTTPS
▼
open.larksuite.com (Lark 卡片)
│
▼
人审批 → 回写 → Cloud Agent 继续
派发 + 审批一条龙,6 步:
- 先做 Enterprise 准备(一次性,per worker host):
- 在 Cursor Self-Hosted Pool 装 worker(
curl https://cursor.com/install -fsS | bash 然后 agent worker start --pool)。详见 USER_GUIDE Cloud HITL §"Install steps (γ)"。
- 在同 host 装
popolad + popolaloom-mcp(pipx install popolaloom),并 popolad up 让 RPC 绑到 127.0.0.1:<popolad_port>(绝不绑公网接口)。
- 在 Cloud Agents dashboard 注册自定义 MCP server(transport:Command (stdio)),命令
popolaloom-mcp,args [](v0.8.7 入口默认就是 cloud HITL 桥),env 至少含 POPOLAD_BASE_URL + POPOLAD_API_KEY(per-tenant scoped)。env 白名单(per SECURITY L2)由 systemd / launchd unit 配 Environment= + EnvironmentFile= 完成(运维负责),popolaloom-mcp 进程本身不再持有 shell / git / 云 creds。
- 配
popolad.toml [hitl.cloud](一次性,可选 — 默认值已经 ok):
[hitl.cloud]
timeout_seconds = 1800
idempotency_window_s = 3600
max_concurrent_per_run = 1
- 派发一个会用到 HITL 的云端任务(broad-audience cloud dispatch + 让 cloud agent 知道有 HITL 工具可用):
export CURSOR_API_KEY="cr_..."
export LARK_HITL_TARGET_OPEN_ID="ou_xxx"
popola dispatch "迁移生产数据库 schema,遇到 destructive 操作请通过 popolaloom_cloud_hitl_request 求人审批" \
--cli=cursor-cloud \
--cli-flag repo_url=https://github.com/acme/monorepo \
--cli-flag starting_ref=main
- Cloud Agent 在云端 runtime 调 MCP 工具(这一步由 Cloud Agent 自己根据 prompt 与场景判断;典型 tool_call shape 如下,agent 的 LLM 自动构造):
{
"tool_name": "popolaloom_cloud_hitl_request",
"input": {
"task_id": "cursor-cloud-deadbeef",
"cursor_agent_id": "bc-ad-...",
"cursor_run_id": "run-...",
"question_text": "Drop staging.users (10M rows)? This is irreversible.",
"prompt_body": "About to execute: DROP TABLE staging.users; ... full context ...",
"options": ["approve", "reject"],
"responder_policy": "single",
"timeout_s": 1800
}
}
工具内部:(a)走 worker-side popolaloom-mcp → POST /hitl/cloud/request 到 popolad;(b)popolad 写 popola_hitl 行 + 触发 Lark 卡片 cloud_hitl_request_card_v1 推送到 LARK_HITL_TARGET_OPEN_ID;(c)popolaloom-mcp 内部 long-poll GET /hitl/cloud/wait/{hitl_id}?timeout_s=55 一直拉到 30 min 超时为止。
- 真人在 Lark 点 Approve / Reject / Custom(卡片含 verbatim 问题 + 200-字截断 context + Expand 链接 + 三按钮):
- 点 Approve / Reject →
lark/listener.py 接 button event(γ 模式经 lark-cli event consume 长连接,认证由 lark-cli 持有的 bot websocket session 完成 — 无 HMAC 校验在 listener 边界;β 模式才在公网 HTTPS gateway 做 HMAC 校验)→ POST /hitl/cloud/answer/{hitl_id} → bridge.submit_answer(含 expected_cursor_* mis-route 防御)→ mark_answered。
- 点 Custom → 弹
open_input 文本框 → 自由文本回答。
- 30 min 内没人响应 → 工具返回
error.code: "timeout"(显式失败,非默默通过 — per Q-B-3 锁定)。
- Cloud Agent 收到结果继续执行,或在 timeout 时按自己的策略 retry / fail-loud。在本机用
popola attach cursor-cloud-deadbeef --follow 同步看到全过程;用 ~/.popola/events/cursor-cloud-deadbeef.jsonl 审计 cloud_hitl.{requested,answered,failed,transition} 4 类 NDJSON 事件。
关键安全约束(不可省):
- L3 季度轮换 Lark webhook secret(Q1 1/15、Q2 4/15、Q3 7/15、Q4 10/15);γ 与 β 共用同一 secret + 轮换流程。
- L6 Team follow-ups:若组织里多人共用一个 Cloud Agent,禁用 Team follow-ups 或仅让服务账号能跟进 — 否则 teammate B 可能 driver agent 用 user A 的 secret 调 HITL 工具。
- L8 MCP 配置块视为机密:dashboard 里 env / headers 加密+读时屏蔽,但绝不把 JSON blob commit 进 git 或粘贴到聊天。
- L10 Cursor Cloud network access policy:处理 HITL 的 agent 设为 "Allowlist only",仅放行 Egress allowlist 的 host。
- 绝不 用 public IP / port-forward / residential NAT / 入站端口 / VPN 隧道把
popolad 暴露给 Cursor Cloud — 仅 γ 的 outbound HTTPS(worker 主动出站)或 β 的 backend-proxied HTTPS(Cursor backend 代理)是合法路径。详见 docs/known-issues.md §"v0.8.7 — Cloud HITL transport (anti-patterns)"。
β(HTTP MCP backend-proxied 模式)的差异:把第 1 步换成"在 VPC 里搭一个公网 HTTPS gateway,跑 HMAC 校验 + 转发给 popolad",并在 dashboard 里改注册 transport=HTTP 的 MCP server;其余步骤一致。Β 不需要 Cursor Enterprise 套餐,但需要团队自有 SRE 维护 gateway。完整对照见 USER_GUIDE Cloud HITL §"Decision matrix — γ vs β vs neither"。
幂等 + 容灾:同 (task_id, cursor_agent_id, cursor_run_id, question_text) 在 1 h 内重复调,后到的请求短路返回原 hitl_id + deduped: true(不重发卡片,不写新 popola_hitl 行)。popolad 重启不影响 dedup,因为 SQLite 是唯一真相源(无内存 cache)。
出处:v0.8.7 设计与契约见 .local/research/v0.8.7_hitl/{deployment-modes,mcp-tool-contract,lark-card-spec,long-tool-call-probe}.md;安全 gate 见 .local/.agent/active/v0.8.7-cloud-hitl-prod/SECURITY_CHECKLIST.md(10 项 lateral-movement + 4 项 secret hygiene + 4 项 idempotency + 4 项 audit + 3 项 approval policy)。
Workflow 8 — Cross-PR relay (popola relay, v0.8.8+)
⚠️ Q-C-4 deviation:v0.8.8 把 popola relay <task_a> 默认改成 auto-dispatch(偏离默认;详见 RELEASE_NOTES.md 顶部 callout 与 docs/USER_GUIDE.md#cross-pr-relay--popola-relay-v088)。配套 5 项强制缓解:repo allowlist(默认 [] 阻断一切)+ 0o600 audit log + detect-secrets 预扫(6 种 token shape)+ RELEASE_NOTES callout + CI 隔离测试。
把已完成的 cloud task_a 的 PR / branch / summary 接力成新的 cloud task_b,3 步:
- 预演(强烈推荐先
--dry-run):跑一遍完整 policy gate(allowlist + secret scan + size cap),写一行 mode="dry-run" audit row,不发任何 cloud API 请求:
popola relay v088-task-abc --dry-run --json | jq
- 确认 audit 路径 + sha256 后真发(默认就是 auto;同 prompt 同 target 同 idempotency_key 在 1 h 内重复发会短路):
popola relay v088-task-abc
- 跨 org 接力(罕见、需要显式 override):默认
[cloud.relay] repo_allowlist = [] 会阻断任何 target;要想跨 allowlist 必须 --confirm-allowlist,且会在 stderr 打 WARN + 在 audit 行记 gate_decision="override_confirm_allowlist":
popola relay v088-task-abc \
--target-repo https://github.com/external/fork \
--confirm-allowlist
5 项缓解(M1..M5)的快速记忆(详见 USER_GUIDE Cross-PR relay):(1) repo_allowlist 默认 [] 阻断一切;(2) 0o600 append-only audit log,crash 也留 dispatch_inflight 行;(3) detect-secrets 预扫 6 种 shape(AWS/GitHub PAT/Stripe/JWT/Slack/high-entropy),命中即 exit 1 + …<last4> 脱敏;(4) RELEASE_NOTES 顶部 callout(M4 lint 强制);(5) tests/cli/test_relay_safety.py 在默认 CI 走道里跑。要恢复 v0.8.7 「人工确认」默认行为:在 popolad.toml 设 [cloud.relay] mode = "confirm"。
Workflow 9 — popola cloud runs 列出云端 run 历史(v0.8.8+;experimental in v0.9.0)
v0.9.0 GA:popola cloud 子 app 本身 stable,runs verb 在 v0.9.0 仍标 experimental:6 列默认表格布局、--include-events slow-path JSON shape、跨 verb 404→exit 4(vs popola dispatch --cli=cursor-cloud 的 100)可能在 v0.9.x minor 调整(API_STABILITY §3.1;CHANGELOG 会显式记 column / shape 变更)。
popola cloud runs <task_id> 包装 Cursor GET /v1/agents/{id}/runs,按 newest-first 列出该 cloud agent 的全部 run(含手工通过 https://cursor.com/agents 浏览器追加的);popola list 仍保持 single-row-per-task。完整 dispatch → wait → cloud runs 一条龙:
- 派发一个 cloud 任务:
export CURSOR_API_KEY="cr_..."
popola dispatch "重构 caching 模块并补齐 unit test" \
--cli=cursor-cloud \
--cli-flag repo_url=https://github.com/neolix-ai/popola-loom \
--cli-flag starting_ref=main \
--cli-flag model=composer-2.5
- 等任务跑(attach 跟随;多 run 时自动加
[run-N] 前缀 + 分隔行):
popola attach cursor-cloud-deadbeef --follow
- 列出全部 run(默认 6 列表格):
popola cloud runs cursor-cloud-deadbeef
- 要 JSON 写脚本(
--json,full run_id 不截断;分页用 --cursor):
popola cloud runs cursor-cloud-deadbeef --json --limit 100 | jq '.runs[] | .run_id'
- 要 events_summary(
--include-events 慢路径,1 row 多 1 个 GET /runs/{run_id} round-trip):
popola cloud runs cursor-cloud-deadbeef --include-events --json \
| jq '.runs[] | {run_index, state, events_summary}'
和 popola status --verbose 的对比:status --verbose 显示单条最新 run的 5 字段 cost block(cost: n/a + model + wall + link 等;honest disclosure,不编 cost 数字);popola cloud runs 显示全部 run 的可分页历史。错误退出码:task_id 本地未知 → exit 4(per Q-C-1 OQ-1,与 popola dispatch 的 100 退出码不同,CHANGELOG §Changed 显式记录此偏离);401/403 → exit 77(per Q-C-1 OQ-2,对齐 _ERROR_CATALOG);429 / 5xx → exit 75;403 plan_required → exit 78。
出处:wire 级细节见 .local/research/v0.8.8_multi_run/runs-subcommand-spec.md(.local/ 仅本地存在,已 gitignore)。
Workflow 10 — Self-hosted worker handoff (popola cloud worker, v0.9.1+; single-path since v1.6.0)
触发:agent worker / "self-hosted worker" / "My Machines" / "把本机注册到 Cursor 云端"。start / handoff 不创建 popola task id;要 popola-tracked task 用本 workflow 的 dispatch 便捷命令(v1.6.0 起单路径 Path-B JWT,自动设 --cloud-target=self-hosted + --auth-mode=session-jwt)。
v1.6.0 constraint #1:popola cloud worker {start,debug} 不再接受 --pool / --pool-name 标志(My Machines workers 是 popola 唯一支持的模式)。Self-Hosted Pool workers 操作员可继续 agent worker start --pool 直接走上游 CLI;popola 不包装该路径。
5 verb:debug 跑 agent worker debug 预检;start 默认 My Machines(agent login 即可),自动生成 popolaloom-<repo>-<hash> 名称并按 --worker-dir 复用已有进程,--allow-duplicate 才强制开第二份;status 读 /healthz + /readyz + /metrics(loopback only,无需 API key);handoff 输出 prompt + URL 信封,popola_task_id: null;dispatch 直接 POST 到 popolad,预加载 JWT bundle(缺则退 1 并提示 agent login),携带 cli=cursor-cloud、cloud_target=self-hosted、__auth_mode__=session-jwt、worker_name、repo/PR、starting_ref、model extras,把任务路由到当前 workspace worker;派发后 stdout 多打印一行 view: https://cursor.com/agents/<bcId>;--print-only / --dry-run 只输出等价命令。
agent login
popola cloud worker start --worker-dir "$(pwd)" \
--management-addr 127.0.0.1:39231
popola cloud worker status --management-addr 127.0.0.1:39231 --json | jq
popola cloud worker handoff --worker-id <uuid> --prompt "..."
popola cloud worker dispatch "..." --worker-dir "$(pwd)" \
--repo-url https://github.com/acme/repo
完整文档:USER_GUIDE §Self-hosted worker handoff。
Workflow 11 — Dispatch with user preferences (v0.9.10+, experimental)
Repeatable dispatch defaults without shell aliases:
popola init --interactive
popola dispatch "summarize repo state" --use-preferences
popola dispatch "review migration plan" --profile daily-driver --json
Expected: routing/UX defaults only; secrets stay in keyring / CURSOR_API_KEY / 0o600 fallback. Schema experimental until v1.1.1 stable.
Workflow 12 — Path-B self-hosted dispatch (v1.6.0 single path)
v1.6.0 单路径 self-hosted dispatch — --auth-mode=session-jwt 是隐式默认,不要显式传(显式传 --auth-mode=rest 会被拒绝退 2)。Path-B 走 Cursor session JWT + Connect-RPC StartBackgroundComposerFromSnapshot,不查 GitHub/GitLab,每次派发完毕 stdout 多打印一行 view: https://cursor.com/agents/<bcId>:
popola dispatch "feature X" \
--cloud-target=self-hosted --worker-name=<your-worker> \
--model=gpt-5.5 --effort=high --long-running --preset=grind \
--no-auto-branch --no-auto-create-pr --work-on-current-branch \
--cli-flag repo_url=https://github.com/org/repo
派发失败时:401/404 JWT 过期或 worker 不存在 → 让用户跑 agent login 重生成 JWT,或 popola cloud worker start --name <your-worker> --worker-dir <repo-root> 注册 worker;绝不告诉用户回退 --auth-mode=rest(v1.6.0 constraint #5:self-hosted 只有 Path-B 这一条路径)。
Workflow 13 — Guided dispatch with option-group Q&A
Use AskQuestion for target → model → thinking_depth → special_modes, then submit: popola dispatch "..." --wizard locally, or MCP popola_submit({"cli":"cursor-cloud","prompt":...,"cwd":...,"extra":{"model":"gpt-5.5","effort":"high","auto_create_pr":true}}).
Workflow 14 — popola update end-to-end (v1.4.0+)
v1.4.0 GA:popola update 是 install.sh verb_update(lines 502–525)的 Python 等价,flag 矩阵一一对齐(同 --from / --ref / --version / --target / --scope / --with-credentials / --dry-run 等)。区别:install.sh 是 bootstrap 路径(curl | bash 在 Python 都没装的时候用);popola update 是 post-install 路径(wheel 装好后操作员日常用)。两者契约由 tests/test_update_parity.py 锁定。
5 步典型用法:
- 预演(强烈推荐先
--dry-run):检测 install kind(regular / editable / pipx)、解析 pip spec、列出 (target × scope) 矩阵 + doctor 探针,不调 pip / 不写 skill:
popola update --dry-run --json | jq
- 真发(默认
--target=all --scope=both --from=git,跟踪 main):
popola update
- 锁版本(PyPI):
popola update --from=pypi --version=1.4.0
- 跳过 pip(只刷 skill):
popola update --no-skills
popola update --no-doctor
- 拒绝 unsafe install:在 editable 安装(
pip install -e)或 pipx-managed venv 上运行 popola update 会 fail-loud 退 2 并打印 remediation hint:
- editable →
git pull && popola skill upgrade --target=all --global --project
- pipx →
pipx upgrade popolaloom
- 真要强行覆盖:传
--force(会留下 stale .pth 或 break pipx pin tracking — 自负风险)。
daemon 重启注意:升级完 wheel 后若 popolad.sock 还在,popola update 不会自动重启 daemon(in-flight tasks 会被打断),但会在 stderr 打 warn: 提示让你手动 popola popolad stop && popola popolad start。
退出码:0 干净;1 pip 失败 / spec 校验失败;2 editable / pipx 拒绝;3 doctor 在升级后仍报 DRIFT/MISS(罕见,通常是文件系统权限问题)。
Configuration
v1.1.1 preferences are nested under eight sections: [user_preferences.routing], [user_preferences.defaults], [user_preferences.cursor], [user_preferences.cursor-cloud], [user_preferences.claude], [user_preferences.codex], [user_preferences.lark], and [user_preferences.dispatch]. Legacy flat [user_preferences] keys auto-migrate to schema_version = 2; use dotted writes such as popola init prefs --set cursor-cloud.model=composer-2.5 --set lark.notify_on_completed=false.
No-Silent-Fallback invariant (v1.5.0+, single-path tightened in v1.6.0)
popola does NOT switch the dispatched CLI adapter, auth-mode, or path-B knob without explicit operator consent. Four dispatch-defining dimensions hard-fail on rejection rather than silently retry on another value:
| Dimension | Failure semantics (v1.6.0) |
|---|
--auth-mode (rest / session-jwt) | --cloud-target=self-hosted forces session-jwt (the implicit default; explicit --auth-mode=rest exits 2). Managed cloud keeps rest default. JWT load failure on the self-hosted path → exit 1; popola NEVER auto-switches to REST. |
--cli (cursor / cursor-cloud / claude / ...) | requested adapter missing → exit 1; [user_preferences.routing].fallback_chain is consulted ONLY when --allow-fallback is passed explicitly AND --cloud-target is NOT self-hosted. With --cloud-target=self-hosted, --allow-fallback is a no-op + bilingual WARN (v1.6.0 constraint #2). |
--cloud-target (self-hosted / cursor-managed) | self-hosted worker absent → exit 78, no swap to cursor-managed. self-hosted dispatch skips the Cursor GitHub-App preflight (v1.6.0 constraint #3 — worker holds its own clone) AND the daemon rejects extra.env.type='pool' for self-hosted (v1.6.0 constraint #1). |
--model | model id rejected upstream → exit 1; no auto-rename to default (operator opts in via --cli-flag model_id_override=<id>). --preset (grind / quick-fix / ...) parse failure → exit 1; preset is NEVER silently stripped. |
Out of scope (allowed to fall back without consent): the SSE → poll observability fallback emitted by cloud_poller.py (event cloud.sse.fallback_to_poll). This sits at the observability layer — the task has already been dispatched; only the event-stream read mode degrades. Dispatch-routing decisions are governed by this invariant; observability-stream choices are not.
Operator-facing escape hatches when the upstream server rejects the default v1.6.0 shape (each requires an explicit opt-in; none of them swap self-hosted to a different transport):
| Symptom | Opt-in command |
|---|
| Server returns "model 'gpt-5.5' not available" | popola dispatch ... --cli-flag model_id_override=gpt-5.5-high (or =gpt-5.5 per attempt) |
--cli=cursor unavailable for managed cloud / local dispatch, want fallback_chain | popola dispatch ... --allow-fallback (per-dispatch opt-in; not persisted; no-op for --cloud-target=self-hosted) |
popolad env injection (v1.5.0+)
popola popolad start walks a 4-tier injection chain to build the daemon child env (so the daemon's cloud dispatches pick up CURSOR_API_KEY even when the operator's current shell didn't export it):
- existing
os.environ (operator's shell — highest precedence; never overwritten)
--env-file <path> (Typer flag; mode 0o600 required)
~/.popola/cursor_api_key.env (existing v0.9.9 boot-time fallback; mode 0o600)
<cwd>/.local/.secrets/cursor_user_api_key.secret (single-line bare key; CLI-side env injection ONLY, does NOT enter resolve_cursor_api_key precedence chain — JWT remains the dispatch primary path)
<cwd>/.env (mode 0o600; legacy dotenv fallback)
popola popolad start --reload-env is a convenience equivalent for stop && start <same flags>; use it after editing one of the file sources to push the new env into a running daemon.
Path-B self-hosted worker dispatch (v1.6.0 single canonical path)
JWT-direct dispatch onto a registered self-hosted worker, skipping git host (GitHub / GitLab) authentication. v1.6.0 makes this the ONLY supported self-hosted path (feedback_for_v1.5.2.md constraints #1–#6); --auth-mode=session-jwt is the implicit default, --pool is removed from popola cloud worker {start,debug}, and --allow-fallback is a no-op + WARN when --cloud-target=self-hosted:
popola dispatch \
--cloud-target=self-hosted --worker-name=<your-worker> \
--model=gpt-5.5 --thinking-level=high --preset=grind \
--no-auto-branch --no-auto-create-pr --work-on-current-branch \
--cli-flag model_id_override=gpt-5.5-high \
"<prompt>"
Note --cli-flag model_id_override=gpt-5.5-high: Cursor's path-B server rejects bare gpt-5.5 when long_running_agent_mode=true (grind preset) with "Model 'gpt-5.5' does not support long-running agent mode"; the -high suffix is the cursor-agent CLI form that the path-B accepts.
Verifying a self-hosted dispatch (v1.6.0)
After popola dispatch ... --cloud-target=self-hosted exits 0, the CLI surfaces two lines on stdout for web-side observability:
self-hosted-feedf00d
view: https://cursor.com/agents/bc-1a2b3c4d-5e6f-7890-abcd-ef1234567890
The view: URL points at Cursor's web UI for the just-dispatched agent — operators can click through to read the workspace contents the worker checked out, watch tool calls live, and inspect the terminal state. To follow events locally too, run popola attach <task_id> --follow in another shell.
If the daemon hasn't emitted cloud.queued within ~2 s (e.g. when the JWT load fails or the worker is unreachable) the CLI prints a bilingual stderr WARN (warn: dashboard_url not surfaced ...) and falls back to the task_id-only line — popola attach <task_id> --follow will show the actual failure cause as soon as the daemon writes task.failed. Per the No-Silent-Failures rule, the URL surface is NEVER silently skipped.
⚠️ Cursor server-side env=machine→pool downgrade (upstream regression, carried into v1.6.0)
Cursor's path-B Connect-RPC StartBackgroundComposerFromSnapshot SILENTLY downgrades env={"type":"machine","name":X} to env={"type":"pool"} server-side. Pool routing picks ANY free worker in your private pool that matches the repo — NOT necessarily the named worker. Empirically verified 2026-05-17 against api2.cursor.sh:
- Request body:
env={type:"machine",name:"popolaloom-dev-worker-v15"}
- Cursor's response (
GET /v1/agents/<bcId>): env={"type":"pool"} — name dropped
popola CANNOT fix this server-side routing — constraint #1 is satisfied at the popola layer (the worker process is My Machines only and the daemon rejects extra.env.type='pool' for self-hosted), but if your account hosts multiple workers Cursor may route the dispatch to a sibling that matches the repo. Operators running ONE worker per repo are unaffected. See docs/known-issues.md for the live upstream-regression log.
⚠️ Cursor REST is_in_use / lastActivityAt always null for named workers (v1.5.1+ empirical)
Cursor's GET /v0/private-workers returns is_in_use=null, active_bc_id=null, AND lastActivityAt=null for a named self-hosted worker EVEN WHILE the agent is mid-run (agent.status=ACTIVE, popola-side cloud_phase=RUNNING). The fields never flip to a non-null value during the agent's lifetime, so treating them as a worker-liveness oracle leads to false-positive "worker free" verdicts and double-dispatch races. Empirically verified 2026-05-18 against api.cursor.com with popolaloom-dev-worker-v15: 5 mid-run snapshots at 30 s intervals (across both running and completed task states) ALL reported {is_in_use: null, active_bc_id: null, lastActivityAt: null} even though the popola-side task definitively reached cloud_phase=RUNNING and then cloud_phase=FINISHED end-to-end.
Replacement oracle (G3). Use the dual signal:
agent.env from GET /v1/agents/<bc-id> — for a dispatch routed at a named worker, the response contains env: {"type": "machine", "name": "<your-worker>"} and this assignment is durable for the agent's lifetime. (NOTE: agent.target is null in this response shape; the routing target lives in the env field instead.)
- Prometheus metric
cursor_self_hosted_worker_last_activity_unix_seconds exposed by the worker's own --management-addr /metrics endpoint.
Treat a worker as free only when now - cursor_self_hosted_worker_last_activity_unix_seconds > 60 AND no in-flight agent (any bc-* from your GET /v1/agents) has env.type == "machine" + env.name == "<X>". Do NOT poll is_in_use for routing decisions.
⚠️ Cursor REST model_details=null for path-A agents (v1.5.1+ empirical)
GET /v1/agents/<bc-id> returns model_details=null for agents created via path-A REST POST /v1/agents, even when the create call passed a non-default model (e.g. gpt-5.5 with --cli-flag model_id_override=gpt-5.5-high). The model_details field is populated only for path-B (StartBackgroundComposerFromSnapshot) sessions, so any post-dispatch model-validation logic that consults model_details will mis-classify path-A agents as "model unset" and either falsely retry or falsely reject. Empirically verified 2026-05-17 against api2.cursor.sh for an agent dispatched with --auth-mode=rest --model=gpt-5.5 --cli-flag model_id_override=gpt-5.5-high.
Replacement oracle (G5). Confirm model wiring not by the model_details field but by the agent's terminal-state outcome: a path-A agent reached the correct model when it transitions to a terminal state without raising the Cursor server error Model '<X>' does not support long-running agent mode. Equivalently, scan the agent's run-event NDJSON log for that exact error string; absence on terminal = success. Do NOT gate dispatch retry on model_details for path-A.
PopolaLoom 用环境变量做配置(per ADR — 显式优于隐式);下表是常用项:
| Env var | Purpose | Default |
|---|
POPOLA_HOME | daemon socket / events / sqlite / pid 根目录 | ~/.popola/ |
LARK_HITL_TARGET_OPEN_ID | Lark HITL 卡片 / 终态通知收件人 open_id | (unset → Lark 静默) |
LARK_NOTIFY_TARGET_OPEN_ID | 终态通知收件人(独立于 HITL) | fallback to LARK_HITL_TARGET_OPEN_ID |
LARK_NOTIFY_ON_COMPLETED | task.completed 时发 Lark 卡(v0.4.1+) | 1 (ON) |
LARK_NOTIFY_ON_FAILED | task.failed 时发 Lark 卡(v0.4.1+) | 1 (ON) |
LARK_NOTIFY_ON_CANCELED | task.canceled 时发 Lark 卡(v0.4.1+) | 1 (ON) |
LARK_NOTIFY_ON_CANCEL_ESCALATED | SIGKILL 升级时单独发卡(v0.4.1+) | 0 (OFF) |
LARK_NOTIFY_PROMPT_TRUNCATE | 卡片 body 内 prompt 字符上限 (50–2000) | 200 |
CODEX_HOME | Codex skill / config 目录 | ~/.codex/ |
POPOLA_USE_GRAPH | 是否启用 LangGraph 子图(v0.3.0+) | 1 |
LARK_PRIORITY_BOT_ID | 出口卡用哪个 bot 发(multi-bot setup) | (unset → 默认 bot) |
CURSOR_API_KEY | Cursor Cloud Agents REST Basic 用户名 (= API key),密码空 | (unset → --cli=cursor-cloud 不可用 / CursorCloudAdapter.is_available()==False) |
User preferences (v0.9.10+, experimental)
[user_preferences] is an experimental until v1.1.1 stable routing/profile schema. Typical keys: default_cli, default_cwd, confirm_before_cloud, prefer_streaming, handoff_tags, and [user_preferences.dispatch] timeout_seconds / extra_cli_flags. Never store secrets here.
[user_preferences]
default_cli = "cursor"
confirm_before_cloud = true
[user_preferences.dispatch]
timeout_seconds = 120
Lark gating:当 lark-cli 不在 PATH 或 LARK_HITL_TARGET_OPEN_ID unset 时,daemon 静默退化为只发本地事件 + 写本机 NDJSON(不抛异常,per the No Silent Failures + degrade-gracefully 双约束)。
Reference
Version + upgrade