Skip to main content
Glama
Zorabi

Concordia

by Zorabi
README.md
# Concordia

> 面向 Codex 与 ZCode 的 local-first MCP 多代理协作控制面,支持单机 stdio 与可选的跨机器 Redis relay。

Concordia 以结构化任务、事件、租约和 Git worktree,将“规划与验收”与“实施”分开:**Codex** 创建任务、回答问题、审查交付;**ZCode** 原子领取任务、实施、按需使用其子代理,并提交可审查的 commit 与验证证据。状态保存在协调主机的本地 SQLite;双方既可在单机直接连接,也可在都没有公网 IP 时通过 Redis 中转。

它适合在一台机器或“远程 Codex + ZCode 执行主机”的两机拓扑中,可靠地协调一个或少量 Git 仓库,不依赖 GitHub Issue 或共享在线文档。

## 它解决什么问题

- **任务是契约**:目标、目标仓库、允许路径、约束、验收条件与交付项均为结构化字段。
- **事件是沟通**:进度、提问、回答、心跳、失败、返工和批准都是带序号的持久事件。
- **提交是证据**:ZCode 提交的 SHA、变更文件、检查结果和风险进入任务记录,供 Codex 审查。
- **租约防止双写**:过期重领后旧执行者会被围栏拒绝,不能继续写入事件或提交。
- **worktree 隔离写入**:每次领取使用独立 Git branch/worktree,避免并发任务互相污染。
- **桌面原生执行**:ZCode 插件的 `/worker` 在当前 Desktop 任务中建立持久 Goal,领取、实施和提交结果都留在可见会话里。
- **一次配置,多仓库共享**:单机默认使用用户级 `~/.concordia/state.db`,无需为每个业务仓库同步修改 Codex/ZCode 配置。

## 边界与非目标

当前版本面向**单协调节点、同一可信用户或团队、少量并发任务**。已实现 SQLite 持久化、任务状态机、Git 隔离、幂等写入、租约恢复、ZCode 插件、Codex 事件唤醒器,以及基于 Redis Streams 和角色签名的跨机器 relay。尚不提供:

- 多用户身份、仓库级 ACL 或租户隔离;
- 多协调节点高可用或 PostgreSQL;
- 多个独立 ZCode 执行机之间的路径映射和 Git 对象传输;
- Web Dashboard 或面向第三方的通用推送订阅;
- 自动合并、推送远程 Git、发布制品或部署;
- 强制 ZCode 使用某个子代理,或替代理制订实现计划;
- 映射为 ZCode 原生侧边栏任务。

合并、推送、发布与生产操作须由用户或上层协调者明确执行。

## 架构

### 单机模式

```text
Codex ── stdio MCP ─┐
                    ├── Concordia ── SQLite(任务、事件、交付物)
ZCode ── stdio MCP ─┘        │
                              ├── 目标 Git 仓库/.worktrees/<task>-zcode-a<attempt>
                              ├── codex-waker ──> Codex App Server(按事件启动审查 turn)
                              └── ZCode Desktop /worker Goal(可见地持续领取与实施)
```

### 跨机器 Redis 模式

```text
Codex MCP relay client ──┐
                         ├── 出站 TLS ──> Redis Streams/response keys
ZCode MCP relay client ──┘                         │
                                                  │ 出站连接
                                      Concordia relay coordinator(ZCode/Git 主机)
                                                  ├── 本机 SQLite
                                                  └── 本机 Git 仓库与 worktrees
```

远程模式必须把协调器部署在持有目标 Git 仓库的执行主机上。两端只需主动连接同一 Redis,无需公网 IP 或入站端口;`claim_task` 和 `submit_task` 仍在协调主机执行本机 Git/worktree 强校验。详细部署、令牌、Codex/ZCode 配置和安全限制见 [跨机器协调指南](docs/remote-coordination.md)。

这里有两个不同的目录:

- **Concordia 源码目录**:本仓库,包含 `src/`、构建产物与 `zcode-plugin/`;例如 `/absolute/path/to/concordia`。
- **被管理的目标 Git 仓库**:代理真正修改的业务项目;例如 `/absolute/path/to/example-app`。任务 `workspace` 必须是它的 Git 根目录。worktree 创建在这个目标仓库内,而非 Concordia 源码目录。

单机模式下,Codex 与 ZCode 的 stdio MCP 进程默认都打开当前用户的 `~/.concordia/state.db`,与客户端当前仓库或进程 `cwd` 无关。只有需要隔离多个 Concordia 控制面时才设置 `CONCORDIA_HOME` 或 `CONCORDIA_DB`。跨机器模式下,SQLite 只保留在协调主机本地,远程客户端通过 Redis relay 访问,禁止多台机器直接打开网络共享目录中的 SQLite 文件。

### 状态生命周期

`DRAFT` 是协议中保留的类型;当前 `create_task` 会直接创建并发布为 `READY`。

```text
READY ── claim ──> CLAIMED ── progress ──> RUNNING ── submit ──> REVIEW
                                             │                       │
                                             ├─ question ─> WAITING_INPUT
                                             │               └─ answer ─> RUNNING
                                             ├─ failed ───> FAILED
                                             ├─ cancelled ─> CANCELLED
                                             └──────────────── request_changes ─> READY(重新领取)
                                                                approve ─────────> APPROVED
```

终态为 `APPROVED`、`FAILED`、`CANCELLED`。只有 Codex 能创建、审批或要求返工;只有 ZCode 能领取和提交任务。

## 前置条件

- Node.js **22.13+**(使用 `node:sqlite`)。
- Git,且每个目标工作区必须是可访问的 Git 仓库根目录。
- npm。
- 可使用 Codex 和/或 ZCode 的 MCP 功能。
- 跨机器模式额外需要 Redis 7+ 或兼容服务;单机模式不需要 Redis 服务。

```sh
node --version
git --version
npm --version
```

## 安装、构建与运行

在 **Concordia 源码目录**执行:

```sh
npm install
npm run build
npm test
```

构建产生:

- `dist/src/index.js`:本机 stdio MCP 服务;
- `dist/src/relay.js`:运行在 ZCode/Git 主机上的 Redis relay coordinator;
- `dist/src/codex-waker.js`:运行在 Codex 主机上的事件监听与 App Server 唤醒进程;
- `dist/src/zcode-waker.js`:运行在 ZCode 主机上的事件监听与 ZCode CLI 唤醒进程;
- `zcode-plugin/dist/index.mjs`:随 ZCode 本地插件分发的单文件 bundle。

服务使用 stdio,通常由 MCP 客户端启动。以下命令只用于验证进程可启动,随后会等待标准输入上的 MCP 客户端:

```sh
CONCORDIA_AGENT_ID=codex \
npm start
```

MCP 协议仅写 stdout;启动与错误日志写 stderr。默认数据库为 `~/.concordia/state.db`,因此同一系统用户下的 Codex 与 ZCode 会自动共享状态。

若希望 Codex 在 ZCode 提交、提问或失败时自动恢复审查任务,而不是让模型持续调用 `wait_events`,启动可选事件唤醒器:

```sh
CONCORDIA_TRANSPORT=stdio \
npm run start:waker
```

waker 空闲时只运行 Node.js 事件循环,不调用模型。完整配置、跨机器路径规则、可靠性语义和故障排查见 [Codex 事件唤醒器指南](docs/codex-waker.md)。
可复制的环境变量起点见 [.env.waker.example](.env.waker.example)。

希望结果留在 ZCode Desktop 时,在一个可见任务中运行插件命令 `/worker`。它会建立持久 Goal,并在当前 Desktop 会话内反复调用 `wait_events`、领取和实施任务。MCP 是被客户端调用的工具协议,本身不能主动创建模型 turn;因此 Desktop 原生 Goal 才是推荐的持续执行入口。详见 [ZCode Desktop 工作器](docs/zcode-desktop-worker.md)。

旧的外置 `zcode-waker` 仍作为无界面兼容模式保留:

```sh
CONCORDIA_TRANSPORT=stdio \
npm run start:zcode-waker
```

`zcode-waker` 通过 CLI 执行,不保证结果出现在当前 ZCode Desktop 任务中;对桌面工作流不要启动它。详见 [ZCode 事件唤醒器指南](docs/zcode-waker.md)。

跨机器协调器使用:

```sh
CONCORDIA_CONFIG_FILE=/absolute/path/to/concordia.config.json \
CONCORDIA_DB=/absolute/path/to/state.db \
CONCORDIA_REDIS_URL='rediss://user:password@redis.example.com:6379/0' \
CONCORDIA_RELAY_NAMESPACE='team-a' \
CONCORDIA_RELAY_CODEX_TOKEN='<至少 32 字符的随机 token>' \
CONCORDIA_RELAY_ZCODE_TOKEN='<另一个至少 32 字符的随机 token>' \
npm run start:relay
```

协调器不监听入站 HTTP 端口,只主动连接 Redis。跨网络使用 `rediss://`,完整配置见 [跨机器协调指南](docs/remote-coordination.md)。

## 配置

| 变量 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `CONCORDIA_TRANSPORT` | 否 | `stdio` | MCP 客户端传输模式:`stdio` 直接访问本机状态,`redis` 通过中转。 |
| `CONCORDIA_AGENT_ID` | 是 | 无 | 只允许 `codex` 或 `zcode`;角色不匹配的工具会被拒绝。 |
| `CONCORDIA_HOME` | 否 | `~/.concordia` | 单机共享状态目录;同时决定默认任务库和 waker 状态库位置。 |
| `CONCORDIA_CONFIG_FILE` | 否 | 无 | 可选的仓库范围加固配置;设置时优先于 `CONCORDIA_ROOTS`,每次工作区授权校验加载。Redis client 不设置。 |
| `CONCORDIA_CONFIG_STALE_GRACE_MS` | 否 | `5000` | 配置运行时读取失败后允许继续使用 last-known-good 的毫秒数,范围 `0`–`60000`;超出后 fail-closed。 |
| `CONCORDIA_ROOTS` | 否 | 无 | 可选的旧版允许根目录列表;仅在未设置 `CONCORDIA_CONFIG_FILE` 时使用。 |
| `CONCORDIA_DB` | 否 | `~/.concordia/state.db` | 显式覆盖本机 SQLite 路径;Redis client 不设置,绝不能跨机器共享。 |
| `CONCORDIA_REDIS_URL` | Redis 模式 | 无 | Redis URL;远程默认要求 `rediss://`。 |
| `CONCORDIA_RELAY_NAMESPACE` | 否 | `concordia` | 隔离不同部署的 Redis 键,1–64 个安全字符。 |
| `CONCORDIA_RELAY_CODEX_TOKEN` | Codex relay/协调器 | 无 | Codex 请求 HMAC token,至少 32 字符。 |
| `CONCORDIA_RELAY_ZCODE_TOKEN` | ZCode relay/协调器 | 无 | ZCode 请求 HMAC token,至少 32 字符且与 Codex token 不同。 |

### 可选仓库范围加固

默认不再维护仓库白名单:Codex 只能创建任务,ZCode 只能领取已发布任务;两者仍受 Git 根校验、任务 `ownedPaths`、排除路径、worktree 和租约围栏约束。信任边界是运行 MCP 的本机用户及其文件系统权限,因此新增任何本机可访问 Git 仓库都不需要改配置或重启。

如果部署环境需要额外限制 Concordia 可操作的目录,再启用共享 roots 配置。复制 [concordia.config.example.json](concordia.config.example.json) 到一个**不提交进业务仓库**的稳定绝对路径,例如 `/Users/me/.config/concordia/roots.json`:

```json
{
  "version": 1,
  "allowedRoots": [
    "/Users/me/src"
  ]
}
```

`allowedRoots` 的每一项都必须是存在的绝对目录;启用后,每个任务的 `workspace` 必须是其中某项之内的 Git 根目录。给所有本机组件设置同一个 `CONCORDIA_CONFIG_FILE` 即可共享这一额外边界。

服务会在每次**工作区授权校验**时重新读取此文件:在已有根目录下新增或移除仓库后,无需改两端 MCP 配置或重启服务。移除根目录会立即阻止该范围内的新任务、存量任务访问和 waker 后续唤醒。撤权被视为信任边界:重新加入后会恢复 API 访问,但撤权期间被全局 waker 游标越过的旧事件不会自动补发;需要继续的任务应发送新的适用事件或重新创建。首次添加此变量、变更其路径,或更新 MCP/waker/relay 的其他环境变量时,仍须重启相应进程。

首次加载无效或文件不可读时,服务会失败且不会退回到 `CONCORDIA_ROOTS`。已经成功加载过的进程若运行时读取失败(包括 JSON 无效、文件暂不可读或根目录无效),只会在 `CONCORDIA_CONFIG_STALE_GRACE_MS` 的宽限期内继续使用 last-known-good(默认 5000 ms,设为 `0` 即立即拒绝);超过宽限期后工作区授权会 fail-closed。配置修复后,下一次工作区授权校验自动恢复,无需重启。

更新时先在同一目录写入临时文件,验证 JSON 后用原子 `rename` 替换正式文件,避免读到半写入内容。例如:

```sh
mkdir -p /Users/me/.config/concordia
chmod 700 /Users/me/.config/concordia
cp concordia.config.example.json /Users/me/.config/concordia/roots.json.tmp
chmod 600 /Users/me/.config/concordia/roots.json.tmp
mv /Users/me/.config/concordia/roots.json.tmp /Users/me/.config/concordia/roots.json
```

目录和文件应仅允许运行这些进程的用户读取和修改(通常目录 `0700`、文件 `0600`)。不要把配置文件放入 Git、同步盘或所有用户可写的目录;能改此文件的主体可扩大 Concordia 可操作的仓库范围。

### 旧版根目录变量

尚未迁移时仍可使用:

```sh
CONCORDIA_ROOTS=/Users/me/src/project-a,/Users/me/src/project-b
```

路径范围字段 `ownedPaths`、`excludedPaths`、`changedFiles` 必须使用相对工作区的 POSIX 路径,例如 `src/api`,不要使用反斜杠。

## 接入 Codex 与 ZCode

下面使用一组固定示例路径。配置时请把它们全部替换为你机器上的真实绝对路径:

| 含义 | 本节示例值 |
| --- | --- |
| Concordia 源码目录 | `/Users/me/tools/concordia` |
| 被管理的目标 Git 仓库 | `/Users/me/src/example-app` |
| 默认双端共享数据库 | `/Users/me/.concordia/state.db` |

先完成一次构建并确认两个入口文件存在:

```sh
cd /Users/me/tools/concordia
npm install
npm run build

test -f /Users/me/tools/concordia/dist/src/index.js
test -f /Users/me/tools/concordia/zcode-plugin/dist/index.mjs
git -C /Users/me/src/example-app rev-parse --show-toplevel
```

最后一条命令应输出 `/Users/me/src/example-app`。如果输出的是其他目录,应把后续配置中的目标仓库路径改成实际 Git 根目录。

### 在 Codex 中配置

Codex 桌面应用、CLI 和 IDE 扩展在同一 Codex host 上共用 MCP 配置。配置文件可放在全局 `~/.codex/config.toml`,也可放在可信目标项目的 `.codex/config.toml`。以下方式任选一种,详见 [Codex MCP 官方文档](https://developers.openai.com/codex/mcp/)。

#### 方式 A:编辑 `config.toml`(推荐)

将以下内容追加到 `~/.codex/config.toml`。如果只想让服务器在一个项目中可用,则追加到 `/Users/me/src/example-app/.codex/config.toml`:

```toml
[mcp_servers.concordia]
command = "node"
args = ["/Users/me/tools/concordia/dist/src/index.js"]
enabled = true
startup_timeout_sec = 20
# wait_events 最长等待 60 秒,工具超时需略大于 60 秒。
tool_timeout_sec = 70

[mcp_servers.concordia.env]
CONCORDIA_TRANSPORT = "stdio"
CONCORDIA_AGENT_ID = "codex"
```

注意:

- `args` 指向 Concordia 源码构建出的 `dist/src/index.js`,不是目标项目中的文件。
- `CONCORDIA_AGENT_ID` 在 Codex 端必须是 `codex`。
- 默认状态库与当前仓库无关;任务的 `workspace` 可以是当前用户可访问的任意 Git 根目录。
- 需要额外范围限制时,再同时给 Codex 与 ZCode 添加同一个 `CONCORDIA_CONFIG_FILE`。

保存后重启 Codex MCP server:桌面应用可进入 **Settings → MCP servers**,找到 `concordia` 后选择 **Restart**;CLI 或 IDE 扩展可重新启动会话。然后在 Codex 会话中输入 `/mcp`,应能看到 `concordia` 及其 8 个工具。

#### 方式 B:使用 Codex CLI 添加

如果本机安装了 Codex CLI,可以运行:

```sh
codex mcp add concordia \
  --env CONCORDIA_TRANSPORT=stdio \
  --env CONCORDIA_AGENT_ID=codex \
  -- node /Users/me/tools/concordia/dist/src/index.js

codex mcp list
```

CLI 写入的也是 Codex MCP 配置。默认用户级状态路径不依赖 MCP 进程从哪个目录启动。

#### Codex 端验证

在一个新的 Codex 会话中要求它“调用 Concordia 的 `list_tasks`”。若返回任务列表或空数组,说明连接成功。Codex 端具有读取权限,并可调用 `create_task`、以 `sender: "codex"` 发送 `ANSWER`/`CANCELLED`,以及调用 `review_task`;它不能领取或提交任务。

### 在 ZCode 中配置

推荐以 User scope 安装仓库内置插件,因为它会同时提供 Concordia MCP server 和 `/tasks`、`/task`、`/watch`、`/worker` 命令,并在所有工作区使用同一用户级状态库。参见 [ZCode Plugin 文档](https://zcode.z.ai/en/docs/plugin)和 [ZCode MCP 文档](https://zcode.z.ai/en/docs/mcp-services)。

插件安装后的待办查看、任务领取、事件监听和提交操作,详见 [ZCode 使用指南](docs/zcode-usage.md)。

#### 方式 A:安装 Concordia 插件(推荐)

本仓库根目录的 `marketplace.json` 已将 `zcode-plugin/` 声明为可安装插件。操作步骤:

1. 先按前文执行 `npm run build`,确保 `zcode-plugin/dist/index.mjs` 存在。
2. 在 ZCode 中打开**目标项目** `/Users/me/src/example-app`,不要把 Concordia 源码目录当作目标项目打开。
3. 打开 **Settings → Plugins**。如果页面提示先打开 workspace,请先完成上一步。
4. 点击右上角 **Create → Add marketplace**。
5. 本地开发时选择目录 `/Users/me/tools/concordia`;发布 GitHub 后也可以填写 Concordia 仓库 URL。
6. 在 **Personal** 区域找到 `concordia-local` marketplace,再找到 `concordia` 插件,点击 **Install** 并确认已启用。
7. 打开 **Settings → MCP Servers**,在 **Plugin MCP servers** 分组确认 `plugin:concordia:concordia` 已启用。
8. 新建一个 ZCode 会话,输入 `/tasks`。能够返回任务表或“没有匹配任务”即表示插件和 MCP 都已加载。

插件内置的 `zcode-plugin/.mcp.json` 为:

```json
{
  "mcpServers": {
    "concordia": {
      "type": "stdio",
      "command": "node",
      "args": ["${CLAUDE_PLUGIN_ROOT}/dist/index.mjs"],
      "env": {
        "CONCORDIA_TRANSPORT": "stdio",
        "CONCORDIA_AGENT_ID": "zcode"
      },
      "enabled": true,
      "timeoutMs": 70000
    }
  }
}
```

其中:

- `${CLAUDE_PLUGIN_ROOT}`(也可写成 `${ZCODE_PLUGIN_ROOT}`)由 ZCode 替换为已安装插件目录。
- 未设置 `CONCORDIA_DB` 时,插件和 Codex 都使用 `~/.concordia/state.db`。
- 插件不再把 MCP 权限或状态绑定到 `${CLAUDE_PROJECT_DIR}`,因此切换或新增仓库无需修改配置。
- `timeoutMs` 略大于 `wait_events` 允许的最长 60 秒等待,避免正常长轮询被客户端提前中止。

修改 Concordia 或插件源码后,应重新运行 `npm run build`,然后在 ZCode 的 Marketplace sources 中刷新 `concordia-local`。如果插件已经被复制进 ZCode 缓存而非直接引用源码,刷新或重新安装后再开新会话。

#### 方式 B:只手动添加 MCP server

不需要斜杠命令时,可在 ZCode 中打开 **Settings → MCP Servers → New MCP Server**,选择 **User** scope,切换到 **Full configuration**,粘贴以下 JSON:

```json
{
  "mcpServers": {
    "concordia": {
      "type": "stdio",
      "command": "node",
      "args": ["/Users/me/tools/concordia/zcode-plugin/dist/index.mjs"],
      "env": {
        "CONCORDIA_TRANSPORT": "stdio",
        "CONCORDIA_AGENT_ID": "zcode"
      },
      "enabled": true,
      "timeoutMs": 70000
    }
  }
}
```

也可以手动写入用户级 `~/.zcode/cli/config.json`:

```json
{
  "mcp": {
    "servers": {
      "concordia": {
        "command": "node",
        "args": ["/Users/me/tools/concordia/zcode-plugin/dist/index.mjs"],
        "env": {
          "CONCORDIA_TRANSPORT": "stdio",
          "CONCORDIA_AGENT_ID": "zcode"
        },
        "enable": true
      }
    }
  }
}
```

ZCode 的用户级配置位于 `~/.zcode/cli/config.json`。本工具建议使用用户级配置;保存后在 MCP 列表确认服务器已启用,并新建会话测试 `list_tasks`。

#### 双端联通验证

1. 分别启动或重启 Codex 与 ZCode 中的 `concordia` MCP server。
2. 在 Codex 中调用 `create_task` 创建一个目标仓库为 `/Users/me/src/example-app` 的任务。
3. 在 ZCode 中运行 `/tasks`,或要求 ZCode 调用 `list_tasks`。
4. 如果 ZCode 能看到刚创建的任务,说明两端正在使用同一数据库。
5. 若看不到,检查两端是否覆盖了不同的 `CONCORDIA_HOME`/`CONCORDIA_DB`;默认情况下不应配置这两个变量。

插件提供的 `/tasks`、`/task <task-id>`、`/watch <task-id>` 是只读辅助命令;`/worker` 会在当前 ZCode Desktop 任务内持续领取、实施和提交。

### 切换为跨机器 Redis relay

只有需要跨设备时才设置 `CONCORDIA_TRANSPORT=redis`。远端 Codex 的最小配置为:

```toml
[mcp_servers.concordia]
command = "node"
args = ["/Users/me/tools/concordia/dist/src/index.js"]
enabled = true
startup_timeout_sec = 20
tool_timeout_sec = 80

[mcp_servers.concordia.env]
CONCORDIA_AGENT_ID = "codex"
CONCORDIA_TRANSPORT = "redis"
CONCORDIA_REDIS_URL = "rediss://user:password@redis.example.com:6379/0"
CONCORDIA_RELAY_NAMESPACE = "team-a"
CONCORDIA_RELAY_CODEX_TOKEN = "<至少 32 字符的 Codex token>"
```

ZCode/Git 机器需要另外运行 `npm run start:relay`。ZCode MCP 可设置同一个 Redis URL 与 namespace、使用独立的 `CONCORDIA_RELAY_ZCODE_TOKEN`;也可在协调主机继续用默认 `stdio`,直接连接协调器所用的本机 SQLite。Redis 模式下,任务 `workspace` 一律填写 ZCode/Git 机器上的绝对路径。

可直接复制的 ZCode JSON、协调器命令、Redis ACL/TLS 要求和联通步骤见 [跨机器协调指南](docs/remote-coordination.md)。

## MCP 工具

工具返回 JSON 内容及结构化结果。业务错误统一形如 `{ code, message, retryable, details? }`。每次写操作都需唯一 `idempotencyKey`(最长 256 字符);完全相同的重试返回原结果,不重复写入。

| 工具 | 角色 | 作用与关键输入 |
| --- | --- | --- |
| `create_task` | Codex | 创建并发布 `READY` 任务。输入 `spec` 和 `idempotencyKey`。 |
| `claim_task` | ZCode | 原子领取任务;可按 `taskId` 精确领取事件对应任务,或按 `workspace` 筛选最早匹配任务;`leaseSeconds` 为 1–3600(默认 60)。无任务返回 `{ task: null }`;成功包含 `leaseToken`。 |
| `get_task` | 两者 | 获取任务契约、状态、租约摘要、交付物、提交记录与近期事件;`eventLimit` 默认 20、最大 100。 |
| `list_tasks` | 两者 | 以 `status`、`assignee`、`workspace`、`updatedAfter`、`limit` 筛选;默认 20、最大 100。 |
| `send_event` | 两者 | 追加授权事件;可带 `expectedVersion`。ZCode 的写入必须带当前 `leaseToken`。 |
| `wait_events` | 两者 | 查询 `afterEventId` 后的新事件,最长等待 60 秒(默认 30 秒);超时返回空数组。 |
| `submit_task` | ZCode | 提交当前 attempt worktree 的 HEAD commit、精确变更列表、检查、风险和摘要,转入 `REVIEW`。 |
| `review_task` | Codex | 对 `REVIEW` 任务 `approve` 或 `request_changes`;返工至少需要一条 finding。 |

`send_event` 权限:

| 发送者 | 可发送事件 |
| --- | --- |
| Codex | `ANSWER`、`CANCELLED` |
| ZCode | `PROGRESS`、`QUESTION`、`AGENT_STATUS`、`HEARTBEAT`、`FAILED` |

`TASK_CREATED`、`TASK_CLAIMED`、`COMPLETED`、`CHANGES_REQUESTED`、`APPROVED` 由专用工具生成,不能经 `send_event` 伪造。

### 任务契约

```ts
{
  id: string;                 // 字母数字开头,最多 128 字符,可含 . _ -
  objective: string;
  workspace: string;          // 目标 Git 仓库根目录,不是 Concordia 源码目录
  baseCommit?: string;        // 省略时创建时解析为目标仓库 HEAD 的完整 SHA
  ownedPaths: string[];       // 至少一个相对路径
  excludedPaths?: string[];
  constraints: string[];
  acceptance: string[];       // 至少一项
  deliverables: ("commit" | "changed_files" | "checks" | "risks")[];
  delegation: { mode: "auto" | "disabled"; maxConcurrency: number; maxDepth: 1 };
  timeoutSeconds: number;
}
```

`baseCommit` 在创建时解析为完整 SHA。`timeoutSeconds`、`delegation.mode` 与 `maxConcurrency` 目前是被验证和保存的契约信息,不会由运行时自动杀死、调度或限流代理;`maxDepth` 必须为 `1`。

提交检查只接受以下 `commandId` 白名单:`build`、`format-check`、`git-diff-check`、`lint`、`npm-build`、`npm-lint`、`npm-test`、`npm-typecheck`、`test`、`typecheck`。Concordia 记录检查证据,不会执行 payload 中的 shell 命令。

## leaseToken、attempt 与 worktree

### 租约与 fencing token

`claim_task` 成功结果中的 `leaseToken` 是当前执行权的短期凭据。ZCode **必须**把它带入后续每个 `send_event`(包括 `HEARTBEAT`)及 `submit_task`。token 不会进入 `get_task`、事件、提交记录或日志;不要回显、提交或持久化它。

默认租约 60 秒。长任务应在到期前用 `HEARTBEAT` 续租,并可传 `leaseSeconds`(1–3600)。租约过期后另一个领取者可以接手,服务会颁发新 token;旧 token 被围栏拒绝。`expectedVersion` 是可选的乐观并发控制,适合读—改—写过程。

提交进入 `REVIEW` 时租约立即失效。Codex 要求返工后任务回到 `READY`;ZCode 必须再次调用 `claim_task`,在新 attempt worktree 中继续,并使用新 `leaseToken`。旧 token 永远不能恢复使用。

### 每个 attempt 的独立工作区

每次领取创建或复用该尝试专属路径:

```text
<目标 Git 仓库>/.worktrees/<task-id>-zcode-a<attempt>/
```

对应分支为 `concordia/<task-id>-zcode-a<attempt>`。首次从 `baseCommit` 建立;租约过期后的新 attempt 从上一次 worktree 的已提交 `HEAD` 快照恢复(没有可恢复 worktree 时从 base commit 开始)。旧 attempt 目录不会被新 attempt 重用。

**ZCode 必须修改 `claim_task` 返回的 `task.worktreePath`,而不是目标仓库的原始检出目录。** 提交时服务验证:

1. 目标仓库/worktree 均在允许根目录内,且不存在符号链接逃逸;
2. `baseCommit` 仍解析为原完整 SHA,提交历史从其演进;
3. 提交 SHA 是当前 attempt worktree 的完整、不可变 `HEAD`;
4. `changedFiles` 与 Git diff 精确一致;
5. 最终 diff、所有中间提交及未提交改动仅触及 `ownedPaths`,且不触及 `excludedPaths`。

## 端到端示例

假设目标仓库为 `/absolute/path/to/example-app`。单机默认下两端会自动连接同一用户级 `~/.concordia/state.db`,不需要在目标仓库创建或配置数据库。以下 JSON 是 MCP 工具参数,不是 shell 命令。

1. Codex 获取目标仓库基准并调用 `create_task`:

   ```sh
   git -C /absolute/path/to/example-app rev-parse HEAD
   ```

   ```json
   {
     "spec": {
       "id": "docs-api-001",
       "objective": "为 API 客户端补充使用文档",
       "workspace": "/absolute/path/to/example-app",
       "baseCommit": "<完整 SHA>",
       "ownedPaths": ["docs", "README.md"],
       "excludedPaths": [".github"],
       "constraints": ["不要修改运行时代码"],
       "acceptance": ["文档包含安装与 API 示例", "npm test 通过"],
       "deliverables": ["commit", "changed_files", "checks", "risks"],
       "delegation": { "mode": "auto", "maxConcurrency": 1, "maxDepth": 1 },
       "timeoutSeconds": 1800
     },
     "idempotencyKey": "docs-api-001:create:v1"
   }
   ```

2. ZCode 领取并保存返回的 `task.worktreePath` 与 `leaseToken`。由 `zcode-waker` 唤醒时必须传入事件对应的 `taskId`,避免领取另一个 READY 任务:

   ```json
   { "agentId": "zcode", "taskId": "docs-api-001", "workspace": "/absolute/path/to/example-app" }
   ```

3. 在返回的 attempt worktree 工作,并用 token 上报进度:

   ```json
   {
     "taskId": "docs-api-001",
     "sender": "zcode",
     "recipient": "codex",
     "type": "PROGRESS",
     "payload": { "phase": "writing", "summary": "正在编写 API 示例" },
     "idempotencyKey": "docs-api-001:progress:writing",
     "leaseToken": "<领取结果的 leaseToken>"
   }
   ```

   该事件将 `CLAIMED` 变为 `RUNNING`。需要澄清时发送 `QUESTION`;Codex 用 `ANSWER` 回复,任务从 `WAITING_INPUT` 返回 `RUNNING`。长任务发送 `HEARTBEAT` 续租。

4. 在 **返回的** worktree 中完成、验证、提交:

   ```sh
   cd /absolute/path/to/example-app/.worktrees/docs-api-001-zcode-a1
   npm test
   git add docs README.md
   git commit -m "docs: add API client guide"
   git rev-parse HEAD
   ```

5. ZCode 调用 `submit_task`。`commit` 为完整 SHA,`changedFiles` 必须与 `baseCommit..commit` diff 完全一致:

   ```json
   {
     "taskId": "docs-api-001",
     "leaseToken": "<领取结果的 leaseToken>",
     "commit": "<完整提交 SHA>",
     "changedFiles": ["README.md", "docs/api-client.md"],
     "checks": [{ "commandId": "npm-test", "exitCode": 0, "summary": "npm test passed" }],
     "risks": [],
     "summary": "补充 API 客户端安装、认证和调用示例。",
     "idempotencyKey": "docs-api-001:submit:1"
   }
   ```

6. Codex 用 `get_task` 读取交付证据并审查 diff,之后调用 `review_task`:

   ```json
   {
     "taskId": "docs-api-001",
     "decision": "approve",
     "summary": "文档内容和测试证据已核对。",
     "idempotencyKey": "docs-api-001:approve:1"
   }
   ```

   返工时使用 `decision: "request_changes"` 并给出至少一个 `{ path?, line?, severity, message }` finding。任务会回到 `READY`,ZCode 需要重新领取。批准只表示任务协议完成,**不会自动合并** worktree branch;后续合并、cherry-pick 或丢弃由用户或上层协调者决定。

## ZCode 命令

| 命令 | 用途 |
| --- | --- |
| `/tasks [status]` | 列出当前工作区近期任务;可按一个或多个状态过滤,显示 ID、状态、执行者、更新时间和目标。 |
| `/task <task-id>` | 显示任务契约、状态、路径范围、验收条件、租约摘要、交付证据与近期事件。 |
| `/watch <task-id>` | 读取当前事件游标后循环 `wait_events` 显示新事件;终态、用户中止或达到观察限制时停止。 |
| `/worker` | 在当前 ZCode Desktop 任务中建立持久 Goal,持续领取、实施、验证并提交 Concordia 任务。 |

前三个命令只查询或观察;`/worker` 是 Desktop 原生执行入口。

## 安全模型

Concordia 的默认边界是“同一本机用户/可信团队 + 结构化任务范围”,不是完整的多租户平台。已实现的约束包括:

- 必须显式声明 `codex` 或 `zcode`;工具和事件发送者均做角色校验;
- 工作区必须为真实 Git 根;可选的 `allowedRoots`/`CONCORDIA_ROOTS` 可进一步限制目录范围;
- 路径范围拒绝绝对路径、`..`、反斜杠、Git/Concordia 控制目录和越界符号链接;
- worktree 及既有目录会解析真实路径,防止逃离目标仓库;
- SQLite 使用外键、WAL、事务和任务 version 乐观锁;事件 idempotency key 全局唯一;
- `leaseToken` 使用时序安全比较,轮换后旧领取者无法继续写入;
- Redis relay 使用分角色 HMAC-SHA256 签名、时间戳、nonce 防重放、响应 TTL 和单协调器锁;
- 远程 Redis 默认强制 `rediss://`,角色 token 与 Redis 凭据均不进入日志;
- `submit_task` 不执行任意命令;只记录白名单检查 ID 的结果;
- 业务错误不回显环境变量、凭据或完整命令输出。

任何能读写本地数据库、Git 工作区、Redis 数据或 MCP 配置的用户仍在同一信任域。跨机器生产部署应使用 Redis ACL 和 TLS。

## 测试与开发

```sh
npm run typecheck  # 严格 TypeScript 类型检查
npm run build      # 编译服务并打包 ZCode 插件
npm test           # build 后运行 Node 内置测试
```

若本机有测试 Redis,可额外执行真实 relay 往返测试:

```sh
CONCORDIA_TEST_REDIS_URL=redis://127.0.0.1:6379 npm test
```

测试覆盖完整领取—运行—提交—返工重领—批准流程、并发领取、幂等、租约恢复、fencing token、数据库重启持久化、事件等待、两个 waker 的游标与投递恢复、Codex App Server 与 ZCode CLI 生命周期、基准提交验证、worktree 恢复、relay 签名/权限/TLS 校验,以及路径/符号链接/提交历史范围校验。

详见:[系统设计](docs/design.md)、[开发指南](docs/development.md)、[ZCode 使用指南](docs/zcode-usage.md)、[Codex 事件唤醒器指南](docs/codex-waker.md)与 [ZCode 事件唤醒器指南](docs/zcode-waker.md)。

## 目录结构

```text
concordia/                         # Concordia 源码目录
├── src/
│   ├── index.ts                   # stdio MCP server 与 8 个工具
│   ├── protocol.ts                # 类型、验证、错误模型
│   ├── database.ts                # SQLite 初始化、迁移、事务
│   ├── paths.ts                   # 用户级共享状态路径
│   ├── events.ts                  # 事件追加、查询、有界等待
│   ├── tasks.ts                   # 状态机、租约、幂等、交付验证
│   ├── workspace.ts               # Git/worktree、路径范围
│   ├── relay-protocol.ts          # relay 签名信封与安全校验
│   ├── relay-client.ts            # Redis 模式 MCP client
│   ├── relay.ts                   # Redis Streams coordinator
│   ├── codex-app-server.ts        # Codex App Server JSONL 客户端
│   ├── zcode-cli.ts               # ZCode headless CLI 客户端
│   ├── waker-source.ts            # SQLite/Redis 共享事件源
│   ├── codex-waker.ts             # 事件过滤、唤醒与重试循环
│   ├── zcode-waker.ts             # ZCode CLI 事件唤醒与重试循环
│   └── waker-state.ts             # 独立游标、线程和投递状态库
├── tests/                         # 核心、relay、CLI 与 waker 测试
├── marketplace.json               # ZCode 本地/GitHub marketplace 入口
├── zcode-plugin/                  # 可加载的 ZCode 本地插件
│   ├── .mcp.json
│   ├── .zcode-plugin/plugin.json
│   └── commands/                  # /tasks、/task、/watch、/worker
├── docs/
│   ├── zcode-usage.md            # ZCode 待办、监听与执行指南
│   ├── zcode-desktop-worker.md   # ZCode Desktop 原生工作器
│   ├── codex-waker.md            # Codex 事件驱动唤醒与部署
│   ├── zcode-waker.md            # ZCode 事件驱动唤醒与部署
│   ├── design.md
│   └── development.md
├── package.json
├── concordia.config.example.json  # 热加载共享允许根目录配置模板
├── .env.waker.example             # Codex waker 环境变量模板
├── .env.zcode-waker.example       # ZCode waker 环境变量模板
└── tsconfig.json

<目标 Git 仓库>/
└── .worktrees/<task>-zcode-a<attempt>/  # Concordia 运行时创建
```

本仓库 `.gitignore` 忽略 `node_modules/`、`dist/`、`.concordia/`、`.worktrees/` 和 TypeScript 构建缓存。

## 开源协议

Concordia 使用 [MIT License](LICENSE) 开源。你可以自由使用、复制、修改、合并、发布和分发本软件,但必须保留原始版权与许可声明。本软件按“原样”提供,不附带任何明示或默示担保。

## 故障排查

| 现象 | 处理 |
| --- | --- |
| `CONCORDIA_AGENT_ID is required` | 为 MCP server 设置 `CONCORDIA_AGENT_ID=codex` 或 `zcode`。 |
| `CONCORDIA_CONFIG_FILE` 加载失败 | 确认路径为可读的绝对路径、JSON 符合 `{ "version": 1, "allowedRoots": ["/absolute/path"] }`,且每一根目录存在。运行中修复后会在下一请求生效。 |
| 配置持续加载失败后请求被拒绝 | 在 `CONCORDIA_CONFIG_STALE_GRACE_MS`(默认 5000 ms)内修复或原子替换配置;超过该期限会 fail-closed,修复后下一次工作区授权校验自动恢复。 |
| `WORKSPACE_DENIED` | 确认 `workspace` 是可访问的真实 Git 根且没有越界软链接;若启用了 roots 加固,再检查目标路径是否在范围内。 |
| 两端看不到彼此任务 | 检查是否有一端覆盖了不同的 `CONCORDIA_HOME`/`CONCORDIA_DB`;默认两端均应使用 `~/.concordia/state.db`。 |
| `Redis relay request timed out` | 确认协调器运行中,Redis URL、数据库编号和 namespace 一致,ACL 允许所需命令。 |
| 远程 `redis://` 被拒绝 | 生产环境改用 `rediss://`;只有可信开发网络才设置 `CONCORDIA_RELAY_ALLOW_INSECURE=true`。 |
| `claim_task` 返回 `task: null` | 没有匹配的 `READY` 任务,也没有过期可恢复任务;用 `list_tasks` 查看。 |
| `LEASE_CONFLICT` | token 不正确、租约过期或已被重领。重新领取,切勿复用旧 token。 |
| `STALE_VERSION` | 读取后任务被其他写操作改变;重新 `get_task` 后继续。 |
| `BASE_COMMIT_MISMATCH` | 基准 SHA 无法解析或尝试历史不从其演进;检查 Git 历史和任务 `baseCommit`。 |
| `PATH_SCOPE_VIOLATION` / 提交被拒绝 | 核对 `ownedPaths`/`excludedPaths`;最终 diff、中间提交与未提交改动必须全部在允许范围内。 |
| 提交不是当前 HEAD | 在当前 `task.worktreePath` 中提交,并传 `git rev-parse HEAD` 的完整 SHA。 |
| SQLite 初始化被锁定 | 稍后重试;初始化有有限重试。若持续发生,检查异常进程是否占用同一数据库。 |
| `waker.event_failed` 反复出现 | 检查 Codex CLI 登录、App Server、Codex 端 Concordia MCP 与 `waker.db` 中的 `last_error`。 |

## 当前限制

- 当前版本为 `0.6.0`,`package.json` 标为 `private: true`,尚未作为 npm 包发布。
- Node 的 `node:sqlite` 在部分 Node 22 发行版可能显示实验性 API 警告;采用前请按自身 Node 策略评估。
- Redis relay 当前只支持单活动协调器,不提供多协调器高可用、远程备份或自动清理旧 attempt worktree。
- `timeoutSeconds`、`delegation.maxConcurrency`、`delegation.mode` 不会被运行时强制调度或限流。
- MCP 不能主动唤醒一个空闲客户端。ZCode Desktop 使用 `/worker` Goal 保持可见执行;外置 `zcode-waker` 仅作为 CLI 兼容模式保留。
- 提交路径校验不替代人工/自动代码审查、CI、合并策略和发布流程。