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. **纯人工 + 收据**:至少让「没跑」留下痕迹。

判据只有一条:**它必须有个会失败的地方,并且有人或机器在跑它。**