From 570ddcf9c766260724fdab91a36cc2f4fe7dc225 Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 17 Oct 2026 19:52:07 +0000 Subject: [PATCH 01/18] Record the task card for feature-delivery-model-v3 Why: establish the authorized task workspace and its initial baseline. Evidence: preserved precheck receipt in .task/feature-delivery-model-v3.md; R0=6c90e6c92aca0968158b6fa50ec8065ba0869913. Impact: task metadata only; the feature workspace starts at 6c90e6c92aca0968158b6fa50ec8065ba0869913. Refs: .task/feature-delivery-model-v3.md --- .task/feature-delivery-model-v3.md | 153 +++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 .task/feature-delivery-model-v3.md diff --git a/.task/feature-delivery-model-v3.md b/.task/feature-delivery-model-v3.md new file mode 100644 index 00000000..bdc331da --- /dev/null +++ b/.task/feature-delivery-model-v3.md @@ -0,0 +1,153 @@ +# feature/delivery-model-v3 任务卡 + +任务主记录:;状态:进行中;owner:weichen + +## 基线与授权 + +- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 6c90e6c92aca0968158b6fa50ec8065ba0869913 +- 工作区:/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3(worktree);分支:feature/delivery-model-v3 +- 分支类型:initiative;initiative_id:feature-delivery-model-v3;root_target:main;immediate_target:main;accepting_requirements:true +- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=af43f790b177ff9945bafab8a985234c46569af63270b8ca0bc34516ce73f410;triage_receipt=/tmp/govv3-triage-20261018.json;receipt_sha256=8030b2bf877f30fe42e68ba688877586ec22ea020099863366e6b8e2d47fe4fb + +- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进) +- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md) +- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入) +- 人工决策点:仅限目标/范围/验收变更、重大取舍、高后果操作或无法自动判定的业务验收 +- 当前阶段:需求拆解;当前增量:I1(待定义) +- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位) + +## 目标与非目标 + +<预期结果、受影响入口/模块/契约;明确不做什么。> + +## Agent 实施收据 + +按根目录 AGENT-EXECUTION.md 路由;外部 Agent 一单元一新进程/工作区。实施、修复与测试的正常候选仅为 Cursor、Antigravity(agy)和 PI;每个候选对安全可恢复失败最多 3 次全新尝试,然后才换下一候选。三者都安全耗尽后,才允许一次全新的 Codex CLI 紧急尝试。Codex 平时只用于规划、方案设计、实施计划或独立 Review。显式 fail-stop(failed-unknown、scope-expansion、blocked、cancelled、无效配置、未对账 Git 状态与安全失败)立即停止。不因推理时长、工具次数、规划方式或首次写文件时间判定失败;尝试只在显式终态失败、进程退出、总超时,或 stdin/stdout/stderr 均无活动的空闲超时时失败。CLI 可在目标、范围与安全边界内自主检查、计划、测试并迭代。实施集成必须是成功 transport 上的 scripts/agent-exec apply,或 pre-commit admit 的精确内容来源。host 直接管理的豁免仅为 .task/**、docs/superpowers/specs/** 与 docs/superpowers/plans/**。fallback 列只记录该行是否为 Codex 紧急尝试。安全耗尽不发出 host-fallback-authorized,终态为 unavailable 或 blocked。不授权 host 直接撰写该单元。原始日志保留在源码外收据路径。 + +| 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 | +|---|---|---|---|---|---|---|---|---|---|---| +| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — | + +## 关联方向与规划 + +- 适用状态:待核对(无适用规划时说明原因) +- 主记录路径/版本:待核对(路径按仓库根目录定位,归档后不变;只引用唯一正文) +- 对应里程碑:待核对 + +## 需求分析 + +| ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 | +|---|---|---|---|---|---|---| +| R1 | 待填写 | 待评估 | 待定 | 待定义 | 待确认 | 待评审 | + +## 方案与计划 + +- 方案引用:待填写(含对象版本) +- 实施计划引用:待填写(含增量与需求 ID) +- 测试计划引用:待填写(含需求 ID 与验收条件) + +## 阶段记录 + +| 阶段 | 对象版本 | 状态 | 决策/执行者 | 时间 | 依据 | +|---|---|---|---|---|---| +| 需求拆解 | 待定 | 待执行 | — | — | — | +| 需求评估 | 待定 | 待执行 | — | — | — | +| 需求表确认 | 待定 | 待执行 | — | — | — | +| 方案设计 | 待定 | 待执行 | — | — | — | +| 实施计划 | 待定 | 待执行 | — | — | — | +| 实施与自检 | 待定 | 待执行 | — | — | — | +| 正式测试与验收 | 待定 | 待执行 | — | — | — | +| 代码 Review | 待定 | 待执行 | — | — | — | +| 合入 | 待定 | 待执行 | — | — | — | +| 合入核验与收口 | 待定 | 待执行 | — | — | — | + +## 探索试验与迭代 + +| 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 | +|---|---|---|---|---|---|---|---| +| 待定义 | — | — | — | 待核对 | NOT_RUN | 待评估 | 待评估 | + +## 同步记录 + +| 节点 | sync_base_sha | tested_head_sha | checked_at | +|---|---|---|---| +| 启动 | 6c90e6c92aca0968158b6fa50ec8065ba0869913 | 6c90e6c92aca0968158b6fa50ec8065ba0869913 | 2026-10-17T19:52:06Z | + +## 证据(Evidence) + + +| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) | +|---|---|---|---|---|---| + +## 交付调度 + +- 调度状态:未入队;调度记录:无 +- entry_id:无;attempt_id:无;窗口执行者:无 +- 固定候选:无(base/head、组件与相关配置引用实际尝试) +- 调度依据:policy/merge-queue.md;新模式启用前按既有交付门禁 +- 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合 + +## 结果与验证(完成后填写) + +- 结果: +- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表> +- 影响与剩余项: + +## 规划进展与下一步 + +- 本次推进/剩余:待核对(依据实际成果更新适用方案/规划) +- 首选下一任务:待核对(无适用规划、已完成或受阻时如实说明,不虚构任务) +- 推荐依据:待填写 +- 前置条件/待确认:待核对 +- 预期产物/验收条件:待定义 +- 重复任务核对:待核对(已有/在途任务、PR 与规划记录) +- 启动授权:未授权(建议不自动启动新任务,仍须明确授权) + +## 合入与清理 + +- Review 记录:NOT_RUN(审阅智能体/平台身份、base/head、结论、问题闭合及重放依据) +- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测) +- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果) +- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验) +- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置) + + +```json +{ + "schema": 1, + "source": "SELF", + "timestamp": "2026-10-17T19:51:04.541476+00:00", + "command": "make precheck", + "command_sha256": "8fd03cdee8adbb8562846a1a114cc0623779a8654412cd20aac9b495e7eb0450", + "cwd": "/home/weichen/projects/Agent-System-Dev-gov", + "common_dir": "/home/weichen/projects/Agent-System-Dev-gov/.git", + "before": { + "head": "6c90e6c92aca0968158b6fa50ec8065ba0869913", + "branch": "main", + "target_branch": "main", + "target_head": "6c90e6c92aca0968158b6fa50ec8065ba0869913", + "status": "", + "tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untracked": {}, + "submodules": " 4f583ca3eeeb07a56a714c5086de84dcd6ad2980 deps/agent-command (remotes/origin/feature/ext10-slash-intent-1-g4f583ca)\n ed2af3e6048928b1fd0ba6da1f9d0c1bafec82d4 deps/agent-command/deps/development-guidelines (remotes/origin/feature/delivery-mechanism-5-ged2af3e)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 deps/agent-native (remotes/origin/feature/serial-tools-partition-1-gc348441)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-native/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 490b0b2dfa1567cb16ff7a7230fd78e52bc2938e deps/agent-squad (remotes/origin/feature/bump-agent-native-1-g490b0b2)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-squad/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 deps/agent-squad/deps/agent-native (remotes/origin/feature/serial-tools-partition-1-gc348441)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-squad/deps/agent-native/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/logging-system/deps/development-guidelines (26b562d)\n 2688fd2be6a52c0bf110e7e6366b6f291ab5e4eb deps/development-guidelines (remotes/origin/feature/evidence-provenance-10-g2688fd2)\n 0552b7358d6955057d6eea090326f83317ed992a deps/development-manager (remotes/origin/feature/p4p5-close-27-g0552b73)\n 2688fd2be6a52c0bf110e7e6366b6f291ab5e4eb deps/development-manager/development/guidelines (remotes/origin/feature/evidence-provenance-10-g2688fd2)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/logging-system/deps/development-guidelines (26b562d)\n b00e1924407989e52350ecdfa17f16640815ed89 modules/backend (v0.5.0-26-gb00e192)\n 4f583ca3eeeb07a56a714c5086de84dcd6ad2980 modules/backend/deps/agent-command (remotes/origin/feature/ext10-slash-intent-1-g4f583ca)\n ed2af3e6048928b1fd0ba6da1f9d0c1bafec82d4 modules/backend/deps/agent-command/deps/development-guidelines (remotes/origin/feature/delivery-mechanism-5-ged2af3e)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 modules/backend/deps/agent-native (remotes/origin/feature/serial-tools-partition-1-gc348441)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-native/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 490b0b2dfa1567cb16ff7a7230fd78e52bc2938e modules/backend/deps/agent-squad (remotes/origin/feature/bump-agent-native-1-g490b0b2)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-squad/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 modules/backend/deps/agent-squad/deps/agent-native (remotes/origin/feature/serial-tools-partition-1-gc348441)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/logging-system/deps/development-guidelines (26b562d)\n 1a5ef6b75cc74088786487d82b6f6b70bf977280 modules/backend/deps/agent-system-core (v0.4.0-3-g1a5ef6b)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/logging-system/deps/development-guidelines (26b562d)\n 1a5ef6b75cc74088786487d82b6f6b70bf977280 modules/core (v0.4.0-3-g1a5ef6b)\n 73bbb4cea686eb84826d5066bb4c44bf883c9365 modules/documents (remotes/origin/feature/layered-python-env)\n 92864db8d33bc0601f06e25d9ae2af4a404b9b6a modules/frontend (v0.4.0-72-g92864db)\n c9187473fb1ae9e8f3946cdf9b67055ee732054a modules/ui (v0.4.0-9-gc918747)\n" + }, + "after": { + "head": "6c90e6c92aca0968158b6fa50ec8065ba0869913", + "branch": "main", + "target_branch": "main", + "target_head": "6c90e6c92aca0968158b6fa50ec8065ba0869913", + "status": "", + "tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untracked": {}, + "submodules": " 4f583ca3eeeb07a56a714c5086de84dcd6ad2980 deps/agent-command (remotes/origin/feature/ext10-slash-intent-1-g4f583ca)\n ed2af3e6048928b1fd0ba6da1f9d0c1bafec82d4 deps/agent-command/deps/development-guidelines (remotes/origin/feature/delivery-mechanism-5-ged2af3e)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 deps/agent-native (remotes/origin/feature/serial-tools-partition-1-gc348441)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-native/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 490b0b2dfa1567cb16ff7a7230fd78e52bc2938e deps/agent-squad (remotes/origin/feature/bump-agent-native-1-g490b0b2)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-squad/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 deps/agent-squad/deps/agent-native (remotes/origin/feature/serial-tools-partition-1-gc348441)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c deps/agent-squad/deps/agent-native/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/agent-squad/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/agent-squad/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/agent-squad/deps/logging-system/deps/development-guidelines (26b562d)\n 2688fd2be6a52c0bf110e7e6366b6f291ab5e4eb deps/development-guidelines (remotes/origin/feature/evidence-provenance-10-g2688fd2)\n 0552b7358d6955057d6eea090326f83317ed992a deps/development-manager (remotes/origin/feature/p4p5-close-27-g0552b73)\n 2688fd2be6a52c0bf110e7e6366b6f291ab5e4eb deps/development-manager/development/guidelines (remotes/origin/feature/evidence-provenance-10-g2688fd2)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 deps/logging-system/deps/development-guidelines (26b562d)\n b00e1924407989e52350ecdfa17f16640815ed89 modules/backend (v0.5.0-26-gb00e192)\n 4f583ca3eeeb07a56a714c5086de84dcd6ad2980 modules/backend/deps/agent-command (remotes/origin/feature/ext10-slash-intent-1-g4f583ca)\n ed2af3e6048928b1fd0ba6da1f9d0c1bafec82d4 modules/backend/deps/agent-command/deps/development-guidelines (remotes/origin/feature/delivery-mechanism-5-ged2af3e)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 modules/backend/deps/agent-native (remotes/origin/feature/serial-tools-partition-1-gc348441)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-native/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 490b0b2dfa1567cb16ff7a7230fd78e52bc2938e modules/backend/deps/agent-squad (remotes/origin/feature/bump-agent-native-1-g490b0b2)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-squad/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n c348441691e8bdab784a03aa9be64d53dacff341 modules/backend/deps/agent-squad/deps/agent-native (remotes/origin/feature/serial-tools-partition-1-gc348441)\n 343b1e6d9a5b58e47bf7e594826221036ae8175c modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent (remotes/origin/feature/agent-stack-i2-1-g343b1e6)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/agent-intent/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/agent-native/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/agent-native/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/agent-native/deps/logging-system/deps/development-guidelines (26b562d)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/development-guidelines (26b562d)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/agent-squad/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/agent-squad/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/agent-squad/deps/logging-system/deps/development-guidelines (26b562d)\n 1a5ef6b75cc74088786487d82b6f6b70bf977280 modules/backend/deps/agent-system-core (v0.4.0-3-g1a5ef6b)\n a3ee67bba269e1276c14e22c026171ecf2cf6190 modules/backend/deps/error-code-system (a3ee67b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/error-code-system/deps/development-guidelines (26b562d)\n 49b5d8be604d05e4246dffa0d10fa1c2b7ad45d7 modules/backend/deps/logging-system (49b5d8b)\n 26b562d2e34fbc560711296a309c91c74ffc55b1 modules/backend/deps/logging-system/deps/development-guidelines (26b562d)\n 1a5ef6b75cc74088786487d82b6f6b70bf977280 modules/core (v0.4.0-3-g1a5ef6b)\n 73bbb4cea686eb84826d5066bb4c44bf883c9365 modules/documents (remotes/origin/feature/layered-python-env)\n 92864db8d33bc0601f06e25d9ae2af4a404b9b6a modules/frontend (v0.4.0-72-g92864db)\n c9187473fb1ae9e8f3946cdf9b67055ee732054a modules/ui (v0.4.0-9-gc918747)\n" + }, + "after_error": null, + "exit_code": 0, + "output": "make[1]: Entering directory '/home/weichen/projects/Agent-System-Dev-gov'\n[repo] branch=main head=6c90e6c92a\n[remote] origin/main=6c90e6c92a ahead=0 behind=0\n[tree] clean submodules_drift=0 uninit=0\n[worktrees] linked=0\n[initiatives] local cards (not authoritative): none\n[cards] 闭合分支超过 24 小时未合入:feature/card-close-agent-cli-enforcement feature/card-close-workbench-flow-20261015\n[hooks] core.hooksPath=scripts/hooks(pre-commit/post-merge 实际接线)\n[facts] policy/facts.md 6 条事实均在有效期内\n[required-reading] core/baseline.md, workflows/delivery.md §1, policy/delivery.md\n[R0] 6c90e6c92a(完整:6c90e6c92aca0968158b6fa50ec8065ba0869913)\n[worktree] before-sync: recursive component checks passed\n[verdict] READY — 保留本次 precheck receipt 后建任务工作区:PRECHECK_RECEIPT=/tmp/task-precheck.json make task-start NAME=feature/your-branch\nmake[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev-gov'\n", + "root_target": "main", + "immediate_target": "main", + "output_sha256": "89a93d0dafd31c9e5667b0063229908928d610b273bf0bcdf2292ef424404399", + "receipt_sha256": "b871e6ffccdcb0d355639451bcfd182edfe4e26ae68a2d5212276f01bb022d1b" +} +``` From 10815cb3c3a51ccc921ac052f26b9e7454821189 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 00:07:43 +0000 Subject: [PATCH 02/18] agent-exec transport 538cd301c4df07fe79ee50f73e1150cb/0cb29a01efcc --- PROJECT.md | 2 ++ modules/documents | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/PROJECT.md b/PROJECT.md index 02a0d283..33e78049 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -12,6 +12,8 @@ 目录中的既有文档不因被收录而自动成为已批准规划。记录、任务关联及收尾建议按 [交付规范的长期方向与任务衔接](policy/delivery.md#长期方向与任务衔接) 执行。 +- [分层分支交付与规范精简 r1](modules/documents/docs/proposals/2026-10-18-delivery-model-v3.md):2026-10-18 用户批准实施。固定两级分支、任务卡合入前折叠闭合、按层级分配门禁、任务分类通道与规范精简;由治理大任务 `feature/delivery-model-v3` 推进。 + - [文档职责矩阵整改 r2](modules/documents/docs/proposals/2026-10-15-rdm-duty-matrix-rework.md):项目设置的文档职责改为三视图分配,不创建文档、不执行审签。2026-10-15 按实施方案执行;部署与真实账号验收未授权。 - [svc 插件组合部署(--with)方案 r1](modules/documents/docs/proposals/2026-10-14-svc-plugin-compose-deploy.md):待评审。部署时把登记插件与 Dev 基座编成一个二进制并校验激活。收录不构成批准。 diff --git a/modules/documents b/modules/documents index 73bbb4ce..e2f5729e 160000 --- a/modules/documents +++ b/modules/documents @@ -1 +1 @@ -Subproject commit 73bbb4cea686eb84826d5066bb4c44bf883c9365 +Subproject commit e2f5729e8f384f3d57d04e3ef1f45fb18c73dd54 From b103f3c58a67f859fb2f6c08513849cedf8c0db1 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 00:18:05 +0000 Subject: [PATCH 03/18] Record the governance initiative charter and unit receipts Why: the delivery-model-v3 initiative needs its charter, child registry and agent-exec receipts on the card. Evidence: unit receipts /tmp/govv3-u1-receipt.json, /tmp/govv3-u2-receipt.json, /tmp/govv3-u2r2-receipt.json (SELF). Impact: task card only. Refs: feature/delivery-model-v3 Co-authored-by: Cursor --- .task/feature-delivery-model-v3.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/.task/feature-delivery-model-v3.md b/.task/feature-delivery-model-v3.md index bdc331da..50a418dc 100644 --- a/.task/feature-delivery-model-v3.md +++ b/.task/feature-delivery-model-v3.md @@ -9,17 +9,27 @@ - 分支类型:initiative;initiative_id:feature-delivery-model-v3;root_target:main;immediate_target:main;accepting_requirements:true - 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=af43f790b177ff9945bafab8a985234c46569af63270b8ca0bc34516ce73f410;triage_receipt=/tmp/govv3-triage-20261018.json;receipt_sha256=8030b2bf877f30fe42e68ba688877586ec22ea020099863366e6b8e2d47fe4fb -- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 授权与交付终点:2026-10-18 用户批准计划"分层交付与规范精简"并指示按计划实施至全部待办完成;终点为本 L1 合入 main 并完成收口。未授权:分支保护/平台设置变更、改写历史、发布版本(双钥匙)。 - 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进) - 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md) - 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入) - 人工决策点:仅限目标/范围/验收变更、重大取舍、高后果操作或无法自动判定的业务验收 -- 当前阶段:需求拆解;当前增量:I1(待定义) +- 当前阶段:实施与自检;当前增量:R(主记录与 L1 建立) - 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位) ## 目标与非目标 -<预期结果、受影响入口/模块/契约;明确不做什么。> +L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"——任务卡合入前折叠闭合、统一 CI 等待入口与子模块并行物化、 +门禁按层级分配(L2→L1 轻量)、任务分类通道、规范精简。接纳范围:Dev 仓治理脚本、CI、任务卡模板与 policy/AGENTS 文本。 +非目标:业务代码、组件仓治理同步、分支保护等平台设置、自动合并队列或调度服务。 +主记录:modules/documents/docs/proposals/2026-10-18-delivery-model-v3.md(Documents #112,e2f5729)。 +验收:P1–P3 各项在本分支可用并有直接测试;`make test` 与 CI 在最终 HEAD 通过;复测计划(M)登记在案。 + +## 子任务登记 + +| 子任务分支 | 必需 | 状态 | PR | +|---|---|---|---| +| (本 L1 以 agent-exec 单元直接推进,未派生 L2) | 否 | — | — | ## Agent 实施收据 @@ -27,7 +37,12 @@ | 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 | |---|---|---|---|---|---|---|---|---|---|---| -| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — | +| govv3-u1-doc-proposal(Documents 仓) | docs/proposals/2026-10-18-delivery-model-v3.md、INDEX.md | 76bc59db1d0e | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | success → Documents transport c1b0b35,apply 快进;Documents #112 合入 e2f5729 | c1b0b35 | false | /tmp/govv3-u1-receipt.json | +| govv3-u2-dev-proposal-link(第一次运行) | PROJECT.md、modules/documents | caefcb72b970 | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | failed-unknown(单元包要求 git update-index,被判为未解释的 git 管理变更)→ fail-stop | 无 | false | /tmp/govv3-u2-receipt.json | +| govv3-u2-dev-proposal-link-r2(第二次运行) | 同上 | caefcb72b970 | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | success → transport 10815cb,apply 快进 | 10815cb | false | /tmp/govv3-u2r2-receipt.json | + +- u2 第一次运行对账:规范检出未变、无 transport、私有工作区保留;处置:单元包改用既有 gitlink 提升模式(本地 clone --no-checkout 后 detach,禁止 submodule/add/update-index),全新派发。 +- Documents #111(host 直写 docs/proposals,不在豁免路径内)已关闭并删除分支,改由 u1 撰写。 ## 关联方向与规划 From 2757b6f68e25d8161df43965c059521e7676e17d Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 00:49:09 +0000 Subject: [PATCH 04/18] agent-exec transport b9c7fb196afb6984f4367f30ec8ec18a/162bb30bb46e --- AGENTS.md | 13 +++-- Makefile | 4 +- policy/delivery.md | 8 ++- policy/governance.md | 3 +- scripts/evidence.py | 12 +++- scripts/hooks/post-merge | 7 ++- scripts/task.sh | 102 +++++++++++++++++++++++++++++++++ scripts/task_pr_check.py | 29 +++++++--- tests/test_evidence.py | 26 +++++++++ tests/test_task.py | 109 ++++++++++++++++++++++++++++++++++++ tests/test_task_pr_check.py | 36 ++++++++++++ 11 files changed, 325 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ae3422fe..01e23625 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,12 +113,13 @@ policy/delivery.md。 `TASK_WRITERS_STOPPED=1 make task-finish`。父子仓改动、未跟踪/ignored 唯一文件先保存或显式处置;删除被拒时 保留现场并按原因处理,禁止强删兜底;无法证明写者已退出时才请 owner 确认。放弃未合入工作用 `make task-finish ABANDON="--abandon <原因>"` 并记录原因;放弃不豁免现场保护。 -- **任务卡闭合(防欠账)**:分支合入 main 后,其任务卡须回填"已完成"+主记录 PR 链接并 - 归档至 `.task/archive/`。`task-finish` 回收后按提示运行 `make task-close`,不借回收修改 - 共享主检出或推送另一任务。主检出 pull/merge 的 post-merge 钩子仅提示待办,不自动写入或推送。 - `make task-close` 会提交、推送并可能创建 PR,标准闭合属于已授权交付的收口;仍须核对只包含本任务卡,否则停止扩围。闭合在主检出运行→建 - `feature/card-close-*` 分支+PR,任务分支内加 `FOLD=--fold` 随当前 PR 合入。 - precheck 的 `[cards]` 行报告欠账,验收前应清零;闭合 PR 同样由智能体按 +- **任务卡闭合(防欠账)**:默认在合入前闭合——最终同步/合入前,在任务 worktree 运行 + `make task-close SELF=1`:本分支任务卡改为"已完成(合入前闭合)"、主记录写入本 PR 链接并归档至 + `.task/archive/`,随同一 PR 合入,该 PR 的合并提交即落地记录,无需单独闭合 PR 或合入后回填。 + 它会提交并推送当前任务分支,属于已授权交付的收口;只处理本分支的卡。主检出 pull/merge 的 + post-merge 钩子仅提示,不自动写入或推送。主检出的 `make task-close`(建 `feature/card-close-*` + 分支+PR)或任务分支内 `FOLD=--fold` 只用于已合入但未闭合的遗留卡;仍须核对只包含目标任务卡, + 否则停止扩围。precheck 的 `[cards]` 行报告欠账,验收前应清零;遗留卡的闭合 PR 同样由智能体按 policy/delivery.md 完成 Review、冲突处置及当前 HEAD 的 CI 核验后合入。 ## 证据与验证门禁(强制) diff --git a/Makefile b/Makefile index 77f2f6f9..d1fd4f17 100644 --- a/Makefile +++ b/Makefile @@ -54,8 +54,10 @@ task-resume: task-sync: @bash $(TASK) sync +# 任务卡闭合:默认在任务 worktree 合入前运行 SELF=1(本卡随当前 PR 完成并归档,无需单独闭合 PR); +# 不带参数(主检出)或 FOLD=--fold 仅用于已合入未闭合的遗留卡。 task-close: - @bash $(TASK) close $(FOLD) + @bash $(TASK) close $(if $(SELF),--self,$(FOLD)) task-finish: @bash $(TASK) finish $(ABANDON) diff --git a/policy/delivery.md b/policy/delivery.md index 67bde148..606676bb 100644 --- a/policy/delivery.md +++ b/policy/delivery.md @@ -157,7 +157,7 @@ host 可以直接编写并提交的豁免只有 `.task/**`、`docs/superpowers/s - **子任务分支(child)**:从远端所属大任务分支的当前头建立(须等于分流收据核验的 head,`make task-start` 校验),即时目标为大任务分支。负责一个有界单元的实施、独立 Review 与局部验证,通过 PR 合入大任务分支。 - **普通任务分支(standalone)**:无需阶段集成的小需求或独立修复,直接从最新 `origin/main` 建立并向 `main` 交付。 -每个任务卡记录 `root_target` 与 `immediate_target`(子任务为所属大任务分支,其余为 `main`)。`make task-resume`、`make task-sync` 与 `make task-finish` 以 `immediate_target` 为准;`make task-close` 仅在 `FOLD=--fold` 时以其为闭合 PR base。 +每个任务卡记录 `root_target` 与 `immediate_target`(子任务为所属大任务分支,其余为 `main`)。`make task-resume`、`make task-sync` 与 `make task-finish` 以 `immediate_target` 为准;任务卡默认在合入前以 `make task-close SELF=1` 闭合并随本 PR 合入(归档后上述命令仍从 `.task/archive/` 读取合入前闭合的卡);遗留卡的 `make task-close` 仅在 `FOLD=--fold` 时以其为闭合 PR base。 每次接到新需求,创建分支前必须分流,事实来源为 origin(本地 `.task/` 不参与发现): 1. **发现**:`make task-triage REQ='<需求>' TRIAGE_RECEIPT=<路径>` 查询远端。候选须已推送、存在以 `main` 为 base 的开放 PR,且该远端分支的任务卡声明 `branch_kind=initiative`、`accepting_requirements=true` 且未标记已完成。无候选时收据直接为 `NEW`;有候选时为 `DECISION_REQUIRED`(退出码 5)。远端或 Gitea 不可核验时记为 `REMOTE_UNVERIFIED`(退出码 6),不得得出 `NEW`。 @@ -353,8 +353,10 @@ Review 只将阻塞项列为必修;非阻塞意见标明后续建议,不得 清理前保存任务主记录与证据。执行回收时将命令输出用 evidence 的 `RECEIPT` 模式保存到 待删目录之外;回收后的结果写入后续闭合记录,不能向已删除的任务卡补写收据。 -任务卡须回填真实主 PR 和完成状态并归档;闭合 PR 的创建核对当前闭合授权,合入按上述 -智能体 Review 与 PR 合入流程执行。 +任务卡默认在最终同步/合入前于任务 worktree 运行 `make task-close SELF=1`:写入本 PR 链接与 +"已完成(合入前闭合)"状态并归档,随同一 PR 合入,不另建闭合 PR。仅已合入未闭合的遗留卡在主检出 +运行 `make task-close`(`feature/card-close-*` PR)或用 `FOLD=--fold` 回填归档;其闭合 PR 的创建核对 +当前闭合授权,合入按上述智能体 Review 与 PR 合入流程执行。 post-merge 默认只提示,不因 pull 或 merge 自动提交、推送或创建闭合 PR。 中断或交接时记录当前有效版本、批准范围、未决假设、最新试验结论、剩余事项及下一动作。 diff --git a/policy/governance.md b/policy/governance.md index 30985805..7e328e43 100644 --- a/policy/governance.md +++ b/policy/governance.md @@ -138,7 +138,8 @@ Git 对含子模块的工作区需要显式删除选项时,只有全部现场 子仓 pin 之外的 refs/stash/reflog 历史采取保守拒绝,即使其中部分可能已在远端: owner 须先独立确认其保存与归属,工具不把缓存的远端引用当作唯一数据已经备份的证据。 远端分支删除与本地回收分别报告,失败不能声明两者全部完成。 -回收后由所有者执行 `make task-close` 闭合卡片;回收命令不顺带改写共享主检出。闭合 PR 默认 base 为 `main`;仅 `FOLD=--fold`(随任务分支合入)时 base 为卡片 `immediate_target`。 +任务卡默认在合入前闭合:最终同步/合入前在任务 worktree 运行 `make task-close SELF=1`,本卡改为"已完成(合入前闭合)"并归档,随同一 PR 合入,无需单独闭合 PR;回收命令不顺带改写共享主检出。 +主检出的 `make task-close`(闭合 PR base 为 `main`)或 `FOLD=--fold`(随任务分支合入,base 为卡片 `immediate_target`)只用于已合入未闭合的遗留卡。 post-merge 仅提示,不自动调用闭合。`make task-close` 会提交、推送并可能创建 PR, 执行者先按迭代交付规则核对该动作的授权及所选任务,保留未授权的其他任务现场。 回收命令使用待删目录之外的 `RECEIPT` 保存输出,清理结果由后续闭合记录引用。 diff --git a/scripts/evidence.py b/scripts/evidence.py index b534860c..fc36c290 100644 --- a/scripts/evidence.py +++ b/scripts/evidence.py @@ -23,6 +23,7 @@ GIT_SHA = re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})\Z") BRANCH = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]{0,199}\Z") # 可选的分支目标字段:两者须同时出现或同时缺省(旧收据没有)。 TARGET_FIELDS = ("root_target", "immediate_target") +PRE_MERGE_CLOSED = "已完成(合入前闭合)" BASE_FIELDS = {"schema", "source", "timestamp", "command", "command_sha256", "cwd", "common_dir", "before", "after", "after_error", "exit_code", "output", "output_sha256", "receipt_sha256"} @@ -255,7 +256,7 @@ def task_context(current=None): if (root / ".task").is_symlink(): raise EvidenceError("Task registration must belong to the current checkout") card = find_card() - if card.is_symlink() or card.resolve().parent != root / ".task": + if card.is_symlink() or card.resolve().parent not in (root / ".task", root / ".task" / "archive"): raise EvidenceError("Task registration must belong to the current checkout") content = card.read_text() locations = re.findall(r"^- 工作区:(.+?)\s*[((]worktree[))];分支:([^\s]+)\s*$", content, re.M) @@ -318,8 +319,15 @@ def find_card(): card = root / ".task" / (branch.replace("/", "-") + ".md") if card.is_file(): return card + owner = re.compile(r"^- 工作区:.*分支:" + re.escape(branch) + r"(?:\s|$)", re.M) for candidate in (root / ".task").glob("*.md"): - if re.search(r"^- 工作区:.*分支:" + re.escape(branch) + r"(?:\s|$)", candidate.read_text(), re.M): + if owner.search(candidate.read_text()): + return candidate + # A card fold-closed before merge (task.sh close --self) lives in the archive until its PR lands. + for candidate in sorted((root / ".task" / "archive").glob("*.md")): + text = candidate.read_text() + status = re.search(r";状态:([^;\n]*)", text) + if owner.search(text) and status and status.group(1).startswith(PRE_MERGE_CLOSED): return candidate raise EvidenceError("No task card exists for the current branch") diff --git a/scripts/hooks/post-merge b/scripts/hooks/post-merge index fab194bd..dcbe397a 100755 --- a/scripts/hooks/post-merge +++ b/scripts/hooks/post-merge @@ -1,6 +1,7 @@ #!/usr/bin/env bash -# 本地钩子(post-merge):pull/merge 落地后提示 owner 按授权闭合任务卡;ff-only 也会触发。 -# precheck [cards] 报告欠账;此钩子只提示,不执行闭合或其他写入。 +# 本地钩子(post-merge):pull/merge 落地后提示任务卡闭合方式;ff-only 也会触发。 +# 新任务在合入前用 make task-close SELF=1 闭合本卡;precheck [cards] 报告的遗留卡才需主检出闭合。 +# 此钩子只提示,不执行闭合或其他写入。 # 触发条件收紧到"主检出 + 集成分支 main"——任务 worktree 里 task-sync 的 merge 不触发, # 防止把卡片闭合提示混进任务分支现场。 common=$(git rev-parse --git-common-dir) && common=$(cd "$common" 2>/dev/null && pwd) || exit 0 @@ -8,5 +9,5 @@ mainrepo=$(dirname "$common") [ "$mainrepo" = "$PWD" ] || exit 0 [ "$(git rev-parse --abbrev-ref HEAD)" = "main" ] || exit 0 [ -f "$mainrepo/scripts/task.sh" ] || exit 0 -echo "[next] owner 按已有授权在主检出运行 make task-close 闭合已合入任务卡;未授权时先取得授权。" +echo "[next] 新任务在合入前于任务 worktree 运行 make task-close SELF=1 闭合本卡;主检出的 make task-close 仅用于 precheck [cards] 报告的遗留卡,owner 按已有授权运行,未授权时先取得授权。" exit 0 diff --git a/scripts/task.sh b/scripts/task.sh index ff0d5e6d..ac0d6d28 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -456,6 +456,8 @@ cmd_sync() { # (PR 链接+合并 SHA)与闭合记录,并 git mv 到 .task/archive/。变更落点:主检出运行→ # 新建 feature/card-close-* 分支推送(集成分支受保护,不直推);任务分支内运行须 --fold # (随当前任务 PR 合入)。合并判定用合并提交主题(分支删除后仍可追溯),不依赖分支存活。 +# 默认闭合方式是 close --self:任务分支在合入前把本分支的卡改为"已完成(合入前闭合)"并归档, +# 随本 PR 合入;上述合入后回填只用于遗留卡。 card_of_branch() { sed -n 's/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/\1/p' "$1" | head -1; } card_status() { sed -n 's/^.*;状态:\([^;]*\).*/\1/p' "$1" | head -1; } @@ -467,13 +469,20 @@ card_initiative_id() { card_field "$1" initiative_id; } card_root_target() { local v; v=$(card_field "$1" root_target); printf '%s\n' "${v:-${TARGET_BRANCH}}"; } card_immediate_target() { local v; v=$(card_field "$1" immediate_target); printf '%s\n' "${v:-${TARGET_BRANCH}}"; } card_accepting_requirements() { local v; v=$(card_field "$1" accepting_requirements); printf '%s\n' "${v:-false}"; } +PRE_MERGE_CLOSED="已完成(合入前闭合)" branch_card() { # $1=branch → 当前检出中该分支的任务卡路径(无则空) + # 顶层优先;顶层无卡时只认 .task/archive/ 中状态为"已完成(合入前闭合)"的卡(close --self 合入前已归档)。 local pc=".task/$(echo "$1" | tr '/' '-').md" [ -f "$pc" ] && { printf '%s\n' "$pc"; return 0; } for pc in .task/*.md; do [ -f "$pc" ] || continue [ "$(card_of_branch "$pc")" = "$1" ] && { printf '%s\n' "$pc"; return 0; } done + for pc in .task/archive/*.md; do + [ -f "$pc" ] || continue + [ "$(card_of_branch "$pc")" = "$1" ] || continue + case "$(card_status "$pc")" in "${PRE_MERGE_CLOSED}"*) printf '%s\n' "$pc"; return 0 ;; esac + done return 0 } active_initiatives() { # → 每行 "branch|initiative_id|card":根目标为集成分支、未完成、仍接收需求且未合入/待闭合 @@ -665,8 +674,101 @@ open(path, 'w', encoding='utf-8').write(s) PY } +self_close_pr() { # $1=branch → 打印 Gitea 上 head 为该分支的开放 PR 号(空格分隔;token 只在进程内使用) + CLOSE_BRANCH="$1" TASK_SCRIPT_DIR="$SCRIPT_DIR" python3 - <<'PY' +import os, subprocess, sys +sys.path.insert(0, os.environ["TASK_SCRIPT_DIR"]) +from task_triage import Stop, api_base, open_pulls, read_token, repo_slug +branch = os.environ["CLOSE_BRANCH"] +try: + origin = subprocess.run(["git", "remote", "get-url", "origin"], capture_output=True, text=True) + if origin.returncode != 0: + raise Stop(6, "origin remote is not configured") + token = read_token() + if not token: + raise Stop(6, "no Gitea token: set GITEA_TOKEN or ~/.secrets/gitea ACCESS_TOKEN") + pulls = open_pulls(api_base(), repo_slug(origin.stdout.strip()), token) +except Stop as stop: + print(f"ERROR: cannot look up the open PR for {branch}: {stop.message}", file=sys.stderr) + sys.exit(6) +numbers = [] +for pr in pulls: + if not isinstance(pr, dict) or pr.get("state", "open") != "open": + continue + head = pr.get("head") if isinstance(pr.get("head"), dict) else {} + if head.get("ref") == branch and str(pr.get("number", "")).isdigit(): + numbers.append(str(pr["number"])) +print(" ".join(numbers)) +PY +} + +card_self_close() { # $1=card $2=pr_num $3=pr_url(原地改写:状态、主记录、结果节标题、合入前闭合记录) + python3 - "$1" "$2" "$3" <<'PY' +import re, sys +path, pr, link = sys.argv[1:4] +s = open(path, encoding='utf-8').read() +s, n = re.subn(r';状态:[^;\n]*', ';状态:已完成(合入前闭合)', s, count=1) +if n != 1: + sys.exit(f"ERROR: {path} has no ';状态:' field; refuse to fold-close") +s = re.sub(r'任务主记录:[^;\n]*', f'任务主记录:PR #{pr} {link}', s, count=1) +s = s.replace('## 结果与验证(完成后填写)', '## 结果与验证', 1) +s = s.rstrip('\n') + f''' + +## 闭合记录(合入前折叠) + +- 本卡在 PR #{pr}({link})合入前闭合并归档,随该 PR 一同合入。 +- 该 PR 的合并提交即本任务的落地记录。 +- 合入后无需回填,也无需单独的闭合 PR。 +''' +open(path, 'w', encoding='utf-8').write(s) +PY +} + +cmd_close_self() { + local branch card dest pr nums web_base url owner repo link msg + branch=$(git symbolic-ref --quiet --short HEAD) || die "close --self 须在任务分支上运行(当前为 detached HEAD)" + [ "$branch" != "${TARGET_BRANCH}" ] || die "close --self 不能在 ${TARGET_BRANCH} 上运行;请在任务 worktree 内对本分支的卡运行" + [ -z "$(git status --porcelain | grep -v '^.. \.zcode/' || true)" ] || die "任务工作区不干净——先提交或处置现场再 close --self" + card=$(branch_card "$branch") + [ -n "$card" ] || die "找不到分支 ${branch} 的任务卡" + case "$card" in .task/archive/*) die "${card} 已在合入前闭合归档,无需重复闭合" ;; esac + dest=".task/archive/${card##*/}" + [ ! -e "$dest" ] || die "归档目标已存在:${dest}" + if [ -n "${TASK_PR:-}" ]; then + case "$TASK_PR" in *[!0-9]*) die "TASK_PR 只能是 PR 号(纯数字)" ;; esac + pr="$TASK_PR" + else + nums=$(self_close_pr "$branch") || die "无法经 Gitea 查询 ${branch} 的开放 PR;可用 TASK_PR=<号> 显式指定" + case "$nums" in + ''|*' '*) die "需要恰好一个 head 为 ${branch} 的开放 PR(查到:${nums:-无});先建 PR,或用 TASK_PR=<号> 指定" ;; + esac + pr="$nums" + fi + web_base="${TASK_CARD_WEB_BASE:-https://git-home.u1680832.nyat.app:30319}" + url=$(git remote get-url origin); url="${url%.git}" + repo=$(basename "$url"); owner=$(basename "$(dirname "$url")") + link="${web_base}/${owner}/${repo}/pulls/${pr}" + card_self_close "$card" "$pr" "$link" + mkdir -p .task/archive + git mv "$card" "$dest" + git add -- "$dest" + msg=$(mktemp) + { echo "Fold-close the task card before merge" + echo + echo "Why: close the task card inside its own PR so landing it needs no post-merge backfill or separate close PR." + echo "Evidence: ${card} -> ${dest}; status ${PRE_MERGE_CLOSED}; PR #${pr} ${link}." + echo "Impact: task metadata only; the card is archived within PR #${pr} and lands with its merge commit." + echo "Refs: task.sh close --self" + } > "$msg" + git commit -q -F "$msg"; rm -f "$msg" + # push 失败重试一次:家服隧道间歇抖动(同 cmd_close)。 + git push -q -u origin HEAD || { sleep 5; git push -q -u origin HEAD; } + echo "[closed] ${card} 已在 PR #${pr} 合入前闭合并归档;合入后无需 task-close" +} + cmd_close() { need_repo + if [ "${1:-}" = "--self" ]; then shift; cmd_close_self "$@"; return; fi local fold=0 [ "${1:-}" = "--fold" ] && fold=1 fetch_target diff --git a/scripts/task_pr_check.py b/scripts/task_pr_check.py index d928476c..19b6d92d 100644 --- a/scripts/task_pr_check.py +++ b/scripts/task_pr_check.py @@ -8,7 +8,8 @@ 须仍有开放 PR 指向其 root_target。--final(仅 initiative):accepting_requirements=false,且 `## 子任务登记` 中「必需=是」的子任务为「已合入」(PR 已合并入本 initiative)或「已延期」。 ci - 仅 pull_request 事件:按 GITHUB_HEAD_REF 在检出的 .task/ 顶层取卡片,核对 GITHUB_BASE_REF。 + 仅 pull_request 事件:按 GITHUB_HEAD_REF 在检出的 .task/ 顶层取卡片(顶层无卡时取 .task/archive/ + 中状态为「已完成(合入前闭合)」的卡),核对 GITHUB_BASE_REF。 不联网、不需要 token;非 pull_request 事件跳过(退出 0)。 期望目标:child → immediate_target;其余(initiative / standalone / 无卡)→ root_target(无卡即 main)。 @@ -24,11 +25,12 @@ import sys import urllib.parse from pathlib import Path -from task_triage import (EXIT_REMOTE, Stop, api_base, as_dict, card_branch, http_get_json, +from task_triage import (EXIT_REMOTE, Stop, api_base, as_dict, card_branch, card_status, http_get_json, open_pulls, read_token, repo_slug, target_branch) EXIT_FAILED = 1 EXIT_USAGE = 2 +PRE_MERGE_CLOSED = "已完成(合入前闭合)" CHILD_COLUMNS = ("子任务分支", "必需", "状态", "PR") DONE_STATES = ("已合入", "已延期") @@ -64,16 +66,27 @@ def expected_base(meta): return meta["immediate_target"] if meta["kind"] == "child" else meta["root_target"] -def find_card(branch): - """只看检出树的 .task/ 顶层(archive 不参与);返回 (路径, 文本),无卡返回 (None, None)。""" +def matching_card(directory, branch, archived): + """directory 下声明该分支的唯一卡片;archived=True 时只认状态以「已完成(合入前闭合)」开头的卡。""" found = [] - for path in sorted(Path(".task").glob("*.md")): + for path in sorted(Path(directory).glob("*.md")): text = path.read_text(encoding="utf-8", errors="replace") - if card_branch(text) == branch: - found.append((path, text)) + if card_branch(text) != branch: + continue + if archived and not card_status(text).startswith(PRE_MERGE_CLOSED): + continue + found.append((path, text)) if len(found) > 1: raise Stop(EXIT_FAILED, f"ambiguous task cards for {branch}: {', '.join(str(p) for p, _ in found)}") - return found[0] if found else (None, None) + return found[0] if found else None + + +def find_card(branch): + """先看检出树的 .task/ 顶层;顶层无卡时再看 .task/archive/ 中合入前闭合(close --self)的卡, + 其余归档卡不参与。返回 (路径, 文本),无卡返回 (None, None)。""" + return (matching_card(".task", branch, False) + or matching_card(".task/archive", branch, True) + or (None, None)) def branch_meta(branch, target): diff --git a/tests/test_evidence.py b/tests/test_evidence.py index e179cbc3..520d4a27 100644 --- a/tests/test_evidence.py +++ b/tests/test_evidence.py @@ -385,6 +385,32 @@ class EvidenceTests(unittest.TestCase): cwd=self.repo, capture_output=True, text=True) self.assertNotIn("WARN", checked.stdout + checked.stderr) + def archive_card(self, status): + self.card.unlink() + archived = self.repo / ".task/archive/feature-done.md" + archived.parent.mkdir() + archived.write_text(f"# Done task\n任务主记录:PR #3 fixture;状态:{status};owner:test\n" + "- 工作区:fixture(worktree);分支:main\n" + "- 分支类型:child;initiative_id:feature-alpha;root_target:main;" + "immediate_target:feature/alpha;accepting_requirements:false\n") + return archived + + def test_evidence_appends_to_pre_merge_closed_archived_card(self): + archived = self.archive_card("已完成(合入前闭合)") + result = self.run_evidence("printf 'archived\\n'") + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + text = archived.read_text() + self.assertIn("## 证据(Evidence)", text) + self.assertIn('"immediate_target": "feature/alpha"', text) + + def test_evidence_ignores_archived_card_with_other_status(self): + archived = self.archive_card("已完成(回填)") + original = archived.read_text() + result = self.run_evidence("touch should-not-exist") + self.assertNotEqual(result.returncode, 0) + self.assertFalse((self.repo / "should-not-exist").exists()) + self.assertEqual(archived.read_text(), original) + def test_receipts_without_target_metadata_default_to_main(self): data = self.capture() self.assertEqual((data["root_target"], data["immediate_target"]), ("main", "main")) diff --git a/tests/test_task.py b/tests/test_task.py index dd6ff536..e8ed576a 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -1490,6 +1490,115 @@ class TaskLifecycleTests(unittest.TestCase): self.assertIn("已包含于 origin/main", archived) self.assertIn("PR #5", archived) + # ---- close --self:合入前在任务分支内闭合本卡 ---- + + def self_close_env(self): + return self.close_env() | {"GITEA_API_URL": self.gitea.url, "GITEA_REPO": "fixture/repo"} + + def start_standalone(self, name="feature/solo"): + receipt = self.capture_precheck() + triage = self.triage_receipt() + self.task(self.repo, "start", name, + env=self.env | {"PRECHECK_RECEIPT": str(receipt), "TRIAGE_RECEIPT": str(triage)}) + return self.root / ("repo-wt-" + name.replace("/", "-")) + + def open_pr(self, wt, branch, base="main"): + self.git(wt, "push", "-q", "-u", "origin", branch) + number = len(self.gitea.prs) + 1 + sha = self.git(wt, "rev-parse", "HEAD").stdout.strip() + self.gitea.prs.append(FakeGitea.pr(number, branch, sha, base=base)) + return number + + def test_close_self_archives_own_card_inside_its_pr(self): + wt = self.start_standalone() + (wt / "work").write_text("task result\n") + self.git(wt, "add", "work") + self.git(wt, "commit", "-m", "Task work") + number = self.open_pr(wt, "feature/solo") + result = self.task(wt, "close", "--self", env=self.self_close_env()) + self.assertIn(f"[closed] .task/feature-solo.md 已在 PR #{number} 合入前闭合并归档;合入后无需 task-close", + result.stdout) + self.assertNotIn(self.gitea.token, result.stdout) + self.assertFalse((wt / ".task/feature-solo.md").exists()) + text = (wt / ".task/archive/feature-solo.md").read_text() + self.assertIn(";状态:已完成(合入前闭合);", text) + self.assertIn(f"任务主记录:PR #{number} {self.gitea.web}/{self.root.name}/remote/pulls/{number};", text) + self.assertIn("## 结果与验证\n", text) + self.assertNotIn("## 结果与验证(完成后填写)", text) + self.assertIn("## 闭合记录(合入前折叠)", text) + self.assertIn(f"PR #{number}", text.split("## 闭合记录(合入前折叠)", 1)[1]) + self.assertEqual(self.git(wt, "log", "-1", "--format=%s").stdout.strip(), + "Fold-close the task card before merge") + body = self.git(wt, "log", "-1", "--format=%B").stdout + for field in ("Why:", "Evidence:", "Impact:", "Refs: task.sh close --self"): + self.assertIn(field, body) + self.assertEqual(self.git(wt, "status", "--porcelain").stdout, "") + head = self.git(wt, "rev-parse", "HEAD").stdout.strip() + self.assertEqual(self.git(self.remote, "rev-parse", "refs/heads/feature/solo").stdout.strip(), head) + self.assertEqual(self.git(self.remote, "show", "feature/solo:.task/archive/feature-solo.md").stdout, text) + again = self.task(wt, "close", "--self", check=False, env=self.self_close_env()) + self.assertNotEqual(again.returncode, 0, again.stdout) + self.assertEqual(self.git(wt, "rev-parse", "HEAD").stdout.strip(), head) + + def test_close_self_accepts_explicit_pr_number_without_lookup(self): + wt = self.start_standalone() + env = self.close_env() | {"TASK_PR": "42", "GITEA_API_URL": "http://127.0.0.1:9/api/v1"} + result = self.task(wt, "close", "--self", env=env) + self.assertIn("PR #42 合入前闭合", result.stdout) + self.assertIn("任务主记录:PR #42 ", (wt / ".task/archive/feature-solo.md").read_text()) + + def test_close_self_refuses_main_detached_dirty_and_missing_pr(self): + main_head = self.git(self.repo, "rev-parse", "HEAD").stdout.strip() + on_main = self.task(self.repo, "close", "--self", check=False, env=self.self_close_env()) + self.assertNotEqual(on_main.returncode, 0, on_main.stdout) + self.assertIn("不能在 main 上运行", on_main.stdout) + self.assertEqual(self.git(self.repo, "rev-parse", "HEAD").stdout.strip(), main_head) + wt = self.start_standalone() + head = self.git(wt, "rev-parse", "HEAD").stdout.strip() + card = wt / ".task/feature-solo.md" + original = card.read_text() + + def assert_refused(result, reason): + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertIn(reason, result.stdout) + self.assertNotIn(self.gitea.token, result.stdout) + self.assertEqual(self.git(wt, "rev-parse", "HEAD").stdout.strip(), head) + self.assertEqual(card.read_text(), original) + self.assertFalse((wt / ".task/archive").exists()) + + (wt / "stray").write_text("uncommitted\n") + assert_refused(self.task(wt, "close", "--self", check=False, env=self.self_close_env()), "不干净") + (wt / "stray").unlink() + assert_refused(self.task(wt, "close", "--self", check=False, env=self.self_close_env()), + "需要恰好一个 head 为 feature/solo 的开放 PR") + assert_refused(self.task(wt, "close", "--self", check=False, + env=self.self_close_env() | {"TASK_PR": "7; rm"}), "TASK_PR") + self.git(wt, "checkout", "-q", "--detach") + assert_refused(self.task(wt, "close", "--self", check=False, env=self.self_close_env()), "detached HEAD") + + def test_close_self_child_keeps_initiative_target_from_archive(self): + parent = self.start_initiative() + wt, _ = self.start_child() + number = self.open_pr(wt, "feature/alpha-login", base="feature/alpha") + self.task(wt, "close", "--self", env=self.self_close_env()) + archived = wt / ".task/archive/feature-alpha-login.md" + self.assertIn("immediate_target:feature/alpha", archived.read_text()) + self.assertIn(f"PR #{number}", archived.read_text()) + (parent / "parent-later").write_text("later initiative work\n") + self.git(parent, "add", "parent-later") + self.git(parent, "commit", "-m", "Later initiative work") + self.publish_initiative("feature/alpha") + parent_head = self.git(parent, "rev-parse", "HEAD").stdout.strip() + synced = self.task(wt, "sync") + self.assertIn("sync_target=feature/alpha", synced.stdout) + self.assertIn(f"sync_base={parent_head}", synced.stdout) + self.assertIn(f"| 同步(记录前对象) | {parent_head} |", archived.read_text()) + self.git(parent, "merge", "--no-ff", "feature/alpha-login", "-m", "Merge child into initiative") + self.publish_initiative("feature/alpha") + finished = self.task(wt, "finish") + self.assertIn("[verified] feature/alpha-login is included in parent initiative feature/alpha", + finished.stdout) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_task_pr_check.py b/tests/test_task_pr_check.py index 4454fd26..cb9b7c34 100644 --- a/tests/test_task_pr_check.py +++ b/tests/test_task_pr_check.py @@ -396,6 +396,42 @@ class PrCheckTests(unittest.TestCase): self.assertEqual(result.returncode, code, result.out) self.assertIn(text, result.out) + def archive_child_card(self, status): + """把子任务卡移到 .task/archive/ 并改写状态(模拟 close --self 合入前折叠)。""" + self.build_child() + top = self.repo / ".task" / "feature-init-child.md" + text = top.read_text(encoding="utf-8").replace(";状态:进行中;", f";状态:{status};") + archive = self.repo / ".task" / "archive" + archive.mkdir(exist_ok=True) + (archive / top.name).write_text(text, encoding="utf-8") + top.unlink() + self.git(self.repo, "add", "-A", ".task") + self.git(self.repo, "commit", "-m", "Archive child card") + + def test_ci_mode_reads_pre_merge_closed_card_from_archive(self): + self.archive_child_card("已完成(合入前闭合)") + result = self.ci(base="main", head="feature/init-child") + self.assertEqual(result.returncode, 1, result.out) + self.assertIn("expected 'feature/init' for child branch feature/init-child", result.out) + result = self.ci(base="feature/init", head="feature/init-child") + self.assertEqual(result.returncode, 0, result.out) + self.assertIn("[verdict] PR_CHECK_OK base=feature/init head=feature/init-child kind=child", result.out) + + def test_ci_mode_ignores_archived_card_with_other_status(self): + self.archive_child_card("已完成(回填)") + result = self.ci(base="feature/init", head="feature/init-child") + self.assertEqual(result.returncode, 1, result.out) + self.assertIn("expected 'main' for standalone branch feature/init-child", result.out) + + def test_ci_mode_rejects_ambiguous_archived_cards(self): + self.archive_child_card("已完成(合入前闭合)") + archive = self.repo / ".task" / "archive" + (archive / "copy.md").write_text((archive / "feature-init-child.md").read_text(encoding="utf-8"), + encoding="utf-8") + result = self.ci(base="feature/init", head="feature/init-child") + self.assertEqual(result.returncode, 1, result.out) + self.assertIn("ambiguous task cards for feature/init-child", result.out) + if __name__ == "__main__": unittest.main() From b7f6a8bb9ce1515dae4189a658fefc6352b8fc69 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 02:29:25 +0000 Subject: [PATCH 05/18] agent-exec transport e4ff44791a4210144c6165ceadebb342/692536c1c7d0 --- AGENTS.md | 2 + Makefile | 10 +- scripts/check-worktree.py | 2 +- scripts/ci_wait.py | 159 +++++++++++++++++++++++++ scripts/task.sh | 4 +- scripts/task_triage.py | 5 +- tests/test_check_worktree.py | 36 ++++++ tests/test_ci_wait.py | 219 +++++++++++++++++++++++++++++++++++ 8 files changed, 430 insertions(+), 7 deletions(-) create mode 100644 scripts/ci_wait.py create mode 100644 tests/test_check_worktree.py create mode 100644 tests/test_ci_wait.py diff --git a/AGENTS.md b/AGENTS.md index 01e23625..d3cd45ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,6 +153,8 @@ policy/delivery.md。 - 主题行:英文祈使句一行(≤100 列,不以句点结尾);body 写 `Why: / Evidence: / Impact: / Refs:` 四段;任务卡与 PR 描述为同一份文档。 - 跨仓 gitlink 变更须经组合验证(`make test`)后在 Dev 仓推进;受保护分支经 PR 合入。 +- **等待 CI** 只用一条阻塞命令 `make ci-wait SHA=<完整 SHA>`(15 s 轮询、失败即停、默认 20 分钟超时); + 禁止手写 `sleep` + `curl` 轮询循环,禁止对同一 SHA 重复/并行轮询;等待期间去做其他独立工作。 - **高后果操作双钥匙**:改写历史、解除/变更分支保护、删除归档引用、发布版本——Agent 只准备 方案与命令,由用户确认并亲手执行;Agent 不得自行实施(baseline 硬边界 1 的授权范围要求)。 - 模块边界、公开契约与隔离档位见 `policy/modules/`(如有)与 PROJECT.md。 diff --git a/Makefile b/Makefile index d1fd4f17..f63b68b2 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ PLUGINS := plugins COMBO := core=$(shell git -C $(CORE) rev-parse --short HEAD 2>/dev/null)+backend=$(shell git -C $(BACKEND) rev-parse --short HEAD 2>/dev/null)+frontend=$(shell git -C $(FRONTEND) rev-parse --short HEAD 2>/dev/null)+ui=$(shell git -C $(UI) rev-parse --short HEAD 2>/dev/null)+agentcmd=$(shell git -C deps/agent-command rev-parse --short HEAD 2>/dev/null)+documents=$(shell git -C modules/documents rev-parse --short HEAD 2>/dev/null)+native=$(shell git -C deps/agent-native rev-parse --short HEAD 2>/dev/null)+squad=$(shell git -C deps/agent-squad rev-parse --short HEAD 2>/dev/null)+intent=$(shell git -C deps/agent-intent rev-parse --short HEAD 2>/dev/null) VERSION := $(shell git describe --tags --always --dirty 2>/dev/null)+$(COMBO) -.PHONY: help sync status build frontend svc svc-list svc-info svc-commits svc-history svc-update svc-recover svc-watchdog test-backend test-backend-pg test-multi-instance test-lifecycle test-frontend test-ui test-core test-agentcmd test-pins test-libs dev pin clean setup precheck task-triage task-triage-decide task-start task-resume task-sync task-close task-finish task-pr-check facts-verify evidence version agent-exec-validate agent-exec-apply agent-exec-admit +.PHONY: help sync status build frontend svc svc-list svc-info svc-commits svc-history svc-update svc-recover svc-watchdog test-backend test-backend-pg test-multi-instance test-lifecycle test-frontend test-ui test-core test-agentcmd test-pins test-libs dev pin clean setup precheck task-triage task-triage-decide task-start task-resume task-sync task-close task-finish task-pr-check ci-wait facts-verify evidence version agent-exec-validate agent-exec-apply agent-exec-admit version: @echo "agent-system $(VERSION)" @@ -32,6 +32,7 @@ export TASK_INITIATIVE setup: git config core.hooksPath scripts/hooks + git config submodule.fetchJobs 8 chmod +x scripts/hooks/pre-commit scripts/hooks/post-merge scripts/task.sh scripts/evidence.sh scripts/facts-verify.sh scripts/agent-exec @echo "OK: pre-commit/post-merge 钩子已启用(core.hooksPath=scripts/hooks)" @@ -66,6 +67,10 @@ task-finish: task-pr-check: python3 scripts/task_pr_check.py local $(if $(PR),--pr $(PR)) $(if $(FINAL),--final) +# CI 等待(单条阻塞命令):轮询 Gitea 合并状态,失败即停;两次连续全绿才算成功。默认 SHA=HEAD、20 分钟超时。 +ci-wait: + python3 scripts/ci_wait.py $(if $(SHA),--sha $(SHA)) $(if $(TIMEOUT),--timeout $(TIMEOUT)) $(if $(INTERVAL),--interval $(INTERVAL)) $(if $(CONTEXTS),--contexts $(CONTEXTS)) + # 证据写入:执行命令并把结果行追加进当前任务卡(AGENTS.md「证据与验证门禁」)。 # 本地执行只记 SELF;RECEIPT 捕获启动收据,IMPORT_RECEIPT 导入原始收据而不重跑。 # 用法:CMD="<单行命令>" make evidence [TAIL=15];退出码随被执行命令。 @@ -100,6 +105,7 @@ help: @echo "make task-triage-decide TRIAGE_RECEIPT= VERDICT=JOIN|NEW [INITIATIVE=] REASON='<理由>' 记录显式分流决策(不按数量自动 JOIN)" @echo "make task-start NAME=feature/ TRIAGE_RECEIPT= [KIND=...] [INITIATIVE=...] 建任务前核验分流收据(失败不建分支)" @echo "make task-pr-check [PR=N] [FINAL=1] 核验当前任务分支 PR 的 base/head 门禁(远端为准;FINAL=1 用于 initiative 收口)" + @echo "make ci-wait [SHA=<完整 SHA>] [TIMEOUT=1200] [INTERVAL=15] [CONTEXTS=a,b] 阻塞等待该 SHA 的 CI 结论(失败即停)" agent-exec-validate: scripts/agent-exec validate --repo . @@ -112,7 +118,7 @@ agent-exec-admit: scripts/agent-exec admit --repo . sync: - git submodule update --init --recursive + git submodule update --init --recursive --jobs 8 status: @git submodule status diff --git a/scripts/check-worktree.py b/scripts/check-worktree.py index a9503cc4..cba774c0 100644 --- a/scripts/check-worktree.py +++ b/scripts/check-worktree.py @@ -97,7 +97,7 @@ def materialize(repo): for child, expected in submodules(repo): check_children(repo, "before-update") result = subprocess.run(["git", "-C", str(repo), "submodule", "update", "--init", - "--checkout", "--", str(child.relative_to(repo))]) + "--jobs", "8", "--checkout", "--", str(child.relative_to(repo))]) if result.returncode: raise RuntimeError(f"component materialization failed: {child}; preserve and reconcile before retrying") materialize(child) diff --git a/scripts/ci_wait.py b/scripts/ci_wait.py new file mode 100644 index 00000000..6d4b712d --- /dev/null +++ b/scripts/ci_wait.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""CI 等待:以单条阻塞命令代替手写 `sleep N; curl` 轮询。 + +用法:ci_wait.py [--sha SHA] [--interval SECONDS] [--timeout SECONDS] [--contexts NAME[,NAME...]] + --sha 缺省为 `git rev-parse HEAD`,须解析为完整 40 位 SHA;--interval 缺省 15(最小 5);--timeout 缺省 1200。 + +轮询 Gitea 合并状态 GET {api}/repos/{owner}/{repo}/commits/{sha}/status: + - 任一状态为 failure/error:逐条打印 `[ci-wait] FAIL `,立即退出 1; + - 成功须同时满足:至少一条状态、全部 success、每个 --contexts 名称是某个 context 的子串, + 且连续两次轮询观察到同一全绿集合(防止迟注册的 job 被提前判绿); + - 超时:打印最后观察到的状态,退出 2; + - 远端/API/token 问题:`[verdict] REMOTE_UNVERIFIED`,退出 6(瞬时错误在超时内重试, + 截止前从未成功轮询才以 6 放弃)。 +状态变化时才打印一行,结束时打印 `[verdict] CI_SUCCESS|CI_FAILED|CI_TIMEOUT sha=`。 + +退出码:0 CI_SUCCESS;1 CI_FAILED;2 CI_TIMEOUT 或用法错误;6 REMOTE_UNVERIFIED。 +仅使用标准库,复用 task_triage 的 Gitea 客户端与 token 读取。token 不打印、不写入任何产物。 +""" +import argparse +import re +import subprocess +import sys +import time +import urllib.parse + +from task_triage import EXIT_REMOTE, Stop, api_base, http_get_json, read_token, repo_slug + +EXIT_FAILED = 1 +EXIT_TIMEOUT = 2 +EXIT_USAGE = 2 +SHA_RE = re.compile(r"^[0-9a-f]{40}$") +FAILING = ("failure", "error") +FATAL_HTTP = (400, 401, 403, 404, 410) + + +def git(*args): + proc = subprocess.run(["git", *args], capture_output=True, text=True, encoding="utf-8", errors="replace") + return proc.stdout.strip() if proc.returncode == 0 else None + + +def resolve_sha(sha): + value = (sha or "HEAD").strip() + if not SHA_RE.match(value.lower()): + value = git("rev-parse", "--verify", "--quiet", f"{value}^{{commit}}") or "" + value = value.lower() + if not SHA_RE.match(value): + raise Stop(EXIT_USAGE, f"--sha {sha or 'HEAD'} does not resolve to a full 40-hex SHA") + return value + + +def interval_arg(text): + value = float(text) + if value < 5: + raise argparse.ArgumentTypeError("interval must be at least 5 seconds") + return value + + +def timeout_arg(text): + value = float(text) + if value <= 0: + raise argparse.ArgumentTypeError("timeout must be positive") + return value + + +def observe(data): + """[(context, state, target_url)],按 context 排序;忽略非 dict 项。""" + items = data.get("statuses") if isinstance(data, dict) else None + rows = [] + for item in items if isinstance(items, list) else []: + if not isinstance(item, dict): + continue + state = str(item.get("status") or item.get("state") or "").strip().lower() or "unknown" + rows.append((str(item.get("context") or "(no context)"), state, str(item.get("target_url") or ""))) + return sorted(rows) + + +def describe(rows): + return " ".join(f"{ctx}={state}" for ctx, state, _ in rows) if rows else "(no statuses yet)" + + +def green_key(rows, contexts): + """满足全绿与 --contexts 时返回可比较的集合键,否则 None。""" + if not rows or any(state != "success" for _, state, _ in rows): + return None + if any(not any(name in ctx for ctx, _, _ in rows) for name in contexts): + return None + return tuple((ctx, state) for ctx, state, _ in rows) + + +def wait(sha, interval, timeout, contexts, sleep, clock): + token = read_token() + if not token: + raise Stop(EXIT_REMOTE, "no Gitea token (GITEA_TOKEN or ~/.secrets/gitea ACCESS_TOKEN=)") + origin = git("remote", "get-url", "origin") + if not origin: + raise Stop(EXIT_REMOTE, "cannot read origin remote URL") + owner, name = (urllib.parse.quote(part, safe="") for part in repo_slug(origin).split("/")) + url = f"{api_base()}/repos/{owner}/{name}/commits/{sha}/status" + deadline = clock() + timeout + rows, shown, previous_green, polled, last_error = [], None, None, False, None + while True: + try: + data = http_get_json(url, token) + except Stop as stop: + if stop.status in FATAL_HTTP: + raise + last_error = stop.message + else: + polled = True + rows = observe(data) + line = describe(rows) + if line != shown: + print(f"[ci-wait] sha={sha[:12]} {line}", flush=True) + shown = line + failing = [row for row in rows if row[1] in FAILING] + if failing: + for ctx, _, target in failing: + print(f"[ci-wait] FAIL {ctx} {target or '(no target_url)'}") + print(f"[verdict] CI_FAILED sha={sha}") + return EXIT_FAILED + key = green_key(rows, contexts) + if key is not None and key == previous_green: + print(f"[verdict] CI_SUCCESS sha={sha}") + return 0 + previous_green = key + remaining = deadline - clock() + if remaining <= 0: + break + sleep(min(interval, remaining)) + if not polled: + raise Stop(EXIT_REMOTE, last_error or "no successful status poll before the deadline") + print(f"[ci-wait] timeout after {timeout:g}s; last observed: {describe(rows)}") + missing = [n for n in contexts if not any(n in ctx for ctx, _, _ in rows)] + if missing: + print(f"[ci-wait] missing contexts: {', '.join(missing)}") + print(f"[verdict] CI_TIMEOUT sha={sha}") + return EXIT_TIMEOUT + + +def main(argv=None, sleep=time.sleep, clock=time.monotonic): + parser = argparse.ArgumentParser(prog="ci_wait.py", description="阻塞等待指定 SHA 的 Gitea CI 结论") + parser.add_argument("--sha") + parser.add_argument("--interval", type=interval_arg, default=15.0) + parser.add_argument("--timeout", type=timeout_arg, default=1200.0) + parser.add_argument("--contexts", default="") + args = parser.parse_args(argv) + contexts = [name.strip() for name in args.contexts.split(",") if name.strip()] + try: + sha = resolve_sha(args.sha) + return wait(sha, args.interval, args.timeout, contexts, sleep, clock) + except Stop as stop: + print(f"[ci-wait] ERROR {stop.message}") + if stop.code == EXIT_REMOTE: + print("[verdict] REMOTE_UNVERIFIED") + return stop.code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/task.sh b/scripts/task.sh index ac0d6d28..c5ddbbac 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -230,7 +230,7 @@ cmd_start() { git worktree add -q "$wt" -b "$name" "$start_ref" # 物化后按 submodule status 实际状态核对,不只信退出码:网络抖动或嵌套指针 # 漂移时 update 可能静默留坑(2026-09-30 实测深嵌套层带病通过,构建期才炸)。 - (cd "$wt" && git submodule update --init --recursive >/dev/null 2>&1) \ + (cd "$wt" && git submodule update --init --recursive --jobs 8 >/dev/null 2>&1) \ || echo "[warn] submodule update 非零退出,见下方状态核对" local sub_drift sub_drift=$(git -C "$wt" submodule status --recursive 2>/dev/null | grep -cE '^[-+U]' || true) @@ -956,7 +956,7 @@ cmd_finish() { return 1 fi # Keep card closure separate: its push/PR side effects are not implied by local disposal. - echo "[next] inspect task-card closure with make task-close in the main checkout when authorized" + echo "[next] cards are normally fold-closed before merge with make task-close SELF=1; only legacy cards reported by precheck [cards] need make task-close in the main checkout when authorized" } case "${1:-}" in diff --git a/scripts/task_triage.py b/scripts/task_triage.py index acf83b24..32c03fd1 100644 --- a/scripts/task_triage.py +++ b/scripts/task_triage.py @@ -51,10 +51,11 @@ EXIT_VERIFY = 8 class Stop(Exception): """受控失败:退出码 + 不含 token 的说明。""" - def __init__(self, code, message): + def __init__(self, code, message, status=None): super().__init__(message) self.code = code self.message = message + self.status = status def utc_now(): @@ -152,7 +153,7 @@ def http_get_json(url, token): with urllib.request.urlopen(request, timeout=20) as response: body = response.read() except urllib.error.HTTPError as exc: - raise Stop(EXIT_REMOTE, f"Gitea API returned HTTP {exc.code}") + raise Stop(EXIT_REMOTE, f"Gitea API returned HTTP {exc.code}", status=exc.code) except (urllib.error.URLError, http.client.HTTPException, OSError) as exc: raise Stop(EXIT_REMOTE, f"Gitea API unreachable ({type(exc).__name__})") try: diff --git a/tests/test_check_worktree.py b/tests/test_check_worktree.py new file mode 100644 index 00000000..bd300932 --- /dev/null +++ b/tests/test_check_worktree.py @@ -0,0 +1,36 @@ +"""scripts/check-worktree.py 的组件物化参数:逐个 submodule 物化,并以 --jobs 8 并行拉取。""" +import importlib.util +import subprocess +import unittest +from pathlib import Path +from unittest import mock + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "check-worktree.py" +spec = importlib.util.spec_from_file_location("check_worktree", SCRIPT) +check_worktree = importlib.util.module_from_spec(spec) +spec.loader.exec_module(check_worktree) + + +class MaterializeTests(unittest.TestCase): + def test_submodule_update_uses_parallel_jobs_per_component(self): + repo = Path("/fixture/repo") + child = repo / "modules" / "core" + calls = [] + + def fake_submodules(path): + return [(child, "a" * 40)] if path == repo else [] + + def fake_run(argv, *args, **kwargs): + calls.append(argv) + return subprocess.CompletedProcess(argv, 0) + + with mock.patch.object(check_worktree, "submodules", fake_submodules), \ + mock.patch.object(check_worktree, "check_children", lambda *a: None), \ + mock.patch.object(check_worktree.subprocess, "run", fake_run): + check_worktree.materialize(repo) + self.assertEqual(calls, [["git", "-C", str(repo), "submodule", "update", "--init", + "--jobs", "8", "--checkout", "--", "modules/core"]]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ci_wait.py b/tests/test_ci_wait.py new file mode 100644 index 00000000..d6fe4b4b --- /dev/null +++ b/tests/test_ci_wait.py @@ -0,0 +1,219 @@ +"""CI 等待(scripts/ci_wait.py):临时 Git 仓库 + 进程内 Gitea 合并状态桩 + 注入的时钟与 sleep。 + +全部离线运行:不联网、不读取真实 ~/.secrets(HOME 指向临时目录),token 为合成值。 +""" +import contextlib +import http.server +import io +import json +import os +import socket +import subprocess +import sys +import tempfile +import threading +import unittest +from pathlib import Path +from unittest import mock + +SOURCE = Path(__file__).resolve().parents[1] +SCRIPT = SOURCE / "scripts" / "ci_wait.py" +sys.path.insert(0, str(SOURCE / "scripts")) + +import ci_wait # noqa: E402 + +TOKEN = "synthetic-gitea-token-not-real" + + +def status(context, state, url=None): + return {"context": context, "status": state, "target_url": url or f"https://ci.invalid/{context}"} + + +class FakeGitea: + """Gitea 合并状态桩(127.0.0.1):校验 token;每次 GET 依次返回脚本中的下一份 statuses(末项重复)。""" + + def __init__(self, token=TOKEN): + self.token = token + self.script = [] + self.requests = [] + stub = self + + class Handler(http.server.BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def _send(self, code, obj): + body = json.dumps(obj).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + if self.headers.get("Authorization") != f"token {stub.token}": + return self._send(401, {"message": "unauthorized"}) + parts = self.path.split("?", 1)[0].split("/") + if parts[1:4] != ["api", "v1", "repos"] or len(parts) != 9 or parts[6] != "commits" \ + or parts[8] != "status": + return self._send(404, {"message": "not found"}) + stub.requests.append("/".join(parts[4:6]) + "@" + parts[7]) + statuses = stub.script.pop(0) if len(stub.script) > 1 else (stub.script or [[]])[0] + return self._send(200, {"sha": parts[7], "statuses": statuses}) + + self.server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler) + self.url = f"http://127.0.0.1:{self.server.server_address[1]}/api/v1" + threading.Thread(target=self.server.serve_forever, daemon=True).start() + + def close(self): + self.server.shutdown() + self.server.server_close() + + +def closed_port_url(): + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + return f"http://127.0.0.1:{port}/api/v1" + + +class FakeClock: + def __init__(self): + self.now = 0.0 + self.sleeps = [] + + def clock(self): + return self.now + + def sleep(self, seconds): + self.sleeps.append(seconds) + self.now += seconds + + +class CiWaitTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory(prefix="ci-wait-") + self.addCleanup(self.tmp.cleanup) + root = Path(self.tmp.name) + self.repo = root / "repo" + env = {k: v for k, v in os.environ.items() + if not k.startswith("GIT_") and k not in {"GITEA_API_URL", "GITEA_REPO", "GITEA_TOKEN"}} + env.update({"GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_TERMINAL_PROMPT": "0", + "GIT_AUTHOR_NAME": "CI Wait Test", "GIT_AUTHOR_EMAIL": "ci-wait@example.invalid", + "GIT_COMMITTER_NAME": "CI Wait Test", "GIT_COMMITTER_EMAIL": "ci-wait@example.invalid", + "HOME": str(root / "no-home"), + "NO_PROXY": "127.0.0.1,localhost", "no_proxy": "127.0.0.1,localhost", + "GITEA_TOKEN": TOKEN}) + self.gitea = FakeGitea() + self.addCleanup(self.gitea.close) + env["GITEA_API_URL"] = self.gitea.url + self.env = env + env_patch = mock.patch.dict(os.environ, env, clear=True) + env_patch.start() + self.addCleanup(env_patch.stop) + self.git(root, "init", "--initial-branch=main", str(self.repo)) + (self.repo / "README").write_text("fixture\n") + self.git(self.repo, "add", "README") + self.git(self.repo, "commit", "-m", "Create fixture") + self.git(self.repo, "remote", "add", "origin", "ssh://git@git-ssh.example.invalid:31411/fixture/repo.git") + self.sha = self.git(self.repo, "rev-parse", "HEAD") + cwd = os.getcwd() + os.chdir(self.repo) + self.addCleanup(os.chdir, cwd) + + def git(self, cwd, *args): + return subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, check=True).stdout.strip() + + def run_wait(self, *args, env=None): + clock = FakeClock() + out = io.StringIO() + with mock.patch.dict(os.environ, env or {}), contextlib.redirect_stdout(out), \ + contextlib.redirect_stderr(out): + code = ci_wait.main(list(args), sleep=clock.sleep, clock=clock.clock) + text = out.getvalue() + self.assertNotIn(TOKEN, text) + return code, text, clock + + def test_success_requires_two_consecutive_all_green_polls(self): + self.gitea.script = [[status("ci/test", "pending")], [status("ci/test", "pending")], + [status("ci/test", "success")], [status("ci/test", "success")]] + code, out, clock = self.run_wait("--timeout", "300") + self.assertEqual(code, 0, out) + self.assertIn(f"[verdict] CI_SUCCESS sha={self.sha}", out) + self.assertEqual(len(self.gitea.requests), 4, out) + self.assertEqual(self.gitea.requests[0], f"fixture/repo@{self.sha}") + self.assertEqual(clock.sleeps, [15.0, 15.0, 15.0], out) + # 状态行只在变化时打印:pending 与 success 各一行。 + lines = [line for line in out.splitlines() if line.startswith("[ci-wait] sha=")] + self.assertEqual(len(lines), 2, out) + + def test_late_registering_job_does_not_succeed_early(self): + self.gitea.script = [[status("ci/test", "success")], + [status("ci/test", "success"), status("ci/build", "pending")], + [status("ci/test", "success"), status("ci/build", "success")], + [status("ci/test", "success"), status("ci/build", "success")]] + code, out, _ = self.run_wait("--timeout", "300") + self.assertEqual(code, 0, out) + self.assertEqual(len(self.gitea.requests), 4, out) + self.assertIn("ci/build=pending", out) + + def test_failure_exits_fast_with_target_url(self): + url = "https://ci.invalid/runs/42" + self.gitea.script = [[status("ci/test", "pending"), status("ci/build", "success")], + [status("ci/test", "failure", url), status("ci/build", "success")]] + code, out, _ = self.run_wait("--sha", self.sha, "--timeout", "300") + self.assertEqual(code, 1, out) + self.assertEqual(len(self.gitea.requests), 2, out) + self.assertIn(f"[ci-wait] FAIL ci/test {url}", out) + self.assertNotIn("FAIL ci/build", out) + self.assertIn(f"[verdict] CI_FAILED sha={self.sha}", out) + + def test_missing_required_context_waits_until_timeout(self): + self.gitea.script = [[status("ci/test", "success")]] + code, out, clock = self.run_wait("--contexts", "ci/test,e2e", "--interval", "5", "--timeout", "30") + self.assertEqual(code, 2, out) + self.assertEqual(clock.now, 30.0) + self.assertIn("last observed: ci/test=success", out) + self.assertIn("missing contexts: e2e", out) + self.assertIn(f"[verdict] CI_TIMEOUT sha={self.sha}", out) + + def test_context_names_match_as_substrings(self): + self.gitea.script = [[status("CI / test (pull_request)", "success")]] + code, out, _ = self.run_wait("--contexts", "test", "--timeout", "60") + self.assertEqual(code, 0, out) + + def test_unreachable_api_exits_6_after_retrying_until_deadline(self): + code, out, clock = self.run_wait("--timeout", "60", env={"GITEA_API_URL": closed_port_url()}) + self.assertEqual(code, 6, out) + self.assertEqual(clock.now, 60.0) + self.assertIn("[verdict] REMOTE_UNVERIFIED", out) + + def test_unauthorized_or_missing_token_exits_6_immediately(self): + code, out, clock = self.run_wait("--timeout", "60", env={"GITEA_TOKEN": "wrong-token"}) + self.assertEqual(code, 6, out) + self.assertEqual(clock.sleeps, [], out) + self.assertIn("HTTP 401", out) + with mock.patch.dict(os.environ): + del os.environ["GITEA_TOKEN"] + code, out, _ = self.run_wait("--timeout", "60") + self.assertEqual(code, 6, out) + self.assertIn("[verdict] REMOTE_UNVERIFIED", out) + + def test_sha_must_resolve_to_full_sha(self): + code, out, _ = self.run_wait("--sha", "not-a-ref") + self.assertEqual(code, 2, out) + self.assertIn("full 40-hex SHA", out) + self.assertEqual(self.gitea.requests, []) + + def test_cli_entry_point_reports_remote_unverified(self): + env = self.env | {"GITEA_API_URL": closed_port_url()} + result = subprocess.run([sys.executable, str(SCRIPT), "--interval", "5", "--timeout", "0.2"], + cwd=self.repo, env=env, capture_output=True, text=True, timeout=60) + out = result.stdout + result.stderr + self.assertEqual(result.returncode, 6, out) + self.assertIn("[verdict] REMOTE_UNVERIFIED", out) + self.assertNotIn(TOKEN, out) + + +if __name__ == "__main__": + unittest.main() From fb4c89d9dca99d718e4be4459cf3dfa7903cc0b5 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 02:37:50 +0000 Subject: [PATCH 06/18] agent-exec transport 92d779408cc72a3fdf9fdf6f005e9b5e/09f250401f29 --- .gitea/workflows/ci.yaml | 6 ++ Makefile | 6 +- scripts/ci_wait.py | 8 +- scripts/test_affected.py | 141 +++++++++++++++++++++++++++++++++ tests/test_ci_wait.py | 16 ++++ tests/test_test_affected.py | 150 ++++++++++++++++++++++++++++++++++++ 6 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 scripts/test_affected.py create mode 100644 tests/test_test_affected.py diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 74fc560c..a70ae342 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -186,6 +186,9 @@ jobs: # 缺服务时失败而非跳过(make test-backend-pg 硬校验环境变量); # 组件版本随 job 输出固定,供验收记录对齐(方案 KTD12/U2)。 test-pg: + # 分级门禁:base 非 main 的 PR(L2 child → L1 initiative)只跑轻量 CI(test 作业); + # 进入 main 的 PR 与 main 推送跑全量门禁。 + if: ${{ github.event_name != 'pull_request' || github.base_ref == 'main' }} runs-on: ubuntu-latest env: GOPROXY: https://goproxy.cn,direct @@ -479,6 +482,9 @@ jobs: # identity 领养/verify、release 切换、伴生工作台、RDM 全轨部署。缺件即失败, # 不静默跳过(SVC_TEST_REAL=1 硬开关)。 svc-realcombo: + # 分级门禁:base 非 main 的 PR(L2 child → L1 initiative)只跑轻量 CI(test 作业); + # 进入 main 的 PR 与 main 推送跑全量门禁。 + if: ${{ github.event_name != 'pull_request' || github.base_ref == 'main' }} runs-on: ubuntu-latest env: GOPROXY: https://goproxy.cn,direct diff --git a/Makefile b/Makefile index f63b68b2..66ef7dea 100644 --- a/Makefile +++ b/Makefile @@ -205,7 +205,11 @@ plugin-new: @test -n "$(NAME)" || (echo "用法:make plugin-new NAME="; exit 2) bash scripts/plugin-new.sh "$(NAME)" -.PHONY: test test-governance test-worktree contracts +.PHONY: test test-governance test-worktree test-affected contracts + +# host 最小直接检查:按 BASE(缺省为卡片 immediate_target 或 origin/main)以来的改动选 make 目标;DRY=1 只打印。 +test-affected: + python3 scripts/test_affected.py $(if $(BASE),--base $(BASE)) $(if $(DRY),--dry-run) test-governance: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p 'test_*.py' -v diff --git a/scripts/ci_wait.py b/scripts/ci_wait.py index 6d4b712d..813cda27 100644 --- a/scripts/ci_wait.py +++ b/scripts/ci_wait.py @@ -6,7 +6,7 @@ 轮询 Gitea 合并状态 GET {api}/repos/{owner}/{repo}/commits/{sha}/status: - 任一状态为 failure/error:逐条打印 `[ci-wait] FAIL `,立即退出 1; - - 成功须同时满足:至少一条状态、全部 success、每个 --contexts 名称是某个 context 的子串, + - 成功须同时满足:至少一条状态、全部 success(skipped/warning 视同 success,不算失败)、每个 --contexts 名称是某个 context 的子串, 且连续两次轮询观察到同一全绿集合(防止迟注册的 job 被提前判绿); - 超时:打印最后观察到的状态,退出 2; - 远端/API/token 问题:`[verdict] REMOTE_UNVERIFIED`,退出 6(瞬时错误在超时内重试, @@ -30,6 +30,8 @@ EXIT_TIMEOUT = 2 EXIT_USAGE = 2 SHA_RE = re.compile(r"^[0-9a-f]{40}$") FAILING = ("failure", "error") +# skipped:分级门禁下 base 非 main 的 PR 跳过全量作业;与 warning 一样是非失败终态。 +DONE_OK = ("success", "skipped", "warning") FATAL_HTTP = (400, 401, 403, 404, 410) @@ -79,8 +81,8 @@ def describe(rows): def green_key(rows, contexts): - """满足全绿与 --contexts 时返回可比较的集合键,否则 None。""" - if not rows or any(state != "success" for _, state, _ in rows): + """满足全绿(success/skipped/warning)与 --contexts 时返回可比较的集合键,否则 None。""" + if not rows or any(state not in DONE_OK for _, state, _ in rows): return None if any(not any(name in ctx for ctx, _, _ in rows) for name in contexts): return None diff --git a/scripts/test_affected.py b/scripts/test_affected.py new file mode 100644 index 00000000..e1567813 --- /dev/null +++ b/scripts/test_affected.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""按分支改动选择最小直接检查:host 实施中的 make 目标子集(完整套件仍以 CI 为准)。 + +用法:test_affected.py [--base REF] [--dry-run] + base:--base,否则环境变量 BASE,否则当前分支卡片的 immediate_target(task_pr_check 助手), + 再否则 origin/main;裸分支名在 origin/ 存在时解析为 origin/。 + +改动路径 = `git diff ...HEAD` ∪ 未提交的已跟踪改动(相对 HEAD)。路径到目标: + scripts/** tests/** .gitea/** policy/** Makefile AGENTS.md AGENT-EXECUTION.md PROJECT.md → test-governance + modules/{core,backend,frontend,ui} → test-{core,backend,frontend,ui};deps/agent-command → test-agentcmd; + deps/agent-{intent,native,squad} → test-libs;gitlink 本身或 .gitmodules → test-pins; + modules/** 或 scripts/lint-boundaries.sh → test-boundaries;其余(.task/**、docs/**、其他 *.md)不选。 +选中任一目标时先跑 test-worktree;单次 `make ` 执行,退出码随 make。 + +退出码:make 的退出码;无目标或 --dry-run 为 0;用法/ref 错误为 2。仅使用标准库。 +""" +import argparse +import os +import subprocess +import sys + +DEFAULT_BASE = "origin/main" +EXIT_USAGE = 2 +GITLINK_MODE = "160000" +ORDER = ("test-governance", "test-boundaries", "test-pins", "test-libs", "test-core", + "test-backend", "test-frontend", "test-ui", "test-agentcmd") +GOVERNANCE_DIRS = ("scripts/", "tests/", ".gitea/", "policy/") +GOVERNANCE_FILES = ("Makefile", "AGENTS.md", "AGENT-EXECUTION.md", "PROJECT.md") +COMPONENTS = { + "modules/core": "test-core", + "modules/backend": "test-backend", + "modules/frontend": "test-frontend", + "modules/ui": "test-ui", + "deps/agent-command": "test-agentcmd", + "deps/agent-intent": "test-libs", + "deps/agent-native": "test-libs", + "deps/agent-squad": "test-libs", +} + + +class Stop(Exception): + pass + + +def git(*args): + proc = subprocess.run(["git", *args], capture_output=True, text=True, encoding="utf-8", errors="replace") + if proc.returncode != 0: + raise Stop(f"git {' '.join(args)} failed: {proc.stderr.strip() or proc.returncode}") + return proc.stdout + + +def ref_exists(ref): + return subprocess.run(["git", "rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}"], + capture_output=True).returncode == 0 + + +def card_target(): + """当前分支卡片的 immediate_target;无分支/无助手/无卡时返回 None。""" + try: + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + import task_pr_check + meta, text = task_pr_check.branch_meta(task_pr_check.current_branch(), "main") + except Exception: + return None + return meta.get("immediate_target") if text is not None else None + + +def resolve_base(requested): + name = (requested or os.environ.get("BASE", "")).strip() or card_target() or DEFAULT_BASE + if not name.startswith("origin/") and ref_exists(f"refs/remotes/origin/{name}"): + name = f"origin/{name}" + if not ref_exists(name): + raise Stop(f"base {name} does not resolve to a commit") + return name + + +def raw_changes(*diff_args): + """{path: is_gitlink},取自 `git diff --raw -z --no-renames`。""" + out = git("diff", "--raw", "-z", "--no-renames", *diff_args) + fields = out.split("\0") + changes = {} + for meta, path in zip(fields[0::2], fields[1::2]): + modes = meta.lstrip(":").split()[:2] + changes[path] = changes.get(path, False) or GITLINK_MODE in modes + return changes + + +def changed_paths(base): + merge_base = git("merge-base", base, "HEAD").strip() + changes = raw_changes(merge_base, "HEAD") + for path, gitlink in raw_changes("HEAD").items(): + changes[path] = changes.get(path, False) or gitlink + return changes + + +def targets_for(path, gitlink): + found = set() + if path.startswith(GOVERNANCE_DIRS) or path in GOVERNANCE_FILES: + found.add("test-governance") + for prefix, target in COMPONENTS.items(): + if path == prefix or path.startswith(prefix + "/"): + found.add(target) + if gitlink or path == ".gitmodules": + found.add("test-pins") + if path.startswith("modules/") or path == "scripts/lint-boundaries.sh": + found.add("test-boundaries") + return found + + +def select(changes): + found = set() + for path, gitlink in changes.items(): + found |= targets_for(path, gitlink) + return ["test-worktree", *(t for t in ORDER if t in found)] if found else [] + + +def main(argv=None): + parser = argparse.ArgumentParser(prog="test_affected.py", description="按分支改动运行最小直接检查") + parser.add_argument("--base") + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args(argv) + try: + os.chdir(git("rev-parse", "--show-toplevel").strip()) + base = resolve_base(args.base) + changes = changed_paths(base) + except Stop as stop: + print(f"[affected] ERROR {stop}") + return EXIT_USAGE + targets = select(changes) + print(f"[affected] base={base} changed={len(changes)} targets={','.join(targets) or 'none'}", flush=True) + if not targets: + print("[affected] nothing to test") + return 0 + if args.dry_run: + print(f"[affected] dry-run: make {' '.join(targets)}") + return 0 + return subprocess.run(["make", *targets]).returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_ci_wait.py b/tests/test_ci_wait.py index d6fe4b4b..79c5275d 100644 --- a/tests/test_ci_wait.py +++ b/tests/test_ci_wait.py @@ -177,6 +177,22 @@ class CiWaitTests(unittest.TestCase): self.assertIn("missing contexts: e2e", out) self.assertIn(f"[verdict] CI_TIMEOUT sha={self.sha}", out) + def test_skipped_and_warning_contexts_count_as_done_not_failed(self): + self.gitea.script = [[status("ci/test", "pending"), status("ci/test-pg", "skipped")], + [status("ci/test", "success"), status("ci/test-pg", "skipped"), + status("ci/svc-realcombo", "warning")]] + code, out, _ = self.run_wait("--contexts", "test-pg", "--timeout", "300") + self.assertEqual(code, 0, out) + self.assertEqual(len(self.gitea.requests), 3, out) + self.assertNotIn("FAIL", out) + self.assertIn(f"[verdict] CI_SUCCESS sha={self.sha}", out) + + def test_skipped_does_not_mask_a_pending_context(self): + self.gitea.script = [[status("ci/test", "pending"), status("ci/test-pg", "skipped")]] + code, out, _ = self.run_wait("--interval", "5", "--timeout", "30") + self.assertEqual(code, 2, out) + self.assertIn(f"[verdict] CI_TIMEOUT sha={self.sha}", out) + def test_context_names_match_as_substrings(self): self.gitea.script = [[status("CI / test (pull_request)", "success")]] code, out, _ = self.run_wait("--contexts", "test", "--timeout", "60") diff --git a/tests/test_test_affected.py b/tests/test_test_affected.py new file mode 100644 index 00000000..7eceb319 --- /dev/null +++ b/tests/test_test_affected.py @@ -0,0 +1,150 @@ +"""按改动选检查(scripts/test_affected.py):临时 Git 仓库 + 本地裸 origin,全部离线。""" +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +SOURCE = Path(__file__).resolve().parents[1] +SCRIPT = SOURCE / "scripts" / "test_affected.py" + + +class TestAffectedTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory(prefix="test-affected-") + self.addCleanup(self.tmp.cleanup) + root = Path(self.tmp.name) + self.repo = root / "repo" + self.env = {k: v for k, v in os.environ.items() + if not k.startswith(("GIT_", "MAKE")) and k not in {"BASE", "TASK_TARGET_BRANCH"}} + self.env.update({"GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_TERMINAL_PROMPT": "0", "HOME": str(root / "no-home"), + "GIT_AUTHOR_NAME": "Affected Test", "GIT_AUTHOR_EMAIL": "affected@example.invalid", + "GIT_COMMITTER_NAME": "Affected Test", + "GIT_COMMITTER_EMAIL": "affected@example.invalid"}) + self.git(root, "init", "-q", "--bare", "--initial-branch=main", str(root / "origin.git")) + self.git(root, "init", "-q", "--initial-branch=main", str(self.repo)) + self.git(self.repo, "remote", "add", "origin", str(root / "origin.git")) + self.write("README.md", "fixture\n") + self.write("modules/ui/notes.txt", "ui\n") + self.commit("Create fixture") + self.git(self.repo, "push", "-q", "origin", "main") + self.git(self.repo, "checkout", "-q", "-b", "feature/work") + + def git(self, cwd, *args): + return subprocess.run(["git", *args], cwd=cwd, env=self.env, capture_output=True, text=True, + check=True).stdout.strip() + + def write(self, rel, text): + path = self.repo / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + def commit(self, message, gitlinks=()): + self.git(self.repo, "add", "-A") + sha = self.git(self.repo, "rev-parse", "HEAD") if gitlinks else None + for path in gitlinks: + self.git(self.repo, "update-index", "--add", "--cacheinfo", f"160000,{sha},{path}") + self.git(self.repo, "commit", "-q", "-m", message) + + def run_affected(self, *args, base=None): + env = self.env | ({"BASE": base} if base is not None else {}) + result = subprocess.run([sys.executable, str(SCRIPT), *args], cwd=self.repo, env=env, + capture_output=True, text=True, timeout=60) + return result.returncode, result.stdout + result.stderr + + def summary(self, out): + lines = [line for line in out.splitlines() if line.startswith("[affected] base=")] + self.assertEqual(len(lines), 1, out) + return dict(part.split("=", 1) for part in lines[0].split()[1:]) + + def test_mapping_dry_run_orders_and_dedupes_targets(self): + self.write("scripts/tool.py", "print('x')\n") + self.write("tests/test_tool.py", "\n") + self.write("modules/core/a.go", "package a\n") + self.write("deps/agent-native/x.go", "package x\n") + self.write("deps/agent-squad/y.go", "package y\n") + self.write("docs/guide.md", "doc\n") + self.write(".task/card.md", "card\n") + self.commit("Branch work", gitlinks=["deps/agent-command"]) + self.write("modules/ui/notes.txt", "uncommitted tracked change\n") + code, out = self.run_affected("--dry-run") + self.assertEqual(code, 0, out) + info = self.summary(out) + self.assertEqual(info["base"], "origin/main") + self.assertEqual(info["changed"], "9", out) + self.assertEqual(info["targets"], "test-worktree,test-governance,test-boundaries,test-pins,test-libs," + "test-core,test-ui,test-agentcmd", out) + self.assertIn("[affected] dry-run: make test-worktree test-governance", out) + + def test_gitlink_and_gitmodules_select_pins_and_module_target(self): + self.write(".gitmodules", "[submodule \"modules/backend\"]\n\tpath = modules/backend\n") + self.commit("Bump backend", gitlinks=["modules/backend"]) + code, out = self.run_affected("--dry-run") + self.assertEqual(code, 0, out) + self.assertEqual(self.summary(out)["targets"], + "test-worktree,test-boundaries,test-pins,test-backend", out) + + def test_lint_boundaries_and_governance_docs_select_targets(self): + self.write("scripts/lint-boundaries.sh", "#!/bin/sh\n") + self.write("AGENT-EXECUTION.md", "x\n") + self.commit("Lint") + code, out = self.run_affected("--dry-run") + self.assertEqual(code, 0, out) + self.assertEqual(self.summary(out)["targets"], "test-worktree,test-governance,test-boundaries", out) + + def test_docs_only_change_runs_nothing(self): + self.write("docs/guide.md", "doc\n") + self.write(".task/card.md", "card\n") + self.write("NOTES.md", "notes\n") + self.commit("Docs only") + self.write("README.md", "edited\n") + code, out = self.run_affected() + self.assertEqual(code, 0, out) + info = self.summary(out) + self.assertEqual((info["changed"], info["targets"]), ("4", "none"), out) + self.assertIn("[affected] nothing to test", out) + self.assertNotIn("dry-run", out) + + def test_base_env_resolves_bare_branch_to_origin(self): + self.write("scripts/tool.py", "x\n") + self.commit("Tool") + code, out = self.run_affected("--dry-run", base="main") + self.assertEqual(code, 0, out) + self.assertEqual(self.summary(out)["base"], "origin/main") + + def test_base_env_local_branch_limits_changes_since_that_base(self): + self.write("modules/core/a.go", "package a\n") + self.commit("Core") + self.git(self.repo, "branch", "local-base") + self.write("docs/later.md", "later\n") + self.commit("Later docs") + code, out = self.run_affected("--dry-run", base="local-base") + self.assertEqual(code, 0, out) + info = self.summary(out) + self.assertEqual((info["base"], info["changed"], info["targets"]), ("local-base", "1", "none"), out) + code, out = self.run_affected("--dry-run", "--base", "origin/main", base="local-base") + self.assertEqual(self.summary(out)["base"], "origin/main", out) + + def test_unresolvable_base_is_a_usage_error(self): + code, out = self.run_affected("--dry-run", base="no-such-branch") + self.assertEqual(code, 2, out) + self.assertIn("[affected] ERROR base no-such-branch does not resolve", out) + + def test_default_base_is_card_immediate_target(self): + self.git(self.repo, "push", "-q", "origin", "main:feature/init") + self.git(self.repo, "fetch", "-q", "origin") + self.write(".task/child.md", "# child\n- 工作区:/tmp/x;分支:feature/work\n" + "- 分支类型:child;initiative_id:init;root_target:main;immediate_target:feature/init\n") + self.commit("Card") + code, out = self.run_affected("--dry-run") + self.assertEqual(code, 0, out) + self.assertEqual(self.summary(out)["base"], "origin/feature/init", out) + self.git(self.repo, "checkout", "-q", "--detach") + code, out = self.run_affected("--dry-run") + self.assertEqual(self.summary(out)["base"], "origin/main", out) + + +if __name__ == "__main__": + unittest.main() From 9a3ef83c7f4af152ef8360b50901300eb06bdc09 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 02:45:45 +0000 Subject: [PATCH 07/18] agent-exec transport 8112713bc92d127e3ad4155251672a88/be7ee7a3e957 --- Makefile | 4 +- scripts/task.sh | 142 ++++++++++++++++++++++++++++++-------- tests/test_task.py | 165 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 254 insertions(+), 57 deletions(-) diff --git a/Makefile b/Makefile index 66ef7dea..f95d380d 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ task-triage-decide: @VERDICT="$(VERDICT)" INITIATIVE="$(INITIATIVE)" REASON="$(REASON)" bash $(TASK) triage-decide task-start: - @if [ -z "$(NAME)" ]; then echo "usage: make task-start NAME=feature/ TRIAGE_RECEIPT="; exit 2; fi + @if [ -z "$(NAME)" ]; then echo "usage: make task-start NAME=feature/ TRIAGE_RECEIPT= [PRECHECK_RECEIPT=](KIND=child 已登记在父卡时可省略 TRIAGE_RECEIPT)"; exit 2; fi @bash $(TASK) start "$(NAME)" task-resume: @@ -103,7 +103,7 @@ help: @echo "make agent-exec-admit 校验暂存路径具备 transport 来源或属于 host 豁免" @echo "make task-triage REQ='<需求>' TRIAGE_RECEIPT= 以远端为准发现接纳中的 initiative 并写分流收据(候选存在时 exit 5,待决策)" @echo "make task-triage-decide TRIAGE_RECEIPT= VERDICT=JOIN|NEW [INITIATIVE=] REASON='<理由>' 记录显式分流决策(不按数量自动 JOIN)" - @echo "make task-start NAME=feature/ TRIAGE_RECEIPT= [KIND=...] [INITIATIVE=...] 建任务前核验分流收据(失败不建分支)" + @echo "make task-start NAME=feature/ TRIAGE_RECEIPT= [KIND=...] [INITIATIVE=...] [PRECHECK_RECEIPT=] 建任务前核验分流收据(失败不建分支);KIND=child 已登记在父卡「子任务登记」时可省略 TRIAGE_RECEIPT;省略 PRECHECK_RECEIPT 时自动运行启动检查" @echo "make task-pr-check [PR=N] [FINAL=1] 核验当前任务分支 PR 的 base/head 门禁(远端为准;FINAL=1 用于 initiative 收口)" @echo "make ci-wait [SHA=<完整 SHA>] [TIMEOUT=1200] [INTERVAL=15] [CONTEXTS=a,b] 阻塞等待该 SHA 的 CI 结论(失败即停)" diff --git a/scripts/task.sh b/scripts/task.sh index c5ddbbac..1a2b8aac 100755 --- a/scripts/task.sh +++ b/scripts/task.sh @@ -152,29 +152,59 @@ cmd_start() { echo "$initiative" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "invalid parent initiative branch" case "${name#*/}" in "${initiative#*/}"-?*) ;; *) die "child slug must start with parent initiative slug plus '-'" ;; esac fi - [ -n "${TRIAGE_RECEIPT:-}" ] || die "TRIAGE_RECEIPT required: make task-triage REQ='<需求>' TRIAGE_RECEIPT= and decide first (no branch created)" + local slug planned_child=0 + slug=$(echo "$name" | tr '/' '-') + if [ -z "${TRIAGE_RECEIPT:-}" ]; then + [ "$kind" = child ] || die "TRIAGE_RECEIPT required: make task-triage REQ='<需求>' TRIAGE_RECEIPT= and decide first (no branch created)" + planned_child=1 + fi fetch_target - [ -n "${PRECHECK_RECEIPT:-}" ] || die "PRECHECK_RECEIPT required: capture RECEIPT=/tmp/task-precheck.json CMD='make precheck' make evidence first" + if [ -z "${PRECHECK_RECEIPT:-}" ]; then + PRECHECK_RECEIPT=$(mktemp "${TMPDIR:-/tmp}/task-precheck-${slug}.XXXXXX") + # evidence 捕获只接受未占用路径(独占创建):mktemp 只用来取唯一名。 + rm -f -- "$PRECHECK_RECEIPT" + # start 只接受严格 READY 的 delivery precheck,local 模式的 LOCAL_READY 不能用于建任务。 + (cd "$(git rev-parse --show-toplevel)" \ + && TASK_WORK_MODE=delivery RECEIPT="$PRECHECK_RECEIPT" CMD='make precheck' bash "${SCRIPT_DIR}/evidence.sh") \ + || die "auto precheck is not READY (receipt=${PRECHECK_RECEIPT}); resolve the verdict above first (no branch created)" + echo "[precheck] auto receipt=${PRECHECK_RECEIPT}" + fi PRECHECK_RECEIPT=$(realpath -- "$PRECHECK_RECEIPT") VALIDATE_RECEIPT="$PRECHECK_RECEIPT" VALIDATE_RECEIPT_PURPOSE=start bash "${SCRIPT_DIR}/evidence.sh" - # 分流核验必须先于任何 worktree/分支创建:收据与当前远端候选不一致则无副作用拒绝。 - [ -f "$TRIAGE_RECEIPT" ] || die "triage receipt missing: ${TRIAGE_RECEIPT} (no branch created)" - TRIAGE_RECEIPT=$(realpath -- "$TRIAGE_RECEIPT") - local -a triage_args=(verify --receipt "$TRIAGE_RECEIPT" --kind "$kind") - if [ "$kind" = child ]; then triage_args+=(--initiative "$initiative"); fi - local triage_out triage_line triage_head triage_verdict triage_init triage_rsha triage_digest triage_note - if ! triage_out=$(python3 "${SCRIPT_DIR}/task_triage.py" "${triage_args[@]}"); then - printf '%s\n' "$triage_out" >&2 - die "triage receipt rejected; no worktree or branch was created" + local triage_out triage_line triage_head="" triage_verdict triage_init triage_rsha triage_digest triage_note + local parent_head="" parent_source="" parent_card + if [ "$planned_child" -eq 1 ]; then + # 计划内子任务免分流:父 initiative 卡片的子任务登记已列出本分支(delivery 读 origin,local 读本地父分支)。 + if [ "$(work_mode)" = delivery ]; then + parent_head=$(remote_parent_head "$initiative") || die "remote parent initiative ${initiative} unavailable; no worktree or branch was created" + parent_source=origin + else + parent_head=$(git rev-parse --verify "refs/heads/${initiative}^{commit}" 2>/dev/null) || die "parent initiative branch ${initiative} missing locally" + parent_source="local(非正式准入)" + fi + parent_card=".task/$(echo "$initiative" | tr '/' '-').md" + git show "${parent_head}:${parent_card}" 2>/dev/null | registered_child "$name" \ + || die "${name} is not registered in ${initiative} ${parent_card} ## 子任务登记; register it in the parent card first or provide TRIAGE_RECEIPT (no worktree or branch was created)" + triage_note="- 需求分流:免(父 initiative 子任务登记已列出 ${name};parent_head_sha=${parent_head})" + else + # 分流核验必须先于任何 worktree/分支创建:收据与当前远端候选不一致则无副作用拒绝。 + [ -f "$TRIAGE_RECEIPT" ] || die "triage receipt missing: ${TRIAGE_RECEIPT} (no branch created)" + TRIAGE_RECEIPT=$(realpath -- "$TRIAGE_RECEIPT") + local -a triage_args=(verify --receipt "$TRIAGE_RECEIPT" --kind "$kind") + if [ "$kind" = child ]; then triage_args+=(--initiative "$initiative"); fi + if ! triage_out=$(python3 "${SCRIPT_DIR}/task_triage.py" "${triage_args[@]}"); then + printf '%s\n' "$triage_out" >&2 + die "triage receipt rejected; no worktree or branch was created" + fi + triage_line=$(printf '%s\n' "$triage_out" | sed -n 's/^\[triage-verified\] //p') + triage_head=$(triage_field selected_head_sha "$(printf '%s\n' "$triage_out" | sed -n 's/^\[triage-head\] //p')") + triage_verdict=$(triage_field verdict "$triage_line") + triage_init=$(triage_field selected_initiative "$triage_line") + triage_rsha=$(triage_field requirement_sha256 "$triage_line") + triage_digest=$(triage_field receipt_sha256 "$triage_line") + [ -n "$triage_verdict" ] && [ -n "$triage_rsha" ] && [ -n "$triage_digest" ] || die "triage verification output unparsable; no branch created" + triage_note="- 需求分流:verdict=${triage_verdict};selected_initiative=${triage_init:-无};requirement_sha256=${triage_rsha};triage_receipt=${TRIAGE_RECEIPT};receipt_sha256=${triage_digest}" fi - triage_line=$(printf '%s\n' "$triage_out" | sed -n 's/^\[triage-verified\] //p') - triage_head=$(triage_field selected_head_sha "$(printf '%s\n' "$triage_out" | sed -n 's/^\[triage-head\] //p')") - triage_verdict=$(triage_field verdict "$triage_line") - triage_init=$(triage_field selected_initiative "$triage_line") - triage_rsha=$(triage_field requirement_sha256 "$triage_line") - triage_digest=$(triage_field receipt_sha256 "$triage_line") - [ -n "$triage_verdict" ] && [ -n "$triage_rsha" ] && [ -n "$triage_digest" ] || die "triage verification output unparsable; no branch created" - triage_note="- 需求分流:verdict=${triage_verdict};selected_initiative=${triage_init:-无};requirement_sha256=${triage_rsha};triage_receipt=${TRIAGE_RECEIPT};receipt_sha256=${triage_digest}" check_worktree before-sync # 已合入的同名分支不得重建(2026-10-05 实测:合入后再 task-start 同名会从新 main 长出 # 第二段历史,空卡覆盖已合入任务卡,两轮证据差点丢失)。复核/追加沿用原分支或换新 slug。 @@ -188,16 +218,17 @@ cmd_start() { [ "$(git status --porcelain | { grep -v '^.. \.zcode/' || true; } | wc -l)" -eq 0 ] || die "dirty tree — resolve first (precheck verdict BUSY)" [ "$(git submodule status --recursive 2>/dev/null | { grep -cE '^\+|^U' || true; })" -eq 0 ] || die "submodule drift/conflict — resolve first (precheck verdict BUSY)" git merge-base --is-ancestor "origin/${TARGET_BRANCH}" HEAD || die "local ${TARGET_BRANCH} behind origin — pull --ff-only first" - local rmain slug wt mainroot start_ref parent_head="" parent_source="" parent_note="" + local rmain wt mainroot start_ref parent_note="" rmain=$(git rev-parse "origin/${TARGET_BRANCH}") start_ref=$rmain - slug=$(echo "$name" | tr '/' '-') local branch_meta case "$kind" in initiative) branch_meta="- 分支类型:initiative;initiative_id:${slug};root_target:${TARGET_BRANCH};immediate_target:${TARGET_BRANCH};accepting_requirements:true" ;; child) - if [ "$(work_mode)" = delivery ]; then + if [ "$planned_child" -eq 1 ]; then + : # 父头已在登记核验时解析(delivery 为刚取回的 origin 头)。 + elif [ "$(work_mode)" = delivery ]; then # 交付模式:父 initiative 只认 origin 上刚取回的头,且必须等于分流收据核验的头(未准入的本地提交不算)。 [ -n "$triage_head" ] || die "triage receipt carries no selected initiative head; re-run make task-triage (no worktree or branch was created)" parent_head=$(remote_parent_head "$initiative") || die "remote parent initiative ${initiative} unavailable; no worktree or branch was created" @@ -238,14 +269,26 @@ cmd_start() { && echo "[warn] worktree 子模块物化不完整:${sub_drift} 项漂移/未初始化——构建前先核对(git -C $wt submodule status --recursive)" local card="$wt/.task/${slug}.md" mkdir -p "$wt/.task" - cat > "$card" </dev/null || echo '(local)') + owner=$(git config user.name || echo unknown) + { + cat <;状态:进行中;owner:$(git config user.name || echo unknown) +任务主记录:;状态:进行中;owner:${owner} -## 基线与授权 - -- 权威仓:$(git -C "$wt" remote get-url origin 2>/dev/null || echo '(local)');目标分支:${TARGET_BRANCH};R0(initial_base_sha)= ${rmain} +EOF + [ "$template" = lite ] || printf '## 基线与授权\n\n' + cat < +EOF + if [ "$template" = charter ]; then + cat <<'EOF' +<章程:用途与目标、接纳范围与非目标、方案/计划链接、分级测试策略、验收条件、追加领域规则、生命周期。> + +## 子任务登记 + +| 子任务分支 | 必需 | 状态 | PR | +|---|---|---|---| +| 待登记 | 否 | — | — | +EOF + else + echo '<预期结果、受影响入口/模块/契约;明确不做什么。>' + fi + cat <<'EOF' ## Agent 实施收据 -按根目录 AGENT-EXECUTION.md 路由;外部 Agent 一单元一新进程/工作区。实施、修复与测试的正常候选仅为 Cursor、Antigravity(agy)和 PI;每个候选对安全可恢复失败最多 3 次全新尝试,然后才换下一候选。三者都安全耗尽后,才允许一次全新的 Codex CLI 紧急尝试。Codex 平时只用于规划、方案设计、实施计划或独立 Review。显式 fail-stop(failed-unknown、scope-expansion、blocked、cancelled、无效配置、未对账 Git 状态与安全失败)立即停止。不因推理时长、工具次数、规划方式或首次写文件时间判定失败;尝试只在显式终态失败、进程退出、总超时,或 stdin/stdout/stderr 均无活动的空闲超时时失败。CLI 可在目标、范围与安全边界内自主检查、计划、测试并迭代。实施集成必须是成功 transport 上的 scripts/agent-exec apply,或 pre-commit admit 的精确内容来源。host 直接管理的豁免仅为 .task/**、docs/superpowers/specs/** 与 docs/superpowers/plans/**。fallback 列只记录该行是否为 Codex 紧急尝试。安全耗尽不发出 host-fallback-authorized,终态为 unavailable 或 blocked。不授权 host 直接撰写该单元。原始日志保留在源码外收据路径。 +路由与 fail-stop 规则见根目录 AGENT-EXECUTION.md 与 policy/delivery.md;每次尝试一行,原始日志留在源码外收据路径。 | 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 | |---|---|---|---|---|---|---|---|---|---|---| | 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — | +EOF + [ "$template" = lite ] || cat <<'EOF' ## 关联方向与规划 @@ -308,6 +371,8 @@ ${parent_note} | 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 | |---|---|---|---|---|---|---|---| | 待定义 | — | — | — | 待核对 | NOT_RUN | 待评估 | 待评估 | +EOF + cat < | 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) | |---|---|---|---|---|---| +EOF + [ "$template" = lite ] || cat <<'EOF' ## 交付调度 @@ -328,12 +395,16 @@ ${parent_note} - 固定候选:无(base/head、组件与相关配置引用实际尝试) - 调度依据:policy/merge-queue.md;新模式启用前按既有交付门禁 - 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合 +EOF + cat <<'EOF' ## 结果与验证(完成后填写) - 结果: - 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表> - 影响与剩余项: +EOF + [ "$template" = lite ] || cat <<'EOF' ## 规划进展与下一步 @@ -344,6 +415,8 @@ ${parent_note} - 预期产物/验收条件:待定义 - 重复任务核对:待核对(已有/在途任务、PR 与规划记录) - 启动授权:未授权(建议不自动启动新任务,仍须明确授权) +EOF + cat <<'EOF' ## 合入与清理 @@ -353,6 +426,7 @@ ${parent_note} - 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验) - 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置) EOF + } > "$card" (cd "$wt" && IMPORT_RECEIPT="$PRECHECK_RECEIPT" bash "${SCRIPT_DIR}/evidence.sh") git -C "$wt" add .task git -C "$wt" commit -q -m "Record the task card for ${slug}" \ @@ -511,6 +585,16 @@ remote_parent_head() { # $1=父 initiative 分支 → 打印刚从 origin 取回 fi git rev-parse --verify "refs/remotes/origin/$1^{commit}" } +registered_child() { # stdin=父 initiative 卡片;$1=子任务分支 → 子任务登记表(task_pr_check 同一解析)列出该分支返回 0 + python3 -c 'import sys +sys.path.insert(0, sys.argv[1]) +from task_pr_check import Stop, child_registry +try: + rows = child_registry(sys.stdin.read()) +except Stop as error: + sys.exit("ERROR: " + error.message) +sys.exit(0 if any(row["子任务分支"].strip("`").strip() == sys.argv[2] for row in rows) else 1)' "$SCRIPT_DIR" "$1" +} resolve_parent_ref() { # $1=父 initiative 分支 → 打印用于同步/核验的提交(delivery 仅远端;local 取远端与本地包含对方的一侧) git check-ref-format "refs/heads/$1" >/dev/null || die "invalid parent branch $1" if [ "$(work_mode)" = delivery ]; then diff --git a/tests/test_task.py b/tests/test_task.py index e8ed576a..c8939497 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -1,6 +1,7 @@ """Lifecycle regressions against real disposable Git repositories and remotes.""" import os import json +import re import hashlib import http.server import threading @@ -15,6 +16,10 @@ import urllib.parse SOURCE = Path(__file__).resolve().parents[1] TRIAGE_COMMANDS = ("start", "triage", "triage-decide") +ROUTING_LINE = ("路由与 fail-stop 规则见根目录 AGENT-EXECUTION.md 与 policy/delivery.md;" + "每次尝试一行,原始日志留在源码外收据路径。") +LITE_SECTIONS = ["## 目标与非目标", "## Agent 实施收据", "## 同步记录", "## 证据(Evidence)", + "## 结果与验证(完成后填写)", "## 合入与清理"] class FakeGitea: @@ -89,6 +94,8 @@ class TaskLifecycleTests(unittest.TestCase): "GIT_CONFIG_COUNT": "2", "GIT_CONFIG_KEY_0": "gc.auto", "GIT_CONFIG_VALUE_0": "0", "GIT_CONFIG_KEY_1": "maintenance.auto", "GIT_CONFIG_VALUE_1": "false", + # task-start 自动 precheck 收据落在 TMPDIR:限定在夹具目录内随测试清理。 + "TMPDIR": str(self.root), # 桩位于 127.0.0.1:禁止经代理转发,保证测试不触达外部网络。 "NO_PROXY": "127.0.0.1,localhost", "no_proxy": "127.0.0.1,localhost"}) for key in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GITEA_TOKEN", "GITEA_API_URL", @@ -771,12 +778,30 @@ class TaskLifecycleTests(unittest.TestCase): else: self.gitea.prs.append(FakeGitea.pr(len(self.gitea.prs) + 1, branch, sha)) - def test_start_requires_precheck_receipt_before_creating_worktree(self): + def test_start_without_precheck_receipt_runs_and_imports_auto_precheck(self): triage = self.triage_receipt() + result = self.task(self.repo, "start", "feature/new", + env=self.env | {"TRIAGE_RECEIPT": str(triage)}) + line = next(row for row in result.stdout.splitlines() if row.startswith("[precheck] auto receipt=")) + receipt = Path(line.split("=", 1)[1]) + self.assertEqual(receipt.parent, self.root) + self.assertIn("feature-new", receipt.name) + data = json.loads(receipt.read_text()) + self.assertEqual(data["command"], "make precheck") + self.assertIn("[verdict] READY", data["output"]) + card = (self.root / "repo-wt-feature-new/.task/feature-new.md").read_text() + self.assertIn(data["receipt_sha256"], card) + + def test_start_refuses_busy_auto_precheck_without_side_effects(self): + triage = self.triage_receipt() + (self.repo / "stray").write_text("unfinished\n") result = self.task(self.repo, "start", "feature/new", check=False, env=self.env | {"TRIAGE_RECEIPT": str(triage)}) self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertIn("SHARED_WORKSPACE_BUSY", result.stdout) + self.assertIn("auto precheck is not READY", result.stdout) self.assertFalse((self.root / "repo-wt-feature-new").exists(), result.stdout) + self.assertEqual(self.git(self.repo, "branch", "--list", "feature/new").stdout.strip(), "") def test_start_preserves_merged_local_and_remote_branch_names(self): receipt = self.capture_precheck() @@ -833,23 +858,9 @@ class TaskLifecycleTests(unittest.TestCase): "| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — |", agent_receipts, ) - self.assertIn("Cursor、Antigravity(agy)和 PI", agent_receipts) - self.assertIn("最多 3 次全新尝试", agent_receipts) - self.assertIn("一次全新的 Codex CLI 紧急尝试", agent_receipts) - self.assertIn("规划、方案设计、实施计划或独立 Review", agent_receipts) - self.assertIn("显式 fail-stop", agent_receipts) - self.assertIn("不因推理时长、工具次数、规划方式或首次写文件时间判定失败", agent_receipts) - self.assertIn("stdin/stdout/stderr 均无活动的空闲超时", agent_receipts) - self.assertIn("自主检查、计划、测试并迭代", agent_receipts) - self.assertIn("scripts/agent-exec apply", agent_receipts) - self.assertIn("pre-commit admit", agent_receipts) - self.assertIn(".task/**", agent_receipts) - self.assertIn("docs/superpowers/specs/**", agent_receipts) - self.assertIn("docs/superpowers/plans/**", agent_receipts) - self.assertIn("fallback 列只记录该行是否为 Codex 紧急尝试", agent_receipts) - self.assertIn("不授权 host 直接撰写该单元", agent_receipts) - self.assertIn("安全耗尽不发出 host-fallback-authorized", agent_receipts) - self.assertIn("终态为 unavailable 或 blocked", agent_receipts) + self.assertIn(ROUTING_LINE, agent_receipts) + self.assertNotIn("Cursor、Antigravity(agy)和 PI", agent_receipts) + self.assertNotIn("## 子任务登记", card) for field in ("当前阶段", "当前增量", "方案引用", "实施计划引用", "测试计划引用"): self.assertIn(field + ":", card) self.assertIn("## 交付调度\n", card) @@ -1252,6 +1263,8 @@ class TaskLifecycleTests(unittest.TestCase): self.assertEqual(self.card_helper("card_immediate_target", card), "feature/alpha") self.assertEqual(self.card_helper("card_initiative_id", card), "feature-alpha") self.assertEqual(self.card_helper("card_root_target", card), "main") + self.assertIn("- 需求分流:verdict=JOIN;selected_initiative=feature/alpha;", text) + self.assert_lite_card(text, "feature/alpha-login") def test_start_child_rejects_missing_initiative_or_wrong_slug(self): self.start_initiative() @@ -1263,6 +1276,104 @@ class TaskLifecycleTests(unittest.TestCase): self.assertNotEqual(result.returncode, 0, result.stdout) self.assertFalse(wt.exists()) + def register_child(self, parent, child, initiative="feature/alpha", publish=True): + card = parent / ".task" / (initiative.replace("/", "-") + ".md") + text = card.read_text() + self.assertIn("| 待登记 | 否 | — | — |", text) + card.write_text(text.replace("| 待登记 | 否 | — | — |", f"| `{child}` | 是 | 进行中 | — |", 1)) + self.git(parent, "add", ".task") + self.git(parent, "commit", "-m", f"Register {child}") + if publish: + self.publish_initiative(initiative) + return self.git(parent, "rev-parse", "HEAD").stdout.strip() + + def planned_child_env(self): + return self.env | {"TASK_BRANCH_KIND": "child", "TASK_INITIATIVE": "feature/alpha", + "TASK_PARENT_PR": "https://example.invalid/repo/pulls/7"} + + def assert_lite_card(self, text, name): + self.assertEqual([line for line in text.splitlines() if line.startswith("## ")], LITE_SECTIONS) + self.assertEqual(text.splitlines()[0], f"# {name} 任务卡") + self.assertRegex(text, r"(?m)^任务主记录:;状态:进行中;owner:.+$") + self.assertRegex(text, r"(?m)^- 权威仓:.+;目标分支:main;R0(initial_base_sha)= [0-9a-f]{40}$") + self.assertRegex(text, rf"(?m)^- 工作区:.+(worktree);分支:{re.escape(name)}$") + for prefix in ("- 分支类型:", "- 需求分流:", "- 授权与交付终点:", + "- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进)", + "- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)", + "- 合入执行:agent(", + "- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)"): + self.assertRegex(text, "(?m)^" + re.escape(prefix)) + self.assertIn(ROUTING_LINE, text) + self.assertIn("| 待定义 | — | NOT_RUN | NOT_RUN |", text) + self.assertRegex(text, r"(?m)^\| 启动 \| [0-9a-f]{40} \| [0-9a-f]{40} \| ") + self.assertIn("| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |", text) + for field in ("Review 记录", "冲突记录", "CI 记录", "合入记录", "清理记录"): + self.assertIn(field + ":NOT_RUN", text) + self.assertNotIn("## 阶段记录", text) + + def test_start_planned_child_without_triage_when_registered_on_origin(self): + parent = self.start_initiative() + parent_head = self.register_child(parent, "feature/alpha-login") + result = self.task(self.repo, "start", "feature/alpha-login", env=self.planned_child_env()) + self.assertIn("[precheck] auto receipt=", result.stdout) + wt = self.root / "repo-wt-feature-alpha-login" + card = wt / ".task/feature-alpha-login.md" + text = card.read_text() + self.assertIn("- 需求分流:免(父 initiative 子任务登记已列出 feature/alpha-login;" + f"parent_head_sha={parent_head})", text) + self.assertIn(f"parent_head_sha:{parent_head};parent_pr:https://example.invalid/repo/pulls/7;" + "parent_source:origin", text) + self.git(wt, "merge-base", "--is-ancestor", parent_head, "HEAD") + self.assert_lite_card(text, "feature/alpha-login") + self.assertEqual(self.card_helper("card_immediate_target", card), "feature/alpha") + + def test_start_planned_child_refuses_unregistered_child_without_side_effects(self): + parent = self.start_initiative() + self.publish_initiative("feature/alpha") + for case in ("placeholder-only", "registered-locally-only"): + with self.subTest(case=case): + if case == "registered-locally-only": + self.register_child(parent, "feature/alpha-login", publish=False) + result = self.task(self.repo, "start", "feature/alpha-login", check=False, + env=self.planned_child_env()) + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertIn("not registered in feature/alpha", result.stdout) + self.assertIn("provide TRIAGE_RECEIPT", result.stdout) + self.assertFalse((self.root / "repo-wt-feature-alpha-login").exists()) + self.assertEqual(self.git(self.repo, "branch", "--list", "feature/alpha-login").stdout.strip(), "") + self.assertEqual(self.git(self.repo, "ls-remote", "--heads", "origin", + "feature/alpha-login").stdout.strip(), "") + + def test_start_initiative_card_is_charter_with_child_registry(self): + wt = self.start_initiative() + card = wt / ".task/feature-alpha.md" + text = card.read_text() + goal = text.split("## 目标与非目标\n", 1)[1].split("\n## ", 1)[0] + for item in ("用途与目标", "接纳范围与非目标", "方案/计划链接", "分级测试策略", "验收条件", + "追加领域规则", "生命周期"): + self.assertIn(item, goal) + sections = [line for line in text.splitlines() if line.startswith("## ")] + self.assertEqual(sections[sections.index("## 目标与非目标") + 1], "## 子任务登记") + self.assertIn("## 阶段记录", sections) + self.assertIn(ROUTING_LINE, text) + rows = self.run_cmd(self.repo, "python3", "-c", + "import json, sys; sys.path.insert(0, 'scripts');" + "from task_pr_check import child_registry;" + "print(json.dumps(child_registry(open(sys.argv[1]).read()), ensure_ascii=False))", + str(card)).stdout + self.assertEqual(json.loads(rows), [{"子任务分支": "待登记", "必需": "否", "状态": "—", "PR": "—"}]) + + def test_start_fix_and_hotfix_cards_are_lite(self): + triage = self.triage_receipt() + for name in ("fix/typo", "hotfix/urgent"): + with self.subTest(name=name): + self.task(self.repo, "start", name, env=self.env | {"TRIAGE_RECEIPT": str(triage)}) + slug = name.replace("/", "-") + text = (self.root / f"repo-wt-{slug}/.task/{slug}.md").read_text() + self.assert_lite_card(text, name) + self.assertIn("- 分支类型:standalone;root_target:main;", text) + self.assertIn("- 需求分流:verdict=NEW;", text) + def test_sync_child_merges_new_parent_initiative_commits(self): parent = self.start_initiative() wt, _ = self.start_child() @@ -1315,14 +1426,16 @@ class TaskLifecycleTests(unittest.TestCase): def test_start_without_triage_receipt_creates_no_worktree_or_branch(self): receipt = self.capture_precheck() - result = self.task(self.repo, "start", "feature/untriaged", check=False, - env=self.env | {"PRECHECK_RECEIPT": str(receipt)}) - self.assertNotEqual(result.returncode, 0, result.stdout) - self.assertIn("TRIAGE_RECEIPT required", result.stdout) - self.assertFalse((self.root / "repo-wt-feature-untriaged").exists()) - self.assertEqual(self.git(self.repo, "branch", "--list", "feature/untriaged").stdout.strip(), "") - self.assertEqual(self.git(self.repo, "ls-remote", "--heads", "origin", - "feature/untriaged").stdout.strip(), "") + for kind in ("standalone", "initiative"): + with self.subTest(kind=kind): + result = self.task(self.repo, "start", "feature/untriaged", check=False, + env=self.env | {"PRECHECK_RECEIPT": str(receipt), "TASK_BRANCH_KIND": kind}) + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertIn("TRIAGE_RECEIPT required", result.stdout) + self.assertFalse((self.root / "repo-wt-feature-untriaged").exists()) + self.assertEqual(self.git(self.repo, "branch", "--list", "feature/untriaged").stdout.strip(), "") + self.assertEqual(self.git(self.repo, "ls-remote", "--heads", "origin", + "feature/untriaged").stdout.strip(), "") def test_start_child_refuses_non_join_receipt_without_side_effects(self): self.start_initiative("feature/alpha") From 48a80297d9dea893b2856d2602d87b5108c6f477 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 02:52:36 +0000 Subject: [PATCH 08/18] agent-exec transport 5bb458118449d7cc0a3fe4bc92623f71/356ad35bf41b --- policy/delivery.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/policy/delivery.md b/policy/delivery.md index 606676bb..42751976 100644 --- a/policy/delivery.md +++ b/policy/delivery.md @@ -168,9 +168,43 @@ host 可以直接编写并提交的豁免只有 `.task/**`、`docs/superpowers/s PR 目标:子任务 PR 的 base 为所属大任务分支;普通任务与大任务 PR 的 base 为 `main`。`make task-pr-check` 在本地核验 base 与 head(远端为准),CI 的 `PR base gate` 以同一规则复核。 两级合入与验证: -1. **子任务合入大任务**:子任务完成时,通过 `make task-sync` 合并大任务分支的远端最新状态(delivery 只认远端头),解决文本与语义冲突,完成直接相关测试与独立 Review 后,以大任务分支为 base 提 PR,并通过 `make task-pr-check` 与 CI `PR base gate`; +1. **子任务合入大任务**:子任务完成时,通过 `make task-sync` 合并大任务分支的远端最新状态(delivery 只认远端头),解决文本与语义冲突,完成直接相关测试与独立 Review 后,以大任务分支为 base 提 PR,并通过 `make task-pr-check` 与 CI `PR base gate`;L2→L1 按下文「层级、任务通道与分级门禁」的轻量门禁执行,不排 #518 窗口、不要求三段证据; 2. **大任务合入主干**:所有必需子任务已合入或已延期后,将大任务卡的 `accepting_requirements` 置为 `false`,通过 `make task-sync` 同步最新 `origin/main`,再运行 `make task-pr-check FINAL=1` 核验 `## 子任务登记` 中必需子任务均为「已合入」或「已延期」;随后执行完整组合验证、独立 Review 与必需 CI,以 `main` 为 base 合入。 +### 层级、任务通道与分级门禁 + +层级固定两级,不再加深: +- **L1 initiative**:从 `main` 切出,最终一次合入 `main`。 +- **L2 child**:从 L1 切出,合入 L1。L2 内部用 agent-exec 单元控制上下文,单元不开 PR。 +- L1 超过约 2 周或 6–8 个子任务时,横向拆成多个并列 L1,用规划主记录串联;L2 过大时拆成兄弟 L2 或更多单元。 + +L1 章程与运行: +- 章程写在 L1 任务卡 `## 目标与非目标`:用途与目标、接纳范围与非目标、方案/计划链接、分级测试策略、验收条件、追加领域规则、生命周期;并维护 `## 子任务登记`。 +- 建立 L1 时即开一个 base 为 `main` 的 [WIP] PR,供分流发现;其评论区作为该 L1 的仓外调度记录。 +- 已登记在 `## 子任务登记` 的子任务可免分流收据,直接 `make task-start KIND=child …`。 +- 子任务可继承 L1 章程中的全部或部分领域规则,并在子任务卡注明继承范围。 +- L1 不追 `main`,只在最终窗口、需要 `main` 上某修复、或长周期 L1 的里程碑/每周一次时运行 `make task-sync`。 + +任务通道按 修复/完善/扩展/新增 分类: +- **修复**:`fix/`(P0 生产事故用 `hotfix/`),standalone 直达 `main`,在合入队列中优先;hotfix 可在 #518 写明原因后插队。 +- **完善**:进入所在领域的“完善列车” L1,按周或按批合入 `main`。 +- **扩展**:优先 JOIN 同主题在途 L1,没有时新建。 +- **新增**:建立 L1 并写章程。 +- 治理与规范调整集中到一个治理 L1。 +- 只改记录(任务卡、报告)的变更不单独推进 `main`,随所属任务 PR 合入。 +- 组件 pin 跟随用到它的 L1/任务一起推进,不单独开 pin PR(紧急修复除外)。 + +分级门禁: + +| 层级 | 检查 | Review | CI | 合入窗口与证据 | 人工 | +|---|---|---|---|---|---| +| 单元→L2 | worker 自检 + host 最小直接检查(`make test-affected`) | host 审阅实际 diff | 无 | 无 | 无 | +| L2→L1 | `make task-sync` 同步 L1 头 + `make test-affected` | 一次全新会话独立 Review | 轻量 CI(PR base 非 `main` 时只跑 `test` 作业,`test-pg`/`svc-realcombo` 跳过)作为 GATE | 不排 #518 窗口、不需分流收据(已登记子任务)、不要求三段证据;任务卡合入前 `make task-close SELF=1` | 无 | +| L1 的 WIP PR | — | — | 完整 CI,作为非阻塞预警 | — | — | +| L1 或修复→main | 最终窗口内 `make task-sync` + 完整组合验证 | 整体独立 Review | 完整 CI(`test`、`test-pg`、`svc-realcombo`) | #518 窗口、三段证据、合入前折叠闭合 | L1 最终一次用户专属验收(如有) | + +人工审批只保留 L1 章程确认、L1 最终用户专属验收与高后果双钥匙操作;等待 CI 一律 `make ci-wait SHA=<完整 SHA>`。 + ## 目标优先、范围控制与问题分流 执行顺序为: From a28318c97a143268a4949ed4464009ffcf6bf45d Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 02:56:59 +0000 Subject: [PATCH 09/18] agent-exec transport 0f1877f2365acb5d35a176055575367f/d8466ef2fcee --- AGENTS.md | 3 +- policy/governance.md | 5 ++ scripts/check-evidence.py | 73 ++++++++++++++++++++- scripts/evidence.py | 76 +++++++++++++++++++++- tests/test_check_evidence.py | 123 +++++++++++++++++++++++++++++++++++ tests/test_evidence.py | 54 +++++++++++++++ tests/test_task.py | 5 +- 7 files changed, 335 insertions(+), 4 deletions(-) create mode 100644 tests/test_check_evidence.py diff --git a/AGENTS.md b/AGENTS.md index d3cd45ce..de09a87b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,7 +134,8 @@ policy/delivery.md。 合入与验收须核验 GATE 的运行链接/身份及 SHA,或 REPLAY 的独立审阅者、命令和对象; 本地 evidence 只生成 SELF,不能靠 `SRC` 自行升级;与重验不一致时以重验为准。 - **证据由工具写入**:命令输出进入任务卡/PR 一律经 `CMD="<命令>" make evidence` 写入 - (记录脱敏命令、执行前后对象、退出码、完整脱敏输出与摘要)。凭据只经环境或受保护文件 + (卡内写摘要块:脱敏命令、前后 HEAD、退出码、输出尾部与摘要;含执行前后对象和完整脱敏输出的 + 完整收据一次性存于仓外记录路径,见 policy/governance.md)。凭据只经环境或受保护文件 引用;命令、输出和摘要统一脱敏,摘要只证明记录完整性,不证明执行者身份。手抄、转写、 凭记忆重构的输出视为无效证据;补写输出与伪造测试同级红线。 - **完成声明三段输出**(precheck / task-sync / 测试)必须由 evidence 工具写入任务卡, diff --git a/policy/governance.md b/policy/governance.md index 7e328e43..5b1eeba8 100644 --- a/policy/governance.md +++ b/policy/governance.md @@ -103,6 +103,11 @@ PR 合入由智能体执行:先完成有效 Review,再同步并解决冲突 命令含敏感值时展示脱敏形式,并记录原命令摘要。执行前后对象分别记录;命令改变 HEAD 时不把执行前 SHA 冒充执行后对象。不能读取执行后状态时明确记录缺项。 +写入任务卡(`CMD` 直写或 `IMPORT_RECEIPT` 导入)时,卡内只放摘要块(命令、退出码、前后 HEAD、 +输出尾部及各摘要);完整收据以 0600 一次性写到仓外的记录路径(默认 +`~/.cache/agent-system-dev/evidence/.json`,可用 `AGENT_EVIDENCE_DIR` 覆盖), +不覆盖不同内容。`check-evidence` 在本机可读时用完整收据核对摘要,缺失则警告未验证;两者仍是 SELF。 + ### 候选固定前后 固定前,完成同步、必要检查、任务卡及本地收据提交,核对干净的组件组合,再读取和推送最终 HEAD。 diff --git a/scripts/check-evidence.py b/scripts/check-evidence.py index 70395128..7e56ab96 100644 --- a/scripts/check-evidence.py +++ b/scripts/check-evidence.py @@ -97,6 +97,72 @@ def check_receipt(value): return before_stale or after_stale +SUMMARY_FIELDS = {"summary_schema", "source", "timestamp", "command", "exit_code", "cwd", + "before_head", "after_head", "branch", "target_branch", "target_head", + "before_tracked_diff_sha256", "after_tracked_diff_sha256", "output_tail", + "output_sha256", "receipt_sha256", "receipt_path"} + + +def optional_match(pattern, value): + return value is None or (isinstance(value, str) and pattern.fullmatch(value) is not None) + + +def check_summary_fields(value): + if type(value.get("summary_schema")) is not int or value["summary_schema"] != evidence.SUMMARY_SCHEMA: + raise ValueError("unsupported summary schema") + keys = set(value) + targets = keys & set(evidence.TARGET_FIELDS) + if targets not in (set(), set(evidence.TARGET_FIELDS)) or keys - targets != SUMMARY_FIELDS: + raise ValueError("invalid summary fields") + for key in targets: + if not evidence.valid_branch(value[key]): + raise ValueError("invalid summary target: " + key) + for key in ("source", "timestamp", "command", "cwd", "branch", "target_branch", "receipt_path"): + if not isinstance(value[key], str): + raise ValueError("invalid summary field: " + key) + if value["source"] != "SELF" or type(value["exit_code"]) is not int or not 0 <= value["exit_code"] <= 255: + raise ValueError("invalid summary source or exit code") + try: + datetime.fromisoformat(value["timestamp"].replace("Z", "+00:00")) + except ValueError: + raise ValueError("invalid summary timestamp") from None + if not isinstance(value["before_head"], str) or not evidence.GIT_SHA.fullmatch(value["before_head"]): + raise ValueError("invalid summary before_head") + for key in ("after_head", "target_head"): + if not optional_match(evidence.GIT_SHA, value[key]): + raise ValueError("invalid summary field: " + key) + for key in ("before_tracked_diff_sha256", "output_sha256", "receipt_sha256"): + if not isinstance(value[key], str) or not evidence.SHA256.fullmatch(value[key]): + raise ValueError("invalid summary digest: " + key) + if not optional_match(evidence.SHA256, value["after_tracked_diff_sha256"]) or ( + (value["after_head"] is None) != (value["after_tracked_diff_sha256"] is None)): + raise ValueError("invalid summary digest: after_tracked_diff_sha256") + if not isinstance(value["output_tail"], list) or not all(isinstance(line, str) for line in value["output_tail"]): + raise ValueError("invalid summary output tail") + path = Path(value["receipt_path"]) + if not Path(value["cwd"]).is_absolute() or not path.is_absolute() or ( + path.name != value["receipt_sha256"] + ".json"): + raise ValueError("invalid summary paths") + + +def check_summary(label, value): + """Verify a card summary against its full receipt when it is stored on this machine.""" + check_summary_fields(value) + path = Path(value["receipt_path"]) + if not path.is_file(): + warn(label, f"unverified: full receipt not available locally ({path})") + try: + heads = [value["before_head"]] + ([value["after_head"]] if value["after_head"] else []) + return any([check_head(head) for head in heads]) + except (ValueError, OSError): + return False + full = json.loads(path.read_text(encoding="utf-8")) + stale = check_receipt(full) + if evidence.build_summary(full, value["receipt_path"], len(value["output_tail"])) != value: + raise ValueError("summary does not match full receipt") + return stale + + def records(content): """Yield fenced JSON and timestamped legacy rows without interpreting text.""" block = None @@ -125,7 +191,12 @@ def main(): return for label, kind, record in records(content): try: - stale = check_receipt(json.loads(record)) if kind == "json" else check_head(record) + if kind == "json": + value = json.loads(record) + stale = (check_summary(label, value) if isinstance(value, dict) and "summary_schema" in value + else check_receipt(value)) + else: + stale = check_head(record) except (ValueError, TypeError, KeyError, OSError, evidence.EvidenceError) as exc: warn(label, "unverified: " + str(exc)) continue diff --git a/scripts/evidence.py b/scripts/evidence.py index fc36c290..a40a5e84 100644 --- a/scripts/evidence.py +++ b/scripts/evidence.py @@ -14,9 +14,12 @@ from pathlib import Path import re import subprocess import sys +import tempfile SCHEMA = 1 +SUMMARY_SCHEMA = 1 +DEFAULT_TAIL = 15 SECRET_NAME = re.compile(r"token|password|passwd|secret|api[_-]?key|credential|private[_-]?key", re.I) SHA256 = re.compile(r"[0-9a-f]{64}\Z") GIT_SHA = re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})\Z") @@ -332,8 +335,79 @@ def find_card(): raise EvidenceError("No task card exists for the current branch") +def evidence_dir(): + configured = os.environ.get("AGENT_EVIDENCE_DIR") + if configured: + base = Path(configured) + else: + base = Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "agent-system-dev" / "evidence" + base.mkdir(mode=0o700, parents=True, exist_ok=True) + return base.resolve() + + +def store_receipt(receipt): + """Write the full receipt once under its digest; identical existing content is accepted.""" + directory = evidence_dir() + path = directory / (receipt["receipt_sha256"] + ".json") + encoded = json.dumps(receipt, ensure_ascii=False, indent=2) + "\n" + if path.exists() or path.is_symlink(): + try: + same = not path.is_symlink() and json.loads(path.read_text(encoding="utf-8")) == receipt + except (OSError, ValueError): + same = False + if not same: + raise EvidenceError("Full receipt path holds different content: " + str(path)) + return path + fd, temporary = tempfile.mkstemp(prefix=".receipt-", dir=directory) + try: + with os.fdopen(fd, "w", encoding="utf-8") as stream: + os.fchmod(stream.fileno(), 0o600) + stream.write(encoded) + stream.flush() + os.fsync(stream.fileno()) + try: + os.link(temporary, path) + except FileExistsError: + return store_receipt(receipt) + finally: + os.unlink(temporary) + return path + + +def tail_lines(): + value = os.environ.get("TAIL", "") + return int(value) if value.isdigit() else DEFAULT_TAIL + + +def build_summary(receipt, receipt_path, tail=None): + """Card-sized view of a full receipt; every field is derived from the sanitized receipt.""" + tail = tail_lines() if tail is None else tail + before, after = receipt["before"], receipt["after"] + summary = { + "summary_schema": SUMMARY_SCHEMA, "source": "SELF", + "timestamp": receipt["timestamp"], "command": receipt["command"], + "exit_code": receipt["exit_code"], "cwd": receipt["cwd"], + "before_head": before["head"], "after_head": after["head"] if after else None, + "branch": before["branch"], "target_branch": before["target_branch"], + "target_head": before["target_head"], + } + for key in TARGET_FIELDS: + if key in receipt: + summary[key] = receipt[key] + summary.update({ + "before_tracked_diff_sha256": before["tracked_diff_sha256"], + "after_tracked_diff_sha256": after["tracked_diff_sha256"] if after else None, + "output_tail": receipt["output"].splitlines()[-tail:] if tail > 0 else [], + "output_sha256": receipt["output_sha256"], + "receipt_sha256": receipt["receipt_sha256"], + "receipt_path": str(receipt_path), + }) + return summary + + def append_card(card, receipt): - encoded = json.dumps(receipt, ensure_ascii=False, indent=2) + path = store_receipt(receipt) + encoded = json.dumps(build_summary(receipt, path), ensure_ascii=False, indent=2) block = "\n## 证据(Evidence)\n\n" + ( "\n" + "```json\n" + encoded + "\n```\n") diff --git a/tests/test_check_evidence.py b/tests/test_check_evidence.py new file mode 100644 index 00000000..0d55a188 --- /dev/null +++ b/tests/test_check_evidence.py @@ -0,0 +1,123 @@ +"""Exercise check-evidence.py on card summaries and legacy full receipt blocks.""" +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" + + +class CheckEvidenceTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.base = Path(self.temp.name) + self.repo = self.base / "repo" + self.repo.mkdir() + self.git("init", "-b", "main") + self.git("config", "user.name", "Evidence Test") + self.git("config", "user.email", "evidence@example.invalid") + (self.repo / "tracked").write_text("initial\n") + self.git("add", "tracked") + self.git("commit", "-qm", "Initial") + self.git("update-ref", "refs/remotes/origin/main", self.git("rev-parse", "HEAD")) + self.card = self.repo / ".task/main.md" + self.card.parent.mkdir() + self.card.write_text("# Test task\n") + self.env = os.environ.copy() + for key in ("CMD", "SRC", "RECEIPT", "IMPORT_RECEIPT", "VALIDATE_RECEIPT", "TAIL"): + self.env.pop(key, None) + self.env["AGENT_EVIDENCE_DIR"] = str(self.base / "evidence") + + def git(self, *args): + return subprocess.check_output(["git", *args], cwd=self.repo, text=True, + stderr=subprocess.DEVNULL).strip() + + def record(self, command="printf 'checked\\n'"): + result = subprocess.run(["bash", str(SCRIPTS / "evidence.sh")], cwd=self.repo, + env=self.env | {"CMD": command}, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + text = self.card.read_text() + block = text.split("```json\n", 1)[1].split("\n```", 1)[0] + return json.loads(block) + + def check(self): + result = subprocess.run(["python3", str(SCRIPTS / "check-evidence.py"), str(self.card)], + cwd=self.repo, capture_output=True, text=True) + return result.stdout + result.stderr + + def replace_block(self, value): + prefix, rest = self.card.read_text().split("```json\n", 1) + suffix = rest.split("\n```", 1)[1] + self.card.write_text(prefix + "```json\n" + json.dumps(value, indent=2) + "\n```" + suffix) + + def advance(self): + (self.repo / "tracked").write_text("changed\n") + self.git("commit", "-qam", "Change code") + + def test_fresh_summary_passes(self): + summary = self.record() + self.assertNotIn("submodules", self.card.read_text()) + self.assertEqual(self.check(), "") + self.assertTrue(Path(summary["receipt_path"]).is_file()) + + def test_summary_detects_stale_head_through_full_receipt(self): + self.record() + self.advance() + output = self.check() + self.assertIn("recorded HEAD predates non-card changes", output) + self.assertNotIn("unverified", output) + + def test_tampered_summary_warns(self): + summary = self.record() + for field, value in (("command", "make test"), ("exit_code", 1), ("output_tail", ["forged"]), + ("before_head", "0" * 40)): + with self.subTest(field=field): + self.replace_block(dict(summary, **{field: value})) + self.assertIn("unverified: summary does not match full receipt", self.check()) + + def test_malformed_summary_fields_warn(self): + summary = self.record() + for field, value in (("receipt_sha256", "zz"), ("exit_code", True), ("output_tail", "text"), + ("receipt_path", "relative.json")): + with self.subTest(field=field): + self.replace_block(dict(summary, **{field: value})) + self.assertIn("unverified", self.check()) + missing = dict(summary) + del missing["command"] + self.replace_block(missing) + self.assertIn("unverified: invalid summary fields", self.check()) + + def test_tampered_full_receipt_warns(self): + summary = self.record() + path = Path(summary["receipt_path"]) + full = json.loads(path.read_text()) + full["output"] = "changed\n" + path.write_text(json.dumps(full)) + self.assertIn("unverified: receipt checksum mismatch", self.check()) + + def test_missing_full_receipt_warns_and_still_checks_summary_heads(self): + summary = self.record() + path = Path(summary["receipt_path"]) + path.unlink() + output = self.check() + self.assertIn(f"unverified: full receipt not available locally ({path})", output) + self.assertNotIn("predates", output) + self.advance() + self.assertIn("recorded HEAD predates non-card changes", self.check()) + + def test_legacy_full_receipt_block_is_still_accepted(self): + summary = self.record() + full = json.loads(Path(summary["receipt_path"]).read_text()) + self.replace_block(full) + self.assertEqual(self.check(), "") + full["output"] = "changed\n" + self.replace_block(full) + self.assertIn("unverified: receipt checksum mismatch", self.check()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_evidence.py b/tests/test_evidence.py index 520d4a27..01fc018b 100644 --- a/tests/test_evidence.py +++ b/tests/test_evidence.py @@ -57,6 +57,7 @@ class EvidenceTests(unittest.TestCase): self.head = self.git("rev-parse", "HEAD") self.git("update-ref", "refs/remotes/origin/main", self.head) self.receipt = self.base / "receipt.json" + self.evidence_dir = self.base / "evidence" self.card = self.repo / ".task/main.md" self.card.parent.mkdir() self.card.write_text("# Test task\n") @@ -71,6 +72,7 @@ class EvidenceTests(unittest.TestCase): for key in ("CMD", "SRC", "RECEIPT", "IMPORT_RECEIPT", "VALIDATE_RECEIPT", "VALIDATE_RECEIPT_PURPOSE", "TASK_WORK_MODE"): env.pop(key, None) + env["AGENT_EVIDENCE_DIR"] = str(self.evidence_dir) if command is not None: env["CMD"] = command env.update({key: str(value) for key, value in values.items()}) @@ -337,6 +339,58 @@ class EvidenceTests(unittest.TestCase): recorded = self.card.read_text() self.assertEqual(self.import_receipt().returncode, 0) self.assertEqual(self.card.read_text(), recorded) + self.assertEqual(recorded.count("```json"), 1) + + def card_summaries(self): + blocks = self.card.read_text().split("```json\n")[1:] + return [json.loads(block.split("\n```", 1)[0]) for block in blocks] + + def test_card_holds_summary_and_full_receipt_is_stored_outside_repo(self): + result = self.run_evidence("seq 1 30") + self.assertEqual(result.returncode, 0, result.stderr) + text = self.card.read_text() + self.assertIn(" +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-18T03:04:27.873341+00:00", + "command": "make test-affected", + "exit_code": 2, + "cwd": "/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3", + "before_head": "d7e7138ec55613c7a0fa1f2c78757c7678d73ec3", + "after_head": "d7e7138ec55613c7a0fa1f2c78757c7678d73ec3", + "branch": "feature/delivery-model-v3", + "target_branch": "main", + "target_head": "49e9b5fa52ca68858ed78060bb372df10c9ec711", + "root_target": "main", + "immediate_target": "main", + "before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "output_tail": [ + "make[1]: Entering directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'", + "python3 scripts/test_affected.py ", + "[affected] base=origin/main changed=27 targets=test-worktree,test-governance,test-boundaries,test-pins", + "make[2]: Entering directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'", + "python3 scripts/check-worktree.py verify", + "ERROR: uninitialized submodule: /home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3/modules/documents/deps/development-guidelines; initialize and inspect it first", + "make[2]: *** [Makefile:218: test-worktree] Error 1", + "make[2]: Leaving directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'", + "make[1]: *** [Makefile:212: test-affected] Error 2", + "make[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'" + ], + "output_sha256": "f16410beea6b8479fec65bd8a0ab32769298e1c656f4d3d9e350bf5ff0b63408", + "receipt_sha256": "11beb57f2e09f8130e137d3e5e94a162ce5f848e66eda7e71f31436fada5ba09", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/11beb57f2e09f8130e137d3e5e94a162ce5f848e66eda7e71f31436fada5ba09.json" +} +``` + + +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-18T03:04:46.585302+00:00", + "command": "make test-affected", + "exit_code": 0, + "cwd": "/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3", + "before_head": "d7e7138ec55613c7a0fa1f2c78757c7678d73ec3", + "after_head": "d7e7138ec55613c7a0fa1f2c78757c7678d73ec3", + "branch": "feature/delivery-model-v3", + "target_branch": "main", + "target_head": "49e9b5fa52ca68858ed78060bb372df10c9ec711", + "root_target": "main", + "immediate_target": "main", + "before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "output_tail": [ + "ok deps/agent-native agent-intent", + "warn: deps/agent-native deps/development-guidelines is 26b562d2e34f, Dev is 2688fd2be6a5", + "ok deps/agent-native error-code-system", + "ok deps/agent-native logging-system", + "ok deps/agent-squad agent-intent", + "ok deps/agent-squad agent-native", + "warn: deps/agent-squad deps/development-guidelines is 26b562d2e34f, Dev is 2688fd2be6a5", + "ok deps/agent-squad error-code-system", + "ok deps/agent-squad logging-system", + "warn: deps/agent-intent deps/development-guidelines is 26b562d2e34f, Dev is 2688fd2be6a5", + "ok deps/agent-intent error-code-system", + "ok deps/agent-intent logging-system", + "pins ok", + "make[2]: Leaving directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'", + "make[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'" + ], + "output_sha256": "00f34f001b61cf9be90d5a7a6eec7af2e38a78986cf09eece537357c59bc51ef", + "receipt_sha256": "129da9f9364b30bdb7ecb7940aa73d3bd882806c3fd7189aa3d5440d609dfba5", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/129da9f9364b30bdb7ecb7940aa73d3bd882806c3fd7189aa3d5440d609dfba5.json" +} +``` From ae10b951c1a56adc4e2fd15589ec575ff12c84b6 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 04:18:10 +0000 Subject: [PATCH 13/18] Record the sync checkpoint Why: bind the task validation checkpoint to the integration baseline. Evidence: SELF recursive component checks; sync_base=32b585aa0a1c70039cb4bdbd9020390676145d70; before_record=9cc7ceb9f2e19a56ca7cb64a6e42f9a93f51afb5. Impact: task metadata only. Refs: .task/feature-delivery-model-v3.md --- .task/feature-delivery-model-v3.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.task/feature-delivery-model-v3.md b/.task/feature-delivery-model-v3.md index 0ad8f8ee..34bfb4db 100644 --- a/.task/feature-delivery-model-v3.md +++ b/.task/feature-delivery-model-v3.md @@ -263,3 +263,4 @@ L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"— "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/129da9f9364b30bdb7ecb7940aa73d3bd882806c3fd7189aa3d5440d609dfba5.json" } ``` +| 同步(记录前对象) | 32b585aa0a1c70039cb4bdbd9020390676145d70 | 9cc7ceb9f2e19a56ca7cb64a6e42f9a93f51afb5 | 2026-10-18T04:18:10Z | From b63b19e0235467959e2150d70645f97c5ddaa208 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 04:19:06 +0000 Subject: [PATCH 14/18] Record review, sync and final acceptance state in the task card Why: L1 stops accepting requirements; independent review approved; main synced. Evidence: task-sync receipts fdf8a6bf (exit 2) and current sync checkpoint (SELF); review APPROVE (fresh session). Impact: .task only. Refs: PR #608 Co-authored-by: Cursor --- .task/feature-delivery-model-v3.md | 87 +++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/.task/feature-delivery-model-v3.md b/.task/feature-delivery-model-v3.md index 34bfb4db..32348e67 100644 --- a/.task/feature-delivery-model-v3.md +++ b/.task/feature-delivery-model-v3.md @@ -6,7 +6,7 @@ - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 6c90e6c92aca0968158b6fa50ec8065ba0869913 - 工作区:/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3(worktree);分支:feature/delivery-model-v3 -- 分支类型:initiative;initiative_id:feature-delivery-model-v3;root_target:main;immediate_target:main;accepting_requirements:true +- 分支类型:initiative;initiative_id:feature-delivery-model-v3;root_target:main;immediate_target:main;accepting_requirements:false - 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=af43f790b177ff9945bafab8a985234c46569af63270b8ca0bc34516ce73f410;triage_receipt=/tmp/govv3-triage-20261018.json;receipt_sha256=8030b2bf877f30fe42e68ba688877586ec22ea020099863366e6b8e2d47fe4fb - 授权与交付终点:2026-10-18 用户批准计划"分层交付与规范精简"并指示按计划实施至全部待办完成;终点为本 L1 合入 main 并完成收口。未授权:分支保护/平台设置变更、改写历史、发布版本(双钥匙)。 @@ -92,8 +92,8 @@ L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"— | 方案设计 | Documents e2f5729 | 完成 | u1(cursor) | 2026-10-18 | Documents #112 | | 实施计划 | u1–u9 单元包 | 完成 | host | 2026-10-18 | Agent 实施收据表 | | 实施与自检 | 8b769e8 | 完成 | u2–u9(cursor)+ host 直接检查 | 2026-10-18 | 单元收据;host 复跑 test_task*/test_evidence/test_ci_*/test_test_affected | -| 正式测试与验收 | 待定 | 待执行 | — | — | — | -| 代码 Review | 待定 | 待执行 | — | — | — | +| 正式测试与验收 | 6f1968e / ae10b95 | 完成(最终 head CI 待核验) | host | 2026-10-18 | test-affected、task-sync 证据;PR #608 CI | +| 代码 Review | 8b769e8 | APPROVE | code-reviewer(fresh session) | 2026-10-18 | 合入与清理·Review 记录 | | 合入 | 待定 | 待执行 | — | — | — | | 合入核验与收口 | 待定 | 待执行 | — | — | — | @@ -125,9 +125,9 @@ L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"— ## 结果与验证(完成后填写) -- 结果: -- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表> -- 影响与剩余项: +- 结果:R1–R5 已实现并集成(u1–u9);R6 复测计划已登记,执行日约 2026-11-01。 +- 验证:make test-affected PASS(SELF,receipt 129da9f9,466 tests OK/18 skipped);task-sync PASS(SELF,sync_base 32b585a);PR #608 CI 在 6f1968e 上 test/test-pg/svc-realcombo 均 success(GATE);最终 head 的 CI 见 CI 记录。 +- 影响与剩余项:非阻塞后续(独立 Review 提出,未在本 L1 处理):①同分支多份合入前闭合归档卡时 task.sh/evidence.py 静默取第一份,宜与 task_pr_check 一致拒绝;②scripts/test_affected.py 函数内 import 移到顶部,并纳入未跟踪新文件;③AGENTS.md 三段证据表述补"按层级,见分级门禁";④close --self 核对卡内分支行与 PR base;⑤自动 precheck 临时路径改用 mktemp -d。 ## 规划进展与下一步 @@ -141,8 +141,8 @@ L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"— ## 合入与清理 -- Review 记录:NOT_RUN(审阅智能体/平台身份、base/head、结论、问题闭合及重放依据) -- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测) +- Review 记录:独立 Review,Cursor code-reviewer 子代理(fresh_session=true,模型 not-observable,未接收实施会话),base 6c90e6c / head 8b769e8,结论 APPROVE、无阻塞;审阅者 REPLAY 重跑 test_ci_wait/test_test_affected/test_check_evidence/test_evidence/test_task_pr_check/test_task 全部通过;5 条非阻塞建议登记于"影响与剩余项"。 +- 冲突记录:同步 origin/main 32b585a(#610、#611:.task 归档、deps/development-manager、policy/delivery.md +5 行 worker Git 规则)无冲突,merge-tree 干净;组件检查 after-sync 通过。首次 task-sync 因瞬时未跟踪文件被拒(receipt fdf8a6bf,exit 2),重试通过。 - CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果) - 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验) - 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置) @@ -264,3 +264,74 @@ L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"— } ``` | 同步(记录前对象) | 32b585aa0a1c70039cb4bdbd9020390676145d70 | 9cc7ceb9f2e19a56ca7cb64a6e42f9a93f51afb5 | 2026-10-18T04:18:10Z | + + +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-18T04:17:43.583000+00:00", + "command": "make task-sync", + "exit_code": 0, + "cwd": "/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3", + "before_head": "6f1968e1bcdd112a7d8d3f64cec0ac628d2775f3", + "after_head": "ae10b951c1a56adc4e2fd15589ec575ff12c84b6", + "branch": "feature/delivery-model-v3", + "target_branch": "main", + "target_head": "32b585aa0a1c70039cb4bdbd9020390676145d70", + "root_target": "main", + "immediate_target": "main", + "before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "output_tail": [ + " rename .task/{ => archive}/feature-agent-cli-enforcement.md (99%)", + " rename .task/{ => archive}/feature-execenv-python-sandbox-path.md (95%)", + " rename .task/{ => archive}/feature-workbench-flow-completion.md (77%)", + " create mode 100644 .task/archive/fix-agent-exec-terminal-wrapper.md", + "[worktree] before-update: recursive component checks passed", + "Submodule path 'deps/development-manager': checked out '67784f43eb0430bf596c2cda685cbb4946d2e032'", + "[worktree] materialize: recursive component checks passed", + "[worktree] after-sync: recursive component checks passed", + "admit: ok", + "[evidence] WARN JSON receipt line 151: recorded HEAD predates non-card changes ——关键检查须重跑并以 make evidence 重新写入", + "[evidence] WARN JSON receipt line 191: unverified: recorded component state was not at its pinned SHA ——关键检查须重跑并以 make evidence 重新写入", + "[evidence] WARN JSON receipt line 227: recorded HEAD predates non-card changes ——关键检查须重跑并以 make evidence 重新写入", + "[synced] sync_base=32b585aa0a1c70039cb4bdbd9020390676145d70 tested_head=ae10b951c1a56adc4e2fd15589ec575ff12c84b6 checked_at=2026-10-18T04:18:10Z changed=1 sync_target=main", + "[next] 跑正式验收(make test 等),证据写入任务卡", + "make[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'" + ], + "output_sha256": "26d5c33e54a7cbbec422e5a42d02d62e119bba898fc65b280cf82f1ee04dd3be", + "receipt_sha256": "35167e8a947176b88afdfc8563890f1d023186a2514717fc910815a6c0f882ec", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/35167e8a947176b88afdfc8563890f1d023186a2514717fc910815a6c0f882ec.json" +} +``` + + +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-18T04:17:10.174750+00:00", + "command": "make task-sync", + "exit_code": 2, + "cwd": "/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3", + "before_head": "6f1968e1bcdd112a7d8d3f64cec0ac628d2775f3", + "after_head": "6f1968e1bcdd112a7d8d3f64cec0ac628d2775f3", + "branch": "feature/delivery-model-v3", + "target_branch": "main", + "target_head": "32b585aa0a1c70039cb4bdbd9020390676145d70", + "root_target": "main", + "immediate_target": "main", + "before_tracked_diff_sha256": "8fe71487d00e38ab3bd3835b1b8633aa516b0681d34f9286a4209ff00e8132ad", + "after_tracked_diff_sha256": "8fe71487d00e38ab3bd3835b1b8633aa516b0681d34f9286a4209ff00e8132ad", + "output_tail": [ + "make[1]: Entering directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'", + "ERROR: unsaved, untracked files in /home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3; preserve them before continuing", + "make[1]: *** [Makefile:56: task-sync] Error 1", + "make[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'" + ], + "output_sha256": "2a00f6e4683852acb963c36ebe471bc1b029872c358ead2050c6e480cedbdd00", + "receipt_sha256": "fdf8a6bf8a4825c6a9075115fcd8165eaf18e4f0d15109589a66305c43ae5e6c", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/fdf8a6bf8a4825c6a9075115fcd8165eaf18e4f0d15109589a66305c43ae5e6c.json" +} +``` From 0e58b6331bd7727708134a616e0adb72d4204bca Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 04:19:41 +0000 Subject: [PATCH 15/18] Record the final PR gate check in the task card Why: FINAL=1 PR base/head gate before fold-close. Evidence: receipt 7b088696 (SELF), PR_CHECK_OK. Impact: .task only. Refs: PR #608 Co-authored-by: Cursor --- .task/feature-delivery-model-v3.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.task/feature-delivery-model-v3.md b/.task/feature-delivery-model-v3.md index 32348e67..1b3e4a22 100644 --- a/.task/feature-delivery-model-v3.md +++ b/.task/feature-delivery-model-v3.md @@ -335,3 +335,33 @@ L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"— "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/fdf8a6bf8a4825c6a9075115fcd8165eaf18e4f0d15109589a66305c43ae5e6c.json" } ``` + + +```json +{ + "summary_schema": 1, + "source": "SELF", + "timestamp": "2026-10-18T04:19:26.208754+00:00", + "command": "make task-pr-check FINAL=1", + "exit_code": 0, + "cwd": "/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3", + "before_head": "b63b19e0235467959e2150d70645f97c5ddaa208", + "after_head": "b63b19e0235467959e2150d70645f97c5ddaa208", + "branch": "feature/delivery-model-v3", + "target_branch": "main", + "target_head": "32b585aa0a1c70039cb4bdbd9020390676145d70", + "root_target": "main", + "immediate_target": "main", + "before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "output_tail": [ + "make[1]: Entering directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'", + "python3 scripts/task_pr_check.py local --final", + "[verdict] PR_CHECK_OK base=main head=b63b19e0235467959e2150d70645f97c5ddaa208 kind=initiative", + "make[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev-gov-wt-feature-delivery-model-v3'" + ], + "output_sha256": "7b33bf83810670d2bb32c5ffafe41a0d298d0a1b2017288e0874ff9eb0335e61", + "receipt_sha256": "7b0886966b72b9d9b1b139c3c23782ab4c6c0d548067f3a99e38d046679d0576", + "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/7b0886966b72b9d9b1b139c3c23782ab4c6c0d548067f3a99e38d046679d0576.json" +} +``` From e5ba5ffa8aa0fadba886d5c7f71be0688e14891c Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 04:23:58 +0000 Subject: [PATCH 16/18] agent-exec transport 507e19cd3dbf43d3bb3646b3b9a2bcfb/fb1caddea8fe --- AGENTS.md | 6 ++-- scripts/task_triage.py | 59 +++++++++++++++++++++++++++++---------- tests/test_task_triage.py | 56 +++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5d1a05b6..6b7d522a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,10 +86,8 @@ API 前缀 `/api/v1`;偶发 broken pipe / 超时,重试即可)。 - **Git over SSH**:`ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/.git` (.gitmodules 与各仓 origin 均为此格式)。 -- **API token**:`~/.secrets/gitea`,两行键值 `URL_PREFIX=…` 与 - `ACCESS_TOKEN=<40 位 hex>`;取值 `grep ACCESS_TOKEN ~/.secrets/gitea | cut -d= -f2-`, - 用作 `Authorization: token <值>` 请求头。**token 值不入仓、不回显、不写进产出物** - (baseline 硬边界 2:只记录来源、字段与可用状态)。 +- **API token**:`~/.secrets/gitea` HOME/IMC 段各有 `- URL_PREFIX=`/`- ACCESS_TOKEN=<40 hex>`,本仓取 HOME 段(F2): + `awk '/^HOME:/{h=1;next} /^IMC:/{h=0} h&&sub(/^- ACCESS_TOKEN=/,""){print;exit}' ~/.secrets/gitea`,作 `Authorization: token <值>` 头。**token 值不入仓、不回显、不进产出物**(baseline 硬边界 2)。 - **SSH key**:`~/.ssh/home_gitea`(ED25519,公钥指纹 `SHA256:GYXaOXIMeloQ1fKSfdJ/ndgPQzs/Ix7Hk62ekfkfQ20`,Gitea 侧密钥名 GMKtec); `~/.ssh/config` 已有 `git-ssh.u1680832.nyat.app` 主机条目,克隆/推送直接可用。 diff --git a/scripts/task_triage.py b/scripts/task_triage.py index 32c03fd1..c77742c4 100644 --- a/scripts/task_triage.py +++ b/scripts/task_triage.py @@ -35,6 +35,8 @@ DEFAULT_MAX_AGE = 14400 PAGE_LIMIT = 50 MAX_PAGES = 100 SECRETS_FILE = "~/.secrets/gitea" +SECTION_RE = re.compile(r"^([A-Za-z][A-Za-z0-9_-]*):$") +TOKEN_RE = re.compile(r"[A-Za-z0-9._~+/=-]+") CARD_RE = re.compile(r"^\.task/[^/]+\.md$") CANDIDATE_KEYS = ("branch", "head_sha", "card_path", "card_sha256", "pr_number", "pr_state", "pr_base", "pr_head_sha") RECEIPT_KEYS = ("repo", "root_target", "root_target_sha", "requirement", "discovered_at", @@ -117,21 +119,43 @@ def api_base(): return (os.environ.get("GITEA_API_URL", "").strip() or DEFAULT_API).rstrip("/") -def read_token(): - token = os.environ.get("GITEA_TOKEN", "").strip() - if token: - return token +def secrets_file_token(): + """HOME/IMC 分段时只取 HOME 段的 ACCESS_TOKEN(同 policy/facts.md F2);无分段时取第一条。""" try: with open(os.path.expanduser(SECRETS_FILE), encoding="utf-8") as fh: - for raw in fh: - line = raw.strip() - if line.startswith("- "): - line = line[2:].strip() - if line.startswith("ACCESS_TOKEN="): - return line.split("=", 1)[1].strip() or None + lines = fh.read().splitlines() except (OSError, UnicodeDecodeError): return None - return None + first = home = None + sectioned = False + section = None + for raw in lines: + line = raw.strip() + header = SECTION_RE.match(line) + if header: + sectioned = True + section = header.group(1) + continue + if line.startswith("- "): + line = line[2:].strip() + if not line.startswith("ACCESS_TOKEN="): + continue + value = line.split("=", 1)[1].strip() + if first is None: + first = value + if section == "HOME" and home is None: + home = value + return (home if sectioned else first) or None + + +def read_token(): + token = os.environ.get("GITEA_TOKEN", "").strip() or secrets_file_token() + if not token: + return None + if not TOKEN_RE.fullmatch(token): + raise Stop(EXIT_REMOTE, "Gitea token is malformed (expected one value without whitespace); " + "check GITEA_TOKEN or ~/.secrets/gitea") + return token def remote_heads(): @@ -147,15 +171,20 @@ def remote_heads(): def http_get_json(url, token): - request = urllib.request.Request( - url, headers={"Accept": "application/json", "Authorization": f"token {token}"}) try: + request = urllib.request.Request( + url, headers={"Accept": "application/json", "Authorization": f"token {token}"}) with urllib.request.urlopen(request, timeout=20) as response: body = response.read() except urllib.error.HTTPError as exc: - raise Stop(EXIT_REMOTE, f"Gitea API returned HTTP {exc.code}", status=exc.code) + raise Stop(EXIT_REMOTE, f"Gitea API returned HTTP {exc.code}", status=exc.code) from None except (urllib.error.URLError, http.client.HTTPException, OSError) as exc: - raise Stop(EXIT_REMOTE, f"Gitea API unreachable ({type(exc).__name__})") + raise Stop(EXIT_REMOTE, f"Gitea API unreachable ({type(exc).__name__})") from None + except Stop: + raise + except Exception as exc: + # 异常文本可能含 Authorization 头(如 http.client 的 Invalid header value),只报类型名。 + raise Stop(EXIT_REMOTE, f"Gitea API request failed ({type(exc).__name__})") from None try: return json.loads(body.decode("utf-8")) except ValueError: diff --git a/tests/test_task_triage.py b/tests/test_task_triage.py index 3c650243..80231ba5 100644 --- a/tests/test_task_triage.py +++ b/tests/test_task_triage.py @@ -11,6 +11,7 @@ import sys import tempfile import unittest from pathlib import Path +from unittest import mock sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from test_task import FakeGitea # noqa: E402 @@ -397,5 +398,60 @@ class RedactionTests(unittest.TestCase): self.assertEqual(task_triage.redact_url("/tmp/plain/remote.git"), "/tmp/plain/remote.git") +class TokenSafetyTests(unittest.TestCase): + FAKE_A = "a" * 40 + FAKE_B = "b" * 40 + + def setUp(self): + tmp = tempfile.TemporaryDirectory(prefix="triage-token-") + self.addCleanup(tmp.cleanup) + self.home = Path(tmp.name) + + def env(self, token=""): + return mock.patch.dict(os.environ, {"HOME": str(self.home), "GITEA_TOKEN": token}) + + def write_secrets(self, text): + (self.home / ".secrets").mkdir(exist_ok=True) + (self.home / ".secrets" / "gitea").write_text(text, encoding="utf-8") + + def test_multiline_env_token_is_rejected_without_echoing_values(self): + with self.env(f"{self.FAKE_A}\n{self.FAKE_B}"): + with self.assertRaises(task_triage.Stop) as caught: + task_triage.read_token() + self.assertEqual(caught.exception.code, task_triage.EXIT_REMOTE) + self.assertIn("malformed", caught.exception.message) + for value in (self.FAKE_A, self.FAKE_B): + self.assertNotIn(value, caught.exception.message) + self.assertNotIn(value, str(caught.exception)) + + def test_secrets_file_with_sections_yields_home_value(self): + self.write_secrets("IMC:\n- URL_PREFIX=https://imc.invalid\n- ACCESS_TOKEN=" + self.FAKE_B + "\n" + "HOME:\n- URL_PREFIX=https://home.invalid\n- ACCESS_TOKEN=" + self.FAKE_A + "\n") + with self.env(): + self.assertEqual(task_triage.read_token(), self.FAKE_A) + + def test_secrets_file_without_home_access_token_returns_none(self): + self.write_secrets("HOME:\n- URL_PREFIX=https://home.invalid\nIMC:\n- ACCESS_TOKEN=" + self.FAKE_B + "\n") + with self.env(): + self.assertIsNone(task_triage.read_token()) + + def test_missing_configuration_returns_none(self): + with self.env(): + self.assertIsNone(task_triage.read_token()) + + def test_request_value_error_does_not_leak_token(self): + token = self.FAKE_A + leak = ValueError(f"Invalid header value b'token {token}\\n{self.FAKE_B}'") + with mock.patch.object(task_triage.urllib.request, "urlopen", side_effect=leak): + with self.assertRaises(task_triage.Stop) as caught: + task_triage.http_get_json("https://gitea.invalid/api/v1/x", token) + self.assertEqual(caught.exception.code, task_triage.EXIT_REMOTE) + self.assertEqual(caught.exception.message, "Gitea API request failed (ValueError)") + self.assertIsNone(caught.exception.__cause__) + self.assertTrue(caught.exception.__suppress_context__) + for value in (token, self.FAKE_B): + self.assertNotIn(value, caught.exception.message) + + if __name__ == "__main__": unittest.main() From 17ea7f552027dab2c1db783c4903224288d2c158 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 04:25:29 +0000 Subject: [PATCH 17/18] Record the u10 token-leak fix and incident in the task card Why: a malformed token printed credentials in a traceback before merge; u10 fixes it. Evidence: u10 receipt /tmp/govv3-u10-receipt.json (SELF); fake-value repro shows no token in output. Impact: .task only. Refs: PR #608 Co-authored-by: Cursor --- .task/feature-delivery-model-v3.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.task/feature-delivery-model-v3.md b/.task/feature-delivery-model-v3.md index 1b3e4a22..2d29e7c4 100644 --- a/.task/feature-delivery-model-v3.md +++ b/.task/feature-delivery-model-v3.md @@ -48,6 +48,9 @@ L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"— | govv3-u7-lanes-level-gates-doc | policy/delivery.md 层级/通道/门禁 | caefcb72b970 | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | success,apply 快进 | 48a8029 | false | /tmp/govv3-u7-receipt.json | | govv3-u8-evidence-summary-in-card | evidence 摘要入卡、完整收据源码外、check-evidence | caefcb72b970 | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | success,apply 快进 | a28318c | false | /tmp/govv3-u8-receipt.json | | govv3-u9-slim-spec | AGENTS.md 入口化、合入调度并入 delivery.md、去重 | caefcb72b970 | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | success,apply 快进 | 8b769e8 | false | /tmp/govv3-u9-receipt.json | +| govv3-u10-no-token-in-tracebacks | task_triage read_token 校验与 HOME 段选择、http_get_json 不外泄异常文本、AGENTS.md token 取值事实 | caefcb72b970 | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | success,apply 快进 | e5ba5ff | false | /tmp/govv3-u10-receipt.json | + +- 安全事件(2026-10-18,合入前发现):host 按 AGENTS.md 旧事实 `grep ACCESS_TOKEN … | cut` 取值,得到 HOME/IMC 两段拼接的两行值传入 GITEA_TOKEN;`close --self` 经 task_triage.http_get_json 触发 http.client 的 Invalid header value,未捕获的 traceback 把两个 token 明文打印到本会话工具输出。未进入仓库、任务卡、PR 或 CI。处置:u10 修复(格式校验、异常只报类型名、HOME 段选择、AGENTS.md 事实改为 F2 命令)并以假值复现确认;两个 token 需由 owner 轮换(凭据操作,Agent 不代为执行)。 - u2 第一次运行对账:规范检出未变、无 transport、私有工作区保留;处置:单元包改用既有 gitlink 提升模式(本地 clone --no-checkout 后 detach,禁止 submodule/add/update-index),全新派发。 - Documents #111(host 直写 docs/proposals,不在豁免路径内)已关闭并删除分支,改由 u1 撰写。 @@ -127,7 +130,7 @@ L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"— - 结果:R1–R5 已实现并集成(u1–u9);R6 复测计划已登记,执行日约 2026-11-01。 - 验证:make test-affected PASS(SELF,receipt 129da9f9,466 tests OK/18 skipped);task-sync PASS(SELF,sync_base 32b585a);PR #608 CI 在 6f1968e 上 test/test-pg/svc-realcombo 均 success(GATE);最终 head 的 CI 见 CI 记录。 -- 影响与剩余项:非阻塞后续(独立 Review 提出,未在本 L1 处理):①同分支多份合入前闭合归档卡时 task.sh/evidence.py 静默取第一份,宜与 task_pr_check 一致拒绝;②scripts/test_affected.py 函数内 import 移到顶部,并纳入未跟踪新文件;③AGENTS.md 三段证据表述补"按层级,见分级门禁";④close --self 核对卡内分支行与 PR base;⑤自动 precheck 临时路径改用 mktemp -d。 +- 影响与剩余项:非阻塞后续(独立 Review 提出,未在本 L1 处理):①同分支多份合入前闭合归档卡时 task.sh/evidence.py 静默取第一份,宜与 task_pr_check 一致拒绝;②scripts/test_affected.py 函数内 import 移到顶部,并纳入未跟踪新文件;③AGENTS.md 三段证据表述补"按层级,见分级门禁";④close --self 核对卡内分支行与 PR base;⑤自动 precheck 临时路径改用 mktemp -d。待 owner:轮换 ~/.secrets/gitea 中 HOME 与 IMC 两个 ACCESS_TOKEN(见安全事件)。 ## 规划进展与下一步 From 8e85b86912086ce1c92e67ecb143fa44bacf8939 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 04:25:30 +0000 Subject: [PATCH 18/18] Fold-close the task card before merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: close the task card inside its own PR so landing it needs no post-merge backfill or separate close PR. Evidence: .task/feature-delivery-model-v3.md -> .task/archive/feature-delivery-model-v3.md; status 已完成(合入前闭合); PR #608 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/608. Impact: task metadata only; the card is archived within PR #608 and lands with its merge commit. Refs: task.sh close --self --- .task/{ => archive}/feature-delivery-model-v3.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename .task/{ => archive}/feature-delivery-model-v3.md (99%) diff --git a/.task/feature-delivery-model-v3.md b/.task/archive/feature-delivery-model-v3.md similarity index 99% rename from .task/feature-delivery-model-v3.md rename to .task/archive/feature-delivery-model-v3.md index 2d29e7c4..a15413bc 100644 --- a/.task/feature-delivery-model-v3.md +++ b/.task/archive/feature-delivery-model-v3.md @@ -1,6 +1,6 @@ # feature/delivery-model-v3 任务卡 -任务主记录:[PR #608](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/608);状态:进行中;owner:weichen +任务主记录:PR #608 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/608;状态:已完成(合入前闭合);owner:weichen ## 基线与授权 @@ -126,7 +126,7 @@ L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"— - 调度依据:policy/delivery.md#合入调度(原 policy/merge-queue.md);新模式启用前按既有交付门禁 - 固定后证据:源码外 RECEIPT;实际结果后按授权原样导入和闭合 -## 结果与验证(完成后填写) +## 结果与验证 - 结果:R1–R5 已实现并集成(u1–u9);R6 复测计划已登记,执行日约 2026-11-01。 - 验证:make test-affected PASS(SELF,receipt 129da9f9,466 tests OK/18 skipped);task-sync PASS(SELF,sync_base 32b585a);PR #608 CI 在 6f1968e 上 test/test-pg/svc-realcombo 均 success(GATE);最终 head 的 CI 见 CI 记录。 @@ -368,3 +368,9 @@ L1 章程(治理大任务):落实"分层分支交付与规范精简 r1"— "receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/7b0886966b72b9d9b1b139c3c23782ab4c6c0d548067f3a99e38d046679d0576.json" } ``` + +## 闭合记录(合入前折叠) + +- 本卡在 PR #608(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/608)合入前闭合并归档,随该 PR 一同合入。 +- 该 PR 的合并提交即本任务的落地记录。 +- 合入后无需回填,也无需单独的闭合 PR。