mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
🦄 Sync 2026-08-26 20:42:53
This commit is contained in:
1 parent
53bb25bf38
commit
e7a277e681
487 files changed
+55081
-19590
No files matched your search
@@ -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 文件,**只在它所针对的页面上加载**。
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 页面时才值得重新考虑。
|
||||
|
||||
|
||||
@@ -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 |
|
||||
Reference in new issue
Block a user