Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/12255-governed-workflows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

objectui#12255: `.github/workflows/**` joins the governed surfaces in `scripts/check-governed-queue-guard.mjs`, `AGENTS.md` and the CI guide. Repository tooling and docs only; no published package changes.
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,13 +527,14 @@ ls <dir>/* | wc -l # 两个数不等 ⇒ 工

> 同意

**本仓的受管面 —— 五项,已逐条对本仓实际目录核实:**
**本仓的受管面 —— 六项,已逐条对本仓实际目录核实:**

- `AGENTS.md` —— **你正在读的这个文件本身就是受管面**。
- `CLAUDE.md` —— 仓根一个。
- `.claude/**` —— **整棵树**:hooks、settings、launch 配置、内部 skills,一个不落,**不是只有 skills**。
- `skills/**` —— 仓根**发布给使用者**的那棵 skills 树(如 `skills/objectui/`),guard 里的 `skills-catalog` 一项 —— 见下方 ⚠️,这一项本段曾经写反。
- `docs/adr/**` —— 本仓**确实有**这个目录;它和上面四项同级,不因为篇数少而降级。
- `.github/workflows/**` —— CI、部署与发版 workflow 决定每次 push 跑什么、拿什么门禁挡合并队列,改它们由维护者批准(维护者 2026-10-12 裁决,原文照录:「两仓加治理行」,objectui#12255)。guard 自己的 `governed-surface-guard.yml` 也在内;Dependabot 的 `github-actions` 升级同样受管 —— `dependabot-auto-merge.yml` 照常批准并入队,`merge_group` 腿照样拒绝,它等的也是一条获授权的批准。`.github/` 下其余文件(`dependabot.yml`、`labeler.yml`、`prompts/`)**不受管**。

⚠️ **两棵 skills 树都受管 —— 别把它们和 skill 的安装位置弄混:**

Expand All @@ -559,7 +560,7 @@ sha pin **退休**之后这条**更重、不是更轻**(维护者 2026-09-04 裁
- **CI 全绿、已 review 都不构成例外。** 这类文件是后续每一次 dispatch 读的操作规程,绿灯说明不了它该不该成为规程。
- **发现自己已经挂上了怎么办**:把 PR 转回 **draft** 是唯一能可靠退出合并队列的动作 —— 只调 `disable_pr_auto_merge` 会摘掉 auto-merge 但**不取消队列成员资格**,两个都要做。⚠️ 只回收**你自己**挂上的:本仓多 agent 共用同一 GitHub 身份,不是你设置的状态就属于别的 actor —— 去问、去报告,别替他回退。

**本仓的机械兜底只有一件,它的两条腿意思不同:`merge_group` 腿拦得住合并队列(2026-09-27 起,见下),`pull_request` 腿只告警 —— 别把 PR 页上的绿读成不受管,也别再读成「什么都没有」。** 本仓仍然没有 CODEOWNERS(核实:仓内不存在该文件),受管面上也没有钩子;但 `.github/workflows/governed-surface-guard.yml`(check 名 `Governed Surface Queue Guard`,判定逻辑在 `scripts/check-governed-queue-guard.mjs`)**是活的**:`pull_request` 腿是早期告警、**故意 exit 0**(受管 PR 停在 draft 正是健康终态,所以**绿不等于不受管**),`merge_group` 腿才是会拒绝的那条 —— 它要求 `GOVERNED_APPROVERS`(`os-zhuang` / `hotlong`)里某个账号的一条 latest-decisive APPROVED review,**留在哪个 commit 上都算**;DISMISSED 与被顶掉的批准(同一 reviewer 后续给了 CHANGES_REQUESTED)不算,该集合之外账号的 APPROVED 不算,review 列表为空或读不到则 fail closed —— 判定读的是**有没有一条人工批准记录**,不问它是对哪些字节给的(维护者 2026-09-04 裁,逐字未译:「你的门禁有问题,只需要有人工批准记录就行,不需要卡最新的提交。」;sha pin 是**退休**不是放宽,守卫里已没有任何判定读 `commit_id`)。⚠️ 已接受的代价:批准之后的 push 不再被这道门重审,一个已批准的受管 PR 可以带着批准者没读过的字节落地 —— 维护者接受这一点,而这道拒绝先印的补救仍是转回 draft、交人类合并。⚠️ 而这条拒绝腿上的判据**不止上面那一条** —— #9018(PR #9212)给 `merge_group` 腿加了**第二条、与受管面完全无关的**判据:它枚举该 merge group 要落地的**每一个** PR(逐 commit 分解,不是只读 `merge_group.head_ref`),读每个 PR 的 label,其中任何一个仍挂着 `needs:contract-review` 就拒绝(**exit 6**);label 读不到、或该 group 指不出任何 PR,同样拒绝(**exit 7**)。⚠️ 这条判据读的是**挂在 PR 上的 label**,不读任何路径、不问 diff 命中了什么 —— ⇒ **一个 diff 完全没碰受管面的 merge group,现在也可能被这道 check 拒绝**;`pull_request` 腿不受影响,不读 label。⚠️ 它拦得住,是因为这个 check 名进了 ruleset 的 required 集合 —— 下面是一段有日期的历史,不是本段替你复核的现状:ruleset 开关只有维护者能翻;2026-09-14 的读数里它还不在集合中,那时这条拒绝腿只报告、不阻止队列;ruleset 的最后一次编辑在 2026-09-27,三分钟后 objectui#6596 以 completed 关闭,2026-09-28 实测它已在集合里。**在集合里,它的拒绝(上面的 exit 6 / exit 7 同样)就会把 PR 踢出合并队列。** 它现在还在不在,跑 `node scripts/check-required-check-set.mjs` 读,⛔ 别把它的答案抄进本段。⚠️ 这是按配置推出的,不是观察到的:登记后到 2026-09-28 的 `merge_group` 运行全部通过,拒绝路径还没在队列里真正触发过一次。事后一侧:`../objectstack` 的 report-only 合并后审计(`scripts/pm/check-governed-merges.mjs`)自 objectstack#9619 起**已覆盖本仓**(四个受管仓一次扫完),它把受管面的合并列出来,但它不阻止任何事。⇒ 一个没有获授权批准记录的受管 PR 经合并队列落不了地,这一半队列替你拦;**拦不住的是批准本身**:席位共用一个 GitHub 身份,守卫分不出那条 APPROVED 是不是人留的,绕过 ruleset 的人也不经这道门(谁能绕过,见上面 ruleset 那一条)—— 所以第五条禁令的效力仍然全在于你读到了它并照做。**⛔ 别再把本段当成兜底工具的完整清单** —— 覆盖面以脚本自己的 `GOVERNED_SURFACES` 为准(它随树变化,本段不会);⚠️ 该清单曾与上面的受管面清单**并不一致**(脚本的集合含已发布 `skills/**`),这一分歧**已裁**:维护者第 5 场决裁批 #7 采 **Option A**(#6866 评论 5469339478)—— 已发布 `skills/**` **受管**,守卫的读法才是裁定的那个;该裁决**已随本段上方的清单落地**(#6866):上面五项已含 `skills/**`,与脚本的 `GOVERNED_SURFACES` 一致,曾经那条「仓根 `skills/**` 不受管、自行入队」的豁免**已作废**,⛔ 别再照它行事。
**本仓的机械兜底只有一件,它的两条腿意思不同:`merge_group` 腿拦得住合并队列(2026-09-27 起,见下),`pull_request` 腿只告警 —— 别把 PR 页上的绿读成不受管,也别再读成「什么都没有」。** 本仓仍然没有 CODEOWNERS(核实:仓内不存在该文件),受管面上也没有钩子;但 `.github/workflows/governed-surface-guard.yml`(check 名 `Governed Surface Queue Guard`,判定逻辑在 `scripts/check-governed-queue-guard.mjs`)**是活的**:`pull_request` 腿是早期告警、**故意 exit 0**(受管 PR 停在 draft 正是健康终态,所以**绿不等于不受管**),`merge_group` 腿才是会拒绝的那条 —— 它要求 `GOVERNED_APPROVERS`(`os-zhuang` / `hotlong`)里某个账号的一条 latest-decisive APPROVED review,**留在哪个 commit 上都算**;DISMISSED 与被顶掉的批准(同一 reviewer 后续给了 CHANGES_REQUESTED)不算,该集合之外账号的 APPROVED 不算,review 列表为空或读不到则 fail closed —— 判定读的是**有没有一条人工批准记录**,不问它是对哪些字节给的(维护者 2026-09-04 裁,逐字未译:「你的门禁有问题,只需要有人工批准记录就行,不需要卡最新的提交。」;sha pin 是**退休**不是放宽,守卫里已没有任何判定读 `commit_id`)。⚠️ 已接受的代价:批准之后的 push 不再被这道门重审,一个已批准的受管 PR 可以带着批准者没读过的字节落地 —— 维护者接受这一点,而这道拒绝先印的补救仍是转回 draft、交人类合并。⚠️ 而这条拒绝腿上的判据**不止上面那一条** —— #9018(PR #9212)给 `merge_group` 腿加了**第二条、与受管面完全无关的**判据:它枚举该 merge group 要落地的**每一个** PR(逐 commit 分解,不是只读 `merge_group.head_ref`),读每个 PR 的 label,其中任何一个仍挂着 `needs:contract-review` 就拒绝(**exit 6**);label 读不到、或该 group 指不出任何 PR,同样拒绝(**exit 7**)。⚠️ 这条判据读的是**挂在 PR 上的 label**,不读任何路径、不问 diff 命中了什么 —— ⇒ **一个 diff 完全没碰受管面的 merge group,现在也可能被这道 check 拒绝**;`pull_request` 腿不受影响,不读 label。⚠️ 它拦得住,是因为这个 check 名进了 ruleset 的 required 集合 —— 下面是一段有日期的历史,不是本段替你复核的现状:ruleset 开关只有维护者能翻;2026-09-14 的读数里它还不在集合中,那时这条拒绝腿只报告、不阻止队列;ruleset 的最后一次编辑在 2026-09-27,三分钟后 objectui#6596 以 completed 关闭,2026-09-28 实测它已在集合里。**在集合里,它的拒绝(上面的 exit 6 / exit 7 同样)就会把 PR 踢出合并队列。** 它现在还在不在,跑 `node scripts/check-required-check-set.mjs` 读,⛔ 别把它的答案抄进本段。⚠️ 这是按配置推出的,不是观察到的:登记后到 2026-09-28 的 `merge_group` 运行全部通过,拒绝路径还没在队列里真正触发过一次。事后一侧:`../objectstack` 的 report-only 合并后审计(`scripts/pm/check-governed-merges.mjs`)自 objectstack#9619 起**已覆盖本仓**(四个受管仓一次扫完),它把受管面的合并列出来,但它不阻止任何事。⇒ 一个没有获授权批准记录的受管 PR 经合并队列落不了地,这一半队列替你拦;**拦不住的是批准本身**:席位共用一个 GitHub 身份,守卫分不出那条 APPROVED 是不是人留的,绕过 ruleset 的人也不经这道门(谁能绕过,见上面 ruleset 那一条)—— 所以第五条禁令的效力仍然全在于你读到了它并照做。**⛔ 别再把本段当成兜底工具的完整清单** —— 覆盖面以脚本自己的 `GOVERNED_SURFACES` 为准(它随树变化,本段不会);⚠️ 该清单曾与上面的受管面清单**并不一致**(脚本的集合含已发布 `skills/**`),这一分歧**已裁**:维护者第 5 场决裁批 #7 采 **Option A**(#6866 评论 5469339478)—— 已发布 `skills/**` **受管**,守卫的读法才是裁定的那个;该裁决**已随本段上方的清单落地**(#6866):上面六项已含 `skills/**`,与脚本的 `GOVERNED_SURFACES` 一致,曾经那条「仓根 `skills/**` 不受管、自行入队」的豁免**已作废**,⛔ 别再照它行事。

### 服务纪律(本仓库与 `../objectstack` 多 agent 并行开发)

Expand Down
15 changes: 11 additions & 4 deletions content/docs/guide/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -1788,8 +1788,10 @@ each *free* package is already imported by.
**Blocks a PR?** Not on the pull request, by design. On a merge-queue build it refuses.

The **governed surface** is a fixed list — `AGENTS.md`, `CLAUDE.md`, `.claude/**`, `skills/**`,
`docs/adr/**` — and the rule about it is that a change to any of them is merged by a human, not by
the queue. That rule used to live only in prose. On
`docs/adr/**` and, since the maintainer's ruling on
[#12255](https://github.com/objectstack-ai/objectui/issues/12255), `.github/workflows/**` — and
the rule about it is that a change to any of them is merged by a human, not by the queue. That rule
used to live only in prose. On
[#6183](https://github.com/objectstack-ai/objectui/pull/6183) an `AGENTS.md` change was correctly
parked as a draft; a GitHub MCP `update_pull_request` call passing only `reviewers` silently also
set `draft: false`; the pull request entered the merge queue and landed with no human approval, and
Expand Down Expand Up @@ -1857,8 +1859,13 @@ the sweeper copy whose H31 compared the gate's two carriers was retired
carriers either. The `CLEAR` rendering says so out loud, so a green is never read as "the review
happened".

**What it deliberately does not do.** It does not govern its own workflow or CI configuration
generally: that would be a larger rule than the one that was ruled. It cannot stop a maintainer
**What it deliberately does not do.** It does not govern the rest of `.github/` — `.github/dependabot.yml`,
`.github/labeler.yml`, `.github/prompts/` — because the ruling named the workflows, not their
directory. It *does*
govern its own workflow file, and a Dependabot `github-actions` bump with it: that bump edits
`.github/workflows/**`, so `dependabot-auto-merge.yml` still approves and enqueues it, the queue leg
refuses it, and it waits for an authorized approval like any other workflow change. It
cannot stop a maintainer
merging a governed pull request by hand, and does not try — under this regime the human merge *is*
the review record. And it does not make itself required: that is a branch-protection setting only
the maintainer can flip. Until it is flipped, the queue leg reports without stopping anything. What
Expand Down
28 changes: 20 additions & 8 deletions scripts/__tests__/check-governed-queue-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,17 @@ describe('check-governed-queue-guard is wired, not merely present', () => {
* nothing, and the reverse (a surface silently dropped) is the rule quietly
* shrinking.
*/
describe('the governed surface is the 2026-08-18 definition, and it is real', () => {
it('declares exactly the five ruled surfaces', () => {
describe('the governed surface is the ruled definition, and it is real', () => {
it('declares exactly the six ruled surfaces', () => {
// The five of the 2026-08-18 definition, then `.github/workflows/**`, which
// the maintainer's ruling on objectui#12255 added (「两仓加治理行」).
expect(GOVERNED_SURFACES.map((s) => s.glob)).toEqual([
'docs/adr/**',
'.claude/**',
'skills/**',
'AGENTS.md',
'CLAUDE.md',
'.github/workflows/**',
]);
});

Expand All @@ -174,12 +177,21 @@ describe('the governed surface is the 2026-08-18 definition, and it is real', ()
}
});

it('does not govern its own workflow, or CI configuration generally', () => {
// The widening deliberately NOT taken. Promoting CI config to a governed
// surface is a strictly larger rule than the one ruled, and taking it in an
// implementation would be a governance gate acquiring policy nobody agreed
// to. Pinned so a later edit has to be a decision.
expect(governedPathsIn([WORKFLOW, GATE, '.github/workflows/ci.yml'])).toEqual([]);
it('governs every workflow, its own included, and not the script that workflow runs', () => {
// This case used to pin the opposite ("does not govern its own workflow, or
// CI configuration generally") as the widening deliberately NOT taken. The
// maintainer took it on objectui#12255, so the pin is reversed, not deleted:
// a later edit that drops the row still has to be a decision.
const hit = governedPathsIn([WORKFLOW, GATE, '.github/workflows/ci.yml']);
expect(hit.map((s) => s.id)).toEqual(['workflows']);
expect(hit[0].files).toEqual([WORKFLOW, '.github/workflows/ci.yml']);
});

it('does not govern the rest of .github/ — the ruling named the workflows, not their directory', () => {
// YAML paths only: a markdown literal here would read, to
// `markdown-test-inputs.mjs`, as a document this test consumes. The self-test
// carries the `.github/prompts/**` negative.
expect(governedPathsIn(['.github/dependabot.yml', '.github/ISSUE_TEMPLATE/config.yml', '.github/labeler.yml'])).toEqual([]);
});

it('the two root rows are EXACT — a vendored copy is ordinary source', () => {
Expand Down
Loading
Loading