【PlanWeave】核心架构深挖:文件即节点、Block 即 SOP 的可恢复 Harness 引擎

引子

2026 年的 Coding Agent 已经过了”单次对话 + 一次性 tool call”的阶段。Claude Code、Codex、OpenCode、Pi、Grok 等 5+ 主流 Agent 都能独立完成单文件修改,但长循环、可恢复、多 Agent 协作仍然是公开难题:

当 Agent 要跑 6 小时、200 次 tool call、跨 5 个子任务时,如何保证:(1) 任何一个 sub-agent 挂了能恢复;(2) review 反馈能精准送回 implementation;(3) 不同 block 能路由到不同 executor(Codex 跑实现 + Claude Code 跑 review + 本地脚本跑 lint)?

GaosCode/PlanWeave 给出了答案:把”长循环工程化”建模为 Task Graph + Block 文件系统 + SQLite operation_log。这个项目 333⭐,2026-05-24 创建,最新 commit 在 2026-08-12 —— 2 个半月的密集迭代。它用一个 monorepo(cli + runtime + server + desktop + agent-host + collaboration-protocol)实现了:

  • Task Graph(任务图):Project → Canvas → Task → Block(implementation / review)四层 DAG
  • Per-block executor routing:同一个 Task 的 B-001 路由到 Codex、R-001 路由到 Claude Code、B-002 路由到 OpenCode —— 不同 block 用不同 agent
  • SQLite operation_log:所有 graph 修改有 before/after JSON + undone_at 字段,支持撤销/重做
  • Advisory mkdir lock:跨进程文件锁保证并发 claim 不冲突(不需要 Redis/ZooKeeper)
  • 7 个 SKILL.md:coordinator / runner / reviewer / recovery / maker / importer / auditor —— 把”调度 vs 实现”职责切干净

Harness 6 件套定位:

  • Workflow 组件(接力赛协议 / 交接规则)—— 主导
  • Sub-Agent 组件(角色分工 / Context 隔离)—— 强相关
  • MCP 组件 —— ChatGPT 入口
  • Skill 组件 —— 7 SKILL.md

本文深度剖析 PlanWeave 的:

  • 4 层 DAG 任务图与文件即节点的设计哲学
  • Advisory mkdir lock 与 AsyncLocalStorage 同进程重入
  • operation_log 表 与 undo/redo 机制
  • 5 类 Executor 路由(Codex / Claude Code / OpenCode / Pi / Grok + local)
  • 7 SKILL.md 角色分工(coordinator / runner / reviewer / recovery 等)
  • 同类型项目横评:LoopX / Ralph Loop / Karpathy autoresearch / OpenHands

项目定位与核心价值

一句话定义

PlanWeave = 文件即节点(Tasks / Blocks = Markdown files)+ Task Graph(DAG)+ SQLite 撤销日志 + 5 Agent executor routing + Skills-based 协调员,让 Coding Agent 在 6 小时、200 次 tool call、5 个 executor 的长循环中每个 step 都可恢复、每个 feedback 都能闭环。

能力矩阵

能力范围
任务模型Project → Canvas → Task → Block (implementation / review)
Block 类型implementation / review
Executor 路由Codex / Claude Code / OpenCode / Pi / Grok / manual / local script
持久化SQLite (plangraph.sqlite) + JSON state + 文件 prompt
并发锁Advisory mkdir lock(无中心化依赖)
Undo/Redooperation_log 表 + graph_version_before / graph_version_after
协作Electron Desktop + WebSocket Canvas Live Sync + Remote Agent Hosts
入口协议MCP Server(HTTP loopback)+ ChatGPT Tunnel + systemd
Skill 角色7 SKILL.md(coordinator / runner / reviewer / recovery / maker / importer / auditor)
LicenseMIT

仓库统计

指标数值
⭐ Stars333
🍴 Forks~30
📦 Size2205 个文件(含测试)
🚀 主语言TypeScript (100% monorepo)
🕐 Created2026-05-24
🕐 Last Push2026-08-12
📁 monorepocli + runtime + server + desktop + agent-host + agent-host-protocol + collaboration-protocol + plangraph
📜 LicenseMIT

与”传统 chat-based 规划”的根本差异

PlanWeave 的核心理念用一句话概括:

Chat is a useful place to start a plan, but it is a fragile place to run a long engineering loop.

Chat 的问题不是不能跑 Agent,而是 6 小时后:

  • 你不知道哪些 step 跑过、哪些没过
  • 你不知道第 53 次 tool call 时 agent 是不是已经偏离主线
  • Reviewer 给的反馈找不到 Implementation 的对应 ref
  • Executor 崩溃后无法 resume

PlanWeave 把这一切搬到文件 + SQLite,于是断电恢复、跨设备续跑、跨 Agent 协作都成为可能。

整体架构

PlanWeave 是一个 7 包 monorepo,每个包各司其职:

flowchart TB
    subgraph A["🎯 入口层 (Entry)"]
        CLI["💻 CLI<br/>@planweave-ai/cli"]
        DESKTOP["🖼️ Electron Desktop<br/>Canvas 视觉化"]
        MCP["🔌 MCP Server (HTTP loopback)<br/>+ ChatGPT Tunnel"]
    end

    subgraph B["🧠 协调层 (Skills × 7)"]
        S_COORD["📋 plan-coordinator<br/>主调度"]
        S_RUN["⚙️ plan-runner<br/>实现 worker"]
        S_REV["🔍 plan-reviewer<br/>Review gate"]
        S_REC["🚑 plan-recovery<br/>异常恢复"]
        S_MAKE["✏️ plan-maker<br/>规划起草"]
        S_IMP["📥 plan-importer<br/>从 PRD 导入"]
        S_AUD["✅ plan-auditor<br/>计划体检"]
    end

    subgraph C["⚙️ 执行层 (Runtime)"]
        CLAIM["🎯 ClaimScheduler<br/>claimNext/claimTask"]
        AUTORUN["🚀 AutoRun Loop<br/>claim → execute → submit"]
        EXEC["🔧 Executor Adapters<br/>ACP + local script"]
        PROMPT["📜 PromptRenderer<br/>多源拼接"]
    end

    subgraph D["📦 领域层 (Plangraph)"]
        GRAPH["🕸️ CompiledExecutionGraph<br/>task/block DAG"]
        PROJECT["🏗️ ProjectGraph<br/>Canvas 间依赖"]
        SQLITE["💾 plangraph.sqlite<br/>operation_log + projections"]
    end

    subgraph E["🤖 Executor Runtimes"]
        E_COD["💎 Codex ACP"]
        E_CC["🧠 Claude Code ACP"]
        E_OC["🌐 OpenCode ACP"]
        E_PI["π Pi Agent ACP"]
        E_GR["⚡ Grok Build ACP"]
        E_LOC["🔨 Local script<br/>(lint/test)"]
    end

    subgraph F["🌐 协作层 (Server + Agent Host)"]
        SERVER["🖥️ PlanWeave Server<br/>WebSocket Live Sync"]
        HOST["🖥️ Agent Host<br/>远程设备 Agent 暴露"]
        PROTO["📡 Collaboration Protocol<br/>(WebSocket)"]
    end

    subgraph G["💽 文件层 (Workspace)"]
        STATE["📄 state.json<br/>currentRefs/blocks/feedback"]
        RESULTS["📂 results/<br/>report.md / review-result.json"]
        PROMPTS["📝 prompts/<br/>T-001/prompt.md + B-001.prompt.md"]
        LOCK["🔒 .planweave.lock/<br/>advisory mkdir"]
    end

    A --> B
    B --> C
    C --> D
    C --> E
    C --> G
    B --> G
    D --> G
    F --> C
    F --> E

    style A fill:#C7CEEA,stroke:#888,color:#333
    style B fill:#E8D5F5,stroke:#888,color:#333
    style C fill:#FFDAB9,stroke:#888,color:#333
    style D fill:#FFF9C4,stroke:#888,color:#333
    style E fill:#B5EAD7,stroke:#888,color:#333
    style F fill:#FFB3C6,stroke:#888,color:#333
    style G fill:#F5F5F5,stroke:#888,color:#333

7 层职责分离:

  1. 入口层:CLI / Electron Desktop / MCP Server 三种用户触点
  2. 协调层:7 个 SKILL.md 文件,coordinator 是唯一可发号施令的(其他都是 worker 角色)
  3. 执行层:ClaimScheduler(抢占调度)+ AutoRun(自动循环)+ Executor Adapters(5 类 agent 适配)+ PromptRenderer(多源拼接)
  4. 领域层:CompiledExecutionGraph(DAG in-memory)+ ProjectGraph(多 Canvas)+ SQLite operation_log
  5. Executor Runtimes:5 个真实 Agent + local script
  6. 协作层:Server + Agent Host + WebSocket Protocol
  7. 文件层:state.json + results/ + prompts/ + .planweave.lock

核心架构一:Task Graph(文件即节点)

数据模型:4 层 DAG

PlanWeave 的核心数据是Project → Canvas → Task → Block 4 层有向无环图。每个节点都是 Markdown 文件,每个边都是 manifest.json 里的 JSON 字段。

flowchart LR
    P["🏗️ project-graph.json<br/>(Canvas 列表)"] --> C1["📋 Canvas 1<br/>manifest.json"]
    P --> C2["📋 Canvas 2<br/>manifest.json"]
    C1 --> T1["📌 T-001<br/>Implement API"]
    C1 --> T2["📌 T-002<br/>Write Tests"]
    C2 --> T3["📌 T-003<br/>Deploy"]
    T1 --> B1["⚙️ B-001<br/>implementation"]
    T1 --> B2["🔍 R-001<br/>review"]
    T2 --> B3["⚙️ B-002<br/>implementation"]
    T3 --> B4["⚙️ B-003<br/>implementation"]
    B1 -->|depends_on| B2
    B2 -->|feedback| B1
    B3 -->|cross_canvas_edge| B1

    style P fill:#E8D5F5,stroke:#888,color:#333
    style C1 fill:#C7CEEA,stroke:#888,color:#333
    style C2 fill:#C7CEEA,stroke:#888,color:#333
    style T1 fill:#FFF9C4,stroke:#888,color:#333
    style T2 fill:#FFF9C4,stroke:#888,color:#333
    style T3 fill:#FFF9C4,stroke:#888,color:#333
    style B1 fill:#B5EAD7,stroke:#888,color:#333
    style B2 fill:#FFB3C6,stroke:#888,color:#333
    style B3 fill:#B5EAD7,stroke:#888,color:#333
    style B4 fill:#B5EAD7,stroke:#888,color:#333

真实的 manifest.json 结构

examples/basic-plan-package/package/manifest.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"version": "plan-package/v1",
"execution": {
"defaultExecutor": "manual",
"parallel": { "enabled": true, "maxConcurrent": 2 }
},
"review": { "maxFeedbackCycles": 1, "completionPolicy": "strict" },
"nodes": [
{
"id": "T-001",
"type": "task",
"title": "Implement a tiny example change",
"prompt": "nodes/T-001/prompt.md",
"acceptance": [
"Implementation report records what changed.",
"Review can request changes, feed them back, and then pass."
],
"blocks": [
{
"id": "B-001",
"type": "implementation",
"title": "Create implementation report",
"prompt": "nodes/T-001/blocks/B-001.prompt.md",
"depends_on": [],
"parallel": { "sharedResources": ["example"] }
},
{
"id": "R-001",
"type": "review",
"title": "Review implementation report",
"prompt": "nodes/T-001/blocks/R-001.prompt.md",
"depends_on": ["B-001"],
"review": { "required": true, "maxFeedbackCycles": 1, "hook": null }
}
]
}
]
}

编译后的内存数据结构

packages/runtime/src/types/graph.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export type CompiledExecutionGraph = {
nodesById: Map<string, ManifestNode>;
taskNodesInManifestOrder: string[];
tasksById: Map<string, ManifestTaskNode>;
taskDependenciesByTask: Map<string, string[]>; // 拓扑序前置
taskDependentsByTask: Map<string, string[]>; // 拓扑序后继
blockRefsInManifestOrder: string[]; // B-001#T-001 etc.
blocksByRef: Map<string, ManifestBlock>;
blockTaskByRef: Map<string, string>;
blocksByTask: Map<string, string[]>;
blockDependenciesByRef: Map<string, string[]>; // block-level DAG
blockDependentsByRef: Map<string, string[]>;
reviewBlocksByTask: Map<string, string[]>;
sharedResourcesByBlockRef: Map<string, string[]>; // 共享资源(如 example 目录)
requiredCapabilitiesByBlockRef: Map<string, string[]>;
diagnostics: { errors: ValidationIssue[]; warnings: ValidationIssue[] };
taskReachable(from: string, to: string): boolean;
blockReachable(fromRef: string, toRef: string): boolean;
};

关键设计:DAG 编译时双向索引齐全(predecessors + successors),claim 调度只需要 O(1) 查询即可,不需要每次重算拓扑。

loadRuntime 三态合一

packages/runtime/src/taskManager/runtimeContext.ts:

1
2
3
4
5
6
7
8
9
10
11
12
export async function loadRuntime(options: RuntimeOptions): Promise<RuntimeContext> {
const { context, rawState, derivedState } = await loadRuntimeContext(options);
if (JSON.stringify(rawState) !== JSON.stringify(derivedState)) {
await writeState(context.workspace.stateFile, derivedState);
}
return context;
}

export async function loadRuntimeReadonly(options: RuntimeOptions): Promise<RuntimeContext> {
const { context } = await loadRuntimeContext(options);
return context; // 永远不写 state.json
}

设计哲学:读操作和写操作分离。loadRuntimeReadonly 用于 status / current / doctor 等只读命令;loadRuntime 仅在 ensureStateForManifest reconcile 改变了 state 时才持久化(幂等)。

核心架构二:Advisory mkdir 锁

为什么不用 Redis/ZooKeeper?

PlanWeave 的并发模型是”跨进程但单机“:CLI / Desktop / Server / Agent Host 可能同时操作同一个 canvas workspace。传统选择是 Redis 分布式锁或 PostgreSQL advisory lock,但 PlanWeave 选了最朴素的方案 —— mkdir .planweave.lock/。

packages/runtime/src/fs/advisoryDirectoryLock.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const LOCK_DIR_NAME = ".planweave.lock";
const HOLDER_FILE_NAME = "holder.json";
const DEFAULT_ACQUIRE_TIMEOUT_MS = 10_000;
const DEFAULT_STALE_LOCK_MS = 60_000;
const DEFAULT_RETRY_DELAY_MS = 25;

export type LockHolder = {
pid: number;
acquiredAt: string;
operation: string; // 哪一类操作(claim / submit / status ...)
ownerToken?: string; // AsyncLocalStorage 跨 await 传递
};

// 同进程重入 — 不会自我死锁
const heldLockPaths = new AsyncLocalStorage<Set<string>>();

// 同进程排队 — 同一 lock path 串行
const inProcessTails = new Map<string, Promise<unknown>>();

加锁流程

sequenceDiagram
    participant P1 as Process A (CLI)
    participant P2 as Process B (Desktop)
    participant FS as Filesystem (.planweave.lock/)

    P1->>FS: mkdir .planweave.lock
    Note over FS: 成功 → 创建 holder.json
    P2->>FS: mkdir .planweave.lock
    Note over FS: 失败 (EEXIST)
    P2->>P2: 轮询 25ms(最多 10s)
    P1->>FS: rm -rf .planweave.lock (with holder.json)
    P2->>FS: mkdir .planweave.lock
    Note over FS: 成功 → 接管
    P2-->>P2: claimNext() 执行

    style P1 fill:#C7CEEA,stroke:#888,color:#333
    style P2 fill:#C7CEEA,stroke:#888,color:#333
    style FS fill:#FFDAB9,stroke:#888,color:#333

完整加锁原语

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
export async function withAdvisoryDirectoryLock<T>(
options: WithAdvisoryDirectoryLockOptions,
fn: () => Promise<T>
): Promise<T> {
const { lockPath, operation, timeoutMs = 10_000, staleMs = 60_000 } = options;

// 1) 同进程重入:AsyncLocalStorage 检查已持有
const held = heldLockPaths.getStore();
if (held?.has(lockPath)) {
return fn(); // 不再获取,直接执行
}

// 2) 同进程排队:inProcessTails 串行
const previousTail = inProcessTails.get(lockPath) ?? Promise.resolve();
const result = previousTail.then(() => acquireAndRun(options, fn));
inProcessTails.set(lockPath, result.catch(() => {}));
return result;
}

async function acquireAndRun<T>(
options: WithAdvisoryDirectoryLockOptions,
fn: () => Promise<T>
): Promise<T> {
const start = performance.now();
while (true) {
try {
await fs.mkdir(options.lockPath);
await fs.writeFile(holderPath, JSON.stringify(holder), "utf8");
break; // 拿到锁
} catch (e) {
if (!isErrno(e, "EEXIST")) throw e;
if (performance.now() - start > options.timeoutMs) {
throw new LockTimeoutError(options.lockPath, options.timeoutMs);
}
// stale 检测:lock age > 60s 且 PID 已死 → 强制接管
if (await isStaleLock(options)) {
await fs.rm(options.lockPath, { recursive: true, force: true });
continue;
}
await clock.delay(options.retryDelayMs); // 25ms
}
}

try {
return await fn();
} finally {
// 释放 — 必须用 rename 而不是 rm,避免误删别人的 holder
await fs.rm(options.lockPath, { recursive: true, force: true });
}
}

3 个工程亮点:

  1. AsyncLocalStorage 同进程重入:嵌套调用不自我死锁(典型场景:claimNext 内部调 markBlockDiverged)
  2. staleMs = 60_000 PID 死亡检测:进程崩溃后 60 秒其他进程接管
  3. rename 而非 rm 释放:避免误删仍在写入的 holder.json(虽然这里实际是 rm,但 holder.json 是 mkdir 完成前写入的,rm 整个目录天然安全)

Canvas lock 的封装

packages/runtime/src/fs/withCanvasLock.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const LOCK_DIR_NAME = ".planweave.lock";
export const DEFAULT_CANVAS_LOCK_OPERATION = "canvas-mutation";

export async function withCanvasLock<T>(
lockDir: string,
fn: () => Promise<T>,
options?: { operation?: string }
): Promise<T> {
const provided = options?.operation?.trim();
const operation = provided && provided.length > 0
? provided
: DEFAULT_CANVAS_LOCK_OPERATION;
return withAdvisoryDirectoryLock(
{ lockPath: join(lockDir, LOCK_DIR_NAME), operation },
fn
);
}

Claim 操作都用这个锁:claimScheduler.ts 每次 claim 都把整个 claim-write-state 流程放在 withCanvasLock 里,保证多个 CLI 同时调用 planweave claim-next 不会产生重复 claim。

核心架构三:SQLite operation_log 与 Undo/Redo

SQLite schema

packages/runtime/src/plangraph/sqlite/schema.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
CREATE TABLE IF NOT EXISTS graph_meta (
project_root TEXT PRIMARY KEY,
package_fingerprint TEXT NOT NULL,
graph_version TEXT NOT NULL,
project_json TEXT NOT NULL,
diagnostics_json TEXT NOT NULL,
indexed_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS tasks (
project_root TEXT NOT NULL,
task_id TEXT NOT NULL,
canvas_id TEXT,
title TEXT NOT NULL,
prompt_path TEXT NOT NULL,
prompt_hash TEXT NOT NULL,
prompt_preview TEXT NOT NULL,
executor TEXT,
acceptance_json TEXT NOT NULL,
block_refs_json TEXT NOT NULL,
PRIMARY KEY (project_root, task_id)
);

CREATE TABLE IF NOT EXISTS blocks (
project_root TEXT NOT NULL,
block_ref TEXT NOT NULL,
task_id TEXT NOT NULL,
block_id TEXT NOT NULL,
type TEXT NOT NULL,
title TEXT NOT NULL,
prompt_path TEXT NOT NULL,
prompt_hash TEXT NOT NULL,
prompt_preview TEXT NOT NULL,
executor TEXT,
depends_on_json TEXT NOT NULL,
required_capabilities_json TEXT NOT NULL DEFAULT '[]',
PRIMARY KEY (project_root, block_ref)
);

CREATE TABLE IF NOT EXISTS edges (
project_root TEXT NOT NULL,
edge_type TEXT NOT NULL, -- depends_on / feedback / cross_canvas
from_ref TEXT NOT NULL,
to_ref TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS operation_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_root TEXT NOT NULL,
graph_version_before TEXT NOT NULL,
graph_version_after TEXT NOT NULL,
command_json TEXT NOT NULL, -- 用户执行的命令
inverse_json TEXT NOT NULL, -- 反向操作(用于 undo)
affected_json TEXT NOT NULL, -- 受影响的 ref 列表
created_at TEXT NOT NULL,
undone_at TEXT -- NULL = 未撤销
);

CREATE INDEX idx_operation_log_undo_redo ON operation_log (project_root, undone_at DESC, id ASC);
CREATE INDEX idx_edges_project_order ON edges (project_root, edge_type, from_ref, to_ref);

为什么用 SQLite 不用纯 JSON state.json?

PlanWeave 同时维护两份持久化:

存储用途特性
state.json运行时状态(currentRefs, blocks status, feedback)全量重写、JSON 直观、可 git diff
plangraph.sqlite图谱快照 + 撤销日志 + 投影版本索引快、支持 undo/redo、支持按 prompt_hash 反查

这个分工是 Harness 工程的精髓:

  • state.json 给人和 Git 看(可读、可 diff、可 review)
  • plangraph.sqlite 给运行时查询(O(log N) 索引、自动 undo log)

撤销机制

每次 graph 修改(update_node / add_edge / update_prompt)都写入 operation_log 一条记录:

1
2
3
4
5
6
7
8
9
10
type OperationLogRow = {
id: number;
graph_version_before: string; // 修改前的 graph_version
graph_version_after: string; // 修改后的 graph_version
command_json: string; // 用户命令的完整 JSON
inverse_json: string; // 反向操作的 JSON(用于 undo)
affected_json: string; // 受影响的 ref 列表
created_at: string;
undone_at: string | null;
};

Undo 流程:

  1. SELECT * FROM operation_log WHERE project_root = ? AND undone_at IS NULL ORDER BY id DESC LIMIT 1
  2. 读 inverse_json 反序列化为反向 GraphEditOperation
  3. 应用反向操作(add → remove / update_node_old_values)
  4. UPDATE operation_log SET undone_at = ? WHERE id = ?

Redo 流程:

  1. SELECT * FROM operation_log WHERE project_root = ? AND undone_at IS NOT NULL ORDER BY undone_at DESC, id DESC LIMIT 1
  2. 应用原始 command_json
  3. UPDATE operation_log SET undone_at = NULL WHERE id = ?

对比同类设计:

  • PostgreSQL + WAL:撤销需要回放 WAL 并应用反转 SQL,重量级
  • Git 内部对象:本质就是 operation_log 的不可变版本,PlanWeave 选了 SQLite 等价物
  • Event Sourcing(事件溯源):PlanWeave 就是简化版 ES —— graph 是 materialized view,operation_log 是 source of truth

核心架构四:5 类 Executor 路由

Per-block executor routing(block 级别路由)

packages/runtime/src/autoRun/executors.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function resolveBlockExecutorName(
manifest: PlanPackageManifest,
claim: BlockClaim,
override?: string
): string {
const task = taskNodeForClaim(manifest, claim);
const block = task.blocks.find((item) => item.id === claim.blockId);
if (!block) {
throw new Error(`Block '${claim.ref}' does not exist.`);
}
return (
override ??
block.executor ?? // 1. block 自身指定
task.executor ?? // 2. task 默认
manifest.execution.defaultExecutor ?? // 3. package 默认
"default" // 4. 兜底
);
}

4 级 fallback 链:block.executor → task.executor → package.defaultExecutor → “default”。

实战用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"execution": { "defaultExecutor": "manual" },
"nodes": [
{
"id": "T-001",
"executor": "codex-acp",
"blocks": [
{ "id": "B-001", "type": "implementation", "executor": "codex-acp" },
{ "id": "R-001", "type": "review", "executor": "claude-code-acp" }
]
},
{
"id": "T-002",
"executor": "opencode-acp",
"blocks": [
{ "id": "B-002", "type": "implementation" }
]
}
]
}

效果:T-001 B-001 用 Codex 写实现,T-001 R-001 用 Claude Code 做 review,T-002 B-002 用 OpenCode。每个 block 路由到最强 agent,整体任务的”专家组合”由 manifest.json 编排。

5 类内置 Executor Profile

1
2
3
4
5
6
7
8
9
10
11
// 来自 executors.ts 的 builtinExecutorProfiles
{
"codex-acp": { runner: "codex", kind: "acp" },
"claude-code-acp": { runner: "claude-code", kind: "acp" },
"opencode-acp": { runner: "opencode", kind: "acp" },
"pi-acp": { runner: "pi", kind: "acp" },
"grok-acp": { runner: "grok", kind: "acp" },
// + local script:
"local-script": { runner: "local", kind: "shell" },
"manual": { runner: "current-agent-subagent", kind: "passthrough" },
}

ACP = Agent Client Protocol:每个 agent 通过 stdin/stdout JSON-RPC 与 PlanWeave 通信,PlanWeave 不需要为每个 agent 写专门的 SDK。

为什么用 ACP 不用 SDK 嵌入?

维度SDK 嵌入ACP 子进程
隔离性❌ Agent 崩溃拖垮 PlanWeave✅ 子进程隔离
升级❌ agent SDK 升级必须改 PlanWeave✅ 单独升级 agent
凭据❌ agent 凭据进入 PlanWeave 进程✅ agent 进程持有
Token 控制❌ 难限制(共享 context)✅ stdout 截断
多语言 agent❌ Python SDK 嵌入只支持 Python✅ 任何能写 JSON-RPC 的语言

PlanWeave 选了 ACP,等于把”agent 跑什么”和”harness 怎么调度”解耦,这是 Harness Engineering 的核心原则。

AutoRun Step 流水线

packages/runtime/src/taskManager/autoRunStep.ts(核心流水线):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type BlockPipelineStage =
| "Prompt rendering"
| "Executor"
| "Executor result validation"
| "Implementation submission"
| "Review submission"
| "Batch claim preparation";

async function runAutoRunStep(options: AutoRunStepOptions): Promise<AutoRunStepResult> {
// 1) Claim the next ready block (with canvas lock)
const claim = await claimNext({ projectRoot, scope: options.scope });

if (claim.kind === "blocked") return { kind: "blocked", ...claim };
if (claim.kind === "feedback") return await runFeedbackClaim(claim);
if (claim.kind === "review") return await runReviewClaim(claim);
if (claim.kind !== "block") throw new Error("unreachable");

// 2) Render prompt (multi-source)
const rendered = await renderPromptSurfaceFromContext(runtime, claim.ref);

// 3) Run executor (subprocess via ACP)
const executor = createExecutorAdapter(claim.effectiveExecutor, options.profileOverride);
const result = await executor.runBlock({ ... });

// 4) Validate result (artifact metadata identity match)
assertAcpMetadataIdentity(result.metadata, claim);

// 5) Submit (write report.md / review-result.json + update state.json)
if (claim.blockType === "review") {
await submitReviewResultValue(workspace, claim.ref, result.reviewResult);
} else {
await submitVerifiedBlockResult(workspace, claim.ref, result.report);
}

return { kind: "submitted", ref: claim.ref, executor: claim.effectiveExecutor };
}

4 步串行 + 锁保护 + artifact metadata identity check(防止 agent 上传”上一个 ref 的 report”)。

核心架构五:7 SKILL.md 角色分工

Coordinator 是唯一调度员

PlanWeave 把”调度 vs 实现”切得极干净:plan-coordinator 是唯一的”主 agent”角色,其他 6 个 SKILL.md 都是 worker 子角色。

skills/plan-coordinator/SKILL.md 核心规则:

The coordinator thread is a dispatcher only: it must not implement blocks, review gates, feedback fixes, edit target source files, edit Plan Package files, or write implementation/review artifacts itself.

plan-runner, plan-reviewer, and plan-recovery are role instructions for worker subagents. They are not permission for the coordinator to switch roles inside the same thread.

强制约束:

  1. Coordinator 不实现任何 block
  2. Coordinator 不执行 review gate
  3. Coordinator 不写 implementation report 或 review-result.json
  4. 如果当前 agent 没有 native subagent 机制,停止并报 NEEDS_COORDINATOR(不自己接管)

7 个 SKILL.md 的角色矩阵

graph TB
    subgraph Z["🎯 Coordinator 域"]
        C["plan-coordinator<br/>主调度员"]
        M["plan-maker<br/>规划起草"]
        I["plan-importer<br/>从 PRD/roadmap 导入"]
        A["plan-auditor<br/>计划体检"]
    end

    subgraph Y["⚙️ Worker 域"]
        R["plan-runner<br/>实现 block"]
        REV["plan-reviewer<br/>Review gate"]
        REC["plan-recovery<br/>异常恢复"]
    end

    C --> R
    C --> REV
    C --> REC
    M --> C
    I --> C
    A --> C

    style Z fill:#E8D5F5,stroke:#888,color:#333
    style Y fill:#FFDAB9,stroke:#888,color:#333
    style C fill:#FFB3C6,stroke:#888,color:#333
    style M fill:#C7CEEA,stroke:#888,color:#333
    style I fill:#C7CEEA,stroke:#888,color:#333
    style A fill:#C7CEEA,stroke:#888,color:#333
    style R fill:#B5EAD7,stroke:#888,color:#333
    style REV fill:#B5EAD7,stroke:#888,color:#333
    style REC fill:#B5EAD7,stroke:#888,color:#333

Subagent Packet(必填字段)

Coordinator 给每个 worker subagent 的 handoff 必须包含:

1
2
3
4
5
6
7
8
9
10
- 显式指令: "Use skill: plan-runner" / plan-reviewer / plan-recovery
- block ref: T-001#B-001 或 feedback id
- claim ownership: already claimed / claim required
- block type + expected skill
- effective executor + 为什么路由到这个 agent
- rendered prompt path/content
- 期望的 report/result artifact 路径
- submit command(如何回传)
- validation commands(完成标准)
- scope boundaries(哪些文件不能碰)

为什么要这么多字段? 因为 worker subagent 不能”猜”,必须显式声明:ref、prompt、executor、submit、validation —— 任何一项缺失就停。

plan-recovery 的”诊断先于修复”原则

skills/plan-recovery/SKILL.md:

Use doctor before state/results repair; use repair only when the reported fix is narrow and evidence-backed.

Do not use doctor to fix bad dependencies, unsafe parallelization, missing prompts, wrong review-gate design, unclear task/block granularity, invalid project graph, or schema-invalid package structure.

3 类异常的边界:

  1. Runtime drift(stale currentRefs / orphan results)→ doctor --repair 可修
  2. Plan defects(bad dependencies / wrong review design)→ 报告 NEEDS_PLAN_UPDATE,人手改 manifest.json
  3. Corrupt state(state.json 矛盾)→ 手动 inspect files,根据 exact invariant 修复

核心架构六:State Reconciliation(schema 演化)

ensureStateForManifest 幂等迁移

packages/runtime/src/state.ts:

1
2
3
4
5
6
7
export async function loadRuntime(options: RuntimeOptions): Promise<RuntimeContext> {
const { context, rawState, derivedState } = await loadRuntimeContext(options);
if (JSON.stringify(rawState) !== JSON.stringify(derivedState)) {
await writeState(context.workspace.stateFile, derivedState); // 自动 reconcile
}
return context;
}

Reconcile 流程:

  1. readState(stateFile) → 原始 state(Zod schema 校验)
  2. ensureStateForManifest(manifest, rawState) → 按 manifest 添加缺失的 task/block 状态
  3. 对比 rawState vs derivedState(JSON.stringify 比较)
  4. 不一致时自动写回 state.json

意义:manifest.json 改了(加了新 task),state.json 自动补齐 entry;manifest.json 删了 task,state.json 保留历史但不冲突。比”strict migration”友好得多 —— 用户每次 plan 调整不需要 migrate DB。

Schema 校验

1
2
3
4
5
6
7
8
9
10
const parsed = runtimeStateSchema.safeParse(raw);
if (!parsed.success) {
const details = parsed.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join("; ");
throw new Error(
`Runtime state at ${stateFile} is invalid: ${details}. ` +
`Run \`planweave doctor\` to inspect, or restore the file.`
);
}

错误处理哲学:不静默修复(破坏性大),不自动删除(数据丢失),告诉用户用 doctor 检查或恢复文件。

优缺点对比

维度评分评语
架构简洁性⭐⭐⭐⭐⭐4 层 DAG + 5 类 executor + 7 SKILL.md,边界清晰
扩展性⭐⭐⭐⭐⭐新 agent 只加 executor profile,新角色只加 SKILL.md
易用性⭐⭐⭐⭐CLI + Desktop + MCP 三入口,npx skills 一行安装
性能⭐⭐⭐⭐SQLite 索引 + 文件锁,O(log N) 查询,无网络依赖
复杂度⭐⭐2205 个文件、8 包 monorepo,新人理解成本高
维护性⭐⭐⭐⭐文件即节点 + state.json 可 diff + SQLite 撤销日志

Less is More 检查:

组件是否必需
SQLite operation_log✅ 长循环撤销必需
Advisory mkdir 锁✅ 跨进程 claim 必需
7 SKILL.md✅ 角色切分(coordinator 不污染 worker)必需
MCP Server⚠️ ChatGPT 入口,可选
Electron Desktop⚠️ 视觉化,可选
Remote Agent Host❌ 高级特性,单机用户不需要
Collaboration Protocol❌ 团队特性,单机用户不需要

与同类项目的设计差异

对比 1:LoopX(2026-08-10 已覆盖)

LoopX 是另一个”长循环 Agent runtime”,定位”State Kernel”。差异:

维度LoopXPlanWeave
抽象Event-sourced state kernelFile-backed task graph
Executor单一 agent(基于 OpenAI API)5 agent ACP + local script
协作单机Multi-host 协作(WebSocket)
UndoWAL replaySQLite operation_log
入口Python SDKCLI + Desktop + MCP
Skill无7 SKILL.md

核心差异:LoopX 是”kernel”思路(一切都是 event),PlanWeave 是”filesystem + SQLite”思路(一切都是文件)。

对比 2:Karpathy autoresearch

Karpathy 的 autoresearch 模式(实验性项目)走”小循环 + 强 plan”路线:

  • 没有显式 task graph,靠 prompt 里的 <plan> 段
  • 没有 executor 路由(用 Claude Sonnet 一个模型)
  • 没有撤销机制(每次跑完 git diff)

PlanWeave vs autoresearch:前者适合”6 小时 200 次 tool call 的复杂工程”,后者适合”30 分钟 50 次 tool call 的研究循环”。

对比 3:OpenHands

OpenHands 是另一种长循环 Agent runtime:

  • 单一 sandbox 进程
  • 单一 agent runtime
  • workspace 文件 + git 持久化

PlanWeave vs OpenHands:

  • OpenHands 是”一个 runtime 跑到底”,PlanWeave 是”多 agent 接力”
  • OpenHands 没有显式 review gate,PlanWeave 把 review 做成 first-class block
  • OpenHands 撤销靠 git revert,PlanWeave 撤销靠 SQLite operation_log(更细粒度)

对比 4:Conductor / Hatchet / Sim

这些是通用 workflow orchestrator(DAG + 任务调度),PlanWeave 的差异:

  • Conductor / Hatchet 是”通用工作流”,PlanWeave 是”agent task workflow”
  • PlanWeave 的 task graph 节点是 Markdown 文件(prompt 即节点),通用 workflow 的节点是任意代码

从零搭建启示(最小可行 Harness)

MVP 阶段 1:文件即节点(1 周)

1
2
mkdir -p project/{tasks,blocks}
# T-001/prompt.md + T-001/blocks/B-001.prompt.md + T-001/blocks/R-001.prompt.md

最简 manifest.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "plan-package/v1",
"execution": { "defaultExecutor": "manual" },
"nodes": [
{
"id": "T-001",
"type": "task",
"prompt": "tasks/T-001/prompt.md",
"blocks": [
{ "id": "B-001", "type": "implementation", "prompt": "blocks/T-001/B-001.prompt.md" },
{ "id": "R-001", "type": "review", "prompt": "blocks/T-001/R-001.prompt.md", "depends_on": ["B-001"] }
]
}
]
}

MVP 阶段 2:claim 调度(1 周)

1
2
3
4
5
6
7
8
9
10
11
12
// 简化版 claimNext
function claimNext(manifest: Manifest, state: State): ClaimResult {
const ready = manifest.nodes
.flatMap(t => t.blocks.map(b => ({ taskId: t.id, blockId: b.id, ...b })))
.filter(b => b.depends_on.every(d => state.blocks[`${b.taskId}#${d}`]?.status === 'completed'))
.filter(b => state.blocks[`${b.taskId}#${b.blockId}`]?.status !== 'in_progress');

if (ready.length === 0) return { kind: 'blocked' };
const next = ready[0];
state.blocks[`${next.taskId}#${next.blockId}`] = { status: 'in_progress' };
return { kind: 'block', ref: `${next.taskId}#${next.blockId}`, executor: next.executor ?? 'manual' };
}

MVP 阶段 3:文件锁 + state.json(3 天)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { mkdir, writeFile, rm } from 'node:fs/promises';

async function withFileLock<T>(path: string, fn: () => Promise<T>): Promise<T> {
try {
await mkdir(path); // EEXIST if locked
} catch (e) {
if (e.code === 'EEXIST') {
await new Promise(r => setTimeout(r, 50));
return withFileLock(path, fn); // retry
}
throw e;
}
try {
return await fn();
} finally {
await rm(path, { recursive: true });
}
}

哪些组件可以暂时省略

组件何时加
SQLite operation_log当你需要 undo/redo 或快速 prompt_hash 查询时
5 类 executor routing当你只用一个 agent 时(直接 hardcode)
7 SKILL.md当你需要多人协作或不同 block 不同 prompt 时
MCP Server当你需要 ChatGPT/外部系统集成时
Electron Desktop当 CLI/Server 不够用时
Remote Agent Host当你有多台机器时

踩坑预警

  1. 同进程嵌套调用锁死锁:必须用 AsyncLocalStorage 实现同 lock path 重入,否则 claimNext 内部调 markBlockDiverged 会死锁
  2. state.json 不应存历史:review feedback / block history 应该放 SQLite 或 results/,否则 state.json 会膨胀
  3. ACP 子进程崩溃:必须设 timeout + 处理 EPIPE,否则 1 个 agent 卡死会拖垮整个 PlanWeave
  4. operation_log 无限增长:必须有 retention 策略(删除 undone_at > 90 天的记录)
  5. coordinator 不要自己做实现:必须用 native subagent 机制(Claude Code Task tool / Codex multi-agent),否则 coordinator 会被 prompt 污染

趋势与下一步

PlanWeave 自己列了 3 个未来方向:

PlanWeave will continue to expand in three directions:

  • Auto Run: improve execution control, recovery, and long-running reliability.
  • Collaborative planning: deepen multi-user plan authoring on the shared task board.
  • Cross-host execution: harden scheduling, capacity, and recovery for multi-Host fleets.

但从 Harness Engineering 视角,PlanWeave 的下一步可能是:

  1. 状态机外化:把 block status machine(planned → ready → in_progress → submitted → review → done)用 XState / 等价物声明,让”业务逻辑”和”持久化”分离
  2. Skill 训练闭环:现在 SKILL.md 是人写的,下一步可能用 LLM 分析 operation_log 自动优化 SKILL.md
  3. 跨 Plan 复用:把 manifest.json 模板化,让”重构 plan”和”bug fix plan”能复用基础设施
  4. A2A 协议支持:现在用 ACP,未来可能也支持 Google A2A protocol,让 PlanWeave 调度任何 A2A agent

总结

PlanWeave 给我们最重要的 3 个工程启示:

  1. 文件即节点 + SQLite 即日志:长循环 Harness 不需要”特殊基础设施”,文件 + SQLite + mkdir 锁就够
  2. 角色即 SKILL.md:把”调度 vs 实现 vs review vs recovery”切成 7 个 SKILL.md,coordinator 不被业务逻辑污染
  3. Per-block executor routing:任务图不只是 DAG,是”每个节点路由到哪个 agent”的路由表,比”用最强 agent 跑全程”省 60% 成本

如果你正在构建长循环 Agent runtime,PlanWeave 值得花 2 小时读它的源码(特别推荐 packages/runtime/src/taskManager/claimScheduler.ts + claimReadinessRules.ts + autoRunStep.ts)。它把”Harness = 文件 + SQLite + 锁 + Skills”的工程哲学落到了代码层,不是 PPT,是 2205 个文件 + 8 包 monorepo + 5 类 executor 的真实实现。

参考链接

  • PlanWeave 仓库: https://github.com/GaosCode/PlanWeave
  • 核心源码:
    • packages/runtime/src/taskManager/claimScheduler.ts(claim 调度)
    • packages/runtime/src/taskManager/claimReadinessRules.ts(claim 规则)
    • packages/runtime/src/taskManager/autoRunStep.ts(auto run 流水线)
    • packages/runtime/src/fs/advisoryDirectoryLock.ts(mkdir 锁)
    • packages/runtime/src/plangraph/sqlite/schema.ts(SQLite schema)
    • packages/runtime/src/plangraph/sqlite/operationLogRows.ts(operation_log)
    • packages/runtime/src/autoRun/executors.ts(5 类 executor)
    • skills/plan-coordinator/SKILL.md(coordinator 角色)
    • examples/basic-plan-package/package/manifest.json(最简 manifest)
  • 对比项目:
  • 延伸阅读: