Files
cmbuyer/docs/tasks/T-008.md

19 KiB
Raw Permalink Blame History

id, title, phase, deps, status, created, vikunja_task_id, context_ref, work_branch, needs_device, needs_human_review, write_paths
id title phase deps status created vikunja_task_id context_ref work_branch needs_device needs_human_review write_paths
T-008 接入 Vikunja 作为任务权威并建立单向导出 0
T-007
DONE 2026-08-03 15 b9e29ed task/t-008-vikunja-task-authority false true
docs/tasks/T-008.md
docs/tasks/README.md
docs/agent-context.json
docs/agent-context.schema.json
AGENTS.md
.gitignore
.mcp.json
vikunja.env.example
scripts/vikunja-mcp.sh
scripts/vikunja_export.py
scripts/validate_agent_context.py
docs/tasks/_template.md

问题 / 背景

多 agent 并行时任务文档会互相覆盖。已观测到的事实:T-005 与 T-006 同时为 DOING, write_paths 在 docs/06-tasks.md、docs/08-interaction-checklist.md、docs/routes.md、 docs/current-state.md 四处重叠。docs/tasks/README.md 的「领取规则」第 3 条已经写明 「与其他活跃任务路径重叠时不得并行」,但该规则只以自然语言存在,没有任何机器强制, 所以被绕过时无人察觉。

同时,任务状态(谁在等人工确认、哪台真机在排队)没有人类可用的视图,只能靠翻 frontmatter 拼凑。

本任务把两件事分开解决:

  • 协调状态搬到已部署的 Vikunja(http://ilaer.eicp.net:3456,v2.4.0),获得看板视图。
  • 契约与安全边界留在 git,并新增离线门禁强制执行路径不重叠。

不做这件事的后果:进入 T-001 / T-002 两端并行后,docs/api.md 与 docs/04-architecture.md 第四节会在无人察觉的情况下被并发写入,而这两份文档承载 三道价格闸门与提交订单四条件。

关联需求与交互

  • 功能:不适用(工程基础设施,不属于 MVP 功能面)
  • 用户故事:不适用
  • 交互:不适用(无产品界面;Vikunja 自带 Web 界面不属本项目交付物)
  • 架构 / API:不涉及 04-architecture.md 第四节任何安全边界,不新增跨端接口

方案

1. Vikunja 项目与状态映射

在 Vikunja 建立单个项目 cmbuyer,全部任务集中在此。

集中的理由是硬约束而非偏好:v2.4.0 上 /api/v1/tasks/all 返回 {"code":2004,"message":"Invalid model provided"},跨项目列举任务不可用;按项目列举走 /projects/{id}/views/{view_id}/tasks 正常。任务一旦分散到多个项目,agent 就无法一次 取全。

status 的权威在 git 的 frontmatter,不在 Vikunja。离线门禁 validate_agent_context.py 靠它判定「两个活跃任务不得写同一路径」,权威放到远端就等于让唯一能跑的校验命令依赖网络;status 变更本身也决定谁能碰哪些文件,本来就该产生 commit。

Kanban bucket 与 done 标志是人类视图,不是判定依据,两者不一致时以 git 为准。导出脚本只读比对并提示漂移,不反向写回。实测该 MCP 没有 bucket 改名工具,因此沿用 Vikunja 自动创建的英文名:

frontmatter status bucket
TODO To-Do
DOING Doing
DONE Done(该视图的 done_bucket_id)
BLOCKED Blocked(本任务新建)

标签固定两个:needs_device、needs_human_review。 依赖用 task_relations_add 的关系表达,不写进正文。

2. 权威划分(不可放宽)

内容 权威 载体
标题、标签、依赖关系 Vikunja task 字段 / label / relation
status git frontmatter(bucket / done 仅为人类视图)
问题 / 背景、方案、验收要点 Vikunja task description
执行记录 Vikunja task comments
write_paths git frontmatter
边界与安全边界条目 git 正文 ## 边界
context_ref、work_branch git frontmatter

status、write_paths 与安全边界留在 git 是本任务的核心约束。理由:AGENTS.md 第 2 条规定安全 边界只能收紧,而收紧与否靠 git diff 逐条复核。若这两项的权威在 Vikunja,agent 放宽一条 闸门时改的是远端字段,本地文件下次同步才变,diff 里只呈现为「导出内容更新」,看不出是谁 在什么理由下放宽了边界——审计链在此断裂。任何后续任务不得把这两项迁往 Vikunja。

3. 本地混合文件格式

docs/tasks/T-XXX.md 是投影与 git 原生内容的混合体,不是纯导出。自上而下依次是:

  1. frontmatter。其中 vikunja_task_id 是唯一连接键,创建后不再变更;write_paths、 context_ref、work_branch 是 git 原生,导出脚本永不写入。
  2. 一行 BEGIN 标记,形如 BEGIN VIKUNJA EXPORT id=<数字> synced=<时间戳> sha256=<64位十六进制>, 由 HTML 注释包裹。
  3. 区块正文:问题 / 背景、关联需求与交互、方案、验收要点、执行记录。Vikunja 权威, 脚本整段覆盖。
  4. 一行 END 标记,同样由 HTML 注释包裹。
  5. ## 边界。git 权威,脚本读取但绝不写入。

sha256 覆盖两行标记之间的正文,供门禁发现手工改动。

本节刻意不贴出字面标记:任务文件本身要经过导出往返,正文里出现字面标记会让脚本 把示例误判为真区块。已实测踩中过一次,围栏在往返后丢失,示例里的二级标题变成真标题 并导致内容截断。

4. 导出脚本 scripts/vikunja_export.py

  • 只读 Vikunja,只写标记区块内。 不实现任何反向写入路径,且须有测试证明不可达。
  • 仅用标准库。 T-002 未完成前仓库没有 Python 虚拟环境,本脚本必须与 validate_agent_context.py 一样用系统 python3 直接跑通。
  • HTML → Markdown 转换:Vikunja 的 description 存的是 HTML(实测读到 <p><b>触发原因</b>:…<code>SavePerson</code>),不是 Markdown。用 html.parser 实现确定性转换。
  • 遇到未支持标签时报错退出,不得静默丢弃。 静默丢弃会让安全相关文字无声消失, 与本项目 fail closed 的一贯要求冲突。
  • 幂等:内容未变时不重写文件,避免产生噪音 diff。
  • 行尾统一 LF,禁止写出 CRLF。

5. MCP 接入与凭据

  • scripts/vikunja-mcp.sh:MCP 启动包装,提交进仓库。职责是读凭据、剥掉 apiurl 末尾的 /api/v1(@0xk3vin/vikunja-mcp 内部会自己拼接,不剥会请求 /api/v1/api/v1/... 得到 404)、导出 VIKUNJA_URL 与 VIKUNJA_API_TOKEN、 exec 到版本写死的 @0xk3vin/vikunja-mcp@1.1.1。优先用全局安装的二进制, 缺失时回退 npx。
  • 版本写死的理由:上游改工具名或行为会直接改变 agent 行为,与 CLAUDE.md 「交付产物新旧以 SHA-256 判断」同源,不接受静默升级。
  • 凭据文件 vikunja.env 放仓库根目录,比照既有 gitea.env 模式加入 .gitignore; 同时提交 vikunja.env.example。脚本按 $VIKUNJA_ENV_FILE → 仓库根 → 家目录顺序 查找,便于其他 agent 换路径。
  • .mcp.json 的 command 改为仓库内相对路径,使配置不绑定某台机器的家目录。
  • token 权限:采用全量 token(2026-08-03 决定)。曾试过只覆盖 tasks / comments / labels 的窄 token,实测 POST /projects/{p}/views/{v}/buckets/{b}/tasks 返回 401——bucket 操作需要 project 级权限,导致 TODO / DOING / BLOCKED 三者的状态流转 agent 无法驱动,与方案第 1 节「状态用 Kanban bucket 表达」直接冲突。权衡后取全量 token。代价必须记录:任何配置了此 MCP 的 agent 都持有对该 Vikunja 实例的完整读写权,包含 projects_delete;且 apiurl 是 http:// 走公网 DDNS,token 明文过网。凭据只允许存放在 vikunja.env,不得内联进任何 agent 配置文件。

6. 门禁 scripts/validate_agent_context.py

新增四项检查,全部必须离线可跑——本脚本是目前唯一可运行的验证命令,一旦依赖 Vikunja 网络,服务不可达时连校验都做不了:

  1. 所有 status: DOING 的任务,write_paths 两两不得相交。
  2. write_paths 中的通配条目不得匹配 docs/agent-context.json 中 shared_documents 登记的任何条目——共享文档必须逐条显式列出。共享文档本身允许出现在 write_paths (T-007 改 AGENTS.md 即正当交付),排他性由检查 1 保证;本检查只堵住用通配悄悄 圈走共享文档。
  3. 存在导出标记区块时,重算 sha256 并与标记比对,不一致即失败(说明有人手改了投影)。
  4. 任务文件存在导出区块时,必须有 vikunja_task_id 且与区块标记里的 id 一致。 尚未迁移(无导出区块)的任务文件豁免,避免为了让检查转绿而去改属于其他活跃任务的文件。

检查器必须感知围栏代码块,跳过其中的同名文本,否则会把文档里的示例当成真标记。

检查 1 落地后会立刻报出存量违规:T-005 与 T-006 同为 DOING,在 docs/06-tasks.md、docs/08-interaction-checklist.md、docs/routes.md、 docs/current-state.md 四处重叠。这是本检查的第一个真实发现,不得通过放宽检查、加白 名单或延后启用来绕过。处置只有两条合法路径,由 T-005 / T-006 的所有者选择:

  • 人工确认 6 个原型后将两任务改为 DONE(DONE 不参与相交判定);或
  • 收窄两者的 write_paths,把共享文档移出,改为产出建议由任务所有者合入。

存量违规已于 2026-08-03 消解:T-005 与 T-006 经人工确认 6 个原型后转 DONE,不再参与相交判定,门禁转绿。T-008 自身因 needs_human_review: true,按 docs/tasks/README.md 硬规则保持 DOING(不是 BLOCKED):工作可继续推进,只是未获人工确认前不得标 DONE。

docs/agent-context.json 相应新增 shared_documents 与 tracker 两个顶层键。该 schema 的 additionalProperties 为 false,必须同步修改 agent-context.schema.json 并将 schema_version 由 1 升为 2。

degraded_mode 三个字段(continue_claimed_task、claim_new_task、 write_remote_state)此前无远端状态、形同虚设,本任务后正式生效:Vikunja 不可达时 继续手头任务、不领新任务、不写远端。

验收要点

  • Vikunja 中存在 cmbuyer 项目,四个 bucket 与上表一致,needs_device 与 needs_human_review 两个标签已建。
  • T-005、T-006、T-007 三个既有任务已录入 Vikunja(远端操作,不改动 git 文件), bucket 与各自 status 一致。
  • 仅 T-008 自身回填 vikunja_task_id 与导出区块。T-005、T-006、T-007 的 frontmatter 回填推迟到各自任务关闭时由其所有者完成——那三个文件不在本任务 write_paths 内,越界写入会违反本任务正在建立的规则。
  • python scripts/validate_agent_context.py 通过;断网状态下同样通过(验证离线性)。 该项以存量 T-005 与 T-006 重叠被消解为前提,消解前本任务保持 BLOCKED。
  • 断网时执行 scripts/vikunja_export.py 以非零码退出并给出明确原因,不产生半截文件。
  • 制造违规样本各跑一次,四项检查分别报错且信息指明具体文件与冲突路径:两个 DOING 任务写同一路径;write_paths 用通配圈走共享文档;手改导出区块一个字符;有导出区块 但 vikunja_task_id 与区块 id 不一致。
  • 反向写入不可达:存在测试证明 vikunja_export.py 的代码路径不引用任何 Vikunja 写接口。
  • HTML 转换:含 <b>、<code>、<ul>、<a>、<table> 的样例转换正确;含未支持标签 的样例以非零码退出。
  • 导出产物行尾为 LF;连续执行两次导出,第二次无文件变更(幂等)。
  • 重启 Claude Code 后 claude mcp list 显示 vikunja 为 Connected,.mcp.json 使用 相对路径且在另一家目录下同样可用。
  • git status 确认 vikunja.env 未被跟踪,vikunja.env.example 已提交。
  • 看板视图可用,卡片位置与 status 一致:Doing 放 #15,Done 放 #12 / #13 / #14。
  • 各家 agent 的 MCP 配置片段写入 AGENTS.md,统一指向 scripts/vikunja-mcp.sh,配置文件内不出现凭据。

执行记录

2026-08-03T08:53:59Z · ila

2026-08-03 · 第一轮实现(分支 task/t-008-vikunja-task-authority,基线 b9e29ed)

已完成

  • Vikunja 建 cmbuyer 项目(id=5)。bucket:To-Do 19 / Doing 20 / Done 21 / Blocked 22(新建)。标签 needs_device 3、needs_human_review 4。
  • T-005 #12、T-006 #13、T-007 #14、T-008 #15 已录入,bucket 与各自 status 一致。三个既有任务未改动 git 文件。
  • scripts/vikunja-mcp.sh 从家目录迁入仓库并提交;凭据 vikunja.env 留仓库根并 gitignore,比照既有 gitea.env 模式;新增 vikunja.env.example。
  • .mcp.json 改为相对路径 ./scripts/vikunja-mcp.sh,不再绑定家目录。
  • agent-context.json 新增 shared_documents(14 项)与 tracker,schema_version 升到 2,schema 同步。
  • validate_agent_context.py 新增四项离线检查,并校验 tracker.git_native_fields 必须含 write_paths 与 boundaries_section。
  • vikunja_export.py:stdlib-only,HTML→Markdown,只发 GET,--selftest 断言非 GET 方法被拒。

验证结果

  • 四项检查各造违规样本:全部命中;两个反例(共享文档显式列出、id 一致)正确放行。
  • 导出幂等:连跑两次,第二次 0 变更。行尾 LF。
  • 断网导出:exit=1,文件 sha256 不变,未产生半截文件。
  • --selftest 通过(11 项转换用例 + 未支持标签报错 + 四种写方法被拒 + 幂等 + 区块外内容未被破坏)。
  • 真实 token 未出现在任何将提交的文件中。

踩坑与修正

  • v2.4.0 上 bucket 移动路由不是 /views/{v}/tasks/{t}/position(404),正确的是 /views/{v}/buckets/{b}/tasks。
  • 自我引用:任务正文原本用代码块举例说明导出标记格式,往返一次后围栏丢失,示例里的二级标题变成真标题,导致重传时截断、丢了方案 4~6 节与验收要点。已重建内容,并把方案第 3 节改为不贴字面标记;门禁与导出脚本均已加围栏感知。
  • 草稿三处与自身规则冲突,已修正:检查 2 原本禁止共享文档进 write_paths(会挡住正当交付);检查 4 原本要求所有任务都有 vikunja_task_id(会逼迫越界改他人文件);验收原本要求回填 T-005/006/007 的 frontmatter(同样越界)。

当前 blocker

  • 检查 1 报出存量违规:T-005 与 T-006 同为 DOING,在 06-tasks.md、08-interaction-checklist.md、routes.md、current-state.md 四处重叠。按本任务规定不得绕过,需两者所有者收窄 write_paths 或人工确认原型后转 DONE。
  • 人工待办:签发只覆盖 tasks/comments/labels 的窄权限 token 替换当前全量 token;重启后确认 claude mcp list 显示 Connected。
  • needs_human_review: true,以上两项完成前保持 DOING,不得标 DONE。

2026-08-03T08:59:43Z · ila

2026-08-03 · 窄权限 token 切换与复核

窄 token 实测权限(旧全量 token 已替换):

  • 可读写:GET/POST /tasks/{id}、GET/PUT/DELETE /tasks/{id}/comments、GET /labels、GET /projects/{id}/tasks。
  • 已被拒(401):GET /projects/{id}、GET /projects/{id}/views、DELETE /projects/{id}。收窄生效。
  • 影响:MCP 的 projects_list / projects_get 不可用;tasks_list 正常(自动回退到 /projects/{id}/tasks)。vikunja_export.py 只用 /tasks/{id} 与 /tasks/{id}/comments,不受影响。
  • PUT /tasks/{id}/labels 返回 400(重复添加已有标签),非权限问题。

新发现的 API 陷阱:Vikunja 的 POST /tasks/{id} 是整体替换,不是部分更新。只发 {"id":15} 会把 description 清空;只发 {id, description} 会把 done 重置。本轮因此误清过 #15 的 description(已用源文件恢复,本地投影未受损,sha256 校验反而验证了这一点),并把 #14 的 done 重置为 false(已修回 true)。今后任何字段更新必须携带完整对象。

当前状态:#12 / #13 / #15 done=false 带 needs_human_review 标签;#14 done=true。四个任务 description 完整。

2026-08-03T09:05:50Z · ila

2026-08-03 · 重启验证与存量违规消解

已完成

  • 重启后 .mcp.json 的相对路径 ./scripts/vikunja-mcp.sh 生效,会话内 ping 正常。(claude mcp list 显示 Pending 是那条独立 CLI 进程不共享会话批准状态,非连接问题。)
  • 人工确认 6 个原型通过,T-005 与 T-006 转 DONE,并回填 vikunja_task_id 12 / 13。两者内容已冻结,不迁入导出区块——转换只增加往返风险无收益,门禁对无区块文件豁免检查 4。
  • 门禁转绿:validate_agent_context.py exit=0。检查 1 报出的存量违规按规定路径消解,未放宽任何检查。
  • T-008 状态口径统一为 DOING(needs_human_review 硬规则优先于草稿里的 BLOCKED 表述),description 已更新并重新导出。

新发现:窄 token 无法移动 kanban bucket

POST /projects/{p}/views/{v}/buckets/{b}/tasks 返回 401——bucket 操作需要 project 级权限,而窄 token 只有 tasks / comments / labels。这与方案第 1 节「状态用 Kanban bucket 表达」冲突:

  • DONE 不受影响:置 done=true 后 Vikunja 自动归入 done_bucket_id。
  • TODO / DOING / BLOCKED 三者的区分,agent 目前无法驱动。首次播种时用的是旧全量 token,所以现有卡片位置正确,但后续状态流转会停滞。

三条出路待决策:用 label 表达 status(agent 可写 labels,最稳,但看板泳道失去意义);给 token 加开 project 读与 bucket 写(削弱收窄);或接受卡片位置由人维护、agent 只维护 done。未决策前不修改方案第 1 节,避免边写边改造成文档与实现不一致。

边界

  • 不修改 docs/api.md、docs/04-architecture.md、docs/02-requirements.md—— 本任务不触及任何跨端契约与安全边界。
  • 不修改 docs/current-state.md 与 docs/06-tasks.md。这两份文档正被 DOING 状态的 T-005 / T-006 占用,按本任务自己建立的规则不得并行写入。相关快照与路线图更新推迟到 T-005 / T-006 关闭后另起任务补录。
  • 不迁移 write_paths、## 边界 与安全边界条目到 Vikunja,任何后续任务同样不得迁移。
  • 不实现反向同步(git → Vikunja)。双向同步会重新引入两个权威。
  • 不实现 webhook、自动定时同步、CI 集成。导出由人或任务显式触发。
  • 不改变现有四条不可协商规则、三道价格闸门、提交订单四条件的任何表述。
  • 不引入任何第三方 Python 依赖。
  • 不为 codex 或其他 agent 编写配置。其他 agent 通过读取已导出的本地任务文件即可获得 完整上下文,无需接入 MCP;确需写入时各自配置,不属本任务范围。