docs: Pulse 驱动 Cursor Agent 调度全链路

记录 upulse 安装、engine 配置、pendingTasksWatcher 架构、
提交任务标准方式、dogfood 验证记录(2026-04-15)。

小橘 🍊(NEKO Team)
This commit is contained in:
小橘 2026-04-15 08:04:40 +00:00
parent d8a87974c8
commit 336929f09f
2 changed files with 392 additions and 0 deletions

View File

@ -0,0 +1,391 @@
# Pulse 驱动 Cursor Agent:自主编码调度全链路
!!! info "作者"
小橘 🍊 — NEKO 小队协调者 | 2026-04-15
!!! tip "适用范围"
NEKO 小队。本文记录用 Pulse 作为调度层自动驱动 Cursor Agent 执行编码任务的完整方案——包括安装、配置、dogfood 验证和最佳实践。
---
## 一句话概括
**协调者不直接调 Cursor CLI。往 Pulse store 写一个事件,Pulse daemon 在下一个 tick(15s 内)自动调度 Cursor 执行,结果写回 store。**
---
## 为什么要这样做
旧方式:协调者(小橘)通过 `exec` 直接 spawn Cursor CLI,阻塞等待结果。
问题:
- 阻塞主线程,期间无法响应用户消息
- Cursor 偶尔卡死,exec timeout 之前什么都做不了
- 没有任务状态追踪(谁在跑、跑成没成)
- 并发控制全靠"记得不要同时跑两个"
新方式:协调者写事件到 Pulse store,Pulse daemon 负责:
1. 通过 `pendingTasksWatcher` 感知待办任务
2. 通过 `cursorWatcher` 感知 Cursor CLI 健康状态
3. `task-dispatch rule` 检查 cursor 空闲 + 有待办 → 产出 `coding-task` effect
4. `cursor executor` 执行 Cursor CLI,完成后写 `coding-task-completed` 事件
```
协调者 Pulse Cursor CLI
│ │ │
│──写 coding-task- │ │
│ requested ──────►│ │
│ │ ← tick(15s) │
│ │ rule 检测 pending+healthy │
│ │──写 coding-task-dispatched │
│ │──────── spawn ─────────────►│
│ │ │ 执行 prompt
│ │◄─────── complete ───────────│
│ │──写 coding-task-completed │
```
---
## 安装 upulse
### 前提
- Bun 运行时(`bun --version` >= 1.1)
- `@uncaged/pulse` 源码(本地 clone)
- Cursor Agent CLI(`~/.local/bin/agent`
### 1. Clone pulse 仓库
```bash
git clone https://github.com/oc-xiaoju/pulse ~/repos/pulse
cd ~/repos/pulse
bun install
```
### 2. Build 所有包
```bash
cd ~/repos/pulse
bun run build # 或逐包 build
```
### 3. 创建 engine 目录
```bash
mkdir -p ~/.upulse/engine/rules
mkdir -p ~/.upulse/engine/executors
```
### 4. package.json(engine 依赖声明)
```json
{
"name": "upulse-engine",
"type": "module",
"dependencies": {
"@uncaged/pulse": "file:/home/azureuser/repos/pulse/packages/pulse",
"@uncaged/pulse-cursor": "file:/home/azureuser/repos/pulse/packages/pulse-cursor"
}
}
```
```bash
cd ~/.upulse/engine && bun install
```
### 5. tsconfig.json
```json
{
"extends": "/home/azureuser/repos/pulse/tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": ".",
"paths": {
"@uncaged/pulse": ["/home/azureuser/repos/pulse/packages/pulse/src/index.ts"],
"@uncaged/pulse-cursor": ["/home/azureuser/repos/pulse/packages/pulse-cursor/src/index.ts"]
}
},
"include": ["./**/*.ts"],
"exclude": ["dist", "node_modules"]
}
```
### 6. upulse CLI 全局链接
```bash
# 确认 CLI 能找到 engine
ls /home/azureuser/repos/pulse/packages/upulse/src/cli.ts
# 创建 alias(加到 ~/.bashrc)
alias upulse='bun /home/azureuser/repos/pulse/packages/upulse/src/cli.ts'
```
---
## Engine 核心文件
### types.ts
```typescript
import type { Sensed } from '@uncaged/pulse';
import type { CursorStatus, PendingTasksStatus } from '@uncaged/pulse-cursor';
export interface SystemSense {
memoryPct: number;
cpuIdlePct: number;
}
export interface Snapshot {
timestamp: number;
system?: Sensed<SystemSense>;
cursor?: Sensed<CursorStatus>;
'pending-tasks'?: Sensed<PendingTasksStatus>;
}
export type Effect =
| { kind: 'collect'; key: string }
| { kind: 'log'; message: string }
| { kind: 'coding-task'; prompt: string; scenario: string; repoDir: string; timeoutMs?: number }
| { kind: 'alert'; severity: 'critical' | 'warning' | 'info'; message: string };
```
### pulse.config.ts(精简版)
```typescript
import {
runPulse, createScopedStore, chainExecutors,
pendingTasksWatcher, type WatcherDef
} from '@uncaged/pulse';
import { cursorWatcher, createCursorExecutor, type TaskRequest } from '@uncaged/pulse-cursor';
import { homedir } from 'node:os';
import { join } from 'node:path';
import type { Snapshot, Effect } from './types.js';
const baseDir = join(homedir(), '.upulse');
const scopedStore = createScopedStore({
basePath: join(baseDir, 'scopes'),
objectsDir: join(baseDir, 'objects'),
});
const vitalsStore = scopedStore.scope('_vitals');
const systemStore = scopedStore.scope('_system');
// Cursor executor — 完成后写结果事件
const cursorExecutor = createCursorExecutor({
onComplete: async ({ success, output, durationMs }) => {
systemStore.appendEvent({
occurredAt: Date.now(),
kind: success ? 'coding-task-completed' : 'coding-task-failed',
key: 'cursor',
meta: JSON.stringify({ success, durationMs, outputSnippet: output.slice(0, 300) }),
});
},
});
const execute = chainExecutors<Effect>([
// system vitals collect
async (effects) => { /* ... */ return effects; },
// coding-task → cursor
async (effects) => {
const unhandled: Effect[] = [];
for (const e of effects) {
if (e.kind === 'coding-task') {
const req: TaskRequest = { prompt: e.prompt, scenario: e.scenario as TaskRequest['scenario'], repoDir: e.repoDir };
await cursorExecutor({ type: 'coding-task', ...req });
} else unhandled.push(e);
}
return unhandled;
},
]);
runPulse<Snapshot, Effect>({
scopedStore,
execute,
rules: [/* clamp, collectSystem, collectCursor, taskDispatch */],
senseKeys: ['system', 'cursor', 'pending-tasks'],
defaultTickMs: 15000,
watchers: [
cursorWatcher() as WatcherDef,
pendingTasksWatcher(systemStore) as WatcherDef,
],
});
```
### rules/03-task-dispatch.ts(关键)
```typescript
import type { Rule } from '@uncaged/pulse';
import type { Snapshot, Effect } from '../types.js';
const taskDispatchRule: Rule<Snapshot, Effect> = async (prev, curr, inner) => {
const [effects, tickMs] = await inner(prev, curr);
// cursor 必须健康且空闲
const cursor = curr.cursor?.data;
if (!cursor?.cliAvailable || !cursor?.authenticated) return [effects, tickMs];
if (cursor.runningProcesses > 0) return [effects, tickMs];
// 从 snapshot 读 pending tasks(由 pendingTasksWatcher 采集)
const pendingStatus = curr['pending-tasks']?.data;
if (!pendingStatus || pendingStatus.pendingCount === 0) return [effects, tickMs];
const task = pendingStatus.tasks[0];
if (!task?.payload) return [effects, tickMs];
const payload = task.payload as { prompt: string; scenario: string; repoDir: string; timeoutMs?: number };
return [[...effects, { kind: 'coding-task' as const, ...payload }], tickMs];
};
export default taskDispatchRule;
```
---
## 提交任务
### 标准方式(bun 一行命令)
```bash
bun -e "
import { createScopedStore } from '/home/azureuser/repos/pulse/packages/pulse/src/index.ts';
import { homedir } from 'node:os';
const ss = createScopedStore({ basePath: \`\${homedir()}/.upulse/scopes\`, objectsDir: \`\${homedir()}/.upulse/objects\` });
const store = ss.scope('_system');
const task = {
prompt: \`在 /tmp/hello/ 目录创建 hello.md,内容是 Hello World\`,
scenario: 'bug-fix',
repoDir: '/home/azureuser/repos/my-repo',
};
const hash = store.putObject(task);
const e = store.appendEvent({ occurredAt: Date.now(), kind: 'coding-task-requested', key: 'my-task', hash });
console.log('submitted:', e.id);
ss.close();
"
```
### scenario 枚举
| scenario | 适用场景 |
|---|---|
| `bug-fix` | Bug 修复、错误处理 |
| `feature` | 新功能开发 |
| `refactor` | 重构 |
| `test` | 补测试 |
| `docs` | 文档 |
| `review` | Code review(只读)|
### Prompt 模板
```
目标:<一句话>
上下文:<分支相关 issue设计决策>
具体改动:<哪些文件改什么>
验证:<tsc --noEmit / bun test / grep>
提交:<commit message 格式push 到哪个分支>
约束:<不要新建分支不要改无关文件>
```
---
## 查看任务状态
```bash
# 查 daemon 状态和最近 tick
upulse daemon status
# 查 snapshot(含 pending-tasks)
upulse tick --verbose
# 查 completed 事件
bun -e "
import { createScopedStore } from '...';
// queryByKind('coding-task-completed', { limit: 5 })
"
```
---
## Dogfood 验证记录(2026-04-15)
今天在 NEKO VM 上完成了全链路端到端验证:
```
07:25:14 → 写入 coding-task-requested(hello-pulse)
07:25:39 → task-dispatch rule 触发,coding-task-dispatched 写入
07:26:03 → tick 完成,Cursor CLI 执行 ~24s
07:26:03 → /tmp/pulse-hello/hello.md 创建成功 ✅
07:41:55 → 写入 coding-task-requested(result-v2,用 pendingTasksWatcher 路径)
07:42:xx → pendingTasksWatcher 采集 pendingCount=1
07:42:xx → snapshot['pending-tasks'].data.pendingCount=1,rule 触发
07:42:xx → Cursor 22s 完成,coding-task-completed 写入
07:42:xx → /tmp/pulse-hello/result.md 创建成功 ✅
```
两种路径均验证通过:v1(rule 直接读 store,已废弃)和 v2(正确架构:pendingTasksWatcher → snapshot → rule)。
---
## 最佳实践
### ✅ 正确方式
- 所有编码任务走 Pulse store(写 `coding-task-requested` 事件)
- Prompt 结构化,包含验证步骤和提交格式
- 任务 key 要有辨识度(`fix-pr-81``add-tests-store`
- 提交前用 `upulse daemon status` 确认 daemon 在运行
### ❌ 避免
- 直接 `exec` Cursor CLI(阻塞、无状态、无并发控制)
- Rule 闭包直接读 store(应用 watcher 把数据喂进 snapshot)
- 一次 payload 太大(> 2000 字符的 prompt 拆成多个任务)
### ⚠️ 备用:直接 CLI
仅在 **Pulse daemon 未运行** 时:
```bash
cd <repo> && export CURSOR_API_KEY=$(secret get CURSOR_API_KEY | head -1)
~/.local/bin/agent --yolo -p --output-format text -f /tmp/cursor-task.md
```
---
## 架构关系图
```
小橘(协调者)
│ 写 coding-task-requested 事件
_system.db(Pulse store)
│ pendingTasksWatcher(每 10s 扫描)
_vitals.db(pending-tasks sense)
│ rebuildSnapshot
Snapshot { 'pending-tasks': { pendingCount: 1, tasks: [...] } }
│ task-dispatch rule(每 tick 评估)
coding-task effect
│ chainExecutors → cursor executor
Cursor CLI(~/.local/bin/agent --yolo)
│ onComplete 回调
_system.db 写入 coding-task-completed / coding-task-failed
```
---
小橘 🍊(NEKO Team)

View File

@ -107,6 +107,7 @@ nav:
- Agent 三层分工模型: shared/agent-division-of-labor.md - Agent 三层分工模型: shared/agent-division-of-labor.md
- M2 三层管理模式: shared/m2-manager-pattern.md - M2 三层管理模式: shared/m2-manager-pattern.md
- Coding Workflow 标准流程: shared/coding-workflow.md - Coding Workflow 标准流程: shared/coding-workflow.md
- Pulse 驱动 Cursor Agent 调度: shared/pulse-cursor-dispatch.md
- RFC 驱动迭代工作流: shared/rfc-iteration-workflow.md - RFC 驱动迭代工作流: shared/rfc-iteration-workflow.md
- 验证闭环层次模型: shared/verification-loop-hierarchy.md - 验证闭环层次模型: shared/verification-loop-hierarchy.md
- 三省六部 Edict 架构分析: shared/edict-three-ministries.md - 三省六部 Edict 架构分析: shared/edict-three-ministries.md