feat(butler): ADR-0074 落地——孤儿标签移除 + deadman 双层触发 + hc.io 实配(ADR-0074) - #230
Conversation
…artbeat-watch 仓内兜底)+ DEADMAN_PING_URL 已配实记(W1 运行反馈,owner 授权)
|
Warning Review limit reached
Next review available in: 4 minutes Limit details: You’ve used all 10 included reviews currently available. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?Wait for the limit to reset, then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (7)
Comment |
PR Summary by Qodobutler: ADR-0074 落地(移除孤儿标签检查 + deadman 双层触发)
AI Description
Diagram
High-Level Assessment
Files changed (8)
|
There was a problem hiding this comment.
Pull request overview
This PR implements ADR-0074’s butler “dead-man” refinements by removing the noisy closed-issue “orphan state:* label” check, introducing an internal heartbeat staleness watchdog as a second-layer trigger, and consolidating trip enforcement logic into a single shared script used by both entry workflows.
Changes:
- Remove closed-issue orphan
state:*label scanning frombutler-reconcileand update policy/docs accordingly. - Add
governance/deadman-trip.shas the single enforcement implementation and switch the trip workflow to invoke it. - Add
butler-heartbeat-watchworkflow and new policy thresholddeadman_stale_hoursto trigger trips when the heartbeat workflow’s last successful run is stale.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| governance/policy/butler.yaml | Removes orphan-label check; adds deadman-watch service and deadman_stale_hours threshold. |
| governance/deadman-trip.sh | New shared enforcement core for dead-man trips (breaker variable, strip automerge, open P0). |
| governance/butler-reconcile.sh | Removes orphan-label check and adjusts reporting/audit payloads. |
| docs/deadman-setup.md | Updates runbook to describe dual-layer triggering and current setup status. |
| .github/workflows/gate.yml | Registers deadman-trip.sh for bash syntax gating. |
| .github/workflows/butler-heartbeat-watch.yml | New cron watchdog that checks heartbeat staleness and triggers trip via shared script. |
| .github/workflows/butler-deadman-trip.yml | Refactors trip workflow to call governance/deadman-trip.sh. |
Suppressed comments (1)
governance/butler-reconcile.sh:274
- 本脚本已移除 orphan_state_labels 检查并在 findings/infra-fail 审计 JSON 里删掉了该字段,但 ok 分支的审计 JSON 仍保留
orphan_state_labels":0,会导致同一 butler 的 AUDIT actions 字段 schema 不一致,增加后续机器解析成本。建议从 ok 分支也移除该字段。
if [[ $FINDINGS -gt 0 ]]; then
audit findings "{\"repos\":$REPO_COUNT,\"stale_in_progress\":$STALE_COUNT,\"stale_quarantine\":$QUAR_COUNT,\"report\":\"$REPORT_ACTION\"}"
exit 1
fi
audit ok "{\"repos\":$REPO_COUNT,\"stale_in_progress\":0,\"orphan_state_labels\":0,\"stale_quarantine\":0,\"report\":\"$REPORT_ACTION\"}"
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| STALE_H="${STALE_HOURS_OVERRIDE:-$(python3 -c 'import yaml; print(yaml.safe_load(open("governance/policy/butler.yaml", encoding="utf-8"))["deadman_stale_hours"])' | tr -d '\r')}" | ||
| [[ "$STALE_H" =~ ^[0-9]+([.][0-9]+)?$ ]] || { echo "::error::deadman_stale_hours 非数值: $STALE_H" >&2; exit 2; } | ||
| # 最近一次成功 heartbeat run 的完成时间(workflow 文件名路由;无成功 run = 视为最陈旧) | ||
| LAST=$(gh api "repos/Cloudbird-Software/.github/actions/workflows/butler-heartbeat.yml/runs?status=success&per_page=1" \ | ||
| --jq '.workflow_runs[0].updated_at // empty' 2>/dev/null) | ||
| if [[ -z "$LAST" ]]; then | ||
| if gh api "repos/Cloudbird-Software/.github/actions/workflows/butler-heartbeat.yml/runs?per_page=1" --jq '.total_count' 2>/dev/null | grep -qE '^[0-9]+$'; then | ||
| AGE_S=999999999 # 有 run 但零成功 → 视为远古(会 trip) | ||
| AGE_H="∞(存在 run 但零成功)" | ||
| else | ||
| echo "::error::heartbeat runs 查询失败——fail-closed 变红(不盲 trip)" >&2 | ||
| exit 2 | ||
| fi | ||
| else | ||
| AGE_S=$(( $(date -u +%s) - $(date -u -d "$LAST" +%s) )) | ||
| AGE_H=$(( AGE_S / 3600 )) | ||
| fi | ||
| THRESH_S=$(( STALE_H * 3600 )) | ||
| echo "AUDIT | butler=heartbeat-watch | trigger=${BUTLER_TRIGGER:-schedule} | outcome=checked | actions={\"last_success\":\"${LAST:-none}\",\"age_hours\":\"$AGE_H\",\"threshold_hours\":$STALE_H}" |
| if ! gh api -X PATCH "orgs/$ORG/actions/variables/$CB" -f name="$CB" -f value=true >/dev/null 2>&1; then | ||
| if ! gh api -X POST "orgs/$ORG/actions/variables" -f name="$CB" -f value=true -f visibility=all >/dev/null 2>&1; then | ||
| infra "org 变量 $CB 置位失败(PATCH/POST 均败)" | ||
| fi | ||
| fi | ||
| act "熔断变量 $CB=true 已置位(与 cost-check 共用——宪法 §6 缺席即停;$SRC)" |
| -H "Authorization: Bearer $PAT" \ | ||
| https://api.github.com/repos/Cloudbird-Software/.github/dispatches \ | ||
| -d '{"event_type":"deadman-tripped"}' | ||
| gh api -X POST repos/Cloudbird-Software/.github/dispatches -H "Authorization: Bearer $PAT" -f event_type=deadman-tripped |
| REPORT_BODY="管家 reconcile 报告(唤醒矩阵行 1,ADR-0057,运行 $(date -u +%FT%TZ),trigger=$TRIGGER): | ||
|
|
||
| - 僵尸卡(state:in-progress 停滞 > ${STALE_DAYS}d): $STALE_COUNT | ||
| - 孤儿标签(closed 仍挂 state:*;v1 只报告不纠正——INV-02 状态标签写须 App 令牌经仲裁): $ORPHAN_COUNT | ||
| - 隔离超时(state:quarantine 停滞 > ${STALE_Q_DAYS}d): $QUAR_COUNT | ||
|
|
||
| 孤儿标签清单: | ||
| ${ORPHAN_ROWS:-(无)} | ||
|
|
||
| (僵尸卡/隔离超时的明细见 label butler:needs-human 的 issue;本报告聚合计数与孤儿清单。)" | ||
| (closed issue 的历史 state:* 标签不检——ADR-0074:退出状态机即为历史事实。 | ||
| 僵尸卡/隔离超时的明细见 label butler:needs-human 的 issue;本报告聚合计数。)" |
Code Review by Qodo
1. Stale阈值读取路径错误
|
| echo "::error::缺 GOVERNANCE_TOKEN——陈旧度不可查,fail-closed 变红(不盲 trip)" >&2 | ||
| exit 2 | ||
| fi | ||
| STALE_H="${STALE_HOURS_OVERRIDE:-$(python3 -c 'import yaml; print(yaml.safe_load(open("governance/policy/butler.yaml", encoding="utf-8"))["deadman_stale_hours"])' | tr -d '\r')}" |
There was a problem hiding this comment.
2. Stale阈值读取路径错误 🐞 Bug ≡ Correctness
butler-heartbeat-watch 用 Python 从 governance/policy/butler.yaml 读取 deadman_stale_hours,但该字段实际位于 thresholds.deadman_stale_hours,导致命令替换失败并使该 workflow 永远 exit 2(infra 红、不 trip)。结果是仓内兜底层不可用,无法按 ADR-0074 预期自动触发缺席即停。
Agent Prompt
### Issue description
`butler-heartbeat-watch.yml` reads `deadman_stale_hours` from `governance/policy/butler.yaml` using `yaml.safe_load(...)["deadman_stale_hours"]`, but the policy file defines it under `thresholds.deadman_stale_hours`. This makes the Python snippet raise a KeyError, resulting in an empty `STALE_H` and the workflow exiting with infra (2), effectively disabling the stale watchdog.
### Issue Context
This watchdog is the ADR-0074 “repo-internal layer”; if it always fails infra, deadman will not be automatically tripped when the heartbeat workflow is broken/disabled.
### Fix Focus Areas
- .github/workflows/butler-heartbeat-watch.yml[48-49]
- governance/policy/butler.yaml[58-68]
### Suggested change
Update the Python expression to read the nested key:
- `...["thresholds"]["deadman_stale_hours"]`
Also consider adding a defensive default/error message if the key is missing, so it fails with a clear infra error rather than a confusing empty value.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| THRESH_S=$(( STALE_H * 3600 )) | ||
| echo "AUDIT | butler=heartbeat-watch | trigger=${BUTLER_TRIGGER:-schedule} | outcome=checked | actions={\"last_success\":\"${LAST:-none}\",\"age_hours\":\"$AGE_H\",\"threshold_hours\":$STALE_H}" |
There was a problem hiding this comment.
3. 小数阈值导致算术错误 🐞 Bug ☼ Reliability
butler-heartbeat-watch 允许 deadman_stale_hours 为小数(正则接受 3.5),但随后用 Bash 算术扩展 `THRESH_S=$(( STALE_H * 3600 ))` 计算秒数,Bash 不支持浮点会直接报错并使 job 失败。该失败会把“阈值合法但为小数”的配置误判成 infra 红。
Agent Prompt
### Issue description
`butler-heartbeat-watch.yml` validates `STALE_H` with a regex that allows decimals, but then uses Bash arithmetic expansion to compute seconds. Bash arithmetic is integer-only, so values like `3.5` will cause a runtime arithmetic error.
### Issue Context
Even if today’s default is `3`, the workflow explicitly documents/accepts decimals; future overrides (e.g., drills) can break the watchdog.
### Fix Focus Areas
- .github/workflows/butler-heartbeat-watch.yml[48-66]
### Suggested change (pick one)
1) If decimals are not needed: tighten validation to integers only (`^[0-9]+$`).
2) If decimals are desired: compute `THRESH_S` via Python:
- `THRESH_S=$(python3 -c 'import sys; print(int(float(sys.argv[1])*3600))' "$STALE_H")`
and then keep the `(( AGE_S <= THRESH_S ))` comparison integer-based.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
来源 PR 复查发现物(#176..#390 全量复查): - 误提交 .pyc 出库:scripts/__pycache__(#230 引入)、 specs/IR-0004/suite/__pycache__(#338)、specs/ISSUE-263/suite/__pycache__(#336); 附 .gitignore(__pycache__/、*.pyc)防复发 - governance/metrics.py:移除恒未读局部 val(#253 引入,ADR-0073 计算库) - governance/dashboard-update.py:TemporaryDirectory with 块内 isdir 恒真检查删除 - scripts/sli-report.sh:X if False else 死分支简化 + die 后不可达 OR 链残串清理 (#184/#197-203/#357 落位文件,ADR-0059 口径零改动) 不改动:gate/verdict/drift 语义、workflow YAML、REPOS.yaml/治理数据、pinned SHA。 验证:python -m compileall(除既有 escalate-test-issue.py 语法错, 见 issue 392 另报);bash -n 全绿;governance/tests/test-*.sh 10 套全过; sli-report --self-test PASS=8 FAIL=0;yaml 解析 OK。 Card: #392 ADR: ADR-0059(SLI 口径保持)、ADR-0073(metrics/dashboard 纯函数契约保持)
adversary-gate(ADR-0067/0082)对 specs/** 任意变更(含缓存删除) fail-closed 要求 survived 审计。本卡为技术债清扫而非 spec 套件变更, 按保守原则不触发该审计面:specs/*/suite/__pycache__/*.pyc 暂留库, 作为已申报债务记录于 issue #392 与 PR 描述。 Card: #392 refactor: 构建产物出库 + 死代码移除(行为保持;近一周 PR 技债清扫) 来源 PR 复查发现物(#176..#390 全量复查): - 误提交 .pyc 出库:scripts/__pycache__(#230 引入)、 specs/IR-0004/suite/__pycache__(#338)、specs/ISSUE-263/suite/__pycache__(#336); 附 .gitignore(__pycache__/、*.pyc)防复发 - governance/metrics.py:移除恒未读局部 val(#253 引入,ADR-0073 计算库) - governance/dashboard-update.py:TemporaryDirectory with 块内 isdir 恒真检查删除 - scripts/sli-report.sh:X if False else 死分支简化 + die 后不可达 OR 链残串清理 (#184/#197-203/#357 落位文件,ADR-0059 口径零改动) 不改动:gate/verdict/drift 语义、workflow YAML、REPOS.yaml/治理数据、pinned SHA。 验证:python -m compileall(除既有 escalate-test-issue.py 语法错, 见 issue 392 另报);bash -n 全绿;governance/tests/test-*.sh 10 套全过; sli-report --self-test PASS=8 FAIL=0;yaml 解析 OK。 Card: #392 ADR: ADR-0059(SLI 口径保持)、ADR-0073(metrics/dashboard 纯函数契约保持) (注:specs/*/suite/__pycache__/*.pyc 经重估移出本 PR——adversary-gate 对 specs/** 变更类 fail-closed 要求 survived 审计;作为已申报债务留档 issue #392 与 PR 描述。)
adversary-gate(ADR-0067/0082)对 specs/** 任意变更(含缓存删除) fail-closed 要求 survived 审计。本卡为技术债清扫而非 spec 套件变更, 按保守原则不触发该审计面:specs/*/suite/__pycache__/*.pyc 暂留库, 作为已申报债务记录于 issue #392 与 PR 描述。 Card: #392 refactor: 构建产物出库 + 死代码移除(行为保持;近一周 PR 技债清扫) 来源 PR 复查发现物(#176..#390 全量复查): - 误提交 .pyc 出库:scripts/__pycache__(#230 引入)、 specs/IR-0004/suite/__pycache__(#338)、specs/ISSUE-263/suite/__pycache__(#336); 附 .gitignore(__pycache__/、*.pyc)防复发 - governance/metrics.py:移除恒未读局部 val(#253 引入,ADR-0073 计算库) - governance/dashboard-update.py:TemporaryDirectory with 块内 isdir 恒真检查删除 - scripts/sli-report.sh:X if False else 死分支简化 + die 后不可达 OR 链残串清理 (#184/#197-203/#357 落位文件,ADR-0059 口径零改动) 不改动:gate/verdict/drift 语义、workflow YAML、REPOS.yaml/治理数据、pinned SHA。 验证:python -m compileall(除既有 escalate-test-issue.py 语法错, 见 issue 392 另报);bash -n 全绿;governance/tests/test-*.sh 10 套全过; sli-report --self-test PASS=8 FAIL=0;yaml 解析 OK。 Card: #392 ADR: ADR-0059(SLI 口径保持)、ADR-0073(metrics/dashboard 纯函数契约保持) (注:specs/*/suite/__pycache__/*.pyc 经重估移出本 PR——adversary-gate 对 specs/** 变更类 fail-closed 要求 survived 审计;作为已申报债务留档 issue #392 与 PR 描述。) Co-authored-by: randypanding <randypanding@users.noreply.github.com>
…70) (#478) * chore: 技术债清扫收尾——.pyc 编译产物出库 + escalate-test-issue.py 语法修复(ADR-0061/ADR-0070) - git rm --cached 3 个误提交的 .pyc(specs/IR-0004/suite/__pycache__ ×2、specs/ISSUE-263/suite/__pycache__ ×1,源自 #230/#336/#338;.gitignore 防复发已在位) - scripts/escalate-test-issue.py L97 f-string 内层 ASCII 引号改「」(#392 卡另报的语法错误——修复前 py_compile 失败,脚本不可运行) 卡内其余三项(metrics.py 死变量 / dashboard-update.py isdir 防御 / sli-report.sh 死分支)已在先前 PR 完成,本 PR 仅收尾。 * feat: 处理仓库其他Issues Co-authored-by: traeagent <traeagent@users.noreply.github.com> --------- Co-authored-by: randypanding <randypanding@users.noreply.github.com> Co-authored-by: traeagent <traeagent@users.noreply.github.com>
动机(owner 授权的三项收尾)
butler-heartbeat-watch(6h)检测 heartbeat 成功 run 陈旧度 >deadman_stale_hours(3h) → 自动 trip;外部层(hc.io→owner 告警)承担"Actions 整体静默"形态——ADR-0074 决策 2。DEADMAN_PING_URLorg secret 已配置,butler-heartbeat 实测 ping 成功(run 32508780521 attempt 1/2)。变更
governance/butler-reconcile.sh:检查 (b) 移除(closed 清单不再拉取)governance/deadman-trip.sh(新):trip 执法核心单一实现(trip workflow 与 watch 共用).github/workflows/butler-deadman-trip.yml:改为调脚本(语义不变:脚本 0→run 变红=可见信号).github/workflows/butler-heartbeat-watch.yml(新):陈旧度兜底;API 失败=infra 红不盲 trip(ADR-0040 决策 5 同款)governance/policy/butler.yaml:+deadman_stale_hours、+deadman-watch 服务行、行 1 描述更新docs/deadman-setup.md:双层触发说明 + 当前状态实记(剩余 owner 侧仅 grace 核对与告警通道勾选)验证
回滚:revert 本 PR + 删 watch workflow。
Card: Cloudbird-Software/.github#168