agent-self-maintenance · git:20261104.8c7c4cc · 2026-11-04 · sha256 267e572f1433ee6f
agent-self-maintenance git:20261104.8c7c4ccA
Immutable. This exact content never changes and is served at /api/v1/blob/267e572f1433ee6f.
--- name: agent-self-maintenance description: 用五个「会失败」的小机制让 AI 编码助手长期维护自己的记忆——日记闸门、开局对账、事实核查、自我观察、自检收据。适用于任何有 journal/ 与自检脚本的仓库;含各机制的判据与边界(目录缺失时说什么、数据为什么不能随机制搬走)。 --- # Agent 自我维护:五个会失败的小机制 ## 为什么需要它们 一个会长期工作的 agent 最容易发生的不是「做错」,而是**「没人问」**: - 日记停在某一天,没人发现; - 自检红了三天,没人知道; - 事实库写着「上游已迁移」,而实际没有; - 「她说的」和「我量的」被混成一句结论。 这五件事的共同点:**不做也不会报错**。所以每个机制都必须有一个「会失败」的形态, 并且要能回答「谁在跑它」。 ## 五个机制 ### 1. 日记闸门(journal gate) 判据(两条,都从本 session 自己的事件日志读): - **做过事**:本 session 里出现过 `git commit`; - **没记事**:`journal/<当日>.md` 不存在或未被本 session 修改。 两条同时成立 → 收尾时提醒一次。**是门,不是自觉。** 边界:只在「有 journal 惯例的仓库」启用;没配置就安静——不要每个 session 都念 「你没写日记」,那会变成噪音,而噪音会被忽略。 ### 2. 开局对账(journal catchup) 闸门有一个结构性缺口:**它只在 session 里活着**。没有 session 的工作(终端里改的、 定时任务动的)它看不见。所以开局要读一次账: | 输入 | 要回答的问题 | |---|---| | 自检收据 | 上次自检是什么时候?红了哪几项? | | 日记 | 上次日记是哪天?之后有没有提交? | | 事实库 | 有没有过期的事实?(每条事实带日期) | **三种状态不合并**:ok / red / stale。「没跑过」必须说出来。 ### 3. 事实核查(facts check) 事实库里的每一条都是**活标本**:带日期、带证据、带探针。 - **探针**:一条能重新量出该事实的命令; - **第三种状态**:`无法核验`(探针跑不了)——它既不是通过也不是失败; - **抄来的与量到的分开记**:`source: upstream-doc` vs `source: measured`。 ### 4. 自我观察(inquiry) 每一条观察回答三个问题: 1. 这是**她说的**(引用 + 时间)? 2. 还是**我量的**(命令 + 输出)? 3. 还是**不确定**(明确标注)? 条目必须带日期与证据。**不要在一句话里混用两种来源。** ### 5. 自检收据(selfcheck receipt) 自检脚本每跑一次写一份收据(时刻、rev、失败项)。判据有五种状态: | 状态 | 含义 | |---|---| | `ok` | 刚跑过且全绿 | | `red` | 跑过但有失败项 | | `stale` | 太久没跑(阈值由仓库定) | | `missing` | 没有收据 | | `unreadable` | 收据在但读不出来 | **只有 `ok` 是安静的**;其余四种都要在开局说出来。 ## 边界:机制搬走,数据留在原地 | 东西 | 归属 | |---|---| | 五个机制的**判据与形态** | 技能 / 预设(可移植) | | 事实条目、观察条目、预算阈值、自检阈值 | **仓库自己的数据**(不随机制搬) | | 收据文件(`state/selfcheck.json`) | 仓库自己的产物(不进版本库) | **为什么这条重要**:机制是「怎么判断」,数据是「判断什么」。把数据随机制一起下放, 等于替使用者断言了他们的事实——而那些事实第二天就会过期。 ## 适配层:本仓的东西写在本仓 通用层只写「机制长什么样」,**具体落点属于各仓库自己的适配层**。分法是一句话: | 写在这里(通用) | 写在适配层(各仓自己的) | |---|---| | 判据的形态(「做过事」+「没记事」两条同时成立才提醒) | 判据读哪个文件、用文件 mtime 还是 `git commit` | | 五种状态的语义(ok / red / stale / missing / unreadable) | 阈值是多少、收据文件叫什么名字 | | 「数据不随机制搬走」这条边界 | 事实库、观察库、预算各自放在哪 | **判据只有一句:换个仓库还成立吗?** 成立写这里,不成立写适配层。 写成适配层时至少要说清三件事:**实现文件在哪**、**谁在跑它**、**哪些数字必须现取** (写死的数字第二天就会变成假话)。找不到「谁在跑」那一栏,就先别写——那说明它还是 说明书,不是机制。 ### 6. 长任务的结局(stay-or-not) **问题**:跑一个二十分钟的构建时,agent 会做一件很自然、但毫无机制支撑的事——说 「结果一出来我就报」,然后**阻塞在等待里**。等不到,这一轮就结束;之后没有任何东西会 叫醒它。更糟的是**说不清自己在等什么**:被问「你靠什么跟」时,答不出机制,只能答愿望。 **判据(三条,都验「文件变了什么」,不验「命令跑没跑」)**: 1. 结局**落盘**:`state/<name>.status` 从 `running <ISO>` 变成追加一行 `done <ISO> exit=N`; 2. `running` 那一行**不能**匹配「完成」的判据(否则一启动就误触发); 3. 退出码**必须写进文件**——没有它,唤醒之后还得再猜一次成没成。 **反例(同一个坑栽过三次)**:只验「命令执行了」。`sed` 跑了、没报错、文件没变对—— 三次都长这样。**改完要读回来**,不是看它有没有报错。 **谁在跑它**:这是适配层问题,按环境分岔: | 环境 | 靠什么唤醒 | |---|---| | 会话活着、等的是自己发起的后台任务 | 后台任务的完成通知(多数 harness 都有) | | 进程会重启、或等的是外部事件(文件被改、端口起来、别人提交) | **持久化的条件监听**——存在一个跨进程存活、条件变化时能唤醒会话的机制(某些 harness 提供;没有的话这条路就不成立,别假装) | | 都没有 | 明确**不要**说「我跟着」——那是陪坐 | **通用形式**(任何 shell 都成立,不依赖 harness): ```bash # 用法:run-long.sh <名字> <命令…> printf 'running %s\n' "$(date --iso-8601=seconds)" > state/$NAME.status "$@" > state/$NAME.log 2>&1; printf 'done %s exit=%d\n' "$(date --iso-8601=seconds)" "$?" >> state/$NAME.status ``` **边界**:把「结局」写成文件是通用的;**怎么被叫醒是适配层的**。没有条件监听的环境, 正确做法是「缩小阻塞窗口 + 显式说明这一点」,不是假装自己在跟。 ## 最小落地 不要求任何特定 harness。三个可移植形态: 1. **钩子**(harness 支持时):session 结束 / 开始时触发; 2. **收尾命令**:`make check` 或 `scripts/maintain.sh` 里串一遍; 3. **纯人工 + 收据**:至少让「没跑」留下痕迹。 判据只有一条:**它必须有个会失败的地方,并且有人或机器在跑它。**