feat(tooling): integrate automatic PI subagent executor into dev

This commit is contained in:
weichen
2026-09-24 05:54:59 +00:00
42 changed files with 6261 additions and 0 deletions
+5
View File
@@ -12,6 +12,11 @@
"type": "stdio",
"command": "codegraph",
"args": ["serve", "--mcp", "--path", "${workspaceFolder}"]
},
"pi-subagents": {
"type": "stdio",
"command": "node",
"args": ["scripts/mcp/pi-subagents-mcp.mjs"]
}
}
}
+23
View File
@@ -0,0 +1,23 @@
---
description: Route PI analysis/review/test subagent work through scripts/pi_subagents.py (not Cursor native agents).
alwaysApply: true
---
# PI subagents(工程工具)
本机 PI 子任务走 `scripts/pi_subagents.py` + 模型配置 `.doc/ops/2026-09-24-subagent-model-config.md`。
## 自动派发(主 Agent)
1. **预览配置**:`pi_subagent_roles` / CLI `roles`(不调模型)。
2. **按阶段选角色**:research → planning → reviews → diagnosis → test_run / acceptance_check → report;实施/修复仍由 Cursor Auto。
3. **派发 `run`**:携带 task_id、role_id、唯一 request_id、goal、authorization_ref、冻结 source_sha、input_files、timeout;test 角色另附 approved_commands + plan_digest。
4. **租约**:报告路径 `path:.doc/test-reports/assets/pi-subagents/<task>/<request>`;裸调拒绝。
5. **取回**:`pi_subagent_result` / CLI `result`;`run_status` 与 `verdict`/`review_state` 分离——模型成功仅 `pending`,测试 pass 仅来自批准命令证据。
6. **门禁**:审查/测试结论须带回报告路径与 receipt 字段;不得用模型叙事冒充 PASS。
## 边界
- **Cursor Auto**:唯一代码实施车道;本规则不把 implementation 派给 PI。
- **禁止**:自动换模型、M3 关键角色、伪造 lease、递归把本修复任务再派给 PI worker、`pkill -f` 广谱杀进程。
- **MCP**:`.cursor/mcp.json` → `pi-subagents`(roles/run/result);executor 来自 MCP 安装根。
+1
View File
@@ -33,6 +33,7 @@
| 我要… | 读这里 |
|--------|--------|
| Subagent 模型配置 / 流程 | [可调整模型配置](ops/2026-09-24-subagent-model-config.md) · [PI 执行器](ops/2026-09-24-pi-subagent-executor.md) · [流程草案](ops/2026-09-24-subagent-workflow-standard.md) · [研究依据](audit/2026-09-24-subagent-workflow-research.md) |
| 规则来源/生效版本 | [规范登记](ops/2026-09-21-policy-registry.md) |
| 选择测试/报告范围 | [验证契约](ops/2026-09-21-validation-contract.md) |
| 任务模板与会话收口 | [任务模板](ops/templates/short-term-task.md) · [收口清单](ops/templates/session-close-checklist.md) |
@@ -0,0 +1,39 @@
# PI Subagent 执行器初版边界审查
task_id:subagent-workflow-standard-2026-09-24;issue_id:ISSUE-20260924-pi-executor-boundaries。
owner:Cursor(修复)/ Codex(复核);issue_state:resolved;severity:high。
本记录针对尚未发布的执行器初版,不表示 dev 或产品 Runner 存在这些实现。
## 事实与机制
1. 独立验证发现 PI 内置工具的路径规范化与扩展守卫不一致。安装版本 0.85.1 的 tools/path-utils.js 中 resolveToCwd 使用 stripAtPrefix 并展开 home;初版守卫使用 path.resolve(root,input)。
2. 以专门创建的 /tmp/pi-guard-audit-* 临时目录及 outside.txt 假文件测试 @绝对路径,实际结果为 guard_blocked=false、actual_tool_path_escapes=true。没有读取真实密钥或业务文件。
3. 同类范围:@ 前缀、~ 前缀、相对父路径、符号链接、Unicode/读取路径规范化;MCP 的同仓/工作区验证和临时文件写入;报告幂等与损坏收据;进程树/输出上限/环境变量隔离。
4. 初版静态阅读另发现:跨仓 MCP 可能执行目标仓脚本;输出仅在 communicate 完成后截断;只回收仍存活 leader 的进程组;测试环境黑名单不足;模型报告用关键词推导 PASS。上述需要实现修复和负例,不以本行代替实测。
5. 初版测试夹具存在 eager stderr.read(),在 MCP 仍存活时阻塞。host 两次仅终止 /tmp/pi-subagent-test-* 的假 MCP 子进程以释放测试,未终止产品或用户进程。
## 复现与迭代
- 初次 RED:19 项,7 failures / 11 errors,主实现未创建;存在性/调用失败为已缺失功能,不当作实现通过。
- 初版回归:19 项,4 failures / 7 errors;耗时含夹具死锁处理。后续错误数不作为发布质量结论,待主机完整重跑。
- Cursor 首轮 supervisor 已由 host 为明确的修复交接发送 TERM,退出143;源文件保留。没有未知写副作用后盲目重派,第二轮是带具体修复清单的新执行。
- 本机 Qwen3.8-Flash、GLM-5.3-Flash 无工具短请求连通通过,实际 provider/model 与配置一致,正常 stop + agent_end/agent_settled。仅连接证据。
- GLM-5.3 对初版整包快照的只读审查 300 秒超时,无完整结论;不报审查 PASS。进程由 subprocess 超时处理结束,未授予工具,无仓库写入。
## 修复与验收要求
Cursor 修复清单已交接:统一/拒绝路径规范化;MCP 同仓注册验证和无租约写入;有界双向进程通信/整体回收;测试环境白名单;严格请求/配置与冻结 SHA;损坏收据禁止重派;报告完成与审查通过分离;最终事件完整性;禁用 PI 隐式重试;相关真实 Git/进程/MCP 回归。
关闭条件:静态发现均落实对应修复或有证据排除;原例与同类负例通过;真实 PI 调用经新执行器完成;审查与测试报告保存。修复中,不新增 validated 经验;验证后再总结。
## 2026-09-24 修复关闭
issue_state=resolved 仅针对未发布初版的约定缺陷;代码提交 69ffaa677e23a1dd9aa0528bb09b3acfcd38b119,主机完整工程回归 173 项通过,提交后专项 34 项通过;最终 Cursor CLI→MCP→PI 中 ls/read 全通过。实际覆盖、早期失败和独立 GLM 审查边界见[报告](../test-reports/2026-09-24-pi-executor-test-report.md)。
新增[经验](../ops/lessons/2026-09-24-pi-tool-boundaries.md)。本文件前面的“修复中/待验证”为初版历史,当前结论以本节为准。dev 集成/客户端 IDE 验收不由本问题关闭状态推导。
## cursor-cli-mcp-path
issue_id:ISSUE-20260924-cursor-cli-mcp-path;issue_state:deferred;severity:medium;owner:工程维护;日期:2026-09-24。
本机 Cursor CLI 2026.09.18 在项目 MCP args 中未展开 workspaceFolder 变量,原样传给 Node。pi-subagents 新入口已改工作区相对路径并实测通过。
同类只读检查:agent mcp list 也显示既有 agentctl / jev Connection failed,它们仍使用相同变量写法;codegraph ready。未进一步修改/验收这两个既有服务,也未断言 IDE 同样失败。官方支持声明与本机观测须分开。
此残差不阻塞 PI MCP(其内部直接调用主仓治理 CLI),不构成本轮产品发布结论;后续由工程维护在 Cursor CLI 升级或这两项 MCP 接入验收时检查路径/stdio 协议,形成独立修复与实际客户端证据。仅保留问题,不宣称已修复。
@@ -0,0 +1,111 @@
# Cursor Auto 与 Subagent 工作流研究及文档验证报告
日期:2026-09-24;owner:Codex;task_id:subagent-workflow-standard-2026-09-24。
范围:现行规范、Cursor 项目配置及官方文档;交付[规范草案](../ops/2026-09-24-subagent-workflow-standard.md)。未运行 Cursor 对话/真实 subagent,未修改产品或运行环境。
## 1. 研究结论
推荐主 Agent 协调、按需委派、独立审查与验证的流程。用户提出的八步可作为起点,但需在开始明确授权/验收标准,在方案阶段设计测试,在实施中自测,合并后核对实际运行结果,最后归档和清理。
| 方案 | 收益 | 代价/缺陷 | 结论 |
|---|---|---|---|
| 单 Agent 顺序完成全部阶段 | 简单、额外上下文成本低 | 实施者自审容易沿用错误假设 | 适合小型文档/研究;代码交付补独立审查 |
| 每阶段固定一个 Agent / 固定模型 | 阶段职责直观 | 无效启动和文档重复;固定模型背离 Auto;阶段间易丢授权/版本 | 不采用 |
| 主 Agent + 按需 analyst/reviewer/verifier | 保留上下文入口,独立检查,成本可控 | 需可靠派发包、证据接收和异常恢复 | 推荐;实现并行按风险逐步启用 |
这是本项目基于现状的设计判断,不是 Cursor 平台保证。
## 2. 官方能力与推论
查阅日期为 2026-09-24。在线文档会变化,客户端接入必须另验;不采用社区传闻作为技术契约。
| 来源 | 查证事实 | 对本规范的影响 |
|---|---|---|
| [Cursor Subagents](https://cursor.com/docs/subagents) | 自定义角色可位于 .cursor/agents;model 默认 inherit;readonly 与后台字段可配置;子上下文需要主 Agent 提供输入;默认共享 checkout | 采用继承策略、显式交接、仓库管理的写隔离;只读报告由主 Agent 保存 |
| [Cursor Models & Pricing](https://cursor.com/docs/models-and-pricing) | Auto 是平台模型路由选项,公开文档列有不同优化模式 | 规则不绑定模型、价格或固定路由;按实际 UI/账户能力记录 |
| [Cursor Rules](https://cursor.com/docs/rules) | 支持 AGENTS.md 和项目规则 .mdc | 根入口/客户端入口只指向单一规范,避免重复规则漂移 |
推论与限制:inherit 能表达继承意图,不足以证明实际模型身份或所有 Auto 请求的路由;独立上下文不保证跨模型独立性;只读权限与运行测试的写产物需要区别处理。Auto 可用性、角色加载、权限、真实派发及费用均未在本会话实测,不保证任何套餐或所有版本可用。云执行不在本次授权与建议默认范围内。
## 3. 当前仓库证据
检查基线:dev = 5423b261f97f371bdf7fa5066e9305f9eddaef42;本地 origin/dev 跟踪引用比较为 ahead 74 / behind 0。未 fetch,不能据此声明服务器当前引用。
- AGENTS / PROJECT:已定义授权分级、任务 worktree、独立状态、业务实施和研究边界。
- worktree 操作规范:已有 agentctl 租约、候选验证/合入、共享环境持锁、清理负例和收口要求。
- 验证契约:依据变更范围取并集,要求真实用户流程与持久证据;文档变更不要求产品回归。
- 文档地图/任务模板:已有最小文档原则,可以按阶段留证而不制造九份重复文档。
- .cursor/rules:已有 agentctl、文档、Git 和测试入口。rg --files .cursor 未发现项目 .cursor/agents 角色定义;不推断用户全局目录没有角色。
- .cursor/hooks.json:存在 beforeShellExecution 的 Git 守卫;未见本流程阶段完整性校验。此处仅配置阅读,没有实际触发 hook。
- cursor --version:3.21.16 / 8ae78e8eee1e63479c7e0504b664bc0a80c68000 / x64;agent --version:2026.09.18-9a7762b。CLI 版本不代表 IDE 当前会话配置已验收。
- agentctl status:当前配置 policy_revision 为 agent-governance-2026-09-23-fav-constraint-restored;存在其他任务持 env:dev。研究不使用该环境,不改他人任务。
已读规范修订与工具配置 revision 不同,记录两者分别是什么,不把一个全局字段视作所有 Markdown 修订版本。
## 4. ISSUE-20260924-cursor-test-rule-drift
issue_state:open;严重度:medium;owner:Codex(研究);evidence_basis:本次源码/配置阅读;产品影响未复现。
预期:客户端测试规则只引用验证契约,由适用范围和批准环境决定套件/Runner。
实际:.cursor/rules/test-reports-standard.mdc(alwaysApply=true)仍要求报告附全量 go test -v ./...,固定 MiniMax-M2.7-highspeed,并指向已不存在的 PROJECT §8。与验证契约中的范围选择、实际批准模型、普通小任务记录方式不一致。
已证实机制:客户端复制完整测试规则形成第二份来源,主规范修订后副本继续陈旧。未证实:实际 Cursor 是否因此执行了错误模型/测试、费用或运行行为是否受影响。
为何已有保护不足:静态读取显示没有针对该副本文案与验证契约的一致性证据;本次没有运行 hook/CI,不能断言所有自动防护都缺失。
同类检查范围:.cursor/rules 下五个规则文件及 hooks 配置、根 AGENTS/PROJECT、验证契约、规范登记、任务/收口模板。其余四个 mdc 主要转发入口;本次未检查所有其他客户端和全局规则。
推荐修复:将该 mdc 缩为验证契约与报告路径指针;结构要求归并现有权威文件,避免再复制模型与全量命令。不是改产品 Runner 模型或删除必要验证。
验收:规则文本无固定模型/无条件全量测试/失效章节;文档任务、Go 任务、产品 PI 任务分别映射到既有验证矩阵;Cursor 实际读取入口并正确选择验证层级。静态修复与实机行为分别报告。
时间线:2026-09-24 发现并登记;本次仅提出修复,未实施,issue 保持 open。无新 validated lesson;未验证修复不包装成经验。
## 5. 风险和关键设计
- 阶段和角色不是一对一关系:固定九名 Agent 会增加交接成本;初期三角色足够,主 Agent 可实施。
- 只读 reviewer 无法自行写审查文件:主 Agent 转存并保留作者/ID,避免“要求只读同时要求落盘”的矛盾。
- verifier 运行构建/测试会写缓存、数据甚至环境:单独授权,持对应租约,不能装作普通只读检查。
- 子 Agent 默认共享目录:采用单写者;并行写另建登记 worktree,主会话的自动隔离配置不替代仓库门禁。
- 审查和测试必须绑定版本:修复后重审受影响范围;候选变化重验;运行制品与报告提交分开。
- 清理在验收后:唯一日志/截图先归档,owner 退出后由维护者复查,不主动删除当前使用的工作区。
- 人工确认默认在可审方案形成后;已有端到端授权则沿用。用户未回复偏好,本草案采用该可调整默认,不把沉默当批准。
- 模型路由和自动审批不同:Auto 不扩大业务或环境权限,普通 Cursor 不套用 Herdr 固定模型。
- 阶段规范为 manual;文档审核通过不等于自动执行系统已实现。
## 6. 文档验收与未运行项
本次仅文档研究。静态检查结果在任务“验证与证据”节记录;检查对象为本次全部变更文件内容。
情景自审覆盖:研究-only、简单文档、标准功能、修复回环、并行冲突、readonly 转存、子任务未知、候选变化、验收失败、清理拒绝、上下文恢复。该项是文档推演,不能作为运行负例 PASS。
本次未执行:Cursor Auto 真实派发、Cursor 项目角色配置安装、并行写锁实测、业务测试、产品 PI、浏览器、部署和清理。新规范的 V1–V10 实机清单是后续接入验收项。
## 7. 文档情景自审记录
审查者:Codex 主 Agent;独立性:self_review。以下为阅读草案逐项推演,结果仅表示文字路径可执行,不表示已运行客户端。
| 情景 | 对照位置 | 结论 |
|---|---|---|
| 只研究方案,未授权实现 | §2、S3 | 可交付研究,其后标不适用;不会顺推业务实现 |
| 一处低风险文档修改 | §4、§7 | 主 Agent 自检,一份主文档可承载阶段,无需九个角色 |
| 代码已修改但审查未回收 | S5、§5 | 不进入集成;后台结果需接收 |
| 修复后旧审查/旧测试仍通过 | S5–S7 | 新版本需适用复核与验证,旧 PASS 不覆盖 |
| readonly reviewer 需要报告 | §4、§7 | 子角色回传,主 Agent 在租约内转存 |
| 测试生成缓存或写环境 | S6、§8 | verifier 可写但须明确产物范围/环境与租约 |
| 两个子任务争用同路径 | §5 | 单写者或独立登记隔离,不能无锁并发 |
| 子任务超时且副作用未知 | §6 | 保留 unknown,核对现场后决定恢复 |
| 合并前 dev 已变化 | S7 | 重建候选并重新验证 |
| 已合入但运行验收失败 | S8、S9 | 不宣布完成、不清理;修复/交接 |
| owner 仍在 worktree 或有唯一日志 | S9、§5 | 保留现场,归档后由独立维护者复查 |
| 会话中断后恢复 | §6、§7 | 从首个未满足门禁继续,核对未回收子任务 |
初次静态检查真实输出:`FILES=8 LOCAL_LINKS=84 STAGES=10 ISSUE_IDS=5 / PASS`。
命令:`git diff --check`(exit 0);Python 内联检查全部本次 Markdown 变更(含 untracked)的相对链接/锚点、S0–S9、规则状态字段和 issue ID 唯一性(exit 0)。不验证外部网页永久可达或实际 Cursor 行为。
只读 `agentctl docs drift`:status=ok、regressions=[];开放能力候选未修改,不等于产品就绪。
## 8. 后续决策:本机 PI 与可调整模型配置
用户在同一任务后续对话明确:子智能体采用 gmktec 本机 PI;仅允许 minimax-cn/MiniMax-M3、zai-coding-cn/glm-5.3、zai-coding-cn/glm-5.3-flash、qwen-token-plan-cn/qwen3.8-max、qwen-token-plan-cn/qwen3.8-flash。随后明确代码由 Cursor 实施,并因 M3 的实际使用反馈将其移出默认流程,最后确认该默认安排并要求可调整文档。
这些用户决策取代本报告前文 r1 中 Cursor 原生 .cursor/agents/inherit 的接入建议;前文保留为研究历史,不再作为当前实现方向。现行模型映射唯一来源为[模型配置](../ops/2026-09-24-subagent-model-config.md),流程草案同步到 r2。没有改变 Herdr 或产品 Runner。
2026-09-24 再次执行 pi --offline --no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files --no-approve --list-models,exit 0;精确五个 provider/model 均存在。此命令没有模型推理,不证明各角色能力或连接状态。配置字段由主 Agent 人工读取,当前不含执行器/热加载/自动校验实现。
## 9. 执行器已接入(后续实现)
用户进一步授权接入自动调用 PI 的执行器。代码提交 69ffaa677e23a1dd9aa0528bb09b3acfcd38b119,真实 Cursor CLI→MCP→PI 和 173 项工程回归通过,详情见[验证报告](../test-reports/2026-09-24-pi-executor-test-report.md)及[运行说明](../ops/2026-09-24-pi-subagent-executor.md)。前文关于未接入/未调用的说明属于当时研究阶段;当前交付范围与未覆盖项以上述新报告为准。完整流程草案审批、dev 集成和远端同步分别记录。
+3
View File
@@ -13,3 +13,6 @@
| ISSUE-20260924-admin-reset-dead | auth / adminResetPassword / T20-F4 / chenwei | 管理台重置密码死按钮;验收口令不可知后无法恢复 | high | resolved | Cursor | 2026-09-24;test 库实查 + API 单测 | [主记录](2026-09-24-admin-password-reset-issues.md#issue-20260924-admin-reset-dead) | 待补 LESSON |
| ISSUE-20260924-login-error-envelope | auth / AUTHZ / 无权访问 / auth.ts | 登录失败展示裸 JSON / 「无权访问」误导 | medium | resolved | Cursor | 2026-09-24;wire 复现 + 前端单测 | [主记录](2026-09-24-admin-password-reset-issues.md#issue-20260924-login-error-envelope) | 待补 LESSON |
| ISSUE-20260924-zombie-credentials | auth / user_credentials / LEFT JOIN / CreateUser | 空哈希静默建号导致永久无法登录 | high | resolved | Cursor | 2026-09-24;三环境只读盘点;机制修复不含存量清理 | [主记录](2026-09-24-admin-password-reset-issues.md#issue-20260924-zombie-credentials) | 待补 LESSON |
| ISSUE-20260924-cursor-test-rule-drift | Cursor / test-reports / 固定模型 / 规则副本 | Cursor 测试规则与验证契约存在固定模型及范围冲突 | medium | open | Codex(研究) | 2026-09-24;静态规则比对,客户端行为未复现 | [主记录](2026-09-24-subagent-workflow-research.md#4-issue-20260924-cursor-test-rule-drift) | 未修复验证,不新增 validated 经验 |
| ISSUE-20260924-pi-executor-boundaries | PI executor / 路径规范化 / MCP / 子进程 / 幂等 | 未发布初版只读边界与执行可靠性缺陷 | high | resolved | Cursor/Codex | 2026-09-24;173 工程回归与真实 Cursor CLI/MCP/PI | [主记录](2026-09-24-pi-executor-boundaries.md) | [工具路径与输入隔离](../ops/lessons/2026-09-24-pi-tool-boundaries.md) |
| ISSUE-20260924-cursor-cli-mcp-path | Cursor CLI / MCP / workspaceFolder | 既有 agentctl/jev 项目 MCP 入口在本机 CLI 连接失败 | medium | deferred | 工程维护 | 2026-09-24;CLI list/list-tools,PI 入口已修复 | [主记录](2026-09-24-pi-executor-boundaries.md#cursor-cli-mcp-path) | 沿用工具路径经验 |
+6
View File
@@ -45,3 +45,9 @@ CURRENT 是带时间的活跃协调快照;Git/机器状态用于核验实际
修订 ID:`codegraph-governance-2026-09-24-r1`;owner:`Cursor`;state:`active`(生效 tip `19eb56e3744d3c7d4b69b065c87e27689e8caa53` / fixup `0c676afe1faf831020c7c3201509d7cec0328d53`);工具能力:盘点命令与 hooks sync 为 `enforced`(脚本已落地);常驻 daemon 装载仍为 `manual`(本机默认不装载)。用户授权:将 CodeGraph 纳入治理启动/建场/sync,并对 worktree 清理与 FEATURE-MATRIX 做只读盘点提醒(不自动删除、不自动升 done)。
唯一来源增补:[AGENTS §1/§4](../../AGENTS.md)、[worktree 操作规范 §8](./2026-09-21-agent-worktree-workflow.md)、[收口清单](./templates/session-close-checklist.md)。命令入口:`agentctl cleanup report`、`agentctl docs drift`、`agentctl hooks codegraph-sync`、建场 `agent-worktree.sh create` 的 best-effort `codegraph init`。不改产品 SWBOOT-10 stub,不装载 systemd coordinator(需另授)。
## Subagent 工作流拟议修订
修订 ID:`subagent-workflow-2026-09-24-r2`;owner:Codex;state:`proposed`;能力:`manual`;effective_commit:无。
用户要求研究 Cursor Auto 下的 subagent 分工、阶段流程及报告,授权与本次验收见[任务](short-term/2026-09-24-subagent-workflow-standard.md)。[规范草案](2026-09-24-subagent-workflow-standard.md)为拟议行为的唯一来源,[研究报告](../audit/2026-09-24-subagent-workflow-research.md)给出事实和待修冲突,[模板](templates/subagent-stage-record.md)承载交接。
用户后续明确采用 Cursor Auto 实施 + gmktec 本机 PI 子任务,并批准默认模型分工及可调整文档。[模型配置](2026-09-24-subagent-model-config.md)为模型路由唯一来源,decision_state=approved;流程整体仍 proposed,未合入 dev。原 Cursor 原生角色/inherit 方案被替代。执行器已在任务分支实现并验证 Cursor CLI→MCP→PI,代码 69ffaa677e23a1dd9aa0528bb09b3acfcd38b119;[运行说明](2026-09-24-pi-subagent-executor.md)与[验证报告](../test-reports/2026-09-24-pi-executor-test-report.md)限定已验证的机器边界。dev 集成/完整流程审批仍独立记录;不以派发器可用宣称全阶段自动门禁。
@@ -0,0 +1,47 @@
# 本机 PI Subagent 执行器接入方案与实施记录
task_id:subagent-workflow-standard-2026-09-24;operation_task_id:subagent-workflow-standard-2026-09-24-executor。
用户授权:“接入自动调用 pi 的执行器”;既有约束:Cursor Auto 实施,PI 模型限[配置](2026-09-24-subagent-model-config.md),M3 默认停用。本次是同任务的实现续作。
## 目标与边界
Cursor 能通过项目 MCP 自动按角色调用 gmktec 本机 PI,读取配置,提供冻结输入,获得持久报告和可核验状态。CLI 同步提供可复现入口。
不修改产品服务/Runner、不操作真实业务数据、不安装 daemon、不 push。代码只在原登记 worktree 修改,当前 main 有其他任务的未跟踪 promotion receipt,保留不动。
本次复核 local dev=6858e21d477571696a8458032e2c884449e773fd;原任务 HEAD=52462b45e985f596b2a503fc70ac12c3bd5fe571;新增 dev 差异为 internal/squad,与本次脚本无交叉。没有重新建场或切换主目录分支。
## 分工与执行方式
- Cursor Auto:编写执行器、MCP 接口、权限限制与测试。host Codex:需求/契约、文档、证据核验、审查与受控提交。
- Cursor CLI 在已登记原任务 worktree、agentctl with 监督和精确路径租约内执行;不授予提交/合并/推送/环境操作权限。模型请求 Auto;实际底层模型不可见时记录 unknown。
- 本次使用 ce-work 的执行/验证思路、CodeGraph 定位、test-driven-development 和 writing-for-agents。仓库 AGENTS 的原任务 worktree/agentctl 规则优先于技能中 detached 临时工作区机制;使用仓库 supervisor 派发 Cursor,代码路径单写者,外层核对差异。该隔离不是 OS 沙箱。
- 范围:scripts/pi_subagents.py、scripts/pi-subagent-readonly.ts、scripts/mcp/pi-subagents-mcp.mjs、scripts/tests/test_pi_subagents.py、scripts/mcp/README.md、.cursor/mcp.json、.cursor/rules/pi-subagents.mdc,以及本任务相关 .doc 文档。
- 认领能力仅 docs:publish;不注册 env:* 或 git:dev。后续集成另按受控候选流程处理。
## 设计契约
1. 配置文档是唯一模型来源,每次派发解析严格表格,记录 revision/hash 和实际 provider/model。禁用、未知、循环、M3 关键职责路由失败关闭;没有静默备用。
2. 新鲜 PI 会话;冻结 source SHA 与显式 tracked 输入文件(拒绝秘密/业务 data/路径穿越/符号链接),上限限制。模型只得到选定输入与允许的只读工具,无任意 shell、源码写入或递归派发。
3. 测试角色仅运行主 Agent 显式提供、带来源的 argv 计划;独立源码副本、净化环境、超时和输出限制,模型不生成可执行命令。共享业务环境本版不接入,返回明确限制。
4. MCP 提供角色预览、运行、结果查询;跨 worktree 调用核对同仓与 task 注册;运行由权威 agentctl with 包裹,并由执行器校验继承租约。
5. 每个 request_id 绑定输入摘要;同请求重放只查询,变更载荷拒绝,running/unknown 不重派。每请求独立短范围 report 路径锁,跨调用者统一并发上限。
6. 运行前保存 running;完成保存 receipt.json/report.md,含命令、版本、输入摘要、配置摘要、状态/结论、报告来源。结果未知/失败不冒充通过,进程退出0不替代模型最终事件和必需测试结果。
7. timeout/cancel 处理进程组并回收;未知副作用保留现场。凭证仅由 PI 客户端读取,绝不进入模型输入/报告;测试子进程不继承模型密钥或租约身份。
8. MCP 用标准 stdio JSON-RPC,查询不被运行调用阻塞。项目规则告诉 Cursor 何时选择哪种 role_id,具体模型仍从配置读取。
9. 实施与修复始终交给 Cursor;PI 负责分析/审查/验证。模型结论和 acceptance_owner 的最终验收分开。
## 验证计划
- 隔离真实 Git/agentctl:注册/租约负例、路径范围、报告落盘、请求幂等、并发与异常退出。
- 本地假 PI 仅作为协议传输夹具:模型参数、权限开关、JSON 事件、错误/空响应/超时/子进程清理;不得当作真实模型能力验收。
- MCP 实进程:initialize、tools/list、roles、run、result;错误必须 isError。
- 实际本机 PI:至少一条真实分析请求与独立代码审查;模型/权限/配置/源码身份可追溯。不得把 CLI/MCP 接口实测写成 Cursor IDE 页面已实测。
- 主机核验聚焦测试与工程脚本套件,按失败证据补回归;无需运行无关产品全量 Go、重建服务或产品 PI。
- 收口保留运行结果和剩余条件,分别记录 local commit / integration / deployment / remote_sync / cleanup。
## 当前进度
2026-09-24:Cursor Auto 已在 agentctl supervisor 内启动;本轮尚未宣称实现完成。机器租约身份和终端输出由 host 核验,待实施结果后补测试与审查。
## 已验证结果
执行器代码已提交为 69ffaa677e23a1dd9aa0528bb09b3acfcd38b119。完整工程回归 173 项、提交后专项 34 项、真实 Cursor CLI→MCP→PI 自动路由与最终 ls/read 均通过,详见[报告](../test-reports/2026-09-24-pi-executor-test-report.md)。文档配置已改为程序读取;完整审批状态机仍人工/Agent 遵约。当前交付限本地任务分支,dev 集成等待共享环境验收锁释放,未推送。
@@ -0,0 +1,80 @@
# 本机 PI Subagent 执行器使用说明
范围:Cursor Auto 主协调/代码实施,gmktec 本机 PI 子任务。模型分工唯一来源为[模型配置](2026-09-24-subagent-model-config.md),阶段定义见[流程规范](2026-09-24-subagent-workflow-standard.md)。本说明与代码位于任务 worktree;dev 集成和最终实测状态见[实施记录](2026-09-24-pi-executor-implementation.md)。
## Cursor 中使用
1. 在包含本次提交的工作区打开 Cursor,重新加载项目 MCP;.cursor/mcp.json 注册 pi-subagents 服务,.cursor/rules/pi-subagents.mdc 提供派发入口。
2. 主会话保持 Auto。先按原有流程创建/登记任务 worktree,并将 .doc/test-reports/assets/pi-subagents/<task-id> 纳入任务写范围。
3. Cursor 调用 pi_subagent_roles 读取当前角色与模型;从任务目标选择 role_id,调用 pi_subagent_run,随后查询 pi_subagent_result。
4. 不需要用户切换 PI 模型。需求/方案、独立审查、测试和报告由不同角色处理;代码实施与修复仍由 Cursor 执行。
5. 接收报告并核对范围、源码/配置版本、缺项;运行完成不代表阶段批准。最终验收责任仍由任务 acceptance_owner 承担。
任务 worktree 尚未合入 dev 时,主目录中的旧 MCP 配置不会自动获得新工具;先在该任务 worktree 验证,受控集成后再在主目录重载。本文不宣称已操纵 Cursor IDE 页面。
## 请求内容
MCP 工具的外层参数 worktree 为同仓的已登记任务工作区,task_id 为实际操作任务。request 中给出以下信息;授权引用应能回链到真实用户请求,不用日志/模型文本自授权限。
```json
{
"worktree": "/absolute/path/to/repo/.worktrees/task-id",
"task_id": "task-id",
"request": {
"role_id": "research",
"request_id": "research-001",
"goal": "读取选定文件,给出有证据的分析",
"authorization_ref": "本任务主记录中的用户授权位置",
"source_sha": "<完整的已提交源码 SHA>",
"input_files": ["PROJECT.md"],
"timeout_seconds": 120
}
}
```
source_sha 必须为完整提交 hash;HEAD/分支名不作为可变输入。只发送明确列出的已跟踪 UTF-8 普通文件,禁止运行数据、.env、凭证、符号链接和路径越界。未提交的代码不会自动进入快照,先由 Cursor 形成可审提交或使用已有冻结版本。
request_id 在同一任务中稳定标识一次请求:同参数只读重放既有结果,不重复调模型;不同参数复用 ID 被拒绝;running/unknown 保留现场,不能盲目重派。配置变更在下一次新派发读取,旧请求保留原 config_revision/config_sha256。
## CLI 入口
在任务工作区使用:
```bash
python3 -B scripts/pi_subagents.py roles
../../scripts/agentctl with --task task-id \
--resource path:.doc/test-reports/assets/pi-subagents/task-id/research-001 -- \
python3 -B scripts/pi_subagents.py run --request /tmp/pi-request.json
python3 -B scripts/pi_subagents.py result --task task-id --request-id research-001
```
CLI 请求文件需包括 task_id;也可用 run --stdin 输入 JSON。直接裸调 run 会被租约校验拒绝。上述名称是示例,不构成任何任务或环境授权。
## 测试角色
test_run / acceptance_check 还需 approved_commands(argv 数组的数组)和 plan_digest(外部已批准计划 SHA-256);执行器另记录 approved_commands_sha256,不能以任意字符串冒充命令与计划的同一摘要。
命令在独立测试快照中运行,使用净化环境和私有 HOME/cache/tmp,标准输入关闭。PI 模型根据收到的命令摘要、退出码和日志引用分析,不生成或修改被执行命令;完整日志由主 Agent 核验并在需要时补充上下文。模型分析快照保持原始源码内容,与测试可写副本分离。必要的 stdout/stderr 保存到本请求报告目录以便核对,不仅保留一段摘要。
依赖未安装、用例缺失或命令失败须如实记录。本版不自动安装依赖,不直接连接共享 dev/test/main 或操作真实业务数据;相关检查继续由主 Agent 通过现有环境授权/租约流程执行。命令退出 0 只证明命令层执行成功;必需用例是否执行、SKIP、持久性/UI 和业务验收仍按验证契约核对。
## 证据与状态
输出位置:.doc/test-reports/assets/pi-subagents/<task-id>/<request-id>/。
- receipt.json:请求摘要、配置摘要、源码/输入文件摘要、配置与观察到的模型、运行状态、命令结果、工具调用摘要。
- report.md:可读报告;模型分析是审查输入,主 Agent 核对后再推进阶段。
- 命令日志:按收据记录的位置查询;报告目录受对应 path 租约保护。
- 模型私有 thinking 不作为归档材料。错误输出与日志按敏感信息规则处理;不保存凭证。
run_status 与 verdict 分开:模型正常结束可以是 succeeded,但非测试报告仍为 review_state=pending / verdict=not_applicable。测试报告的 commands_passed/pass 只针对提交的命令,不能直接赋值到任务的 review_state 或 acceptance。失败、取消、结果未知不能换成 PASS。
超时、取消和输出超限处理整个受控进程组。只读结果查询可推断 orphan 为 unknown,但不能通过查询无租约改写收据。错误前置检查未形成业务收据时,MCP 返回可核对的执行拒绝,不能让主 Agent 一直等待不存在的结果。
## 调整与边界
角色默认模型、enabled、升级角色和并发上限在模型配置文档维护。禁用角色前同步处理指向它的升级关系。程序仅接受当前明确批准的五个 provider/model 组合;扩大模型池还需用户授权并更新硬边界校验,不是任意改一行即放宽权限。
本工具提供配置解析、模型启动、有限工具/输入范围、租约、进程监督和证据保存;没有把全部 S0–S9 的审批实现成不可跳过的状态机。Cursor 通过规则决定何时调用;模型失败不会触发隐式换 provider 或降级到 M3。
进程组和工具权限限制不是 OS 沙箱。批准的测试代码与本机工程工具属于可信执行面;外部不可信项目/代码不能因使用本工具就获得运行授权。普通 Cursor/PI 配置与 Herdr、产品 PI Runner 各自独立。
@@ -0,0 +1,88 @@
# Subagent 模型分配配置
config_revision:`subagent-models-2026-09-24-r1`
owner:用户;维护执行者:当前任务主 Agent。
decision_state:`approved`(本会话确认);integration_state:`pending`;execution:`enforced`(本执行器已验证的配置/租约/工具/进程边界);阶段审批仍为 manual。
**这是模型分配的唯一配置文档。** 用户调整下列表格;主 Agent 每次派发前读取,并把解析结果写入[派发记录](templates/subagent-stage-record.md)。`scripts/pi_subagents.py` 已直接解析本文件,Cursor MCP 提供角色预览、派发与结果查询;每次新派发读取配置,旧请求保留旧摘要。实测与边界见[运行说明](2026-09-24-pi-subagent-executor.md)和[报告](../test-reports/2026-09-24-pi-executor-test-report.md)。完整流程见[协作规范](2026-09-24-subagent-workflow-standard.md)。
## 1. 日常调整入口
| 想调整什么 | 修改位置 | 生效方式 |
|---|---|---|
| 某角色的默认模型 | §3 对应角色的“默认模型”列 | 修改后递增 config_revision;下一次派发读取 |
| 复杂任务的升级去向 | §3 “升级角色”列 | 升级时保留原报告与原因;以目标角色重新派发 |
| 启停某角色 | §3 enabled 列 | false 时不自动派发;引用该角色的升级关系须同时调整;必需职责暂停或显式重新分配,不能跳过门禁 |
| 允许使用的 provider/model | §2 模型池 | 使用本机精确 ID;新增模型需用户明确调整授权范围并更新程序硬边界校验 |
| 并发量、失败处理 | §4 | 主 Agent 按配置与任务实际资源取更严格边界 |
| 只改本次任务 | 在任务主记录写 override | 记录用户指令、角色、原值/新值、期限;不改全局默认 |
文档中表格值为配置;其他文件只引用本文件,不重复维护模型名。用户可以直接编辑,也可以说“把本任务 code_review 改用 qwen_max”或“以后 report 都用 glm_full”。后者需按仓库 worktree/受控集成流程保存。
字段规则:enabled 只取 true/false;默认模型引用 §2 的 alias;升级角色引用 §3 的 role_id,或 none;缺字段、未知 alias、循环升级、角色越权时停止该派发并报告错误,不自行猜测模型。
## 2. 允许的模型池
主会话:**Cursor,模型选择 Auto**;工程子任务:**gmktec 本机 PI CLI**。这份配置不改变 Cursor UI 模型开关、PI 全局默认设置或产品 PI Agent Runner。
| alias | provider | model(精确 ID) | 使用限制 |
|---|---|---|---|
| qwen_max | qwen-token-plan-cn | qwen3.8-max | 可分配分析、规划、报告与复核 |
| qwen_flash | qwen-token-plan-cn | qwen3.8-flash | 默认检索、资料与初稿;复杂任务升级 |
| glm_full | zai-coding-cn | glm-5.3 | 默认评估、独立审查和复杂归因 |
| glm_flash | zai-coding-cn | glm-5.3-flash | 默认测试执行与证据收集;复杂判断升级 |
| minimax_aux | minimax-cn | MiniMax-M3 | 默认停用,仅明确启用后用于易核验辅助任务;不能承担关键判断 |
本机 provider 是 qwen-token-plan-cn;用户提到的 qwen-token-plan 在此明确映射到该通道。相同模型名出现在其他 provider 不构成可替换授权。M3 不进入规划、代码审查、根因裁决、最终验收或主力自动备用链;需要改变这些限制时由用户明确修订本节。
目录核对:2026-09-24,通过本机 pi 的离线 --list-models 核实以上五个组合。存在于目录不等于远端连通、工具调用、性能和任务质量已实测。
## 3. 角色路由表
代码实施与修复固定由 **Cursor Auto** 承担;下表只路由 PI 子任务。PI 不修改产品代码或测试源码,所需修复/新测试交回 Cursor。
| role_id | 工作 | enabled | 默认模型 | 升级角色 | 执行边界 |
|---|---|---|---|---|---|
| research | 检索、代码定位、资料摘录、需求初稿 | true | qwen_flash | planning | 只读;结论回传主 Agent |
| planning | 需求分析、方案制定、复杂问题综合分析 | true | qwen_max | none | 只读;保留假设与依据 |
| requirements_review | 独立评估需求与影响 | true | glm_full | none | 新上下文;不代替用户取舍 |
| plan_review | 独立方案审查 | true | glm_full | none | 不自审自己编制的方案 |
| code_review | 代码审查与回归风险检查 | true | glm_full | none | 冻结 diff;不修改被审内容 |
| diagnosis | 复杂失败归因、跨模块机制分析 | true | glm_full | none | 只读分析;复现操作由 test_run 在授权环境执行 |
| test_run | 按已批准验证计划执行测试、收集证据、初步分析 | true | glm_flash | diagnosis | 仅批准命令、测试产物与环境;不修改源码 |
| acceptance_check | 按既定 AC 执行检查、核对真实证据 | true | glm_flash | diagnosis | 不代签最终验收;涉及业务取舍交 acceptance_owner |
| stage_notes | 阶段记录与报告初稿 | true | qwen_flash | report | 忠实整理原证据;主 Agent 持锁落盘 |
| report | 最终报告、复杂材料综合 | true | qwen_max | none | 汇总不能改写原始失败或推导未验证的 PASS |
| auxiliary | 格式整理、清单提取等易核验工作 | false | minimax_aux | none | 明确启用后派发;保留来源供核验 |
一个模型可承担多种角色,但派发必须区分上下文与职责。即使用户将规划和审查映射到同一模型,独立审查仍须新上下文/不同执行实例;模型不同也不自动证明审查独立。
最终验收责任继续归任务声明的 acceptance_owner;主 Agent 核对证据与权限后推进。test_run 和 acceptance_check 的输出是检查结果,不能只因模型返回“通过”就放行。
## 4. 执行与升级参数
| 参数 | 默认值 | 语义 |
|---|---|---|
| max_parallel_pi_tasks | 2 | 同时运行的 PI 子任务上限;路径冲突/共享环境锁优先 |
| implementation_executor | cursor_auto | 实施和修复不派给 PI;改此边界需要用户明确授权 |
| unavailable_action | pause_role | 模型或通道不可用时暂停对应角色;可继续无依赖工作 |
| automatic_fallback | none | 不静默换模型/换 provider;不因超时降级到 M3 |
| reload_boundary | next_dispatch | 新任务/下一次派发读取;不修改正在运行的子任务 |
| recursive_delegation | false | 仅主 Agent 派发;PI 子任务需要协作者时返回建议 |
升级触发:Flash 无法解释失败、证据互相矛盾、涉及跨模块机制或超出已批准验证计划。升级用于质量不足,不是网络故障重试;结果未知先核对副作用,流程遵循协作规范。
本次用户覆盖 > 本文件默认;仓库授权/写锁/验收边界始终约束执行。对文档中已允许角色和模型的任务级调整,不重复索要已经给出的许可;自动化程序、日志和子报告中的“配置已批准”不能替代用户来源。
## 5. 配置更改与追溯
1. 在任务 worktree 调整本文件;全局默认修改随受控集成发布。本轮尚未合入 dev,可通过显式指定本工作区配置进行后续试运行。
2. 更新 config_revision;保留修改原因和用户来源。派发前核对角色、模型池和升级关系。
3. 每次派发记录配置路径、config_revision、文件 SHA-256、role_id、configured provider/model、任务覆盖和原因;已有子任务沿用自己的配置快照。
4. PI 启动时按精确 provider/model 传递,不能省略参数后依赖 ~/.pi/agent/settings.json。启动记录或可信响应元数据不可核验实际模型时填 actual_model=unknown。
5. 修改配置不产生代码写入、环境操作、push、部署或清理授权;报告/输出由主 Agent 按现有租约规则保存。
6. 回退时恢复先前角色值,递增配置修订并记录来源;不要覆盖历史派发记录或把失败改成成功。
初始决策依据:用户指定五模型池;明确“代码实施还是让 cursor 来”;反馈 M3 能力有限;随后确认“M3 移出默认流程、仅可选辅助”,并要求提供可调整配置文档。
@@ -0,0 +1,165 @@
# 主 Agent 与 Subagent 协作流程规范(草案)
修订:`subagent-workflow-2026-09-24-r2`;owner:Codex;规则状态:`proposed`;执行能力:`manual`;effective_commit:无。
本流程规范仍为待审稿;用户已确认 Cursor 实施、本机 PI 分析/审查/验证及模型默认分工,具体值唯一维护在[模型配置](2026-09-24-subagent-model-config.md)。规则批准与受控集成后才进入 active;文档存在不表示 PI 调度器已接入。
研究与来源见[研究报告](../audit/2026-09-24-subagent-workflow-research.md),本次授权及交付状态见[任务](short-term/2026-09-24-subagent-workflow-standard.md)。
## 1. 目标、适用范围和权威
用户在 Cursor 主会话保持 Auto;Cursor 负责主协调、代码实施和修复,通过 gmktec 本机工程 PI CLI 委派分析、审查、测试和报告。每阶段产生可恢复记录。此配置不自动适用其他客户端或 Herdr。
本规范只定义角色、阶段、交接和证据引用。授权边界沿用 [AGENTS](../../AGENTS.md),写锁/候选合并/环境/清理沿用 [worktree 操作规范](2026-09-21-agent-worktree-workflow.md),测试层级沿用[验证契约](2026-09-21-validation-contract.md),问题闭环沿用[既有规范](2026-09-23-issue-learning-workflow.md)。不建立第二套测试门禁。
普通 Cursor 会话不启用 Herdr 角色和固定模型路由;只有明确使用 Herdr 时适用其专门规范。工程 subagent 与产品 PI Agent Runner 分开记录,前者的执行不能替代后者的验收。
## 2. Auto、授权和推进规则
- **模型策略**:主会话选择 Auto;PI 子任务按[模型配置](2026-09-24-subagent-model-config.md)的角色、provider 和 model 派发。Cursor 原生 subagent 的 model: inherit 方案已被用户后续选择替代,本规范不以 .cursor/agents 实现 PI 路由。
- **能力核验**:首次接入、客户端升级或模型行为异常时,记录客户端版本、角色可发现性、派发 ID、工作目录、配置模型策略及可观察的实际模型;不可见时写 `actual_model=unknown`。不得从自我介绍推断模型。
- **证据边界**:记录配置指定值与可核验的实际模型;未核验不声称模型调用成功。模型多样性不替代独立上下文和证据。
- **授权策略**:Auto 只表示模型选择,不表示自动授权 shell、外部写入、合并、部署、推送或删除。任务开始记录已有授权,后续不重复询问相同范围。
- **默认确认点(待用户采纳)**:需求形成、评估、方案编制可连续进行;方案可审后,一次确认方案与实施范围。已明确授权按该方案端到端执行,则该确认已经满足。只有范围/环境/外部副作用扩大、破坏性动作或需要用户作业务取舍时再确认。
- **验收人**:需求阶段明确 acceptance_owner。技术验收检查可由获授权 acceptance_check 执行;产品效果/业务取舍需要用户判定时,技术通过后保留 `acceptance=pending`,不由 Agent 代签。用户预先委托按明确客观标准验收时,记录委托来源和证据即可。
- 仅请求研究/方案的任务交付可审产物后结束。实现、运行验证、合并等阶段逐项写 `not_applicable + 原因`,不为了流程齐全擅自执行。
## 3. 标准流程
```mermaid
flowchart TD
A[建场与授权记录] --> B[形成需求和验收标准]
B --> C[评估需求与影响]
C --> D[制定方案和验证计划]
D --> E{实施授权是否满足}
E -->|是| F[实施和开发自测]
E -->|否| P[交付可审方案]
F --> G[独立代码审查]
G -->|需修改| F
G -->|通过| H[冻结交付版本并验证]
H -->|失败| F
H -->|通过| I[构造候选并验证合并结果]
I -->|通过| J[受控合入 dev]
J --> K[按授权部署与运行验收]
K -->|失败| R[保留现场并修复或交接]
R --> F
K -->|通过或不适用| L[确认交付并归档证据]
L --> M[owner退出后独立复查清理]
```
测试计划在方案阶段形成;自测贯穿实施;独立审查和必要测试必须覆盖最终版本。清理安排在验收与证据保存之后。合并前的工程验证与合并后的运行验收分别报告。
### 阶段、产物和放行条件
每行都是阶段记录要求,不要求每行一个文件。记录使用[阶段模板](templates/subagent-stage-record.md)。
| 阶段 | 责任与输入 | 最小产物 | 放行条件 / 失败去向 |
|---|---|---|---|
| S0 建场 | 主 Agent;用户请求、当前规则、Git/认领状态 | task 主记录:目标、授权、基线、写路径、环境、交付终点、policy_revision | 允许范围明确;持久成果在任务 worktree;按现行入口登记 |
| S1 形成需求 | Cursor 与用户,可委派 research/planning;原始描述 | 需求记录:用户场景、范围/非目标、R-ID、AC-ID、优先级、验收人 | 可验证预期明确;影响方案的歧义解决,小型假设标注 |
| S2 评估需求 | requirements_review;S1 和当前证据 | 评估报告:现状差距、替代方案、依赖、风险、复杂度和建议 | 结论为 proceed / revise / defer;高风险未知有验证路径;revise 回 S1 |
| S3 制定方案 | Cursor 协调 planning 编制、plan_review 独立复核;S2 | 方案:改动位置、接口/数据影响、实施依赖、写者、验证矩阵、恢复方案;附方案审查结论 | AC 映射到验证项;必要权限齐备;授权不足交付方案等待;设计疑问回 S2 |
| S4 实施与自测 | Cursor;已授权方案 | 实施记录:变更、提交、偏差、自测与失败记录 | 实现满足方案且自测有证据;范围变化回 S1/S3;不得降低 AC 迁就实现 |
| S5 独立代码审查 | code_review;需求/方案、base/head、完整差异 | 审查报告:范围、审查者、严重度、位置、依据、处置与复核 | 无未解决阻断项;修复回 S4,变更后的代码需复核 |
| S6 交付验证 | test_run;冻结版本、AC、验证契约 | 测试报告:预期用例、实际命令/输入/输出、环境、SHA、PASS/FAIL/SKIP/未运行 | 适用必需项均通过;SKIP/基础设施失败保留阻塞;失败回 S4 或 S3 |
| S7 候选与集成 | 串行集成执行者;S5/S6、最新 dev D、冻结 T | 集成报告:D/T/M、候选验证收据、锁内结果、实际 dev | 按现行 --no-ff 候选与 --ff-only 入口;D/T/M 变化须重新准入 |
| S8 运行与交付验收 | acceptance_check + acceptance_owner;受控制品、AC | 验收报告:实际制品、环境、用户流程、持久状态、逐项 AC 结论 | 所需运行/UI 验收通过;无运行影响写 not_required;失败不清理、不冒充完成 |
| S9 归档与清理 | 主 Agent 固化证据,owner 退出后维护者执行清理 | 收口报告:提交/集成/部署/远端/cleanup 分离;归档证据、清理结果与遗留 owner | 满足现行普通 remove/-d 条件;拒绝保留 pending,登记触发和接管者 |
S5 与 S6 可以在同一不可变版本上并行;必要产物必须都回来才能进入 S7。测试产生构建目录/缓存/数据也算写副作用,先核对其环境和锁。code_review 阅读稳定 Git 快照,不在 Cursor 正在改写的工作树上宣布最终通过。
S7 候选需执行该范围的真实检查,不能用分支自测替代合并结果验证。S8 后新增的报告提交也需受控集成;分别记录被测业务 SHA 与报告 SHA,说明仅文档变化的复用依据。影响运行内容的后续变更使相关验收失效。
## 4. 角色和分工
角色是职责,不要求每阶段单独启动一个进程。精确 role_id、enabled、默认模型和升级链只维护在[模型配置](2026-09-24-subagent-model-config.md),下表说明权限边界。
| 角色 | 职责 | 写权限和独立性 |
|---|---|---|
| Cursor / coordinator | 与用户对话、授权核对、任务图、派发、证据汇总、冲突处理、推进门禁和交接 | 唯一任务协调者;报告汇总不改变原作者/证据/结论;不能从子报告推导额外授权 |
| research / planning / report / stage_notes | 需求影响、方案、资料和报告 | 默认只读;返回结构化报告,由主 Agent 落盘;若编制方案,另一个上下文审查方案 |
| Cursor / implementation | 实施方案、修复问题、编写和修改测试源码 | 产品和测试源码的实施者;按认领/租约操作,PI 建议交回 Cursor 处理 |
| requirements_review / plan_review / code_review / diagnosis | 独立评估、审查与复杂故障归因 | 新的只读上下文;不修改被审内容;发现问题返回 Cursor;复现交给获授权的 test_run |
| test_run / acceptance_check | 按冻结 AC 和已批准计划执行验证,核对真实行为 | 仅写批准的测试产物/环境;不改源码、不代签最终验收,需要修复交回 Cursor |
| integrator / maintainer | 候选验证、受控合并;owner 退出后的清理 | 串行短时角色,使用现有工具/权限;不能把长驻 worker 同时当作可删除的 worktree owner |
同模型的独立上下文可以提供独立检查,但不消除共同盲区;验证仍以源码、命令和用户流程证据为准。主 Agent 自审只能记录 self_review。要求独立审查的代码任务如无 subagent 能力,交给另一个独立会话/人员;该项完成前不自动放行。低风险纯文档可自检并记录理由。
## 5. 何时派发与如何并行
- 适合派发 PI:较长只读研究、独立审查、按批准计划验证、报告整理。代码实施和修复保留给 Cursor。
- 主 Agent 直接处理:一次定位、简短文案、单一顺序操作;仍留下对应阶段记录。无需为了覆盖阶段机械启动九个 Agent。
- 并发上限及递归委派规则以模型配置为准;资源不足串行。它是项目预算,不是平台上限;子 Agent 发现进一步分解需求先返回建议。
- 先画依赖再并行;有先后依赖的任务等待前置证据。有相同文件、公共 lockfile、路由、迁移号或共享环境的写任务不能同时认领。
- 同一任务 worktree 的源码写者为 Cursor,PI 只读任务读取冻结快照;只读报告由 Cursor 持锁保存。PI 验证涉及产物/环境写入必须在有效 agentctl supervisor 下运行;无法保证时停止执行并报告条件缺失。
- 若另行开展多 Cursor 并行实施,仍须独立登记 worktree,各自从明确 dev SHA 创建;父子关系用 parent_task_id/subtask_id 关联。此条不授权 PI 实施代码。
- 父任务汇总子任务成果,源码仍由 Cursor 实施。涉及测试产物写入的子任务登记独立操作 ID/路径;只读子审查复用父任务文档,不重复创建任务链。子分支结果通过现行候选集成流程交付;不能由 Cursor 的自动隔离/自动合并绕过本仓注册和准入。
- 有后台任务时,主 Agent 可继续独立工作,但不得在结果未回收时发布依赖它的 PASS、合并或清理。各子任务的工具可用性不代表授权可用性。
## 6. 派发、回传与失败处理
每次派发至少包含:
1. parent_task_id、subtask_id、role_id、具体目标、输入版本(需求/方案/代码 SHA);配置路径、config_revision、文件 SHA-256、解析后的 provider/model 和任务覆盖依据。
2. 需要读的规范和证据、已有用户授权来源、明确的非目标。
3. 工作目录、允许写路径、环境、mutex/租约执行方式;只读者明确只读。
4. 完成条件、输出格式/报告路径、需要验证的 AC;子 Agent 不依赖未传入的主会话历史。
5. 预算(时间或步骤)、进度回传点、失败/超时和副作用核对方式。
回传至少包括:执行身份/PI run_id、配置版本与指定/实际模型、实际输入版本、结果与证据、变更文件/提交、实际命令与环境、结论、缺项/风险、遗留进程/产物。主 Agent 校验路径、版本和证据后标记 accepted;子 Agent 自报 done 只表示执行结束。
- 分开记录 run_status(running/succeeded/failed/cancelled/unknown)与 verdict(pass/changes_required/blocked/not_applicable),不可把退出成功等同质量通过。
- 超时/失联先检查进程、worktree、Git 和外部副作用;结果未知时保留 unknown,不同时重派同一写任务。纯只读且确认无副作用的失败可有限重试,并保留原记录。
- 审查/验证角色有异议时,主 Agent 依据原需求和证据裁决。能修复则回 S4;需要业务取舍则向用户汇总一个关键问题,不以多数投票覆盖阻断证据。
- 连续两轮修复仍未解决同类阻断时,停止机械重试,回 S2 重查机制/测试前提;授权内可继续研究。不得为放行删除失败用例或降低 AC。
- 用户中断、客户端退出、上下文压缩时,先固化阶段、版本、未回收子任务、owner 和接管条件;恢复后从首个未满足门禁继续,不能整条流程重跑或直接跳到收口。
## 7. 文档与报告契约
“一步一记录”,无需“一步一文件”。现有文件优先复用,原始证据只存一处:
| 任务类型 | 最小文档集合 |
|---|---|
| 信息问答,无持久成果或运行副作用 | 对话答复 |
| 小型规范/研究/低风险变更 | 一个 task 或分析主文档,含适用阶段、验证与交接 |
| 标准功能/缺陷修复 | task + 方案 + 报告;评估/实施/审查/测试/验收可为清楚分段 |
| 跨模块、迁移或部署 | task + 必要 plan + 分开的审查/测试/验收证据;handoff 仅在独立信息有价值时拆分 |
每份阶段记录包含 stage_id、输入版本、producer、reviewer/独立性、decision、证据链接、缺项和下一步。需求用 R-ID,验收用 AC-ID,测试用 T-ID;报告提供 R → AC → 改动 → 审查 → 测试 → 运行验收映射,避免仅列“文档已生成”。
路径沿用 .doc taxonomy:任务在 ops/short-term,研究/审查在 audit,方案在 remediation 或既有 ops 主文档,独立测试/验收在 test-reports,附件在其 assets。只读 subagent 由主 Agent 保存原始结论及必要摘录,标明“由主 Agent 转存”与 agent_id;不伪造子 Agent 已写文件。
任务 status 继续只有 planned/in_progress/blocked/done/cancelled;phase 与 review_state、integration_state、deployment、cleanup、remote_sync、acceptance 独立记录。done 取决于授权交付终点。清理待办不扩充任务状态,但必须有人接管。需求验收 pending 时,若验收属于授权交付终点,任务不能 done。
## 8. Cursor → 本机 PI 接入(任务分支已实现)
### 配置与能力边界
用户已明确选择 Cursor Auto 主协调和实施、gmktec 本机 PI 承担子任务;原 r1 的 Cursor 原生 .cursor/agents + inherit 方案废止,历史研究保留说明。当前[模型配置](2026-09-24-subagent-model-config.md)可人工调整,由执行器每次派发读取;不是 PI 原生配置格式,不改变 PI 全局模型设置。CLI/MCP 接入与限制见[运行说明](2026-09-24-pi-subagent-executor.md),实际验证见[报告](../test-reports/2026-09-24-pi-executor-test-report.md)。
PI 模型和角色路由以配置文档为唯一来源。主 Agent 根据任务读取 role_id,显式传入 provider/model、独立会话、输入版本、工作目录与工具范围,收取结果后按本规范形成阶段记录。不能把主会话可用工具全部自动授予子任务。
- 只读研究/审查不需要 bash/edit/write;后续接入时显式限制工具,并核对扩展是否引入额外能力。
- 测试角色需要执行批准的命令并产生缓存/产物,不能假称只读;将测试工作目录、数据和环境放在批准范围内,使用既有 agentctl 租约。
- PI 报告由主 Agent 持锁落盘;PI 不修改业务代码或测试源码。将诊断建议交回 Cursor 实施。
- 模型不可用、实际模型不符、配置缺失或未知结果遵循配置和 §6,不隐式换通道或降级。
- 普通本机 PI 调度不复用 Herdr 自动审批/模型预算;产品 PI Runner 仍按目标环境批准配置独立验收。
### 后续接入步骤与验收
| 步骤 | 变更 | 必需证据 |
|---|---|---|
| A 文档配置 | 已确认分工,维护模型配置/派发模板;流程整体另按规范登记 | 默认值与用户选择一致;修改可追溯;未合入前不称全仓 active |
| B 消除旧规则冲突 | 修订 Cursor 测试规则副本为验证契约指针 | 相关 ISSUE 单独修复验证;本次不改变 Runner |
| C 实现派发入口 | 已实现 CLI + 项目 MCP;本地任务分支代码 69ffaa67 | 配置/租约/模型/快照/结果校验与异常回归已验证;dev 集成另记 |
| D 实机试运行 | 小型文档任务和受控代码任务(代码由 Cursor 写) | Auto 不需手动切模型;PI 角色/模型/run_id/证据可追溯;不冒充真实产品 PI |
| E 异常演练 | 同路径写冲突、PI 失联、必需测试缺项、dev 变化、验收失败、清理拒绝 | 正确暂停/升级;保留失败证据 |
| F 工具门禁 | 据试运行决定是否增加阶段校验/hook | 实现和真实负例验收后才将相应能力标 enforced |
接入验收 V1–V10:V1 Cursor Auto 与配置指定 PI provider/model;V2 授权/需求上下文传递;V3 独立只读审查与报告转存;V4 租约内测试;V5 输入 SHA 变化后复核;V6 写冲突拒绝;V7 缺项不合并;V8 验收失败保留现场;V9 清理拒绝有 owner;V10 中断后恢复。新增 V11:修改配置只影响下一次派发、记录 hash;V12:M3 停用/越权路由及未知 alias 被拒绝。本轮执行器已完成 173 项工程回归与真实 Cursor CLI→MCP→PI 验证,覆盖及未覆盖项以测试报告为准;这不是宣称全部 S0–S9 审批、产品 UI/共享环境或清理已自动执行。
### 日常使用与调整
用户只需保持 Cursor Auto,说明任务目标与授权终点;主 Agent 读取本规范和模型配置,按需要派发 PI。修改默认分工只编辑模型配置文档;临时调整写入任务 override,不静默改全局默认。流程、模型配置和某任务授权是三个不同对象。
用户可以授权“按已确认方案执行到 local dev 集成与 dev 验收”;该授权不会自动包含 push 或晋升 test/main。验收后归档证据,当前 owner 退出后再由独立维护者复查清理。
回退:调度入口或权限不满足时暂停相应 PI 阶段,保留现场与证据,交回 Cursor 处理;不能以主 Agent 自审替代要求的独立审查。修复接入后再验证。
+4
View File
@@ -18,6 +18,10 @@
| owner | task | branch | worktree | mutex | status |
|---|---|---|---|---|---|
| Codex | subagent-workflow-standard-2026-09-24-land | feature/subagent-workflow-standard-2026-09-24 | .worktrees/subagent-workflow-standard-2026-09-24 | pi-subagent-integration | in_progress |
| Cursor/Codex | subagent-workflow-standard-2026-09-24-executor | feature/subagent-workflow-standard-2026-09-24 | .worktrees/subagent-workflow-standard-2026-09-24 | pi-subagent-executor | done |
| Codex | subagent-workflow-standard-2026-09-24-config-r2 | feature/subagent-workflow-standard-2026-09-24 | .worktrees/subagent-workflow-standard-2026-09-24 | subagent-workflow-docs | done |
| Codex | subagent-workflow-standard-2026-09-24 | feature/subagent-workflow-standard-2026-09-24 | .worktrees/subagent-workflow-standard-2026-09-24 | subagent-workflow-docs | done |
| Cursor | integrate-gate-nightly-fixes-b-2026-09-24 | feature/integrate-gate-nightly-fixes-b-2026-09-24 | .worktrees/integrate-gate-nightly-fixes-b-2026-09-24 | git:dev | done |
| Cursor | integrate-gate-nightly-fixes-2026-09-24 | feature/integrate-gate-nightly-fixes-2026-09-24 | .worktrees/integrate-gate-nightly-fixes-2026-09-24 | git:dev | done |
| Cursor | integrate-docs-gate-promo-report-2026-09-24 | feature/integrate-docs-gate-promo-report-2026-09-24 | .worktrees/integrate-docs-gate-promo-report-2026-09-24 | git:dev | done |
@@ -0,0 +1,26 @@
---
lesson_id: LESSON-20260924-pi-tool-boundaries
lesson_state: validated
owner: Cursor/Codex
reviewer: Codex
last_reviewed: 2026-09-24
reviewed_revision: 69ffaa677e23a1dd9aa0528bb09b3acfcd38b119
source_issues: [ISSUE-20260924-pi-executor-boundaries]
keywords: [PI, MCP, readonly, path, snapshot, process-group]
---
# 文件工具的路径检查须匹配实际客户端语义
触发:为第三方 Agent 包装只读文件工具、复制输入快照或监督子进程。
机制:PI 0.85.1 工具会展开 @/~,初版守卫只做 path.resolve,检查对象与实际打开对象不同;另有 exact-root 空相对路径误拒、内部扩展复制进模型快照导致合法枚举被白名单拒绝。见[问题](../../audit/2026-09-24-pi-executor-boundaries.md)与[验证](../../test-reports/2026-09-24-pi-executor-test-report.md)。
实施前检查:
1. 读实际工具的路径规范化实现;拒绝不支持的前缀/参数,核对 canonical realpath、符号链接与选定文件。不能仅假设安全的相对拼接等于最终访问路径。
2. 模型输入、测试可写副本、客户端内部配置/扩展分开;允许清单只包含模型该读的材料。内部文件不要塞进源码快照。
3. 同时验证拒绝路径和合法根目录/子目录、默认 path;只有负例通过不足以证明工具可用。
4. MCP 直接协议实测和真实客户端派发都要做。官方文档支持变量不证明本机 CLI 已展开;真实路径错误用版本/命令留证。
5. 进程完成与验收通过分开,leader 退出不证明整个进程组退出。保留报告和失败证据,再按实际运行身份回收。
范围:本次 gmktec/Linux、PI 0.85.1、Cursor CLI 的工程工具;客户端升级和其它系统需复核。这里的工具/进程守卫不是 OS 沙箱。
+1
View File
@@ -9,3 +9,4 @@
| LESSON-20260923-matrix-status-cas | FEATURE-MATRIX、docs-apply、record_id、merge conflict、CAS | 矩阵/CURRENT 行变更走 docs apply;合并后核验状态列未被回退 | validated | [矩阵状态 CAS](2026-09-23-matrix-status-cas.md);2026-09-23 |
validated 的证据范围见各条目;本日复核为历史材料和源码阅读,未重跑产品回归。后续按机制补充,过时条目标 superseded/retired 并保留替代链接。
| LESSON-20260924-pi-tool-boundaries | PI、MCP、readonly、path、snapshot、process-group | 路径语义与快照边界一致,合法路径和拒绝路径都验收 | validated | [文件工具边界](2026-09-24-pi-tool-boundaries.md);2026-09-24 |
@@ -0,0 +1,132 @@
---
title: "Cursor Auto 与 Subagent 协作规范研究"
task_id: subagent-workflow-standard-2026-09-24
status: done
created: 2026-09-24
updated: 2026-09-24
owner: Codex
task_type: implementation
task_tier: multi_stage
policy_revision: "agent-governance-2026-09-21-r1 + issue-learning-2026-09-23-r1 + systemic-issue-analysis-2026-09-23-r1 + codegraph-governance-2026-09-24-r1; dev@5423b261f97f371bdf7fa5066e9305f9eddaef42"
branch: feature/subagent-workflow-standard-2026-09-24
worktree: .worktrees/subagent-workflow-standard-2026-09-24
base_dev_sha: 5423b261f97f371bdf7fa5066e9305f9eddaef42
write_scope: ["scripts/pi_subagents.py", "scripts/pi-subagent-readonly.ts", "scripts/mcp/pi-subagents-mcp.mjs", "scripts/mcp/README.md", "scripts/tests/test_pi_subagents.py", ".cursor/mcp.json", ".cursor/rules/pi-subagents.mdc", ".doc", ".doc/ops/2026-09-24-subagent-model-config.md", ".doc/ops/2026-09-24-subagent-workflow-standard.md", ".doc/ops/short-term/2026-09-24-subagent-workflow-standard.md", ".doc/audit/2026-09-24-subagent-workflow-research.md", ".doc/ops/templates/subagent-stage-record.md", ".doc/ops/2026-09-21-policy-registry.md", ".doc/README.md", ".doc/audit/ISSUES.md", ".doc/ops/CURRENT.md"]
allowed_environments: []
mutex: subagent-workflow-docs
review_state: approved
integration_state: pending
deployment: not_required
cleanup: retained-with-reason
cleanup_owner: "owner 退出后的独立维护者"
cleanup_trigger: "规范采纳并集成、证据保存且 owner 退出后复查"
remote_sync: not_requested
---
# 用户目标与授权
用户原请求:在规范中加入 subagent 规范,日常在 Cursor 使用 Auto;梳理“形成需求→评估需求→制定方案→实施方案→代码审查→测试→合并并清理 worktree→验收”,每一步形成文档或报告,研究制定更合理的新规范。
本任务授权为研究与规范编制,交付终点为可审规范、依据、模板和接入顺序;不含业务实施、Cursor 全局设置、共享运行环境、Herdr 编排、远端 push。新规范以 proposed 交付,不宣称全仓 active 或已经实现自动工作流。
## 需求与验收
| R-ID | 用户要求 | AC-ID / 本次完成标准 |
|---|---|---|
| R1 | 日常无需选择模型 | AC1:解释 Auto/inherit 支持范围与实测缺口,不固定具体模型 |
| R2 | 梳理合理流程 | AC2:阶段依赖、返工、合并前后验证和清理顺序明确 |
| R3 | 加入 subagent 规范 | AC3:职责、派发/回传、独立性、并发写与授权边界可执行 |
| R4 | 每步文档/报告 | AC4:阶段产物、模板、追踪和小任务减负规则明确 |
| R5 | 基于研究制定 | AC5:引用当前仓库与官方依据,核对已有规范冲突并列接入验收 |
验收人:本次文档质量由 Codex 自检;新规范采纳由用户决定。未回复默认确认点问题,草案采用“方案确认后授权范围内自动推进”,可按用户反馈修订。
## 研究与实施记录
- 启动检查主目录 dev 干净,worktree/机器认领已盘点;工具创建本任务 worktree,登记八个文档写路径及 docs:publish。
- 本地 dev 相对缓存 origin/dev ahead 74 / behind 0,未 fetch、未 push;服务器状态未核验。
- CURRENT 在本任务 worktree 通过 docs apply 新增自己的认领,request_id=subagent-workflow-standard-start-20260924;主目录 CURRENT 未直接编辑。
- 已读 AGENTS、PROJECT、CURRENT、文档地图、worktree/验证/规则登记/问题闭环/任务及收口模板。
- 问题/经验检索词:subagent、Cursor、worktree、并行、清理、规范、审批。命中 ISSUE-20260923-matrix-status-regression 和 LESSON-20260923-matrix-status-cas;采纳共享状态经 docs apply、合并后核对状态,其他内容无直接 subagent 经验命中。
- 新发现 ISSUE-20260924-cursor-test-rule-drift,见研究报告;仅登记与建议,未修复。
- 文档组织采用 writing-for-agents 技能的单一来源与条件入口原则;brainstorming/ce-plan 用于流程与选项研究。
- 没有执行产品代码、客户端角色安装或自动化脚本开发。未新增经验:本次设计尚无实机验证,已有经验继续复用。
## 产物
- [规范草案](../2026-09-24-subagent-workflow-standard.md):唯一流程定义与接入步骤。
- [研究报告](../../audit/2026-09-24-subagent-workflow-research.md):来源、取舍、规则漂移问题与证据边界。
- [阶段/派发模板](../templates/subagent-stage-record.md):输入版本、授权、角色输出与验收映射。
- 文档地图和规范登记增加 proposed 入口;不修改 active 规范行为、不创建 .cursor/agents。
## 验证与证据
| 范围 | 实际检查 | 结果与限制 |
|---|---|---|
| 本次八个文档 | git diff --check;Python 内联链接/锚点/S0–S9/字段/ID 检查 | 初次 FILES=8 LOCAL_LINKS=84 STAGES=10 ISSUE_IDS=5 / PASS;收口再检 |
| 流程一致性 | 逐项对照需求 AC1–AC5 和 12 个文档情景 | 均有对应条款;self_review,未作独立 Agent 审查 |
| 状态盘点 | agentctl docs drift | status=ok、regressions=[];未改能力声明 |
| 清理盘点 | agentctl cleanup report | exit 0;本任务 blocked:dirty_or_ignored_files、registry:TASK_ACTIVE;为提交前快照,不删除 |
| CodeGraph | codegraph sync -q | exit 0;只更新可再生本地索引 |
| 产品/客户端运行 | 未执行 | 纯文档研究不要求产品测试;Auto/角色实机 V1–V10 尚未运行 |
本次 AC1–AC5 达到研究交付标准;规范采纳仍 review_state=pending,规则生命周期 proposed。证据摘要与情景逐项结果见研究报告,未把未运行的项目记 PASS。
## 收口与交接
本记录与草案保存在本任务分支的本地 docs 提交,精确提交可由 git log -1 feature/subagent-workflow-standard-2026-09-24 核对;本次未合入 dev、未部署、未 push。后续按规范接入 A–F 推进,先处理采纳与客户端规则冲突,再实机验证 Auto/角色/门禁行为。
本任务工作区保留供审阅,未合入 dev;cleanup 由独立维护者在满足祖先/证据/进程条件后复查。无需重建产品服务;未申请或使用 env:dev。
本地认领在结束前标记研究 done;此状态只关闭本次规范编制,不关闭 open 问题或自动批准规范。文件含可再生 .codegraph 缓存,独立维护者清理前另行盘点;草案是唯一新增研究成果,当前工作区需保留供审阅。
## 后续修订 r2:用户确认模型配置
逻辑 task_id 沿用 subagent-workflow-standard-2026-09-24;本机 agentctl 不支持重开已关闭记录,也不允许原记录隐式扩 scope,因此仅创建 operation_task_id=subagent-workflow-standard-2026-09-24-config-r2 作为本次写入租约身份,仍在原 worktree、分支和本主记录继续,不另建业务任务。
写范围沿用原规范/研究/入口/模板/主记录并增加 .doc/ops/2026-09-24-subagent-model-config.md;未认领业务源码、PI 全局配置或任何运行环境。
授权来源:用户指定本机 PI 和五模型池;明确代码由 Cursor 实施;确认 M3 不进入默认流程;本轮“可以,默认这样,可以设置一个配置文档,让我可以根据需要进行调整”。
交付:可编辑模型配置 + 旧草案中的执行方式修正 + 配置版本/覆盖来源进入派发模板。模型分配 decision_state=approved;流程整体仍 proposed,自动派发器未实现,未扩大为客户端安装、产品实施、dev 集成或 push。
验收项:
- AC6:五个精确 provider/model,Cursor 独占代码实施与修复。
- AC7:M3 辅助角色 enabled=false,禁止关键判断/自动备用链。
- AC8:角色默认、升级关系、启停、并发和任务覆盖可在文档中调整。
- AC9:配置版本/hash 随派发记录,下一次派发生效;文件明确尚无自动解析器。
- AC10:模型配置为唯一来源;流程与研究历史明确取代旧原生 Cursor/inherit 方案。
使用 writing-for-agents 的单一来源/短入口原则维护文档。继续采纳 LESSON-20260923-matrix-status-cas;不新增已验证经验,未将文档推演当运行证明。
本轮验证结果(文档静态检查,非运行验收):
- git diff --check:exit 0。
- Python 内联检查:五个模型精确匹配本机目录;11 个 role_id 唯一且升级关系无循环;auxiliary=false,M3 仅辅助;implementation_executor=cursor_auto;无自动备用;模板包含 config_revision/config_sha256。初次输出 MODELS=5 ROLES=11 STAGES=10,PASS。
- 本次变更 Markdown 的相对链接及锚点检查通过;旧 PI implementer 配置已移除。保留的 inherit 提及均说明旧方案已废止。
- agentctl docs drift:status=ok,regressions=[];cleanup report 为只读盘点,本任务因未合入/文件及登记状态不能删除。未操作其他任务。
- codegraph sync -q:exit 0。仅文档变更,产品测试、真实模型调用和派发器验收不适用/未运行,未记 PASS。
本轮交付已完成;逻辑任务恢复 done,模型分工已获用户确认。新增配置与配套文档保存于本地任务分支;未合入 dev、未 push,主目录与 PI 全局设置均未修改。自动调用 PI 的执行器、角色工具限制实测、流程整体采纳和相关旧规则修复作为后续工作,不因配置文档存在视为已完成。
提交前检查记录:首次 git diff --cached --check 检出新配置头部两处 Markdown 行末双空格,提交未执行。已改为段落分隔并重跑检查;此前 git diff --check 不包含未跟踪新文件,最终以暂存区检查为准。
## 执行器实现续作
用户授权“接入自动调用 pi 的执行器”。继续原逻辑 task_id;操作登记 subagent-workflow-standard-2026-09-24-executor,Cursor Auto 负责代码,Codex 主协调。[实施契约与验证计划](../2026-09-24-pi-executor-implementation.md)。阶段 in_progress,原文“尚未实现”为此前交付历史,当前结果以本节后续报告为准。
## 执行器收口
- 实施与验证完成:代码由 Cursor Auto 编写,代码提交 69ffaa677e23a1dd9aa0528bb09b3acfcd38b119;Codex 主机复核与验收,本机 GLM-5.3 独立审查守卫候选。模型配置默认值按用户已确认方案。
- [使用说明](../2026-09-24-pi-subagent-executor.md) · [完整报告](../../test-reports/2026-09-24-pi-executor-test-report.md)。实际 Cursor CLI 调用 roles/run/result,无 model_override、无 shell PI 替代;最终 ls/read 成功。
- 完整工程 suite 173 项 PASS;提交后专项 34 项 PASS;node 检查、arch-check block、文档与证据核对通过。产品 Go/前端/PG/真实产品 Runner 无改动,未冒充验收。
- 本轮授权交付终点:执行器源码与 Cursor CLI 本地接入验证。全套阶段审批状态机和 IDE 页面验证不在本次完成声明中。
- integration_state=pending:本地 dev 已前进至 3de5bc5c396fc21887f0f19e6e3baa8784347f99(新增 nightly/测试修复),主目录干净;但 dev-full-gate-test-promo-2026-09-24 持 env:dev 真实验收租约,同锁还有别人的候选排队,不能抢占。未向 dev 写入本任务成果,未 push,未重建服务。
- 接管:环境槽位释放后,从届时最新 dev 创建受控候选,重读规则并验证,再集成本任务分支。不得复用旧 expected-dev 或抢占别人队列。
- cleanup 保留:未合入 dev,当前 worktree 用于配置审阅与运行;唯一报告/日志已纳入本任务文档提交,CodeGraph 为可再生缓存。由 owner 退出后的独立维护者按祖先/进程/证据条件复查,不强删。
- ISSUE-20260924-pi-executor-boundaries 已按本地范围验证关闭;新增 LESSON-20260924-pi-tool-boundaries。既有测试规则漂移仍 open;其它 Cursor MCP 入口兼容性残差单独 deferred。研究/代码完成不自动关闭这些问题。
本轮临时操作登记为 subagent-workflow-standard-2026-09-24-executor;逻辑 task_id 保持原值。最终头部状态仅表示上述源码/本地验收完成,integration/deployment/remote_sync/cleanup 各自独立。
## 本地 dev 集成续作
用户明确授权“合入 dev”。沿用本逻辑 task_id;文档协调操作登记 subagent-workflow-standard-2026-09-24-land。读取 dev 基线 c1a70e67e5edfd4a4c52fe087933e92f3c6d1e9c,远端跟踪引用 ahead 3 / behind 0(未 fetch);主目录干净、机器 runs/queue 均为空。AGENTS/PROJECT/worktree/验证契约对比原基线无差异。
预合并仅 CURRENT/ISSUES 文档冲突;采纳 LESSON-20260923-matrix-status-cas,在原任务 worktree 用 docs apply @document 对齐最新 dev 并保留自己的认领,问题台账保留 dev 全部行并追加本任务三行。docs:admin 仅用于本次一致性对齐;不改其他任务状态、不改集成槽位。
下一步从此精确 dev 构造 --no-ff 候选,验证后通过 agentctl integrate 合入;不 push、不重建产品服务、不清理他人工作区。
@@ -0,0 +1,47 @@
# Subagent 派发与阶段记录模板
用途:配合[协作规范草案](../2026-09-24-subagent-workflow-standard.md),复制适用段落到已有任务/报告;本模板尚未作为强制门禁启用。
## 派发包
- parent_task_id / subtask_id / stage_id / role_id:
- [模型配置](../2026-09-24-subagent-model-config.md)路径 / config_revision / config_sha256:
- 解析后的 provider / model / enabled / upgrade_role;本任务 override、用户来源与期限:
- 目标、非目标、授权来源和交付终点:
- 输入:R-ID / AC-ID / 方案版本 / base_sha / head_sha:
- 必读规则:AGENTS、policy_revision、适用专门规范:
- cwd / branch / allowed_paths / allowed_environments:
- 只读或写入;mutex / agentctl supervisor 或主 Agent 应用补丁方式:
- 前置产物 / 完成条件 / 输出位置:
- 时间或步骤预算、回传点、异常处理:
## 子任务回传
- PI run_id / producer / 开始结束时间:
- configured_model / actual_model(不可观察填 unknown):
- 实际输入版本、实际 cwd:
- run_status:running | succeeded | failed | cancelled | unknown
- verdict:pass | changes_required | blocked | not_applicable
- 结论及依据:
- 变更文件 / commit / 未提交内容:
- 真实命令 / 环境 / 退出码 / 证据链接 / 脱敏范围:
- 已查范围、未覆盖项、风险:
- 存活进程、外部副作用、未归档产物、接管建议:
## 阶段收据
| 阶段 | 输入版本 | 产物/证据 | producer | reviewer/独立性 | 决定与理由 | 接收者/下一步 |
|---|---|---|---|---|---|---|
| S0–S9 中适用阶段 | | | | independent / self_review / not_required | pass / changes_required / blocked / not_applicable | |
只读子 Agent 的结论由主 Agent 转存时保留 agent_id 与原结论,注明转存者。未执行阶段记录原因;报告存在不自动满足门禁。
## 需求追踪与验收
| R-ID | AC-ID / 可观察标准 | 改动位置/版本 | 审查记录 | T-ID / 实际结果 | 运行验收证据 | acceptance_owner / 结论 |
|---|---|---|---|---|---|---|
| | | | | | | |
## 收口字段
沿用[任务模板](short-term-task.md)的 status、review_state、integration_state、deployment、cleanup、remote_sync,追加 phase、acceptance、待回收 subtask_id。记录失败和重试关系;声明 PASS 需有实际证据。
@@ -0,0 +1,54 @@
# 本机 PI Subagent 执行器验证报告
日期:2026-09-24。task_id:subagent-workflow-standard-2026-09-24;操作任务:subagent-workflow-standard-2026-09-24-executor。
代码提交:`69ffaa677e23a1dd9aa0528bb09b3acfcd38b119`。真实模型请求的材料来自 `52462b45e985f596b2a503fc70ac12c3bd5fe571`;输入材料提交与执行器版本分开。测试期间执行器文件冻结,随后原样提交,代码摘要见客户端证据。
## 范围与结论
Cursor Auto 经项目 MCP 自动读取模型配置、派发本机 PI、取回持久报告的 CLI 链路通过。代码实施/修复由 Cursor 承担,PI 工具与输入范围受限,M3 默认禁用且未调用。
这是个人工程工具验收,不代表产品 PI Agent Runner、产品 UI、PG 持久性或发布门禁通过。未重建服务、修改产品模型或 push。
## 环境
gmktec / Linux;PI 0.85.1;Cursor CLI 2026.09.18-9a7762b;Node v26.9.0;Python 3.14。
入口:scripts/pi_subagents.py、scripts/mcp/pi-subagents-mcp.mjs、scripts/pi-subagent-readonly.ts。
[配置](../ops/2026-09-24-subagent-model-config.md) · [运行说明](../ops/2026-09-24-pi-subagent-executor.md)。
模型身份来自 PI 结构化消息与调用参数的比对,不是服务商内部路由的独立证明。
## 实际执行结果
| 检查 | 命令 / 输入 | 结果 | 证据 |
|---|---|---|---|
| 完整工程回归 | python3 -B -m unittest discover -s scripts/tests -p 'test_*.py' -v | 173 tests / OK / exit 0;111.229 秒 | [日志](assets/pi-executor-2026-09-24/engineering-tests.txt) |
| 提交后专项回归 | python3 -B -m unittest discover -s scripts/tests -p 'test_pi_subagents.py' -v | 34 tests / OK / exit 0;38.229 秒;HEAD=69ffaa67 | [日志](assets/pi-executor-2026-09-24/committed-focused.txt) |
| 架构门禁 | ./scripts/arch-check.sh --mode block | exit 0;archtest 两组通过 | [日志](assets/pi-executor-2026-09-24/arch-check.txt) |
| JS / 差异 | node --check scripts/mcp/pi-subagents-mcp.mjs;git diff --check | exit 0 | 本报告及最终检查 |
| Cursor 工具发现 | agent mcp list-tools pi-subagents | 3 个工具可发现 | [客户端证据](assets/pi-executor-2026-09-24/cursor-dispatch.json) |
| MCP 研究 | real-mcp-research-02 | Qwen3.8-Flash 返回目录声明及标记;早期 ls 误拒已保留并在最终链路重验 | [收据](assets/pi-subagents/subagent-workflow-standard-2026-09-24-executor/real-mcp-research-02/receipt.json) |
| MCP 测试 | real-mcp-test-01;Python 检查文件及 env 隔离 | GLM-5.3-Flash;命令 exit 0,PI_ISOLATED_TEST_PASS;非产品测试 | [收据](assets/pi-subagents/subagent-workflow-standard-2026-09-24-executor/real-mcp-test-01/receipt.json) |
| MCP 规划 | real-mcp-plan-01 | Qwen3.8-Max 正确总结 Cursor/M3/升级规则并返回 MODEL_PLAN_OK | [收据](assets/pi-subagents/subagent-workflow-standard-2026-09-24-executor/real-mcp-plan-01/receipt.json) |
| 禁用角色 | real-mcp-disabled-01 / auxiliary | ROLE_DISABLED、isError=true,未调用 M3 | [汇总](assets/pi-executor-2026-09-24/live-mcp.json) |
| Cursor Auto 完整派发 | roles → run → result;cursor-auto-research-02;无 model_override 或 shell PI 替代 | succeeded;Qwen3.8-Flash;ls/read 均 isError=false;CURSOR_PI_AUTOMATION_OK | [报告](assets/pi-subagents/subagent-workflow-standard-2026-09-24-executor/cursor-auto-research-02/report.md) · [工具调用](assets/pi-executor-2026-09-24/cursor-dispatch.json) |
PI 单测使用明确的假传输;Git/锁/进程/MCP 为隔离实测,真实模型请求另列。未操作 Cursor IDE 页面。
## 覆盖与审查
回归覆盖配置/角色/循环校验、M3 越权拒绝、注册/租约/伪造身份、冻结 SHA、路径穿越/秘密/符号链接、TS 守卫合法根目录和负例、输出上限/超时/存活子进程、终止事件、PID 身份/只读查询、幂等/损坏收据/配置重载、环境白名单、测试与模型快照隔离、命令失败不可被模型叙事升级、MCP 主仓入口及拒绝原因。
命令 pass/commands_passed 只表示显式命令结果;必需用例、任务验收与人工批准另判。non-test 模型完成仍为 pending/not_applicable。
Cursor Auto 实施,Codex 主机复核完整差异和测试;本机 GLM-5.3 独立审查只读守卫:[原审查](assets/pi-executor-2026-09-24/guard-review.json)。有效目录/默认路径候选已通过隔离与 inventory 检查落实;paths[] 不属于实际 PI 内置 schema,其假设不当作已证实漏洞,接口另外拒绝未知路径载体。最终修复由主机与回归确认,未声称 GLM 对整套最终代码出具通过结论。
## 失败过程及修复
- 初版路径规范化问题用假文件复现并修复,没有读取真实敏感文件;其它进程、环境、幂等和判定问题见[主记录](../audit/2026-09-24-pi-executor-boundaries.md)。
- 第一次 GLM 整包审查 300 秒超时,无完整结果,不记 PASS;改为有完整返回的分段审查与主机复核。
- 第一次 Cursor 会话未挂载工具。list-tools 证明本机 CLI 未展开 ${workspaceFolder};本次新入口改为相对路径并启用本工作区 pi-subagents 后通过。[官方 CLI MCP 文档](https://cursor.com/docs/cli/mcp)描述共用配置,本机行为以实际命令为准。
- 合法目录枚举的两次失败分别来自 exact-root 判断和内部扩展污染模型快照;修复、新回归和最终 Cursor 调用证明 ls/read 正常,旧收据保留。
- 部分 Cursor 编码客户端退出后留下语言服务子进程,agentctl 正确保留 recovery_required;host 仅核对/终止本任务记录的进程组后 recover,没有删锁或杀他人进程。
## 交付边界
dev 集成以任务和机器收据为准;共享 dev 正由另一任务持 env:dev 验收租约,不能抢占或绕过集成。模型分工已确认,流程整体草案未因此自动全仓生效。
执行器不是全套 S0–S9 审批状态机,也不是 OS 沙箱;批准测试代码属于可信执行面。本版不直接操作共享环境或自动安装依赖。PI 获得命令摘要/日志引用,完整日志由主 Agent 核验并按需补充上下文,不能用预览证明所有必需用例。
原始 Cursor 会话、模型 thinking 不入 Git;只留必要工具调用/结果/摘要。未归档凭证、业务正文或敏感截图。
@@ -0,0 +1,5 @@
arch-check mode=block
boundaries=/home/weichen/projects/Agent-System/.worktrees/subagent-workflow-standard-2026-09-24/.doc/system-design/module-boundaries.yaml
ok chat-agent-system/internal/archtest 0.222s
ok chat-agent-system/internal/archtest 0.193s
arch-check block: production gate passed
@@ -0,0 +1,88 @@
test_00_executor_module_present (test_pi_subagents.PiSubagentsTests.test_00_executor_module_present) ... ok
test_approved_commands_do_not_mutate_model_snapshot (test_pi_subagents.PiSubagentsTests.test_approved_commands_do_not_mutate_model_snapshot) ... ok
test_config_catalog_parses_known_pool_and_roles (test_pi_subagents.PiSubagentsTests.test_config_catalog_parses_known_pool_and_roles) ... ok
test_config_rejects_unknown_alias_and_cycle (test_pi_subagents.PiSubagentsTests.test_config_rejects_unknown_alias_and_cycle) ... ok
test_config_reload_snapshot_on_dispatch (test_pi_subagents.PiSubagentsTests.test_config_reload_snapshot_on_dispatch) ... ok
test_corrupt_receipt_fail_closed (test_pi_subagents.PiSubagentsTests.test_corrupt_receipt_fail_closed) ... ok
test_crash_before_receipt_terminal_is_unknown_on_status (test_pi_subagents.PiSubagentsTests.test_crash_before_receipt_terminal_is_unknown_on_status) ... ok
test_disabled_auxiliary_and_m3_critical_remap_denied (test_pi_subagents.PiSubagentsTests.test_disabled_auxiliary_and_m3_critical_remap_denied) ... ok
test_extension_outside_model_snapshot_inventory (test_pi_subagents.PiSubagentsTests.test_extension_outside_model_snapshot_inventory)
Model snapshot must contain only declared inputs; -e guard lives outside it. ... ok
test_failed_replay_stays_failure (test_pi_subagents.PiSubagentsTests.test_failed_replay_stays_failure) ... ok
test_forged_lease_env_refused (test_pi_subagents.PiSubagentsTests.test_forged_lease_env_refused) ... ok
test_global_concurrency_limit (test_pi_subagents.PiSubagentsTests.test_global_concurrency_limit) ... ok
test_idempotent_replay_and_digest_mismatch (test_pi_subagents.PiSubagentsTests.test_idempotent_replay_and_digest_mismatch) ... ok
test_input_path_traversal_symlink_secret_refusal (test_pi_subagents.PiSubagentsTests.test_input_path_traversal_symlink_secret_refusal) ... ok
test_leader_exit_reaps_orphan_child (test_pi_subagents.PiSubagentsTests.test_leader_exit_reaps_orphan_child) ... ok
test_malformed_and_empty_outputs_fail (test_pi_subagents.PiSubagentsTests.test_malformed_and_empty_outputs_fail) ... ok
test_mcp_from_worktree_uses_main_agentctl (test_pi_subagents.PiSubagentsTests.test_mcp_from_worktree_uses_main_agentctl)
Server INSTALL_ROOT may be a linked worktree; agentctl must resolve from main. ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_mcp_initialize_list_run_result (test_pi_subagents.PiSubagentsTests.test_mcp_initialize_list_run_result) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_mcp_running_poll_is_not_error (test_pi_subagents.PiSubagentsTests.test_mcp_running_poll_is_not_error) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_mcp_surfaces_role_disabled_and_scope_refusal (test_pi_subagents.PiSubagentsTests.test_mcp_surfaces_role_disabled_and_scope_refusal) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=10 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=6 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=8 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_model_mismatch_fails (test_pi_subagents.PiSubagentsTests.test_model_mismatch_fails) ... ok
test_naked_run_without_lease_refused (test_pi_subagents.PiSubagentsTests.test_naked_run_without_lease_refused) ... ok
test_orphan_result_is_readonly (test_pi_subagents.PiSubagentsTests.test_orphan_result_is_readonly) ... ok
test_output_cap_on_stdout_flood (test_pi_subagents.PiSubagentsTests.test_output_cap_on_stdout_flood) ... ok
test_readonly_guard_allows_exact_root_listing (test_pi_subagents.PiSubagentsTests.test_readonly_guard_allows_exact_root_listing)
Exact-root ls/grep must pass when snapshot inventory is allowlisted-only. ... ok
test_readonly_guard_rejects_at_prefix (test_pi_subagents.PiSubagentsTests.test_readonly_guard_rejects_at_prefix)
Exercise extension path policy via Node strip-types if available. ... ok
test_replay_keeps_old_config_after_disable (test_pi_subagents.PiSubagentsTests.test_replay_keeps_old_config_after_disable) ... ok
test_roles_does_not_invoke_pi (test_pi_subagents.PiSubagentsTests.test_roles_does_not_invoke_pi) ... ok
test_run_emits_exact_pi_flags_and_receipt (test_pi_subagents.PiSubagentsTests.test_run_emits_exact_pi_flags_and_receipt) ... ok
test_scope_refusal_outside_report_path (test_pi_subagents.PiSubagentsTests.test_scope_refusal_outside_report_path) ... ok
test_scrub_env_drops_pi_dir_for_tests (test_pi_subagents.PiSubagentsTests.test_scrub_env_drops_pi_dir_for_tests) ... ok
test_source_sha_rejects_symbolic_refs (test_pi_subagents.PiSubagentsTests.test_source_sha_rejects_symbolic_refs) ... ok
test_test_run_requires_approved_commands_and_records_failure (test_pi_subagents.PiSubagentsTests.test_test_run_requires_approved_commands_and_records_failure) ... ok
test_timeout_kills_process_group (test_pi_subagents.PiSubagentsTests.test_timeout_kills_process_group) ... ok
----------------------------------------------------------------------
Ran 34 tests in 38.229s
OK
@@ -0,0 +1,64 @@
{
"client": {
"session_id": "ef25c056-edb9-4902-bf4e-dcafb255e9f9",
"model": "Auto"
},
"tools": [
{
"tool": "pi_subagent_roles",
"server": "pi-subagents",
"arguments": {
"worktree": "/home/weichen/projects/Agent-System/.worktrees/subagent-workflow-standard-2026-09-24",
"task_id": "subagent-workflow-standard-2026-09-24-executor"
}
},
{
"tool": "pi_subagent_run",
"server": "pi-subagents",
"arguments": {
"worktree": "/home/weichen/projects/Agent-System/.worktrees/subagent-workflow-standard-2026-09-24",
"task_id": "subagent-workflow-standard-2026-09-24-executor",
"request": {
"role_id": "research",
"request_id": "cursor-auto-research-02",
"goal": "call ls(path='.') and confirm the snapshot contains PROJECT.md; call read on PROJECT.md; quote two directory names declared in its repository map; finish CURSOR_PI_AUTOMATION_OK. Report any tool failure truthfully; do not call failed ls/read success.",
"authorization_ref": "user requested PI executor isolated acceptance",
"source_sha": "52462b45e985f596b2a503fc70ac12c3bd5fe571",
"input_files": [
"PROJECT.md"
],
"timeout_seconds": 90
}
}
},
{
"tool": "pi_subagent_result",
"server": "pi-subagents",
"arguments": {
"request_id": "cursor-auto-research-02",
"task_id": "subagent-workflow-standard-2026-09-24-executor",
"worktree": "/home/weichen/projects/Agent-System/.worktrees/subagent-workflow-standard-2026-09-24"
}
},
{
"tool": "pi_subagent_result",
"server": "pi-subagents",
"arguments": {
"request_id": "cursor-auto-research-02",
"task_id": "subagent-workflow-standard-2026-09-24-executor",
"worktree": "/home/weichen/projects/Agent-System/.worktrees/subagent-workflow-standard-2026-09-24"
}
}
],
"implementation_commit": "69ffaa677e23a1dd9aa0528bb09b3acfcd38b119",
"code_sha256": {
"scripts/pi_subagents.py": "5a656ee93d8effb1ae347d113a4e2b579bdc78a795d848dc6bbf9057c0d2a0df",
"scripts/pi-subagent-readonly.ts": "8d818dccb3c2f3bff9de0fcfae1ace110a50b1e3055afd21179171320c334284",
"scripts/mcp/pi-subagents-mcp.mjs": "9a3a2d2b8793e0dbbc19110af4ba42352661b791e72987136aa1f3a13a59c143",
"scripts/mcp/README.md": "37a25cb07e4405e199074b3c4714792269cba562f134e7652bd0598a642e9b96",
"scripts/tests/test_pi_subagents.py": "4276c56ab42abca268f4807f3814741fe06341eb56adc1b215debbd8452e374a",
".cursor/mcp.json": "690815b2fe5ec9b48d39c9c1600fa0d7a39f343917bb1e187c89d4dfbcce8f88",
".cursor/rules/pi-subagents.mdc": "560682ba89daab2be100be68983eba0b9fcb7cfca73d99747a5fb54352feb2c7"
},
"scope": "Real Cursor CLI via MCP, not IDE UI; no model override; no shell substitute"
}
@@ -0,0 +1,250 @@
test_linux_permission_error_path_remains_fail_closed (test_agent_worktree.ProcessVisibilityTest.test_linux_permission_error_path_remains_fail_closed) ... ok
test_unknown_same_uid_inaccessible_cwd_is_not_skipped (test_agent_worktree.ProcessVisibilityTest.test_unknown_same_uid_inaccessible_cwd_is_not_skipped) ... ok
test_branch_delete_refusal_preserves_branch_without_force_with_visibility_fixture (test_agent_worktree.WorktreeTest.test_branch_delete_refusal_preserves_branch_without_force_with_visibility_fixture) ... ok
test_cleanup_actual_branch_mapping_with_visibility_fixture (test_agent_worktree.WorktreeTest.test_cleanup_actual_branch_mapping_with_visibility_fixture) ... ok
test_cleanup_legacy_mapping_and_nonforced_branch_delete_with_visibility_fixture (test_agent_worktree.WorktreeTest.test_cleanup_legacy_mapping_and_nonforced_branch_delete_with_visibility_fixture) ... ok
test_cleanup_lifecycle_lock_prevents_a_new_run (test_agent_worktree.WorktreeTest.test_cleanup_lifecycle_lock_prevents_a_new_run) ... ok
test_cleanup_refuses_active_registry_operation_even_with_other_cwd (test_agent_worktree.WorktreeTest.test_cleanup_refuses_active_registry_operation_even_with_other_cwd) ... ok
test_cleanup_refuses_live_process_cwd (test_agent_worktree.WorktreeTest.test_cleanup_refuses_live_process_cwd) ... ok
test_cleanup_refuses_tracked_untracked_and_ignored_evidence (test_agent_worktree.WorktreeTest.test_cleanup_refuses_tracked_untracked_and_ignored_evidence) ... ok
test_cleanup_refuses_unfinished_git_operation (test_agent_worktree.WorktreeTest.test_cleanup_refuses_unfinished_git_operation) ... ok
test_cleanup_refuses_unmerged_commit (test_agent_worktree.WorktreeTest.test_cleanup_refuses_unmerged_commit) ... ok
test_create_from_feature_uses_current_dev_and_canonical_main (test_agent_worktree.WorktreeTest.test_create_from_feature_uses_current_dev_and_canonical_main) ... ok
test_create_with_main_on_unrelated_branch_uses_dev (test_agent_worktree.WorktreeTest.test_create_with_main_on_unrelated_branch_uses_dev) ... ok
test_forged_inherited_lease_is_refused (test_agent_worktree.WorktreeTest.test_forged_inherited_lease_is_refused) ... ok
test_inherited_worktree_lease_does_not_double_lock (test_agent_worktree.WorktreeTest.test_inherited_worktree_lease_does_not_double_lock) ... ok
test_list_is_read_only_without_init (test_agent_worktree.WorktreeTest.test_list_is_read_only_without_init) ... ok
test_rejects_traversal_nested_and_protected_branches (test_agent_worktree.WorktreeTest.test_rejects_traversal_nested_and_protected_branches) ... ok
test_same_task_id_and_existing_branch_do_not_collide (test_agent_worktree.WorktreeTest.test_same_task_id_and_existing_branch_do_not_collide) ... ok
test_stable_lock_refuses_concurrent_writer (test_agent_worktree.WorktreeTest.test_stable_lock_refuses_concurrent_writer) ... ok
test_symlink_parent_or_target_is_refused (test_agent_worktree.WorktreeTest.test_symlink_parent_or_target_is_refused) ... ok
test_write_requires_explicit_init (test_agent_worktree.WorktreeTest.test_write_requires_explicit_init) ... ok
test_admin_document_cannot_bypass_done_with_compact_cells (test_agentctl.AgentctlTests.test_admin_document_cannot_bypass_done_with_compact_cells) ... ok
test_admin_document_cannot_overwrite_human_sections (test_agentctl.AgentctlTests.test_admin_document_cannot_overwrite_human_sections) ... ok
test_admin_document_cannot_overwrite_integration_slot (test_agentctl.AgentctlTests.test_admin_document_cannot_overwrite_integration_slot) ... ok
test_admin_document_cas_preserves_slot_and_needs_capability (test_agentctl.AgentctlTests.test_admin_document_cas_preserves_slot_and_needs_capability) ... ok
test_admin_full_document_requires_explicit_admin_capability (test_agentctl.AgentctlTests.test_admin_full_document_requires_explicit_admin_capability) ... ok
test_claim_blocks_task_close_until_released (test_agentctl.AgentctlTests.test_claim_blocks_task_close_until_released) ... ok
test_claim_concurrent_twenty_exactly_one_held (test_agentctl.AgentctlTests.test_claim_concurrent_twenty_exactly_one_held) ... ok
test_claim_frozen_rejects_even_holder (test_agentctl.AgentctlTests.test_claim_frozen_rejects_even_holder) ... ok
test_claim_parent_child_overlap_and_sibling_ok (test_agentctl.AgentctlTests.test_claim_parent_child_overlap_and_sibling_ok) ... ok
test_claim_request_id_idempotent_and_reuse_guard (test_agentctl.AgentctlTests.test_claim_request_id_idempotent_and_reuse_guard) ... ok
test_claim_unrelated_resources_parallel_ok (test_agentctl.AgentctlTests.test_claim_unrelated_resources_parallel_ok) ... ok
test_commit_lease_must_cover_staged_files_not_only_task_scope (test_agentctl.AgentctlTests.test_commit_lease_must_cover_staged_files_not_only_task_scope) ... ok
test_daemon_does_not_grant_register_or_docs_apply (test_agentctl.AgentctlTests.test_daemon_does_not_grant_register_or_docs_apply) ... ok
test_daemon_single_instance_lock_busy_with_holder_pid (test_agentctl.AgentctlTests.test_daemon_single_instance_lock_busy_with_holder_pid) ... ok
test_daemon_stop_leaves_pending_queue_untouched (test_agentctl.AgentctlTests.test_daemon_stop_leaves_pending_queue_untouched) ... ok
test_descendant_resource_guard_accepts_only_held_resource (test_agentctl.AgentctlTests.test_descendant_resource_guard_accepts_only_held_resource) ... ok
test_docs_cannot_launder_unmanaged_other_task_changes (test_agentctl.AgentctlTests.test_docs_cannot_launder_unmanaged_other_task_changes) ... ok
test_docs_foreign_current_row_is_rejected (test_agentctl.AgentctlTests.test_docs_foreign_current_row_is_rejected) ... ok
test_docs_multi_file_preflight_failure_has_no_partial_writes (test_agentctl.AgentctlTests.test_docs_multi_file_preflight_failure_has_no_partial_writes) ... ok
test_docs_project_parallel_apply_keeps_both_rows (test_agentctl.AgentctlTests.test_docs_project_parallel_apply_keeps_both_rows) ... ok
test_docs_project_preview_apply_receipts_and_projections (test_agentctl.AgentctlTests.test_docs_project_preview_apply_receipts_and_projections) ... ok
test_docs_project_same_row_race_base_changed_no_partial (test_agentctl.AgentctlTests.test_docs_project_same_row_race_base_changed_no_partial) ... ok
test_docs_row_cas_preserves_unrelated_rows_and_is_idempotent (test_agentctl.AgentctlTests.test_docs_row_cas_preserves_unrelated_rows_and_is_idempotent) ... ok
test_env_guard_accepts_clean_main_and_requires_all_target_leases (test_agentctl.AgentctlTests.test_env_guard_accepts_clean_main_and_requires_all_target_leases) ... ok
test_env_guard_refuses_linked_worktree_even_with_capability (test_agentctl.AgentctlTests.test_env_guard_refuses_linked_worktree_even_with_capability) ... ok
test_failed_child_releases_lease_and_preserves_failure (test_agentctl.AgentctlTests.test_failed_child_releases_lease_and_preserves_failure) ... ok
test_failed_revalidation_revokes_old_success_receipt (test_agentctl.AgentctlTests.test_failed_revalidation_revokes_old_success_receipt) ... ok
test_guard_build_requires_exact_clean_commit (test_agentctl.AgentctlTests.test_guard_build_requires_exact_clean_commit) ... ok
test_hook_checks_reject_out_of_scope_and_secret_paths (test_agentctl.AgentctlTests.test_hook_checks_reject_out_of_scope_and_secret_paths) ... ok
test_hook_rejects_shared_document_without_publish_receipt (test_agentctl.AgentctlTests.test_hook_rejects_shared_document_without_publish_receipt) ... ok
test_hooks_allow_published_shared_document_receipt (test_agentctl.AgentctlTests.test_hooks_allow_published_shared_document_receipt) ... ok
test_hooks_allow_scoped_task_commit_under_live_lease (test_agentctl.AgentctlTests.test_hooks_allow_scoped_task_commit_under_live_lease) ... ok
test_hooks_refuse_overwriting_foreign_hook (test_agentctl.AgentctlTests.test_hooks_refuse_overwriting_foreign_hook) ... ok
test_init_cannot_steal_live_legacy_lock (test_agentctl.AgentctlTests.test_init_cannot_steal_live_legacy_lock) ... ok
test_init_preserves_existing_legacy_lock_inode (test_agentctl.AgentctlTests.test_init_preserves_existing_legacy_lock_inode) ... ok
test_installed_hooks_reject_direct_dev_commit (test_agentctl.AgentctlTests.test_installed_hooks_reject_direct_dev_commit) ... ok
test_interrupted_validation_stops_child_before_releasing_scope (test_agentctl.AgentctlTests.test_interrupted_validation_stops_child_before_releasing_scope) ... ok
test_invalid_document_request_shape_has_no_write_side_effects (test_agentctl.AgentctlTests.test_invalid_document_request_shape_has_no_write_side_effects) ... ok
test_linked_worktree_uses_only_main_coordination_root (test_agentctl.AgentctlTests.test_linked_worktree_uses_only_main_coordination_root) ... ok
test_live_and_stale_credentials_cannot_be_spoofed (test_agentctl.AgentctlTests.test_live_and_stale_credentials_cannot_be_spoofed) ... ok
test_live_run_cannot_be_recovered_or_task_closed (test_agentctl.AgentctlTests.test_live_run_cannot_be_recovered_or_task_closed) ... ok
test_matrix_done_without_evidence_refused (test_agentctl.AgentctlTests.test_matrix_done_without_evidence_refused) ... ok
test_matrix_duplicate_or_rename_record_id_rejected (test_agentctl.AgentctlTests.test_matrix_duplicate_or_rename_record_id_rejected) ... ok
test_matrix_record_id_layout_migration_without_new_dones (test_agentctl.AgentctlTests.test_matrix_record_id_layout_migration_without_new_dones)
Additive record_id column must not trip EVIDENCE_REQUIRED when statuses unchanged. ... ok
test_matrix_unknown_columns_and_lineage_preserved (test_agentctl.AgentctlTests.test_matrix_unknown_columns_and_lineage_preserved) ... ok
test_multi_resource_acquisition_is_atomic (test_agentctl.AgentctlTests.test_multi_resource_acquisition_is_atomic) ... ok
test_parent_child_path_conflict_and_disjoint_coexistence (test_agentctl.AgentctlTests.test_parent_child_path_conflict_and_disjoint_coexistence) ... ok
test_path_scope_and_capability_are_enforced (test_agentctl.AgentctlTests.test_path_scope_and_capability_are_enforced) ... ok
test_policy_invalid_json_rejects_write (test_agentctl.AgentctlTests.test_policy_invalid_json_rejects_write) ... ok
test_policy_missing_rejects_write_but_status_stays_readonly (test_agentctl.AgentctlTests.test_policy_missing_rejects_write_but_status_stays_readonly) ... ok
test_policy_protocol_too_low_rejects_write (test_agentctl.AgentctlTests.test_policy_protocol_too_low_rejects_write) ... ok
test_quarantine_sweep_keeps_live_child_and_release_refuses (test_agentctl.AgentctlTests.test_quarantine_sweep_keeps_live_child_and_release_refuses) ... ok
test_quarantine_ttl_demotes_never_auto_completes (test_agentctl.AgentctlTests.test_quarantine_ttl_demotes_never_auto_completes) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/email/errors.py:12: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x762ddceb67a0>
class MessageParseError(MessageError):
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/email/errors.py:12: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x762ddceb7c40>
class MessageParseError(MessageError):
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/email/errors.py:12: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x762ddbe9f790>
class MessageParseError(MessageError):
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/email/errors.py:12: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x762ddbe9e4d0>
class MessageParseError(MessageError):
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/email/errors.py:12: ResourceWarning: unclosed database in <sqlite3.Connection object at 0x762ddbe9ec50>
class MessageParseError(MessageError):
ResourceWarning: Enable tracemalloc to get the object allocation traceback
{"demoted": [{"from": "running", "run_id": "hb-stale", "to": "suspect"}, {"from": "suspect", "run_id": "sus-stale", "to": "quarantine"}], "heartbeat_ttl_seconds": 300, "status": "ok", "suspect_ttl_seconds": 600}
ok
test_quarantine_unreadable_proc_stops_at_suspect (test_agentctl.AgentctlTests.test_quarantine_unreadable_proc_stops_at_suspect) ... {"demoted": [{"from": "running", "run_id": "run-unknown", "to": "suspect"}], "heartbeat_ttl_seconds": 300, "status": "ok", "suspect_ttl_seconds": 600}
ok
test_queue_enqueue_list_cancel_roundtrip (test_agentctl.AgentctlTests.test_queue_enqueue_list_cancel_roundtrip) ... ok
test_queue_schedule_promotes_validated_candidate (test_agentctl.AgentctlTests.test_queue_schedule_promotes_validated_candidate) ... ok
test_queue_subparser_registered_once (test_agentctl.AgentctlTests.test_queue_subparser_registered_once)
Duplicate queue registration must not shadow handler-compatible flags. ... usage: python3.14 -m unittest [-h] [--repo REPO]
{init,status,doctor,task,with,guard,worktree-check,docs,queue,cleanup,recover,operation,freeze,unfreeze,hooks,validate,integrate,deploy,claim,quarantine,daemon} ...
python3.14 -m unittest: error: unrecognized arguments: --head-sha deadbeef
ok
test_recover_git_dev_requires_verification_marker (test_agentctl.AgentctlTests.test_recover_git_dev_requires_verification_marker) ... ok
test_recovery_refuses_dead_supervisor_with_live_child (test_agentctl.AgentctlTests.test_recovery_refuses_dead_supervisor_with_live_child) ... ok
test_registered_active_task_must_close_before_cleanup (test_agentctl.AgentctlTests.test_registered_active_task_must_close_before_cleanup) ... ok
test_replaced_compatibility_inode_is_not_a_new_free_lock (test_agentctl.AgentctlTests.test_replaced_compatibility_inode_is_not_a_new_free_lock) ... ok
test_shared_lease_does_not_satisfy_write_guard (test_agentctl.AgentctlTests.test_shared_lease_does_not_satisfy_write_guard) ... ok
test_stale_fence_env_is_stale_generation (test_agentctl.AgentctlTests.test_stale_fence_env_is_stale_generation) ... ok
test_task_close_does_not_auto_promote_matrix_done (test_agentctl.AgentctlTests.test_task_close_does_not_auto_promote_matrix_done) ... ok
test_task_index_lease_serializes_index_but_not_disjoint_work (test_agentctl.AgentctlTests.test_task_index_lease_serializes_index_but_not_disjoint_work) ... ok
test_task_registration_rejects_protected_branch_and_invalid_scopes (test_agentctl.AgentctlTests.test_task_registration_rejects_protected_branch_and_invalid_scopes) ... ok
test_uninitialized_status_and_doctor_do_not_create_state (test_agentctl.AgentctlTests.test_uninitialized_status_and_doctor_do_not_create_state) ... ok
test_unset_fence_env_matches_default_zero (test_agentctl.AgentctlTests.test_unset_fence_env_matches_default_zero)
Self-bootstrap: hooks re-exec updated agentctl without AGENTCTL_FENCE set. ... ok
test_validated_linear_commit_cannot_bypass_candidate_entrypoint (test_agentctl.AgentctlTests.test_validated_linear_commit_cannot_bypass_candidate_entrypoint) ... ok
test_validated_merge_candidate_is_exactly_promoted_to_dev (test_agentctl.AgentctlTests.test_validated_merge_candidate_is_exactly_promoted_to_dev) ... ok
test_boot_and_self_proc_are_observable (test_agentctl.ProcessPlatformTests.test_boot_and_self_proc_are_observable) ... ok
test_descent_unreadable_is_unknown (test_agentctl.ProcessPlatformTests.test_descent_unreadable_is_unknown) ... ok
test_gone_pid_is_dead (test_agentctl.ProcessPlatformTests.test_gone_pid_is_dead) ... ok
test_lease_denies_when_supervisor_proc_unreadable (test_agentctl.ProcessPlatformTests.test_lease_denies_when_supervisor_proc_unreadable) ... ok
test_unreadable_proc_is_unknown_not_dead (test_agentctl.ProcessPlatformTests.test_unreadable_proc_is_unknown_not_dead) ... ok
test_zombie_is_not_alive (test_agentctl.ProcessPlatformTests.test_zombie_is_not_alive) ... ok
test_build_projection_row_canonical (test_agentctl_w4.AgentctlW4Tests.test_build_projection_row_canonical) ... ok
test_deploy_recover_loop_failure_sets_recovery_required (test_agentctl_w4.AgentctlW4Tests.test_deploy_recover_loop_failure_sets_recovery_required) ... ok
test_deploy_recover_loop_success_with_fake_command (test_agentctl_w4.AgentctlW4Tests.test_deploy_recover_loop_success_with_fake_command) ... ok
test_document_projection_cas_mismatch_refuses (test_agentctl_w4.AgentctlW4Tests.test_document_projection_cas_mismatch_refuses) ... ok
test_project_cleanup_markdown_header_is_report_only (test_agentctl_w4.AgentctlW4Tests.test_project_cleanup_markdown_header_is_report_only) ... ok
test_recover_deploy_from_recovery_required (test_agentctl_w4.AgentctlW4Tests.test_recover_deploy_from_recovery_required) ... ok
test_row_projection_ambiguous_record_refuses (test_agentctl_w4.AgentctlW4Tests.test_row_projection_ambiguous_record_refuses) ... ok
test_row_projection_cas_and_insert (test_agentctl_w4.AgentctlW4Tests.test_row_projection_cas_and_insert) ... ok
test_cwd_visibility_structured_on_host (test_agentctl_w4.DarwinCwdVisibilityTests.test_cwd_visibility_structured_on_host) ... ok
test_cleanup_report_never_deletes (test_codegraph_governance.CodegraphGovernanceTest.test_cleanup_report_never_deletes) ... ok
test_codegraph_sync_fail_open_without_binary (test_codegraph_governance.CodegraphGovernanceTest.test_codegraph_sync_fail_open_without_binary) ... ok
test_create_skips_codegraph_when_missing (test_codegraph_governance.CodegraphGovernanceTest.test_create_skips_codegraph_when_missing) ... ok
test_docs_drift_detects_done_regression (test_codegraph_governance.CodegraphGovernanceTest.test_docs_drift_detects_done_regression) ... ok
test_hooks_install_includes_codegraph_sync_hooks (test_codegraph_governance.CodegraphGovernanceTest.test_hooks_install_includes_codegraph_sync_hooks) ... ok
test_both_expected_tests_pass (test_go_test_evidence.TestEvidence.test_both_expected_tests_pass) ... ok
test_missing_partial_skip_failure_and_duplicate_are_not_pass (test_go_test_evidence.TestEvidence.test_missing_partial_skip_failure_and_duplicate_are_not_pass) ... ok
test_subtests_and_case_mismatches_do_not_prove_parent_execution (test_go_test_evidence.TestEvidence.test_subtests_and_case_mismatches_do_not_prove_parent_execution) ... ok
test_all_profiles_have_noninteractive_flags_and_explicit_pi_routes (test_herdr_agents.HerdrTests.test_all_profiles_have_noninteractive_flags_and_explicit_pi_routes) ... ok
test_explicit_prewrite_retry_reaches_flash (test_herdr_agents.HerdrTests.test_explicit_prewrite_retry_reaches_flash) ... ok
test_implementation_fallback_is_explicit (test_herdr_agents.HerdrTests.test_implementation_fallback_is_explicit) ... ok
test_kimi_atomic_limit_across_eight_processes (test_herdr_agents.HerdrTests.test_kimi_atomic_limit_across_eight_processes) ... ok
test_kimi_reason_disabled_marker_and_cross_lane_budget (test_herdr_agents.HerdrTests.test_kimi_reason_disabled_marker_and_cross_lane_budget) ... ok
test_kimi_uncertain_send_consumes_budget (test_herdr_agents.HerdrTests.test_kimi_uncertain_send_consumes_budget) ... ok
test_main_or_wrong_target_pane_cannot_start (test_herdr_agents.HerdrTests.test_main_or_wrong_target_pane_cannot_start) ... ok
test_outside_herdr_and_wrong_task_cwd_never_contact_transport (test_herdr_agents.HerdrTests.test_outside_herdr_and_wrong_task_cwd_never_contact_transport) ... ok
test_report_routes_to_max_not_kimi_or_minimax (test_herdr_agents.HerdrTests.test_report_routes_to_max_not_kimi_or_minimax) ... ok
test_same_role_ambiguity_and_foreign_cwd_do_not_dispatch (test_herdr_agents.HerdrTests.test_same_role_ambiguity_and_foreign_cwd_do_not_dispatch) ... ok
test_uncertain_transport_error_and_blocked_do_not_fallback (test_herdr_agents.HerdrTests.test_uncertain_transport_error_and_blocked_do_not_fallback) ... ok
test_authorized_pinned_rebuild_preserves_no_build (test_ops_guards.OpsGuardsTest.test_authorized_pinned_rebuild_preserves_no_build) ... ok
test_build_accepts_nightly_evidence_excluded_from_build_context (test_ops_guards.OpsGuardsTest.test_build_accepts_nightly_evidence_excluded_from_build_context) ... ok
test_build_context_is_frozen_when_managed_source_changes (test_ops_guards.OpsGuardsTest.test_build_context_is_frozen_when_managed_source_changes) ... ok
test_build_dirty_source_refuses_before_docker_or_receipts (test_ops_guards.OpsGuardsTest.test_build_dirty_source_refuses_before_docker_or_receipts) ... ok
test_build_mismatched_commit_refuses_before_docker_or_receipts (test_ops_guards.OpsGuardsTest.test_build_mismatched_commit_refuses_before_docker_or_receipts) ... ok
test_build_untracked_source_refuses_before_docker_or_receipts (test_ops_guards.OpsGuardsTest.test_build_untracked_source_refuses_before_docker_or_receipts) ... ok
test_clean_build_records_exact_source_commit (test_ops_guards.OpsGuardsTest.test_clean_build_records_exact_source_commit) ... ok
test_environment_lease_cannot_deploy_dirty_main_source (test_ops_guards.OpsGuardsTest.test_environment_lease_cannot_deploy_dirty_main_source) ... ok
test_invalid_action_does_not_initialize_environment (test_ops_guards.OpsGuardsTest.test_invalid_action_does_not_initialize_environment) ... ok
test_linked_worktree_writes_refuse_and_queries_remain_read_only (test_ops_guards.OpsGuardsTest.test_linked_worktree_writes_refuse_and_queries_remain_read_only) ... ok
test_one_environment_lease_does_not_authorize_all (test_ops_guards.OpsGuardsTest.test_one_environment_lease_does_not_authorize_all) ... ok
test_queries_never_initialize_environment (test_ops_guards.OpsGuardsTest.test_queries_never_initialize_environment) ... ok
test_service_writes_without_lease_refuse_before_any_side_effect (test_ops_guards.OpsGuardsTest.test_service_writes_without_lease_refuse_before_any_side_effect) ... ok
test_valid_lease_cannot_hide_linked_caller_or_linked_script (test_ops_guards.OpsGuardsTest.test_valid_lease_cannot_hide_linked_caller_or_linked_script) ... ok
test_00_executor_module_present (test_pi_subagents.PiSubagentsTests.test_00_executor_module_present) ... ok
test_approved_commands_do_not_mutate_model_snapshot (test_pi_subagents.PiSubagentsTests.test_approved_commands_do_not_mutate_model_snapshot) ... ok
test_config_catalog_parses_known_pool_and_roles (test_pi_subagents.PiSubagentsTests.test_config_catalog_parses_known_pool_and_roles) ... ok
test_config_rejects_unknown_alias_and_cycle (test_pi_subagents.PiSubagentsTests.test_config_rejects_unknown_alias_and_cycle) ... ok
test_config_reload_snapshot_on_dispatch (test_pi_subagents.PiSubagentsTests.test_config_reload_snapshot_on_dispatch) ... ok
test_corrupt_receipt_fail_closed (test_pi_subagents.PiSubagentsTests.test_corrupt_receipt_fail_closed) ... ok
test_crash_before_receipt_terminal_is_unknown_on_status (test_pi_subagents.PiSubagentsTests.test_crash_before_receipt_terminal_is_unknown_on_status) ... ok
test_disabled_auxiliary_and_m3_critical_remap_denied (test_pi_subagents.PiSubagentsTests.test_disabled_auxiliary_and_m3_critical_remap_denied) ... ok
test_extension_outside_model_snapshot_inventory (test_pi_subagents.PiSubagentsTests.test_extension_outside_model_snapshot_inventory)
Model snapshot must contain only declared inputs; -e guard lives outside it. ... ok
test_failed_replay_stays_failure (test_pi_subagents.PiSubagentsTests.test_failed_replay_stays_failure) ... ok
test_forged_lease_env_refused (test_pi_subagents.PiSubagentsTests.test_forged_lease_env_refused) ... ok
test_global_concurrency_limit (test_pi_subagents.PiSubagentsTests.test_global_concurrency_limit) ... ok
test_idempotent_replay_and_digest_mismatch (test_pi_subagents.PiSubagentsTests.test_idempotent_replay_and_digest_mismatch) ... ok
test_input_path_traversal_symlink_secret_refusal (test_pi_subagents.PiSubagentsTests.test_input_path_traversal_symlink_secret_refusal) ... ok
test_leader_exit_reaps_orphan_child (test_pi_subagents.PiSubagentsTests.test_leader_exit_reaps_orphan_child) ... ok
test_malformed_and_empty_outputs_fail (test_pi_subagents.PiSubagentsTests.test_malformed_and_empty_outputs_fail) ... ok
test_mcp_from_worktree_uses_main_agentctl (test_pi_subagents.PiSubagentsTests.test_mcp_from_worktree_uses_main_agentctl)
Server INSTALL_ROOT may be a linked worktree; agentctl must resolve from main. ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_mcp_initialize_list_run_result (test_pi_subagents.PiSubagentsTests.test_mcp_initialize_list_run_result) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_mcp_running_poll_is_not_error (test_pi_subagents.PiSubagentsTests.test_mcp_running_poll_is_not_error) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_mcp_surfaces_role_disabled_and_scope_refusal (test_pi_subagents.PiSubagentsTests.test_mcp_surfaces_role_disabled_and_scope_refusal) ... /home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=10 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=6 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=8 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=7 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=4 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/linuxbrew/.linuxbrew/Cellar/python@3.14/3.14.7/lib/python3.14/unittest/case.py:615: ResourceWarning: unclosed file <_io.TextIOWrapper name=5 encoding='UTF-8'>
result = method()
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
test_model_mismatch_fails (test_pi_subagents.PiSubagentsTests.test_model_mismatch_fails) ... ok
test_naked_run_without_lease_refused (test_pi_subagents.PiSubagentsTests.test_naked_run_without_lease_refused) ... ok
test_orphan_result_is_readonly (test_pi_subagents.PiSubagentsTests.test_orphan_result_is_readonly) ... ok
test_output_cap_on_stdout_flood (test_pi_subagents.PiSubagentsTests.test_output_cap_on_stdout_flood) ... ok
test_readonly_guard_allows_exact_root_listing (test_pi_subagents.PiSubagentsTests.test_readonly_guard_allows_exact_root_listing)
Exact-root ls/grep must pass when snapshot inventory is allowlisted-only. ... ok
test_readonly_guard_rejects_at_prefix (test_pi_subagents.PiSubagentsTests.test_readonly_guard_rejects_at_prefix)
Exercise extension path policy via Node strip-types if available. ... ok
test_replay_keeps_old_config_after_disable (test_pi_subagents.PiSubagentsTests.test_replay_keeps_old_config_after_disable) ... ok
test_roles_does_not_invoke_pi (test_pi_subagents.PiSubagentsTests.test_roles_does_not_invoke_pi) ... ok
test_run_emits_exact_pi_flags_and_receipt (test_pi_subagents.PiSubagentsTests.test_run_emits_exact_pi_flags_and_receipt) ... ok
test_scope_refusal_outside_report_path (test_pi_subagents.PiSubagentsTests.test_scope_refusal_outside_report_path) ... ok
test_scrub_env_drops_pi_dir_for_tests (test_pi_subagents.PiSubagentsTests.test_scrub_env_drops_pi_dir_for_tests) ... ok
test_source_sha_rejects_symbolic_refs (test_pi_subagents.PiSubagentsTests.test_source_sha_rejects_symbolic_refs) ... ok
test_test_run_requires_approved_commands_and_records_failure (test_pi_subagents.PiSubagentsTests.test_test_run_requires_approved_commands_and_records_failure) ... ok
test_timeout_kills_process_group (test_pi_subagents.PiSubagentsTests.test_timeout_kills_process_group) ... ok
----------------------------------------------------------------------
Ran 173 tests in 111.229s
OK
@@ -0,0 +1,10 @@
{
"exit_code": 0,
"provider": "zai-coding-cn",
"model": "glm-5.3",
"stopReason": "stop",
"settled": true,
"seconds": 67.6,
"source_sha256": "b1743d0f632c0719d56a39214a419163c195ed11ef53b16d48015ad6dd794ae3",
"review": "## 审查结论:发现 3 项可实际触发的缺陷\n\n### 1. 祖先目录规则使 grep/find 递归读取白名单之外的文件\n\n`allowlistOk()` 中:当 `resolved` 是目录且是某个白名单文件的祖先(`rel.startsWith(f + path.sep)` 分支)时直接放行。但 guard 并不区分工具类型——对 `grep`/`find` 这类递归工具,传入白名单文件的任一祖先目录(包括 snapshot root 本身)即满足该条件,工具会递归扫描目录下**所有**文件,包括未列入 `PI_SUBAGENT_ALLOWED_FILES` 的文件。\n\n**触发**:白名单为 `[\"sub/a.txt\"]`,调用 `grep { pattern: \"secret\", path: \"<root>\" }`(或 `path: \"sub\"`)。`allowlistOk` 因 root 是 `sub/a.txt` 的祖先目录而返回 true,grep 读取整个快照内全部文件内容。\n\n### 2. 未提供路径时完全跳过白名单检查\n\n`paths.length === 0` 分支只检查 `process.cwd()` 是否在 root 内,通过即直接 `return undefined`,`allowlistOk` 从未被调用。依赖默认 cwd 的 `grep`/`find`/`ls` 调用因此绕过白名单。\n\n**触发**:设置 `PI_SUBAGENT_ALLOWED_FILES=[\"a.txt\"]`,进程 cwd 位于 snapshot root 内,调用 `grep { pattern: \"x\" }`(无 path 字段)。guard 仅验证 cwd 在 root 内即放行,grep 递归扫描 cwd 下所有文件(如 `b.txt`、`.env` 以外的任意敏感文件),白名单被完全绕过。(且此分支连 `.env`/`.git` 的 basename 黑名单也不执行。)\n\n### 3. 相对路径按 root 校验、但按 cwd 解析,且 `paths` 数组从不回写\n\n- `contained()` 对相对路径用 `path.resolve(root, candidate)` 校验;\n- 但回写(canonicalize)只覆盖 `path/file/filename/directory/dir` 五个单值键,`paths` 数组、`root`、`cwd` 键只校验不回写;\n- 给出路径时 guard 从不检查 `process.cwd()` 是否在 root 内。\n\n因此对 `paths` 数组中的相对条目(或 `root`/`cwd` 键),guard 验证的是「相对 root」的路径,而下游工具按 pi 的 resolveToCwd 语义相对实际 cwd 打开——校验对象与实际打开对象可以是两个不同文件。\n\n**触发**:`PI_SUBAGENT_SNAPSHOT_ROOT=/tmp/snap`,进程 cwd 为 `/home/user`(root 外)。`/tmp/snap/notes.txt` 在白名单中,同时 `/home/user/notes.txt` 是要窃读的目标。调用 `grep { pattern: \".\", paths: [\"notes.txt\"] }`:guard 校验通过 `/tmp/snap/notes.txt`(存在、在白名单),但 `paths` 未回写,builtin 相对 cwd 打开 `/home/user/notes.txt`——快照与白名单之外的内容被读取。\n\n(次要相关点:即便单值键,也只有 `input.path === p` 精确相等才回写,若输入含前后空白,校验与回写也会脱节,属同一缺陷族的变体。)"
}
@@ -0,0 +1,152 @@
{
"initialize": "PASS",
"tools": 3,
"cases": [
{
"request_id": "real-mcp-research-02",
"role_id": "research",
"run_status": "succeeded",
"verdict": "not_applicable",
"review_state": "pending",
"configured_provider": "qwen-token-plan-cn",
"configured_model": "qwen3.8-flash",
"observed_provider": "qwen-token-plan-cn",
"observed_model": "qwen3.8-flash",
"tool_calls": [
{
"isError": false,
"toolName": "read",
"type": "tool_execution_start"
},
{
"isError": false,
"toolName": "ls",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "ls",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_end"
}
],
"isError": false
},
{
"request_id": "real-mcp-test-01",
"role_id": "test_run",
"run_status": "succeeded",
"verdict": "pass",
"review_state": "commands_passed",
"configured_provider": "zai-coding-cn",
"configured_model": "glm-5.3-flash",
"observed_provider": "zai-coding-cn",
"observed_model": "glm-5.3-flash",
"tool_calls": [
{
"isError": false,
"toolName": "ls",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "ls",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "read",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "read",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_start"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_end"
}
],
"test_outcomes": [
{
"argv": [
"python3",
"-c",
"import os,pathlib; assert pathlib.Path('PROJECT.md').is_file(); assert 'AGENTCTL_RUN_ID' not in os.environ; assert 'PI_CODING_AGENT_DIR' not in os.environ; print('PI_ISOLATED_TEST_PASS')"
],
"cwd": "/tmp/pi-subagent-test-snap-t52sfrdr",
"duration_seconds": 0.086,
"exit_code": 0,
"status": "ok",
"stderr_path": "command-evidence/00-stderr.txt",
"stderr_preview": "",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_path": "command-evidence/00-stdout.txt",
"stdout_preview": "PI_ISOLATED_TEST_PASS\n",
"stdout_sha256": "960dc8f2b47ab4a6214cc9358d79fcd36909f181034664405bc5440d7420b7ec"
}
],
"isError": false
},
{
"request_id": "real-mcp-plan-01",
"role_id": "planning",
"run_status": "succeeded",
"verdict": "not_applicable",
"review_state": "pending",
"configured_provider": "qwen-token-plan-cn",
"configured_model": "qwen3.8-max",
"observed_provider": "qwen-token-plan-cn",
"observed_model": "qwen3.8-max",
"tool_calls": [
{
"isError": false,
"toolName": "ls",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "ls",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "find",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "find",
"type": "tool_execution_end"
}
],
"isError": false
},
{
"code": "ROLE_DISABLED",
"request_id": "real-mcp-disabled-01",
"isError": true
}
]
}
@@ -0,0 +1,83 @@
{
"approved_commands_sha256": null,
"authorization_ref": "user requested automatic PI executor isolated acceptance",
"config_revision": "subagent-models-2026-09-24-r1",
"config_sha256": "0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0",
"configured_alias": "qwen_flash",
"configured_model": "qwen3.8-flash",
"configured_provider": "qwen-token-plan-cn",
"errors": [],
"final_text": "## Result\n\n**ls on snapshot root: blocked.** Every enumeration call returned `directory contains non-allowlisted file`:\n- `ls .` → blocked\n- `ls /tmp/pi-subagent-snap-t4436jt2` → blocked\n- `find * -path /tmp/pi-subagent-snap-t4436jt2` → blocked\n\nSo I could not get a ground-truth directory listing of the snapshot. Single-file reads do work (`PROJECT.md` was read successfully). Notably, the only evidence in the packet is `PROJECT.md` itself — no `AGENTS.md`, `.doc/*`, `cmd/`, `web/`, etc. were provided, and I did not attempt to fetch files outside the allowlist.\n\n**Two repository directories** (from the `PROJECT.md` 仓库地图 table, i.e. documented rather than filesystem-verified):\n\n| 路径 | 用途 |\n|---|---|\n| `cmd/`、`internal/` | 服务入口与 Go 模块 |\n| `web/` | React/Vite 前端 |\n\n**Caveats / scope notes:**\n- The directory names above are *claims from PROJECT.md*, not confirmed to exist in this snapshot — the read-only probe was blocked, so they are unverified.\n- PROJECT.md references credentials at `${HOME}/.secrets/gitea` (`ACCESS_TOKEN`, `URL_PREFIX`). That is outside the snapshot and is a secret store: I did not read, list, or reference it, and the caller goal did not authorize doing so.\n- `PROJECT.md` is only an entry-point doc; it explicitly says it does not cache \"no blocker\" status or current schema, and that env topology (ports 8080/8081/8000/5432-5435, compose names) is convention only, to be verified against target-environment receipts. Any follow-up wanting authoritative state would need `.doc/ops/CURRENT.md`, `.doc/status/FEATURE-MATRIX.md`, `.doc/remediation/gap-matrix.md` — none of which are in this packet.\n- No modifications made; nothing beyond the snapshot was accessed.\n\nCURSOR_PI_AUTOMATION_OK",
"finished_at": 1790222226.6835754,
"goal": "use ls for current snapshot and read PROJECT.md, report two repository directories, finish CURSOR_PI_AUTOMATION_OK",
"input_files": [
{
"bytes": 4183,
"path": "PROJECT.md",
"sha256": "91d148876a9457e646b8adab69670a1a85ac7d067268568ff8db084c0c0801d8"
}
],
"observed_model": "qwen3.8-flash",
"observed_provider": "qwen-token-plan-cn",
"ok": true,
"pi_exit_code": 0,
"plan_digest": null,
"request_digest": "7c7b17d5183cf0fc0dd149cbd9ddcead76e1f78532c8ada9d69d517a16b47c3a",
"request_id": "cursor-auto-research-01",
"retry_disabled": true,
"review_state": "pending",
"role_id": "research",
"run_status": "succeeded",
"source_sha": "52462b45e985f596b2a503fc70ac12c3bd5fe571",
"started_at": 1790222209.62396,
"stderr_preview": "",
"stream_error": null,
"supervisor_boot_id": "a37fa29f-4cbd-4c8a-89d2-d5d31b20615b",
"supervisor_pgid": 2834994,
"supervisor_pid": 2834994,
"supervisor_start": "109360920",
"task_id": "subagent-workflow-standard-2026-09-24-executor",
"tool_calls": [
{
"isError": false,
"toolName": "ls",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "ls",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "ls",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "ls",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "find",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "find",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_start"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_end"
}
],
"verdict": "not_applicable"
}
@@ -0,0 +1,44 @@
# PI subagent report — `cursor-auto-research-01`
- task_id: `subagent-workflow-standard-2026-09-24-executor`
- role_id: `research`
- run_status: `succeeded`
- verdict: `not_applicable`
- config_revision: `subagent-models-2026-09-24-r1`
- config_sha256: `0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0`
- source_sha: `52462b45e985f596b2a503fc70ac12c3bd5fe571`
- configured: `qwen-token-plan-cn/qwen3.8-flash`
- observed: `qwen-token-plan-cn/qwen3.8-flash`
## Goal
use ls for current snapshot and read PROJECT.md, report two repository directories, finish CURSOR_PI_AUTOMATION_OK
## Model output
## Result
**ls on snapshot root: blocked.** Every enumeration call returned `directory contains non-allowlisted file`:
- `ls .` → blocked
- `ls /tmp/pi-subagent-snap-t4436jt2` → blocked
- `find * -path /tmp/pi-subagent-snap-t4436jt2` → blocked
So I could not get a ground-truth directory listing of the snapshot. Single-file reads do work (`PROJECT.md` was read successfully). Notably, the only evidence in the packet is `PROJECT.md` itself — no `AGENTS.md`, `.doc/*`, `cmd/`, `web/`, etc. were provided, and I did not attempt to fetch files outside the allowlist.
**Two repository directories** (from the `PROJECT.md` 仓库地图 table, i.e. documented rather than filesystem-verified):
| 路径 | 用途 |
|---|---|
| `cmd/`、`internal/` | 服务入口与 Go 模块 |
| `web/` | React/Vite 前端 |
**Caveats / scope notes:**
- The directory names above are *claims from PROJECT.md*, not confirmed to exist in this snapshot — the read-only probe was blocked, so they are unverified.
- PROJECT.md references credentials at `${HOME}/.secrets/gitea` (`ACCESS_TOKEN`, `URL_PREFIX`). That is outside the snapshot and is a secret store: I did not read, list, or reference it, and the caller goal did not authorize doing so.
- `PROJECT.md` is only an entry-point doc; it explicitly says it does not cache "no blocker" status or current schema, and that env topology (ports 8080/8081/8000/5432-5435, compose names) is convention only, to be verified against target-environment receipts. Any follow-up wanting authoritative state would need `.doc/ops/CURRENT.md`, `.doc/status/FEATURE-MATRIX.md`, `.doc/remediation/gap-matrix.md` — none of which are in this packet.
- No modifications made; nothing beyond the snapshot was accessed.
CURSOR_PI_AUTOMATION_OK
_Note: model narrative is not an approved review verdict; run_status and verdict are separate._
@@ -0,0 +1,63 @@
{
"approved_commands_sha256": null,
"authorization_ref": "user requested PI executor isolated acceptance",
"config_revision": "subagent-models-2026-09-24-r1",
"config_sha256": "0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0",
"configured_alias": "qwen_flash",
"configured_model": "qwen3.8-flash",
"configured_provider": "qwen-token-plan-cn",
"errors": [],
"final_text": "**Tool results (truthful report):**\n\n1. `ls(path='.')` — **succeeded**. Output: `PROJECT.md`. Confirms the snapshot contains PROJECT.md (and no other entries).\n2. `read(path='PROJECT.md')` — **succeeded**. Content is verbatim identical to the evidence packet (no discrepancies observed). No `.git`, `.env`, or out-of-snapshot paths were accessed; no tool failures occurred.\n\n**Two directory names from the repository map (仓库地图):**\n\n- \"`cmd/`、`internal/` | 服务入口与 Go 模块\" → **`cmd/`**\n- \"`web/` | React/Vite 前端\" → **`web/`**\n\nCURSOR_PI_AUTOMATION_OK",
"finished_at": 1790222752.7294602,
"goal": "call ls(path='.') and confirm the snapshot contains PROJECT.md; call read on PROJECT.md; quote two directory names declared in its repository map; finish CURSOR_PI_AUTOMATION_OK. Report any tool failure truthfully; do not call failed ls/read success.",
"input_files": [
{
"bytes": 4183,
"path": "PROJECT.md",
"sha256": "91d148876a9457e646b8adab69670a1a85ac7d067268568ff8db084c0c0801d8"
}
],
"observed_model": "qwen3.8-flash",
"observed_provider": "qwen-token-plan-cn",
"ok": true,
"pi_exit_code": 0,
"plan_digest": null,
"request_digest": "a83155fc059c23e884be38339c8e442f71d30e49e913966848289ae7d4d107eb",
"request_id": "cursor-auto-research-02",
"retry_disabled": true,
"review_state": "pending",
"role_id": "research",
"run_status": "succeeded",
"source_sha": "52462b45e985f596b2a503fc70ac12c3bd5fe571",
"started_at": 1790222746.4985802,
"stderr_preview": "",
"stream_error": null,
"supervisor_boot_id": "a37fa29f-4cbd-4c8a-89d2-d5d31b20615b",
"supervisor_pgid": 2883592,
"supervisor_pid": 2883592,
"supervisor_start": "109414606",
"task_id": "subagent-workflow-standard-2026-09-24-executor",
"tool_calls": [
{
"isError": false,
"toolName": "ls",
"type": "tool_execution_start"
},
{
"isError": false,
"toolName": "ls",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_start"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_end"
}
],
"verdict": "not_applicable"
}
@@ -0,0 +1,32 @@
# PI subagent report — `cursor-auto-research-02`
- task_id: `subagent-workflow-standard-2026-09-24-executor`
- role_id: `research`
- run_status: `succeeded`
- verdict: `not_applicable`
- config_revision: `subagent-models-2026-09-24-r1`
- config_sha256: `0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0`
- source_sha: `52462b45e985f596b2a503fc70ac12c3bd5fe571`
- configured: `qwen-token-plan-cn/qwen3.8-flash`
- observed: `qwen-token-plan-cn/qwen3.8-flash`
## Goal
call ls(path='.') and confirm the snapshot contains PROJECT.md; call read on PROJECT.md; quote two directory names declared in its repository map; finish CURSOR_PI_AUTOMATION_OK. Report any tool failure truthfully; do not call failed ls/read success.
## Model output
**Tool results (truthful report):**
1. `ls(path='.')` — **succeeded**. Output: `PROJECT.md`. Confirms the snapshot contains PROJECT.md (and no other entries).
2. `read(path='PROJECT.md')` — **succeeded**. Content is verbatim identical to the evidence packet (no discrepancies observed). No `.git`, `.env`, or out-of-snapshot paths were accessed; no tool failures occurred.
**Two directory names from the repository map (仓库地图):**
- "`cmd/`、`internal/` | 服务入口与 Go 模块" → **`cmd/`**
- "`web/` | React/Vite 前端" → **`web/`**
CURSOR_PI_AUTOMATION_OK
_Note: model narrative is not an approved review verdict; run_status and verdict are separate._
@@ -0,0 +1,63 @@
{
"approved_commands_sha256": null,
"authorization_ref": "user:接入自动调用pi的执行器; isolated engineering acceptance",
"config_revision": "subagent-models-2026-09-24-r1",
"config_sha256": "0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0",
"configured_alias": "qwen_max",
"configured_model": "qwen3.8-max",
"configured_provider": "qwen-token-plan-cn",
"errors": [],
"final_text": "快照目录不可列举,以下仅依据证据包中提供的 `.doc/ops/2026-09-24-subagent-model-config.md`(config_revision `subagent-models-2026-09-24-r1`)内容整理。\n\n## 1. Cursor 职责\n\n- **代码实施与修复固定由 Cursor Auto 承担**(§2 模型池说明 + §3 表头前置说明 + §4 `implementation_executor = cursor_auto`)。\n- §3 角色路由表**只路由 PI 子任务**;PI 不修改产品代码或测试源码,所需修复/新增测试一律**交回 Cursor**。\n- 主会话为 Cursor(模型选择 Auto),工程子任务为本机 gmktec PI CLI;该配置不改变 Cursor UI 模型开关、PI 全局默认设置或产品 PI Agent Runner。\n- 修改 `implementation_executor` 这一边界**需要用户明确授权**。\n\n## 2. M3 限制(alias `minimax_aux`,provider `minimax-cn`,model `MiniMax-M3`)\n\n- **默认停用**:§3 中 `auxiliary` 角色 `enabled = false`,仅在明确启用后才派发;false 时不自动派发,必需职责暂停或显式重新分配,**不能跳过门禁**。\n- **只能用于易核验辅助任务**(格式整理、清单提取等),且**不能承担关键判断**;派发后需保留来源供核验。\n- **不得进入**:规划、代码审查、根因裁决、最终验收,以及**主力自动备用链**。\n- §4 `automatic_fallback = none`:**不因超时或故障降级到 M3**,也不静默换模型/换 provider;`unavailable_action = pause_role`。\n- 初始决策依据记录为“M3 能力有限 → 移出默认流程、仅可选辅助”;改变这些限制须由**用户明确修订 §2**。\n\n## 3. Flash 升级规则\n\n**两个 Flash 角色的升级去向(§3 “升级角色”列):**\n\n| 角色 | 默认模型 | 升级角色 |\n|---|---|---|\n| research | qwen_flash | planning(qwen_max) |\n| test_run | glm_flash | diagnosis(glm_full) |\n| acceptance_check | glm_flash | diagnosis(glm_full) |\n| stage_notes | qwen_flash | report(qwen_max) |\n\n**触发条件(§4 升级触发):**\n- Flash 无法解释失败;\n- 证据互相矛盾;\n- 涉及跨模块机制;\n- 超出已批准验证计划。\n\n**规则约束:**\n- 升级**只用于质量不足,不是网络故障重试**;结果未知时先核对副作用,流程遵循协作规范。\n- 升级时**保留原报告与升级原因**,以目标角色重新派发(§1)。\n- 字段规则:升级角色引用 §3 的 `role_id` 或 `none`;出现**缺字段、未知 alias、循环升级、角色越权**时**停止该派发并报告错误,不自行猜测模型**。\n- 边界不因升级而放宽:`test_run` 仅执行已批准命令/测试产物/环境,不改源码,复现操作由其在授权环境执行,`diagnosis` 只做只读分析;`acceptance_check` **不代签最终验收**,业务取舍交 `acceptance_owner`,最终验收责任仍归任务声明的 acceptance_owner。\n- `reload_boundary = next_dispatch`、`recursive_delegation = false`:升级派发在下一次派发时读取配置,PI 子任务不得自行再派发,需要协作者时只返回建议。\n\nMODEL_PLAN_OK",
"finished_at": 1790221229.1172304,
"goal": "根据配置只列出 Cursor 职责、M3 限制、Flash 升级规则三点,最后输出 MODEL_PLAN_OK。不要评价执行器是否已经实现。",
"input_files": [
{
"bytes": 7876,
"path": ".doc/ops/2026-09-24-subagent-model-config.md",
"sha256": "0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0"
}
],
"observed_model": "qwen3.8-max",
"observed_provider": "qwen-token-plan-cn",
"ok": true,
"pi_exit_code": 0,
"plan_digest": null,
"request_digest": "5a5ce640778270c8f3cd718ac665f85d9e118096455cd893928dd26fc0c74b0e",
"request_id": "real-mcp-plan-01",
"retry_disabled": true,
"review_state": "pending",
"role_id": "planning",
"run_status": "succeeded",
"source_sha": "52462b45e985f596b2a503fc70ac12c3bd5fe571",
"started_at": 1790221203.9798584,
"stderr_preview": "",
"stream_error": null,
"supervisor_boot_id": "a37fa29f-4cbd-4c8a-89d2-d5d31b20615b",
"supervisor_pgid": 2750675,
"supervisor_pid": 2750675,
"supervisor_start": "109260353",
"task_id": "subagent-workflow-standard-2026-09-24-executor",
"tool_calls": [
{
"isError": false,
"toolName": "ls",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "ls",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "find",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "find",
"type": "tool_execution_end"
}
],
"verdict": "not_applicable"
}
@@ -0,0 +1,63 @@
# PI subagent report — `real-mcp-plan-01`
- task_id: `subagent-workflow-standard-2026-09-24-executor`
- role_id: `planning`
- run_status: `succeeded`
- verdict: `not_applicable`
- config_revision: `subagent-models-2026-09-24-r1`
- config_sha256: `0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0`
- source_sha: `52462b45e985f596b2a503fc70ac12c3bd5fe571`
- configured: `qwen-token-plan-cn/qwen3.8-max`
- observed: `qwen-token-plan-cn/qwen3.8-max`
## Goal
根据配置只列出 Cursor 职责、M3 限制、Flash 升级规则三点,最后输出 MODEL_PLAN_OK。不要评价执行器是否已经实现。
## Model output
快照目录不可列举,以下仅依据证据包中提供的 `.doc/ops/2026-09-24-subagent-model-config.md`(config_revision `subagent-models-2026-09-24-r1`)内容整理。
## 1. Cursor 职责
- **代码实施与修复固定由 Cursor Auto 承担**(§2 模型池说明 + §3 表头前置说明 + §4 `implementation_executor = cursor_auto`)。
- §3 角色路由表**只路由 PI 子任务**;PI 不修改产品代码或测试源码,所需修复/新增测试一律**交回 Cursor**。
- 主会话为 Cursor(模型选择 Auto),工程子任务为本机 gmktec PI CLI;该配置不改变 Cursor UI 模型开关、PI 全局默认设置或产品 PI Agent Runner。
- 修改 `implementation_executor` 这一边界**需要用户明确授权**。
## 2. M3 限制(alias `minimax_aux`,provider `minimax-cn`,model `MiniMax-M3`)
- **默认停用**:§3 中 `auxiliary` 角色 `enabled = false`,仅在明确启用后才派发;false 时不自动派发,必需职责暂停或显式重新分配,**不能跳过门禁**。
- **只能用于易核验辅助任务**(格式整理、清单提取等),且**不能承担关键判断**;派发后需保留来源供核验。
- **不得进入**:规划、代码审查、根因裁决、最终验收,以及**主力自动备用链**。
- §4 `automatic_fallback = none`:**不因超时或故障降级到 M3**,也不静默换模型/换 provider;`unavailable_action = pause_role`。
- 初始决策依据记录为“M3 能力有限 → 移出默认流程、仅可选辅助”;改变这些限制须由**用户明确修订 §2**。
## 3. Flash 升级规则
**两个 Flash 角色的升级去向(§3 “升级角色”列):**
| 角色 | 默认模型 | 升级角色 |
|---|---|---|
| research | qwen_flash | planning(qwen_max) |
| test_run | glm_flash | diagnosis(glm_full) |
| acceptance_check | glm_flash | diagnosis(glm_full) |
| stage_notes | qwen_flash | report(qwen_max) |
**触发条件(§4 升级触发):**
- Flash 无法解释失败;
- 证据互相矛盾;
- 涉及跨模块机制;
- 超出已批准验证计划。
**规则约束:**
- 升级**只用于质量不足,不是网络故障重试**;结果未知时先核对副作用,流程遵循协作规范。
- 升级时**保留原报告与升级原因**,以目标角色重新派发(§1)。
- 字段规则:升级角色引用 §3 的 `role_id` 或 `none`;出现**缺字段、未知 alias、循环升级、角色越权**时**停止该派发并报告错误,不自行猜测模型**。
- 边界不因升级而放宽:`test_run` 仅执行已批准命令/测试产物/环境,不改源码,复现操作由其在授权环境执行,`diagnosis` 只做只读分析;`acceptance_check` **不代签最终验收**,业务取舍交 `acceptance_owner`,最终验收责任仍归任务声明的 acceptance_owner。
- `reload_boundary = next_dispatch`、`recursive_delegation = false`:升级派发在下一次派发时读取配置,PI 子任务不得自行再派发,需要协作者时只返回建议。
MODEL_PLAN_OK
_Note: model narrative is not an approved review verdict; run_status and verdict are separate._
@@ -0,0 +1,63 @@
{
"approved_commands_sha256": null,
"authorization_ref": "user:接入自动调用pi的执行器; isolated engineering acceptance",
"config_revision": "subagent-models-2026-09-24-r1",
"config_sha256": "0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0",
"configured_alias": "qwen_flash",
"configured_model": "qwen3.8-flash",
"configured_provider": "qwen-token-plan-cn",
"errors": [],
"final_text": "已读取快照文件 `PROJECT.md`(仅读取该文件,未访问 `.env`/`.git`/凭证或快照外路径)。\n\n**两个仓库目录名(来自「仓库地图」):**\n1. `cmd/` — 服务入口(Go)\n2. `web/` — React/Vite 前端\n\nPI_EXECUTOR_REAL_OK",
"finished_at": 1790221154.765425,
"goal": "Call read on PROJECT.md; report two repository directory names and finish PI_EXECUTOR_REAL_OK.",
"input_files": [
{
"bytes": 4183,
"path": "PROJECT.md",
"sha256": "91d148876a9457e646b8adab69670a1a85ac7d067268568ff8db084c0c0801d8"
}
],
"observed_model": "qwen3.8-flash",
"observed_provider": "qwen-token-plan-cn",
"ok": true,
"pi_exit_code": 0,
"plan_digest": null,
"request_digest": "5752326f01bedcee6832987ebf723cbf14334fee3bb82dda1238e5be5b4b5be1",
"request_id": "real-mcp-research-02",
"retry_disabled": true,
"review_state": "pending",
"role_id": "research",
"run_status": "succeeded",
"source_sha": "52462b45e985f596b2a503fc70ac12c3bd5fe571",
"started_at": 1790221148.1277544,
"stderr_preview": "",
"stream_error": null,
"supervisor_boot_id": "a37fa29f-4cbd-4c8a-89d2-d5d31b20615b",
"supervisor_pgid": 2740655,
"supervisor_pid": 2740655,
"supervisor_start": "109254769",
"task_id": "subagent-workflow-standard-2026-09-24-executor",
"tool_calls": [
{
"isError": false,
"toolName": "read",
"type": "tool_execution_start"
},
{
"isError": false,
"toolName": "ls",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "ls",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_end"
}
],
"verdict": "not_applicable"
}
@@ -0,0 +1,28 @@
# PI subagent report — `real-mcp-research-02`
- task_id: `subagent-workflow-standard-2026-09-24-executor`
- role_id: `research`
- run_status: `succeeded`
- verdict: `not_applicable`
- config_revision: `subagent-models-2026-09-24-r1`
- config_sha256: `0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0`
- source_sha: `52462b45e985f596b2a503fc70ac12c3bd5fe571`
- configured: `qwen-token-plan-cn/qwen3.8-flash`
- observed: `qwen-token-plan-cn/qwen3.8-flash`
## Goal
Call read on PROJECT.md; report two repository directory names and finish PI_EXECUTOR_REAL_OK.
## Model output
已读取快照文件 `PROJECT.md`(仅读取该文件,未访问 `.env`/`.git`/凭证或快照外路径)。
**两个仓库目录名(来自「仓库地图」):**
1. `cmd/` — 服务入口(Go)
2. `web/` — React/Vite 前端
PI_EXECUTOR_REAL_OK
_Note: model narrative is not an approved review verdict; run_status and verdict are separate._
@@ -0,0 +1,102 @@
{
"approved_commands_sha256": "73d15c48485e02f893dd6cac36b04f8cb3dfd63d166849f2369dbd5c5c4748f6",
"authorization_ref": "user:接入自动调用pi的执行器; isolated engineering acceptance",
"config_revision": "subagent-models-2026-09-24-r1",
"config_sha256": "0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0",
"configured_alias": "glm_flash",
"configured_model": "glm-5.3-flash",
"configured_provider": "zai-coding-cn",
"errors": [],
"final_text": "## Analysis of approved command output\n\n**Was `PI_ISOLATED_TEST_PASS` printed? — Yes.**\n\nPer the authoritative evidence packet (single approved command, `status: \"ok\"`, `exit_code: 0`):\n\n- `stdout_preview` is exactly `PI_ISOLATED_TEST_PASS\\n` — the marker was printed.\n- `exit_code: 0` is consistent with the command's logic: all three asserts passed (`PROJECT.md` is a file; `AGENTCTL_RUN_ID` and `PI_CODING_AGENT_DIR` are absent from the environment), so the final `print()` executed.\n- `stderr_sha256` (`e3b0c442...b855`) is the well-known SHA-256 of the empty string, matching the empty `stderr_preview` — no error output.\n- `duration_seconds: 0.086` is plausible for a one-shot `python3 -c` run.\n\n### Verification notes (from this snapshot, read-only)\n\n- `PROJECT.md` in this snapshot (`/tmp/pi-subagent-snap-a1uvxn50`) matches the packet's content, consistent with the assert that it exists as a file.\n- The `command-evidence/00-stdout.txt` / `00-stderr.txt` artifacts referenced by `stdout_path`/`stderr_path` are **not present** in this snapshot, and the command's `cwd` was `/tmp/pi-subagent-test-snap-t52sfrdr` (a different snapshot directory than this one). So corroboration rests on the packet's preview and recorded hashes, which are declared authoritative; I cannot independently re-hash them here.\n\n### Scope caveat\n\nThis marker evidences only that the **isolation/environment precondition check** passed (snapshot file present; the two orchestrator env vars absent). It is **not** evidence that any product tests (Go, frontend, or real PI Agent Runner workflow acceptance) were run or passed.",
"finished_at": 1790221203.5930333,
"goal": "Analyze only the approved command output. State whether PI_ISOLATED_TEST_PASS was printed. Do not infer product tests passed.",
"input_files": [
{
"bytes": 4183,
"path": "PROJECT.md",
"sha256": "91d148876a9457e646b8adab69670a1a85ac7d067268568ff8db084c0c0801d8"
}
],
"observed_model": "glm-5.3-flash",
"observed_provider": "zai-coding-cn",
"ok": true,
"pi_exit_code": 0,
"plan_digest": "63e5a1fa27f2cf9f8b905531a3ab17f852d1f4000361d65b14a472ad24dc920b",
"request_digest": "37c2925cf846df8ad240371be4c994b8be4c68cbf442620d8ac59ab33d51cbb9",
"request_id": "real-mcp-test-01",
"retry_disabled": true,
"review_state": "commands_passed",
"role_id": "test_run",
"run_status": "succeeded",
"source_sha": "52462b45e985f596b2a503fc70ac12c3bd5fe571",
"started_at": 1790221155.0332568,
"stderr_preview": "",
"stream_error": null,
"supervisor_boot_id": "a37fa29f-4cbd-4c8a-89d2-d5d31b20615b",
"supervisor_pgid": 2742005,
"supervisor_pid": 2742005,
"supervisor_start": "109255459",
"task_id": "subagent-workflow-standard-2026-09-24-executor",
"test_outcomes": [
{
"argv": [
"python3",
"-c",
"import os,pathlib; assert pathlib.Path('PROJECT.md').is_file(); assert 'AGENTCTL_RUN_ID' not in os.environ; assert 'PI_CODING_AGENT_DIR' not in os.environ; print('PI_ISOLATED_TEST_PASS')"
],
"cwd": "/tmp/pi-subagent-test-snap-t52sfrdr",
"duration_seconds": 0.086,
"exit_code": 0,
"status": "ok",
"stderr_path": "command-evidence/00-stderr.txt",
"stderr_preview": "",
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"stdout_path": "command-evidence/00-stdout.txt",
"stdout_preview": "PI_ISOLATED_TEST_PASS\n",
"stdout_sha256": "960dc8f2b47ab4a6214cc9358d79fcd36909f181034664405bc5440d7420b7ec"
}
],
"tool_calls": [
{
"isError": false,
"toolName": "ls",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "ls",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "read",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_start"
},
{
"isError": true,
"toolName": "read",
"type": "tool_execution_end"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_start"
},
{
"isError": false,
"toolName": "read",
"type": "tool_execution_end"
}
],
"verdict": "pass"
}
@@ -0,0 +1,44 @@
# PI subagent report — `real-mcp-test-01`
- task_id: `subagent-workflow-standard-2026-09-24-executor`
- role_id: `test_run`
- run_status: `succeeded`
- verdict: `pass`
- config_revision: `subagent-models-2026-09-24-r1`
- config_sha256: `0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0`
- source_sha: `52462b45e985f596b2a503fc70ac12c3bd5fe571`
- configured: `zai-coding-cn/glm-5.3-flash`
- observed: `zai-coding-cn/glm-5.3-flash`
## Goal
Analyze only the approved command output. State whether PI_ISOLATED_TEST_PASS was printed. Do not infer product tests passed.
## Model output
## Analysis of approved command output
**Was `PI_ISOLATED_TEST_PASS` printed? — Yes.**
Per the authoritative evidence packet (single approved command, `status: "ok"`, `exit_code: 0`):
- `stdout_preview` is exactly `PI_ISOLATED_TEST_PASS\n` — the marker was printed.
- `exit_code: 0` is consistent with the command's logic: all three asserts passed (`PROJECT.md` is a file; `AGENTCTL_RUN_ID` and `PI_CODING_AGENT_DIR` are absent from the environment), so the final `print()` executed.
- `stderr_sha256` (`e3b0c442...b855`) is the well-known SHA-256 of the empty string, matching the empty `stderr_preview` — no error output.
- `duration_seconds: 0.086` is plausible for a one-shot `python3 -c` run.
### Verification notes (from this snapshot, read-only)
- `PROJECT.md` in this snapshot (`/tmp/pi-subagent-snap-a1uvxn50`) matches the packet's content, consistent with the assert that it exists as a file.
- The `command-evidence/00-stdout.txt` / `00-stderr.txt` artifacts referenced by `stdout_path`/`stderr_path` are **not present** in this snapshot, and the command's `cwd` was `/tmp/pi-subagent-test-snap-t52sfrdr` (a different snapshot directory than this one). So corroboration rests on the packet's preview and recorded hashes, which are declared authoritative; I cannot independently re-hash them here.
### Scope caveat
This marker evidences only that the **isolation/environment precondition check** passed (snapshot file present; the two orchestrator env vars absent). It is **not** evidence that any product tests (Go, frontend, or real PI Agent Runner workflow acceptance) were run or passed.
## Approved test command outcomes
- argv=`['python3', '-c', "import os,pathlib; assert pathlib.Path('PROJECT.md').is_file(); assert 'AGENTCTL_RUN_ID' not in os.environ; assert 'PI_CODING_AGENT_DIR' not in os.environ; print('PI_ISOLATED_TEST_PASS')"]` exit=0 status=ok
_Note: model narrative is not an approved review verdict; run_status and verdict are separate._
@@ -0,0 +1,39 @@
{
"approved_commands_sha256": null,
"authorization_ref": "user:接入自动调用pi的执行器; bounded engineering smoke",
"config_revision": "subagent-models-2026-09-24-r1",
"config_sha256": "0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0",
"configured_alias": "qwen_flash",
"configured_model": "qwen3.8-flash",
"configured_provider": "qwen-token-plan-cn",
"errors": [],
"final_text": "Read `PROJECT.md` (read-only; no files written, no tests run or inferred).\n\nTwo repository directory names from the 仓库地图 in PROJECT.md:\n\n1. `cmd/` — 服务入口 (Go modules, together with `internal/`)\n2. `web/` — React/Vite 前端\n\nPI_EXECUTOR_REAL_OK",
"finished_at": 1790219576.5912876,
"goal": "Use the read tool on PROJECT.md, then provide two repository directory names and finish with PI_EXECUTOR_REAL_OK. Do not write files or infer test results.",
"input_files": [
{
"bytes": 4183,
"path": "PROJECT.md",
"sha256": "91d148876a9457e646b8adab69670a1a85ac7d067268568ff8db084c0c0801d8"
}
],
"observed_model": "qwen3.8-flash",
"observed_provider": "qwen-token-plan-cn",
"ok": true,
"pi_exit_code": 0,
"plan_digest": null,
"request_digest": "12939b982d796ab0c1e5c22b536474c505ddf3a4e925ce37fa551afeb86275ba",
"request_id": "real-research-01",
"retry_disabled": true,
"review_state": "pending",
"role_id": "research",
"run_status": "succeeded",
"source_sha": "52462b45e985f596b2a503fc70ac12c3bd5fe571",
"started_at": 1790219569.6384125,
"stderr_preview": "",
"stream_error": null,
"supervisor_boot_id": "a37fa29f-4cbd-4c8a-89d2-d5d31b20615b",
"supervisor_pid": 2588429,
"task_id": "subagent-workflow-standard-2026-09-24-executor",
"verdict": "not_applicable"
}
@@ -0,0 +1,29 @@
# PI subagent report — `real-research-01`
- task_id: `subagent-workflow-standard-2026-09-24-executor`
- role_id: `research`
- run_status: `succeeded`
- verdict: `not_applicable`
- config_revision: `subagent-models-2026-09-24-r1`
- config_sha256: `0cd96bbdb6b4204dac32058989a3adaf176c5775a3d323353103cc23e2414ca0`
- source_sha: `52462b45e985f596b2a503fc70ac12c3bd5fe571`
- configured: `qwen-token-plan-cn/qwen3.8-flash`
- observed: `qwen-token-plan-cn/qwen3.8-flash`
## Goal
Use the read tool on PROJECT.md, then provide two repository directory names and finish with PI_EXECUTOR_REAL_OK. Do not write files or infer test results.
## Model output
Read `PROJECT.md` (read-only; no files written, no tests run or inferred).
Two repository directory names from the 仓库地图 in PROJECT.md:
1. `cmd/` — 服务入口 (Go modules, together with `internal/`)
2. `web/` — React/Vite 前端
PI_EXECUTOR_REAL_OK
_Note: model narrative is not an approved review verdict; run_status and verdict are separate._
+40
View File
@@ -6,13 +6,53 @@ Zero-dependency Node MCP wrappers used by Cursor (see [`.cursor/mcp.json`](../..
|---|---|---|
| `agentctl` | `agentctl-mcp.mjs` | Lease / docs apply / validate / queue |
| `jev` | `jev-mcp.mjs` | Typed decide/health against local or remote jev-service |
| `pi-subagents` | `pi-subagents-mcp.mjs` | Local PI subagent roles/run/result (engineering tool; not product Runner) |
```bash
# from repo root
node scripts/mcp/agentctl-mcp.mjs
node scripts/mcp/jev-mcp.mjs
node scripts/mcp/pi-subagents-mcp.mjs
```
## pi-subagents
Markdown model config is the sole editable source: `.doc/ops/2026-09-24-subagent-model-config.md`.
| Tool | Behavior |
|---|---|
| `pi_subagent_roles` | Read-only catalog; never invokes PI; executor binary from **this server's INSTALL_ROOT** |
| `pi_subagent_run` | Non-blocking; request JSON in OS tmp; wraps **discovered main worktree** `scripts/agentctl.py with` (governance SSOT lease entry) + **INSTALL_ROOT** `scripts/pi_subagents.py run` |
| `pi_subagent_result` | Read-only receipt; `running` is not `isError`; terminal failure/`UNKNOWN_REQUEST` after a settled supervisor rejection surfaces the child's typed error (ROLE_DISABLED, LEASE_REQUIRED, …) |
**Install vs main:** When the MCP server lives in a linked task worktree, INSTALL_ROOT may differ from the repo main tip. Cross-repo worktrees (different `git-common-dir`) are refused. Optional `config` is worktree-relative registered model config only. `request.config_path` is rejected (use tool `config`).
CLI:
```bash
python3 -B scripts/pi_subagents.py roles --config .doc/ops/2026-09-24-subagent-model-config.md
python3 -B scripts/agentctl.py with --task <id> \
--resource path:.doc/test-reports/assets/pi-subagents/<id>/<request_id> -- \
python3 -B scripts/pi_subagents.py run --request /path/to/req.json \
--config .doc/ops/2026-09-24-subagent-model-config.md
python3 -B scripts/pi_subagents.py result --task <id> --request-id <request_id>
```
The executor resolves `agentctl` helpers from the git main worktree scripts directory (not a stale colocated copy beside itself when running from a feature worktree).
Scoped reads: `scripts/pi-subagent-readonly.ts` — builtin `path` only; `@`/`~` denied; directory walks must contain only allowlisted regular files. Approved tests use a separate snapshot; model evidence stays pristine.
### Cursor CLI compatibility (pi-subagents)
Real Cursor CLI `2026.09.18` (and `2026.09.23` verified here) does **not** expand `${workspaceFolder}` in MCP `args`. The `pi-subagents` entry therefore uses workspace-relative `args: ["scripts/mcp/pi-subagents-mcp.mjs"]` with `type: "stdio"` / `command: "node"`. Absolute `${workspaceFolder}/…` paths fail with `Cannot find module '…/${workspaceFolder}/scripts/mcp/pi-subagents-mcp.mjs'`. After `agent mcp enable pi-subagents`, `agent mcp list-tools pi-subagents` must report exactly three tools: `pi_subagent_roles`, `pi_subagent_run`, `pi_subagent_result`. Older `agentctl` / `jev` entries may still use `${workspaceFolder}` until separately corrected.
### Limitations (v1)
- No shared environment ops (`SHARED_ENV_UNSUPPORTED`).
- Process-group governance, not an OS sandbox.
- Unit suite uses fake PI; Cursor UI MCP connectivity requires reload after checkout and host smoke.
- Host owns live paid-model checks.
## jev credentials (not in git)
- `~/.secrets/jev/api.key` (chmod 600) or env `JEV_API_KEY`
+586
View File
@@ -0,0 +1,586 @@
#!/usr/bin/env node
/**
* pi-subagents MCP (stdio, zero deps).
* Tools: pi_subagent_roles, pi_subagent_run, pi_subagent_result.
*
* - Trusted executor: INSTALL_ROOT/scripts/pi_subagents.py (this server install).
* - Authoritative agentctl: discovered main worktree scripts/agentctl.py (governance SSOT).
* - worktree must share git-common-dir with INSTALL_ROOT.
* - Request JSON only under private OS tmp (no pre-lease repo writes).
*/
import { spawn, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import crypto from 'node:crypto';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const INSTALL_ROOT = path.resolve(__dirname, '../..');
const MAX_PAYLOAD = 2 * 1024 * 1024;
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const active = new Map(); // key -> { child, digest, stdout, stderr, exitCode, rejection }
function gitCommonDir(cwd) {
const r = spawnSync('git', ['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir'], {
encoding: 'utf8',
});
if (r.status !== 0) return null;
return r.stdout.trim();
}
function gitMainWorktree(cwd) {
const list = spawnSync('git', ['-C', cwd, 'worktree', 'list', '--porcelain'], { encoding: 'utf8' });
if (list.status !== 0) return null;
const first = /^worktree (.+)$/m.exec(list.stdout || '');
return first ? first[1] : null;
}
function validateId(value, label) {
if (typeof value !== 'string' || !ID_RE.test(value) || value === '.' || value === '..') {
throw new Error(`invalid ${label}`);
}
}
function resolveRepo(worktree) {
if (!worktree || typeof worktree !== 'string') {
throw new Error('worktree is required');
}
const wt = path.resolve(worktree);
const common = gitCommonDir(wt);
if (!common) throw new Error(`not a git worktree: ${wt}`);
const installCommon = gitCommonDir(INSTALL_ROOT);
if (!installCommon) throw new Error('INSTALL_ROOT is not a git worktree');
if (common !== installCommon) {
throw new Error('worktree does not share INSTALL_ROOT git-common-dir (cross-repo refused)');
}
// Authoritative main = first worktree list entry; executor stays at INSTALL_ROOT.
const discovered = gitMainWorktree(INSTALL_ROOT) || INSTALL_ROOT;
return { main: discovered, discoveredMain: discovered, worktree: wt, common };
}
function agentctlPath(main) {
return path.join(main, 'scripts', 'agentctl.py');
}
function executorPath() {
return path.join(INSTALL_ROOT, 'scripts', 'pi_subagents.py');
}
function safeConfigPath(worktree, config) {
const defaultRel = '.doc/ops/2026-09-24-subagent-model-config.md';
const rel = config || defaultRel;
if (typeof rel !== 'string' || !rel.trim()) throw new Error('invalid config path');
if (path.isAbsolute(rel)) throw new Error('config must be worktree-relative');
if (rel.includes('\0') || rel.split(path.sep).includes('..')) {
throw new Error('config path traversal refused');
}
const abs = path.resolve(worktree, rel);
const normWt = worktree.endsWith(path.sep) ? worktree : worktree + path.sep;
if (!(abs === worktree || abs.startsWith(normWt))) {
throw new Error('config escapes worktree');
}
// refuse symlink config
try {
if (fs.lstatSync(abs).isSymbolicLink()) throw new Error('config symlink refused');
} catch (e) {
if (e.message === 'config symlink refused') throw e;
// missing checked by executor
}
// only the registered config file name
if (path.basename(abs) !== '2026-09-24-subagent-model-config.md') {
throw new Error('config must be the registered model config file');
}
return abs;
}
function payloadDigest(request) {
return crypto.createHash('sha256').update(JSON.stringify(request)).digest('hex');
}
function runSync(argv, { cwd, timeout = 120000, env } = {}) {
const r = spawnSync(argv[0], argv.slice(1), {
cwd,
encoding: 'utf8',
timeout,
maxBuffer: 8 * 1024 * 1024,
env: env || process.env,
});
return {
ok: r.status === 0,
code: r.status ?? 1,
stdout: r.stdout || '',
stderr: r.stderr || '',
error: r.error ? String(r.error) : undefined,
};
}
function textResult(payload) {
const body = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
return { content: [{ type: 'text', text: body }] };
}
function errResult(msg) {
const body = typeof msg === 'string' ? msg : JSON.stringify(msg);
return { content: [{ type: 'text', text: body }], isError: true };
}
function parseJsonLine(text) {
const lines = String(text || '').split(/\n/).map((l) => l.trim()).filter(Boolean);
for (let i = lines.length - 1; i >= 0; i--) {
try {
return JSON.parse(lines[i]);
} catch {
/* continue */
}
}
return null;
}
function sanitizeErr(text) {
return String(text || '')
.replace(/(api[_-]?key|token|secret|password|authorization)\s*[:=]\s*\S+/gi, '$1=[REDACTED]')
.slice(0, 2000);
}
function killActiveTree(meta) {
if (!meta?.child) return;
const child = meta.child;
try {
if (child.pid) {
try { process.kill(-child.pid, 'SIGTERM'); } catch { /* not a group */ }
try { child.kill('SIGTERM'); } catch { /* */ }
}
} catch { /* */ }
setTimeout(() => {
try {
if (child.exitCode === null && child.pid) {
try { process.kill(-child.pid, 'SIGKILL'); } catch { /* */ }
try { child.kill('SIGKILL'); } catch { /* */ }
}
} catch { /* */ }
}, 2000);
}
function reapAll() {
for (const meta of active.values()) {
killActiveTree(meta);
}
}
const TOOLS = [
{
name: 'pi_subagent_roles',
description: 'Read-only: list PI subagent roles/models from markdown config (does not invoke PI)',
inputSchema: {
type: 'object',
properties: {
worktree: { type: 'string', description: 'Absolute path to registered task worktree' },
task_id: { type: 'string' },
config: { type: 'string', description: 'Optional config path relative to worktree' },
},
required: ['worktree', 'task_id'],
additionalProperties: false,
},
},
{
name: 'pi_subagent_run',
description: 'Start one PI subagent run under main-worktree agentctl lease (non-blocking)',
inputSchema: {
type: 'object',
properties: {
worktree: { type: 'string' },
task_id: { type: 'string' },
request: {
type: 'object',
description: 'PI subagent request (task_id optional; injected from outer task_id)',
properties: {
task_id: { type: 'string', description: 'Optional; must match outer task_id when set' },
role_id: { type: 'string' },
request_id: { type: 'string' },
goal: { type: 'string' },
authorization_ref: { type: 'string' },
source_sha: { type: 'string', description: 'Full commit hash' },
input_files: { type: 'array', items: { type: 'string' } },
timeout_seconds: { type: 'integer' },
approved_commands: {
type: 'array',
items: { type: 'array', items: { type: 'string' } },
description: 'test_run/acceptance_check only',
},
plan_digest: { type: 'string', description: 'sha256 hex for test roles' },
model_override: { type: 'object' },
},
required: [
'role_id', 'request_id', 'goal', 'authorization_ref',
'source_sha', 'input_files', 'timeout_seconds',
],
additionalProperties: false,
},
config: { type: 'string', description: 'Worktree-relative registered model config path' },
},
required: ['worktree', 'task_id', 'request'],
additionalProperties: false,
},
},
{
name: 'pi_subagent_result',
description: 'Read durable receipt for a request_id (does not invoke PI). running is not isError; terminal failure is.',
inputSchema: {
type: 'object',
properties: {
worktree: { type: 'string' },
task_id: { type: 'string' },
request_id: { type: 'string' },
},
required: ['worktree', 'task_id', 'request_id'],
additionalProperties: false,
},
},
];
function callTool(name, args = {}) {
try {
if (name === 'pi_subagent_roles') {
validateId(args.task_id, 'task_id');
const { worktree } = resolveRepo(args.worktree);
const config = safeConfigPath(worktree, args.config);
const r = runSync(['python3', '-B', executorPath(), 'roles', '--config', config], {
cwd: worktree,
});
const payload = parseJsonLine(r.stdout) || parseJsonLine(r.stderr);
if (!r.ok) return errResult(payload || sanitizeErr(r.stderr || r.stdout || r.error));
return textResult(payload);
}
if (name === 'pi_subagent_result') {
validateId(args.task_id, 'task_id');
validateId(args.request_id, 'request_id');
const { worktree } = resolveRepo(args.worktree);
const key = `${args.task_id}/${args.request_id}`;
const meta = active.get(key);
const r = runSync([
'python3', '-B', executorPath(), 'result',
'--task', args.task_id, '--request-id', args.request_id,
], { cwd: worktree });
const payload = parseJsonLine(r.stdout) || parseJsonLine(r.stderr);
const surfaceRejection = () => {
if (meta?.rejection) return errResult(meta.rejection);
if (meta && meta.exitCode !== null && meta.exitCode !== 0) {
const errPayload = parseJsonLine(meta.stderr()) || parseJsonLine(meta.stdout()) || {
ok: false,
code: 'SUPERVISOR_FAILED',
message: sanitizeErr(meta.stderr() || meta.stdout() || 'supervisor failed'),
exit_code: meta.exitCode,
};
return errResult(errPayload);
}
return null;
};
if (!payload) {
const rej = surfaceRejection();
if (rej) return rej;
if (meta && meta.exitCode === null) {
return textResult({
ok: true,
run_status: 'running',
task_id: args.task_id,
request_id: args.request_id,
});
}
return errResult({
ok: false,
code: 'NOT_FOUND',
message: sanitizeErr(r.stderr || r.stdout || r.error || 'no receipt'),
});
}
// Settled supervisor rejection with no durable receipt (UNKNOWN_REQUEST).
if (payload.code === 'UNKNOWN_REQUEST') {
const rej = surfaceRejection();
if (rej) return rej;
// Child still in flight: do not treat as terminal unknown.
if (meta && meta.exitCode === null) {
return textResult({
ok: true,
run_status: 'running',
task_id: args.task_id,
request_id: args.request_id,
});
}
return errResult(payload);
}
if (!r.ok && payload.code && payload.code !== 'UNKNOWN_REQUEST') {
return errResult(payload);
}
// In-flight: never isError
if (payload.run_status === 'running') {
return textResult(payload);
}
if (payload.ok === false || (payload.run_status && payload.run_status !== 'succeeded')) {
return errResult(payload);
}
return textResult(payload);
}
if (name === 'pi_subagent_run') {
validateId(args.task_id, 'task_id');
const { main, worktree } = resolveRepo(args.worktree);
const request = args.request;
if (!request || typeof request !== 'object' || Array.isArray(request)) {
return errResult({ ok: false, code: 'INVALID_REQUEST', message: 'request object required' });
}
if (request.task_id && request.task_id !== args.task_id) {
return errResult({ ok: false, code: 'TASK_MISMATCH', message: 'request.task_id must match task_id' });
}
if (Object.prototype.hasOwnProperty.call(request, 'config_path')) {
return errResult({
ok: false,
code: 'INVALID_REQUEST',
message: 'request.config_path is not supported; use tool config argument',
});
}
request.task_id = args.task_id;
const requestId = request.request_id;
try {
validateId(requestId, 'request_id');
} catch (e) {
return errResult({ ok: false, code: 'INVALID_REQUEST', message: String(e.message || e) });
}
const digest = payloadDigest(request);
const key = `${args.task_id}/${requestId}`;
const existing = active.get(key);
if (existing && existing.exitCode === null) {
if (existing.digest === digest) {
return textResult({
ok: true, accepted: true, running: true,
task_id: args.task_id, request_id: requestId, request_digest: digest,
});
}
return errResult({
ok: false,
code: 'REQUEST_DIGEST_MISMATCH',
message: 'active request_id with different payload',
});
}
let config;
try {
config = safeConfigPath(worktree, args.config);
} catch (e) {
return errResult({ ok: false, code: 'CONFIG_INVALID', message: String(e.message || e) });
}
const resource = `path:.doc/test-reports/assets/pi-subagents/${args.task_id}/${requestId}`;
// Private OS tmp only — never write into worktree before lease.
let tmpDir;
let reqPath;
try {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-subagent-mcp-'));
reqPath = path.join(tmpDir, `${requestId}.json`);
fs.writeFileSync(reqPath, JSON.stringify(request), { mode: 0o600 });
} catch (e) {
return errResult({
ok: false,
code: 'TMP_WRITE_FAILED',
message: sanitizeErr(String(e.message || e)),
});
}
let child;
try {
child = spawn(
'python3',
[
'-B', agentctlPath(main),
'with', '--task', args.task_id, '--resource', resource, '--',
'python3', '-B', executorPath(),
'run', '--request', reqPath, '--config', config,
],
{
cwd: worktree,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: true, // own process group for cleanup
},
);
} catch (e) {
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* */ }
return errResult({
ok: false,
code: 'SPAWN_FAILED',
message: sanitizeErr(String(e.message || e)),
});
}
let stdout = '';
let stderr = '';
child.stdout.on('data', (c) => {
stdout += c.toString('utf8');
if (stdout.length > MAX_PAYLOAD) stdout = stdout.slice(-MAX_PAYLOAD);
});
child.stderr.on('data', (c) => {
stderr += c.toString('utf8');
if (stderr.length > MAX_PAYLOAD) stderr = stderr.slice(-MAX_PAYLOAD);
});
const meta = {
child,
digest,
stdout: () => stdout,
stderr: () => stderr,
exitCode: null,
rejection: null,
tmpDir,
reqPath,
};
child.on('error', (err) => {
meta.exitCode = 1;
meta.rejection = {
ok: false,
code: 'SPAWN_FAILED',
message: sanitizeErr(String(err.message || err)),
};
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* */ }
});
child.on('exit', (code) => {
meta.exitCode = code;
if (code && code !== 0) {
const parsed = parseJsonLine(stderr) || parseJsonLine(stdout);
if (parsed && parsed.code) {
meta.rejection = {
ok: false,
code: parsed.code,
message: sanitizeErr(parsed.message || parsed.code),
};
} else {
meta.rejection = {
ok: false,
code: 'SUPERVISOR_FAILED',
message: sanitizeErr(stderr || stdout || `exit ${code}`),
exit_code: code,
};
}
}
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* */ }
});
active.set(key, meta);
return textResult({
ok: true,
accepted: true,
running: true,
task_id: args.task_id,
request_id: requestId,
request_digest: digest,
resource,
});
}
return errResult(`unknown tool: ${name}`);
} catch (e) {
return errResult({ ok: false, code: 'MCP_ERROR', message: sanitizeErr(String(e?.message || e)) });
}
}
function send(msg) {
const body = JSON.stringify(msg);
if (Buffer.byteLength(body, 'utf8') > MAX_PAYLOAD) {
const id = msg?.id;
process.stdout.write(JSON.stringify({
jsonrpc: '2.0', id, error: { code: -32600, message: 'response too large' },
}) + '\n');
return;
}
process.stdout.write(body + '\n');
}
function handle(msg) {
if (!msg || typeof msg !== 'object') return;
if (msg.method && msg.id === undefined) {
return;
}
const { id, method, params } = msg;
try {
if (method === 'initialize') {
send({
jsonrpc: '2.0',
id,
result: {
protocolVersion: params?.protocolVersion || '2024-11-05',
capabilities: { tools: {} },
serverInfo: { name: 'pi-subagents', version: '1.0.0' },
},
});
return;
}
if (method === 'tools/list') {
send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
return;
}
if (method === 'tools/call') {
const name = params?.name;
const args = params?.arguments || {};
const raw = JSON.stringify(args);
if (Buffer.byteLength(raw, 'utf8') > MAX_PAYLOAD) {
send({ jsonrpc: '2.0', id, result: errResult('arguments exceed payload bound') });
return;
}
const result = callTool(name, args);
send({ jsonrpc: '2.0', id, result });
return;
}
if (method === 'ping') {
send({ jsonrpc: '2.0', id, result: {} });
return;
}
send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } });
} catch (e) {
send({ jsonrpc: '2.0', id, error: { code: -32000, message: String(e?.message || e) } });
}
}
let buf = Buffer.alloc(0);
process.stdin.on('data', (chunk) => {
if (buf.length + chunk.length > MAX_PAYLOAD * 4) {
buf = Buffer.alloc(0);
process.stderr.write('payload overflow; reset buffer\n');
return;
}
buf = Buffer.concat([buf, chunk]);
while (true) {
const headerEnd = buf.indexOf('\r\n\r\n');
if (headerEnd !== -1) {
const header = buf.slice(0, headerEnd).toString('utf8');
const m = /Content-Length:\s*(\d+)/i.exec(header);
if (!m) {
buf = buf.slice(headerEnd + 4);
continue;
}
const len = Number(m[1]);
if (len > MAX_PAYLOAD) {
buf = buf.slice(headerEnd + 4);
continue;
}
const start = headerEnd + 4;
if (buf.length < start + len) return;
const body = buf.slice(start, start + len).toString('utf8');
buf = buf.slice(start + len);
try { handle(JSON.parse(body)); } catch (e) {
process.stderr.write(`parse error: ${e}\n`);
}
continue;
}
const nl = buf.indexOf('\n');
if (nl === -1) return;
const line = buf.slice(0, nl).toString('utf8').trim();
buf = buf.slice(nl + 1);
if (!line || line.startsWith('Content-Length')) continue;
try { handle(JSON.parse(line)); } catch (e) {
process.stderr.write(`ndjson parse error: ${e}\n`);
}
}
});
function shutdown(signal) {
process.stderr.write(`pi-subagents-mcp shutting down (${signal})\n`);
reapAll();
setTimeout(() => process.exit(0), 500).unref?.();
}
process.stdin.on('end', () => { reapAll(); process.exit(0); });
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
+264
View File
@@ -0,0 +1,264 @@
/**
* PI subagent readonly scope guard.
*
* Deny-by-default: only read/grep/find/ls. Builtin schema uses a single `path`
* field (read requires it; grep/find/ls default to "."). Reject unknown
* path-carrier fields. Enforce snapshot realpath + selected-file allowlist;
* directory targets must contain only allowlisted regular files (bounded walk).
*/
import fs from "node:fs";
import path from "node:path";
const ALLOWED_TOOLS = new Set(["read", "grep", "find", "ls"]);
const UNKNOWN_PATH_KEYS = new Set([
"file", "filename", "directory", "dir", "root", "cwd", "paths",
]);
const MAX_WALK_ENTRIES = 256;
function snapshotRoot(): string | null {
const raw = process.env.PI_SUBAGENT_SNAPSHOT_ROOT;
if (!raw) return null;
try {
return fs.realpathSync(raw);
} catch {
return null;
}
}
function allowedFiles(): Set<string> | null {
const raw = process.env.PI_SUBAGENT_ALLOWED_FILES;
if (!raw) return null;
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return new Set();
return new Set(parsed.map((p: unknown) => String(p)));
} catch {
return new Set();
}
}
/** Reject PI resolveToCwd / home-expansion inputs before any join. */
function forbiddenPrefix(raw: string): string | null {
const cleaned = raw
.replace(/^\uFEFF/, "")
.replace(/[\u200B-\u200D\u2060\uFEFF]/g, "");
const trimmed = cleaned.trim();
if (!trimmed) return "empty path";
if (trimmed.startsWith("@") || trimmed.startsWith("~")) {
return "@ or ~ prefix refused";
}
if (/^[\uFF20\u0040]/.test(trimmed) || /^[\uFF5E\u007E]/.test(trimmed)) {
return "unicode @/~ lookalike refused";
}
if (trimmed.includes("\0") || trimmed.includes("\n") || trimmed.includes("\r")) {
return "illegal characters";
}
return null;
}
function contained(root: string, candidate: string): { ok: boolean; resolved?: string; reason?: string } {
const prefixErr = forbiddenPrefix(candidate);
if (prefixErr) return { ok: false, reason: prefixErr };
const abs = path.isAbsolute(candidate)
? path.normalize(candidate)
: path.resolve(root, candidate);
let cur = abs;
for (;;) {
try {
if (fs.lstatSync(cur).isSymbolicLink()) {
return { ok: false, reason: "symlink segment refused" };
}
} catch {
/* missing leaf checked below */
}
const parent = path.dirname(cur);
if (parent === cur) break;
cur = parent;
}
let resolved: string;
try {
if (!fs.existsSync(abs)) {
return { ok: false, reason: "path does not exist" };
}
const st = fs.lstatSync(abs);
if (st.isSymbolicLink()) {
return { ok: false, reason: "symlink target refused" };
}
resolved = fs.realpathSync(abs);
} catch {
return { ok: false, reason: "realpath failed" };
}
try {
if (fs.realpathSync(resolved) !== resolved) {
return { ok: false, reason: "realpath unstable" };
}
} catch {
return { ok: false, reason: "realpath recheck failed" };
}
const normRoot = root.endsWith(path.sep) ? root : root + path.sep;
if (!(resolved === root || resolved.startsWith(normRoot))) {
return { ok: false, reason: "escapes snapshot root" };
}
return { ok: true, resolved };
}
function relUnder(root: string, resolved: string): string | null {
const rel = path.relative(root, resolved);
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return null;
return rel;
}
function fileAllowlisted(root: string, resolved: string, allow: Set<string> | null): boolean {
if (!allow) return true;
const rel = relUnder(root, resolved);
return !!rel && allow.has(rel);
}
/** Directory listing/search: every regular file under path must be allowlisted. */
function directoryAllowOk(root: string, resolved: string, allow: Set<string> | null): { ok: boolean; reason?: string } {
if (!allow) return { ok: true };
let count = 0;
const stack = [resolved];
while (stack.length) {
const dir = stack.pop()!;
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return { ok: false, reason: "directory unreadable" };
}
for (const ent of entries) {
count += 1;
if (count > MAX_WALK_ENTRIES) {
return { ok: false, reason: "directory walk exceeded bound" };
}
const child = path.join(dir, ent.name);
let st: fs.Stats;
try {
st = fs.lstatSync(child);
} catch {
return { ok: false, reason: "lstat failed" };
}
if (st.isSymbolicLink()) {
return { ok: false, reason: "symlink descendant refused" };
}
if (st.isDirectory()) {
stack.push(child);
continue;
}
if (!st.isFile()) {
return { ok: false, reason: "special file refused" };
}
if (!fileAllowlisted(root, fs.realpathSync(child), allow)) {
return { ok: false, reason: "directory contains non-allowlisted file" };
}
}
}
// Exact snapshot root: path.relative(root,root) is "" so relUnder is null.
// Inventory above already enforced allowlist; permit root listing/search
// (including empty allowlisted snapshot). fileAllowlisted(root,root) stays false.
if (resolved === root) {
return { ok: true };
}
// Empty dir is ok only if it is an ancestor of some allowlisted file
const rel = relUnder(root, resolved);
if (rel === null) return { ok: false, reason: "directory outside root" };
if (count === 0) {
for (const f of allow) {
if (f === rel || f.startsWith(rel + path.sep)) return { ok: true };
}
return { ok: false, reason: "empty directory not covering allowlist" };
}
return { ok: true };
}
export default function piSubagentReadonly(pi: {
on: (event: string, handler: (event: any, ctx: any) => any) => void;
}) {
pi.on("tool_call", async (event: { toolName: string; input?: Record<string, unknown> }) => {
const name = event.toolName;
if (!ALLOWED_TOOLS.has(name)) {
return {
block: true,
reason: `Tool "${name}" is denied by pi-subagent-readonly (allowlist: read,grep,find,ls)`,
};
}
const root = snapshotRoot();
if (!root) {
return {
block: true,
reason: "PI_SUBAGENT_SNAPSHOT_ROOT is unset; refusing all file tools",
};
}
const allow = allowedFiles();
const input = event.input || {};
for (const key of Object.keys(input)) {
if (UNKNOWN_PATH_KEYS.has(key)) {
return {
block: true,
reason: `Unsupported path carrier field "${key}" (builtin tools use path only)`,
};
}
}
// read requires path; grep/find/ls default to "."
let rawPath: string;
if (typeof input.path === "string" && input.path.trim()) {
rawPath = input.path;
} else if (name === "read") {
return { block: true, reason: 'read requires input.path' };
} else {
rawPath = ".";
}
const base = path.basename(rawPath.replace(/^[@~]+/, ""));
if (
base === ".env" ||
base.startsWith(".env.") ||
base === "auth.json" ||
rawPath.split(path.sep).includes(".git") ||
rawPath.split(path.sep).includes(".secrets") ||
rawPath.split(path.sep).includes("data")
) {
return { block: true, reason: `Path "${rawPath}" is denied by policy` };
}
const check = contained(root, rawPath);
if (!check.ok || !check.resolved) {
return {
block: true,
reason: `Path "${rawPath}" refused (${check.reason || "policy"})`,
};
}
let st: fs.Stats;
try {
st = fs.lstatSync(check.resolved);
} catch {
return { block: true, reason: "stat failed" };
}
if (st.isDirectory()) {
const dirOk = directoryAllowOk(root, check.resolved, allow);
if (!dirOk.ok) {
return { block: true, reason: dirOk.reason || "directory allowlist failed" };
}
} else if (st.isFile()) {
if (!fileAllowlisted(root, check.resolved, allow)) {
return { block: true, reason: `Path "${rawPath}" not in selected-file allowlist` };
}
} else {
return { block: true, reason: "special file refused" };
}
// Canonicalize the only supported field
input.path = check.resolved;
return undefined;
});
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff