Day 18 / 共 20 天 · 第 4 周 平台化与收官

CI 流水线与治理闸门

前面讲的"可信"都是能力;CI 是把这些能力变成"合并前强制执行"的地方。不达标,代码进不来。昨天(Day 17)我们把楼盖好、部署上了生产;今天讲交付前那道「竣工 + 消防验收」——把 Critic、闸门、评测、成本、BOM 这些前 17 天学的"设施",在合并前挨个查一遍,不合格就贴封条不发钥匙。这是"可信靠机制不靠自觉"这句话的落地现场。

📍 你在 20 天里的位置(第 4 周:平台化与收官)
D16 门户 D17 部署 D18 CI 流水线与治理闸门 D19 起新 Agent D20 收官
💡 用一个类比先兜住今天(延续「盖楼/物业」世界观) CI 就是楼盘交付前的「竣工验收 + 消防验收」流程。前 17 天我们把消防栓、烟感、承重墙(Critic/闸门/评测/成本)都装好了——但"装了"不等于"合格"。CI 是那位铁面验收员:拿着清单挨个查(7 个 stage),任何一项不达标就贴封条、禁止交付(阻止 merge),绝不会"这里差一点,下次改吧"地放行。可信从"靠师傅自觉"变成"靠验收员把关"——这就是本讲的灵魂 R-CI-DEFAULT-FAIL。
L01

CI = 把"可信"变成硬约束

前 17 天讲了 Critic、闸门、评测、成本治理、BOM 版本锁……但这些如果只是"能力",总有人偷懒不用。CI(持续集成,.gitlab-ci.yml)的作用就是在合并前强制跑一遍,不达标就拦住

核心原则叫 R-CI-DEFAULT-FAIL:任何一道 gate 失败都阻止 merge(allow_failure: false)。

🚫

默认失败原则(R-CI-DEFAULT-FAIL)

闸门不是"建议",是"硬门"。评测掉了、成本涨了、BOM 版本漂了、敏感度红线碰了——任一触发,流水线红、合并按钮点不动。可信从"靠自觉"变成"靠机制"。

L02

.gitlab-ci.yml 走读:stage 与"默认失败"

CI 的一切写在根目录 .gitlab-ci.yml。先看它声明的 stage 顺序(.gitlab-ci.yml:28):

.gitlab-ci.yml:28-35
stages:
  - lint                          # BOM 审计
  - test                          # pytest
  - ci-gates                      # 核心治理闸门群(最凶,L03-L06 拆它)
  - canary                        # 金丝雀(L08)
  - publish-internal              # tag 触发发 Nexus wheel
  - build-publish-portal-frontend # 门户前端镜像
  - nightly-sync                  # 仅 schedule 触发 · UI 仓 → monorepo 同步

PR 从提交到合并,要顺序穿过这些 stage,前一个全绿才进下一个。真正的治理重头戏在 ci-gates 这一层。它的"默认失败"是靠一个共享模板 .ci-gate-base 实现的(.gitlab-ci.yml:57):

.gitlab-ci.yml:57-73
.ci-gate-base:                    # 所有闸门 extends 它
  stage: ci-gates
  image: python:3.12-slim
  before_script:
    - pip install --quiet "uv==${UV_VERSION}.*"
    - uv sync --all-packages      # 和 Day 02/17 本地环境一致
  allow_failure: false            # ← R-CI-DEFAULT-FAIL · 任一 fail 阻 merge
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "master"'

# 每道闸只写 script 一行,其余全继承上面
ci-gate-eval:
  extends: .ci-gate-base
  script:
    - uv run python scripts/ci/full_agent_eval.py --changed

逐行讲:allow_failure: false 是灵魂——GitLab 里如果某个 job 设 allow_failure: true,它挂了流水线仍算通过(只是黄一下);设 false,它一挂整条流水线红、merge 按钮就点不动。所有闸门都 extends: .ci-gate-base 继承这个 false,于是每道闸都是"硬门"。每个具体闸门自己只需写一行 script,环境准备(装 uv、sync)全从基模板继承——DRY。

你的 PR 提交 lintBOM 审计 testpytest ci-gates核心闸门群 canary金丝雀 ✅ 全绿 → 可 merge 🚫 任一红 → 拦 ci-gates 里:eval回归 / 成本 / 敏感度红线 / BOM版本 / DB迁移…全 allow_failure:false R-CI-DEFAULT-FAIL:任一道闸红,合并按钮就点不动
图注(控制流):PR 顺序穿过 7 个 stage;任何一道 allow_failure:false 的闸不达标,整条流水线变红、merge 锁死。
💡 设计取舍:为什么把 allow_failure: false 放在共享基模板里,而不是每个闸各写一遍?如果让每道闸自己写 allow_failure,只要有一个人漏写(默认就是 false?不,GitLab 默认 false,但显式声明+集中管理更安全),或者某天有人图省事偷偷改成 true 放自己过,"硬门"就破了。集中在基模板声明一次,等于把"闸门必须是硬的"这条元规则本身也锁死了——想放水得改基模板,改基模板 review 时一眼就能看见。这是"用结构强制策略"的思路。
🚶 第一人称·你就是那个 PR,走一遍验收 「现在你是一个 PR,改了 sre-rca 一段取证逻辑,想合进主干——
① 先撞 lint 门bom-audit 查你有没有偷偷绕过 BOM 直接 pin 版本(L03)。
test 门:全套 pytest(FakeLLM、不联网)跑一遍。
ci-gates 门(最凶):eval 掉分没?成本涨了没?碰红线没?——任一 allow_failure:false 的 job 挂,当场贴封条
④ 全过才到 canary 小流量试跑(L08)。想合并不是'我觉得没问题',而是'每一道机器闸都说没问题'——这就是可信靠机制。」
L03

BOM 审计闸走读(呼应 Day 05/17)

第一道 stage lint 只有一个 job:bom-audit,它跑 scripts/check_bom_lockfile.py.gitlab-ci.yml:41)。这个脚本文件头把它干的三件事和退出码写得清清楚楚(check_bom_lockfile.py:22):

scripts/check_bom_lockfile.py:8-24(文件头注释,原话裁剪)
# 三件事:
# 1. Adoption audit(默认 strict)
#    扫 14 个 apps//pyproject.toml + portal backend,看是否引 ai-trust-toolkit-bom
# 2. Drift check(永远 fail)
#    BOM manifest.py 的 LOCKED_VERSIONS dict 必须跟 BOM pyproject.toml extras 一致
# 3. Toolkit version pin(永远 fail)
#    uv.lock 里 ai-trust-toolkit 锁定版本必须 = BOM 的 ==0.5.0(否则有人装了野生 toolkit)
#
# Exit code:
#   0 = 全过 / 1 = drift OR pin 不齐 / 2 = strict 下 adoption 不齐

大白话: 数一遍所有 agent(AGENTS_DIRS 列表,check_bom_lockfile.py:45)是不是都引了 BOM——少一个都不行。 校验 BOM 自己的版本清单(Python 里的 LOCKED_VERSIONS dict)和它的 pyproject.toml 声明一致。 校验 uv.lock 里锁的 toolkit 版本 = BOM 规定的版本,防止有人绕过 BOM 装了个野生版本。

exit 0
三件全过 → 流水线绿。
exit 1
drift(版本清单不一致)或 pin 不齐 → allow_failure:false → 阻 merge。
exit 2
strict 模式下 adoption 不齐(有 agent 没接 BOM)→ 阻 merge。.gitlab-ci.yml:49 注释原话"adoption < 15/15 → exit 2 → fail"。
为什么这么在意依赖? 20+ 个 agent 共享一套 toolkit,如果各自 pin 不同版本,很快就"这个 agent 要 0.5、那个要 0.6",地狱般的依赖冲突。强制走 BOM(Day 05)+ CI 审计,保证全平台永远用同一组对齐的版本——这是 monorepo 能规模化的前提。CI 里还有另两道细化闸 ci-gate-bom-pin.gitlab-ci.yml:125,禁直 pin 子模块)和 ci-gate-bom-version-sync:131,三方版本号一致),跟 bom-audit 分工。
💡 设计取舍:为什么用"exit code 分级"(0/1/2)而不是简单的"过/不过"?不同的失败严重度不同、处理方式也不同。exit 2(adoption 不齐)在 --no-strict 的 dev 调试模式下可以退化成 warning(方便本地开发),而 exit 1(drift / pin 错)"永远 fail"绝不放水。用细分的退出码把"这是硬错误"和"这个可以视场景放宽"区分开——同一个脚本既能当 CI 硬门,也能当本地宽松自检。
L04

评测回归闸走读(呼应 Day 10)

Day 10 的评测能力,在这里变成硬门 ci-gate-eval,跑 scripts/ci/full_agent_eval.py --changed.gitlab-ci.yml:86)。先看它评哪几维、用什么标准判"退步"(full_agent_eval.py:52):

scripts/ci/full_agent_eval.py:52-54, 87-105
DIMENSIONS = ["latency", "quality", "cost", "red_line", "critic_pass"]   # 5 维
REGRESS_SIGMA = 2.0                                                       # 超 2σ 算退步

def detect_regress(agent, dim, pr_value, baseline_mean, baseline_std, sigma=2.0):
    """判定 pr_value 相对 baseline 是否 regress > sigma。"""
    if baseline_std <= 0:
        return None                                       # 没 baseline std → 跳过
    deviation = abs(pr_value - baseline_mean) / baseline_std   # 偏离几个标准差
    if deviation > sigma:
        return f"{agent}.{dim} regress · pr={pr_value:.3f} ... ({deviation:.2f}σ > {sigma}σ)"
    return None                                           # 在 2σ 内 → 不算退步

逐行讲:对每个 agent 的 5 个维度,拿这次 PR 的分数 pr_value 和历史基线的均值/标准差比。deviation = |pr - mean| / std 就是"偏离了几个标准差"。超过 2σ 就判定退步——为什么用统计学的 σ 而不是"掉了 5% 就算"?见下面取舍。

另一半亮点是 --changed 模式怎么"只验改动的 agent"(full_agent_eval.py:139):

full_agent_eval.py:139-164
def _get_changed_agents() -> list[str]:
    """`--changed` 模式 · grep git diff 看哪些 apps/<agent>/ 改了。"""
    target = os.environ.get("CI_MERGE_REQUEST_DIFF_BASE_SHA") or "HEAD~1"
    out = subprocess.check_output(["git", "diff", "--name-only", f"{target}..HEAD"], ...)
    changed = set()
    for line in out.splitlines():
        if not line.startswith("apps/"):     # 只看 apps/ 下的改动
            continue
        agent_dir = line.split("/")[1]        # apps/doc-checker/xxx → doc-checker
        if agent_dir in AGENTS:
            changed.add(agent_dir)
    return sorted(changed)
📝 举个例子:--changed 只验改动的那户 你这个 PR 只改了 apps/doc-checker/...
_get_changed_agentsgit diff --name-only,发现只有 doc-checker 变了 → 返回 ["doc-checker"]
→ 只重跑 doc-checker 的 5 维评测(不必把 14 个 agent 全验一遍,省几十分钟)
→ 任一维 deviation > 2σ → 🚫 流水线红。就像验房只重验你刚装修那一户,没动的邻居不用重验。
💡 设计取舍:为什么用"2σ"这种统计阈值,而不是"掉 5% 就 fail"?因为 LLM 评测天然有波动——同一个 agent 跑两次,分数本就会小幅上下抖。如果用固定百分比阈值,正常抖动就会频繁误报(flaky),大家很快就学会"重跑一下碰运气",闸门形同虚设。用 (偏离两个标准差)意味着"这次的分数已经明显超出了历史正常波动范围"——把'真退步'和'正常噪声'区分开,误报率低、可信度高。baseline_std <= 0 时直接跳过(:93),是防止除零的边界处理。
闭环:线上发现 Agent 判错 → 用 case-to-eval-sample skill 把那个 case 变成 eval 样本追进 golden set → 以后任何 PR 都会跑这条样本 → 同样的错误不会再溜过去。每一次线上事故都变成一道永久的回归防线——这是评测进 CI 最有价值的地方。
L05

成本闸走读:diff 拦截 + label 放行

ci-gate-costscripts/ci/cost_impact.py.gitlab-ci.yml:118),拦的是 Day 11 那个 cost-routing.json 的改动。核心算法就一个除法(cost_impact.py:120):

scripts/ci/cost_impact.py:120-126
def compute_diff(baseline: dict, pr: dict) -> float:
    """返 (pr_total - baseline_total) / baseline_total · 单位 1 = 100%。"""
    baseline_total = sum(baseline.values())
    pr_total = sum(pr.values())
    if baseline_total <= 0:
        return 0.0                                    # 防除零边界
    return (pr_total - baseline_total) / baseline_total   # 涨幅比例

它拿 master 的成本向量(baseline)和这次 PR 的成本向量(都用 estimate_cost_vectorcost-routing.json 算出),算出总成本涨了百分之几。判定逻辑(cost_impact.py:187 起):

cost_impact.py:56-57, 187-196
DEFAULT_THRESHOLD = 0.10                     # 默认阈值 10%
REQUIRED_LABEL = "cost-impact-reviewed"      # 放行标签

if abs(diff) <= threshold:
    print(f"[OK] diff ≤ {threshold*100:.0f}% · PASS")
    return 0                                 # 涨幅在 10% 内 → 过
labels = ... _get_labels_from_env()          # 读 CI_MERGE_REQUEST_LABELS
if REQUIRED_LABEL in labels:                 # 超阈值,但 MR 打了"已审查"标签
    print(f"[OK] diff > 阈值 但已含 `{REQUIRED_LABEL}` label")
    return 0                                 # → 放行(人工审查过了)
# 否则列出 top 成本涨幅的 agent,fail(enforce 模式下 return 1)
💡 设计取舍①:涨幅超阈值不是直接拒绝,而是"有 cost-impact-reviewed 标签就放行",为什么留这个口子?因为成本涨有时是合理的——比如某 agent 确实需要升到更强的模型才能达标。一刀切拒绝会挡住正当需求。设计成"超阈值 → 默认拦 → 但人工审查确认后打个标签就能过",等于把'异常涨幅'从'静默通过'变成'必须有人显式签字'。既拦住手滑的误改,又给正当变更留了合规通道。红线闸(L06)也是同款 label-escape 机制。
💡 设计取舍②:脚本里有个 enforce=False(observe-only)模式(cost_impact.py:142/207),为什么?这是灰度上线闸门本身的手法。一道新闸刚加时,先跑 observe-only:算 diff、打印出来,但不真的 fail。观察一两周,确认阈值合理、不误伤,再切成 enforce=True 真拦。闸门自己也要"金丝雀"——上来就硬拦,很可能因阈值没调好把一堆正常 PR 挡在外面,反而逼大家想办法绕过它。
📝 举个例子:一次会被成本闸拦下的 PR 你在 cost-routing.jsondoc-checker 从便宜的 haiku 改成贵的 sonnet
estimate_cost_vector 算出 pr 总成本比 baseline 涨了 40%
compute_diff 返回 0.40,abs(0.40) > 0.10 阈值
→ MR 没打 cost-impact-reviewed 标签 → 🚫 fail,逼你要么说明理由加标签、要么改回去。相当于"想动承重墙(高危配置),验收员立刻要专项审批"。
L06

敏感度红线闸走读(呼应 Day 08)

ci-gate-red-linescripts/ci/red_line_regression.py.gitlab-ci.yml:112),守 DD-001 数据主权/敏感度红线。它不看"分数退步",而是看"这次 PR 有没有红线文件/代码"。检测逻辑(red_line_regression.py:100):

scripts/ci/red_line_regression.py:44-49, 100-118
RED_LINE_KEYWORD_RE = re.compile(
    r"(SENSITIVITY_LEVEL|sensitivity_level\s*=|red_line\s*=|RED_LINE_SERVICES)")
REQUIRED_LABEL = "red-line-reviewed"

def detect_red_line(files: list[str], diff_text: str) -> list[str]:
    reasons = []
    # ① 改动的文件路径命中红线目录/文件名(如含 sensitivity)
    for f in files:
        if _path_matches_red_line(f):
            reasons.append(f"red-line file: {f}")
    # ② diff 内容里新增/删除行命中红线关键词
    for line in diff_text.splitlines():
        if line.startswith(("+", "-")) and RED_LINE_KEYWORD_RE.search(line):
            reasons.append(f"red-line keyword in diff line: {line[:80]!r}")
            break                                # 一次足够触发,不刷屏
    return reasons                               # 空 = 没碰红线

大白话:两条探针—— 看这次改的文件路径有没有命中红线目录(business-data 包、文件名含 sensitivity 等); 看 diff 的增删行里有没有出现 SENSITIVITY_LEVELred_line= 这些敏感关键词。任一命中就返回一条 reason。判定跟成本闸同款(red_line_regression.py:123):碰了红线 + 没打 red-line-reviewed 标签 = FAIL;没碰 = 直接过。

⚠️ 边界:注意 :113-115 特意跳过 diff 的 +++/--- 文件头行——否则文件名里带 sensitivity 的 diff header 会误触。还有 break:117):命中一次关键词就够触发了,不再遍历全部 diff 行刷屏。这些都是"让检测既灵敏又不啰嗦"的细节处理。
💡 设计取舍:为什么红线闸是"改了就拦"(diff 拦截),而不像 eval 闸那样"效果变差才拦"?因为敏感度红线(哪些 service 是高敏、分级边界在哪)是安全/合规底线,改错的后果是数据泄漏级的,不能等"看效果"——效果暴露时可能已经出事了。所以策略换成"只要 diff 碰到这些文件/关键词,就必须有 DD-001 owner 签字(打标签)才放行"。把"改动本身"当作信号,而不是"改动的效果"。高危区域用最保守的门禁,这是纵深防御的体现。
L07

DB 迁移 & 兼容矩阵

还有几道闸值得一提。ci-gate-db-dryrun.gitlab-ci.yml:96)会起一个临时的真数据库来验迁移脚本:

.gitlab-ci.yml:96-109
ci-gate-db-dryrun:
  extends: .ci-gate-base
  services:
    - name: pgvector/pgvector:pg16     # 起一个带 pgvector 的临时 PG
      alias: postgres
  variables:
    DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/test"
    PGVECTOR_DIM: "1536"
  script:
    - uv run python scripts/ci/db_migration_dryrun.py   # 真跑 alembic upgrade + downgrade

它用 GitLab CI 的 services 起一个真的 pgvector/pgvector:pg16 容器,然后真跑一遍 alembic upgrade + downgrade——保证数据库 schema 迁移脚本不仅能升、还能回滚

ci-gate-matrix
.gitlab-ci.yml:76:14 agent × 5 extras 兼容矩阵,用 parallel: matrix: SHARD:[1..5] 分 5 片并行跑(当前是 --dry-run 占位)。
ci-gate-frontend
:142/155:门户前端 Playwright e2e + Lighthouse 性能预算、Day 16 讲的双仓 drift 检测(ci-gate-frontend-sync)和镜像只读校验(ci-gate-frontend-readonly)。
publish-internal
toolkit-vX.Y.Z tag 触发,把 wheel 包发到公司内部 Nexus(供独立仓 agent 用一行 BOM 依赖拉取,Day 19 会提)。
💡 设计取舍:为什么 DB 迁移要连 downgrade 一起验,而不只验 upgrade?因为生产出事时,最需要的是"能快速退回上一个版本"。如果只验了 upgrade、downgrade 脚本其实是坏的,那等真出事要回滚时才发现退不回去——那是灾难。CI 里把 upgrade+downgrade 都真跑一遍,等于提前验证了"这次迁移可进可退"。可回滚性和可部署性同等重要。
L08

金丝雀 phase.py 走读

scripts/canary/ 是一套金丝雀发布工具包(cli/monitor/phase/prom/rollback/override/state)。最能看清"分阶段放量"逻辑的是 phase.py。先看它的状态和放量档位(phase.py:23):

scripts/canary/phase.py:23-33
PHASE_DURATION_HOURS: int = 24
NEXT_PHASE_DEFAULT = {0: 10, 10: 50, 50: 100, 100: 100}   # 放量档:0%→10%→50%→100%

@dataclass(frozen=True)
class PhaseState:                          # canary 当前状态(持久化到 sqlite ledger)
    phase: Phase                           # 当前放量百分比
    bom_version_current: str
    bom_version_target: str                # 要发的新版本
    bom_version_baseline: str
    phase_start_time: float                # 这一阶段开始的时刻
    baseline_metrics: dict = ...

核心是"能不能推进到下一档"的三个条件(phase.py:63):

phase.py:63-91
def can_advance(state, monitor, *, pre_flight_pass=True, now=None) -> tuple[bool, str]:
    if state.is_terminal():                          # 已到 100%
        return False, "already_terminal"
    # ① 这一档得跑够 24 小时
    elapsed = (now or monotonic()) - state.phase_start_time
    if elapsed < PHASE_DURATION_HOURS * 3600:
        return False, "duration_not_met"
    # ② 监控 3 维全绿(错误率/延迟/敏感度都没恶化)
    if not monitor.all_green(state.baseline_metrics):
        return False, f"monitor_not_green: {monitor.red_dims()}"
    # ③ pre-flight 门(BOM 审计 + ci-gates 历史都 PASS)
    if not pre_flight_pass:
        return False, "pre_flight_fail"
    return True, "ready"

逐条讲:要从当前放量档(比如 10%)升到下一档(50%),必须同时满足: 这一档已经稳定跑够 24 小时; monitor.all_green——盯着 Prometheus 的错误率/延迟/敏感度三维,跟 baseline 比没有恶化; pre-flight 那批 CI 闸历史上是 PASS 的。三条缺一就不推进,还返回具体原因(duration_not_met / monitor_not_green)方便排查。真正切流量的 advancephase.py:94)会调注入进来的 apply_overlay(切 K8s overlay)+ 写 Prometheus 指标。

canary 放量状态机 · NEXT_PHASE_DEFAULT {0→10→50→100} phase 0% baseline phase 10% phase 50% phase 100% terminal can_advance can_advance can_advance 推进 3 条件全满足才走下一档:① 跑够 24h ② monitor 3 维全绿 ③ pre-flight PASS 任一条件不满足 / 指标转红 → rollback 一键退回 baseline
图注(状态机):金丝雀按 0→10→50→100 放量,每次推进都要过 can_advance 三条件;出问题沿红线一键回滚。
金丝雀发布:新版本上线不一次性全量切换,而是先放一小部分流量(10%)给新版本,盯指标没问题再逐步放量(50%→100%),出问题立刻 rollback。名字来自"矿工带金丝雀下矿"——用小代价先探风险。这里的 NEXT_PHASE_DEFAULT 就是那条放量路线图。
💡 设计取舍:为什么 advance 把"真正 kubectl apply 切流量"做成注入参数(apply_overlay),而不在函数里直接调?phase.py:103 注释原话"实际 kubectl apply 由调用方注入 · 本模块仅负责状态转移 · 利于单测"。把"纯状态机逻辑"和"真去操作 K8s 的副作用"分离——phase.py 只算"该不该推进、推进到哪一档",测试时传个假的 apply_overlay 就能验全部逻辑,不用真连集群。这跟 Day 05 那些"业务只填配置、副作用交给框架"是同一种"纯函数 + 依赖注入"的可测试性思维。PhaseStatefrozen=True(不可变)也是为此:状态转移产生新对象,而非原地改。
👶 记忆口诀:CI 五道核心闸「料对、活过、分不掉、钱不涨、线不碰」——料对(BOM 版本一致 L03) · 活过(pytest 全过) · 分不掉(eval 5 维不超 2σ L04) · 钱不涨(成本 diff ≤10% L05) · 线不碰(敏感度红线 diff L06)。五条任缺一,allow_failure:false 就贴封条。canary(L08) 是验收之后再让一小拨住户试住,24h 全绿才多放。
L09

今日小结 + 动手

🧠 今天你应该能回答

  • R-CI-DEFAULT-FAIL 靠什么落地?(.ci-gate-baseallow_failure: false.gitlab-ci.yml:63
  • bom-audit 的三件事 + exit 0/1/2 分别代表什么?
  • eval 闸为什么用 2σ 而不是固定百分比?--changed 怎么只验改动的 agent?
  • 成本闸 / 红线闸的 label-escape(打标签放行)为什么合理?
  • DB 迁移为什么要连 downgrade 一起验?
  • 金丝雀推进的三个条件?为什么 apply_overlay 做成注入参数?

✋ 动手:读今天走读过的真脚本

# 1. stage + 默认失败基模板
sed -n '28,73p' .gitlab-ci.yml

# 2. BOM 审计(看文件头三件事 + exit code)
sed -n '1,55p' scripts/check_bom_lockfile.py

# 3. 评测回归:5 维 + 2σ + --changed
sed -n '52,105p' scripts/ci/full_agent_eval.py
sed -n '139,164p' scripts/ci/full_agent_eval.py

# 4. 成本闸 + 红线闸
sed -n '120,210p' scripts/ci/cost_impact.py
sed -n '100,157p' scripts/ci/red_line_regression.py

# 5. 金丝雀 phase 状态机
sed -n '23,128p' scripts/canary/phase.py

# 6. 本地跑等价门禁(提交前自检)
uv run pytest -q && uv run ruff check .
明天预告 · Day 19:学了这么多,终于到亲手造一个新 Agent——跟 cookbook 一步步 scaffold、写 builder、加评测样本、注册进 registry、跑通。顺带讲 L4 Skills(让 IDE 一句话调 agent)。
← Day 17 部署 Day 19 · 从零开发新 Agent + Skills →