Files

11 KiB
Raw Permalink Blame History

任务书 06:workspace(代码版本)

写 workspace 模块:用 git 存每一份代码版本,取出到目录,比较两个版本改了什么。顺带给 tasks 的 task.json 加一项"不存的东西"。

本任务书已由用户和监督者在 2026-10-02 一起确定。下面说的"监督者",是在参考项目目录里和用户讨论设计的另一个会话。用不用 git、怎么用,是先派一个全新上下文的子 Agent 独立分析、实测之后才定的,下面"实测过"的结论都来自那次分析,监督者复现过其中第 3 条。

开工前先读

  • CLAUDE.md:全部,尤其是"罕见情况怎么办"(什么算边界、哪些不算)、"不设默认值"、"测试"里"不为了方便测试而给产品代码加参数"。

  • docs/结构.md:

    • 第二部分:模块表里 workspace 一行;
    • 第三部分:账本记录第 6 种"冻结代码";
    • 第六部分:只有 runner 和 workspace 能起子进程;
    • 第八部分:tasks 一节。
  • docs/方法.md:

    • 4.3 ⑨:实现与核验、因子位置表、没有归属的改动、不许改的文件;
    • 4.3 里消融格的实现:从完整候选出发关掉因子;
    • 第九部分:冻结的东西不可覆盖,恢复时只核对。
  • vocab 里的 CodeLocation(src/vocab/submissions.ts):代码位置写成"文件"、"文件:行号"或"文件:起-止"。

  • WORK_LOG.md。

  • 参考项目(只读,不要改):

    • /home/iomgaa/Projects/RivalBits/src/board/code-store.ts;
    • /home/iomgaa/Projects/RivalBits/src/rounds/code-diff.ts。

    只为了解它们原来要解决什么问题,第六部分说明为什么都不搬。

一、这个模块做什么

用一个例子走一遍:

  1. 研究开始:把任务目录里的 baseline/ 存成第一个版本,得到一个版本号。
  2. 实验员实现候选 c1:把"本批基线"那个版本取出到一个空目录,实验员在里面改。
  3. 实验员交了:把这个目录现在的内容存成一个版本,得到 c1 的版本号。flow 把它记进账本。
  4. 核验员、研究者要看改了什么:比较本批基线和 c1 两个版本。
  5. 正式运行:把 c1 取出到一个目录,交给 tasks 准备运行目录。
  6. 采纳:c1 成为新基线。git 里什么都不做,账本记一句"现在的基线是哪个版本号"。
  7. 消融格:从 c1 取出,实验员关掉一个因子,再存成一个版本。

"上一版是谁"由账本记,git 里不记。

二、怎么用 git

一个裸仓库,版本号就是目录内容的编号。

  • 一份研究一个裸仓库(git init --bare),位置由调用方给。
  • 版本号用 git 的目录树编号(tree id),不造提交。同样的内容永远得到同一个编号,所以:
    • 中断恢复时重新存一次,得到的号和账本里已经记的一样;
    • 存过的版本没法被覆盖,因为内容变了编号就变。
  • 每存一个版本,挂一个以版本号命名的引用,例如 refs/versions/<编号>,防止 git 清理没有引用指着的对象。实测过:没挂引用的对象,git gc --prune=now 之后就没了。

存版本不用 git add,由我们自己遍历目录、列出文件清单:

  • 每个文件用 git hash-object -w --no-filters 存,再用临时的索引文件(GIT_INDEX_FILE)加 git update-index --index-info 和 git write-tree 得到目录树编号。
  • 为什么不用 git add:实测过,目录里有 .gitignore 时,被它列中的文件 git add 不存,退出码是 0。子目录里有另一个 git 仓库时,里面的文件也会丢。实验员写个 .gitignore 是模型干活时顺手就会做的事,而漏存以后,正式运行跑的代码和实验员交的就不是同一份,谁也发现不了。
  • 记下可执行位:普通文件 100644,可执行文件 100755。

git 不读用户的配置:

  • 我们启动的每个 git 子进程都设 GIT_CONFIG_NOSYSTEM=1 和 GIT_CONFIG_GLOBAL=/dev/null。用户本机的全局配置里有换行符转换、比较算法、lfs 过滤器这些设置,会悄悄改掉存进去的内容或比较输出的格式。这件事没人操作也会发生,所以要防。
  • 仓库位置(GIT_DIR)只写进我们自己启动的子进程的环境变量,不写进 process.env。插件进程的环境会被实验员的 shell 继承,模型顺手敲一句 git checkout,就会动到版本仓库。

取出用 git archive(或者效果一样的命令):写进一个空目录,可执行位要还原。取出的目录里没有 .git,实验员看不到别的版本。

三、对外提供什么

经 src/workspace/index.ts 导出,函数名由你定。仓库位置作为参数传入,不设默认值。

  1. 建仓库:在给定位置建一个空的裸仓库。

  2. 存版本:给一个目录和一份"不存的名字"清单,交回版本号,或者交回"存不了 + 原因"。

    • 遍历目录:名字在清单里的文件或目录,在任何一层都跳过。
    • 遇到符号链接、设备文件这类既不是普通文件也不是目录的东西,交回"存不了"并写明是哪个路径。这个目录是实验员交上来的,属于系统边界(CLAUDE.md"罕见情况怎么办"),flow 会把它当作提交不合格退回给实验员。不要替它猜怎么存。
    • 基线也用这个函数存。
  3. 取出:把一个版本的文件写进一个空目录。

  4. 比较两个版本:给"从哪个版本"和"到哪个版本",交回:

    • 每个改动过的文件:路径,新增、删除还是修改,是不是只改了可执行位,是不是二进制;
    • 每个文件里变了哪几块,新旧两边的行号范围都给。正向核验对照的是新版本的行号(位置表写的是候选代码的行号);消融时位置表写的是完整候选的行号,也就是比较里"从"那一边。只删不加的块在新版本里没有行,用 git diff -U0 那种"第几行之后"的写法表示,在类型里写清楚;
    • 给模型看的差异文本:带上下文的 git diff 原样输出,不加颜色。

    列文件用 git diff-tree -r 这类给程序读的命令,它的输出格式 git 保证不变。二进制文件不拆块,当作整个文件改了。

不归 workspace 管的:

  • 哪些改动没归到任何因子或保障:由拿得到位置表的模块,用这里给的行号范围现算;
  • 哪些改动属于不许改的文件:用这里的文件清单,交给 tasks 现成的函数判断;
  • 给研究者看的只读目录:以后要用时,调用方用"取出"。

四、顺带改 tasks

  • task.json 加一项"不存的名字",字段名你定,必填。填圆写:
    • __pycache__:实验员跑 Python 时会自动生成;
    • .git:模型可能在目录里 git init;
    • solution.json、metrics.json:运行输出,不是代码。
  • tasks 读任务时一并交回这份清单,flow 存版本时传给 workspace。workspace 不引用 tasks。
  • protectedFiles 里的 solution.json 删掉:它现在在"不存"清单里,harness 开跑前也会删掉旧的。
  • 相应的测试和 docs/结构.md 第八部分 tasks 一节跟着改。

五、测试

workspace 的测试用真实 git,测试仓库建在测试自己的临时目录里。

  • 同内容同号:同一个目录存两次;把目录复制一份、改了文件时间再存,都得到同一个版本号。
  • 取出再存回来,号不变:内容、可执行位都还原。
  • .gitignore 不起作用:目录里有 .gitignore 写了 *.json,cfg.json 照样存进去。
  • 不读用户配置:测试里给 git 一份会改行为的全局配置(例如 core.autocrlf=true,或者改比较前缀的 diff.noprefix=true),结果不受影响。这份配置用测试自己的环境变量(例如 HOME 指向临时目录)造出来,不给产品代码加参数。
  • "不存"清单:清单里的名字在顶层和子目录里都跳过。
  • 存不了:目录里有符号链接时,交回"存不了"和那个路径。
  • 引用防清理:存完以后 git gc --prune=now,版本照样能取出。
  • 比较:
    • 新增、删除、修改、只改可执行位的文件各一个,以及一个二进制文件;
    • 行号:手写两个小文件,算好预期的新旧行号范围再比,至少包括改一行、加几行、只删不加三种块;
    • 预期值手算,不从实现里推。
  • tasks:task.json 少了新字段时报错;读任务交回这份清单。

workspace 关系到"冻结的东西不可覆盖"和中断恢复(CLAUDE.md"工作方式"),写完派一个全新上下文的子 Agent 审阅一轮。告诉审阅者:

  • 每条意见先回答"不写会怎样";
  • 只防三类:没人操作也会发生的、模型正常工作时就会做的、出错后发现不了的;
  • 模块之间传错参数、有人用 shell 故意破坏,都只列出不改;
  • 下面这些分析时已经判断过不用防,除非有新证据:
    • 并发存版本:每次用自己的临时索引,控制进程只有一个;
    • 换行符和大文件:--no-filters 原样存;
    • 嵌套仓库:清单是自己列的,不会出现。

审阅意见逐条判断,采纳或不采纳都在报告里写理由。

六、参考项目的保留与删除

处理 内容
删除 code-store.ts 整个文件:复制、逐个重算哈希、改只读、强制写盘、目录已存在时逐字节比对。git 的内容编号本身就保证了这些
删除 code-diff.ts 整个文件:自写的逐行比较算法,改用 git。它的理由是"换台机器结果也一样",但一份研究只在一台机器上算
删除 candidateContentHash 里的可执行位处理:git 的目录树本来就记可执行位
借鉴 无

七、规模与文档

  • 预期 200–300 行,不含测试。超过 300 行就停下来检查。
  • 完成后更新:
    • docs/结构.md 第八部分:补 workspace 对外的函数和类型,每项一句话;tasks 一节按第四部分改;
    • WORK_LOG.md:在"已完成"补一条。
  • 不新建 md 文件。

八、验收与报告

做完后本地提交,然后在回复里交一份报告,监督者会对着实际文件逐项核对:

  1. 提交号,新增和修改的文件列表,每个文件的行数,workspace 的总行数。
  2. npm run check 和 npm test 的完整输出。
  3. 比较两个版本的实际输出样例:一份结构化结果,一份差异文本。
  4. 审阅意见逐条:内容、采纳或不采纳、理由。
  5. 你和这份任务书、结构.md、方法.md 不一致的地方,以及为什么。
  6. 规则不清楚的地方:读 CLAUDE.md、结构.md、方法.md 时,哪里让你犹豫、哪里两份文件说法不一致、哪里你是靠猜的。没有就写"没有"。