## 无人值守自我迭代协议(ITERATION.md) > 本协议由自动化 Agent 执行,也可供人工参考。任何迭代必须遵守 `CODING.md`、`AGENTS.md`、`ARCHITECTURE.md` 三份规范,门禁 `npm run check` 不通过的一切产出作废。 ## 运行模式:连续迭代 + 心跳监护 调度器(每小时唤起)**不是**迭代器,只做两件事:健康检查 + 中断重启。真正的迭代是单次唤起内的**连续循环**——反复执行《每轮迭代流程》,直到 backlog 清空或时间预算耗尽,中间不停歇。 - **并发锁**:`.ua/loop.lock`(含时间戳,不入库)。唤起时若锁存在且心跳在 15 分钟内 → 已有实例在跑,直接退出;否则写锁开始循环。每轮开始前刷新锁内时间戳作心跳。 - **时间预算**:单次唤起最多连续迭代约 50 分钟,到点在完成当前轮后停止(防止与下次唤起重叠)。**预算判断必须基于客观时钟**:点火时用 `date +%s` 记录起点并写入 `.ua/loop.lock`,每轮开始时重新取 `date +%s` 与起点求差,差值 ≥ 2700 秒(45 分钟)即到点;**禁止凭感觉估算时长**——LLM 无内部时钟,自估时长系统性偏高(实测自报 40+ 分钟而实际约 30 分钟)。 - **熔断**:同一 backlog 条目连续 2 轮失败 → 停止循环,锁照常释放,等待下次唤起。 - **重启语义**:锁心跳超过 15 分钟视为循环中断(崩溃/超时),调度器删旧锁、重新点火。 - **调度器约束(踩过的坑,勿重复)**:本地调度器只支持 `HOURLY` 及以上粒度(`DAILY/WEEKLY/MONTHLY/YEARLY`),**`FREQ=MINUTELY` / `FREQ=SECONDLY` 不受支持**——写入后 `next_run_at` 会变成 `NULL`,首跑之后永久静默(实测 10/23–10/26 停摆三天)。因此「高频监护」不能靠加密 rrule 实现,只能靠**单次唤起内的连续循环**;心跳间隔 = 1 小时,循环预算 45–50 分钟,两者共同保证覆盖面。任何对自动化的改动(改 prompt、改 rrule、暂停恢复)都必须回查 `next_run_at` 非空,改 prompt 会把状态重置为 `PAUSED`,需再单独置 `ACTIVE`。 - **有效性自检**:每轮迭代首步除同步仓库外,检查 `.ua/loop.lock` 与 `automation_runtime_state.last_run_at`;若发现上次运行距今超过 2 小时(说明调度中断),在留痕 `summary` 里注明「调度中断后恢复」,便于周报统计停摆时长。 ## 多实例错位接力 单条自动化的占空比受平台语义限制:**`next_run_at` = 本次运行结束时刻 + `INTERVAL`,不是整点对齐**(实测 12:46:50 结束 → 下次 13:46:50)。`INTERVAL` 下限 1 小时,45 分钟预算下单条仅 `45/(45+60) ≈ 43%` 覆盖面,即「干 45 分、歇 60 分」。因此用**两条同构自动化错位 30 分钟**补齐空档: | 实例 | 锚点(错位) | 角色 | |---|---|---| | A `无人值守迭代循环` | 整点链(当前 13:46:50 → 逐轮顺延) | 主循环 | | B `…B(错位接力)` | A 之后 30 分钟(当前 14:16:50) | 补位循环 | - **接力靠 `missed` 队列自动完成**:到点时若另一实例仍持锁,该次被记为 `missed` 并保留 `missedScheduledAt`,待当前实例释放锁后**立即补跑**(实测间隔 9 秒)。无需人工干预,也不占用额外配置。 - **抢锁失败是正常行为**:看到 15 分钟内新鲜锁的实例直接回复「循环存活,未干预」退出——不计失败、不写留痕、不改 backlog。 - **同一把锁、同一份 backlog**:多实例共享 `.ua/loop.lock` 与 `qa/*` 分支流水线,任何时刻只有一个实例在写,冲突由锁而非分支解决。 - **调整排期的方法**:改 `validFrom`(`next = max(now, validFrom) + INTERVAL`),改完必须回查 `next_run_at` 非空且落在期望时刻;改 prompt 会把状态重置为 `PAUSED`,须再单独置 `ACTIVE`。 ## 分支流水线 ``` qa/probe ──门禁通过──▶ qa/stable ──人工审阅──▶ main(受保护,禁直接推送) ``` | 分支 | 用途 | 谁能写 | |---|---|---| | `main` | 稳定发布线,受 Gitea 分支保护 | 仅人工审阅后合并 | | `qa/stable` | 集成测试线,所有通过门禁的迭代落在这里 | 自动化(仅 fast-forward 合并) | | `qa/probe` | 试验分支,每轮迭代的工作现场,可被强制重置 | 自动化 | ## 每轮迭代流程(自动化 Agent 执行) 1. **同步**:`git fetch --all --prune`,切到 `qa/stable` 并 `git pull`;从其最新提交强制重建 `qa/probe` 并切换过去。 2. **选题**:读 `.ua/backlog.json`,先取 `status === "open"` 且 `domain === "visual"` 的条目(有多条取编号最小的一条);没有视觉欠账时再按数组顺序取第一条 `status === "open"` 的条目;若全部关闭,按《选题规则》自行补充一条新点子后继续。 3. **实现**:按 `CODING.md` 编码规范实现,保持低侵入,单文件 < 200 行,涉及组件需同步 manifest/docs/llms-full.txt。 4. **门禁**:跑 `npm run check`(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual → audit:contrast)。 - **通过**:提交到 `qa/probe`,提交信息按 `COMMITTING.md` 规范(`iter(<条目id>): <中文祈使句>`,元数据变更用 `chore(backlog): 记录 完成`;commit-msg 钩子自动校验),推送 `qa/probe`,fast-forward 合并到 `qa/stable` 并推送;将 backlog 条目 `status` 置为 `done` 并随迭代提交。 - **失败**:尝试修复一次;仍失败则放弃本轮——`qa/probe` 直接重置回 `qa/stable`,backlog 条目在 `notes` 里追加失败原因、`status` 保持 `open`,提交并推送该元数据变更(仅改 `.ua/backlog.json`,不碰代码)。 5. **留痕**:在 `.ua/iterations.log` 追加一行 JSON,**字段统一**:`{"ts":"本地ISO时间","id":"条目id","result":"done|aborted","commit":"短哈希","summary":"一句话"}`。硬性规范:`ts` 必须来自 `date` 命令实测(本地时间,带 +08:00 偏移,**禁止手写或用 UTC 换算**);时长结论必须来自两次时间查询之差,禁止使用「大约/接近」等估算词;并发实例格式不得自创变体。随本轮提交。`ua lint-iterations` 会把每行 `commit` 拿到 git 历史里回查:哈希必须真实存在、主题必须指向同一条目(`iter()`,早期无 scope 的 `iter: ` 也认),伪造或抄错哈希直接判违规。 6. **边界**:不改 `main` 分支保护、不删分支、不动 `.workbuddy/`、不重写历史、不 force push 到 `qa/stable` 与 `main`。 ## 选题规则(自主补点子时) - 只从以下方向选:组件补充(皮肤包缺的常规组件)、既有组件的可用性细节、文档站与 Agent 文档完善、构建/门禁工具链改进、演示页完善。 - 单轮改动范围可控:新增文件 + 少量既有文件修改,避免跨包大规模重构。 - 每条点子必须写清 `title / motivation / acceptance`(验收标准,可被 `npm run check` 与人工检查验证)。 ### 域轮换与视觉欠账优先 `backlog.json` 的条目带可选字段 `domain`:`new`(新增组件)/ `tune`(既有组件打磨)/ `visual`(视觉地基与观感)/ `tooling`(门禁、CLI、文档、配方)。选题按下面三条排队,保证视觉欠账会被持续消化,而不是永远排在「加新组件」后面: 1. **域轮换**:连续 3 轮迭代中,至少 1 轮的条目 `domain` 为 `visual`。若已连续 3 轮都不是 visual,下一轮必须从 open 的 visual 条目里选。 2. **视觉欠账优先**:只要存在 `status === "open"` 且 `domain === "visual"` 的条目,它的优先级高于 `domain === "new"` 的条目——先清视觉欠账,再加新组件(视觉地基不牢,新组件只是把漂移再复制一份)。 3. **新增组件的入库门槛**:`domain === "new"` 的条目,验收标准里必须写明「样式只消费 `--ua-*` 令牌,禁止新增硬编码阴影/时长/字号」,否则该条不得入库。视觉类改进的验收标准要能落到 `npm run audit:visual` 的计数上(涨了就是没做完),涉及颜色令牌的还要落到 `npm run audit:contrast`(对比度跌破阈值即没做完)。 ## 视觉回归抽查清单(人工,视觉类条目收尾时逐条过) 视觉改动只能靠人眼验收,凭记忆看一遍必然漏项。本节把六个高频组件的观感要点固化成可勾选项,视觉类条目(`domain === "visual"`)提交前照单过一遍,勾不上的就是没做完。 - **格式约定**:三条横线以下是清单本体,每个组件一个 `### <组件id>` 段,每条以 `- [ ] 【维度】` 开头;维度取值固定五类:`边框` / `阴影` / `过渡` / `焦点环` / `暗色`。 - **门禁口径**:`ua verify` 校验本节存在、六个组件齐全、每个组件 ≥3 条、合计 ≥18 条、五个维度全覆盖;条目改写或删漏直接报错。 - **与自动门禁的分工**:`npm run audit:visual` 管「字面量有没有回流成硬编码」,本节管「令牌用对了但观感对不对」——两者都过才算一轮视觉改动完成。 --- ### button - [ ] 【边框】次按钮与描边按钮的 1px 描边在页面底与卡片底上亮度一致,不出现某个按钮偏黑 - [ ] 【阴影】hover 只上浮 1–2px 且走 elevation 令牌,没有「影子 + 边框」双层叠加 - [ ] 【过渡】hover 上浮与按下回缩能看出先后,时长不超过 `--ua-duration-fast` - [ ] 【焦点环】键盘 Tab 到按钮时环完整贴边不被容器裁切,与描边按钮的描边不重影 - [ ] 【暗色】暗色下主按钮文字与底色对比仍够,描边按钮的边不消失 ### textField - [ ] 【边框】默认 / hover / 聚焦三态只变色不变粗细(1px 始终 1px) - [ ] 【焦点环】聚焦是光环外扩而非描边变粗;带前后缀时环包住整组而不只是输入框 - [ ] 【过渡】聚焦与失焦的边框色过渡在 `--ua-duration-fast` 内完成,无闪烁感 - [ ] 【暗色】暗色下占位文字(placeholder)与输入文字都可读,边框仍可见 ### select - [ ] 【边框】触发器与相邻 textField 同高同粗细,并排时基线不歪 - [ ] 【阴影】下拉弹层用 `--ua-shadow-2` 档,出现在滚动容器内时边缘不被裁成硬边 - [ ] 【过渡】弹层进出各一次(位移 + 淡入),关闭后不残留一帧 - [ ] 【焦点环】键盘 ↑↓ 唤出弹层时焦点环留在触发器上,不跳到 body ### modal - [ ] 【阴影】弹窗用 `--ua-shadow-4`,与遮罩形成明确层级,不糊成一片 - [ ] 【边框】页头 / 页脚分割线用 `--ua-border-subtle`,观感与卡片页脚一致 - [ ] 【过渡】弹入有轻微缩放位移,Esc 关闭后无残影、无布局跳动 - [ ] 【焦点环】打开后焦点落在弹窗内首个可聚焦元素,Tab 不逃出弹窗 - [ ] 【暗色】暗色下弹窗表面与遮罩可区分,标题与正文层级仍清楚 ### dataTable - [ ] 【边框】行分割线用 `--ua-border-subtle`,连续 20 行不显重,表头下沿不发黑 - [ ] 【阴影】粘性表头滚动透底无效(背景实色 + 底部 1px 细线贴合) - [ ] 【过渡】行 hover 与选中底色过渡柔和,选中行不抢表头与首列的视线 - [ ] 【暗色】暗色下斑马纹可辨但不突兀,选中行与 hover 行仍能区分 ### toast - [ ] 【阴影】通知用最高档投影,堆叠三条时前后层级清楚 - [ ] 【边框】通知卡片描边不抢语义色图标,警示色不溢出到边框 - [ ] 【过渡】进场 / 出场各一次,堆叠时其余条目平滑让位,不出现跳位 - [ ] 【暗色】四种语义色(info / success / warning / error)在暗底上图标与文字都不糊 ## 人工介入点 - 审阅 `qa/stable` 的提交,确认质量后合入 `main`(PR 或本地 merge 后推送)。 - 发现自动化产出质量下滑时:暂停自动化、在 backlog 中把质量要求写进条目、恢复运行。