Skip to content

governed: a fork PR is a proposal, never a delivery — the FORK predicate beside PATH and SIZE, plus the charter's external-contributions section - #19526

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-19470-fork-pr-is-a-proposal
Sep 21, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-19470-fork-pr-is-a-proposal

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #19470

Clause-②: no

The ruling this lands

Maintainer, batch #207 item 1, letter 是, verbatim and untranslated: 「fork PR 的处理同意你的意见。」 — recorded by the director on the card (comment 5754996112) as: a fork PR is a proposal, not a delivery. No agent seat flips it ready, enqueues it, arms auto-merge on it, or approves it — ever. It enters through the card door (card first; the human decides the problem, not the code; the seat adopts the diff and never lands the fork PR; a fork's CI is never approved to run by a seat, zero check runs read NOT MEASURED, never green), with the minimal mechanism: a second predicate beside the governed one in scripts/pm/check-governed-merges.mjs, one sentence in AGENTS.md, one short 〈外部贡献〉 section in the charter, the NOT-MEASURED wording. No new label, no new sweep; the supply-chain clause is deferred until the first such fork PR appears.

The live specimen, PR #19342 (head jinyitao123/objectstack, author_association: FIRST_TIME_CONTRIBUTOR, 0 check runs on 5fa7b6d, 0 labels), is read here as evidence only. It stays untouched by this PR: its disposal is the director's and triage's act in the same stroke, and this branch writes nothing on it.

What changed (five files, all on the claim's surface)

file before → after net what
scripts/pm/check-governed-merges.mjs 6146 → 6226 lines +80 (98 added, 18 replaced; budget ≤ +80, self-tests included) the FORK predicate beside PATH and SIZE; the check-run reading; a new self-test battery beside the SIZE cases
AGENTS.md 1109 → 1109 0 (ceiling 1109, headroom 0) one sentence in Prime Directive #14's first paragraph, paid by one retired restatement
.claude/skills/pm-dispatch/SKILL.md 815 → 816 +1 (ceiling 819, headroom 3) one rule line in 〈入队与落地〉
.claude/skills/pm-dispatch/references/core-rules.md 151 → 151 0 the landing-rule line rewritten in place to carry the twin
.claude/skills/pm-dispatch/references/external-contributions.md new, 16 lines +16 (budget ≤ 20; widest line 120 B) the ruled 〈外部贡献〉 section, four points + the deferred supply-chain line

1. check-governed-merges.mjs — the FORK limb

  • forkVerdict(pull) (pure, exported, beside sizeVerdict): reads head.repo.full_name and base.repo.full_name off the PR object; NOT MEASURED when there is no base.repo to compare against; a fork when the head repo differs — or is null (the fork was deleted): a deleted fork is still a fork, fail closed.
  • testVerdict(paths, { size, fork, checks }) carries fork and checks; humanMerge fires on any of the three limbs; landsByHumanMerge reads the fork limb too, so a fork exits on the EXISTING GOVERNED code (3) through the Tier H terminal — every caller that already routes 3 to the human hand routes a fork there without learning a new code. applyGeneratedExceptions keeps the fork limb across a generated-artifact lift (a lift moves a PATH, never the head repo).
  • renderForkLines(fork, checks) (pure, exported): the fork sentence under the verdict — a PROPOSAL, never a delivery, whatever its paths; no AI seat flips it ready, enqueues it, arms auto-merge on it or approves it; a seat's review is required INPUT, never the permission; the owning seat adopts the diff onto an internal branch (Co-authored-by: the contributor) and lands THAT; requests to the contributor go only as review comments on the fork PR, which is closed with thanks and the landing link. When the PATH limb is clear the head line reads paths: none on the register — the HEAD REPO decides this PR: (the SIZE limb's shape); on a governed diff the sentence rides under the tier block, Tier S included (a fork head on a .claude/** path is still adopted, never landed).
  • --pr N reads the head's check-run count off GET /repos/{slug}/commits/{sha}/check-runs — one more GET, on the channel already chosen for the PR read. total_count: 0 prints check runs on head SHA: 0 — NOT MEASURED, never green in the register the size limb already uses; a count that did not read prints NOT READ (reason) — read as NOT MEASURED, never green; a non-zero count prints nothing.
  • --test and --branch cannot see a head repo and now say so on stdout (head repo: NOT MEASURED — this verdict cannot tell a fork PR from an internal one; --pr N reads it), the same discipline as the size limb: a verdict silent about a leg it did not run reports a clearance it never measured. The --branch / --test byte-identity pin still holds (both print the same line).
  • --json carries fork and checks beside size.
  • Self-test: battery ⭐ the FORK predicate: head repo ≠ base repo is a proposal, never a delivery (6 cases, floor 6, roster 30 → 31), placed between the SIZE battery and the audit-half battery, never at the tail: fork head on ordinary paths ⇒ H route with the fork sentence and no ordinary queue landing applies; head.repo === null ⇒ the same; same-repo head on ordinary paths ⇒ unchanged (NOT governed, no fork line); same-repo head on AGENTS.md / .claude/agents/os-dev.md ⇒ Tier H / Tier S word for word, while a fork head on the Tier S path still prints the fork sentence; no PR object ⇒ NOT MEASURED said; zero check runs ⇒ the NOT-MEASURED line, 43 ⇒ absent, unread ⇒ NOT READ; and fetchPullFiles against an injected fetch reads the head repo off its own GET and the count off the head sha. Header docblock gains a "The FORK predicate" section; the summary line names the battery.

2. AGENTS.md — one sentence, net 0

Prime Directive #14, first paragraph, :264 before (102 B):

    paragraph names fewer surfaces than the register — or more. When it reds, name the surface here.

:264–:266 after (113 B / 116 B / 106 B):

    paragraph names fewer surfaces than the register — or more. When it reds, name the surface here. A fork PR
    (head repo ≠ base repo) is a proposal, never a delivery, whatever its paths: no AI seat readies, queues, arms
    auto-merge on or approves it; the owning seat adopts the diff onto an internal branch and lands that.

"whatever its paths" is the sentence's shape saying the fork rule is a predicate on the PR, not a surface: the paragraph still names exactly the register's six surfaces and no **-shaped span was added, so pnpm check:pm-governed-prose stays green (quoted below).

Payment (+2 lines bought by one retired restatement, ⛔ not by the ceiling, not by re-wrap): the three paragraphs of Prime Directive #14 are greedy-packed already (878 / 1789 / 918 characters joined ⇒ 8 / 16 / 8 lines at the 120-column cap, exactly what they occupy), so re-wrap buys nothing. Retired: the Skills section's line (pre-edit :800, 117 B, plus its trailing blank line):

⛔ **Both roots are governed surfaces** — `skills/` is Tier H, `.claude/skills/` Tier S (**Prime Directive #14**).

It restated the register and its tiers, whose home is the directive it pointed at. Surviving home and grep proof on this head: grep -n 'skills/\*\*' AGENTS.md → :6 (the CLAUDE-conflict clause), :260 (the register names .claude/** and skills/**), :273 (Tier H names skills/**); grep -n 'Tier S' AGENTS.md → :258, :276 (Tier S = all of .claude/**), :292; grep -n 'Both roots' AGENTS.md → no hits. The Skills section keeps its two-root catalog lines; only the restatement left.

3. SKILL.md — one rule line in 〈入队与落地〉 (+1, 108 B)

Inserted at :643, directly under the section's pointer line (- 细则见 references/landing-operations.md,落地窗口查阅。):

- fork PR = 提案,席位永不放行;采纳 diff 内部落地,见 `references/external-contributions.md`。

Placement (four axes): 业务需求 — the acts the rule forbids (ready / 入队 / auto-merge / 批准) are the acts this section governs, so a seat reading its landing checklist meets the fork rule where it would otherwise act; 长远合理性 — one rule line, one pointer, the detail in a references file, the same shape as the section's first line; 防 AI 犯错 — the line sits before the 条款② gate and the PASS ⇒ ready ⇒ auto-merge line, so the fork stop is read before the enqueue reflex; 创业阶段不扩散 — ≤ +1, no new section. ⛔ Not in the 分诊 / 定级 region (:174–:175, :362, :368–:372, :379 are #19494's) and not on PR #19488's lines (the area:* rows, :119–:134, :215, :243, :259, :294, :310, :371–:372, :470–:471). PR #19513 edits :632–:633 and :647 / :653 of the same section; :643 sits between its two hunks with unchanged context on both sides, so the later lander merges origin/main once with no conflict expected.

4. references/core-rules.md — the twin, paid in place (151 → 151)

:122 before (118 B) → after (112 B):

- 验收后取路径面,命中规则层即分叉 ⛔ 不翻正式不入队;Tier S 席内达档复核 PASS 后入队。
- 验收后取路径面:规则层 ⛔ 不翻正式不入队,Tier S 达档 PASS 后入队;fork PR 永不放行。

Every landing-rule line in the core subset sat at 111–118 B of the 120 B cap, so the twin could only ride a compression: 「命中…即分叉」 and 「席内…复核」 are folded to 「规则层」 and 「达档」 (the SKILL.md lines they summarise are unchanged), and the freed bytes carry the rule. No clause dropped. PR #19513 and PR #19488 touch :55–:69, :102, :106–:113 of this file, not :122.

5. references/external-contributions.md — new (16 lines, every line ≤ 120 B)

Frame as its siblings (title, a 「见 SKILL.md 〈…〉」 pointer line, one rule per line). Bytes per line: 45 · 0 · 117 · 117 · 0 · 113 · 77 · 97 · 92 · 102 · 110 · 113 · 106 · 113 · 116 · 120. Content, the four points of the ruling in substance: ① card first — triage's fire scans open PRs whose head repo ≠ base repo, files a card from the PR body graded like any card, posts the ONE fixed comment (this repository works card-first; filed as #N; this PR stays a draft until the card is graded), a fork PR with no card does not exist on the board; ② the human decides the problem, not the code — a plain reproducible defect routes to a seat, floor items (security / permission boundary, contract, feature) go to the decision box as a business question; ③ the seat adopts the diff, never lands the fork PR — internal branch, cherry-pick or re-implementation, Co-authored-by: the contributor, full gate derivation + at-tier review + the queue as for internal work, requests to the contributor only as review comments on the fork PR, closed with thanks and the landing link when the internal PR lands; ④ a fork's CI is never approved to run by a seat, zero check runs read NOT MEASURED, never green; the machine face (check-governed-merges.mjs --pr N routes head repo ≠ base repo through the H(人合) exit, head.repo null the same); and the deferred supply-chain line.

check-skill-line-ratchet.mjs does not require a CEILINGS row for a new references file (it enumerates only references/lanes/ for uncovered files; the file is simply outside the ratchet's map, as references/instrument-discipline.md on PR #19513 is). No row was added: the ratchet script is outside this card's file surface. Noted under Acceptance notes for the seat.

The two --pr 19342 readings

Before (origin/main 8fc6a5f), node scripts/pm/check-governed-merges.mjs --pr 19342 :: exit 0:

governed-surface predicate: 0 of 4 path(s) hit the register (6 surfaces, repo-agnostic).
  ✅  NOT governed — ordinary queue landing applies to a PR with exactly this file list.
      Derived from GOVERNED_SURFACES, not recalled. Re-run on the FINAL file list: the register
      has grown several times in two days, and a reading taken earlier in the session is recall.
  size: 56 changed line(s) (+48 / -8) ≤ 5000 — under the human-merge threshold (generated files included in the count).

After (this head 0a50588), node scripts/pm/check-governed-merges.mjs --pr 19342 :: exit 3:

governed-surface predicate: 0 of 4 path(s) hit the register (6 surfaces, repo-agnostic).
  paths: none on the register — the HEAD REPO decides this PR:
  ⛔  FORK PR — head jinyitao123/objectstack ≠ base objectstack-ai/objectstack: a PROPOSAL, never a delivery, whatever its paths.
      The Tier H terminal, with its own reason: no AI seat flips it ready, enqueues it, arms auto-merge on it or approves it —
      ever; a seat's review is required INPUT, never the permission. The owning seat adopts the diff onto an internal branch
      (`Co-authored-by:` the contributor) and lands THAT through the ordinary gates; requests to the contributor go only as
      review comments here, and this PR closes with thanks and the landing link.
  ⚠️  check runs on head 5fa7b6dc9d: 0 — NOT MEASURED, never green: a fork's CI does not run until a
      maintainer approves it, and a head that never ran looks exactly like one that passed. No seat approves it to run.
  size: 56 changed line(s) (+48 / -8) ≤ 5000 — under the human-merge threshold (generated files included in the count).

--json on the same run: governed: false, tier: null, humanMerge: true, fork: { measured: true, isFork: true, headRepo: "jinyitao123/objectstack", baseRepo: "objectstack-ai/objectstack" }, checks: { sha: "5fa7b6dc9d76657a57e30fbcd8010277261254f7", total: 0, reason: null }.

Design choices, on the four axes

head.repo === null reads as a fork (fail closed). 实际业务需求: the API returns head.repo: null exactly when a contributor deleted the fork after opening the PR — a PR nobody can rebuild the head of is the least reviewable shape a fork PR takes, and the population it comes from is the fork population. 项目长远合理性: the predicate is closed (head ≠ base, with an unreadable head on the far side of ≠), so no third state grows beside it. 防 AI 犯错: the alternative — treating an unreadable head as "not a fork" — is a ?? fallback in the consumer that turns a missing fact into a clearance; the fail-closed reading is the loud one, and its words name the deleted repo. 创业阶段不扩散: zero extra code — one headRepo === null || in the predicate, one (deleted fork repo) in the words, one self-test case. The one case deliberately left NOT MEASURED rather than forked is a PR object with no base.repo at all (a fixture shape, never seen from the API): there is nothing to compare against, and NOT MEASURED is never a clearance either.

Where the SKILL.md line sits — the four axes are given under §3 above; the 分诊 / 定级 region belongs to #19494 and the feature-axis lines to PR #19488, so the landing section was the only region on the claim.

PM mechanism assumptions — verified

  1. ✅ At 8fc6a5f check-governed-merges.mjs had 0 hits for head.repo / author_association / FIRST_TIME; the PR object is fetched in fetchPullFiles (fetchJsonOver on GET /repos/{slug}/pulls/{n}, the read that gives changed_files and the size pair), and the proxy returns head.repo (jinyitao123/objectstack on feat(auth): allow explicit intranet HTTP OAuth #19342, base.repo objectstack-ai/objectstack). The fork limb reads that same object; the check-run count is a second GET on the same channel.
  2. ✅ The H route's terminal reads ⚖️ landing tier: H(人合) — the maintainer's hand, or an authorized APPROVED review (GOVERNED_APPROVERS) and then the owning seat lands it; the SIZE limb's terminal reads The same terminal as a Tier H governed diff: no seat flips it ready, enqueues it, or arms auto-merge. The fork sentence keeps the three verbs, adds approves it, and adds the adoption step instead of the seat-lands-it clause (a fork PR is never landed by the seat).
  3. ✅ AGENTS.md 1109 / 1109; on the unchanged tree pnpm check:pm-skill-ratchet :: exit 0 and pnpm check:pm-governed-prose :: exit 0; on this head both exit 0 again (quoted below).
  4. node scripts/pm/check-governed-merges.mjs --pr on this PR's own number is run after the PR exists; the reading (expected: exit 3, GOVERNED, Tier H — AGENTS.md and skills-free .claude/** on the register, one Tier H path making the whole PR Tier H) goes into the os-dev-report comment on the card. This PR stays draft for the maintainer's hand.

Verification (this head 0a50588)

  • node scripts/pm/check-governed-merges.mjs --self-test :: exit 0 — 441 assertions, the new battery registered at its floor; the two --pr 19342 readings above; --test README.md :: exit 0 (NOT governed, head repo: NOT MEASURED on stdout), --test AGENTS.md :: exit 3; npx eslint scripts/pm/check-governed-merges.mjs :: exit 0.
  • The four charter gates, exits captured before any pipe: pnpm check:pm-skill-ratchet :: exit 0 (AGENTS.md 1109 / 1109 headroom 0; SKILL.md 816 / 819; core-rules.md 151 / 151), pnpm check:pm-skill-id-lint :: exit 0, pnpm check:pm-governed-prose :: exit 0 (2 surfaces name all 6 registered surfaces and claim no others), pnpm check:nul-bytes :: exit 0; control-character scan of the five files: 0 hits.
  • Derived union, node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack with no paths on 0a50588: 44 commands. Every command run in the foreground with its exit captured before any pipe (cmd > log 2>&1; EXIT=$?), then reconciled: node scripts/pm/dispatch-gates.mjs --ran ran-union.txt --repo objectstack-ai/objectstack :: exit 044 derived, 44 run, 0 NOT-MEASURED, 0 UNRUN, every exit 0. One family needed its inputs built first: pnpm --filter @objectstack/lint run check:doc-formula-expressions answered PREREQUISITE NOT MET (exit 3, @objectstack/formula then @objectstack/lint not built in a fresh worktree) until pnpm --filter '@objectstack/lint...' build ran under the shared verification lock (os-verify-lock.sh, VERDICT command-exit 0, held 176 s); re-run on the built tree it exits 0 (22 record-scoped examples judged clean). Heavy families in the union (check-governed-queue-guard.mjs --self-test, check:pm-dispatch-gates, check-skills-token-ratchet.mjs) all green.
  • Local scope per the dispatch: no package's pnpm test / typecheck is owed — the diff touches no packages/** source; pnpm lint (the repo-level eslint) is CI's run, the edited script was linted directly.

维护者速读(草稿)

改了什么 — 三处一致的规则:fork 上来的 PR(head 仓 ≠ base 仓,含 fork 已删的情况)是提案不是交付。① check-governed-merges.mjs 在「路径受管」「改动超 5000 行」之外加了第三个判据「head 是 fork」,命中即走既有的「人合」出口,并打印自己的理由句;--pr 同时读 head 上的 check run 数,0 条时打印「NOT MEASURED,不是绿」。② AGENTS.md 第 14 条基本原则加一句(行数不变,用 Skills 节里一句重复表述抵付)。③ PM 技能的落地条款加一行,核心条款一行原地改写承接,并新增一页 external-contributions.md 写清四步:先立卡、人裁问题、席位采纳 diff 内部落地、fork 的 CI 席位不代批。供应链条款按裁决延后。

为什么改 — 您 2026-09-21 裁了「fork PR 的处理同意你的意见」。之前合并前防线只看改了哪些文件、只约束我们自己的席位,#19342 那样的外部 PR(OAuth 放宽到明文 HTTP,0 条 CI)按机器判定可走普通队列;现在它被机器点名为提案,任何席位都不能放行。

风险与代价(含回滚) — 代价:每张外部 PR 都要您(或授权批准账户)拍板,今天 100 张里 1 张;--pr 多一次 API 读。行为变化:--test / --branch 的输出多一行「head 仓未测」提示,不改退出码。回滚:revert 本 PR 一个 commit 即可,无数据、无迁移。

席位意见 — (留空,席位定稿时填)

你要做的 — 点合并(Tier H:本 PR 改了 AGENTS.md,只由您的手落地)。#19342 本身按同一批裁决由总监席 / 分诊席处置,不在本 PR 内。

Acceptance notes

  • references/external-contributions.md carries no CEILINGS row in check-skill-line-ratchet.mjs (the ratchet does not require one and the script is outside this card's surface), so the new file is invisible to the line ratchet until pinned; a first pin at the landed count (16) is not a raise. carrier: the domain:skills seat.
  • --pr N now spends one extra GET per run (/commits/{sha}/check-runs?per_page=1) on the chosen channel; a failed read is carried as NOT READ (reason) and never a zero.
  • --test / --branch stdout gained the head repo: NOT MEASURED line under every verdict — deliberate, mirrors the size limb; a grep reader keyed on NOT MEASURED alone now matches on every --test run without size flags too (it already did for the size line).
  • The seat's decision analysis left three confidence gaps open (pull_request_target in four workflow files, the MCP merge route against a fork head, fork PR history beyond the newest 100); none is on this card's surface and none was measured here. carrier: the domain:skills seat.
  • PR feat(auth): allow explicit intranet HTTP OAuth #19342 is not addressed here; it remains open for the director's / triage's disposal.

Generated by Claude Code

…ate beside PATH and SIZE, and the charter's 外部贡献 section

Ruled: batch 207 item 1, letter 是 (maintainer, verbatim: 「fork PR 的处理同意你的意见。」;
the director's record on the card carries the four-step intake). A pull request whose head
lives in another repository — head.repo.full_name differs from base.repo.full_name, or
head.repo is null because the fork was deleted (fail closed) — is a proposal, never a
delivery, whatever its paths: no AI seat flips it ready, enqueues it, arms auto-merge on it
or approves it; the owning seat adopts the diff onto an internal branch (Co-authored-by: the
contributor) and lands that.

scripts/pm/check-governed-merges.mjs (+80 net incl. self-tests): a third limb beside PATH and
SIZE. `forkVerdict(pull)` reads the pair off the PR object `--pr` already fetches;
`testVerdict` carries `fork` and `checks`; `landsByHumanMerge` fires on any limb, so the
fork exits on the existing GOVERNED code through the Tier H terminal with its own reason
sentence. `--pr` also reads the head's check-run count (one GET on the chosen channel): a
head with ZERO check runs prints NOT MEASURED — never green — in the register the size limb
already uses; `--test` / `--branch` cannot see a head repo and say NOT MEASURED there too.
Self-test battery beside the SIZE cases: fork head; deleted fork repo; same-repo head on
ordinary and on governed (H and S) paths unchanged; zero / non-zero / unread check runs;
the readings off an injected `--pr` fetch.

AGENTS.md (1109 → 1109): one sentence in Prime Directive 14's paragraph; paid by retiring
the Skills section's restatement of the register and its tiers, whose home is that
directive (lines 259–275). SKILL.md (+1, 815 → 816 / 819): one rule line in 入队与落地.
references/core-rules.md (151 → 151): the landing-rule line rewritten in place to carry
the twin. New references/external-contributions.md (16 lines, every line ≤ 120 bytes): the
ruled 外部贡献 section — card first, the human decides the problem, the seat adopts the
diff, a fork's CI is never approved by a seat, and the supply-chain clause deferred.

Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0a50588f76051acfa3bc27eb2aa88e16c55682d6

① Derived judgments

② Semver level

None — nothing published; skip-changeset on the PR is the correct declaration. Judged correct.

③ Boundary flags

  • Behaviour change declared by the dev and accepted: --test / --branch stdout now carry a 「head repo: NOT MEASURED」 line under every verdict (the size limb's precedent); exit codes unchanged.
  • references/external-contributions.md has no CEILINGS row (the ratchet enumerates only references/lanes/ for uncovered files) — a first pin at the landed 16 rides the seat's next check-skill-line-ratchet.mjs touch; noted, not a card.
  • The decision analysis's three confidence gaps (pull_request_target in four workflow files; the MCP merge route against a fork head; fork history beyond the newest 100) stay unmeasured — recorded on the seat post as seat items.
  • Tier H: this PR is ACCEPTED by the seat and stays DRAFT for the maintainer's hand; ⛔ no ready, no enqueue, no auto-merge by any seat. The live specimen PR feat(auth): allow explicit intranet HTTP OAuth #19342 is disposed of under the ruling by the director / triage — untouched here, as instructed.

Implemented-by: claude/issue-19470-fork-pr-is-a-proposal
Reviewed-by: session_017ETYWqMQD4qMtZzAGovWNi

VERDICT: PASS


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 21, 2026 08:09
@os-zhuang
os-zhuang requested a review from hotlong as a code owner September 21, 2026 08:09
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 21, 2026
Merged via the queue into main with commit 88920d1 Sep 21, 2026
37 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-19470-fork-pr-is-a-proposal branch September 21, 2026 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants