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/.task/archive/feature-delivery-model-v3.md b/.task/archive/feature-delivery-model-v3.md new file mode 100644 index 00000000..a15413bc --- /dev/null +++ b/.task/archive/feature-delivery-model-v3.md @@ -0,0 +1,376 @@ +# feature/delivery-model-v3 任务卡 + +任务主记录:PR #608 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/608;状态:已完成(合入前闭合);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:false +- 需求分流: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 通过后合入) +- 人工决策点:仅限目标/范围/验收变更、重大取舍、高后果操作或无法自动判定的业务验收 +- 当前阶段:正式测试与验收;当前增量:P1–P3 已全部集成,M 已登记 +- 流程依据:仓库根目录 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 实施收据 + +按根目录 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 | 源码外收据路径 | +|---|---|---|---|---|---|---|---|---|---|---| +| 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 | +| govv3-u3-fold-close-self | task.sh close --self、archive 回退查找、文案 | caefcb72b970 | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | success,apply 快进 | 2757b6f | false | /tmp/govv3-u3-receipt.json | +| govv3-u4-ci-wait-parallel-submodules | scripts/ci_wait.py、make ci-wait、--jobs 8 | caefcb72b970 | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | success,apply 快进 | b7f6a8b | false | /tmp/govv3-u4-receipt.json | +| govv3-u5-light-ci-test-affected(第一次运行) | ci.yaml、test_affected、ci_wait 修正 | — | implementation | cursor, antigravity, pi | 0(未启动) | — | invalid-config(单元包含疑似敏感路径名 tests/test_ci_credentials.py)→ fail-stop,无副作用 | 无 | false | /tmp/govv3-u5-receipt.json | +| govv3-u5-light-ci-test-affected-r2 | 同上(去除该路径) | caefcb72b970 | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | success,apply 快进 | fb4c89d | false | /tmp/govv3-u5r2-receipt.json | +| govv3-u6-task-start-light-child-templates | task.sh 子任务免分流、自动 precheck、三模板 | caefcb72b970 | implementation | cursor, antigravity, pi | 1 | cursor (cursor-agent-stable) | success,apply 快进 | 9a3ef83 | false | /tmp/govv3-u6-receipt.json | +| 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 撰写。 + +## 关联方向与规划 + +- 适用状态:适用 +- 主记录路径/版本:modules/documents/docs/proposals/2026-10-18-delivery-model-v3.md @ Documents e2f5729(PROJECT.md 规划首条链接) +- 对应里程碑:交付模型 v3(分层分支交付与规范精简) + +## 需求分析 + +| ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 | +|---|---|---|---|---|---|---| +| R1 | 任务卡合入前折叠闭合(P1) | 闭合 PR 约占 main 推进 40% | P1 | close --self 可用,查找回退仅限 PRE_MERGE_CLOSED,测试覆盖 | — | 已实现(u3) | +| R2 | 统一 CI 等待与子模块并行物化(P1) | 长 sleep 占用会话时间 | P1 | make ci-wait 快速失败/双次全绿确认;--jobs 8 | — | 已实现(u4、u5) | +| R3 | 门禁按层级分配(P2) | L2→L1 不应跑完整重 CI | P2 | 非 main base 的 PR 跳过 test-pg/svc-realcombo;test-affected;登记子任务免分流 | R1 | 已实现(u5、u6) | +| R4 | 任务通道与 L1 章程(P2) | 修复优先、两层上限 | P2 | delivery.md 层级/通道/门禁表 | — | 已实现(u7) | +| R5 | 证据摘要入卡与规范精简(P3) | 归档卡中位 113 KB | P3 | 卡内只存摘要;AGENTS.md 入口化;合入调度单一权威 | — | 已实现(u8、u9) | +| R6 | 复测计划(M) | 验证收益 | P3 | 基线、方法、日期登记在案 | R1–R5 合入 | 已登记(见下节) | + +## 方案与计划 + +- 方案引用:计划"分层交付与规范精简"(用户 2026-10-18 批准);主记录 Documents e2f5729 +- 实施计划引用:单元 u1–u9(见 Agent 实施收据),对应 R1–R6 +- 测试计划引用:各单元直接测试(tests/test_task*.py、test_evidence、test_check_evidence、test_ci_wait、test_test_affected、test_check_worktree);最终 HEAD 的 CI(make test + make build)为 GATE + +## 复测计划(M) + +- 执行日:本 L1 合入 main 后两周(约 2026-11-01);执行者:下一次治理会话。 +- 基线(2026-10-18 采集,来源 SELF:git log 与 Gitea actions tasks API):14 天 main first-parent 推进 410 次(峰值 85/天,合入窗口引入后约 17/天);3.3 天 200 次 CI 运行,中位 8.3 分钟;闭合相关运行约占机时 31%;每 PR 平均 2.4 次 CI;闭合 PR 约占 main 推进 40%;归档卡中位 113 KB。 +- 方法:`git log --first-parent --since=<14天前> origin/main --oneline | wc -l`(按日分组);`GET /api/v1/repos/weichen/Agent-System-Dev/actions/tasks?limit=N` 统计运行次数、run_started_at→updated_at 机时与 event;按 PR 统计 CI 次数及排队/等待时长;`.task/archive/` 新卡大小中位。 +- 判定:main 推进与闭合相关机时明显下降、每 PR CI 次数不升;否则回到主记录复盘。 + +## 阶段记录 + +| 阶段 | 对象版本 | 状态 | 决策/执行者 | 时间 | 依据 | +|---|---|---|---|---|---| +| 需求拆解 | 计划 r1 | 完成 | host(Cursor 主模型) | 2026-10-18 | 计划"分层交付与规范精简" | +| 需求评估 | 计划 r1 | 完成 | host | 2026-10-18 | 基线数据见复测计划(M) | +| 需求表确认 | 计划 r1 | 完成 | 用户批准 | 2026-10-18 | "Implement the plan as specified" | +| 方案设计 | 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 | +| 正式测试与验收 | 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 记录 | +| 合入 | 待定 | 待执行 | — | — | — | +| 合入核验与收口 | 待定 | 待执行 | — | — | — | + +## 探索试验与迭代 + +| 增量/试验 | 假设 | 范围 | 判断和停止条件 | 授权/决策依据 | 结果 | 变更影响 | 需重审补测 | +|---|---|---|---|---|---|---|---| +| 待定义 | — | — | — | 待核对 | 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/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 记录。 +- 影响与剩余项:非阻塞后续(独立 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(见安全事件)。 + +## 规划进展与下一步 + +- 本次推进/剩余:待核对(依据实际成果更新适用方案/规划) +- 首选下一任务:待核对(无适用规划、已完成或受阻时如实说明,不虚构任务) +- 推荐依据:待填写 +- 前置条件/待确认:待核对 +- 预期产物/验收条件:待定义 +- 重复任务核对:待核对(已有/在途任务、PR 与规划记录) +- 启动授权:未授权(建议不自动启动新任务,仍须明确授权) + +## 合入与清理 + +- 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/分支/任务卡处置) + + +```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" +} +``` + + +```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" +} +``` +| 同步(记录前对象) | 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" +} +``` + + +```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" +} +``` + +## 闭合记录(合入前折叠) + +- 本卡在 PR #608(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/608)合入前闭合并归档,随该 PR 一同合入。 +- 该 PR 的合并提交即本任务的落地记录。 +- 合入后无需回填,也无需单独的闭合 PR。 diff --git a/AGENTS.md b/AGENTS.md index ae3422fe..6b7d522a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,164 +2,77 @@ 本仓开发遵循 Development-Guidelines(见 `deps/development-guidelines`,gitlink 固定版本): 核心规则与术语见其 `core/`,按任务触发阅读 `standards/` 专题;通用交付依据为 `workflows/delivery.md`。 -本项目的需求、设计试验、实施调整及交付**必读 [policy/delivery.md](policy/delivery.md)**: -目标、范围和交付终点一次确认;范围内的分析、方案、实施、修复、验证、代码 Review、 -PR 合入及安全收口由智能体连续推进。只在目标/范围/验收条件变更、存在会影响用户结果的 -重大取舍、触及高后果操作,或业务验收必须由用户判定时请求人工决策。智能体解决冲突并确认 -当前 HEAD 的 CI 通过后执行合入,无需另请人工操作。具体决策点、版本及授权条件见 -policy/delivery.md。 +本项目的需求、设计试验、实施调整及交付**必读 [policy/delivery.md](policy/delivery.md)**(交付规则唯一权威): +目标、范围和交付终点一次确认;范围内的分析、方案、实施、修复、验证、代码 Review、PR 合入及安全收口由智能体连续推进。 +只在目标/范围/验收条件变更、存在会影响用户结果的重大取舍、触及高后果操作,或业务验收必须由用户判定时请求人工决策。 +智能体解决冲突并确认当前 HEAD 的 CI 通过后执行合入,无需另请人工操作。本文件是入口摘要,细则以所链文档为准。 ## 立即生效的最高优先级(强制) -- 在既有授权与强制安全边界内,**先交付用户明确要求的产物**。不得用基础设施、工具治理、 - 通用化、加固、重构或“以后可能用到”替代当前目标;尤其禁止把简单 Markdown、配置或说明 - 修改擅自升级成平台、框架、自动化系统或大规模工程。 -- 采用最小充分实现:先让目标可用、系统可用并通过最小直接验证,然后立即停止并交付;体验、 - 结构、性能与治理完善必须作为后续独立增量,不得拖延当前结果。 -- 只修复直接阻塞目标、可用性、明确验收条件或强制安全门禁的问题。高价值非阻塞项只简记, - 低影响噪音直接忽略;Review/迭代循环只围绕阻塞项,阻塞项闭合即结束。 -- 测试与风险成比例:实施中只跑最小直接检查;完整/长耗时套件仅在最终候选、规范明确要求或 - 实际影响面确有必要时运行一次。禁止在每个小文档、配置或 adapter 修改后重复全量测试。 -- 编排成本不得超过任务本身。实施、修复与测试按 AGENT-EXECUTION.md 的候选和尝试上限调度; - 不限制单次 CLI 的推理时长、工具次数、规划方式或首次写文件时间。除非用户明确要求或已有证据 - 证明是当前目标的直接阻塞,否则不得新增嵌套控制器、额外 worktree、复杂收据体系或新架构。 - 执行路由是手段,不能成为推迟用户产物的理由。 +- **先交付用户产物**:在既有授权与强制安全边界内,先交付用户明确要求的产物;不得用基础设施、工具治理、通用化、加固、重构或“以后可能用到”替代当前目标,禁止把简单 Markdown、配置或说明修改擅自升级成平台、框架、自动化系统或大规模工程。 +- **最小充分实现**:目标可用、系统可用并通过最小直接验证后立即停止并交付;体验、结构、性能与治理完善作为后续独立增量,不得拖延当前结果。 +- **只修阻塞项**:只修复直接阻塞目标、可用性、明确验收条件或强制安全门禁的问题;高价值非阻塞项只简记,低影响噪音直接忽略;Review/迭代只围绕阻塞项,阻塞项闭合即结束。 +- **测试与风险成比例**:实施中只跑最小直接检查;完整/长耗时套件仅在最终候选、规范明确要求或影响面确有必要时运行一次,禁止每个小文档、配置或 adapter 修改后重复全量测试。 +- **编排成本不得超过任务本身**:按 AGENT-EXECUTION.md 的候选和尝试上限调度,不限制单次 CLI 的推理时长、工具次数、规划方式或首次写文件时间;除非用户明确要求或有证据证明直接阻塞,不新增嵌套控制器、额外 worktree、复杂收据体系或新架构;执行路由不能成为推迟用户产物的理由。细则见 [最高优先级](policy/delivery.md#最高优先级目标兑现与最小充分交付)。 ## 任务目标与范围(强制) -- 执行优先级固定为:授权与安全边界 → 本次任务目标和系统可用 → 阻塞目标的必要问题 → - 非阻塞改进。完成标准是已批准验收条件成立且受影响系统保持可用,不是把所有附近问题一并清零。 -- 绝对禁止私自扩大范围。影响目标的问题仅在既有授权内作最小充分修复;必要修复超出范围时,说明 - 阻塞、最小扩围和影响,取得授权后再做。“相关”、“顺手”或“更完美”不构成扩围授权。 -- 新发现按对当前目标的影响分流:阻塞验收、导致不可用/明确回归、违反安全边界或必需门禁失败的问题必须 - 处理;其余不阻塞交付。只有具备明确影响和后续价值的非阻塞问题才记录,不为低影响噪音制造新任务。 -- 先实现端到端可用和可验证的最小闭环,再将体验、结构、性能及治理优化作为独立增量迭代;非阻塞 Review - 意见不得阻止当前目标交付。 +- 优先级固定为:授权与安全边界 → 本次任务目标和系统可用 → 阻塞目标的必要问题 → 非阻塞改进。完成标准是已批准验收条件成立且受影响系统保持可用,不是清零所有附近问题。 +- 绝对禁止私自扩大范围:影响目标的问题只在既有授权内作最小充分修复;必要修复超出范围时,说明阻塞、最小扩围和影响,取得授权后再做。“相关”“顺手”“更完美”不构成扩围授权。 +- 新发现按影响分流:阻塞验收、导致不可用/明确回归、违反安全边界或必需门禁失败的必须处理;其余不阻塞交付,只有明确影响和后续价值的才记录。先实现端到端可用的最小闭环,非阻塞 Review 意见不阻止交付(见 [问题分流](policy/delivery.md#目标优先范围控制与问题分流))。 ## 实施执行路由(强制) -- 实施、修复、测试与审阅调度**必读根目录 [AGENT-EXECUTION.md](AGENT-EXECUTION.md)**(唯一项目执行路由配置)。通用工程 Agent 契约(agent-per-unit 隔离执行、会话独立、JSON 终态收据、无 host 隐式代写、受控 transport/admission 准入)遵循 Development-Guidelines [core/baseline.md(硬边界 8)](deps/development-guidelines/core/baseline.md) 与 [standards/agent-execution.md](deps/development-guidelines/standards/agent-execution.md);项目根目录配置仅声明角色候选顺序与适配器参数。 -- 产生仓库变更的实施按可独立实施的单元拆分;任务卡记 `实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)`。 -- 外部 Agent 在新进程/隔离工作区执行一个有界单元;host 负责计划、拆解、范围审阅、权威验证、规范提交、 - PR/CI/合入、证据与任务元数据及安全清理。Worker 不得提交、推送、开或合 PR、部署、改路由或派遣同伴。 -- 实施、修复与测试的正常候选只有 Cursor、Antigravity(`agy`)和 PI,顺序以配置为准。每个候选对 - 安全可恢复失败(`unavailable`,或外部无副作用已被机制证明的 `failed-clean`)最多 3 次全新尝试,然后才换下一候选。 - 单元包的 `network=false`/`external_cost=false` 只是声明,不是证明;没有强制隔离或可信对账时, - 已启动的失败尝试即使私有克隆可精确恢复也记 `failed-unknown`。成功还须从该 CLI 实际事件信封解出 - 有效的 `completed` 终态记录(schema 写在 worker 提示词中);缺失或无效即 fail closed。 - 三者都安全耗尽后,才允许一次全新的 Codex CLI 紧急尝试。Codex 平时只用于规划、方案设计、实施计划 - 或独立 Review,不进入这三个正常角色。`fallback_to_host` 为 `never`。安全耗尽不发出 - host-fallback-authorized,终态为 unavailable 或 blocked,不授权 host 撰写该单元。旧值 - `all-unavailable` 仍可解析,同样不产生 host 撰写授权。 -- 显式 fail-stop 立即停止,不重试、不换候选、不启动紧急尝试:`failed-unknown`、`scope-expansion`、 - `blocked`、`cancelled`、无效配置、未对账 Git 状态与安全失败。总超时,以及 stdin/stdout/stderr - 都没有活动的空闲超时,归入 `failed-unknown`。 -- 一次尝试只在显式终态失败、进程退出、总超时或上述空闲超时时结束。CLI 可在目标、范围与安全边界内 - 自主检查、计划、测试并迭代。 -- 实施变更只经成功 transport 由 `scripts/agent-exec apply` 集成,或由 pre-commit `admit` 按精确内容 - 做本地完整性核对。本地 HMAC 历史与该钩子只是本地完整性/篡改证据(SELF),不是 GATE、可信身份, - 也不防同一主机 OS 用户;本地记录缺失或无效仍拒绝,交付信任只来自精确 HEAD 的 CI 或独立 REPLAY。 - host 直接管理的豁免只有 `.task/**`、`docs/superpowers/specs/**` 与 - `docs/superpowers/plans/**`。host 不直接撰写实施单元。细则见 - [policy/delivery.md](policy/delivery.md) 与 [policy/governance.md](policy/governance.md)。 - 不削弱 goal-first、严格范围、先可用、独立 Review、证据、平台准入与高后果双钥匙规则。 +- 调度**必读 [AGENT-EXECUTION.md](AGENT-EXECUTION.md)**(唯一项目执行路由配置);通用契约遵循 guidelines [core/baseline.md(硬边界 8)](deps/development-guidelines/core/baseline.md) 与 [standards/agent-execution.md](deps/development-guidelines/standards/agent-execution.md)。产生仓库变更的实施按可独立实施的单元拆分,任务卡记 `实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)`。 +- 外部 Agent 在新进程/隔离工作区执行一个有界单元;host 负责计划、拆解、范围审阅、权威验证、规范提交、PR/CI/合入、证据与任务元数据及安全清理。Worker 不得提交、推送、开或合 PR、部署、改路由或派遣同伴。 +- 实施、修复与测试的正常候选只有 Cursor、Antigravity(`agy`)和 PI,顺序以配置为准;每个候选对安全可恢复失败(`unavailable`,或外部无副作用已被机制证明的 `failed-clean`)最多 3 次全新尝试后才换候选。`network=false`/`external_cost=false` 只是声明;没有强制隔离或可信对账时,已启动的失败尝试记 `failed-unknown`。成功须从该 CLI 实际事件信封解出有效的 `completed` 终态记录,缺失或无效即 fail closed。 +- 三者都安全耗尽后才允许一次全新的 Codex CLI 紧急尝试;Codex 平时只用于规划、方案设计、实施计划或独立 Review。`fallback_to_host` 为 `never`(旧值 `all-unavailable` 同样不授权):安全耗尽不发出 host-fallback-authorized,终态为 unavailable 或 blocked,host 不撰写该单元。 +- 显式 fail-stop 立即停止,不重试、不换候选、不启动紧急尝试:`failed-unknown`、`scope-expansion`、`blocked`、`cancelled`、无效配置、未对账 Git 状态与安全失败;总超时及 stdin/stdout/stderr 都无活动的空闲超时归入 `failed-unknown`。一次尝试只在显式终态失败、进程退出、总超时或空闲超时时结束,CLI 可在目标、范围与安全边界内自主检查、计划、测试并迭代。 +- 实施变更只经成功 transport 由 `scripts/agent-exec apply` 集成,或由 pre-commit `admit` 按精确内容核对。本地 HMAC 历史与该钩子只是本地完整性/篡改证据(SELF),不是 GATE、可信身份,也不防同一主机 OS 用户;本地记录缺失或无效仍拒绝,交付信任只来自精确 HEAD 的 CI 或独立 REPLAY。host 直接管理的豁免只有 `.task/**`、`docs/superpowers/specs/**` 与 `docs/superpowers/plans/**`,host 不直接撰写实施单元。细则见 [policy/delivery.md](policy/delivery.md#agent-实施单元与主模型职责) 与 [policy/governance.md](policy/governance.md);不削弱 goal-first、严格范围、先可用、独立 Review、证据、平台准入与高后果双钥匙规则。 ### 阶段与会话隔离(强制) -- 需求分析、方案、设计、计划、实施、测试、Review 与验收默认各启动一个全新会话;不同阶段、 - 不同实施单元不得复用同一 CLI 会话、conversation、thread 或 session。 -- Review 必须由当前主模型执行,或交给**全新且独立**的 CLI session。绝对禁止实施者使用原 session - 通过 `continue`、`resume`、conversation ID、session ID/路径或等价机制继续充当评审者。 -- 独立评审只接收固定的目标/需求、base/head、实际 diff、测试与证据;默认不转交实施聊天、思维过程、 - 中间自评或原会话历史。确需补充信息时只提供可复核产物,不把实施上下文整体灌入评审会话。 -- 每阶段记录工具、模型、stage、attempt/session 身份及 `fresh_session`。工具不暴露 session ID 时标 - `not-observable`,但仍须用新进程并禁用恢复/续聊;无法证明会话独立时,不得把结果标为独立 Review。 +- 需求分析、方案、设计、计划、实施、测试、Review 与验收默认各启动一个全新会话;不同阶段、不同实施单元不得复用同一 CLI 会话、conversation、thread 或 session。 +- Review 由当前主模型或**全新且独立**的 CLI session 执行;绝对禁止实施者通过 `continue`、`resume`、conversation/session ID、session 路径或等价机制继续充当评审者。 +- 评审只接收固定的目标/需求、base/head、实际 diff、测试与证据,默认不转交实施聊天、思维过程、中间自评或原会话历史;确需补充时只提供可复核产物。 +- 每阶段记录工具、模型、stage、attempt/session 身份及 `fresh_session`;工具不暴露 session ID 时标 `not-observable`,但仍须新进程并禁用恢复/续聊;无法证明会话独立时不得标为独立 Review(见 [会话隔离](policy/delivery.md#阶段会话隔离与独立-review))。 ## UI 设计门禁(强制) -- 本仓一切界面生成/修改**先读根目录 `DESIGN.md`**(四原则:美观、现代、简洁、易用), - 再动手;违反视为设计系统缺陷,不是风格偏好。 -- 颜色/圆角/字号只消费 `@ui/tokens.css` 令牌(字号走 `--fs-ui-*` 标尺);动效只用 - `@ui/motion.css` 的 `mk-*` 工具类;折叠一律用 `@ui` Collapsible 原语,禁止 `
`。 -- 守护测试(UI 仓与 Frontend 仓各一份 `design-guard.test.ts`)以棘轮拦截新增裸色值、 - 私有 `@keyframes` 与行内字号;风格调整须在同一变更集中关联 UI 令牌 PR 与 Dev 的 - `DESIGN.md`/gitlink 组合 PR,并验证精确组件 SHA(跨仓不要求两个文件出现在同一 PR)。 +- 本仓一切界面生成/修改**先读根目录 `DESIGN.md`**(四原则:美观、现代、简洁、易用)再动手;违反视为设计系统缺陷,不是风格偏好。 +- 颜色/圆角/字号只消费 `@ui/tokens.css` 令牌(字号走 `--fs-ui-*` 标尺);动效只用 `@ui/motion.css` 的 `mk-*` 工具类;折叠一律用 `@ui` Collapsible 原语,禁止 `
`。 +- 守护测试(UI 仓与 Frontend 仓各一份 `design-guard.test.ts`)以棘轮拦截新增裸色值、私有 `@keyframes` 与行内字号;风格调整须在同一变更集中关联 UI 令牌 PR 与 Dev 的 `DESIGN.md`/gitlink 组合 PR,并验证精确组件 SHA(跨仓不要求两个文件出现在同一 PR)。 - 异步状态只有一个所有者。事件按 seq 处理,过期事件丢弃。 ## 任务启动门禁(强制) -- **新需求分流检查**:每次接到新需求,在创建分支前以远端为准运行 `make task-triage REQ='<需求>' TRIAGE_RECEIPT=<路径>` 检查接纳中的大任务集成分支(`branch_kind=initiative` 且 `accepting_requirements=true`);有候选时再运行 `make task-triage-decide TRIAGE_RECEIPT=<路径> VERDICT=JOIN|NEW … REASON='<理由>'`;随后 `make task-start … TRIAGE_RECEIPT=<路径>` 核验收据,通过后才建分支。三态分流:`JOIN`(建立对应大任务的子任务 `make task-start KIND=child INITIATIVE=feature/ NAME=feature/- TRIAGE_RECEIPT=<路径>`)、`NEW`(建立普通任务或新的大任务 `make task-start [KIND=initiative] NAME=feature/ TRIAGE_RECEIPT=<路径>`)、`DECISION_REQUIRED`(存在候选时的待决策状态;改变范围/优先级的加入由人工决策)。 -- 任务启动或恢复后,**第一次项目写操作前**通过 - `RECEIPT=/tmp/<本次唯一名称>.json CMD="make precheck" make evidence` 运行一次启动检查并保存 - 完整脱敏收据(不要求已有任务卡);按输出完成必读。`SHARED_WORKSPACE_BUSY / SYNC_REQUIRED / - REMOTE_UNVERIFIED` 均不得进入写入流程;失败后排除原因再启动,不把缓存远端当最新事实。 -- 新任务运行 `make task-start NAME=feature/ TRIAGE_RECEIPT=<分流收据路径> PRECHECK_RECEIPT=/tmp/<本次唯一名称>.json`, - 建立独立 worktree 与任务卡并导入首次收据。恢复已有登记任务在原 worktree 用 - `make task-resume PRECHECK_RECEIPT=/tmp/<本次唯一名称>.json` 验证并原样导入收据。 - 默认 delivery 要求严格 READY;局部恢复接口、启用条件与旧收据处理见 [开发门禁](policy/governance.md#启动与恢复)。任务中途不重复启动检查。 -- **分支层级**:任务分支一律从权威 `origin/main` 切(`task-start` 已强制);同批互不依赖的 - 子任务用同一 R0 的兄弟分支。确有顺序依赖需从父 feature 分支切子分支时(collaboration.md - 允许的堆叠例外):启动命令增加 `PARENT=feature/<父slug> PARENT_PR=<父PR链接>`, - 任务卡记录主干 R0、父分支及其 SHA、父 PR;子分支命名 `/<父slug>-<子题>`, - 不嵌 SHA;合入顺序固定父→子;父合入后子分支须 `make task-sync` 并入最新 main 并把 PR - base 改回 main,方可声明可合入(分支保护要求 head 含 origin/main,未同步必被拦)。 -- **验收、请求审核、合入前**各运行一次 `make task-sync`,并运行 `make task-pr-check`(大任务收口用 `FINAL=1`)核验 PR base/head 门禁;完成声明的 precheck、task-sync、 - 测试三段输出由 `make evidence` 写入任务卡(见「证据与验证门禁」),不手抄。 -- **多个分支交付 / CI 调度**:先读 [合入调度](policy/merge-queue.md),核对采纳状态、协调者、 - 记录及当前窗口。新模式尚未启用时保留既有同步节点;启用后最终同步至合入核验共用窗口, - 同基线 `task-sync` 只核验,固定后检查用源码外 RECEIPT 保存(见 [证据流程](policy/governance.md#候选固定前后))。 -- 合入后核对所有写者退出;能从当前会话、进程、锁与工作区状态证明时,智能体在 - **已登记任务 worktree 根目录**运行 - `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 同样由智能体按 - policy/delivery.md 完成 Review、冲突处置及当前 HEAD 的 CI 核验后合入。 +- **新需求分流**:创建分支前以远端为准运行 `make task-triage REQ='<需求>' TRIAGE_RECEIPT=<路径>` 检查接纳中的大任务(`branch_kind=initiative` 且 `accepting_requirements=true`);有候选时运行 `make task-triage-decide TRIAGE_RECEIPT=<路径> VERDICT=JOIN|NEW … REASON='<理由>'`;`make task-start … TRIAGE_RECEIPT=<路径>` 核验收据通过后才建分支。`JOIN` 建子任务(`KIND=child INITIATIVE=feature/ NAME=feature/-`),`NEW` 建普通任务或新大任务(`[KIND=initiative] NAME=feature/`),`DECISION_REQUIRED` 为有候选时的待决策状态,改变范围/优先级的加入由人工决策。已登记在父卡 `## 子任务登记` 的计划内 L2 子任务可免分流收据。 +- **启动检查**:启动或恢复后、第一次项目写操作前运行一次 `RECEIPT=/tmp/<本次唯一名称>.json CMD="make precheck" make evidence` 保存完整脱敏收据并按输出完成必读;`make task-start` 省略 `PRECHECK_RECEIPT` 时自动捕获,恢复在原 worktree 用 `make task-resume PRECHECK_RECEIPT=<路径>`。`SHARED_WORKSPACE_BUSY / SYNC_REQUIRED / REMOTE_UNVERIFIED` 均不得进入写入流程,失败后排除原因再启动,不把缓存远端当最新事实。默认要求严格 READY,任务中途不重复;局部恢复见 [开发门禁](policy/governance.md#启动与恢复)。 +- **分支层级**:一律从权威 `origin/main` 切,同批互不依赖的子任务用同一 R0 的兄弟分支。确有顺序依赖的堆叠例外:加 `PARENT=feature/<父slug> PARENT_PR=<父PR链接>`,卡记主干 R0、父分支及 SHA、父 PR,命名 `/<父slug>-<子题>` 不嵌 SHA,合入顺序父→子,父合入后子须 `make task-sync` 并把 PR base 改回 main 才可合入(见 [policy/delivery.md](policy/delivery.md) 与 [开发门禁](policy/governance.md#启动与恢复))。 +- **同步与 PR 门禁**:验收、请求审核、合入前各运行一次 `make task-sync` 与 `make task-pr-check`(大任务收口用 `FINAL=1`);三段完成证据由 `make evidence` 写入任务卡。层级、任务通道与分级门禁见 [层级与分级门禁](policy/delivery.md#层级任务通道与分级门禁),多分支交付的最终窗口见 [合入调度](policy/delivery.md#合入调度)。 +- **回收**:合入后核对所有写者退出;能从当前会话、进程、锁与工作区状态证明时,智能体在**已登记任务 worktree 根目录**运行 `TASK_WRITERS_STOPPED=1 make task-finish`,无法证明时才请 owner 确认。父子仓改动、未跟踪/ignored 唯一文件先保存或显式处置;删除被拒时保留现场,禁止强删兜底。放弃未合入工作用 `make task-finish ABANDON="--abandon <原因>"` 并记录原因,不豁免现场保护。 +- **任务卡闭合**:默认在最终同步/合入前于任务 worktree 运行 `make task-close SELF=1`(卡改为“已完成(合入前闭合)”、写入本 PR 链接并归档,随同一 PR 合入;会提交并推送本分支,只处理本分支的卡)。post-merge 钩子仅提示。主检出 `make task-close` 或 `FOLD=--fold` 只用于已合入未闭合的遗留卡,须核对只含目标卡,其闭合 PR 同样经智能体 Review、冲突处置与当前 HEAD CI 后合入。precheck `[cards]` 欠账验收前须清零。 ## 证据与验证门禁(强制) -原则:**「声明已就位」≠「能力已就位」**。任何来源(用户输入、方案、报告、他人或自己的 -叙述)的结果声明都不是证据;证据 = 命令 × 被测对象 SHA × 可复核输出,三者齐备才算事实。 -规范依据:guidelines `standards/verification.md`「声明与证据来源」(draft.10)。 - -- **来源三分**:产出物(任务卡、PR、报告、review 结论)中的通过类断言须标注来源—— - `GATE`(可核验身份及固定对象的 CI/受信执行器)/ `REPLAY`(独立审阅者的重跑记录)/ - `SELF`(作者或本地工具记录,含 precheck)。工具写入、摘要和来源标签不提升可信等级。 - 合入与验收须核验 GATE 的运行链接/身份及 SHA,或 REPLAY 的独立审阅者、命令和对象; - 本地 evidence 只生成 SELF,不能靠 `SRC` 自行升级;与重验不一致时以重验为准。 -- **证据由工具写入**:命令输出进入任务卡/PR 一律经 `CMD="<命令>" make evidence` 写入 - (记录脱敏命令、执行前后对象、退出码、完整脱敏输出与摘要)。凭据只经环境或受保护文件 - 引用;命令、输出和摘要统一脱敏,摘要只证明记录完整性,不证明执行者身份。手抄、转写、 - 凭记忆重构的输出视为无效证据;补写输出与伪造测试同级红线。 -- **完成声明三段输出**(precheck / task-sync / 测试)必须由 evidence 工具写入任务卡, - PR 描述引用任务卡段落,不另手抄。测试通过的权威源是 CI(`.gitea/workflows/ci.yaml`, - 在 PR HEAD 上 `make test`+`make build`),本地输出降级为辅助线索。 -- **验收即重放**:请求审核后,审阅者先在评审环境重放最小重放集(关键门禁命令 + 变更 - 直接相关检查)再读作者证据;重放与声明不一致直接阻塞,并作为失实事件登记。 -- **外部事实零信任**:用户/文档/旧报告里的「可用/已就位」必须先执行 `policy/facts.md` - 登记表中对应验证命令(或当场实测)才能写进产出物;无法当场复核的结论标 `[未经证实]`, - 禁止裸写。登记表每条事实带 `last_verified` 与 TTL,precheck `[facts]` 报告过期项, - `make facts-verify` 逐条重验。 +- **声明不是证据**:「声明已就位」≠「能力已就位」;任何来源的结果声明都不是证据,证据 = 命令 × 被测对象 SHA × 可复核输出(guidelines `standards/verification.md`「声明与证据来源」)。 +- **来源三分**:通过类断言标注 `GATE`(可核验身份及固定对象的 CI/受信执行器)/ `REPLAY`(独立审阅者的重跑记录)/ `SELF`(作者或本地工具记录,含 precheck)。工具写入、摘要和标签不提升可信等级,本地 evidence 只生成 SELF,不能靠 `SRC` 升级;合入与验收须核验 GATE 的运行链接/身份及 SHA,或 REPLAY 的审阅者、命令和对象,与重验不一致以重验为准。 +- **证据由工具写入**:命令输出只经 `CMD="<命令>" make evidence` 写入(卡内写摘要块,完整脱敏收据一次性存于仓外,见 [开发门禁](policy/governance.md#本地记录与独立证据));凭据只经环境或受保护文件引用。手抄、转写、凭记忆重构的输出无效,补写输出与伪造测试同级红线。 +- **完成声明三段输出**(precheck / task-sync / 测试)由 evidence 写入任务卡,PR 描述引用不另手抄;测试通过的权威源是 CI(`.gitea/workflows/ci.yaml` 在 PR HEAD 上 `make test`+`make build`),本地输出只是辅助线索。 +- **验收即重放**:请求审核后,审阅者先在评审环境重放最小重放集(关键门禁命令 + 变更直接相关检查)再读作者证据;不一致直接阻塞并登记为失实事件。 +- **外部事实零信任**:用户/文档/旧报告中的“可用/已就位”须先执行 `policy/facts.md` 对应验证命令或当场实测才能写入产出物,无法复核标 `[未经证实]`;登记项带 `last_verified` 与 TTL,precheck `[facts]` 报告过期项,`make facts-verify` 逐条重验。 - **诚实出口**:未执行/未验证是合法状态,如实标注(NOT_RUN/未验证)优于补写。 ## 提交与 PR -- 主题行:英文祈使句一行(≤100 列,不以句点结尾);body 写 `Why: / Evidence: / Impact: / Refs:` - 四段;任务卡与 PR 描述为同一份文档。 +- 主题行:英文祈使句一行(≤100 列,不以句点结尾);body 写 `Why: / Evidence: / Impact: / Refs:` 四段;任务卡与 PR 描述为同一份文档。 - 跨仓 gitlink 变更须经组合验证(`make test`)后在 Dev 仓推进;受保护分支经 PR 合入。 -- **高后果操作双钥匙**:改写历史、解除/变更分支保护、删除归档引用、发布版本——Agent 只准备 - 方案与命令,由用户确认并亲手执行;Agent 不得自行实施(baseline 硬边界 1 的授权范围要求)。 -- 模块边界、公开契约与隔离档位见 `policy/modules/`(如有)与 PROJECT.md。 -- 验证命令见 PROJECT.md「验证」节;完成声明的三段输出按「证据与验证门禁」节由 - `make evidence` 写入任务卡,不手抄。 +- **等待 CI** 只用一条阻塞命令 `make ci-wait SHA=<完整 SHA>`(15 s 轮询、失败即停、默认 20 分钟超时); + 禁止手写 `sleep` + `curl` 轮询循环,禁止对同一 SHA 重复/并行轮询;等待期间去做其他独立工作。 +- **高后果操作双钥匙**:改写历史、解除/变更分支保护、删除归档引用、发布版本——Agent 只准备方案与命令,由用户确认并亲手执行;Agent 不得自行实施(baseline 硬边界 1 的授权范围要求)。 +- 模块边界、公开契约与隔离档位见 `policy/modules/`(如有)与 PROJECT.md;验证命令见 PROJECT.md「验证」节。 - 首次克隆本仓后运行 `make setup` 启用本地 pre-commit 钩子。 -- 治理脚本改动须运行 `make test-governance`:临时真实仓库覆盖正常、拒绝及异常路径;该套件 - 同时纳入 `make test`。证据、工作区和 CI 凭据边界的操作细则见 `policy/governance.md`。 +- 治理脚本改动须运行 `make test-governance`(临时真实仓库覆盖正常、拒绝及异常路径,已纳入 `make test`);证据、工作区和 CI 凭据边界细则见 `policy/governance.md`。 ## Gitea 与 Git 访问事实(2026-09-30 实测;登记表 `policy/facts.md`) @@ -173,10 +86,8 @@ policy/delivery.md。 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/Makefile b/Makefile index 77f2f6f9..f95d380d 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)" @@ -45,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: @@ -54,8 +55,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) @@ -64,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];退出码随被执行命令。 @@ -96,8 +103,9 @@ 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 结论(失败即停)" agent-exec-validate: scripts/agent-exec validate --repo . @@ -110,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 @@ -197,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/PROJECT.md b/PROJECT.md index 02a0d283..5a6c64a6 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 基座编成一个二进制并校验激活。收录不构成批准。 @@ -61,7 +63,7 @@ U2 起实施与部署分别评审。 ## 验证 -多个分支向 Dev main 交付时按[并行开发与串行合入](policy/merge-queue.md)执行:等待分支不追 main,轮到后同步一次、固定候选、运行当前 HEAD 的完整 CI,再由智能体合入。实际采纳范围见[采纳记录](policy/adoption.md)。 +多个分支向 Dev main 交付时按[合入调度](policy/delivery.md#合入调度)执行:等待分支不追 main,轮到后同步一次、固定候选、运行当前 HEAD 的完整 CI,再由智能体合入。实际采纳范围见[采纳记录](policy/adoption.md)。 `make test` 先核对子模块工作树与固定指针,再运行治理脚本、公开边界、跨仓指针、库及各模块 测试。实际目标以根 Makefile 为准。`make test-governance` 可独立检查开发门禁,使用临时 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 diff --git a/policy/adoption.md b/policy/adoption.md index acfea99a..97238e44 100644 --- a/policy/adoption.md +++ b/policy/adoption.md @@ -7,7 +7,7 @@ - 版本权威:父仓 gitlink - 批准/生效依据:Dev PR#1(19/19 正负例 + make test);guidelines PR#1 - 规范源码版本:`0.1.0-draft.12`,实际合入提交 `2688fd2be6a52c0bf110e7e6366b6f291ab5e4eb`(Guidelines PR #7);本仓 gitlink(`deps/development-guidelines`)固定该源码版本,版本权威为父仓 gitlink。 -- 合入调度通用版本:`0.1.0-draft.11`,实际合入提交 `2f5b19872be2a4597838a4131ad75d1be089ee5e`(Guidelines PR #5);draft.12 未修改其合入调度文件(standards/merge-queue.md、templates/merge-queue.md)。Dev 采用 [manual 串行合入规则](merge-queue.md):并行开发,轮到后才同步、跑最终 CI 并由智能体合入。该规则不要求先完成全写者审计或第二轮试行,也不宣称自动互斥已经实现。 +- 合入调度通用版本:`0.1.0-draft.11`,实际合入提交 `2f5b19872be2a4597838a4131ad75d1be089ee5e`(Guidelines PR #5);draft.12 未修改其合入调度文件(standards/merge-queue.md、templates/merge-queue.md)。Dev 采用 [manual 串行合入规则](delivery.md#合入调度):并行开发,轮到后才同步、跑最终 CI 并由智能体合入。该规则不要求先完成全写者审计或第二轮试行,也不宣称自动互斥已经实现。 - 过程计划固定:Documents PR #96 实际合入 `129cc3579d9c9e3edfb46c99270d65f3e1dad0a0`,包含既有主干文档及获批 U1–U6 计划;当前进展以任务/PR 记录为准。 | 规范/工作流 | 适用条件与范围 | 执行方式/证据 | diff --git a/policy/delivery.md b/policy/delivery.md index 5854fcab..23993d1c 100644 --- a/policy/delivery.md +++ b/policy/delivery.md @@ -162,7 +162,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`。 @@ -173,9 +173,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>`。 + ## 目标优先、范围控制与问题分流 执行顺序为: @@ -217,7 +251,7 @@ Review 只将阻塞项列为必修;非阻塞意见标明后续建议,不得 正式测试和最终 Review 的顺序不限制设计阶段试验、实施期测试或提前审阅。 文本合并无冲突不证明组合兼容;合入后仍核验真实内容与被测候选是否一致。 -多个就绪任务的最终验证与合入按 [合入调度](merge-queue.md) 的采纳状态执行。 +多个就绪任务的最终验证与合入按 [合入调度](#合入调度) 的采纳状态执行。 新模式生效后,阶段 7–9 在同一目标交付窗口内完成;对象未变时各节点重复核验, 不因记录检查再创建提交或重跑有效 CI。窗口领取不代替已触发的必要决策、独立 Review 或平台准入; 未生效时继续既有同步规则。固定后的证据按 [源码外收据流程](governance.md#候选固定前后) 保存。 @@ -315,6 +349,42 @@ Review 只将阻塞项列为必修;非阻塞意见标明后续建议,不得 对 head/base、依赖、需求或契约变化重新判断证据及评审的有效性,按影响补测和复审。 阶段状态、测试状态、代码审核状态及合入状态分别记录,不以一个“完成”互相替代。 +## 合入调度 + +状态:active;执行方式:manual。目标是多个分支并行开发,同时避免各分支随 `main` 移动反复同步、 +解决冲突和重跑最终 CI。本节只安排 Dev `refs/heads/main` 的最终交付顺序;需求、授权、阶段状态和 +证据仍在原任务卡,受保护分支的最终准入仍由 Gitea 执行。 + +- **队列**:Dev main 只用一份[合入队列 #518](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/issues/518), + 只记录顺序和当前窗口:任务/PR、owner、依赖、就绪状态、当前 base/head、CI、合入结果。 +- **职责**:协调智能体维护顺序和窗口;任务 owner 负责分支内容和必要整改;窗口内的同步、冲突处置、 + Review、当前 HEAD CI 核验和所有 PR 合入由智能体执行。 +- **范围**:只有推进 Dev main 的 PR 使用窗口,即 standalone/修复 PR、L1→main 与遗留卡闭合 PR;L2→L1 合入不排窗口 + (见 [层级、任务通道与分级门禁](#层级任务通道与分级门禁))。组件仓按各自目标分支单独安排。 +- **入队与顺序**:入队表示内容基本就绪且已取得所需授权,不要求已含最新 main。依赖未完成或待必要 + 人工决策的任务标 `blocked`,不占窗口。默认按就绪顺序;`fix/`、`hotfix/` 优先,插队须在 #518 写明原因。 +- **等待**:等待分支继续独立开发、提交和局部检查,但不因 main 前进而合并最新 main、解决无关冲突、 + 推送新最终候选或重跑完整最终 CI,只记“待轮到后同步”;开发期检查不作为之后新 HEAD 的准入证据。 + +同一时间只给一个 Dev main 任务最终窗口。领取前确认前一窗口已释放、依赖已完成,并在 #518 记录 +任务、PR、owner、当前 source HEAD 和领取时间。窗口内按序执行: + +1. 运行一次 `make task-sync` 纳入最新 `origin/main`,解决冲突并核对组件组合; +2. 完成并提交候选内容(含任务卡),固定 `base_sha` 与最终 `head_sha`; +3. 运行任务要求的本地检查,固定后的收据保存在源码外(见[候选固定前后](governance.md#候选固定前后)),不为记结果改变 HEAD; +4. 对固定 HEAD 运行完整 CI(`test`、`test-pg`、`svc-realcombo`),用 `make ci-wait SHA=<完整 SHA>` 等待; +5. 完成当前 HEAD 的必要人工决策(如有)和智能体 Review,合入前再核对 main、head、冲突和三项 CI; +6. 以固定 `head_commit_id`、`force_merge=false` 经 PR 合入,回读实际 main 与候选一致后释放窗口,结果写回 #518。 + +同一 base/head 下,验收、Review 和合入前的 `make task-sync` 只核验(`changed=0`),不创建额外提交, +也不重复仍有效的完整 CI。目标 base、候选 HEAD、组件 pin、检查定义或相关环境任一变化,旧最终结果即失效; +受影响任务回队等待新窗口,不复制旧绿色状态、不为省 CI 降低检查。窗口外仍有 main 推进时按对象变化处理并记录。 + +冲突、代码/Review/CI 失败或等待必要人工决策时,当前任务保存现场、标 `blocked` 并释放窗口,其它分支继续 +局部开发。合入请求超时或结果不明时先查询 PR 与 main 的真实状态,查清前暂停该目标,不重复发送合入请求。 +本流程不建设、也不得推断已存在自动队列、优先级、合并列车、批量预测合并、原子租约或旧执行者 fencing, +不修改分支保护、CI workflow 或 runner 配置。 + ## 合入、清理与恢复 ### 智能体 Review 与 PR 合入 @@ -358,8 +428,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..41904cbe 100644 --- a/policy/governance.md +++ b/policy/governance.md @@ -16,21 +16,8 @@ 源码外;未知终态(`failed-unknown`)与 `scope-expansion` 的隔离工作区须保留待对账,不得静默丢弃。 清理须有精确恢复或保留现场的可复核证据。 -实施、修复与测试的正常候选只有 Cursor、Antigravity(`agy`)和 PI。每个候选对安全可恢复失败 -最多 3 次全新尝试,全部安全耗尽后才允许一次 Codex CLI 紧急尝试。Codex 平时只用于规划、方案设计、 -实施计划或独立 Review。`fallback_to_host` 为 `never`。安全耗尽不发出 host-fallback-authorized, -终态为 unavailable 或 blocked,不授权 host 撰写实施单元。显式 fail-stop 立即停止。一次尝试只在显式终态失败、进程退出、总超时, -或 stdin/stdout/stderr 都没有活动的空闲超时时结束;推理时长、工具次数、规划方式和首次写文件时间 -都不是结束条件。CLI 可在目标、范围与安全边界内自主检查、计划、测试并迭代。路由细则见 -[迭代交付](delivery.md)。 - -实施变更进入仓库,只通过成功 transport 的 `scripts/agent-exec apply`,或 pre-commit `admit` -对暂存内容的本地完整性核对。本地 HMAC 执行历史与该钩子只记作本地完整性/篡改证据(SELF), -不是 GATE、可信身份,也不防同一主机 OS 用户;本地记录缺失或无效仍拒绝,交付信任来自精确 HEAD 的 -CI 或独立 REPLAY。host 可以直接管理的豁免只有 `.task/**`、`docs/superpowers/specs/**` -与 `docs/superpowers/plans/**`。host 不直接撰写实施单元。`failed-unknown`、`scope-expansion`、 -无效配置、未对账 Git 状态与安全失败都不授权继续路由或代写。`network=false`/`external_cost=false` -声明不证明外部干净,已启动的失败尝试在没有强制隔离或可信对账时记 `failed-unknown`。 +候选、尝试上限、Codex 紧急尝试、fallback、fail-stop、尝试结束条件,以及 transport/admit +与 host 豁免路径,以[迭代交付](delivery.md#agent-实施单元与主模型职责)为唯一权威。 ## 启动与恢复 @@ -57,7 +44,7 @@ make task-start NAME=feature/my-task TRIAGE_RECEIPT=/tmp/my-task-triage.json PRE 默认 `TASK_WORK_MODE=delivery`,继续要求严格 READY 和当前目标一致。 工作区忙或落后时先由 owner 保存现场并同步;远端不可达时只继续不依赖新鲜基线的阅读和分析。 -局部恢复接口已具备,正式交付仍按[合入调度](merge-queue.md)的单一最终窗口执行。 +局部恢复接口已具备,正式交付仍按[合入调度](delivery.md#合入调度)的单一最终窗口执行。 启用后,既有任务可以显式选择 local,保持原 R0,标记落后,继续已授权局部工作: ```bash @@ -103,6 +90,11 @@ PR 合入由智能体执行:先完成有效 Review,再同步并解决冲突 命令含敏感值时展示脱敏形式,并记录原命令摘要。执行前后对象分别记录;命令改变 HEAD 时不把执行前 SHA 冒充执行后对象。不能读取执行后状态时明确记录缺项。 +写入任务卡(`CMD` 直写或 `IMPORT_RECEIPT` 导入)时,卡内只放摘要块(命令、退出码、前后 HEAD、 +输出尾部及各摘要);完整收据以 0600 一次性写到仓外的记录路径(默认 +`~/.cache/agent-system-dev/evidence/.json`,可用 `AGENT_EVIDENCE_DIR` 覆盖), +不覆盖不同内容。`check-evidence` 在本机可读时用完整收据核对摘要,缺失则警告未验证;两者仍是 SELF。 + ### 候选固定前后 固定前,完成同步、必要检查、任务卡及本地收据提交,核对干净的组件组合,再读取和推送最终 HEAD。 @@ -138,7 +130,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/policy/merge-queue.md b/policy/merge-queue.md index 480515b4..cd24ad33 100644 --- a/policy/merge-queue.md +++ b/policy/merge-queue.md @@ -1,74 +1,4 @@ -# 并行开发与串行合入 +# 并行开发与串行合入(已并入) -状态:active;执行方式:manual。目标是让多个分支并行开发,同时避免所有分支随着 -`main` 持续移动而反复同步、解决冲突和重跑最终 CI。 - -本规则只安排 Dev `refs/heads/main` 的最终交付顺序。各任务的需求、授权、阶段状态和证据 -仍保存在原任务卡;受保护分支的最终准入仍由 Gitea 执行。 - -## 队列与职责 - -- Dev main 使用一份[合入队列 #518](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/issues/518)。 -- 队列只记录顺序和当前窗口:任务/PR、owner、依赖、就绪状态、当前 base/head、CI、合入结果。 -- 协调智能体维护顺序和窗口;任务 owner 负责分支内容和必要整改;窗口内的同步、冲突处置、Review、当前 HEAD CI 核验和所有 PR 合入由智能体执行。 -- 普通功能 PR、修复 PR 和任务卡闭合 PR 都会移动 main,因此使用同一队列。组件仓按各自目标分支单独安排。 - -入队表示内容基本就绪且已取得该任务所需授权,不要求分支已经包含不断移动的最新 main。 -依赖未完成或仍待必要人工决策的任务标为 `blocked`,不占窗口。默认按就绪顺序处理;紧急插队须在 -#518 写明原因。 - -## 开发与等待 - -等待中的分支继续独立开发、提交和运行必要的局部检查,但不因为别的任务合入 main 就自动: - -- 合并最新 main; -- 解决与最终交付无关的冲突; -- 推送新的最终候选; -- 重跑完整的最终 PR CI。 - -main 前进时,等待任务只记录“待轮到后同步”。开发期检查可以按任务需要运行,但不能作为之后 -新 HEAD 的最终准入证据。 - -## 最终交付窗口 - -同一时间只给一个 Dev main 任务最终窗口。领取窗口前,协调智能体确认前一窗口已经释放、依赖 -已完成,并在 #518 记录任务、PR、owner、当前 source HEAD 和领取时间。 - -窗口内按以下顺序执行: - -1. 运行一次 `make task-sync`,纳入当时最新的 `origin/main`,解决冲突并核对组件组合。 -2. 完成需要进入候选的代码、文档和任务卡内容,提交后固定 `base_sha` 与最终 `head_sha`。 -3. 运行任务要求的本地检查;固定后的命令收据保存在源码外,避免为了记结果再改变 HEAD。 -4. 对固定 PR HEAD 运行现有完整 CI;当前必需作业为 `test`、`test-pg`、`svc-realcombo`。 -5. 完成当前 HEAD 的必要人工决策结论(如有)和智能体 Review。合入前再次核对 main、head、冲突和三项 CI。 -6. 智能体使用固定 `head_commit_id`、`force_merge=false` 经 PR 合入,回读实际 main 与候选内容一致后释放窗口。 - -同一 base/head 下,验收、Review 和合入前的 `make task-sync` 只核验,输出 `changed=0`,不创建 -额外提交,也不重复仍然有效的完整 CI。 - -## 对象变化与失败 - -以下任一对象变化都会使旧的最终结果失效:目标 base、候选 HEAD、组件 pin、检查定义或与结果 -相关的环境。受影响任务保留旧结果的适用对象,回到队列等待新窗口;不得复制旧绿色状态或为了 -省 CI 而降低检查。 - -冲突、代码/Review/CI 失败或等待必要人工决策时,当前任务保存现场、标为 `blocked` 并释放窗口, -其它分支继续局部开发。合入请求超时或返回不明时,先查询 PR 和 main 的真实状态;结果查清前 -保持该目标暂停,不重复发送合入请求。 - -若窗口之外仍有 main 推进,当前固定候选按对象变化处理。协调智能体记录实际推进和受影响任务, -然后继续按队列安排;不要求为启用本规则证明所有历史写者、所有角色或自动互斥机制已经覆盖。 - -## 验收标准 - -一次排队交付满足以下条件即完成: - -- 等待任务没有仅为追 main 而反复生成最终候选或重跑最终 CI; -- 轮到的任务只在窗口内同步最新 main,冲突在固定候选前解决; -- 实际合入的 HEAD 拥有当前必需 CI 和有效 Review; -- 合入由智能体执行,实际 main 内容与受审候选一致; -- 窗口结果写回 #518,随后才处理下一任务。 - -本规则不建设自动优先级、合并列车、批量预测合并、原子租约、旧执行者 fencing,也不修改 -分支保护、CI workflow 或 runner 配置。将来需要自动队列时另行设计;不得从本 manual 流程推断 -这些能力已经存在。 +本规则已并入 [policy/delivery.md 合入调度](delivery.md#合入调度),以该节为唯一权威; +本文件只为保持旧链接可用而保留。 diff --git a/policy/tool-adaptation-matrix.md b/policy/tool-adaptation-matrix.md index b5efa38f..88ddb3b5 100644 --- a/policy/tool-adaptation-matrix.md +++ b/policy/tool-adaptation-matrix.md @@ -16,7 +16,7 @@ | A7 证据 | 本地 SELF 收据;外部来源独立核验 | 首次捕获、完整脱敏输出/摘要、对象记录、原样导入 | `tests/test_evidence.py`;CI/审阅链接另核 | | A8 阶段记录与迭代 | goal-first + agent-per-unit(AGENT-EXECUTION.md);模板默认连续执行,仅真实决策点需人工,没有全流程授权强制;实施/修复/测试仅 Cursor、agy、PI,每候选对安全可恢复失败最多 3 次全新尝试,全部安全耗尽后一次 Codex CLI 紧急尝试;Codex 平时仅规划、设计、计划或独立 Review;显式 fail-stop 立即停止;不限制推理时长、工具次数、规划方式或首次写文件时间;集成经成功 transport 的 agent-exec apply,或 pre-commit admit 精确内容来源;host 豁免仅为 .task/** 与 docs/superpowers/specs/**、plans/**;host 不直接实施;fallback_to_host 为 never;安全耗尽不发出 host-fallback-authorized,终态为 unavailable 或 blocked | policy/delivery.md、AGENT-EXECUTION.md;任务卡分别记录阶段状态、决策点、Agent 实施收据(配置摘要/角色/候选/attempt/CLI-模型/终态/transport/紧急尝试/源码外路径)、Review、冲突、当前 HEAD 的 CI 和合入;post-merge 默认仅提示 | `tests/test_task.py` 核对 goal-first/agent-per-unit 模板、正常路由、尝试上限、fail-stop、空闲超时与 apply/admit 来源表述,及钩子不自动写入;`scripts/agent-exec validate`(CI)与 parser·adapter·controller 假 CLI 契约(Development-Manager internal/agentexec 测试,经 deps/development-manager 钉住)为本地 SELF 强制行为;已安装 provider/model 身份待 U5,远端平台准入为 GATE;智能体核对授权范围、实施单元边界、Review、冲突及匹配 HEAD 的 CI/平台准入 | | A9 长期方向与后续任务 | manual;模板不校验规划有效性或自动调度任务 | policy/delivery.md;PROJECT.md 提供规划入口,任务卡关联规划/里程碑并记录下一步建议 | `tests/test_task.py` 核对生成字段与未授权默认值;智能体核对当前规划、进展、重复任务和建议依据,新任务仍不自动获得授权 | -| A10 交付窗口 | manual;无自动互斥或接管 | [合入调度](merge-queue.md)与 #518:同一目标一个最终窗口,等待分支不追 main;轮到后同步、固定、验证并由智能体合入 | 每个实际 PR 核对窗口记录、精确 HEAD 三项 GATE、Review 和合入后 main;不要求额外试行或全写者审计 | +| A10 交付窗口 | manual;无自动互斥或接管 | [合入调度](delivery.md#合入调度)与 #518:同一目标一个最终窗口,等待分支不追 main;轮到后同步、固定、验证并由智能体合入 | 每个实际 PR 核对窗口记录、精确 HEAD 三项 GATE、Review 和合入后 main;不要求额外试行或全写者审计 | | A11 固定后证据 | manual 选择源码外位置;工具捕获绝对未用路径/原始输出;无 OS 冻结 | policy/governance.md;RECEIPT 捕获,后续授权闭合再原样导入 | `tests/test_evidence.py` 固定对象、权限、覆盖拒绝、原样导入与幂等 | ## 失败处理 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/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..813cda27 --- /dev/null +++ b/scripts/ci_wait.py @@ -0,0 +1,161 @@ +#!/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(skipped/warning 视同 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") +# skipped:分级门禁下 base 非 main 的 PR 跳过全量作业;与 warning 一样是非失败终态。 +DONE_OK = ("success", "skipped", "warning") +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): + """满足全绿(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 + 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/evidence.py b/scripts/evidence.py index b534860c..a40a5e84 100644 --- a/scripts/evidence.py +++ b/scripts/evidence.py @@ -14,15 +14,19 @@ 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") 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 +259,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,14 +322,92 @@ 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") +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/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..a64e6d01 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" @@ -230,7 +261,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) @@ -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' ## 交付调度 - 调度状态:未入队;调度记录:无 - entry_id:无;attempt_id:无;窗口执行者:无 - 固定候选:无(base/head、组件与相关配置引用实际尝试) -- 调度依据:policy/merge-queue.md;新模式启用前按既有交付门禁 +- 调度依据:policy/delivery.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}" \ @@ -456,6 +530,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 +543,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":根目标为集成分支、未完成、仍接收需求且未合入/待闭合 @@ -502,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 @@ -665,8 +758,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 @@ -854,7 +1040,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_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/scripts/task_triage.py b/scripts/task_triage.py index acf83b24..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", @@ -51,10 +53,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(): @@ -116,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(): @@ -146,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}") + 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/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_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_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..79c5275d --- /dev/null +++ b/tests/test_ci_wait.py @@ -0,0 +1,235 @@ +"""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_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") + 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() diff --git a/tests/test_evidence.py b/tests/test_evidence.py index e179cbc3..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("