🦄 Sync 2026-08-26 20:42:53

This commit is contained in:
github-actions[bot] committed 2026-08-26 20:42:53 +08:00
1 parent 53bb25bf38
commit e7a277e681
487 files changed
+55081 -19590

No files matched your search

+1 -1
View File
@@ -119,7 +119,7 @@ All rules use `@apply` with Tailwind utilities and CSS Nesting — no raw CSS pr
### On-Demand Third-Party Patches
> Page-scoped JS patches must expose `window.aurora.patches[stem] = { mount, unmount }` (and mount themselves once at eval) so the client-side router can drive them across same-document navigations — see `router.md`.
> Page-scoped JS patches must expose `window.luciPatches[stem] = { mount, unmount }` (and mount themselves once at eval) so the client-side router can drive them across same-document navigations — see `router.md`.
Some third-party LuCI apps ship markup that doesn't adapt to the theme and needs a narrow compatibility override. Instead of bundling every such patch into `main.css` (which would ship them to **every** page), each patch is a standalone CSS file loaded **only on the page it targets**.
@@ -119,7 +119,7 @@ Vite 开发服务器用中间件重写本地请求,使 CSS/JS 资源由你的
### 按需第三方补丁
> 页面级 JS 补丁必须暴露 `window.aurora.patches[stem] = { mount, unmount }`(并在求值时自行挂载一次),这样客户端路由才能在同文档导航之间驱动它们 —— 见 `router_zh.md`。
> 页面级 JS 补丁必须暴露 `window.luciPatches[stem] = { mount, unmount }`(并在求值时自行挂载一次),这样客户端路由才能在同文档导航之间驱动它们 —— 见 `router_zh.md`。
有些第三方 LuCI 应用发布的标记结构无法适配主题,需要一处狭窄的兼容覆盖。与其把每一个这样的补丁都打进 `main.css`(那会把它们发到**每一个**页面),不如让每个补丁成为一个独立 CSS 文件,**只在它所针对的页面上加载**。
+13 -7
View File
@@ -2,6 +2,12 @@
# The client-side router
> The router now ships from `@eamonxg/luci-theme-devkit` (`runtime/router.js`,
> page-scoped patches in `runtime/patches.js`), built into
> `resources/router-aurora.js` by its Vite plugin; this theme keeps only the
> markers, the `luci-navigate` listener in `menu-aurora.js` and the CSS. The
> design below is unchanged; the devkit copy is the maintained one.
How the theme turns a menu click into an in-document view swap instead of a
full page load, where it deliberately does not, and the invariants a router
inside LuCI has to keep. Source: `.dev/src/resource/router-aurora.js`,
@@ -430,10 +436,10 @@ handler in order:
6. **Patches.** `header.ut` emits the installed on-demand patch stems as
`body[data-patches]`; the router applies the same segment-prefix rule the
template applies at render time: matching `patches/<stem>.css` links are
ensured (`<link data-aurora-patch>`, enabled for the page on screen,
ensured (`<link data-luci-patch>`, enabled for the page on screen,
`disabled` — not removed — for the rest, so a return costs nothing);
matching `patches/<stem>.js` files are loaded once and their
`window.aurora.patches[stem]` `{ mount, unmount }` pair is driven per
`window.luciPatches[stem]` `{ mount, unmount }` pair is driven per
visit (the list of stems to mount belongs to the navigation that computed
it, so a superseded one mounts nothing later); URLs the router adds carry
the same `?v=PKG_VERSION` luci.mk stamps on the template's own links,
@@ -443,7 +449,7 @@ handler in order:
the current page still wants that stem and unmounts it otherwise (a
same-stem page reached meanwhile keeps it mounted).
A menu.d node's own `css` (`header.ut` links `<resource>/<node.css>` for
the dispatched node, marked `data-aurora-node-css`) is kept the same
the dispatched node, marked `data-luci-node-css`) is kept the same
way: one `<link>` per stylesheet, enabled for the page whose resolved leaf
declares it, `disabled` for every other page, never removed. Both
attributes are exempt from the poison gate.
@@ -502,13 +508,13 @@ handler in order:
where one suffices.
8. **Focus and announcement.** `#maincontent` (`tabindex=-1`) with
`preventScroll`; the new `document.title` is written into
`#aurora-nav-status` (`role=status`, `aria-live=polite`), since a
`#luci-nav-status` (`role=status`, `aria-live=polite`), since a
same-document swap fires no load a screen reader would announce. The
landmark carries `outline-none`: iOS WebKit (Safari and Chrome for iOS
alike) paints its focus ring for programmatic focus, and the ring's top
edge just under the sticky header was reported as "a progress bar that
never goes away" — it was never the bar.
9. **Progress.** A navigation that outlives 150 ms gets `#aurora-nav-progress`
9. **Progress.** A navigation that outlives 150 ms gets `#luci-nav-progress`
inserted — Turbo Drive's bar, in shape: a hairline at the top whose
`width` is driven inline and **trickles** in ever-smaller steps
(`+ (100 - w) / 30` every 300 ms) until commit, so a slow render keeps
@@ -562,8 +568,8 @@ tall). Hence a gate, not a sweep: before intercepting, any sheet outside `#view`
that is not one of the theme's own marks the document **poisoned** and the
navigation is a full load — the fresh document carries no view CSS, so the
router resumes immediately. "Own" means *marked*: header.ut stamps everything
it renders (`data-aurora-shell` on `main.css`, the font, custom and token
`<style>`s; `data-aurora-patch` on patches; `data-aurora-node-css` on the
it renders (`data-luci-shell` on `main.css`, the font, custom and token
`<style>`s; `data-luci-patch` on patches; `data-luci-node-css` on the
menu.d node css). The boot snapshot the gate compares against is filtered by
those markers, so a sheet the boot page's own modules inserted before the
router loaded still counts as foreign instead of being grandfathered in for
+5 -5
View File
@@ -164,15 +164,15 @@ Speculation Rules 预取够不到这块收益:该 API 只在安全上下文可
3. **环境。** `L.env.requestpath/dispatchpath/pathinfo/nodespec`、`body[data-page]`、`document.title`。alias 在服务端会被重新分发,所以 `requestpath` 和 `data-page` 承载的是 alias 的目标,而 `pathinfo` 保留请求时的 URL;`firstchild` 则两者都保留请求路径。标题后缀(` - 主机名`)从初始文档里读出,因此它与模板输出的内容一致。`nodespec` 驱动 `L.hasViewPermission()`,进而决定「保存/应用」页脚的只读状态 —— 而它的 `readonly` 是**沿 dispatch 路径折叠**出来的,与 `dispatcher.uc` 的做法一致:`ctx_append` 收集每个节点的 `depends.acl`,对并集做一次 `check_acl_depends()`,只要 *任意* 一个组可写就算可写,所以只有当路径上每一个带 acl 的节点都只读时,页面才是只读的。树里的逐节点标志(`apply_tree_acls`)只覆盖该节点自身的 acl;直接把叶子节点原样递过去,会让一个只读用户在某个只读组下的每个页面上都拿到可用的「保存并应用」。树对象不会被修改(`nodespec` 是一份拷贝)。`data-page` 是 `ui.tabs` 会话状态和主题页面级 CSS 的键。
4. **外壳装饰。** `menu-aurora.js` 暴露了 `syncRoute()`:它依据 `L.env.dispatchpath` 在每一个导航界面上重新标注 `is-active-page`/`aria-current`,展开活动的侧边栏/移动端分组并收起其余的,重建头部面包屑,并为新的分区重新渲染 `#tabmenu`。菜单**不会**重建 —— 大菜单在构造时就完成了测量和绑定,命令面板的索引是同一模型的扁平数组 —— 只有它们的状态在变。
5. **暂存。** 一个全新的 `<div id="view" class="view-staging">` 被插到 `#tabmenu` 之后,也就是**树序上的第一个** —— `getElementById('view')` 返回第一个匹配项,所以 LuCI 的 view 链写入的一切都进了这个暂存元素,而离场的页面仍留在屏幕上(变暗,`.view-leaving`)。这个暂存区不可见但**参与布局**(`visibility:hidden; height:0; overflow:hidden`,绝不用 `display:none`):实时图表在 `render()` 内部按 `#view.offsetWidth` 给自己定尺寸,而 `display:none` 的暂存区会递给它们一个宽度为 0 的画布。此时还什么都没被移除。
6. **补丁。** `header.ut` 把已安装的按需补丁词干输出为 `body[data-patches]`;路由施加与模板在渲染时相同的段前缀规则:匹配的 `patches/<stem>.css` 链接被确保存在(`<link data-aurora-patch>`,对屏幕上的页面启用,对其余页面 `disabled` —— 而不是移除,所以回来时不花任何代价);匹配的 `patches/<stem>.js` 文件只加载一次,其 `window.aurora.patches[stem]` 的 `{ mount, unmount }` 对按访问驱动(要挂载的词干列表属于计算出它的那次导航,所以一次被取代的导航之后不会挂载任何东西);路由添加的 URL 带着与模板自身链接相同的 `?v=PKG_VERSION` luci.mk 戳记,从 `body[data-asset-version]` 读出,因此它们命中同一个缓存条目 —— 一个什么都不注册的 JS 补丁就只是被执行一次,和 MPA 一样。补丁脚本在求值时自行挂载;如果用户在它到达之前已经导航走了,它的 `load` 处理器会检查当前页面是否仍然需要那个词干,否则就卸载它(期间到达的同词干页面会让它保持挂载)。
menu.d 节点自身的 `css`(`header.ut` 为被分发的节点链接 `<resource>/<node.css>`,标记为 `data-aurora-node-css`)用同样的方式维护:每张样式表一个 `<link>`,对解析出的叶子声明了它的那个页面启用,对其余每个页面 `disabled`,永不移除。这两个属性都豁免于投毒闸门。
6. **补丁。** `header.ut` 把已安装的按需补丁词干输出为 `body[data-patches]`;路由施加与模板在渲染时相同的段前缀规则:匹配的 `patches/<stem>.css` 链接被确保存在(`<link data-luci-patch>`,对屏幕上的页面启用,对其余页面 `disabled` —— 而不是移除,所以回来时不花任何代价);匹配的 `patches/<stem>.js` 文件只加载一次,其 `window.luciPatches[stem]` 的 `{ mount, unmount }` 对按访问驱动(要挂载的词干列表属于计算出它的那次导航,所以一次被取代的导航之后不会挂载任何东西);路由添加的 URL 带着与模板自身链接相同的 `?v=PKG_VERSION` luci.mk 戳记,从 `body[data-asset-version]` 读出,因此它们命中同一个缓存条目 —— 一个什么都不注册的 JS 补丁就只是被执行一次,和 MPA 一样。补丁脚本在求值时自行挂载;如果用户在它到达之前已经导航走了,它的 `load` 处理器会检查当前页面是否仍然需要那个词干,否则就卸载它(期间到达的同词干页面会让它保持挂载)。
menu.d 节点自身的 `css`(`header.ut` 为被分发的节点链接 `<resource>/<node.css>`,标记为 `data-luci-node-css`)用同样的方式维护:每张样式表一个 `<link>`,对解析出的叶子声明了它的那个页面启用,对其余每个页面 `disabled`,永不移除。这两个属性都豁免于投毒闸门。
7. **视图。**
- **冷**(本文档中从未 require 过 `view.<path>`):`window.L.require(className)` —— 这个 require *就是* 渲染(LuCI 在首次 require 时实例化),而且它必须走 `window.L` 这个运行时实例,绝不能走模块工厂拿到的那个原型式 `L`(`ui` 把 `itemlist`/`showModal` 挂在 `window.L` 上;通过错误的 `L` require 进来的 view 会在三层模块之后死在 `L.itemlist is not a function`,而且由于 `require()` 按名字缓存,绑定会被 *第一个* 请求者固定下来);
- **热**:`require()` 交回那个 `__init__` 已经跑过的缓存实例;LuCI 的类系统设置了 `prototype.constructor`,所以 `new instance.constructor()` 会跑一次全新的 `__init__` → `load()` → `render()` → `dom.content('#view')`,与整页加载的起点完全一致。两种情况下,require 到的值都会用 `instanceof L.view` 检查;不是的话就抛进硬加载路径,而不是把一个非 view 暂存起来。
- **完成是被观测的,不是被假定的**:暂存元素上的一个 `MutationObserver` 会在非 spinner 子节点落地时(或空渲染时 spinner 被移除时)解析。15 秒内没有完成算 **失败**,不算完成:提交 spinner 并释放序列化,会让仍在运行的链绘制到后来某次导航的 `#view` 里,所以超时会 reject,catch 路径硬加载目标地址。完成时 —— 并且仅当这次导航仍是最新的一次 —— 离场区域(`#tabmenu` 与 `<footer>` 之间除暂存元素外的一切)被移除,暂存的 view 在 `document.startViewTransition()` 可用且未开启减弱动效时于其内部显现;该次导航的 `finished` promise 在这次替换之后 resolve。每个离场元素在 `remove()` 之前都要走一遍 `L.dom.content(el, null)`:正是它清掉了元素的 `data-idref` 注册表条目,否则那些条目会把已分离的子树及其类实例一直吊着不放 —— 这正是下文浸泡测试所测量的东西。
- **渲染是被串行化的。** 进行中的 LuCI XHR 和正在跑的 `View.__init__` 链都无法取消(原因同上),而每条链都会绘制到绘制时刻*恰好排第一*的那个 `#view` 里。所以一次导航会先等待上一次导航的完成(受同一个超时约束),然后才拆除任何东西或暂存任何东西 —— 上一条链会完成到它自己的暂存元素里,随后被丢弃。因此快速的 A→B→C 绝不会交错:当 C 在 B 还没开跑时就到达,B 会被跳过(`event.signal` / 世代),而 C 会等待真正在飞的那次渲染。文档最初那次由 LuCI 渲染的 view 也按同样方式追踪,所以首次加载期间的一次点击不会被它覆盖绘制 —— 而一次永不完成的首次渲染会让那个等待 reject,于是第一次导航走硬加载回退,而不是在一条可能仍会绘制的链旁边做暂存。那次首渲染同样运行在一个渲染窗口内(该窗口在路由自己的监听器注册之后打开),所以它添加的监听器像冷渲染一样被记在其类的账上;而它在路由加载之前注册的那些则够不着。代价是慢加载期间的点击要等那次加载完成;替代方案 —— 逐类包裹 `prototype.render` 并通过重新导航来修复过期的冷渲染 —— 会留下一个真实的窗口,而且需要三套机制去做一套就够的事。
8. **焦点与播报。** 用 `preventScroll` 聚焦 `#maincontent`(`tabindex=-1`);新的 `document.title` 被写入 `#aurora-nav-status`(`role=status`、`aria-live=polite`),因为同文档替换不会触发屏幕阅读器会播报的 load 事件。这个地标带着 `outline-none`:iOS WebKit(Safari 和 iOS 版 Chrome 都一样)会为编程式聚焦绘制焦点环,而这个环紧贴吸顶头部下方的上边缘被用户报告为「一个永远走不完的进度条」—— 它从来就不是进度条。
9. **进度。** 存活超过 150 ms 的导航会插入 `#aurora-nav-progress` —— 形态上就是 Turbo Drive 那根条:顶部一条发丝细线,`width` 以内联方式驱动,并以越来越小的步长**涓流**推进(每 300 ms `+ (100 - w) / 30`)直到提交,这样慢渲染看起来一直在动而不是卡住;提交时填到 100 %,淡出(`data-state="done"`)并从 DOM 中**移除**。更短的导航保持沉默,重叠的导航共用同一根条。浏览器自带的进度条在这里帮不上忙:它只在文档加载时显示,而同文档替换恰恰不是文档加载 —— 这也正是 GitHub、YouTube、Turbo/HEY 和每一个 nprogress 用户都要自己画一根的原因。减弱动效只去掉过渡,不去掉这根条。
8. **焦点与播报。** 用 `preventScroll` 聚焦 `#maincontent`(`tabindex=-1`);新的 `document.title` 被写入 `#luci-nav-status`(`role=status`、`aria-live=polite`),因为同文档替换不会触发屏幕阅读器会播报的 load 事件。这个地标带着 `outline-none`:iOS WebKit(Safari 和 iOS 版 Chrome 都一样)会为编程式聚焦绘制焦点环,而这个环紧贴吸顶头部下方的上边缘被用户报告为「一个永远走不完的进度条」—— 它从来就不是进度条。
9. **进度。** 存活超过 150 ms 的导航会插入 `#luci-nav-progress` —— 形态上就是 Turbo Drive 那根条:顶部一条发丝细线,`width` 以内联方式驱动,并以越来越小的步长**涓流**推进(每 300 ms `+ (100 - w) / 30`)直到提交,这样慢渲染看起来一直在动而不是卡住;提交时填到 100 %,淡出(`data-state="done"`)并从 DOM 中**移除**。更短的导航保持沉默,重叠的导航共用同一根条。浏览器自带的进度条在这里帮不上忙:它只在文档加载时显示,而同文档替换恰恰不是文档加载 —— 这也正是 GitHub、YouTube、Turbo/HEY 和每一个 nprogress 用户都要自己画一根的原因。减弱动效只去掉过渡,不去掉这根条。
10. 任何异常 → `console.error`(静默回退会让每一次路由回归都看起来像「页面就是有点慢」)→ `location.href = destination` —— 一次硬性的整页加载,绝不留下卡住的页面。会先设置一个 `bypass` 标志,这样那次写入产生的 `navigate` 事件会直接放行,而不是被拦回失败路径;随后 handler 会停在一个永不落定的 promise 上,这样就不会再有别的东西对着一个正在离开的文档运行。
## 过期闸门
@@ -185,7 +185,7 @@ luci-base 在后台标签页里会继续轮询。路由在 `visibilitychange`
## 投毒闸门
view 写进 `<head>` 的 `<style>`/`<link rel=stylesheet>` 会在整页加载时随文档消亡,却会在同文档替换中**存活**,然后污染其后的每一个页面(某个已发布的文件管理器用一条未分层的 `!important` 规则在每个配置页上隐藏了保存/重置)。移除它并不是一个选项:在模块求值时导入 CSS 的库永远不会再执行一次,所以删除是单向的(某个编辑器页面回来时变成了一个两百万像素高的黑矩形)。因此这里用的是闸门,不是清扫:在拦截之前,`#view` 之外任何一张不属于主题自己的样式表都会把文档标记为**已投毒**,这次导航就是整页加载 —— 新文档不带任何 view CSS,所以路由会立刻恢复工作。「自己的」意思是*被标记过的*:header.ut 会给它渲染的一切打戳(`main.css`、字体、自定义与 token `<style>` 上的 `data-aurora-shell`;补丁上的 `data-aurora-patch`;menu.d 节点 css 上的 `data-aurora-node-css`)。闸门用来比对的启动快照按这些标记过滤,所以启动页自己的模块在路由加载之前插入的样式表仍然算作外来,而不会被「祖父条款」放行到文档余下的时间里。正确性优先于速度,绝不反过来。
view 写进 `<head>` 的 `<style>`/`<link rel=stylesheet>` 会在整页加载时随文档消亡,却会在同文档替换中**存活**,然后污染其后的每一个页面(某个已发布的文件管理器用一条未分层的 `!important` 规则在每个配置页上隐藏了保存/重置)。移除它并不是一个选项:在模块求值时导入 CSS 的库永远不会再执行一次,所以删除是单向的(某个编辑器页面回来时变成了一个两百万像素高的黑矩形)。因此这里用的是闸门,不是清扫:在拦截之前,`#view` 之外任何一张不属于主题自己的样式表都会把文档标记为**已投毒**,这次导航就是整页加载 —— 新文档不带任何 view CSS,所以路由会立刻恢复工作。「自己的」意思是*被标记过的*:header.ut 会给它渲染的一切打戳(`main.css`、字体、自定义与 token `<style>` 上的 `data-luci-shell`;补丁上的 `data-luci-patch`;menu.d 节点 css 上的 `data-luci-node-css`)。闸门用来比对的启动快照按这些标记过滤,所以启动页自己的模块在路由加载之前插入的样式表仍然算作外来,而不会被「祖父条款」放行到文档余下的时间里。正确性优先于速度,绝不反过来。
一个基于「归属」的精细化方案(从调用栈上把插入模块的身份戳到每张样式表上,对依赖闭包中包含该模块的页面启用它,对其余页面 `disabled`)曾被实现、在设备上验证过,然后又被**移除**了:在这台设备上只有一个 view 页面插入自己的 CSS,省下的不过是离开它时的一次重载,而代价是三处 monkeypatch 加一段内联模板脚本,它的故障模式 —— 某个页面悄无声息地少了一个共享库的 CSS —— 比它避免掉的那次重载更糟。只有当出现一批真实的自带样式的 view 页面时才值得重新考虑。
+102
View File
@@ -0,0 +1,102 @@
# Agent workflows
Two commands, one knowledge skill, five vertical agents, one mechanical
guard. Everything an agent asserts about luci-base or uhttpd must come from
a source checkout (`$LUCI_SRC`, `$UHTTPD_SRC` in the git-ignored `.dev/.env`)
and be cited as `file:line`; the facts they start from live in
`.claude/skills/luci-knowledge/`.
| Command | Use when | Output |
|---|---|---|
| `/luci-change <request>` | anything that edits `.dev/src`, `ucode/`, `htdocs/`, `root/`, `Makefile`, `vite.config.ts`, CI | a reviewed change in the working tree; commit only on request |
| `/upstream-rfc "<topic>"` | the fix belongs in luci-base / uhttpd / rpcd / the feed | `.dev/decisions/<slug>.md` with diagrams; no code change here |
| `/luci-vertical-review [base]` | step 6 of `/luci-change`, or any branch | ranked, verified findings |
## Changing this repository
```mermaid
flowchart LR
R[request] --> S{fix lives here?}
S -- no --> RFC[/upstream-rfc/]
S -- yes --> P[research + plan<br/>files · budget impact · floor · verification]
P --> G1{{gate 1<br/>user approves plan}}
G1 -- no --> P
G1 -- yes --> A[arm guard<br/>.claude/.change-approved]
A --> I[implement<br/>guard allows only approved paths]
I --> V[pnpm test · pnpm build · bench if device]
V --> G2{{gate 2<br/>luci-vertical-review}}
G2 -- blocker/major --> I
G2 -- clean --> D[report · disarm guard<br/>commit only if asked]
```
Gate 1 is plan mode: `ExitPlanMode` needs the user's selection; nothing is
edited before it. Gate 2 is the `luci-vertical-review` workflow:
`aurora-static-reviewer` (templates, CSS, tokens, patches, packaging, CI)
and `aurora-runtime-reviewer` (menu/router JS, teardown, polling, a11y)
review in parallel; every finding then goes to `luci-claim-verifier`, which
tries to refute it against the sources before it can be reported. With a
scope file the reviewers also check that the diff stayed inside the approved
paths.
The guard is `.claude/hooks/guard-edits.sh` (PreToolUse on
`Edit|Write|MultiEdit`): edits to shipped/source paths are refused unless
`.claude/.change-approved` lists a matching prefix. Docs, tests and `.claude/`
are not guarded. Edits made through Bash (`sed -i`, heredocs) are not seen
by the hook — the review's scope check is the second net. To bypass
deliberately, create the marker yourself; to disable, remove the hook from
`.claude/settings.json`.
## Proposing an upstream change
```mermaid
flowchart LR
T[topic] --> I1[analyst: server side<br/>dispatcher · http.uc · uhttpd]
T --> I2[analyst: client side<br/>luci.js · ui.js · theme templates]
I1 --> X{fix location?}
I2 --> X
X -- both say theme --> STOP[abort: use /luci-change]
X -- upstream/both --> C[claims with file:line]
C --> V[one verifier per claim<br/>refute-first]
V --> K[confirmed / corrected]
K --> W[writer: RFC from TEMPLATE.md<br/>mermaid before/after · revises an existing file]
W --> Q[critic: every sentence<br/>treated as a claim]
Q -- gaps --> W2[one revision]
Q -- none --> O[.dev/decisions/slug.md]
W2 --> O
```
Refuted claims never reach the document; unknowns are listed, not filled.
The writer may write only under `.dev/decisions/`. Bounds: at most 12
claims are verified (the rest are logged as dropped), one revision pass.
## Decision records (`.dev/decisions/`, local)
Proposals from `/upstream-rfc` and the closing report of every
`/luci-change` land in `.dev/decisions/`. The directory is git-ignored
except `TEMPLATE.md`: these files are drafts of one maintainer's decision
process — device numbers, half-verified claims, revisions — not
documentation. The durable record lives where the decision is acted on:
the commit body for a change here, the pull request for an upstream one.
Each file keeps a `Decision:` line (pending / submitted / interim /
dropped) so the directory doubles as the ledger.
## Agents
| agent | role | tools |
|---|---|---|
| `luci-runtime-analyst` | traces a request through luci-base/uhttpd sources, returns claims with evidence and where the fix lives | read-only |
| `luci-claim-verifier` | refutes one claim or finding; verdict confirmed / corrected / refuted | read-only |
| `aurora-static-reviewer` | templates, CSS/tokens/budgets, patches, packaging, CI | read-only |
| `aurora-runtime-reviewer` | `menu-aurora.js`, `router-aurora.js`, page JS patches, a11y | read-only |
| `upstream-rfc-writer` | writes/revises one proposal from verified claims | writes `.dev/decisions/` only |
## Where the pattern comes from
| practice | source | here |
|---|---|---|
| orchestrator–workers, parallel reviewers, evaluator–optimizer loop | Anthropic, *Building effective agents* (2024) | workflow scripts; implement → review → fix, two rounds max |
| explore → plan → code; plan mode as the approval point; a second Claude reviews | Anthropic, *Claude Code best practices* | `/luci-change` steps 1–2, gate 2 |
| human-in-the-loop for high-risk actions, mechanical guardrails | OpenAI, *A practical guide to building agents* (2025) | gate 1 + `guard-edits.sh` |
| design doc before a non-trivial change; small, single-purpose commits; reviewers judge correctness, not taste | Google engineering practices | RFC template; one logical change; evidence-only findings |
| fixed proposal sections (motivation, reference, drawbacks, alternatives, unresolved questions) | Rust RFC / ADR | `TEMPLATE.md` |
| one logical patch with a cover letter carrying measurements | Linux kernel / OpenWrt submission norms | RFC "Submission"; bench before/after |