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

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