mcpField
S
session-harvest

session-harvest

github.com/theodoreadAgent 工作流051 次安装4 周前更新
代码仓库

关于 session-harvest

session-harvest 是一个按需技能,用于在 Claude Code 会话结束或压缩前审查内容,并决定保留哪些信息及其存放位置。只读脚本收集会话、仓库、CI 和机器状态证据,同时流程限制 Agent 可写入的位置。

主要功能

  1. 01按需审查会话
  2. 02收集转录与仓库状态
  3. 03检查 CI 和机器状态
  4. 04比较已安装技能与检出版本
  5. 05限制计划和文件的修改范围

适用场景

  1. 01在会话压缩前整理记录
  2. 02检查技能副本是否过时
  3. 03将仓库后续事项记录为计划

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/theodoread/agent-skills --skill session-harvest

安装后在 Claude Code 中输入/session-harvest确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
references/
rationale.md
scripts/
harvest.py

详细介绍

会话梳理

在对话压缩或结束前,检查其中值得持久保存的内容,并确保每个候选内容都放在_正确_的位置,而不是因为记忆是最顺手的工具就默认放进记忆。主要通过显式调用(/session-harvest);上面的描述也适用于自然语言表达,但应依赖显式调用——此前的实践表明,对于如此重要的操作(会写入文件),依靠描述匹配并不可靠(参见 references/rationale.md)。

仅按需运行。安装钩子、配置 settings.json 或按计划运行均明确不在范围内——那属于另一种更复杂的工具(有关考虑过并否决的先前方案,请参见 references/rationale.md)。

此技能读取、运行和写入的内容

  • 读取:Claude Code 在 ~/.claude 下的对话记录和后台任务状态(只读;其他运行环境中没有这些内容);会话涉及的每个仓库的 git 状态;通过 gh 查询 CI 运行;进程和监听套接字列表;docker 镜像;两个计划存储;已安装的技能中心以及 skills-state 解析出的源代码检出目录。套接字步骤会检查监听器服务的目录下是否存在名称与凭据相关的文件(.env、.netrc……),只检查文件名,绝不读取内容。
  • 运行:git(包括对每个涉及仓库自己的上游运行 fetch,除非使用 --no-fetch)、已安装时的 gh 和 docker、Linux 上的 ps/ss,或 Windows 上的 PowerShell/netstat,以及只读的 plans.py absorb。不会通过 shell 运行任何内容。它绝不会安装、重新安装或移除技能:skills-state 会比较已安装副本与其源代码并报告差异;部署修复属于 skill-authoring-taudelta 的流程,而非此技能的职责。
  • 写入:**脚本不会写入任何内容。**流程由 Agent 执行写入,且仅限于其开头声明的范围——会话所在仓库、其中的 plans/,以及通过 plan-conveyor 操作的计划存储。它绝不会编辑、移动或删除他人为其他仓库提交的计划;相关证据会作为一份新计划提交,并注明关联计划。绝不写入已部署的指令文件、已安装的技能副本或其他仓库的目录树。唯一可能产生的非文件副作用,是终止扫描发现的孤立进程;该操作只会被提议,不会执行。
  • 网络:仅使用 git fetch 和 gh,连接各仓库自己的远程端。不向任何地方发送内容;所有报告都输出到标准输出。

脚本

scripts/harvest.py 包含所有机械化步骤:边界检查、已安装副本与检出目录的比较、解析本次会话的对话记录、提取轮次和回答、实时状态扫描、成功声明计数,以及本次会话中先前梳理已提交的内容。请运行脚本,而不要手动拼凑命令——在 2026-09-02 对 1,134 份对话记录中的 24,429 次 Bash 调用进行测量后发现,每次运行都会重新发明这些命令,且逐渐偏离,导致本应能在多次梳理之间进行比较的答案无法比较。如今,六个有记录的故障都已通过脚本代码杜绝(读取而非手动输入上游分支、不会通过 shell 运行任何内容以免管道吞掉退出代码、以 JSON 读取 CI、锚定 depends_on、从任务的 state.json 获取其对话记录、按工具使用 ID 匹配回答)。

H=~/.agents/skills/session-harvest/scripts/harvest.py   # or <checkout>/skills/session-harvest/scripts/harvest.py
python3 $H boundary                                     # step 0, first command of the run
python3 $H transcript --expect '<a command this session ran>'
python3 $H turns                                        # step 4
python3 $H skills-state                                 # step 0 — needs a checkout, see below
python3 $H sweep --boundary <instant>                   # step 5
python3 $H claims --until <instant>                     # step 5, the exit-masked rule
python3 $H filed --until <instant>                      # step 8, every harvest — see below

单独运行 turns、sweep 和 claims 时,会从 $CLAUDE_CODE_SESSION_ID 解析对话记录;Claude Code 会将此变量导出到每次 Bash 调用中,其值就是对话记录自身的文件名主干;后台任务的 state.json 仍具有优先权,而 --session <id|path> 则适用于不导出 ID 的运行环境。直到 2026-09-05,这些命令都无法解析出对话记录——transcript --expect 打印出正确路径后,下一行却因 no transcript resolved 而以状态码 1 退出;两天内的三次梳理都曾手动重新输入 --session,后来脚本才学会读取这个一直存在于运行环境中的变量。 skills-state 会自行解析 --since,并打印所用的值。 在 2026-09-08 之前,上面的代码块写的是 --since <session start>——一个没有注明来源的占位符。这个值确实存在(transcript 的 started: 行),但没人说要从那里获取;而且这两个命令彼此独立,所以批量调用它们的会话必须在 transcript 作出响应前就提供这个标志。六次收集使用了猜测值;其中一次提前了九十分钟,却仍然得出了正确结论,这就是占位符一直留存下来的原因。过早会让流程在证据不足时执行成本最高的步骤;过晚则会把真正取代旧版本的提交排除在时间窗口之外,并在报告看起来干净的情况下默认失败。这个标志仍会保留,可供不导出 ID 的测试框架覆盖使用,也可用于审计不属于当前会话的时间窗口——无论采用哪种情况,since: 行都会说明所用的值,因为问题从来不是窗口错误,而是错误窗口与正确窗口无法区分。现在,提供的值还会说明它与真实开始时间相差多远,以及窗口移动的方向,因为在并排打印这两个时间点之前,猜测值和有意审计的值看起来完全相同。

transcript 会打印会话中最长的空闲间隔,超过一天即表示会话已恢复。 以 started: 为锚点的每个时间窗口都会覆盖这段间隔,因此 skills-state、filed 和 sweep 会填入正确归属于其他会话的行——2026-09-27 的测量结果显示,要找到本会话的 3 个提交,得从约 190 个存储库提交中筛选;在 9 个提交上建议重新读取,其中 7 个并非本会话的提交。请先阅读每行的归属信息,再查看其中的建议。锚点仍保持为会话开始时间,因为对于同一天内的会话——本项目其他所有测量都来自这种情况——移动锚点毫无益处。

变更后的 since 检查以每项技能自身内容载入上下文的时间为基准,而不是以会话开始时间为基准。 对于稍后调用的技能,两者可能相差数小时;而收集流程按设计最后才载入——因此,以会话开始时间为基准,会让这项技能每次运行时都错误地认为自身内容已过时,而这一步的全部目的正是判断是否信任自身指令。2026-09-07 已确认:在会话最后几分钟才调用的收集流程,被告知其持有的副本可能已被三个提交取代,而这些提交在副本尚未读入前就已合并。载入时间取自会话自身的 Skill 调用;如果某项技能没有此类调用,则回退到会话开始时间,每行都会说明采用的是两者中的哪一个。

skills-state 会将安装版本与检出副本进行比较,并能找到检出副本。 按顺序查找:--checkout <path>、$SESSION_HARVEST_CHECKOUT、运行脚本所在目录上方的仓库,然后是 plan-conveyor 配置的 projects_root(默认为 ~/projects)下唯一包含 skills/session-harvest/SKILL.md 的 Git 检出副本(最多遍历三层,绝不跟随符号链接)。因此,只要任意机器上某处存在源代码,安装的副本就能回答步骤 0,与仓库布局无关;分支也算候选,如果有两个候选,它会询问而不是自行选择。它还会打印步骤 6 用于提交技能修复的命令,其中包含检出路径,因此任何技能正文都不必注明作者把仓库放在哪里。如果机器上没有检出副本,它会退出并返回 1,同时说明这一点——这是读者最常遇到的情况:安装了副本,但没有源代码仓库。此时正确的报告行应说明无法进行比较,而不是说没有发生变更。在 2026-09-05 之前,安装端的副本若不使用 --checkout,就无法回答这个问题;有一次收集照原样运行了上面的代码块,却在第一条步骤 0 命令处遇到了错误,证实了这一点。 每个子命令都是只读的,并接受 --json。有两件事是刻意不包含在其中的:重新运行 gate(那是仓库自己的命令,硬编码一个命令会导致所有命令写法不同的仓库都出错),以及任何会写入内容的操作——一个既测量又写入的脚本,是 Agent 不读就会直接运行的脚本。下面的判断是此技能的其余部分,内容不会改变。

流程

收集操作可以写入哪些内容,只在此说明一次。 仅限三个地方,不得写入其他地方:会话正在运行的仓库(常规编辑和提交)、该仓库的 plans/(通过 plan-conveyor),以及通过 plan-conveyor 操作的计划存储库——新计划(包括使用 --for 为另一个仓库提交的计划),以及更正本会话在此处编写的计划。其他人为不同仓库提交的计划,只能读取和引用,绝不编辑。除此之外,不得写入已部署的指令文件、已安装的技能副本、任何其他仓库的工作树,以及生成 ~/.agents/AGENTS.md 的源文件(如果这些源文件位于会话当前所在仓库之外)。对于上述任何位置的修改候选项,都应作为计划提交给其所属仓库。在所属仓库中,编辑才属于常规工作,因此技能仓库和指令片段仓库并非此规则的例外;它们只是这条规则应用于恰好位于其中的会话。用户于 2026-09-03 明确表示:"harvest should exclusively edit things in the repo where the session is happening and, via plan docs, to the plans in the current session's and the plans in the central store repo"——其中的“instructions”指已部署的文件,而不是在其自身仓库中修改的片段源文件。清理时发现并终止孤立进程属于副作用,而非写入;步骤 8 对此作出规定。

0. 检查正在运行的副本是否为当前版本

python3 $H boundary 必须是收集操作运行的第一条命令,在本次运行的任何其他操作之前执行。 保留其值;步骤 5 会将其传给 --until,以便遵循度指标反映会话自身的工作,而非此次收集操作的清理工作。下面每个步骤都会向记录中添加不同类型的 Bash 调用,因此之后再获取边界值,就会漏掉其中一部分。

正在运行的技能是在安装时复制的文件,因此收集操作可能会悄悄执行比源文件更旧的版本——恰好跳过最近添加的检查,并且因为从未检查而报告运行正常。python3 $H skills-state 会将此技能和收集操作所依赖的其他技能的已安装副本与检出版本进行比较,此外还会比较本会话写入过的所有技能(打印为 changed this session:),因为修改源文件正是导致其安装版本过时的原因;对于使用过但未写入的技能,或通过 shell 命令修改过的技能(记录中的编辑工具无法显示这类写入),请添加 --skill <name>。如果检出内容不包含这三项中的任何一项——也就是除本仓库之外的任何技能仓库——它会改为报告该检出中的技能,并打印说明情况的 scope: 行,因为默认行为的全部目的就是防止指定另一个技能时漏掉此技能,而此技能不存在的地方也无须保护。如果存在差异,请如实说明;过时的收集操作比不收集更糟,因为它的报告看起来完全一样。此功能于 2026-08-29 添加,此前用户要求收集操作使用“最新版本”——该技能原先没有此功能;即使用户提出要求,也无法确认是否使用了最新版本。

差异有四种原因,其中只有一种是安装版本过时。 子命令会打印判定结果;请报告它指出的那一行。收集操作会报告安装状态,绝不会部署——推送、安装和重新加载技能属于 skill-authoring-taudelta 的流程,其中包括为何下面每一行都需要不同的处理方式——因此报告会说明适用哪一行,以及应由谁处理:

检出状态含义报告内容
干净,与远程版本一致安装版本已过时已安装的副本落后于已发布的修复
干净,领先若干提交未推送的技能工作修复尚未发布,以及这些提交属于谁
有未提交更改正在进行中的工作无内容可部署;另一个会话可能正在编辑
关联的工作树在分支上编辑修复位于安装不会读取的分支上

最后一行的读取方式与第一行相同:skills-state 会打印一行 worktree:,指出它所属的检出,因为其余输出无法区分两者。请明确说明,最后三种情况都不是安装问题。 尤其是有未提交更改的检出,更多时候表示另一个会话正在实时重构;把它描述成第二种情况,恰恰会诱发全局规则所禁止的跨仓库干扰。2026-08-29 在 plan-conveyor 上确认,当时正进行两层存储拆分:有 672 行未提交的新增内容,没有任何提交领先于远程版本——git log 显示历史已稳定,只有 status --short 能看到这些更改。2026-08-30 又从相反方向确认:同一份非空差异,检出干净且与远程版本一致,两条提交在几分钟前刚推送——完全符合第一行的描述。同一份差异,隔一天代表了截然相反的情况。

结论是解读,不是事实,而重新运行只需一次调用。 收集过程尚未结束时,你所在的行就可能发生变化,因为另一个会话可能向同一检出提交或推送内容——所以,如果引用运行开始时得出的结论,交给用户的可能已是旧答案。在 plan-conveyor 上两次确认过这一点,而间隔长短正是关键:2026-09-03,相隔 13 小时,“未推送的技能工作”变成了“检出干净且已推送,但安装已过时”,状态从无需安装即可修复的一行,变成了可通过安装修复的一行;随后在 2026-09-05,不到 90 秒内,状态跨越了表格中的两行,两个调用分别发生在 23:43 和 23:45,而平行会话的提交在 23:44:21 落地。因此,这并非针对长时间间隔的预防措施——即使运行很快,在开头判定、结尾报告,也同样存在风险;而且这种情况下,恰恰最容易让人忘记重新检查。第 5 步已经将此规则应用于 Git 领先提交数;skills-state 的输出最直接地会成为建议,因此更需要遵循这条规则。

有未提交更改的检出不会阻止运行。上述两个会话都正确完成了收集,因为它们运行的每条命令都针对已提交的版本。顺带确认本次运行需要的命令本身不在未提交的差异中,并且——仅当本会话正在 skills 仓库中工作时——才将其视为第 6 步回填的问题,因为只有这种情况下,回填才会修改另一个会话可能正在使用的检出。在其他任何位置,它都不会阻止任何事情:第 6 步是提交计划,而非进行编辑;提交计划不需要该检出中的任何内容。 Diff 干净并不能说明一切,而这正是它无法发现的情况。 只要安装程序已经运行过,比较结果就会显示一致——但在加载时冻结在_本次会话上下文_中的副本仍然是旧版本。任何文件系统比较都无法发现这一点。后果比运行过时代码更糟:它会对收集流程正在审计的那些技能,给出自信、具体却错误的陈述,而这些内容会出现在无人复核的报告中。--since <session start> 就是为此而设:它会列出自本次会话开始那一刻起该技能下的每个提交,标明各自改动的部分,并针对每个变动部分给出相应的补救措施。如果 SKILL.md 在会话开始后有过变更,无论 diff 显示什么,都要先从磁盘重新读取——如果检出目录是较新的版本,就从检出目录读取。 重新读取已安装副本,可以修正上下文过时但安装副本是最新的情况;如果安装副本本身落后,它返回的就仍是会话已有的那份过时文本,看起来指令已得到满足。2026-08-30 确认:一次收集在该技能提交后十四分钟运行,按指示重新读取了已安装副本,结果又拿到了过时内容;检出目录干净、已推送,并且领先两个段落。

有一种情况会让整个分支都变成空操作,而且在本仓库中很常见:变更技能的提交是_本次会话自己做的_。 子命令会列出这些提交;如果它们全都是本次运行产生的,那么机器上持有最新文本的上下文就没有过时——没有任何内容被取代,因为没有其他人写入内容。用一句话说明,然后继续;不要重新读取四个文件来确认会话还记得自己刚写的内容。重新读取是为_其他会话_的提交恰好落在当前会话进程之下的情况准备的;不区分这两种情况,才会让本流程中代价最高的步骤用在它原本无关的情形上。2026-09-02 确认:本仓库的一次收集发现它所依赖的四个技能都在会话开始后变更,而且全是它自己提交的。不过,下半部分仍然有用——检查已完成的操作是否是在过时措辞下运行的——因为会话中途提交的脚本确实会改变之前调用的返回结果。

同样的情况也有一个适用于下半部分的例外:在技能仓库_中_工作的会话通常一直调用的就是检出目录。 scripts/ 这一行提醒,先前的调用可能运行的是已安装副本;从其他位置看确实如此,但在这里几乎不可能——在该仓库中工作的会话自然会输入相对于仓库的 skills/<name>/scripts/<file> 路径,而它必然是最新的。要从对话记录中确认,不要预先假定哪种情况成立;唯一要确认的是调用使用了哪个路径。2026-09-04 在本仓库中确认:三个技能的 scripts/ 在安装副本和检出目录之间都有差异,三者都被报告为可能运行了过时代码,但会话中的每次调用都使用了检出目录——整个分支都无关紧要。

与之相反的情况,子命令本身也有说明:scripts/ 的提交全都尚未推送。 安装程序会从远程克隆,所以任何安装副本都不包含这些提交;调用了已安装副本的会话运行的是未变更的代码。该行会写明 all unpushed, so no install carries them,并将“阅读 diff”的补救措施限定为对检出目录发起的调用。2026-09-28 确认:一次 invoke-stubs 收集被要求阅读两个 plan-conveyor 提交的 diff,但它自己的条目明明列出了这两个提交尚未推送。 **触发这一切的最低成本方式既免费,又会自动出现:会话中可用技能列表发生变化。**某个技能以前不存在,现在出现了,或者描述被改写,这都是直接证据,表明安装程序在会话开始后运行过——也正是在这种时候,才值得为本次运行依赖的技能付出检查时间戳的成本。它不花钱,不需要命令,还能覆盖 diff 无法触及的情况:即使重新安装涉及的技能并非本次运行使用的,也仍然说明安装状态发生了变化。已于 2026-08-31 确认,这正是触发重新检查的原因,而规定的检查方式不会触发重新检查:两个变更技能的已安装副本都与检出版本一致,因此 diff 对所有内容都报告“相同”,也就不会提示任何事情。

三个子目录的失效方式各不相同,所以子命令会分别报告 SKILL.md 和其余内容。只有 SKILL.md 保存在本次会话的上下文中,因此只有它会在其中变得过时。对它做 diff,而不是重新读取,这与下一段针对 scripts/ 建议的方法相同——diff -u <installed>/SKILL.md <checkout>/skills/<name>/SKILL.md 只返回发生变化的少数几行,而重新读取会返回整个文件。已于 2026-09-07 确认:一次过时的收集任务使用 diff,而不是重新读取,得到 4 个差异块、约 40 行,而文件约有 700 行——并且每个差异块都改变了本次运行随后采取的行动。唯一需要完整重新读取的情况,是会话中途、_技能加载之后_运行了重新安装:只有当上下文中保存的副本是 diff 的一侧时,diff 才可靠;而在这种情况下,保存的文本两边都不是。skills-state 会打印安装的修改时间,因此这种情况可以识别,而不必凭空假设;会话中的列表发生变化(如上所述)是另一个迹象。对于命令由脚本实现的技能,重新读取前先探查:对本次会话实际调用的每个子命令运行 --help,即可确认这些调用的含义是否符合预期,以及现在是否有更好的调用方式;skills-state 会在触发此情况的行上打印确切的探查命令。只有当本次会话依赖的是技能说明而非命令,或者探查结果显示某个已调用的接口发生变化时,才重新读取 SKILL.md。已于 2026-09-26 确认:plan-conveyor 比上下文中保存的约 700 行副本晚了 10 次提交;两次 --help 调用显示 commit -m 未变——因此先前所做的事情都无需修正——此外还新增了 --body 标志和 push 子命令。

scripts/ 会通过 shell 调用,因此下一次调用会直接运行新代码——但本次会话较早时的调用运行的是旧代码。**先查看 diff,再决定一条说明是否足够。**如果变更只是调整输出,添加一行说明即可;如果变更_新增或扩大了检查范围_,就意味着较早的调用回答了一个当前代码会给出不同答案的问题,补救方法是从检出版本重新运行该命令。对单个脚本运行 diff -u 成本很低,而这正是“这个结果已过时”和“这个结果从未计算过”之间的区别。已于 2026-09-03 确认:一次会话多次针对已安装副本运行 plans.py list,该副本只在家族范围检查状态漂移,而检出版本会在每个范围检查;重新运行后结果正常,这就是无害情况的样子,仅添加说明会让情况仍然无法确定。references/ 按需读取,且不会产生影响。已于 2026-08-30 确认:一次目录范围查询触发了重新读取并审计的分支,而提交只改动了 references/ 中的一个页面;由于上下文中保存的措辞从未变化,审计结果为空。它不会影响判定;但对于本次运行实际打开的页面,它就不再是无关因素了——这一区别不会改变安装状态,因此不得影响判定;但从过时副本读取的参考内容,也和其他内容一样是过时的。对该单个文件运行 diff,与另外两个目录的处理方式相同。 boundary、transcript 和 skills-state 都是从受测副本中运行的,调整顺序也无法解决这个问题——解析检出副本是 skills-state 自己的职责,因此总会有某个调用发生在过时状态结论生成之前。所以该子命令会报告自身是否受影响:当正在执行的脚本已过时,它的结论会指出其哪些子命令在安装版本与检出版本之间确实存在差异,并说明上述三个子命令是否在其中。通常它们不在其中——已于 2026-09-07 确认,当时有六个提交未同步,所有更改都在 sweep 中,因此三个预检答案都是最新的,只有手动比对差异才能看出这一点。如果它们确实在其中,请从检出副本重新运行,然后再相信已收集的结果;如果两个副本根本无法比较,结论会明确说明这一点,而不是报告不存在差异。

当检出副本领先时,指出是哪位作者尚未推送的提交消除了过时状态。 根据第 5 步关于未推送提交的规则,推送是对外操作,归提交作者所有,因此报告应以这一事实收尾,而不是给出补救措施。已于 2026-08-30 确认:检出副本显示 ahead 1,而那一个提交恰好包含本次运行刚刚重新读取的措辞。请注意,这种不对称性很容易让人忽略——会话并未受阻,因为读取检出副本就足以正确运行。真正存疑的只是下一步,而这恰恰是没人会重新检查的部分。

而且,如果会话已经根据该技能采取了_行动_,重新读取只是修复的一半。 要问的是,已经完成的操作中是否有任何是在已被取代的措辞下执行的。重新读取可以修正下一次调用,却无法改变已经完成的推送。已于 2026-08-30 确认:某个会话在推送存储库之前,运行了 plan-conveyor 的 scan --mode tree;而在它加载该技能四小时后合入的一个提交,将推送前检查改成了 --mode history。重新读取无法检查已推送的历史记录;只有追溯执行取代旧命令的新命令才能补上检查,而结果显示一切正常。

整个分支要防范的失败案例已于 2026-08-29 确认:某个会话从加载时起就持有 plan-conveyor,后来一个提交指出存储库的推送前检查应为 scan --mode history,而不是 --mode tree,但会话从未看到这项更改,并在计划中写下了相反的结论——这是根据过时措辞推断出的保密性检查要求。

1. 先进行重要性测试

重新阅读对话,找出候选项。对每个候选项,先问:如果它丢失,未来的会话会因此出错吗? 任何不符合这一点的内容都应剔除(可选在报告中注明“已考虑,但不值得持久化”),而不是提出建议。这才是真正的噪声过滤器——应在路由之前应用,而不是之后。

然后,对于任何形式像惯例的内容——“始终执行 X”“绝不执行 Y”——在提出之前先确认它确实成立。 以下所有过滤器都假设候选项是正确的,只决定它该放在哪里。从本次会话碰巧采取的做法,或一两个同类仓库的样本中推断出的惯例,只是一个假设:应先对照工具自身的文档和真实的社区实践进行核实,并说明核实了哪些内容。丢失一个候选项只会导致重新发现一次;把错误内容写进共享文档,则会误导每一位未来的读者,而共享文档恰恰是没人会重新推导内容的地方。已于 2026-08-27 确认:一次信息整理根据两个碰巧没有这样做的同类仓库,建议记录“不要把 tests/ 设为包”;但 pytest 自身的文档对默认的 prepend 导入模式给出了相反建议(“强烈建议将测试模块组织为包”),而该会话实际上为了满足共享的类型检查器配置,已经撤销了 __init__.py。真正的发现是另一条说法,也应写入另一个位置;只有用户提出异议后,这一点才浮出水面。

2. 路由过滤条件

对于通过重要性测试的内容:

  • 计划专属内容 → plans/*.md,绝不写入记忆。 如果会话涉及或产出了 plan-conveyor 技能会跟踪的工作——设计、想法、进行中的实现——就不要为其创建记忆条目。检查相关的 plans/YYYY-MM-DD-*.md 是否已记录此内容;如果仓库使用 plan-conveyor,但计划文件尚未记录,就说明这一点,并提出创建或更新计划文件,而不是保存到记忆中。记忆没有过期机制,因此存放在其中的计划快照只会悄无声息地过时——plans/ 已经负责管理这一生命周期。如果计划文件已存在,但其 status 现已过时(例如,会话刚完成并验证了工作,状态却仍为 planned/in-progress),请直接调用 plan-conveyor 技能,执行其自身的状态升级/退役流程——不要自行提出 AskUserQuestion,询问是否退役。2026-08-23 确认这会造成阻碍:询问用户是否退役,而 plan-conveyor 的「退役计划」部分已经给出了答案(默认:保留,除非其理由已在仓库其他地方有所涵盖);用户的纠正是:「为什么计划文档技能没有启动?」

  • 仓库专属的持久知识 → AGENTS.md/docs//contributing/,绝不写入记忆。 使用以下划分方式,而不是笼统地说「放进 AGENTS.md」:

    • AGENTS.md(或等效的指令文件)——仅包含 Agent 在该仓库的_每项_任务中都需要的操作指令(命令、约定、会改变行为的注意事项)。保持精简:它会无条件加载到每次会话中,因此这是实际用于避免臃肿的手段,而不是记录所有已知信息的地方。
    • docs/*.md——面向使用者的参考资料,从 AGENTS.md 链接,按需阅读。
    • contributing/*.md(如果知识涉及技能本身,而非仓库,也可以放在技能自己的 references/*.md 中)——设计理由、先前方案、实现注意事项,同样按需阅读。
  • 跨仓库/个人偏好(不属于某个项目)→ ~/.agents/AGENTS.md,同样绝不写入记忆。 沿用上述仓库专属划分方式,只是上移一层——通过其真实来源进行版本控制。使用自身路径的运行框架(例如 Claude Code 的 ~/.claude/CLAUDE.md)应保存该文件的副本或符号链接,绝不能成为另一个需要写入的地方。如果 ~/.agents/AGENTS.md 是生成的(由片段组装、通过点文件管理器部署,或从某个仓库创建符号链接),则应修改源文件,而不是已部署的文件——下次运行时该副本会被静默覆盖,因此在副本上的编辑会丢失,也不会传播到其他机器。并且,只有在包含该文件的仓库中进行的会话才能修改源文件;在其他地方时,应按流程开头规定的写入范围,使用 plans.py new --for 将候选内容提交给该仓库。在本文作者的机器上,源文件是另一个独立配置仓库中的片段目录;该仓库自己的 README.md 会说明各片段的归属。起草新增内容前,先阅读规范源文件——会话上下文中加载的已部署副本,在结构上可能已经过时。 2026-08-24 确认:某个会话中的 ~/.agents/AGENTS.md 有约 20 个平铺章节,但源文件已重组为 7 个分组章节,因此若依据上下文中的章节名称起草新增内容,目标标题就可能已不存在。先运行 grep -n '^## ' <source>。如果候选内容是已有规则的某种_变体_,就应扩展该规则所在的现有章节,而不是新建章节。 下文的「已涵盖 → 跳过」适用于完全重复的内容;这里说的是稍有不同的情况:原则虽已写明,但这一特定形式尚未涵盖。默认在已经阐述该原则的章节中追加一段简短说明,因为该文件会加载到每个仓库的每次会话中,新标题会在所有地方增加上下文开销;同时,读者看到同一原则下的三个实例,比看到三条互不相关的规则更容易归纳。只有当触发条件和检测信号都与现有内容真正不同时,才考虑新建章节。2026-08-23 已解决:「不要根据抽样的单个文件描述多文件差异」并入「验证实际发生了什么,而不是输出看起来怎样」——后者已经涵盖了干净标准输出与退出码、测试套件与临时脚本这两种情况,它们都属于「方便观察的表面信号并非真实信号」这一类型。

  • 已有技能负责的主题,优先交给该技能,而不是新增一条始终加载的规则。 ~/.agents/AGENTS.md 并非每个跨仓库发现的默认归属地。规定该文件准入标准的文档就是门槛(在本文作者的机器上,是 power-user-linux-setup/contributing/global-agents-md.md 中的「准许新规则」);如果没有这样的文档,仍应使用下方的分层测试——遗漏后_悄无声息且代价高昂_的规则,属于始终加载的文件;触发条件明确且遗漏后_代价低、容易恢复_的规则,则属于技能。提议前,先根据文件自身的参考指标检查当前篇幅(grep -c '^### '、wc -l;≤15 条规则/≤200 行),并在征求意见时明确报出这些数字——超过指标后,准入确实需要付出代价,用户应知情决策。2026-08-25 已解决:pgrep -f 会匹配运行框架自身的 zsh -c … eval 包装进程(误报看起来像真实进程),因此将其交给 session-bash-audit——该技能本就会收录新发现的 Bash 反模式,而且能_测量_其发生率——而不是在已有 33 条规则/390 行的文件中再增加第 34 条规则。反例:2026-08-28 从另一方向解决的非终止 CI 轮询循环,尽管始终加载的文件已有 37 条规则/446 行,仍然写入该文件,因为分层测试取决于遗漏的后果,而不是篇幅预算——这种遗漏在设计上就悄无声息(无法失败的循环不会输出任何内容,因此「仍在等待」和「永远无法完成」看起来完全一样),而且它已经导致某次会话报告了一个自己不可能观察到的结果。篇幅压力意味着应优先考虑技能,但不能凌驾于「悄无声息且代价高昂」之上。若最终选择始终加载的文件,可用两种方式控制成本:扩展现有章节而非添加新标题(规则数不变);并以替代旧习惯的命令作为规则结尾,而不是停留在警告上。无论如何,都要报告修改前后的行数。

  • 目标文件正在重组 → 写入负责重组它的计划,而不是文件本身。 如果候选内容正确的归属文件正由某个开放的 plans/*.md 计划重组,尤其是该计划还规定了哪些内容可以添加,那么应在_该计划中_记录候选内容,并用 [NEEDS CLARIFICATION: ...] 说明其触发条件,而不是追加到文件中。直接追加会绕过该计划旨在执行的准入标准,可能导致新增内容在重组时未经阅读便被删除,也会与正在进行重组的会话相冲突。此规则适用于任何有开放计划负责其结构的目标文件,不仅限于 ~/.agents/AGENTS.md。2026-08-23 已解决:在精简审查正积极将 ~/.agents/AGENTS.md 从 30 个章节缩减,并添加自身准入规则期间,有两条跨仓库规则原本准备写入该文件(该审查计划现已退役);两条候选规则均暂存于该计划,而非追加到文件,最终在计划结束时才作出处理决定。

  • 属于其他仓库的候选内容应_提交到该仓库_,而不是排队留在当前仓库。 自 2026-08-29 起,plan-conveyor 支持 plans.py new <topic> --for <repo>,可将计划写入位于所有工作树之外的该仓库存储镜像:提交无需跨仓库,而之后在该仓库工作的会话会由 plans.py absorb 接收该计划。优先使用此方式,而不是在当前仓库创建 depends_on 计划;后者是此机制出现前的临时替代方案,会把候选内容留在所属仓库从不会查看的地方。立即在存储库中提交已提交的计划——只要存储库有未提交更改,其他所有会话就只能退而求其次,添加新文件。depends_on 仍有不同含义:这项工作必须等到那个仓库发生更改后才能合并;这是依赖关系,而非交付方式。

  • 技能或指令文件遭到误用、误读或忽略 → 立即针对其所属仓库提交计划。 不要等到报告时才处理,也不要只在结尾用文字描述:使用 plan-conveyor 自身的路由方式——如果此仓库拥有该技能,就用 plans.py new <topic>;如果属于其他仓库,就用 plans.py new <topic> --for <repo>;并立即在存储库中提交。然后在报告中说明发现的问题和计划文件名。规则是「先提交计划,再撰写报告」,因为只存在于报告中的发现会随着终端关闭而消失。 这涵盖了三种情况,而且三种都很容易被误认为是在叙述过程,而非指出问题:

    • 一条规则得到了遵守,却仍导致了错误结果——规则本身有误,或针对的情况不对;
    • 一条规则被绕过了推理——措辞解释的是一种机制,而不是规定一项约束;机制是可以争辩的;
    • 一条规则只是反复未获遵守——措辞没问题,出问题的是别的环节;这是测量问题,而不是改写措辞的问题。

    请说明属于哪一种,因为每一种需要的修复都不同,并附上会话能统计到的任何数据。2026-08-30 已确认:一次会话中三种情况都出现了——一条 ~/.agents/AGENTS.md 记忆规则被推理绕过;一个状态门槛被绕过,会话没有调用 set-status,而是直接编辑 frontmatter;一条禁止使用 head/tail 的规则,在该会话自己的 Bash 调用中有 28% 被违反。每种情况都成了针对拥有该规则的仓库提交的计划;如果只写成一段文字,没有一种能留存下来。

    提交问题前,先检查是否已有记录;不仅要搜索仓库,也要搜索存储库。 在运行并行会话的机器上,规则遭到违反最可能是因为已经有人注意到了——这与步骤 5 中“已有归属”要点背后的逻辑相同;该要点是为实时状态检查编写的,很容易忘记在这里应用。如果问题已有归属,会话的数据就是该计划的_证据_,而证据应放在哪里,取决于计划属于谁。如果是本仓库自己的计划,就直接把数据放进去——放在它的 plans/ 中,或者仓库将计划保存在存储库镜像中时放进镜像;这属于会话所在仓库的常规工作。plan-conveyor 倾向于每个主题只保留一份计划,新增第二份会拆分第一份正在累积的数据。2026-08-30 已确认:一次会话测量了自己的 Bash 调用,发现其中有 36% 通过管道传给 head/tail,正准备提交计划——但已有两份计划记录了这一矛盾,其中一份引用了“同一天另外两个会话中占 25–36%”。最终保留下来的真正新信息,是一条此前无人测量的数据;它作为第四个样本加入了现有计划。

    如果计划属于另一个仓库,且并非由当前会话编写,绝不能直接编辑:使用 --for 将证据提交给该仓库,创建一份新计划,并在开头一行注明它所属的计划。 absorb 会将提交内容与其引用的计划配对,由在归属仓库中工作的会话在那里合并两者。2026-09-13 已确认:一次 repo-tasks 会话以这种方式提交了第二次出现的问题,并请求合并;负责吸收的会话在一次提交中将其并入归属计划。此前的规则要求直接修改原文件,结果两头都付出了代价:它把内容写入多个会话共用的目录,absorb --apply 可能会在编辑后、提交前取走该文件(2026-09-04 已确认:一条宣布新增内容的提交说明下,记录了删除 76 行、插入 0 行);而且这让该技能能够修改自己未编写的持久文件,改用提交方式也能轻松达到同样目的。 本会话刚写的计划若要修正,仍然算作编辑,也仍会与吸收发生竞态,所以要立即一并提交。commit 会说明文件因此去了哪里,因此 (removed) 结果本身就是信息,无需调查;不过,要把这个间隔控制得尽可能短。

  • 已经涵盖 → 跳过。 如果现有文档已经说明了这点,就不要重复编写——先检查。

  • 关于如何在此生态系统中构建事物的元规范(例如“skills 默认应执行 X”)→ 放入相关现有 skill 自身的文档,而不是反馈记忆——尽管表面上“如何开展工作”听起来像属于 feedback 类别。此问题是在设计本 skill 时通过 AskUserQuestion 解决的:关于如何编写_新 skills_的偏好应归入 skill-authoring-taudelta skill(持久、受版本控制,对每位贡献者/工具都可见),而不是本 harness 的私有记忆库。类似情况默认按此处理,不要每次都重新询问。

3. 不存在记忆层级

所有保留项都写入普通文件,步骤 2 就是完整的分类规则。 harness 自带的记忆库绝不能作为存放位置——无论内容是否持久、是否短暂,也不能用作暂存区,更不能说“这次例外”。

[决定:用户于 2026-08-29 明确表示——任何 harness、任何项目、任何理由都不得使用记忆,因为项目数据和用户级实践不得被供应商锁定。例外是 harness 配置(settings.json、hooks、键位绑定),这些内容预计会因 harness 而异,因为它们描述的是工具,而非工作。分类规则:配置描述 harness;任何描述工作的内容都应写入任何 Agent 都能打开的普通文件。]

声明禁令,而非解释机制。 此步骤的早期版本会将“确实是临时的”保留项写入 harness 的记忆库;而解释_为什么_该记忆库并非合适的存放位置,恰恰让会话得以推理出一个例外——参见 references/rationale.md,“为什么完全不设记忆层级”。

如果候选项不符合步骤 2 中的任何筛选条件,就说明应在该步骤添加筛选条件(步骤 6 的自我更新)——绝不能因此转而使用 harness 功能。这个缺口通常没看起来那么大:in-progress 计划已经由 plans.py list 排在所有其他内容之前。

4. 未竟事项检查

这与记忆扫描分开进行:在_本次_对话中,是否存在尚未完成的状态,既不值得记忆(步骤 1 失败),也没有计划文件涵盖,但压缩后仍会丢失?请明确指出——如果确实是值得继续推进的设计/构想工作,建议添加一条 plans/*.md;如果只是确实短暂的任务状态,就明确说明可以放下。

阅读对话,而不只是摘要。 压缩后的会话会交给你别人写的提要:中间摘要恰好会遗漏本步骤要捕捉的未竟事项,而且其自信的语气容易让人以为内容完整。python3 $H turns 会按顺序打印真实的用户发言和 transcript 中每个 AskUserQuestion 的回答——要重新阅读原始指令,而不是对其进行概述的回顾。已于 2026-08-28 确认:简报中“之后需要完整运行一轮分级检查,目前只确定了一个常见原因”这句话没有出现在任何摘要中,明确拒绝进行的使用方全面检查也同样没有出现。 在读取任何内容之前,先确认这份转录属于你。 在后台任务中,会话 ID 不是转录 ID,对它的每一次猜测都会指向属于别人的真实文件——这一点已于 2026-09-01 得到证实:一次采集读取了陌生人的会话,并报告了 386 次调用,没有一次属于该任务,却丝毫没有迹象表明哪里出了问题。脚本会从任务的 state.json 中解析这一信息,并打印最终确定的路径;--expect '<a command this session definitely ran>' 是零成本的核验方式:手头没有 ID 时按内容选择转录,有 ID 时则验证它。绝不要根据推测出的 ID 重构路径——错误 ID 若没有对应文件,会直接报错;若指向错误文件,读取输出时却无法分辨它与正确文件。

任务说明有三种来源,而一次运行即使找到两种,也不会知道自己漏了一种。 一种是用户轮次;第二种是 AskUserQuestion 的回答;第三种是用户在轮次仍在运行时发送的消息,Harness 会将其记录为 queue-operation,而非用户轮次。turns 会标出这三种内容,并将 Harness 自身的噪声(任务通知、子 Agent 交接、打断标记、斜杠命令包装)单独计数,因此“六个用户轮次”不会实际上只代表三个。子 Agent 的报告属于轮次中消息,不是用户消息;直到 2026-09-29,它一直被计作一条此类消息——两个已归档会话中,6 条“轮次中”记录全都是这种情况。2026-09-02 已确认:某个会话中最丰富的指令——新增范围,大约占最后三分之一的内容、两个计划和六次提交——是在轮次中发送的,却没有出现在提取结果报告的六个轮次中。最要紧的时候,遗漏恰恰无从察觉,因为用户会在 Agent 工作时想到新内容,于是发送轮次中消息;因此这类消息极有可能是全新范围,之前没有任何线索可供还原。

回答占任务说明的一半;在工具驱动的会话中,回答包含的内容甚至更多。 用户轮次是带有文本的 type == "user";AskUserQuestion 的回答不属于此类,因此针对前者编写的扫描不会找到后者。2026-08-31 已确认:一次持续五小时的会话,提取结果返回了十个“用户轮次”,其中大多数是斜杠命令包装,还有一个是 /clear——而会话中的每一条实质性指令都是以回答的形式发送的,一条也没出现在这些轮次中。若采集只读取轮次,就会得出用户几乎没说什么的结论,并根据这个结论进行采集。两类内容在内容上有所不同,这也是不完整的筛选比计数错误更糟的原因:采集者可以根据会话后续行为推断出选项列表中的一两个词,而用户输入的回答则是用户用文字写下自己想要什么;这正是此步骤要还原的内容。

[PITFALL:此筛选器曾在两个方向上都出过错,而且两次错误的版本看起来都像是正确的。 起初,有个启发式规则会在工具结果的任何位置查找“question”和“answers”,因而把 Read 输出也当成真实回答。随后又将规则收窄,只认 Your questions have been answered: 这个标记,却漏掉了所有以 The user answered: 开头的输入回答——当时有个会话的三条输入回答承载了任务说明的后半部分。收窄规则自己的确认备注,早已预言了后续版本的问题,只是当时没有意识到:“这里无伤大雅,因为两条真实回答都找到了;但在回答更多的会话中,恰好就会返回一个看似合理却不完整的集合”。现在,turns 会按工具调用 ID匹配——它会查询转录中调用的是什么工具,而不是工具输出看起来像什么——并同时打印原始前言计数,以便发现不一致,而非悄悄消除差异。出现不一致很正常:本技能的文本本身就引用了这两个标记,因此任何加载了本技能的转录都会把本技能自身计入。]

5. 实时状态扫描

根本不在对话中的那些“悬而未决”事项。转录记录的是原本_打算_做什么;这些检查说明现在实际情况如何。运行 python3 $H sweep --boundary <instant> 来检查:进程、监听套接字、磁盘产物、会话写入过的每个仓库的 Git 状态、CI、两个存储、吸收队列、depends_on 计划、写在所有仓库之外的文件,以及编辑记录中提及但并不存在的路径。对于转录无法显示的仓库,添加 --repo <path>。即使会话看起来井井有条,也要运行此检查,因为这些问题每一种都至少出现过一次。

其中两个步骤会调用平台自带的工具:在 Linux 上,进程信息来自 ps -eo,监听端口信息来自 ss -ltnp;在 Windows 上,则来自 Get-CimInstance Win32_Process 和 netstat -ano。本技能中的所有测量都使用 Linux 这对工具;Windows 这对工具的行为是根据有文档记录的输出推断的,且只针对样本文本进行了测试。因此,初次运行 Windows 检查时,应把结果视为有待验证的说法,而非定论。在 macOS 上,或在任何无法运行某项列表命令的环境中,该步骤都会说明自身不可用及原因——这是诚实的答复,而非一切正常的证明。检查中的其他内容使用 git、执行框架自身的文件和 Python,可在任何地方运行。

脚本无法判断某项发现意味着什么。这需要依据以下列表:

此处每一行都说明了一项后果;如果某行只说明状态,就会有人替它编造后果。 报告必须说明某项为何被列入,因此只报告事实便戛然而止的条目,会让负责报告的会话自行补充理由——而它会合理地、根据自己记得最接近的事情来补充。已于 2026-09-08 确认:存储中“未推送提交”这一行要求报告数量,却没有说明代价;一次收集过程填补了这个空白,写道“未来会话都不会通过 absorb 获得这些提交”,这句话前后重复了两次,且两次都无人质疑,因为听起来像是机制在正常运作。这是错的;absorb 读取的是本地目录。下文其他每一行都写明了出了什么问题——孤立的服务会把 .env 提供给局域网,退出码被屏蔽则会使成功的说法失去依据——而唯一没有说明后果的那一行,恰好导致了虚构的后果。因此,某项代价很小时,就说明它很小,不要让它悬而不述:明确说明代价很小,就不会被夸大;没有说明的代价则可能被夸大。

  • 进程及其提供的内容。 后台轮询进程会在启动它的轮次结束后继续运行——已于 2026-08-28 确认:有四个 CI 轮询循环已运行 36 小时,仍在轮询,而它们的退出条件永远不可能成立;只有收集过程能发现它们。对于任何持有监听套接字的进程,发现重点应是绑定地址和它提供服务的目录,而非进程是否仍在运行:长期运行的开发服务器本来就_应该_保持运行。开发服务器的默认绑定地址通常是所有网卡;在本机上,这个默认设置看不出来,因为无论是否绑定所有网卡,本地运行的表现都一样,区别只在于可访问的受众范围。已于 2026-08-31 确认:python3 -m http.server --directory <repo root> 已运行 24 小时,绑定在 0.0.0.0,向整个局域网提供该仓库的 .env 和 .git。 绑定到回环地址会缩小受众范围,但不会消除问题。 孤儿服务仍然是孤儿服务,它提供服务的目录仍可被以当前用户身份运行的所有程序读取——在一台运行着多个 Agent 会话和浏览器的机器上,这可不是个小范围。无论绑定地址是什么,只要有未归属的监听器提供仓库根目录的内容,就应报告,并说明其中有什么;扫描会列出被服务的目录以及其中任何看起来像机密文件的文件,而对一个被 git 忽略的路径执行 curl,就能得到一个状态码。2026-09-02 确认:仓库自己的任务有意将 http.server 绑定到 127.0.0.1——这是 2026-08-31 设计时已排除的隐患——它运行了三个半小时,被一次性命令遗留,且其中被 git 忽略的 .env 返回了 200。检查差点止步于“绑定到回环地址,很安全”。这两种情况都应并列写入报告,这样下一位读者就不会把“检查绑定地址”当成完整规则。

    会再次出现的孤儿服务是另一个问题,再次杀掉它并不能解决问题。 报告已完成终止前,请重新检查 PID:同一端口和目录上出现新的 PID,意味着有东西在重启它,因此终止操作只关闭了一个实例,并未解决其他问题;而报告说“已终止”,描述的是一个已经不成立的状态。2026-09-05 确认:应用户要求,终止了同一个仓库根目录上的同一个 http.server,半小时后的扫描却发现它以新的 PID 再次出现,已运行十六分钟,并且像之前一样重新归属于 systemd --user——它再次成为孤儿,而非由仍在运行的会话持有;这项检查能区分这两种情况。应说明终止操作未能维持效果,并由生命周期问题来负责解决;在单个会话中循环终止进程,正是这项技能在其他所有地方都要避免的做法。

    现在扫描本身会回答这两个问题,所以请直接查看扫描结果,而不是重新推导。 每个进程和监听器都会附带其父 PID 及父进程命令;当父进程是 systemd/init/launchd 时会标记为 ORPHANED,是其他进程时会标记为 held by a live process,而父进程未出现在列表中时则会标记为 holder unknown——第三种是如实报告信息缺口,绝不能悄悄当作孤儿。每一行还会注明该进程是否在本次会话上次活动之后启动,借此区分本次会话遗留的进程与他人的进程;在运行多个并行会话的机器上,仅凭进程运行时长无法确定这一点。直到 2026-09-06,这两条都还是写在说明里的规则,每次运行都要执行两次 ps -o pid,ppid——这正是第 6 步所说的“脚本本可以杜绝的错误,就应该在脚本中解决”的情况。

  • 仓库之外的磁盘产物。 容器镜像和构建缓存、临时解释器、卷——这些都无法通过 ps、git status 或任一存储区检查发现。报告它们占用的空间,并附上一条建议的删除命令供用户批准;不要擅自删除,因为构建缓存由机器上的所有其他项目共享,而另一个会话即将复用的镜像若被删除,就要重新构建。应说明哪些是一次性验证产物,哪些是已提交的 README 中提到的内容——只有本次会话还能分辨二者。 2026-09-01 确认:一次验证容器修复的运行留下了一个 4.11 GB 的镜像、其他镜像共 445 MB,以及约 58 GB 的全机范围构建缓存;进程检查报告干净,且确实如此。 在当前会话期间创建的制品不一定属于本会话,这里同样必须严格遵守 git 条目中的并行会话注意事项。 扫描现在会将镜像与本会话是否运行过 docker 交叉核对——若一次 docker 调用都没有,就没有任何镜像可归因于本会话,不管时间戳显示什么——并将无法归因的条目单独列在自己的标题下,而不是直接丢弃,因为无论是谁创建的,这些镜像的大小都值得一看。绝不要为该标题下的条目提出删除命令:那是其他会话的工作,可能是正在运行的会话;本条不擅自删除的另一个理由是,其他会话即将复用的镜像一旦删除,就得重新构建。已于 2026-09-06 确认:一次收集将 20 个镜像(2.4 GB)报告为“本会话新增”,但该会话的 183 次 Bash 调用中根本没有运行过 docker。这种报告看起来很权威,却恰好落在没人核查的方向上——它给出了一个具体、可信的数字,而阅读报告的会话没有简单办法证伪。

  • 本会话编辑过,但没有仓库或存储覆盖的文件。 扫描会减去所有位于 git 仓库内的写入路径,并报告剩余部分;关键是说明如何恢复它。这里的其他每项检查都在确认存储是否整洁,而不属于任何存储的文件并非“不整洁”,而是根本没被看到。仓库中的示例文件加上一个会拒绝错误文件的验证器,是一条切实可行却不那么显而易见的恢复路径;“任何地方都没有副本”则是值得直截了当说明的发现。已于 2026-09-01 确认:某个会话在所有工作树之外编辑了一份未纳入版本控制的配置文件——没有差异、没有历史记录、没有备份,而且这是仓库读取它、并理应保护的那类运行数据。这次编辑是正确的,也得到了用户批准,因此它恰好是合适的例子:没有出错,扫描仍然看不到它,所以错误的编辑也同样会被忽略。

    恢复文件通常需要的是一条命令,而不是一句话;该用哪条命令,取决于这个问题是否会反复出现。 有三种处理方式:

    • 作为计划中某项发现的证据——例如暂存区中的探测脚本或复现脚本:plan-conveyor' 的 attach 会将它复制到计划旁边并在计划中记录;计划退役时,它也会随之退役。已于 2026-09-18 确认:一次收集正确地根据上文将一个复现脚本报告为没有任何副本,只差一条命令;执行 attach … --commit 后便解决了问题。
    • 下次事故会用到的诊断工具——按照仓库自身的惯例,将其作为脚本纳入仓库。附件会随计划一同删除,而“归档在某个稳定的位置”很容易被误以为“仍然存在”:已于 2026-09-20 确认,同一会话将一个探测脚本从即将退役的计划中取出,放回仓库,因为它所回答的问题会反复出现。
    • 位于所有工作树之外、仍在使用的配置文件——两者都不适用。文件会持续变化,副本只会成为过时的重复项,因此应像 2026-09-01 的例子那样,直截了当地说明这一发现。

    应使用哪个 attach 标志,以及为什么本地附件对 scan 不可见,应由 plan-conveyor' 说明;附加文件前,请先阅读其说明。

    测试框架的暂存目录中的条目会单独列为一组——例如会话暂存区或后台作业的 tmp/——因为这些文件本来就是临时的,只有上述第一种处理方式适用于它们。过去它们曾占满整个列表(9 项中有 9 项,2026-09-09);这些条目会保留在列表中,而不是仅计数,因为那里就有用于处理复现脚本的 attach。普通的 /tmp 不单独分组:直接写入其中的文件就是原始发现。 这项检查及其相邻检查——写入文件但文件并不存在的路径——只读取记录,因此都会打印 none 或 skipped,绝不会什么都不打印。 直到 2026-09-08,只要没有解析到记录,它们就会从报告中消失:不是空值,而是完全缺失;报告里的其他部分却照常打印,因此读者根本无从察觉有缺口。两次得到确认——2026-09-03,同一会话的降级运行只覆盖了一个仓库,而解析成功的运行覆盖了三个,并且完全没有生成 CORRECTION? 行;以及 2026-09-04,一次读取了整轮扫描结果的收集流程继续往下执行,直到为后续步骤重读此文件时才发现这个缺口。现在,标头也会说明:出于同样的原因,仓库集合缩减为工作目录。

    两项检查也都会各自打印限制,而它们共同的限制就是工具调用。 这里的每项计数都根据记录得出,因此脚本在单次 Bash 调用内部做了什么,不在检查范围内——而截至 2026-09-08,这个边界问题有三个彼此独立的实例,无法通过修复其他问题来解决:仓库自己的 inv 任务而非 Edit 重写了实时配置,导致这项检查在它本来要检查的那类文件上恰好返回空结果;从脚本内保存的列表中重新克隆了七个库条目,但 argv 只列出两个;以及一个截断计数器没能发现其验证脚本自身产生的 SIGPIPE。读者如果只在某一行看到一次这种情况,仍会把下一行当作完整结果来读,所以这些行本身要说明这一点,不能只写在本页。

  • 本会话自身对规则的遵守情况,扫描中的其他内容都无法触及:

    python3 ~/.agents/skills/session-bash-audit/scripts/audit.py --session <session-id> \
      --until <the step 0 boundary> \
      --compare ~/.local/state/session-bash-audit/<baseline>.json
    

    基准是你用该技能的 --save-baseline 保存的基准,默认写入 ~/.local/state/session-bash-audit/。如果没有基准,仍要运行审计并去掉 --compare——各项比率和 exit-masked 结果本身仍然有效,只有比较才需要基准。绝不要拿别人机器上测得的基准来比较:那样报告的是你的会话与对方配置的差异,而不是与你的规则之间的差异。

    还要检查比较是否跨越了检测工具本身的变更,这也是同类错误,只是发生在更低一层:由不同 audit.py 写入的基准,会把模式定义的变更归因于你的会话行为,无论哪种归因方向恰好对它更有利。基准会在 instrument 字段中记录脚本自身的提交;如果该字段为 null,或文件写于该字段出现之前而不包含它,就必须从其他地方找答案——写入该基准的会话记录,以及它调用的脚本的 mtime。跨越变更的比较并不会自动作废;要作出判断,依据应是差异内容,而非提交本身:查看期间哪些行实际发生了变化,并据此说明。2026-09-05 得到确认:那个会话先固定了这些模式,然后拿三十六分钟前保存的基准进行比较——结论是干净的 11/11,而且仍然可读,因为十一项受评行中没有一项是固定模式时改动的。这样表述才是诚实的结果,JSON 中没有任何内容能得出这一点。

    [陷阱:最初的措辞写的是“跳过此步骤”,读起来像是跳过审计——而该步骤本身就是审计。2026-09-04,一次会话按狭义理解执行了这句话,将问题记录下来;它还是照常运行了审计,也因此得到了最有用的数字:189 次调用中有 23% 的 exit-masked,有五条消息告诉用户某个门控检查通过了,且每一条都来自经过 | tail 处理的运行。这五次重跑后都再次通过,正因如此,这种措辞很容易被忽视——如果不运行审计,这次会话根本不可能知道跳过会导致漏掉这些结果。] 从脚本解析出的转录中获取 <session-id>,不要随便从某个路径中取值 ——在后台任务中,任务输出路径里的 ID 指向的是另一份转录;审计会针对它干净地运行, 最终判定描述的却不是你的会话。

    --until 不是可选项,原因也不是扫描结果看起来不好。 包含这次扫描的数据衡量的是扫描本身: 它的调用是检查,与会话中的工作命令属于不同的总体,而且会把比例拉向它们自身的形态,具体 方向取决于它们的倾向。2026-09-01 对同一天的两次运行进行测量——一次在 head/tail 上 从 37% 升至 40%,另一次在 git -C 上从 22% 降至 17%。“收集过程抬高了自身的数字” 这一说法不对,也会让第二次运行看起来像是反例。报告两个数字,用一行写明:“工作期间 为 6%,包含此次扫描为 14%。”前者代表会话;后者代表诚实。

    claims 和 audit.py 对 exit-masked 的计数方式不同,出现不一致是正常的。 claims 会匹配原始命令,因此字符串中引用的管道或 heredoc 正文都会计入;audit.py 会剥离引号 部分和 heredoc 正文,因此不会计入。截至 2026-09-06 的一周内,claims 标记而 audit.py 未标记的 390 次调用中,有 388 次是 audit.py 的错误(现已修复),另有 2 次是 claims 将引用的文字也计入。现在 claims 的结果预计会略高,不要把任一数字当作对另一个数字的核验 ——某个会话中两者分别为 7 和 4,多出的 3 次全是探测的 heredoc 正文中的 2>&1。

    在对此作出任何判断前,先看 gate/listing 的区分。 审计行会打印 exit-masked = n m wrapped a gate, k a listing,而且当 m 为零时,绿灯从未面临风险, 无论 shell 做了什么:没有任何结论依赖被屏蔽的退出代码,因为没有被屏蔽的调用是 gate。 这比下面的 pipefail 检查更简短、更有力,不需要额外命令,而且这是会话的常见情况:gate 不接管道运行,只有列表命令接管道。2026-09-06 首次收集时确认了这一区分可用:4 次被屏蔽的 调用中,0 次是 gate,而 8 条绿灯 gate 结论全部来自未接管道的运行。只有当 m 大于零时, shell 的状态才会影响判断。

    m 是词表给出的答案,不是事实,因此在走捷径前先看样本。 audit.py 的 GATE_RE 能识别测试、lint 和类型检查运行器;凡是编写该列表时没人想到的 gate,都会被算作 listing。 2026-09-13 已确认:一次收集结果显示三次调用均为 0 wrapped a gate, 3 a listing,而这三次调用 实际上全都是通过 head -5 执行的 plans.py scan ——也就是保密性 gate;一旦失败,就会 不可逆地泄露客户姓名,也是这一类中退出代码最重要的 gate。样本会直接打印在判定下方,因此 只需阅读两秒,无需运行命令;这个零值是依据下方三段中“对有理由怀疑的零值进行检查”的规则 发现的,而不是靠这一段。自 2026-09-28 起,GATE_RE 已匹配 plans.py scan;规则仍然成立: 下一个未列入的 gate 也会以相同方式被算作 listing。 更广泛的问题由 plans/2026-09-13-gate-re-has-no-term-for-the-confidentiality-scan.md 记录。

    如果被屏蔽的调用确实包住了 gate,先询问 shell,再决定是否值得重新运行。 一条调用:

    setopt | rg pipefail          # zsh; `set -o | rg pipefail` under bash
    

    启用 pipefail 后,管道会报告最右侧的非零状态,因此 inv quality.precommit 2>&1 | tail -3 在门禁未通过时本来就会以非零状态退出,本次会话中读到的每个绿色结果 都是真实的退出码。报告次数,并说明保证已生效;不要重新运行。若未启用,下面的重新运行是避免会话误报 通过的唯一办法。已于 2026-09-06 确认,这是 m 大于零的首个会话——5 次被掩盖的调用,其中 4 次是 门禁或套件,3 次绿色声明都依赖于这些调用,setopt 显示 pipefail 已启用,没有重新运行门禁。上方 的分支已于同一天在 m 为零的会话中确认,因此现在两种情况都已实际检查过。

    询问 shell,而不是配置文件。 文件说明应该设置什么;但该选项可能受某个 harness 环境变量控制 (在作者的机器上,~/.zshenv 仅在设置了 CLAUDECODE 时才会启用它),所以会话自身的 shell 与同一台 机器上的其他 shell 得出的答案可能不同。检查必须作为 Bash 调用,在正在审计的会话中运行。

    此保证只涵盖退出码,除此之外一概不管。 | head -20 仍会丢弃输出,因此,若某项声明依赖于 _阅读_门禁输出而非其状态,该声明仍未经验证;而且在 cron、CI、容器或任何未包含该片段的机器上,这条 守卫意味着同一条命令会丢失其状态。会话若学会了这种写法,并将其写进在其他地方运行的脚本,在这里 并无妨碍——这就是为什么 exit-masked 仍然算作一项发现,而本段只是跳过重新运行的理由,绝不是跳过 该行的理由。

    当 pipefail 关闭时,本会话自身的绿色结果未经验证——在相信其中任何结果之前,都要重新运行门禁。 被掩盖的退出码此时并非风格问题:它意味着命令经过一个丢弃真实结果的过滤器后,报告了成功。已于 2026-08-31 确认:某个会话大约二十次运行了 inv quality.precommit 2>&1 | grep -Ei "…" | tail -3,把没有匹配结果当作成功,并推送了三个未通过的提交—— basedpyright 打印了 0 errors, 6 warnings,门禁却以 1 退出,但管道报告的是 grep 的退出码。不要通过 管道运行它并重新执行——运行 inv quality.check > log 2>&1,然后阅读日志——并根据次数决定是否运行门禁, 以免无谓地重新运行耗时的门禁。这是脚本刻意不替你运行的那条命令:这是仓库的命令,不是这项技能的命令。

    重新运行可以确定绿色结果是否属实,但无法改变它们已经被声称的事实。 exit-masked 非零的会话通常 会在整个运行过程中多次告诉用户“门禁通过”,每次都依据已被过滤器丢弃的证据;无论重新运行的结果如何, 这些话都留在对话中。 python3 $H claims --until <the boundary> 会统计这些声明,并将它们与被掩盖的调用次数对照。报告次数,并附上重新运行的判定。“在被掩盖的 调用中,15 次称门禁通过;重新运行以 0 退出,因此这些声明成立”只是补充说明。相同的话若以“重新运行以 1 退出”作结,就是一项读者仍在面对的错误陈述:这与未推送的提交用于更正已推送内容时的情况相同,因此 出于相同理由,它也应列入“现在需要处理的事项”。对话是用户实际依据的记录,也是后续提交无法修改的唯一 产物——只有之后的消息才能更正,而且前提是有人写出那条消息。已于 2026-09-02 确认:一个持续十小时、 exit-masked 占比为 28% 的会话,大约十五次报告门禁通过,每次都来自经 | tail 处理的运行。重新运行以 0 退出,十五次声明都成立——这正是最容易让人忽略此规则的结果,也是这里强调要报告次数而不是发出警告的原因。 在技能与指令滥用组中报告这两个数字和比较,而不是放在结论里;自我鞭挞式的报告会掩盖用户需要的发现。已于 2026-08-30 确认:一个整天都在编写禁止通过 head/tail 管道传递门禁规则的会话,随后在自己三分之一的调用中做出了这种操作,却报告说“进展顺利,门禁始终通过”——这话没错,但离题了。编写规则并不能证明自己遵守了规则,这正是数字必须来自会话记录的原因。

    **这些检测工具中任何一个给出的零,都是一个说法,而你手上有证据可以核实。**报告零之前,先问问非零时会是什么样子,以及你是否看到了——会话自己的消息和命令就在眼前,因此核实成本低,而跳过核实是粗心,不是高效。迄今发现的每个错误的零都是这样找到的,没有一个是先读代码发现的。已于 2026-09-06 确认,同一轮运行中出现两次:claims 报告会话中 0 条门禁通过消息,但会话文本写了六次“两个 CI 阶段都通过”,因为匹配器使用的是本地门禁的词汇表,没有 CI 这个词;遵循率一行则报告所有比率都是 0%,尽管会话输入了 rg -rn …,因为 rg-replace 不在 audit.py 的任何显示集合中。这两项都已记录。与自己期望相符的零,最值得核查;一个明显信号是,你有具体理由认为某行应该命中,却得到了零。低于你记忆中的计数,也要同样核查,因为计数不为零时,其他情况都不会触发这种提醒。已于 2026-09-26 确认:claims 报告一条门禁通过消息,但会话实际上说了两次,因为“Green, 707 tests”从未使用“gate”这个词。只有阅读会话记录的人记得自己说过不止一次,才发现了漏计。

    **而从会话记录中用行级 grep 得出的、比检测工具计数更高的数字,只是另一个问题的上限。**一行 .jsonl 可能包含命令在工具结果中的回显、会话对命令的引用、载入上下文的技能正文,以及嵌入先前上下文的逐条元数据,因此对它运行 rg -c 统计到的是文本,不是调用。应通过 tool_use id 进行核对,或者使用已经具备此功能的 turns、claims 和 audit.py。两次确认过:2026-09-18,一次 grep 显示有 166 次调用以 cd 进入会话的仓库,而去重后的计数是 5;2026-09-27,另一次显示 135 次,而 audit.py 报告 1,且结果正确。第一次只差一个编辑,就能成为持久语料中的一条记录,也就是一个符合真实观察的原因;问题的信号不是数字本身,而是同一屏幕上另一个检测工具给出了不同结果,却无人查看。

  • Git 状态,检查会话触及的每个仓库——不只是主要仓库。触及是指写入仓库,或执行了读取之外的操作:如果只是通过 git -C 读取仓库,以便准确撰写并存档计划,就不在检查范围内,测试框架的重置操作中的 cd 也不算。如果你以会话记录无法显示的方式更改了仓库,请通过 --repo 指定。最常见的实际遗留问题是未推送的提交,而以这种情况结束的会话通常以为自己已经推送了。检查会读取上游分支,而不是直接输入 main(2026-08-30 测量结果:71 个克隆中有 22 个位于 main,比位于 master 的还少);计数命令会在不接管道的情况下运行,因此分支错误时会以 128 退出,而不是平静地打印 0;同时会检查 git fetch 是否成功,之后才会信任任一结果——在这台机器上,fetch 需要 Zenity SSH 密码短语对话框;如果键盘前没有人,就会以 Permission denied (publickey) 失败,使 origin/<branch> 原封不动地停留在原处,因此针对过期引用计算出的超前提交数仍然看似合理。发生这种情况时,机器自己的诊断信息会指出解决方法(这里是 inv ssh.check)——不要改用 ssh-add,并且应将修复作为每次调用的环境变量前缀,而不是使用 export;后者不会保留到下一次 Bash 调用。 计数不会替你做出的三项判断:

    • 谁写了尚未推送的提交。 当多个会话并行运行时,超前提交数不一定是本会话的工作,而“你有两个尚未推送的提交,推送它们吧”会以看似例行的建议,将另一个会话尚未完成的历史发布出去。指出哪些属于本会话,哪些不属于,并让用户决定。2026-08-29 确认:两次推送之间,一个并行会话的两个提交出现在超前提交数中;询问而非推送,是唯一能发现它们的方式。
    • 其中是否有提交修正了已推送的内容。 这不是可以延后的工作——这是读者正在看到的错误信息;如果只是平铺直叙地报告,它就会和超前提交数中的三项计划更新毫无区别。本次检查会列出当前尚未推送、但本会话早些时候已发布的路径;重叠并不能证明有问题,但这是一份值得查看的短清单,而且大多数会话的清单都是空的。将其列入“现在需要处理”,并说明远程端目前声称了什么。2026-08-30 确认:一个会话推送了一段解释软件包存在理由的描述,一小时后发现这一论述是错误的,便提交了修正但没有推送——于是远程端继续提供已知错误的说明,而修正则和普通整理工作一起留在超前提交数中。仅限同一会话的情况;“此仓库目前发布的内容中,是否有已知错误的部分”是另一个问题,任何一次检查都无法在有限时间内回答。 判断是否为修正前,先看差异——这行文字已不再这样做。 直到 2026-09-08,它都会打印 CORRECTION?,并仅凭两组路径就直接归入“需要立即处理”。通过缩小范围,修复了三种故障情形中的两种:并行会话的工作(检查范围与本会话自己的写入相交后,这种情况就消失了),以及本会话从未接触过的仓库。第三种无法修复:“推送前接触过”和“推送后接触过”涵盖了一个长期会话持续工作的所有文件。2026-09-04 确认了这一点:所有三个被标记的路径都是本会话在自己的仓库中写入的,而且每一项都是对已发布内容的_新增_——新段落、追加的事件记录、追加的断言。远程提供的内容更少了,而不是提供了错误的内容。因此,这一行只说明它衡量的内容,只有差异才能做出的判断仍由你来做。
    • 计数是最新的。 在报告时重新读取;在会话较早时读取的计数,只是会话的记忆披上了测量的外衣。2026-08-30 确认过:一个会话报告计划存储有“7 个未推送提交,其中 4 个来自其他会话”;计数时确实如此,但到收集时已变成 27 个,其中 25 个来自其他会话。在这样并行工作的机器上,共享多个会话的仓库中的计数变化最快,而这些也正是最可能被要求在收集时报告的仓库。
  • 此技能本身写入过的同级仓库。 技能自更新(步骤 6)会在本地提交,只有推送并重新安装后才会到达任何地方,因此你的技能仓库——也就是这些技能安装自的那个仓库——是会话接触过、也最容易被遗忘的仓库,因为提交后编辑看起来就像已经完成了。

  • 本会话让其他会话运行的路径。 写入始终加载的指令文件或 SKILL.md 命令块的规则,会指定这台机器上的某个路径——通常是已安装的副本,而不是会话正在编辑的检出版本。扫描会报告写入文件但不存在的、以主目录为根的路径;请运行那些确实存在的路径。2026-08-29 确认过:一个会话部署了一条 ~/.agents/AGENTS.md 规则,指向 ~/.agents/skills/<name>/scripts/<file>,而已安装的技能仍然没有 scripts/ 目录,因此一条适用于整台机器的规则指示之后的每个会话运行一个不存在的文件。检出版本始终运行正常,这就是为什么没有任何迹象暴露这个问题。 只读取 AGENTS.md、CLAUDE.md 和 SKILL.md,因为仅凭文件是否存在无法区分指令与说明。 已于 2026-09-04 确认:一次以各编码 Agent 在何处读取指令为主题的会话,其收获结果列出了十个路径,十个全是假阳性——这些是未在此处安装的 Agent 的供应商目录,安装程序有意不会创建它们;还有一张文档表格记录了另外三个 Agent 会查找的位置,这些位置在这台机器上永远不会存在,而表格内容本身是正确的。代价从来不是噪声:一个小节曾经全是假阳性,下次收获时就会被略读,而上面的真阳性本来会是第十一行。出于同样的原因,同一路径的不同写法现在会合并为一行。

  • CI:针对本次会话推送的任何内容。 本地门禁通过,不代表 CI 运行通过。扫描会读取 gh run list --json,并将结论作为 JSON 读取正是关键:--json 不会返回退出码,因此管道无法丢掉退出码。绝不要手写 until 循环,也绝不要通过过滤器监看——这已确认过三次,每次都是某次收获在警告就在眼前的情况下执行这份检查清单,却把 tail 的 0 报成了运行结果。

  • 任何仓库之外的共享存储,如果你有使用的话。$RESEARCH_HOME——只有在使用 research-library 技能时才设置它——存放克隆、缓存,以及会话添加到任何 git status 都无法涵盖的位置的内容。问题不是缺少文件,而是约定只完成了一半——克隆缺少 SOURCE.md,或克隆过程半途失败;由于此存储没有纳入版本控制,它恰恰不会被这里的其他任何检查发现。还要检查会话修改过的条目,而不只是新增的条目:有意让条目偏离存储约定的做法,看起来会符合约定,因为条目确实存在,元数据也完好无损。已于 2026-08-30 确认:某次会话将一个 --depth 1 的参考克隆加深到约 436 个提交,以便查看某个依赖项的约束历史;这是正确的做法,却导致存储中有一个条目悄然不再符合自身约定。请在该存储用于记录每个条目元数据的文件中,记录这种偏离及其原因。

    mtime 发生变化并不代表这是本次会话所做的工作,而这一条最容易犯这种错误,因为整个库会通过一条命令整体刷新,任何并行会话都可能向其中克隆内容。扫描会根据本次会话自身的命令,归属每个发生变化的条目——依据是条目的目录名称,或创建该条目的 URL 中包含的 <owner>/<repo>——其余条目只打印总数。已于 2026-09-07 确认:一次扫描报告有 30 个条目发生变化,其中包括一整个属于另一会话研究主题的点文件管理器条目集群;其中只有 五个是本次会话所做的,而且其会话记录说明了具体内容。不要把这一条中的总数报告为本次会话的工作——重要的是归属到本次会话的数量;无论是谁创建了条目,上述约定检查都仍然适用。

  • 计划存储库 $PLANS_HOME:单独列出,因为原因不同。 它由 plan-conveyor 技能设置;如果你不使用该技能,它就不会存在,此时扫描会跳过这一项并说明原因。扫描会报告其脏状态和未推送的提交,并以只读模式运行 plans.py absorb。这里有两个责任方不同的问题:未提交的计划是本次会话自己的烂摊子,而一份为此仓库提交的计划则是另一会话留下的东西。之所以在一个主题就是清扫未扫描状态的技能中忽略了这一点,是因为相邻的两条似乎都将其涵盖:存储是一个 git 仓库(所以 git 那条看起来涵盖了它),而且它位于所有工作树之外(所以共享存储那条看起来涵盖了它)。两种归类都把问题推给了另一条。git 那条实际漏掉的是未提交的工作:未提交的计划不是提交,因此任何领先提交数都不会发现它。 提交和推送是两项不同的发现,代价也不同,只有第一项需要紧急处理。 未提交的计划会造成实时的同机并发成本:只要脏状态窗口持续,plan-conveyor 就会让其他所有会话改为新建文件,而不是编辑。每次都要完整、明确地报告这一点。未推送的计划只会带来异机备份风险,不会造成其他影响——对于那些不在任何仓库中的计划,存储区是唯一副本;对于那些用 --for 指定仓库、但尚未被仓库接收的计划,存储区还保存着它们的传输中副本,因此磁盘故障只会导致这些计划丢失。请照此措辞说明,并由用户自行决定;用户已经说明一次只在一台机器上工作,而且会在去其他机器工作前推送。也不要写“存储区没有远程服务”——可共享层有一个远程服务,敏感层则刻意不设远程服务;推送受该技能的内容扫描限制。

    [陷阱:不要采用交接那套说法;它是最容易想到、但错误的答案。 absorb 只读取本地目录,不会读取远程内容——plans.py 中没有任何 fetch、pull 或 ls-remote,而且 plan-conveyor 明确写着 Network: none——所以计划一经归档,其文件一旦存在,就会立刻对机器上的所有会话可见,与推送状态无关。这个错误说法之所以显得可信,是因为旁边确实有一种跨会话交接:new --for 会把计划写入存储区,而该仓库中的下一个会话会收到它。“交接需要推送存储区”是从某个真相出发很容易得出的错误结论,因此才会在一个会话中未经核实地说了两次,2026-09-08。]

    上文的 仓库 超前计数不会因此被夸大,而且应继续保持紧迫性:未推送的仓库提交意味着更改尚未进入产品,CI 也从未见过它;无论使用多少台机器,这一点都成立。2026-09-09 时对此进行了核查,而不是想当然,因为这两行看起来很相似,如果把同一习惯套用两次,就会把两者都降级。 即使 plan-conveyor 已经要求每个会话首先运行 absorb,这里仍会运行 absorb: 只要会话持续运行,队列就会不断补充,因为提交到队列的会话是并发运行的。2026-08-30,在一个正确遵循首次调用规则的会话中测得:会话开始时有 4 个计划,两小时后又有 4 个,五小时后还有 1 个;最后一个涉及凭据泄露,未读地搁置了半小时。如果报告的是事务进行到一半时的存储状态——存在并非由你造成的未提交更改——就不要绕开它;这意味着另一个会话正在主动占用该目录。

  • 阻塞条件可能已解除的 depends_on 计划。 上面的路由筛选器会把需要等待正在重组的仓库的工作放进带有 depends_on 标签的计划中——这样既能安全存放,又不会触发处理。没有任何机制会监视指定的仓库,因此,等待一个已就绪数日的仓库的计划,与等待一个仍在忙碌的仓库的计划无法区分。检查会列出带标签的计划及其目标;由你来分类,因为该标签有两种含义,需要给出相反的判断。因仓库正在重组而搁置的工作(本条所述情形;如今有了 new --for <repo>,这种做法已不推荐)应根据该仓库的当前状态作出判断。plan-conveyor 文档中定义的含义——“没有兄弟仓库的配合,该计划就无法完全落地”——则不是:指定仓库空闲并不能说明什么,因为真正的阻塞条件是该仓库尚未做出的更改;只有阅读计划才能确定它是否已完成。只报告第一种情况的就绪状态;根据干净的 git status 输出得出“七个计划已就绪”,是该检查从未验证过的结论。2026-08-29 确认:有八个带标签的计划,三个兄弟仓库都干净,其中只有一个计划符合队列处理的情形。并且要根据工作树而不是计划中的描述来验证——同一天,检查根据干净的 git status 和已落地的计划得出“手术已完成”,几分钟后却发现有两个已修改文件。

  • 会话承诺过却从未验证的工作——例如添加了某个测试层级却从未运行,或更改了某个使用方却从未全面检查。“落地后我会汇报”是最后一条消息中的承诺,收尾时必须兑现或撤回。

  • 交接给另一会话的工作,以及它在此处造成的阻塞。 这位用户会并行运行多个会话,因此“另一个会话正在处理 X,不要碰它”是常见指令——它还会产生其他检查都发现不了的状态:不是进程状态,不是 Git 状态,不是 CI 状态,也不是未兑现的承诺,而是本会话正因某项依赖而停下,并且有意不去解决它。请在报告中写明这项工作及其阻塞的事项,避免交接内容在两个会话之间遗漏。不要重新探查已交接事项的状态并进行汇报;这样做每调用一次,就违反一次指令。2026-08-28 确认:一个会话完成了工作,但重启后 SSH Agent 为空,无法推送;有人告知它该事项归另一个会话处理——而被阻塞的推送仅剩的记录就是收尾报告。

  • 将发现作为新问题报告之前,先确认是否已有负责人。 回溯历史或跨仓库检查会发现本会话并未造成的问题;在运行并行会话的机器上,真实发现最可能的解释是别人已经发现了它。升级问题之前先检查:兄弟仓库的近期提交及其未完成计划。若某项发现已有负责人,就将其报告为_确认仍未解决_,并注明负责跟进的计划——绝不要将其作为新发现,也不要采取行动处理。2026-08-29 确认:一次保密信息扫描在已发布历史中返回了 55 条命中结果,问题差点就被作为紧急事项报告;当时另一个会话已经重写了两段历史、强制推送、为残留问题提交了支持请求,并把这一切写进了计划。为已完成的工作进行了数次探查,还差点重复报警。

  • 在此处推送会让其他地方承担的义务。 其他检查都针对这台机器自身的状态;这是唯一一种由推送_产生_而非遗留下来的事项,并且这些检查都看不到它——使用方的工作树干净、CI 通过,恰恰是因为那里的更改尚未发生。2026-09-05 确认:一个会话更改了某个仓库的模块,而该仓库现在已被每个门禁步骤调用,随后进行了推送;检查报告显示脏文件数为 0、未推送提交数为 0、CI 通过、没有待处理事项。按照该技能运行的所有检查,这个会话都已经完成;但事实并非如此。该仓库的引导流程尚未固定版本,直到创建版本标签为止,因此每个使用方下一次运行 CI 时都会安装当时 main 分支上的内容,无需使用方采取任何操作,也不会收到通知。触发条件已经写下——但写在一个没人有理由打开的文件里。现在,检查会从机器中推导使用方,采用与 scan 查找私有术语相同的做法:检查自身清单或引导脚本中提到该仓库的所有工作区,以及该仓库面向使用方的文档(如果有)。报告就是全部操作——全面检查某个使用方意味着要在其工作树中运行任务,而这不属于本会话的职责,因此应将其写入报告和下一会话的提示词。该技能早已针对一个特定仓库了解这种情况,见上面关于技能编辑推送_并_重新安装之前不会传达到任何地方的条目;当时它将这一机制写成了特例。 文档写明触发条件时,扫描会读取该条件,只有本次会话的更改符合该条件,或无法读取触发条件时,才会触发义务提示。2026-09-18 确认:一次推送包含 5 个消费者,以及“报告并提交问题”,涉及 CI 工作流、文档、计划和测试,但这些内容都不在仓库自身“何时扫描”一节列出的路径下。对于一个广泛安装的仓库,每次推送都会触发的警告,读者会停止阅读。比较范围涵盖通过编辑工具写入的文件,正如该节结尾一行所述。而且,位于临时目录或任务目录下的仓库根本不会被扫描——改为在一行中点名,因为在临时工作区里随手 git init,曾导致三个部分变得更糟,还匹配到了它的基本名称 clone,而这个名称已由两个无关的仓库安装。

  • **本次会话是否实现了_这个_仓库某个开放计划所设计的内容。**失效情形是:一个会话完成了计划指定的全部内容,写好文档,却从未触及计划;计划仍然标为 idea,其中的问题已由代码解答,absorb 因为没有任何内容处于终态而不会提出它,而下一个查看 list 的会话看到的仍是正在进行的设计工作。2026-09-05 确认:一个晚上里,6 次提交解答了两个计划,完成实现的会话结束时却没有更新任何一个计划的状态,下一次收集工作时差点提议实现已经存在的内容。扫描会列出本仓库中符合以下条件的开放计划:计划点名的源文件有 3 次或以上是本次会话写入的,或本次会话写入的这类文件有 3 个或以上(不论次数),且计划最后修改时间早于会话开始时间;按计划点名的文件数量排序。后一种形式符合设计计划的特征——它会逐一列出将要修改的内容——于 2026-09-28 添加,因为当时漏掉了一个本次会话已明确解答的计划,却列出了一次附带的 conftest.py 修改。这是提示,不是门槛——2026-09-08 对 8 个仓库和 167 个开放计划进行测量发现,43% 的计划点名了后来有更改的源文件,这是噪声;使用主题代理指标后,这一比例也只有 14%。剩下的问题是结构性的:会话修改某个文件,会让所有关于该文件的计划看起来都已过时。只有 set-status 能更改状态。如果某个文件是 5 个或更多开放计划的主题,且这些计划占开放计划的 15% 或更多,就会合并显示:只匹配这类文件的行会合并为一条计数行,--verbose 会列出这些计划。2026-09-26 确认:一次 setup-repo 会话的 15 行合并成了 4 行和一条计数行,因为其中 11 行只匹配了仓库自己的配置文件。同一规则也会合并真正的核心文件(这里是 plans.py,占 20%)——可以接受,因为该行仍会点名它,而实现设计的会话知道自己实现了哪个计划。

  • 本次会话让其他地方的哪些内容过时了——这是对上面那个响应式条目的反向检查。此处的其他检查都在询问本次会话有哪些未完成事项;另一个仓库中的计划若描述了本次会话刚替换的机制,则正好相反,且没有其他检查能发现它:既不是未完成事项,不是流程,也不是 git 状态,而编写它的会话早已结束。代价不是过时的文字,而是一个预定测量的研究对象发生了变化,导致它输出一个看似可信的错误数字,而不是报错。2026-09-05 确认:一个会话替换了某仓库的门控输出机制,而另一个地方的计划记录了该机制作为已实现的第 2 层,并安排了一周后与基线进行比较;该基线专门保存下来,用于隔离这一层——此时若结果为零,就会被解读为“更改没有作用”。扫描会列出其他仓库和可共享存储中的计划,只要它们点名了本次会话写入的源文件;它们只是候选项,并非结论,与 depends_on 的作用相同,因为点名 quality.py 的计划可能讨论的是其他内容。本仓库自己的 plans/ 不会被搜索——会话本来就在查看这些内容,把它们也纳入会将对一个讨论充分的文件所做的修改变成一页虽属实却毫无用处的行。同样,名称不是每个包都各自有一份的文件(例如 __init__.py、pyproject.toml 等,会显示为 not searched)——这是上面的已写入路径检查最先吸取的教训。2026-09-13 确认:24 行中有 22 行只匹配了这两个名称,真正相关的两行排在它们下面。此外,只有当其他地方的计划有一段也点名该文件所属的仓库时,才会计入;若计划位于该仓库的检出目录或存储镜像中,则不受此限:主题是本仓库的 ci.yml,而不是任何名为该名称的文件。2026-09-26 重新运行检查,针对一次在机器的 setup 仓库中更改了 setup.toml、ci.yml 和 util.py 的会话:按基本名称匹配出 19 行,应用规则后剩 4 行,且这 4 行讨论的都是该仓库的文件。代价是可能漏掉只间接提及该仓库的计划;这一点已在本节自己的 limit: 行中说明。

6. 每次运行都要改进技能

这不是可选项,也不仅仅是为了减少摩擦。 这个技能会被实际用于自身:每次调用也是一次测试,作者表示,及时发现并修复问题正是这样做的目的。因此,在撰写报告前,请明确自问:这次运行有没有表明技能在任何方面存在错误、不完整或不够实用? 以下是一些信号,按最常被忽略的程度大致排序:

  • 用户的调用要求了技能尚不具备的行为(见下文第三个触发条件)。
  • 某个步骤执行后毫无价值,或者本次运行最重要的发现来自没有任何步骤要求检查的内容。这两种情况都说明流程方向有误;第二种更容易被忽略,因为最终还是发现了问题。
  • 某个步骤因不适用而被跳过——这个判断对吗?还是步骤的适用范围写得太窄?
  • 用户有任何事情不得不说两遍,或事后才问起。
  • 技能自相矛盾,或者某条指示在实际执行时产生了歧义。

现在就处理,不要留待以后;延后的技能修复就等于不会发生。改动应小而且只做增补。如果脚本能直接完成某项修正,就应该改脚本,而不是新加一段说明——已经有 6 个这样的修正,它们最初都写成了说明文字,之后问题还是再次出现。如果这次运行确实没有发现任何问题,请在报告中用一行说明——明确写出“本次运行无需修改技能”,以证明你做过检查。

当前会话所在的位置决定了“现在就处理”意味着编辑还是归档,这不是偏好问题:

  • 如果当前位于保存技能源代码的仓库中——即步骤 0 中名为 skills-state 的检出目录:编辑源代码并在本地提交,不要询问——改动可撤销,也可通过差异审查;在运行中途暂停等待批准,正是导致修复被搁置的原因。
  • 在其他任何位置,也就是几乎所有采集任务所在的位置:不要编辑,也不要提交。 使用 skills-state 打印的命令进行归档—— file a fix from another repo: python3 <plans.py> new <topic> --for <checkout> —— 在存储库中提交该计划,并在报告中写明文件名。归档就是立即采取行动,而非拖延,因此“现在就做”的要求仍然不变。
  • 如果 skills-state 没有找到检出目录——也就是本机安装的副本没有对应的源代码仓库;这适用于所有非作者用户——那就无处可归档,而读者无法落实的计划终将腐朽。请在报告中用单独的标题列出这些摩擦点,每项一行,不要归档。将问题反馈给技能作者是另一套本技能目前尚不具备的机制;不要自行另辟蹊径。

[DECISION: 全局规则优先于本技能自身的指示,这一点毫无疑问。~/.agents/AGENTS.md 明确禁止写入其他仓库,“无论技能自身如何指示”;这句话读起来就像专门针对当前这一步而写的。本技能之所以主张直接提交,是因为延后的修复不会发生;在写下这条指示时确实如此,但现在已不再如此,因为当时还没有 plans.py new --for,而 absorb 会让归档的技能修复在下次于相关仓库工作的会话中真正得到处理。两者的理由也不对称:在并行会话的工作区中悄悄提交属于正确性问题,而延后修复属于时效性问题。2026-08-30 已通过一次失败案例确认:一次来自无关项目的采集运行对这个文件做了两项正确且通过门禁的修改,并在无权写入的仓库中提交了它们;提交仍留在 git log 中,等待下一位推送的会话处理。] 部署已编辑的技能不属于收集步骤。 推送会面向外部,始终需要征得同意,遵循步骤 5 的规则;之后安装并重新加载技能属于 skill-authoring-taudelta 的流程,报告应将其列为待办事项,而不是代为执行。已于 2026-08-28 确认:此机器上的 Bash(git commit:*) 和 Bash(git push:*) 均在允许列表中,因此推送时不会出现权限提示——这完全依靠指令来约束行为,这是有意为之(参见 ~/.agents/AGENTS.md 中的“Proposing an enforcement mechanism for agent behavior”)。不要把没有出现提示理解为获得了许可。

7. 遇到摩擦时先询问,然后自我更新技能

不只限于本次会话。 有三种触发情形:

  • 某个候选项无法明确归入步骤 2 的任何路由筛选条件(例如,既可以说是特定于计划的,_也_可以说是跨仓库的长期偏好),或者重要性测试本身确实难以判断。
  • 用户纠正了此技能刚做出的路由决策。
  • 用户在调用时要求此技能尚不具备的行为。 诸如“请务必也检查 X,我不确定技能目前会不会这样做”的要求,是规格说明,而不是一次性请求。只在当前运行中应用这些要求,然后就此打住,正是此步骤要防止的失误——而且很容易被忽视,因为本次运行本身可能进行得很顺利。已于 2026-08-28 确认:步骤 5 中每一项实时状态检查都是以这种方式提出的;这些检查都已执行,并产出了本次会话中最有价值的发现,却差点因为没有任何提示要求记录下来而丢失。如果用户不得不问“你更新技能了吗?”,说明此触发情形已经出现,却被漏掉了。

前两种情形下,使用 AskUserQuestion 解决_当前_这项问题——遇到真正的歧义时,绝不要默默选边。第三种情形下,用户已经告诉你他们想要什么;直接将其纳入即可。无论哪种情况,都要把解决结果写回此技能的源文件(见下文),避免再次出现相同的摩擦。只在一次会话中解决问题,会违背共享约定技能的初衷。

应在最终报告之前完成回写,而不是之后——这就是为什么两个技能步骤都安排在报告之前——并在报告中说明更新了哪个位置(此技能的源文件,或始终加载的指令文件)。一次主题完全是“哪些内容可能丢失”的收集,不应以丢失它自身的经验教训作为结尾。

8. 收集报告

最后一步。全文使用项目符号,分组缩进——绝不写成散文段落。 收集报告是用来快速浏览的,而不是细读的;缩进列表才是浏览后能留下印象的形式。

同一会话中第二次收集时,要重新运行完整检查,只报告增量。 这两点看似相互矛盾,但都正确:步骤 5 必须重新运行,因为两次检查之间实时状态会发生变化——已于 2026-08-30 确认,两次检查相隔 20 分钟,其间有一个并行会话已提交到存储,只有重新运行才发现了这项变更;与此同时,重复列出用户 20 分钟前已经看过的发现,会掩盖新增的两条内容。说明自上次报告以来发生了哪些变化,并指出上一次报告,而不是再次复述。 它还会更正第一次归档的内容,然后再写入增量;这比增量本身更重要。报告只是成本低的一半:用户已经读过,而且终端关闭后它就消失了。第一次采集写出的计划会在会话结束后继续存在,而已经写进文件的数字不会有人重新推导。会话中途归档的一次测量是该会话的前缀;如果会话各阶段有所不同,那么会话前缀并不只是整个会话的缩小版。2026-09-07 已确认:同一会话相隔 2h40m 进行了两次采集。第一次归档了一行遵循度数据:n=211 chain=36% head/tail=20% sed-n=0%(1);第二次在边界处测得同一会话的数据为 n=306 chain=44% head/tail=27% sed-n=2%(7)——每项比率都朝着相反方向变化,因为最后三分之一是另一种工作(阅读五个同级仓库的配置),相比之前阶段,更多地使用了链式命令和 sed -n。这很常见,而且还会再发生。没有任何提示促成这次更正:第二次采集恰好记得自己归档过这行;如果用户只要求采集一次,而不是两次,语料库就会把这个前缀当作整个会话的数据。

**python3 $H filed --until <the boundary> 会取代这种记忆,而且每次采集都会运行,不仅仅是第二次采集。**它会统计本会话自身第 0 步的 boundary 调用次数,因此“这是第 #2 次采集”是读取出来的,而不是回忆出来的;会列出本会话写入的计划文件,以及本会话在每个文件中写入的含数字的行,并将文件中其他行标记为 (authorship unestablished);还会列出会话开始后 plans 存储中的每次提交,并标记为本会话的提交、(authorship unestablished) 或 (not attributed)——该存储由多个会话共用,因此,提交处于这个时间窗口内并不意味着它属于你;已标记的行只会被报告,不会被编辑。(not attributed) 行中只显示最新的十条,其余显示为计数;使用 --verbose 可列出全部行:在一次恢复的会话中,为了找出本会话的 3 条记录,需要从约 190 条中查找(2026-09-27)。计数行以及属于本会话或标记为 (authorship unestablished) 的每一行,始终都会完整显示。

(not attributed) 表示检查无法将该提交与本会话关联,仅此而已。它以前显示为 (another session),这断言了证据从未证实的事情,而且在保守解读被认为安全的唯一方向上是错误的:plans.py absorb --apply 会将计划移出存储,因此会话不会在存储路径上写入任何内容,而基于写入路径的测试会把它自己的移除提交判作他人提交。2026-09-07 已确认:某个会话吸收了三个计划,几分钟后分别提交了每次移除,而 filed 报告为 0 commit(s) this session, 20 from elsewhere,其自身的三次提交全都包含在这二十次之中。读取会话的命令及其写入内容解决了这个问题,却又从另一个方向过度认定,通过两种途径都发生了这种情况:归档者被记为吸收计划的所属仓库;被记为更正计划的另一个会话;而且,在运行本步骤自身的确认时,还被记为一条它只是读取过日志的提交。现在,只有凭据能证明某次提交属于本会话,即本会话自己的提交命令打印出的提交 ID。没有凭据的路径匹配会标记为 (authorship unestablished):通常表示所属仓库吸收或更正了你的归档,这意味着它已进入存储,值得说明。如果某条已标记的记录确实通过检查无法看到的途径属于你——例如提交命令的输出经 tail 管道处理而丢失凭据——请在报告中说明,而不要擅自认定其中任何一种情况;对于 0 this session,也应像对待任何其他看似合理但无人能证伪的数字一样保持警惕。 首次收集时,它会改为回答报告开头的几组问题,因此命令块不会在第二次运行时将其设为门槛。这些问题是“东西都去哪了”,而根据会话自身的记忆来拼凑答案,正是本步骤下一段已经提醒过的错误:命名文件前,先确认文件仍然存在。2026-09-07 首次收集时已确认有这个子命令——它列出了该会话写入的五个计划文件,正确地将已退役的那个标记为 MISSING,并报告本会话有 0 次存储提交,而 18 次无法归属;没有参与者能猜到这个比例,而它决定了是否可以安全地编辑共享存储中的计划。这次运行早于上面的删除修复,因此其中的 0 是下限,而不是计数;它所解决的论点并不取决于这个数字如何变化。重新推导它打印的每项未标记测量,并编辑文件,然后写下差异。修正存储中的计划属于本流程开头所列的写入范围;即使是通过 --for 为另一个仓库提交的计划也包括在内。标记为 MISSING 的行只表示文件已不在,原因决定了补救措施:本会话退役的计划无需处理;从存储中吸收进其所属仓库的计划,应在那里通过新提交修正,而不是编辑;其他情况则先查明它去了哪里,再让报告点名。标签以前写作 absorbed, or moved,第 8 步也据此制定了指令。2026-09-20 已确认:一次收集列出了四行 MISSING,其中三行是同一会话一小时前退役的计划;若遵循旧措辞,就会针对这些被刻意删除的计划提交三项修正。退役是会话完成计划后的预期结果,因此运行良好的会话比草率的会话更常遇到这种情况。因此,现在该行会指出删除提交能够确认的两种原因——若本会话自身的输出显示了提交,则写 retired by this session (<sha>);若提交说明表明已吸收,则写 absorbed (<sha>);其余情况保留为 cause not determined。此项修改于 2026-09-28 添加,此前有个会话退役了八个计划,却留下九行未确定记录需要重新推导。第三种原因来自会话自身运行的命令,而不是 git:它运行的 plans.py rename 会显示 renamed by this session to <new>;接着追查新名称的去向,因为旧名称通常从未提交——吸收时重命名已合并的计划是常见情况。若计划写在会话后来已移除的关联工作树中,则应在 main 上查找,路径为仓库中相同的 plans/ 路径,并注明它最初写入时所在的工作树路径;在 2026-09-28 之前,这类计划会完全从列表中消失,而一个通过工作树写入每项计划的后台会话,五项计划只统计到一项。

这里会打印两类数字,只有其中一类会随着会话持续运行而过时。 会话的测量值——遵循情况行、调用次数,以及任何由 --until 限定的内容——按构造就是一个前缀,因此后续边界变化时它也会变化,每次收集都要重新推导。外部世界的测量值——扫描的语料库、读取的规范、文件字节数——不会因本会话继续运行而改变;重新推导它意味着要完整重扫,以确认一个并未变化的数字。应说明每个数字属于哪一类,只有在其对象发生变化时才重新推导第二类。2026-09-22 已确认:一次收集的 filed 同时打印了两类数据,会话范围内的行在每个费率上都发生了变化,而旁边的语料库行——144 份 README 中的 46%——不可能变化。

已发布的判定看起来属于外部世界,实际却像会话范围内的数据,因为重新运行的是别人的扫描器,而不是你的会话。每次都要重新查询。同一次收集中,一项四天前提交的评级跨越了两个等级,而内容唯一的变化只是打印修正。 先从所有内容的去向说起,分成四组,因为“这些内容是否落到了某个持久位置,还是仍只存在于聊天中?”正是整份报告要回答的问题:

  • 写入代码 — 已提交的源代码、测试、配置修改。

  • 写入本仓库的计划 — 此处的 plans/*.md,按文件名列出。

  • 写入其他仓库的计划 — 通过 plans.py new <topic> --for <repo> 创建,并已提交到存储库,按文件名和目标仓库列出。只说它已提交。 不要复述其中的要求;所属仓库的会话会通过 plans.py absorb 获取全部内容。

    确认文件仍然存在后再列出它,并检查你的存储库提交是否已由其他人推送——对存储目录运行 ls,并运行 git -C $PLANS_HOME branch -r --contains <sha>。并行会话可能已将其吸收到目标仓库中;这是正常情况,说明工作已送达;应改为说明这一点。2026-08-30 确认:会话早期提交的一项计划已被吸收、合并进该仓库现有的一项计划,其在存储库一侧的删除也已在收割运行数小时前提交并推送。若报告仅依据会话自身的记忆编写,就会列出一个已不存在的路径,并称一项已发布的提交尚未推送——这两条看似寻常的状态信息,读起来都不像猜测。

  • 仅存在于本次对话中 — 所有未写入任何文件的内容。这一组正是清单的重点:它准确列出了用户必须现在作出决定,或带入下一次会话提示词的内容;窗口关闭后,这些内容就会消失。要让这一组保持简短,就把内容写下来,而不是删掉不写。

接下来按紧急程度从低到高排列,因为略读报告时,最后几行最容易留在读者脑中:

  • 已解决 — 已验证状态正常、无问题,无需采取行动。说明检查了什么,这样“没问题”就有据可依,而非主观印象。
  • 发现技能和指令误用 — 见步骤 2 中的路由筛选;写报告时,每一项都已经提交,因此本节列出发现、提交到的仓库和计划文件名。绝不列出待日后提交的事项。
  • 技能变更 — 步骤 6 对此技能所做的修改;若未发现问题,则用一行说明。
  • 等待决定 — 已记录,无需紧急处理。列出必须_一起_决定的计划。
  • 尚存风险 — 已知且已记录。对每项风险,说明可证伪条件:什么观察结果会证明推理有误。
  • 现在需要采取行动 — 尚未解决的状态,以及任何只能由用户决定的事项(推送、重新安装、破坏性清理)。放在最后,并写具体。
  • 以一行结论收尾:“可以精简”或“暂不可精简——需要先决定 X。”

结论并不代表运行结束——步骤 9 才是,而且每次都要执行。 报告是本次会话留下的内容;提示词是下一次会话的起点。收割若停在结论处,就只完成了耗时的那一半,却省下了轻松的那一半。

与其只报告,不如先处理成本低且没有歧义的问题——终止无人认领的循环,把遗漏的测量结果写入所属计划——然后报告已完成。把报告的最后一部分留给真正需要用户处理的事项。任何对外操作(推送)仍需先询问。

9. 下一次会话的提示词

每次收割都要输出,不论用户是否要求。 截至 2026-09-04,标题仍写着“如果有人要求”;那是从本步骤仅列出待省略内容时遗留下来的说法,而且它确实完全照字面执行:收割都在步骤 8 结束,用户于是问道:“为什么 session-harvest 没有把下一次会话的提示词作为最后一项生成?我以为我们已经加上了。” 这个功能早在 2026-09-01 就已实现,正是因为他们总是手动要求添加。 它永远不会是空的,因此无需设计静默情况。在一次收获充分的会话中,下面的减法会排除大多数候选内容,但优先顺序——先做什么,以及为什么选这个而不是那个——不是任何启动命令会打印的内容,而且总有下一件事要做。如果生成的提示词除了开场白什么都没有,那说明减法做得不好,而不是会话没有可传递的内容。

它是下一次会话的第一步,不是对本次会话的总结。 将它作为可直接粘贴的文本块打印在最末尾——绝不要写入文件。用户会在几分钟内将它粘贴到下一次会话中,这也是它唯一可以断言任何内容的原因;写入文件的提示词会被搁置,过段时间再读时仍然一成不变。

通过减法来构建它,而减法就是全部设计。 下一次会话自己的启动步骤已经会打印出提示词可能想要传递的大部分内容:

它已经会运行所以它已经知道
plans.py absorb为此仓库归档的每个计划,包括从其他地方转入的计划
plans.py list此处有哪些未完成事项、按状态分组的情况,以及待处理的退役事项
git status、git log origin/<branch>..HEAD工作树是否有未提交更改、是否有未推送的提交

运行这三条命令,只包含差异。 它们打印的任何内容都不该放进提示词——那只是噪音,会占用读者的注意力,却毫无益处。这就是判断“是否属于边际细节”的机械标准,也正因如此,提示词才会很短:在收获充分的会话中,大多数候选内容都过不了这一关。

留下的内容只有三类:

  • 优先顺序。 list 会说明有哪些未完成事项;机器上没有任何信息能说明先做什么,或为什么选这个而不是那个。通常只需一句话,而且往往是提示词中最有价值的一行。
  • 时效很短、很快就会失效的状态——仍在运行的后台进程、正在进行的 CI 运行、已编辑但尚未重新安装的技能,否则下一次会话会运行旧版本。这些状态一小时后就可能不再成立,而且任何启动命令都不会揭示它们。
  • 本次会话中做出的、尚未写入任何文件的决定。 这是一项自我检查:如果这项不为空,就说明第 2 步的路由失败了;正确的修复方式是把内容写下来,而不是放进提示词中。

每一项都必须先验证再行动,绝不能直接断言:先运行命令重新确认状态,再说明如果状态仍然成立该怎么做。“推送 103b0b6”是一个到早上可能就不再成立的断言;“git log origin/main..HEAD ——如果仍显示一个提交,就推送它”则不会出错。为文本块标注撰写时间,并注明几小时前写的内容都应重新确认,而不是直接相信。

最多五项,外加一行开场白。 开场白要写明唯一的下一步行动,以及说明细节的文件——好的提示词会告诉下一次会话打开一个文件,而不是给它一份简报。超过五项,它就变成了没有状态字段的计划;而这套约定正是为了避免这种形式。

提示词中绝不要包含以下内容,即使看起来很诱人:三条命令会打印的任何内容;已写入计划的任何内容(只写计划名称,绝不复述内容);做出决定的推理过程(根据第 2 步的路由,这属于计划内容);对本次会话做了什么的叙述(上面的报告已经说明了这些,下一次会话不需要)。 仅涉及此仓库。 针对其他仓库提交的工作已经有对应去处——它们存放在 store 中,而 plans.py absorb 会将其交给能够处理的会话;这是该会话发出的第一个调用。在这里重复记录会产生第二份副本,两份记录各自独立变得过时,还会向唯一无法处理此事的会话发送提醒。已于 2026-08-30 确认:一条提示词带有两个跨仓库事项,而这两项已经提交,也可以通过 absorb 获取;这属于重复记录,而非交接。对于此仓库中的工作列表计划,规则也一样——记录跨仓库工作已提交即可,不要记录其具体内容。

[PITFALL:唯一的例外,且范围很窄,可以用一项测试明确界定。 只有同时满足以下两点,其他仓库才值得在此记录:风险高或不可逆(例如重写历史记录、强制推送、破坏性清理、已发布的凭据),并且会改变此仓库下一次会话的操作。若某个仓库待重写历史记录,且此仓库的 store 会镜像该仓库的计划,则符合这一条件。其他仓库中未推送的提交、未完成的计划、失败的测试都不符合第二项——该仓库自己的下一次会话会接手这些事项,而当前会话无法处理。若确实符合条件,只写一行:说明风险,以及用于判断风险是否仍然存在的检查项。]

本次整理写入仓库的所有内容——plans/*.md 条目、AGENTS.md 增补内容、docs//contributing/ 页面、技能自身的源文件——都必须在提交前通过该仓库的质量门禁(inv quality.precommit 或仓库中的等效检查),与代码相同。Markdown 也不例外:dprint 会重新调整散文的换行格式,而在这些仓库中,跳过质量门禁的纯文档提交是反复导致 CI 失败的唯一原因(已于 2026-08-23 确认)。

自更新机制

解决摩擦点(步骤 6–7)意味着编辑技能的_源文件_,而不是当前会话面前的副本:

  • 正在运行的副本 — ~/.agents/skills/session-harvest/(或项目本地的 .agents/skills/session-harvest/)— 是安装时放在那里的普通文件副本。 手动编辑它会在下次安装时被悄悄覆盖,而且无论如何都不会到达其他项目或 机器。切勿直接编辑它。
  • 规范源是安装这些技能所用的仓库 — 对于这位作者, agent-skills;对于你,则是你自己的检出或分叉。 skills-state 会解析它(--checkout、$SESSION_HARVEST_CHECKOUT、脚本所在仓库的上级目录, 否则就是 plan-conveyor 的 projects_root 中包含此技能源文件的那个检出),并在有两个候选项时询问。 技能不会记录任何作者将仓库放在哪里。
  • 只有已经在该仓库中工作的会话才能编辑它。 从其他地方执行时,折返内容是一个归档条目,符合步骤 6 — 由 skills-state 打印的 --for <checkout> 命令,并提交到存储库中。不要为了写入而定位检出目录:在那里编辑相当于在另一个会话的工作树中提交内容,这正是全局规则明确禁止的做法。归档并非次优方案; absorb 会将其交给下一个在该仓库中工作的会话。如果机器上没有检出目录,也就没有归档条目 — 按步骤 6,将摩擦点写进报告。
  • 当你_确实在_该仓库中时,编辑 skills/session-harvest/SKILL.md(或 scripts/harvest.py,以及 tests/unit/test_harvest.py 中的测试):只做小幅增补。关于_为何_采用某种解决方式的理由应写入 references/rationale.md。
  • 运行该仓库的质量检查,然后提交 — 按步骤 7,无需询问,直接在本地完成。该仓库自己的 tests/unit/test_skill_layout.py 也属于这项检查,并强制执行实际限制(包括描述长度上限),因此要运行它,而不是只凭目测检查 frontmatter。然后告知用户 改动内容。
  • 编辑 scripts/ 不会影响本会话对已安装副本的调用,而且问题会在运行过程中发生,而不是事后。 每次调用 python3 ~/.agents/skills/<name>/scripts/<file> 都会 执行已安装的副本,因此提交脚本改动后,本会话读取的输出来自刚被替换的代码。已于 2026-08-30 确认:某个会话重命名了 absorb 的配对输出并提交,随后运行 absorb --apply,却得到了旧措辞 — 当时无害;如果改动的是行为而不是字符串,后果就不会如此。接下来要么调用检出目录中的副本,要么注明哪些结果早于编辑;不要从新源文件重新推导结果并假定它们一致。
  • 要明确说明,已提交的编辑尚未到达任何已安装副本,包括本会话和同一台机器上其他项目中的副本;部署该改动要遵循 skill-authoring-taudelta 的流程。然后,在报告机器运行的版本前先测量安装状态 — 在有并行会话的机器上,其他会话可能已经部署了它,因此安装状态是共享的。对比差异;这正是 skills-state 的用途。已于 2026-08-30 确认:某次 harvest 以“这个仍在运行旧副本”收尾,用户提出疑问后才发现,已安装副本其实早就包含了修复,而且是另一个会话在二十分钟前部署的 — 在报告中专门用于说明需要采取行动的部分写出一句听起来笃定、具体却错误的话。

完整理由

references/rationale.md 包含先前方案的调研(为何这不是 PreCompact hook、从 learning-loop-skill 借鉴了什么以及刻意舍弃了什么、为何即使在会话中途很容易想加入计划专属内容,也要将其排除在记忆之外),以及自更新机制背后的推理。

评论

支持 Markdown