# xaf_agent_hub —— opencode 多实例「浠艾福(CFC)」会话发现与调度工具 一个只依赖 **Python 标准库** 的命令行工具,用来: 1. **发现**:找出 6 个 opencode 实例上所有与「浠艾福开发」相关的会话(按会话工作目录判定); 2. **调度**:向指定会话投递开发需求,或自动负载均衡地新建会话执行; 3. **智能匹配**:派发时自动找出与任务最相关的已有 CFC 会话并复用,没有相关会话才新建(避免重复会话); 4. **限流**:每个实例同时最多运行 **2 个** CFC 开发会话,**并发安全**(多进程同时调度也不会突破); 5. **自动清理**:一键清理各实例上超过 TTL(默认 10 天)未使用且非运行态的 CFC 会话; 6. **导出**:把全量清单导出为 JSON / CSV,并生成一份给非技术同事直接看的单文件 HTML 总览页。 --- ## 一、环境要求 | 项目 | 要求 | |---|---| | Python | 3.10+(实测 3.13.14,命令为 `python`) | | 第三方包 | **无**(仅 `urllib` / `json` / `csv` / `argparse` / `concurrent.futures` / `msvcrt`·`fcntl` 等标准库) | | 网络 | 能访问 6 个 opencode 实例的 HTTP 端口 | 所有命令都在本目录下执行: ```bash cd E:/cfc/tools/xaf_agent_hub python hub.py instances ``` --- ## 二、文件结构 ``` xaf_agent_hub/ ├── config.json # 实例定义 + cfc_roots + exclude_dirs + 并发上限(**不含任何密码**) ├── config.example.json # 配置模板(占位值,可直接复制) ├── config.local.json # 本地凭证覆盖(已被 .gitignore 忽略,**永不提交**) ├── hub.py # CLI 入口(argparse 子命令 + 中文等宽表格) ├── opencode_api.py # HTTP 客户端(Basic 认证、超时、重试、异常归一、session 归一化) ├── cfc_filter.py # CFC 会话识别规则(归一化 / 根目录 / 路径段 / 排除) ├── scheduler.py # 配置加载 + 并发扫描 + 调度决策 + 并发安全闸门 ├── matcher.py # 语义会话匹配(字符 n-gram + TF-IDF 余弦,无外部依赖) ├── slot_lock.py # 实例互斥锁 + 并发槽位预留(P0 并发正确性) ├── report.py # 导出 JSON / CSV / 单文件 HTML 总览页 ├── README.md # 本文档 └── tests/ └── test_smart_cleanup.py # 单元测试(mock,无需可达实例) ├── .gitignore # 忽略 config.local.json / *.log / __pycache__ ├── cfc_sessions_report.html # 生成产物:总览页 └── out/ # 生成产物:cfc_sessions.json / cfc_sessions.csv ``` 运行期状态目录(不在仓库内):`~/.xaf_agent_hub/locks/*.lock`、`~/.xaf_agent_hub/slots/*.slot`。 --- ## 三、凭证配置(务必先读) **`config.json` 里永远不写密码**,5 个实例的 `password` 一律为 `null`,只保留 `password_env` 提示。 真实密码放在 `config.local.json`(已被 `.gitignore` 忽略),或用环境变量临时覆盖。 密码解析优先级(高 → 低): | 优先级 | 来源 | 用法 | |---|---|---| | 1 | `password_env` 指定的环境变量 | `set XAF_HUB_PW_LAN_189=xxx`(临时覆盖/排障最方便) | | 2 | `XAF_HUB_PW_<实例名大写,`-`/`.`→`_`>` 环境变量 | 同上,不需要改配置 | | 3 | `config.local.json` 的 `instances..password` | 日常使用,随实例配置一起覆盖 | | 4 | `password_file` + `password_key` | 如 `~/.config/opencode/service.json` 的 `password` | `config.local.json` 格式(支持 `{实例名: {...}}` 字典,也支持数组): ```json { "instances": { "lan-189": {"password": "<在此填写>"}, "prod-etotem": {"password": "<在此填写>"} } } ``` > 用 `--config 其它路径` 时,若该路径同级没有 `config.local.json`,会自动回落到 > 本工具目录的 `config.local.json` **只借凭证**:仅合并密码相关字段(`password` / > `password_key` / `password_file` / `password_env`),**不会**把本工具的实例列表或其它配置 > 注入到你自己的配置文件里;你配置里没有的实例名会被直接忽略。 校验是否泄漏: ```bash grep -rn "IwinTrue\|OpenCode@" E:/cfc/tools/xaf_agent_hub/ # 只应命中 config.local.json cd E:/cfc && git check-ignore -v tools/xaf_agent_hub/config.local.json # 必须命中忽略规则 ``` --- ## 四、实例与主要配置字段 | 逻辑名 | 地址 | 环境 | 备注 | |---|---|---|---| | `local-45278` | `http://127.0.0.1:45278` | 本机 | 密码来自 `~/.config/opencode/service.json` | | `lan-248` | `http://192.168.16.248:4090` | 内网 | | | `lan-204` | `http://192.168.16.204:4091` | 内网 | **4091** 端口(不是 4090) | | `lan-189` | `http://192.168.16.189:4090` | 内网 | | | `prod-etotem` | `https://opencode.etotem.com.cn` | **生产** | `danger: true` + `readonly_default: true`,写操作需 `--allow-prod` | | `pub-iwintrue` | `http://opencode.iwintrue.com` | 公网 | **http**(https 会 401) | | 字段 | 说明 | |---|---| | `concurrency_limit` | 每实例同时运行的 CFC 会话上限,默认 `2` | | `session_ttl_days` | 自动清理的「未使用」阈值(天),默认 `10`;超过该时长且非运行态的 CFC 会话可被清理 | | `match_threshold` | 智能匹配的相似度阈值,默认 `0.12`;低于此值视为「无相关会话」,改走新建 | | `auto_match` | 是否默认启用智能匹配(`dispatch` 缺省行为),默认 `true` | | `timeouts.connect_seconds` | TCP 连接超时(默认 8s,预检用) | | `timeouts.read_seconds` | 请求读超时(默认 30s) | | `retries` | 网络错误 / 5xx / 协议中断重试次数(默认 1,4xx 不重试) | | `page_size` | `GET /api/session` 的 `limit`(默认 8000;必须显式传,否则服务端只给 50 条) | | `scan_workers` | 多实例并发扫描线程数(默认 6) | | `lock.slot_ttl_seconds` | 槽位预留有效期(默认 60s,超时自动清理) | | `lock.lock_timeout_seconds` | 获取实例调度锁的最长等待(默认 20s) | | `lock.slot_grace_seconds` | 会话创建后等待 `/api/session/active` 反映的宽限(默认 5s) | | `lock.dir` | 锁与槽位存储目录,`null` = `~/.xaf_agent_hub` | | `instances[].cfc_roots` | 该实例的 CFC 根目录清单(前缀匹配,含子目录) | | `instances[].default_directory` | 该实例新建会话时的默认工作目录 | | `instances[].danger` | `true` = 危险实例(生产),写入需 `--allow-prod` | | `exclude_dirs` / `exclude_substrings` | 排除目录(前缀)/ 排除子串(如 `workbuddy`) | | `fallback_segments` | 兜底路径段:`cfc` / `caremate` / `danshop` / `xaf` | --- ## 五、CFC 会话识别规则 判定依据是会话的 `location.directory`(工作目录),先归一化:统一 `/` 分隔符、折叠重复分隔符、去尾分隔符、整体小写(Windows 盘符大小写不敏感)。规则优先级 **从高到低**: 1. **排除清单**(`exclude_dirs` 前缀 + `exclude_substrings` 子串)——命中即判为非 CFC,即使同时命中后两条。 当前排除:`E:\workspace\mes`、`E:\wbspace\train`、`E:\workspace\totem`、`E:\workspace\iwt`、 `/home/liaoxg/.config/opencode/plugins/omf`、`/sc-data/mes`、`/home/iwt/.config/opencode/plugins/omf`、 `D:\Documents`、`D:\tmp`、`D:\360极速`、`C:\Users\iwt`、含 `workbuddy` 的任意目录。 2. **实例显式根目录**(`cfc_roots`)——目录等于根目录或位于其下: - `local-45278`: `E:\cfc`(覆盖 `E:\cfc\cfc-frontend`、`E:\cfc\docs` 等) - `lan-248`: `/sc-data/cfc` - `lan-204`: `D:\cfc` - `lan-189`: `C:\code\cfc`(覆盖 `book`、`运营文案`、`小红书便秘和腹泻帐号`) - `prod-etotem`: `/root/cfc`、`/root/caremate` - `pub-iwintrue`: `/sc-data/cfc`、`/sc-data/caremate` 3. **兜底路径段**——目录任一段等于 `cfc` / `caremate` / `danshop` / `xaf` 时也算 CFC(识别未知新目录)。 `--keyword` 是在目录判定之上再按 **标题** 做包含过滤(默认不过滤)。 --- ## 六、子命令 ### 1) `instances` —— 实例健康与总量 ```bash python hub.py instances python hub.py instances --json ``` 输出:实例 / 地址 / 状态 / 版本 / 会话总数 / CFC 会话数 / 运行中(总/CFC) / 上限 / 说明。 不可达实例只标记原因,**单实例任何异常都不中断整体扫描**(协议中断、超时、401、5xx 均已归一)。 ### 2) `sessions` —— CFC 会话清单(可导出) ```bash python hub.py sessions # 表格,按最近更新倒序 python hub.py sessions --instance lan-189 # 只看某实例 python hub.py sessions --keyword 打卡 --limit 20 # 标题关键词 + 限制条数 python hub.py sessions --json # JSON 到 stdout python hub.py sessions --out out/cfc_sessions.json # 导出 JSON 全量 python hub.py sessions --out out/cfc_sessions.csv # 导出 CSV(utf-8-sig,Excel 可直接打开) python hub.py sessions --html cfc_sessions_report.html # 生成单文件 HTML 总览页 ``` > `--limit` 只影响屏幕显示;`--out` / `--html` 永远导出 **全量**。 CSV 列:`实例, 会话ID, 工作目录, 运行状态, 最近更新, 创建时间, outcome, 标题, 消息成本` ### 3) `status` —— 负载与可调度性 ```bash python hub.py status python hub.py status --json python hub.py status --allow-prod # 把生产实例也计为可调度(仅影响展示) ``` 输出:运行中总数 / 运行中 CFC / **在途预留** / CFC 会话总数 / 上限 / 是否可调度 / 原因。 可调度判定 = `运行中 CFC + 在途预留 < 上限` 且非生产只读。 ### 4) `dispatch` —— 调度会话做开发 ```bash # 智能匹配:跨全部实例找出与任务最相关的已有 CFC 会话并复用;无相关则新建 python hub.py dispatch --text "修复登录页样式" # 只看计划(含匹配相似度与候选)不发送 python hub.py dispatch --text "修复登录页样式" --dry-run # 指定实例(在该实例内做智能匹配) python hub.py dispatch --text "补充单元测试" --instance lan-189 # 复用指定会话(复用不占新额度,满载时也能追加指令) python hub.py dispatch --text "继续上一轮改造" --session ses_xxx --instance local-45278 # 指定目录 / 标题(--title 关键词匹配) python hub.py dispatch --text "改造打卡页" --instance lan-189 --dir 'C:\code\cfc\book' --title 打卡 # 强制新建(忽略任何匹配,--new 优先级最高) python hub.py dispatch --text "改造打卡页" --instance lan-189 --dir 'C:\code\cfc\book' --title 打卡 --new # 关闭智能匹配(按旧逻辑:无 --session/--title 则新建会话) python hub.py dispatch --text "改造打卡页" --instance lan-189 --no-match # 派发前先自动清理各实例上 10 天未用的 CFC 会话 python hub.py dispatch --text "修复登录页" --auto-cleanup # 生产实例(默认拒绝,需显式放行) python hub.py dispatch --text "只更新文档" --instance prod-etotem --allow-prod --dry-run ``` #### 智能会话匹配(默认行为) 未显式指定 `--session` / `--title` / `--new` 时,`dispatch` 默认启用**智能匹配**: 1. 把每个实例上全部 CFC 会话的「标题 + 工作目录」拼成一个文档; 2. 用**字符 n-gram + TF-IDF + 余弦相似度**对任务文本做检索,排出最相关的前 5 个候选; 3. 从候选中按优先级挑选可复用者: - 相似度 ≥ `match_threshold`(默认 `0.12`)才开始考虑; - **正在运行**的相关会话优先 —— 直接 steer 追加指令,**不占新额度**(即便实例已满也能复用); - 否则若相关会话**空闲**且实例尚有空额 —— 复用并启动运行,**占用一个并发额度**(受并发闸门约束); - 实例已满时,空闲相关会话无法复用,会继续看下一候选; 4. 若全部候选相似度都低于阈值 —— 退化为「在负载最低的可调度实例新建会话」。 > 匹配基于**标题与目录**(字符级),不读取会话正文,因此短标题的语义重合度足够区分「同一需求的续做」与「全新需求」。阈值可在 `config.json` 的 `match_threshold` 调整;`--dry-run` 会打印匹配相似度,便于校准。 执行顺序: 1. 智能匹配(命中则直接复用,跳过后续选实例); 2. 否则选定实例(`--instance`,或自动挑选「可达 + 有 CFC 目录 + 未满载 + 非生产」中负载最低者); 3. 解析目标会话:`--session` → 否则 `--title` 关键词匹配最近更新的 CFC 会话 → 否则新建(`--new` 强制新建); 4. **并发闸门**(对「新建会话」与「复用空闲会话」生效,复用运行中会话不占额度): `运行中 CFC + 在途预留 >= 2` → 拒绝,退出码 **3**; 5. `POST /api/session/:id/prompt`,body `{"text": ...}`,**异步投递**(不阻塞等模型跑完)。 行为要点: - **复用已在运行的会话不占新额度**(`prompt` 是 steer 语义,不新增并发会话),因此满载时仍可给运行中的会话追加指令; - **复用空闲会话会启动运行,等价于占用一个新并发额度**,同样受并发上限约束(避免绕过「每实例最多 2 个」的硬规则); - `--title` 命中已有会话但 `--dir` 与它的目录不一致时,会打印 `[!] 复用了已有会话 …,已忽略 --dir …;如需在指定目录新建请加 --new`; - 目标不是 CFC 会话 → 拒绝,退出码 **5**(`--force` 才放行); - 生产实例(`danger`)→ 拒绝,退出码 **4**(`--allow-prod` 才放行)。 ### 4.5) `cleanup` —— 清理超期未用的 CFC 会话 ```bash # 安全预览:列出各实例上超过 10 天未使用、且当前未在运行的 CFC 会话 python hub.py cleanup # 自定义阈值(天) python hub.py cleanup --ttl-days 30 # 真正删除(默认只预览!必须显式 --execute) python hub.py cleanup --execute # 仅清理某个实例 / 生产实例 python hub.py cleanup --instance lan-189 python hub.py cleanup --allow-prod --execute ``` 判定口径: - 以会话的「最后更新时间」(`time.updated`)为「最后使用」依据;早于 `now - ttl_days` 即视为过期; - **当前正在运行**的会话一律跳过(不删); - `time.updated` 缺失(≤0)的会话保守跳过(避免误删只是未采集到时间的新鲜会话); - `danger` 实例(生产)默认跳过,需 `--allow-prod`; - 工具只枚举 CFC 会话,因此只清理 CFC 会话(非 CFC 会话不在范围内)。 > **默认安全**:`cleanup` 默认只统计、**不删除**(`dry-run`);只有显式 `--execute` 才会真正 `DELETE`。删除不可恢复,建议先用 `cleanup`(无参数)预览确认。 ### 5) `interrupt` —— 中断运行 ```bash python hub.py interrupt --session ses_xxx --instance lan-248 python hub.py interrupt --session ses_xxx # 不指定实例则全实例搜索 ``` 同样受「非 CFC 会话 → 5」「生产 → 4」闸门约束。 ### 6) `watch` —— 观察运行状态变化 ```bash python hub.py watch # 每 10s 打印一次状态变化 python hub.py watch --interval 5 python hub.py watch --once # 只打印一次当前状态 python hub.py watch --instance lan-189 --verbose ``` 只打印变化(新运行 / 开始运行 / 运行结束 / 会话消失),Ctrl+C 退出。 --- ## 七、并发安全机制(为什么不会突破上限) `dispatch` 的新建路径由**两级机制**保护,避免「检查 → 创建」之间的竞态: 1. **实例互斥锁** `~/.xaf_agent_hub/locks/.lock` - Windows 用 `msvcrt.locking`,POSIX 用 `fcntl.flock`,非阻塞轮询 + 超时(默认 20s,超时 → 退出码 3 并提示「未获取到调度锁」); - 持锁期间完成:读 `/api/session/active` → 统计 `running_cfc` → 加上有效在途预留 → 判定 → 创建会话 → 投递指令。 2. **槽位预留** `~/.xaf_agent_hub/slots/..slot`(JSON,含 pid / 创建时间 / session_id) - 判定式:`running_cfc + 有效预留数 >= concurrency_limit` 即拒绝; - 创建成功后写回真实 `session_id` 并重置计时,保留 `slot_ttl_seconds`(默认 60s)作为 `/api/session/active` 的传播窗口; - 超过 TTL、会话已不在 active 且超过 grace、或持锁进程已退出 → 自动清理,不会死占额度; - 失败/异常路径 `try/finally` 立即释放。 实测(3 个进程同时 dispatch,起点 `running_cfc=0`):**恰好 2 个成功、1 个退出码 3,复核 `running_cfc=2`**。 > 说明:`--dry-run` 只做预检(不做锁定),因此两次 dry-run 之间若有人真实调度,结果可能变化;真实执行时会重新加锁复核。 --- ## 八、退出码 | 退出码 | 含义 | 典型场景 | |---|---|---| | `0` | 成功 | 正常输出 / 投递成功 / dry-run 通过 | | `1` | 内部错误(未预期异常) | 打印简短原因,加 `--debug` 看调用栈 | | `2` | 实例不可达 | DNS/连接失败、超时、5xx、401 认证被拒、响应中断 | | `3` | 触发并发上限 / 未取得调度锁 | 目标实例 `running_cfc + 预留 >= 2`;或锁等待超时 | | `4` | 参数 / 配置错误 | 未知实例、`--text` 为空、配置 JSON 非法、生产未放行、导出格式不支持 | | `5` | 目标不是 CFC 会话 | 误操作非 CFC 会话(需 `--force`) | --- ## 九、导出产物 ```bash python hub.py sessions --out out/cfc_sessions.json --html cfc_sessions_report.html python hub.py sessions --out out/cfc_sessions.csv ``` - `out/cfc_sessions.json`:完整结构化数据(`generated_at` / `by_instance` / `unreachable` / `instances` / `sessions`); - `out/cfc_sessions.csv`:9 列清单,`utf-8-sig` 编码,Excel 双击可用; - `cfc_sessions_report.html`:**单文件、无外部依赖**(无 CDN / 无图片 / 无 `src=`),顶部统计卡(实例健康数、CFC 会话总数、正在运行数、各实例分布),下方按实例分组表格,输入框实时过滤标题 / 目录 / 会话 ID,深色标题栏 + 浅色内容,字体优先 `"Microsoft YaHei", system-ui`。 --- ## 十、故障排查 | 现象 | 排查方向 | |---|---| | `不可达: DNS 解析失败` | 域名写错或本机 DNS 不通;`pub-iwintrue` 必须用 **http** | | `不可达: 认证被拒 (HTTP 401)` | 密码错。检查 `config.local.json`,或用 `XAF_HUB_PW_<实例>` 覆盖 | | `不可达: HTTP 502` | 实例服务挂了 / 反代异常(检查端口是否为 4091 而非 4090) | | `不可达: 响应中断 (IncompleteRead)` | 实例端连接被截断;工具已重试 1 次并标记该实例不可达,不影响其他实例 | | `[并发上限] … 未获取到调度锁` | 另有 dispatch 正在执行;等几秒重试,或调大 `lock.lock_timeout_seconds` | | 满载但想给运行中的会话追加指令 | 用 `--session <正在运行的 CFC 会话>`(复用不占新额度) | | 明明目录对却复用了别的会话 | `--title` 会优先匹配已有会话,加 `--new` 强制新建 | | CFC 会话数明显偏少 | 检查 `cfc_roots` 是否遗漏新目录;或该目录被 `exclude_dirs` 命中 | | CFC 会话数明显偏多 | 兜底段 `cfc`/`caremate`/`xaf` 命中了无关目录,把该目录加到 `exclude_dirs` | | 服务端只返回 50 条会话 | `page_size` 被改小;工具默认显式传 `limit=8000` | | 投递成功但会话没反应 | 指令是异步的:`hub.py watch --interval 5` 观察;或该会话正被其他运行占用 | | 新建会话落在了错误目录 | 只有 `location.directory` 生效;`--dir` 必须属于该实例的 CFC 根目录 | 调试技巧:`instances` 看健康 → `status` 看负载/预留 → `sessions --instance X --json --limit 5` 看会话 → `--debug` 看调用栈。 性能参考(6 实例、约 2800 条会话,本机实测):`dispatch --dry-run` ≈ 3–5s、`status` ≈ 7s、`watch --once` ≈ 7.5s、`sessions` ≈ 7s。 `dispatch` 走「只取运行态 + 按需单查会话」的快路径,不再拉全量会话列表。 --- ## 十一、已知限制 1. **opencode 没有 `/api/version`**:版本号取自 `GET /api/info`(`{"version": "2.0.32", ...}`),无此端点时显示 `-`。 2. **并发判定依赖 `/api/session/active`**:槽位预留只能覆盖本工具的调度;若有人绕过本工具、直接用别的客户端开跑,实例端仍可能超载。运行中会话若连目录都查不到(`GET /api/session/:id` 失败),会按「占用额度」处理(fail-closed)并打印 `[warn]`,宁可误拦不漏拦。 3. 预留的传播窗口为 `slot_grace_seconds`(5s):会话创建后 5s 内若已结束且 `/active` 不再返回,会短暂计入占用(偏保守)。 4. **自动选实例**只按「运行中 CFC → CFC 会话数 → 名称」排序,不看 CPU/内存真实负载。 5. `--keyword` 只匹配标题,不搜会话正文。 6. 兜底路径段匹配是启发式:目录名含 `cfc`/`caremate`/`danshop`/`xaf` 作为独立路径段即判为 CFC;同名无关目录请加入 `exclude_dirs`。 7. `status` / `instances` / `sessions` 为统计 CFC 总数必须拉全量会话,耗时随会话数增长(约 7s)。 8. CSV 的 `消息成本` 来自 session 的 `cost` 字段,未开计费时恒为 `0.0000`。 9. 会话被删除后无法恢复;`dispatch --new` 会真实创建会话,请确认目录与用途;`cleanup --execute` 会真实删除过期 CFC 会话(默认仅预览)。 10. 生产实例(`prod-etotem`)默认只读,工具**不会**在没有 `--allow-prod` 的情况下向其发送任何指令。 11. **智能匹配是词面匹配**:仅基于会话「标题 + 目录」的字符 n-gram 相似度,不读取会话正文 / 消息历史;标题信息量过低或需求措辞差异过大时可能匹配不到(退化新建),此时可显式用 `--session` / `--title` 或调高 `match_threshold`。 12. `cleanup` 只枚举并清理 **CFC 会话**(按 `cfc_roots` 判定),非 CFC 会话不受影响;`time.updated` 缺失的会话会被跳过而非删除。 --- ## 十二、定时巡检(loop_resume)与派发任务池 ### 1) 巡检链路 `loop_resume.py` 是常驻/定时巡检编排器(WorkBuddy 自动化「XAF Hub 巡检·10分错峰01-06」每小时错峰跑 `loop_resume.py --once`)。每个周期依次执行: ``` fix_free_limit → check_agents → sync_design_tasks → fill_slots 额度自愈 执行情况检查 需求/设计进度扫描 任务池补位派发 ``` - **fix_free_limit**:撞上「免费额度耗尽 / 限流 429」的会话自动切链上下一档自愈;**若命中 opencode 家族模型的 429/限流,会把 opencode 全家族封禁至次日 08:00**(见「opencode 429 黑窗」节); - **check_agents**:检查各实例会话运行/完成/失败,恢复中断会话,产出巡检报告(含「待分派」节,读取任务池 + 主理人台账); - **sync_design_tasks(2026-10-25 新增)**:扫描需求/设计文档,按完成进度**生成 pending 任务**写入任务池(见下); - **fill_slots**:把每个实例的并发位补满——从 `tasks_backlog.json` 取 `status=pending` 的任务派发,成功后标 `dispatched`,会话结束且 `succeeded` 标 `done`。 可用开关:`--no-freelimit` / `--no-check` / `--no-design` / `--no-fill`(分别对应跳过某步);`--design-script` 可换扫描脚本。 ### 2) 派发任务池 `tasks_backlog.json` 自动任务池,字段:`id` / `title` / `priority`(P0-P3) / `module` / `instance`(空=通用) / `status`(pending|dispatched|done) / `spec`(设计文档相对路径) / `prompt`(派发正文) / `session_id` / `dispatched_at` / `dispatched_to` / `model` / `done_at`。`fill_slots` 按优先级取 `pending` 派发,三级取用:指定本实例 → 通用 → 借用其他实例(各实例都有完整仓库副本)。 **治理纪律(2026-10-25 定稿)**: - **未派发的任务必须进入任务池**(`tasks_backlog.json` 加一条 `pending`),由巡检自动补位,不要在群里口头记; - **人工直接 dispatch 的任务也应回填** `status=dispatched` + `session_id`(或先入池再让 fill_slots 派),保证池子与实际一致; - **主理人台账** `lead_backlog.md` 只放「等你拍板 / 决策类」事项,不放可自动派发的执行任务; - **所有会话完成一项任务后必须自行 `git commit` + `git push` 并回报 commit 哈希,不需要主理人确认**(合并主干仍需人工裁决)——本工具派给远程 opencode 智能体的**每一条**任务指令都必须带上这条纪律; - 纪律文案由 `discipline.py` 的 **单一事实源**统一注入派发链路,且按**目标实例的分支策略**决定口径: `sync_design_tasks`(生成任务的业务正文,纪律不再内嵌)、`fill_slots`(巡检补位)、`resume_interrupted`(恢复中断会话,`check_agents` 复用同一 `RESUME_PROMPT`)、`hub.py dispatch`(人工派发,dry-run 亦可见); `fix_free_limit` 的续做提示(`CONTINUE_PROMPT`)同步采用相同提交口径; 注入点收敛在 `scheduler.plan_dispatch`(目标实例经智能匹配/自动选路确定后才拼纪律),人工派发与巡检补位共用; `tasks_backlog.json` 顶层的 `discipline` 字段可作任务池级覆盖(按实例策略改写后注入)。 ### 2.4) 分支策略:local 只在主分支,不分配其他分支的任务(2026-10-27 主理人定稿) - **branch 模式(远端实例默认)**:基于 `origin/cfclub` 新建 `feature/ 分支`,完成后 push 自己的分支,**不合并主干**——隔离开发,由主理人裁决合并; - **mainline 模式(local 实例)**:直接 checkout 到 `cfclub` 主分支工作,**不新建任何分支**,完成后 push 到 `origin/cfclub`; - 判定口径:实例名以 `local` 开头即 mainline;也可在 `config.json` 实例级显式写 `"branch_policy": "mainline"`(显式声明优先,便于日后把某个远端实例也切到主分支口径); - **lan-204 已显式声明 `branch_policy: mainline`(2026-10-29 主理人定稿)**:204 实例分派时保持在 `cfclub` 主分支工作,不再新建 `feature/` 分支;其余远端实例(lan-248 / lan-189 / prod-etotem / pub-iwintrue)仍为 branch 隔离口径; -派发时 `discipline.rewrite_branch_to_mainline()` 会把任务书/规格文档里写死的「新建 feature/ 分支」类指令改写为主分支口径,剩余由追加的 mainline 纪律段声明覆盖; - **事实性引用不被改写**:`判定=实质完成:分支 feature/u3 @ 300dd0765`、「核验方违规在 feature/xxx 多推了 1 个空commit」这类核验结论只在其处于「新建/push/checkout/基于…」等指令上下文时才改写,避免历史记录失真; - **否定句受保护**:「不要新建 feature/xxx 分支」「不要新建任何分支」原样保留,不会被兜底正则改成病句; - 远端实例行为完全不变(仍为 `feature/` 隔离口径); - 验证脚本 `out/verify_branch_policy.py`(37 项断言)。 ### 3) 需求/设计进度 → 自动生成任务(sync_design_tasks) `sync_design_tasks.py` 实现「检查需求/设计情况,根据完成进度生成新任务」: - 扫描 `docs/superpowers/plans/` 与 `docs/superpowers/specs/` 下 **mtime ≤ 30 天** 的 `.md`; - 仅匹配**表格行**(含 ≥2 个 `|`)中同时含**待派发标记**(`待派发` / `未派发` / `⏳`)与**批次代号**(`F-?数字` / `T数字` / `W0.数字` / `批次` / `步骤` / `任务` / `阶段`)的行; - 取首个单元格为 `title`、`id` 取批次代号(无则 `AUTO--`); - 去重:同 `spec` + (`id` 或 `title`) 已存在则跳过,**只新增 pending、不自动关闭**(关闭以智能体汇报 / 主理人核验为准); - 写入 `tasks_backlog.json`(仅在有新增时改写)。 > 示例:CF 值统一方案文档把 F-8 行标为「待派发收尾」→ 巡检自动把 **F-8 清理** 作为 `pending` 任务入池,下一轮 `fill_slots` 即派发。 > 注意:池任务 `spec` 引用的设计文档**必须已提交到 `origin/cfclub`**,远端 opencode 才能读到;未提交的设计文档请先 commit,否则远程智能体会在 prompt 中被告知「文档不存在则停止并说明」。 ### 2.5) opencode 429 黑窗(2026-10-26 主理人定稿) > **口径:任务分派/模型切换时,无论哪个实例、哪个会话,只要 opencode 报 429(含免费档 > `free usage limit reached`、日文本配额、`rps exhausted` / `quota exhausted` / > `rate limit` 等以 429 呈现的限流),opencode 全家族模型(`opencode/` 与 > `opencode-go/` 前缀)当日一律不可用,直到次日 08:00 才解禁。** - **触发即生效**:`fix_free_limit.py` 命中即写 `.model_state.json` 的 `opencode_blackout_until_ms = 次日 08:00`(含 reason/source/时间戳,可审计);同日内重复触发只延长到次日 08:00,不累加。 - **派发/切换全链收敛**:`scheduler.resolve_instance_model` / `resolve_dispatch_model` 都走 `_blacked_out_models()` 排除黑窗内的 opencode 家族——`fill_slots`、`hub.py dispatch`、`fix_free_limit` 的切换目标、实例链顺延全部生效。 - **显式指定也不放行**:`resolve_dispatch_model(explicit=...)` 对 opencode 家族不再走 `ignore_blackout=True` 的旧语义(2026-10-24 为防「显式模型被策略链替换回 opencode」而设),否则 `config.default_dispatch_model = opencode/deepseek-v4.1-flash-free` 会绕过封禁把新任务继续派到已 429 的免费档;非 opencode 的显式模型仍保持「显式优先」语义。 - **非 429 不封禁**:`endpoint is unavailable` / `no-route` / `model unavailable` / `provider.internal` / `not available in the current token plan` 属「模型不可用」而非限流,只切下一档、**不触发全家族封禁`(常量清单见 `scheduler.RATE_LIMIT_PATTERNS`)。 - **写黑窗的四个入口统一到单一事实源** `scheduler.mark_opencode_blackout(reason, source)`:`fix_free_limit`(idle + running 两条路径)、`hub.py` 的 confirm-wait 配额降级、`confirm_session.py fix` 子命令。 - **修复的历史问题**:2026-10-26 12:54→13:02 出现「axon→切回 opencode free→openencode 又 429→切回 axon」的反复横跳——根因是只有 `confirm_session.py` 会写黑窗,巡检链(fix_free_limit 等)从不调用它;现已把写黑窗接到巡检链并改为「先命中→先写黑窗→再算切换目标」的判定顺序。 - 验证脚本:`out/_verify_blackout.py`(30 项断言:写入/解禁时刻、三个入口跳过 opencode、过期恢复、429 vs 非429特征边界、家族识别)。