Skip to content

tooling(pm): .github/workflows/** joins the governed register, scoped to objectstack, objectui and cloud by a per-row repository key (#23048) - #23129

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-23048-workflows-governed
Oct 13, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/issue-23048-workflows-governed

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23048
Clause-②: no

Ruled B2 (ruling record 6135970276, maintainer 「同意」 2026-10-13T01:17Z, on the decision analysis 6134774406): the governed register gains a per-row repository scope, and .github/workflows/** joins it as a Tier H row scoped to objectstack, objectui and cloud. objectos and hotcrm are not governed by the row.

What changed

scripts/pm/check-governed-merges.mjs — the register and its matchers

  • GOVERNED_SURFACES rows may carry a repos key naming the GOVERNED_REPOS ids they govern; a row with none governs every repository (the default). The six existing rows are untouched and keep their tiers. The new row: { id: 'workflows', prefix: '.github/workflows/', glob: '.github/workflows/**', tier: GOVERNED_TIER_H, repos: ['objectstack', 'objectui', 'cloud'], what: 'CI, deploy and release workflows: …' }.
  • governedPathsIn(paths, repo), governedTierFor(paths, repo), testVerdict(paths, { repo, … }) and classifyCommit(commit, paths, repo, size) REQUIRE the repository and refuse a call without one (assertGovernedRepo throws a TypeError naming the caller, the ids the register knows and the fix). Never a silent "all": a scoped row answers differently per repository.
  • New helpers: repoIdForSlug(slug), surfaceRepos(row), governedSurfacesFor(repo), checkoutRepoFor(origin, mode), checkoutRepoNote.
  • Where each CLI mode gets its repository: the sweep iterates GOVERNED_REPOS (unchanged); --pr owner/repo#N resolves the repository it names, and a slug outside the register is refused (exit 1) rather than answered "not governed"; --test and --branch answer for the checkout at --root — its origin resolved to a register id, the same "this checkout's own repository" a bare --pr N already means — and print ℹ️ judged for REPO (SLUG, the checkout at --root) on stderr. A checkout whose origin is not a governed repository is refused. No --repo flag: the closed-argv ruling in the file's header keeps one spelling, and --root already exists.
  • The verdict carries repo; surfacesChecked counts the rows that govern it; the head line reads (7 surfaces govern objectstack) instead of (6 surfaces, repo-agnostic). The register docblock's "repo-agnostic" sentence is rewritten (default every repository, a row may be scoped), and a header section records the ruling.
  • Self-test: a new battery "the per-row repository scope (ruled B2, 2026-10-13)" (23 cases; roster floor 32 → 33) pins the six default rows, the workflows row's exact scope, scope ⊆ roster, objectstack/objectui/cloud → GOVERNED Tier H, objectos/hotcrm → NOT governed (the default rows still govern them), near misses (.github/ISSUE_TEMPLATE/**, .github/workflowsx/, packages/x/.github/workflows/), surfacesChecked 7 vs 6, every front door refusing a missing or unknown repository with the caller named, repoIdForSlug, checkoutRepoFor in both directions and the head words. The existing register pins move with the row (seven ids in order; Tier H list gains workflows). The two CLI fixture repositories gain an origin naming objectstack, which is what --test/--branch now read.

scripts/pm/check-governed-queue-guard.mjs — every leg passes its repository

  • guardRepository(env) resolves the repository the workflow runs in from GITHUB_REPOSITORY (unset = this file's own); a repository the register does not know is EXIT_CANNOT_RUN with the roster named. main() resolves it BEFORE the diff is read and threads it through liftGeneratedExceptions, runGuard and runSizeGuard; the reader's slug is the same reading.
  • decomposeGovernedWork(rows, repo), guardVerdict({ repo, … }), runGuard({ repo, … }), sizeReading(pr, size, repo, approval), sizeGuardVerdict({ repo, … }), runSizeGuard({ repo, … }), liftGeneratedExceptions(root, base, rows, notes, recompute, repo) — all refuse a missing repository. Verdict objects carry repo; the CLEAR words say (7 surfaces govern objectstack).
  • Self-test: a new battery "the per-row repository scope (ruling B2, 2026-10-13)" (15 cases; floor 24 → 25): a workflow path in this queue decomposes to one Tier H entry and is REFUSED unapproved / cleared on an authorized approval; the same path in objectui and cloud is governed and in objectos/hotcrm is not; the sibling .github/ paths are clear; every builder, both orchestrators and the lift reject without a repository; guardRepository in all three directions. The source-text pin on the size leg now reads testVerdict([], { repo, size, approval }).
  • The guard has NO --test mode (it reads the event payload only); the card's --test .github/workflows/lint.yml reading is the merges predicate's, below.

scripts/pm/check-half-states.mjs — the patrol binds its swept board

  • loadGovernedRegister(load, slug = OWNER_REPO) resolves the swept board's register id through repoIdForSlug and binds the matcher once (matcher(paths) = governedPathsIn(paths, repo)), exposing repo; a board outside GOVERNED_REPOS is available: false with the board named (NOT MEASURED on H43/H48), never judged against another repository's rows. The H43/H48 call sites are unchanged. Four new self-test cases drive the loader with explicit boards (cloud → governed, objectos → not, foreign → NOT MEASURED, and this run's binding equals its board).

scripts/pm/check-governed-prose.mjs — demands the globs of the rows that govern SELF_REPO_ID (governedSurfacesFor), since the instruction files it reads are this repository's; the success line names the repository.

scripts/pm/record-recognisers.mjs — its cross-tool pin drives the guard for this repository (GUARD_REPO, read from the register's SELF_REPO_ID); two call sites. Not on the claim's declared surface: it is a caller of the changed signature, so it is on the ruling's "(3) every caller passes its repository".

Prose regions touched (the card's acceptance item)

  • AGENTS.md rule 14, lines 266–268 (the enumeration check:pm-governed-prose pins): .github/workflows/** is named as a code span; paid by deleting "the file you are reading is one" → "this file is one" and "on a single path hit" → "on one path hit"; "The" moved to the next line. Same eight lines, no line bought.
  • AGENTS.md rule 14, line 279 (the Tier H parenthetical): (人合: every register row except .claude/**) — the explicit Tier H list is replaced by its complement so that the register, not this paragraph, holds the tier column (Tier S is already stated as all of .claude/** on line 282–283).
  • .claude/skills/pm-dispatch/SKILL.md line 184 「governed 面统一定义」: .github/workflows/** appended; paid by removing the spaces around + (「(全量)」 kept). 120 bytes, at the cap.
  • .claude/skills/pm-dispatch/SKILL.md line 185 (the adjacent line of the same definition): 「GOVERNED_REPOS 各仓同治理待遇」 → 「GOVERNED_REPOS 同治理、行可限仓」, so the skill no longer says every repository receives every row; paid by removing the spaces around +. 120 bytes. In-place fix under the four conditions (same defect class as the card, mechanical, no other claim holds the file, same gate family).
  • Line budget: AGENTS.md 1116 → 1116 lines; SKILL.md 319 → 319 lines (both at their ceilings). Edited-line widths (content bytes): AGENTS.md 266: 117 → 114; 267: 116 → 116; 268: 116 → 120; 279: 117 → 81. SKILL.md 184: 102 → 120; 185: 119 → 120.
  • Not touched: references/lanes/skills.md:17, references/state-machine.md:9, scripts/invoked-as.mjs:28 — their --test calls keep working because --test answers for the checkout it runs in. scripts/pm/dispatch-gates*.mjs (frozen) enumerate no surfaces; no workflow file is edited.

Acceptance readings (all on bd5ad0e)

  • node scripts/pm/check-governed-merges.mjs --pr 23043 → exit 3, governed-surface predicate: 1 of 1 path(s) hit the register (7 surfaces govern objectstack). / ⛔ GOVERNED / ⚖️ landing tier: H(人合) / .github/workflows/** ×1.
  • node scripts/pm/check-governed-merges.mjs --test .github/workflows/lint.yml (from this checkout) → exit 3, GOVERNED, Tier H; stderr ℹ️ judged for objectstack (objectstack-ai/objectstack, the checkout at --root).
  • Same --test with --root at a checkout whose origin is objectstack-ai/objectos → exit 0, 0 of 1 path(s) hit the register (6 surfaces govern objectos), ✅ NOT governed; at objectstack-ai/cloud → exit 3, GOVERNED Tier H, (7 surfaces govern cloud); at /home/user/objectui (origin objectstack-ai/objectui) → exit 3, GOVERNED; at a checkout with origin someone/fork → exit 1, ❌ --test answers for the repository the checkout at --root belongs to … names someone/fork, which is not a governed repository.
  • governedPathsIn(['.github/workflows/lint.yml']) with no repository → TypeError: governedPathsIn wants the repository the paths belong to, as a GOVERNED_REPOS id (objectstack, objectui, cloud, objectos, hotcrm); it got null … (pinned in the self-test).
  • pnpm check:pm-governed-prose → exit 0: ✓ check-governed-prose: 2 instruction surface(s) name all 7 registered governed surfaces that govern objectstack (docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md · docs/NORTH-STAR.md · .github/workflows/**) and claim no others.
  • node scripts/pm/check-governed-queue-guard.mjs --self-test → exit 0, 312 cases pass. pnpm check:pm-governed-merges → exit 0, 500 assertions. pnpm check:pm-half-states → exit 0, 4916 cases. pnpm check:pm-record-recognisers → exit 0, 136 cases. pnpm check:pm-skill-ratchet → exit 0.

Gates

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths, the real changeset at bd5ad0e) derives 54 families: the dispatch's 53 plus pnpm check:pm-record-recognisers. All 54 ran alone in the foreground, exit codes read from the shell status; all exit 0 (pnpm --filter @objectstack/lint run check:doc-formula-expressions first answered exit 3 PREREQUISITE NOT MET — the lint and formula packages were unbuilt — and exit 0 after turbo run build --filter=@objectstack/formula --filter=@objectstack/lint under the shared verification lock; pnpm check:pm-dispatch-gates ran via nohup and was waited for: ✓ dispatch-gates self-test (fast tier): 1833 cases pass; 6 slow section(s) deferred).
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RAN_FILE (lines annotated :: exit N) → ✓ dispatch-gates --ran: 54 derived famil(ies) accounted for — 54 run, 0 NOT-MEASURED (a DERIVED zero — all 54 recorded an exit code and none of them is 3).
  • No published package is touched (repo-root scripts/pm/** and governed prose), so skip-changeset is the declaration; no changeset.

Reverse verification

Ablation on the committed tree through scripts/ablation-replace.mjs: deleting repos: Object.freeze(['objectstack', 'objectui', 'cloud']), from the workflows row (anchor x1 → x0, blob ad718b3a906d → 46ec4a7ba233) turned the merges self-test red with 5 failures — the-workflows-row-is-scoped-to-objectstack-objectui-and-cloud-exactly, an-objectos-workflow-path-is-NOT-governed, a-hotcrm-workflow-path-is-NOT-governed, surfacesChecked-counts-the-rows-that-govern-the-repository-not-the-table, the-verdict-head-names-the-repository-and-counts-its-rows (7 surfaces govern objectos). Restored by git checkout HEAD -- …: blob == HEAD (ad718b3a906d), git diff HEAD empty, anchor x1 again. Direction observed: red, as expected.

维护者速读(草稿)

改了什么:治理面登记表每一行可以标注「适用于哪些仓库」,没标的行默认五仓通用(六条旧行一字未动);新增一行把 .github/workflows/**(CI、部署、发布工作流)列为 Tier H 受管面,只覆盖 objectstack、objectui、cloud。读登记表的每个工具(落地前判定、队列守卫、半态巡检、规则文本校验)现在都必须说清自己问的是哪个仓,不说就报错,绝不默认「全部」。AGENTS.md 规则 14 与 pm-dispatch 技能的「governed 面统一定义」行同行改写,两份文件行数不变。

为什么改:您裁的「两仓加治理行」要求两个公开仓的工作流改动先等授权批准;原登记表对五仓一视同仁,直接加行会把 objectos、hotcrm 也管进去,而那两个仓没人裁过。B2 让表上写的恰好等于裁定的范围,cloud 同列是因为 cloud 自己的治理表已有同一行,中央审计与之一致。

风险与代价(含回滚):工作流 PR 从此在三仓都要等授权批准(本轮十个 CI PR 实际已如此),objectos、hotcrm 照旧绿灯即落。工具内部签名加了必填的仓参数,漏传即响亮报错而非静默判错;五个脚本的自测与 54 个门禁族全绿,反向消融证实自测能变红。回滚 = revert 本 PR 一个 commit 序列;登记表无持久化状态。

席位意见:(留空,由席位定稿)

你要做的:本 PR 含 AGENTS.md(Tier H)与 .claude/**(Tier S),保持 draft;请以 GOVERNED_APPROVERS 账号给出一次 APPROVED review,之后由认领席位落地。回一个字即可。

Acceptance notes

  • The queue guard has no --test flag (confirmed: it reads GITHUB_EVENT_PATH and exits 1 without it); the card's --test reading belongs to check-governed-merges.mjs, where it answers GOVERNED, and the guard's own battery pins the workflow case in its queue.
  • The PM's suggested --repo ID override on --test/--branch was not taken: the file's closed-argv section records a ruling that there is no --repo (one spelling, --pr owner/repo#N), and --root already names the checkout. The checkout's origin is the repository, printed on stderr.
  • scripts/pm/record-recognisers.mjs was edited beyond the claim's declared surface (two call sites of the changed signature); declared here and in the report.
  • Observation, not filed: .github/workflows/governed-surface-guard.yml has no paths: filter by design (its header says so), so no workflow edit was needed; dispatch-gates.mjs mentions a register row only in a comment. check-governed-merges.mjs's parsePullTarget still accepts any owner/repo#N; --pr now refuses a slug outside the register before any read.
  • Harness note: the harness-written Co-Authored-By trailer carrying a model name was refused by the pre-push hook on the first push; the commits carry the repository's model-free pair.

Generated by Claude Code

….github/workflows/** joins it for objectstack, objectui and cloud

Ruled B2: GOVERNED_SURFACES rows may carry a repos key (default: every
repository in GOVERNED_REPOS; the six existing rows untouched), and the
matchers — governedPathsIn, governedTierFor, testVerdict, classifyCommit —
take the repository and refuse a call without one. --test and --branch
answer for the checkout at --root; --pr for the repository it names.

Claude-Session: https://claude.ai/code/session_012dGTuJen61ECNHXYXKChoC
Co-authored-by: Claude <noreply@anthropic.com>
…ory; the prose names .github/workflows/**

The queue guard resolves the repository its workflow runs in and threads
it through every leg (guardRepository); the half-states patrol binds its
swept board at load; the prose checker demands the rows that govern this
repository; record-recognisers' pin names the queue it drives. AGENTS.md
rule 14 and the pm-dispatch skill's governed-definition line are rewritten
on their own lines, both files at their line ceilings.

Claude-Session: https://claude.ai/code/session_012dGTuJen61ECNHXYXKChoC
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation labels Oct 13, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 13, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bd5ad0e95aa8fae8a659796289b7d247aa917bad
Local-runs: none

Inputs: card #23048 (body and all eight comments, among them the decision analysis 6134774406, the ruling record 6135970276 with execution parameters (1)–(6) and the fork clause, the claim 6136107596 and the dev report 6136862799); PR #23129's body, file list and the net diff origin/main...bd5ad0e95 (7 files, +585/−209); the check-runs on the head; reads of origin/main and the head with git show / git grep. Nothing was built, run or re-run locally.

Check-runs on the head, read after the last shard completed (02:39Z): 45 runs — 34 success, 11 skipped, 0 failure, 0 in progress. The skips are the paths-filtered lanes (Build Core, Build Docs, Console Pin Gate, the dogfood and temporal lanes gated on filter.outputs.core, the opt-in tarball smoke) and the duplicate labelled-event runs of Auto Label / Check PR Size / Check Changeset — none is this diff's failure; Test Core ran on crosspkg. Of the five required contexts, four are success and Build Core is skipped by its filter.outputs.core gate (no path under packages/** in the diff) — the by-design skip shape, not a red. Of note: Governed Surface Queue Guard ran the head's own guard code with GITHUB_REPOSITORY set and cleared on the pull_request leg — a live reading of guardRepository plus the threaded repository.

① Derived judgments

The ruling's six execution steps and the fork clause (6135970276):

  • (1) the scope key, default every repository, six rows untouched — RIGHT. repos on the row; surfaceRepos is the one reader of the default (a row with no key governs every id in GOVERNED_REPOS); the six existing rows are byte-identical in the diff and keep their tiers; pinned by the-six-pre-existing-rows-declare-no-scope-and-so-govern-every-repository and every-declared-scope-is-a-non-empty-duplicate-free-subset-of-GOVERNED_REPOS-ids (so a repos: [] row, which would govern nothing, is refused by the self-test).
  • (2) the matcher refuses a call without a repository, loudly — RIGHT. assertGovernedRepo throws a TypeError naming the caller, the roster and the fix, on a missing id (undefined, null, '') and on an unknown one (a slug is refused with a pointer to repoIdForSlug); every front door asserts: governedPathsIn, governedTierFor, testVerdict, classifyCommit, governedSurfacesFor. Pinned in both directions, and the dev's ablation of the missing-repository branch turned the battery red.
  • (3) every caller passes its repository, no silent default to every repository anywhere — RIGHT. Enumerated by git grep on the head, not from the report. merges: the sweep hands classifyCommit the GOVERNED_REPOS entry it iterates (line 4174); --pr resolves the slug it names and refuses one outside the register before any read; --test / --branch resolve the checkout at --root (checkoutRepoFor) and refuse a fork or no-origin checkout; emitVerdict's default is undefined, which testVerdict refuses. queue guard: main() resolves guardRepository(process.env) BEFORE the diff is read and threads it through liftGeneratedExceptions, runGuard, runSizeGuard; decomposeGovernedWork, guardVerdict, sizeReading, sizeGuardVerdict all assert; the reader's slug is the same reading. half-states: loadGovernedRegister(load, slug = OWNER_REPO) binds the matcher ONCE to the swept board (resolveSweepRepo(process.env)), and a board outside the roster is available: false with the board named — NOT MEASURED, never judged against another repository's rows. prose gate: governedSurfacesFor(SELF_REPO_ID). record-recognisers: both call sites. No other caller exists on the head: dispatch-gates*.mjs import only GOVERNED_REPOS, SELF_REPO_ID, sizeVerdict, parseNumstat and own a read-census roster their header says is not a copy of the register; fleet-write/* read GOVERNED_REPOS for slugs; no hook, workflow or package script spawns --test. The only defaults left are to ONE named repository, each printed or carried on the verdict: the guard's "this file's own" when GITHUB_REPOSITORY is unset (a local run; Actions always sets it) and the patrol's DEFAULT_SWEEP_REPO. Neither is "all".
  • (4) the workflows row scoped to objectstack, objectui and cloud — RIGHT. prefix: '.github/workflows/', glob: '.github/workflows/**', Tier H, repos exactly the three, cloud's table wording; pinned exactly, and the dev's ablation of the repos key turned five cases red.
  • (5) the named self-tests — RIGHT. an-objectos-workflow-path-is-NOT-governed, a-cloud-workflow-path-is-governed-Tier-H, ⛔ no-repository-is-an-ERROR-never-an-answer-for-every-repository are all in the merges battery, with hotcrm, objectui, the near misses (.github/ISSUE_TEMPLATE/**, .github/workflowsx/, packages/x/.github/workflows/), surfacesChecked 7 vs 6, and the verdict carrying repo; the guard's battery adds the queue-level case (an unapproved workflow PR REFUSED at merge_group on the Tier H terminal, cleared on an authorized head approval) and every builder rejecting without a repository. Floors: battery pins 20 / 14 against 23 / 15 registered cases; rosters 32→33, 24→25.
  • (6) the docblock rewrite — WRONG BY OMISSION. This is the FAIL. The GOVERNED_SURFACES docblock (the sentence the decision analysis cited at old lines 1126–1127) is rewritten as ruled, and so are the testVerdict docblock, the --test head line and the parsePullTarget docblock. But the same file's header — the authority on detail, per AGENTS.md — keeps, in its section "The governed REPOS" (head lines 543–546): "So the surface register above is REPO-AGNOSTIC: the same five surface globs are governed in objectstack, objectui, cloud, objectos and hotcrm, and this sweep covers all five repos in one invocation regardless of the working directory it is run from." Present tense, capitalised, stating exactly the claim B2 overturned, 420 lines below the new header section that states the opposite; and "regardless of the working directory" is now false for --test / --branch, which this very PR makes answer for the checkout at --root. The same header's tier table (lines 417–419) enumerates Tier H as "docs/adr/**, docs/NORTH-STAR.md, skills/**, AGENTS.md, CLAUDE.md … Unchanged." and omits the new Tier H row the self-test pins (Tier-H-is-the-law-…-and-the-workflows-row). On origin/main the file had five non-test "repo-agnostic" sites; the dev rewrote the four lowercase ones and missed the capitalised one. An AI seat reading the header gets the pre-ruling answer. Fix (two docblock edits, no behaviour): rewrite 543–546 to the ruled shape (default every repository, a row may be scoped; the sweep still covers all five; --test / --branch answer for the checkout at --root), and name the workflows row in the Tier H line at 417–419 or replace that list with the complement rule 14 now uses.
  • The fork clause — RIGHT. No caller was left unable to know its repository, so the clause did not fire and nothing defaulted to "all". --test / --branch take the checkout's remote.origin.url through slugFromRemote — the identical reading a bare --pr N already makes (lines 3723–3724) — and refuse when it parses to no governed repository.

Fail-OPEN check — none found for objectstack, objectui or cloud. Every matcher refuses without a repository; --pr refuses an unknown slug (exit 1, roster named); --test / --branch refuse an unknown or missing origin; the guard refuses an unknown GITHUB_REPOSITORY with EXIT_CANNOT_RUN; the patrol marks a foreign board NOT MEASURED; the generated-artifact lift asserts before it lifts; an unscoped row still governs every repository (pinned: AGENTS.md, ADRs and .claude/** still govern objectos and hotcrm). One misuse surface worth naming, not fail-open by the brief's definition: --test run from an objectos or hotcrm checkout against another repository's file list answers for that checkout (exit 0 on a workflow path). The stderr note names the repository judged and the --root remedy; the stale header sentence above is what would invite the misuse.

The governed prose, true against the register after the change:

  • AGENTS.md rule 14, lines 266–268 — TRUE. The seven globs that govern objectstack are named as code spans and no other; check:pm-governed-prose reads the whole directive as its region and is green. The two cuts that paid for the glob ("the file you are reading is one" → "this file is one"; "on a single path hit" → "on one path hit") lose no meaning.
  • AGENTS.md line 279, the Tier H parenthetical "(人合: every register row except .claude/**)" — TRUE today (Tier S is exactly the claude-tree row; the other six are H) and derivable from the rule alone: the enumeration three sentences up minus .claude/**. It says neither less nor more; what it trades away is self-containment, and the prose gate pins globs, not tiers, so a future Tier S row outside .claude/** would silently falsify it — where the explicit list would silently under-claim. Under the byte budget (the line was 117 bytes; the explicit list plus the seventh glob exceeds 120 and both files are at their line ceilings) the complement form is the right choice, and it matches what SKILL.md:204 and os-dev.md:286 already say. Listed for the maintainer below (dev deviation 7).
  • .claude/skills/pm-dispatch/SKILL.md line 184 — TRUE; paid by dropping the spaces around +, 「(全量)」 kept, 120 bytes.
  • SKILL.md line 185 — RIGHT to rewrite, and in scope: 「GOVERNED_REPOS 各仓同治理待遇」 (every repository receives the same treatment) became false the moment one row is scoped; 「同治理、行可限仓」 states the new shape without over- or under-claiming. The in-place conditions (review-checklist.md:25, os-dev.md:68) hold: same defect class (prose against the register), mechanical, no other claim on the file, same gate family. Terse, not misreadable.

The dev's deviations:

  • record-recognisers.mjs edited beyond the claimed surface — RIGHT: it calls the re-exported governedTierFor and runGuard, whose signatures changed; it is a caller under (3), and check:pm-record-recognisers would throw without the edit. Declared in the body and the report.
  • SKILL.md line 185 rewritten — RIGHT (above).
  • --root instead of a --repo flag — RIGHT: the file's closed-argv section records "There is no --repo here, by ruling — one spelling"; a flag would reopen the argv with a second spelling, while --root already exists and its origin is the reading a bare --pr N makes. It is a behaviour change to a seat-facing predicate (a fork or no-origin checkout now refuses --test where it used to answer), in the fail-closed direction; listed for the maintainer.
  • The queue guard has no --test — RIGHT: confirmed by grep (no --test parsing; it reads GITHUB_EVENT_PATH only). The card's acceptance line conflated the two tools; the merges predicate answers GOVERNED Tier H and the guard's own battery pins the workflow case at the queue. Inventing a flag would be scope.

Also judged: the --test head line now reads "(7 surfaces govern objectstack)" and the --json verdict gains repo — no consumer on the head parses the old line or surfacesChecked (git grep), so nothing downstream breaks; check-governed-prose demanding only the rows that govern SELF_REPO_ID is right (this repository's prose must not claim a surface governed only elsewhere); the three untouched --test doc sites (references/lanes/skills.md:17, references/state-machine.md:9, scripts/invoked-as.mjs:28) stay true because a seat runs the predicate in the checkout of the PR's repository.

② Semver level

skip-changeset matches what the diff publishes: nothing. The seven files are repo-root scripts/pm/**, AGENTS.md and .claude/**; no path under packages/**, no published export, no authorable key. Check Changeset is green. Clause-②: no is right — the changed function signatures are internal tooling contracts, documented in the PR body, not a metadata or API surface.

③ Boundary flags

  • open_questions: none — accepted; the fork clause did not fire.
  • Dev deviations (1)–(4): judged RIGHT above. (5) the harness Co-Authored-By trailer refused by the pre-push hook, the commits carrying the repository's model-free pair — correct. (6) building @objectstack/formula and @objectstack/lint so gate 23 could measure — a prerequisite, not a change. (7) the complement form on line 279 — the maintainer's call, below. (8) writes through the fleet relay, read back — fine.
  • out_of_scope_findings, each correctly out: Add metamodel interfaces for ObjectQL/ObjectUI contract #1 parsePullTarget accepting any slug — verified that --pr refuses a slug outside the register before any read (head lines 3736–3745), so no silent answer exists. ✨ Set up Copilot instructions #2 governed-surface-guard.yml has no paths: filter (verified) and dispatch-gates.mjs names a register row only in a comment table (6857), its read-census rosters (9148–9149) being, by its own header, not a copy of the register. Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3 AGENTS.md has 14 lines over 120 bytes (table rows); the ratchet governs edited lines of its covered files, and check:pm-skill-ratchet is green.
  • Reviewer's own, not filed (read-only seat), for the owning seat to carry: the pm-dispatch lane table now has one path under two rows — SKILL.md:165 gives domain:skills "governed 面全量(统一定义见「governed 面统一定义」行)", which after line 184 includes .github/workflows/**, while SKILL.md:164 still routes .github/workflows/(接线) to domain:spec by the anchoring exception. Both sentences are now true of a workflow PR; a follow-up card on the lane table, not this PR.
  • Tier H is not lifted by this record: the PR has 0 reviews; it stays a draft until a GOVERNED_APPROVERS account approves, and the record for the fixed head must be rendered first.

What the maintainer must look at (after the fix push and a fresh PASS record):

  1. Rule 14 line 279: the Tier H parenthetical moved from an explicit list to "every register row except .claude/**" under the byte budget — explicit list or complement is your call (dev deviation 7).
  2. check-governed-merges.mjs --test / --branch now answer for the checkout at --root (its origin resolved to a register id, printed on stderr) and refuse a fork or no-origin checkout; there is no --repo flag, per the file's closed-argv ruling.
  3. The lane-table overlap on .github/workflows/** (SKILL.md 164 vs 165) — a follow-up card for the skills seat, if you want workflow PRs routed one way.

Implemented-by: claude/issue-23048-workflows-governed
Reviewed-by: session_012dGTuJen61ECNHXYXKChoC

VERDICT: FAIL


Generated by Claude Code

…o-agnostic; the tier table names the scoped workflows row

Comments only (rework round 1 on the contract review): the 'governed REPOS'
section states the ruled shape — every row governs every repository by
default, a row may be scoped (workflows: objectstack, objectui, cloud), the
sweep still covers all five, --test/--branch answer for the checkout at
--root — and the Tier H line lists the workflows row instead of claiming the
list is unchanged.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 27902d2686fd743747174af36e996e00c7d3e873
Local-runs: none

Inputs: card #23048 (body and all ten comments, among them the decision analysis 6134774406, the ruling record 6135970276 with execution parameters (1)–(6) and the fork clause, the claim 6136107596, the round-1 dev report 6136862799, the rework scope 6137045911 and the rework report 6137254453); PR #23129's body, file list and comments (the FAIL record 6137036198 on the previous head bd5ad0e95); the net diff origin/main...27902d268 (origin/main at 287fd6e5d; merge-base bcebd8409; 7 files, +603/−218, equal to the PR's file list and to the same diff taken against the merge-base, so main has not moved into these files); the compare bd5ad0e95..27902d268; the check-runs on the head; reads of origin/main and the head with git show / git grep. Nothing was built, run or re-run locally.

Check-runs on the head, read after the last one completed (03:01Z): 41 runs — 33 success, 8 skipped, 0 failure, 0 in progress. The skips: Build Core, Build Docs, Console Pin Gate, the dogfood shard, Dogfood Verify CLI and Temporal Conformance are gated on filter.outputs.* or kept off the per-PR events by design; Packed-tarball smoke is opt-in; Check Changeset short-circuits on its job-level if: because the synchronize payload already carried skip-changeset (the author's declared opt-out, judged right in ② below; not a required context). Of the five required contexts, Lint & Repo Gates, TypeScript Type Check, Test Core and Governed Surface Queue Guard are success and Build Core is skipped by filter.outputs.core: none of the seven paths is a core input, and the filter block's own comment records that scripts/pm/ tools are deliberately not — the same by-design shape the previous head showed. Governed Surface Queue Guard again ran the head's own guard code with GITHUB_REPOSITORY set and cleared on the pull_request leg.

① Derived judgments

What this head is. One commit on bd5ad0e95: 27902d268 touches scripts/pm/check-governed-merges.mjs alone, +18/−9; all 27 changed lines (git diff -U0) are of the * docblock shape and all sit inside the file header's comment block (lines 2–971); the file with every comment line stripped is byte-identical between the two heads; the other six files' blob ids are identical. So no behaviour, no self-test and no prose file moved — the rework is comments only, as its scope demanded.

The rework against scope 6137045911, passage by passage, each read against the code at the head:

  • Passage 1, header section "The governed REPOS" (head 547–554) — RIGHT, and TRUE. The overturned sentence ("REPO-AGNOSTIC … regardless of the working directory") is gone. Each clause of its replacement holds: "by default each row governs all five" — surfaceRepos returns every GOVERNED_REPOS id for a row with no repos key, and the roster is exactly objectstack, objectui, cloud, objectos, hotcrm (1381–1386); "a row may be scoped to a subset: the workflows row names objectstack, objectui and cloud" — row 1183, repos exactly those three, Tier H; "header section 'The register is scoped per row'" — exists, line 108, dated as the ruling is; "this sweep still covers all five repos in one invocation, judging each against the rows that govern it" — the sweep loop hands the GOVERNED_REPOS entry it iterates to classifyCommit (4183), which asserts repo.id and answers through testVerdict → governedPathsIn → governedSurfacesFor(repo); "--test and --branch answer for ONE repository, the checkout at --root (checkoutRepoFor), and say which one on stderr" — runTestMode reads remote.origin.url at rootFromArgs, resolves it through checkoutRepoFor, refuses on error and prints checkoutRepoNote on stderr before emitVerdict(…, where.repo) (3675–3689); runBranchMode does the same (3701–3723). The 2026-08-18 quotation and the dated history around it are context lines in the diff — untouched, as the scope required.
  • Passage 2, the tier table's Tier H line (head 417–423) — RIGHT, and TRUE. "every register row except .claude/**" is the register today: six rows carry GOVERNED_TIER_H (adr, skills-catalog, agents-md, claude-md, north-star, workflows) and one carries GOVERNED_TIER_S (claude-tree, .claude/**); the parenthetical "today:" list names all six, the workflows row with its scope; "Unchanged." is gone and replaced by a sentence that separates what the tier ruling left (the landing) from what moved on 2026-10-13 (the list). It is the same complement form rule 14 line 279 now uses, with an explicit enumeration beside it, so a reader gets both.
  • Step 3's re-grep, the one further hit (head 559–560) — RIGHT, and TRUE. "judged the same across repos" is qualified to "still true of every row that declares no repos scope", which is exactly what surfaceRepos makes true.
  • Nothing else — RIGHT. No fourth hunk; no edit outside the header docblock.

Remaining present-tense statements contradicting B2 — NONE. My own case-insensitive re-grep of the whole head file for agnostic, regardless of the (working) directory, same five, all five, in all of, every repo, any of them, from anywhere, wherever, judged the same, unchanged., same for/in/across all/every/each, alike, uniform, identical in/across, all repos, five repos, every governed repo, the same surface/globs/rows, whichever repo/checkout, no matter which/where, independent of the repo/checkout/working, one register, then applies in/to all, governed in all/every, in every (governed) repo, each of the five, across all/every/the five, same treatment/governance, equally, the five globs, the same set/table, and finally working directory / cwd. Every hit is one of: the sweep covering five repositories in one invocation (still true; header lines 6, 16, 34, the reachability and --since-ref sections); the pure testVerdict docblock saying a seat can hand it any repository's file list because it names which one (1626–1628, true — the function takes repo); the closed-argv section (3890–3895, which states the ruled shape); bare --pr reading its own checkout's origin (3996–3997, a reading of one repository, not a claim about all); a 2026-08-31 measurement note about two sweep invocations in one cwd (706); or test names and test comments. repo-agnostic itself: origin/main had 8 sites (5 prose, 3 test pins), the previous head kept exactly the capitalised one the FAIL named, this head has 0.

Adopted from record 6137036198 as readings of unchanged code (the compare touches only comment lines of check-governed-merges.mjs; the six other blobs are identical; I re-read AGENTS.md 263–283 and SKILL.md 183–186 at the head and both files' line counts, 1116 and 319, directly): execution steps (1) the scope key with the six rows untouched, (2) every front door refusing a call without a repository, (3) every caller passing its repository with no silent default to every repository anywhere, (4) the workflows row scoped to the three, (5) the named self-tests — all RIGHT; the fork clause RIGHT (it did not fire); the fail-OPEN check — none found for objectstack, objectui or cloud; the governed prose TRUE against the register (rule 14 lines 266–268 name the seven globs that govern objectstack as code spans and no other, check:pm-governed-prose green on the head; line 279's complement; SKILL.md 184 at 120 bytes with 「(全量)」 kept; SKILL.md 185 「同治理、行可限仓」 right and in scope under the four in-place conditions); the dev's round-1 deviations (record-recognisers.mjs as a caller under (3), --root over a --repo flag per the closed-argv ruling, no --test on the queue guard) RIGHT. Step (6), WRONG BY OMISSION on the previous head, is now RIGHT: the GOVERNED_SURFACES docblock (1165) and the header both state the ruled shape, and the header no longer gives an AI seat the pre-ruling answer.

Accept-set and public-surface changes the whole diff implies, named: for objectstack, objectui and cloud, .github/workflows/** moves from NOT governed to GOVERNED Tier H — RIGHT, the ruling; for objectos and hotcrm the row does not apply and the six default rows still govern them — RIGHT, pinned; every matcher front door, --pr with a slug outside the register, --test/--branch from a fork or no-origin checkout, the queue guard under an unknown GITHUB_REPOSITORY, and the patrol on a foreign board now REFUSE or say NOT MEASURED where they used to answer — all narrowings in the fail-closed direction, RIGHT; surfacesChecked and the --test head line change wording and the --json verdict gains repo — no consumer on the head parses the old form, RIGHT. Public surface: none; no package, export or authorable key is touched.

② Semver level

skip-changeset matches what the diff publishes: nothing. The seven files are repo-root scripts/pm/**, AGENTS.md and .claude/**; no path under packages/**. The rework commit adds no file. Clause-②: no is right — the changed function signatures are internal tooling contracts, documented in the PR body, not a metadata or API surface; the Check Changeset skip above is this label doing its job, not a gate failure.

③ Boundary flags

  • open_questions: none in either dev report — accepted; the fork clause did not fire and the rework raised none.
  • The optional seat edit named in 6137254453 (the body's "Acceptance readings (all on bd5ad0e)" and "Gates" headings naming the previous head) — does NOT matter, and I recommend leaving the body as it is. A reading is a count plus the tree it was taken against; those readings were taken on bd5ad0e95 and say so, which is the honest anchor. The code they measure is byte-identical on this head (six identical blobs; the merges file identical once comment lines are stripped), the rework report carries the 27902d268 re-readings (self-test 500 assertions, 54 families, --ran 54/54), and the check-runs on this head are the gate verdicts of record. Re-labelling the headings to the new sha without re-measuring would misattribute a reading, and a body rewrite re-enters the footer channel rules for no information gained. If the seat wants a trace, one added line stating that the readings stand on 27902d268 is enough.
  • The rework report's "no merge of origin/main; the branch does not conflict" — verified: mergeable_state is clean, and the diff against origin/main equals the diff against the merge-base.
  • Round-1 deviations (1)–(8) and the three out_of_scope_findings: adopted as judged in 6137036198 (RIGHT / correctly out); the rework added none.
  • Tier H is not lifted by this record: the PR has 0 reviews and stays a draft until a GOVERNED_APPROVERS account approves; this record is the contract-tier review of record for the .claude/** hunk on the current head.

What the maintainer must look at (carried forward from 6137036198; all three still stand, none changed by the rework):

  1. Rule 14 line 279: the Tier H parenthetical moved from an explicit list to "every register row except .claude/**" under the byte budget — explicit list or complement is your call (dev deviation 7); the file header's tier table now uses the same complement with the explicit "today:" list beside it.
  2. check-governed-merges.mjs --test / --branch now answer for the checkout at --root (its origin resolved to a register id, printed on stderr) and refuse a fork or no-origin checkout; there is no --repo flag, per the file's closed-argv ruling.
  3. The lane-table overlap on .github/workflows/** (SKILL.md 164 routes it to domain:spec, 165 gives the governed set to domain:skills) — a follow-up card for the skills seat, not this PR.

Implemented-by: claude/issue-23048-workflows-governed
Reviewed-by: session_012dGTuJen61ECNHXYXKChoC

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

PR #23129 · card #23048 · head 27902d268 · seat domain:skills#2 (session_012dGTuJen61ECNHXYXKChoC), written 2026-10-13T03:12Z. The seat checked the dev's draft against the diff it read; this comment is the version to read.

改了什么(按你裁的 B2)

  • 受管面登记表 GOVERNED_SURFACES 的每一行可以带一个「适用仓」栏 repos。不写就是五个仓全管,原有六行一字未改、档位不变。
  • 新增一行 .github/workflows/**,Tier H,只管 objectstack、objectui、cloud。objectos、hotcrm 的工作流改动照旧绿灯即落。
  • 判定函数现在必须告诉它「这是哪个仓」,不告诉就报错。读登记表的每个工具都改成传入自己的仓:合并后审计、队列守卫、半态巡检、规则文本校验、复核记录识别器。
  • AGENTS.md 规则 14 和 pm-dispatch SKILL.md 的「governed 面统一定义」两行写上了新 glob。两份文件都卡在行数上限,新增的字是靠删字付的。

为什么改

  • 本轮已有 10 个 CI PR 实际在等批准,但登记表没有工作流这一行,工具答「不受管」,席位按规则可以绿灯就落。
  • 你裁的是「两仓」,cloud 自己的表已经管工作流;登记表原先对五仓一视同仁,直接加行会把规则带到没人裁过的两个仓。

风险与代价(含回滚)

  • 只改 PM 工具与两份规则文本,不发布任何包,运行时无变化。
  • 代价:objectstack、objectui、cloud 的工作流 PR 从此要等你或 hotlong 的批准才能落地。
  • 回滚:revert 本 PR。
  • 契约复核两轮:第一轮 FAIL,原因是文件头还写着「登记表对所有仓一样」,与 B2 相反;返工只改了注释,第二轮 PASS。CI 全绿。

席位意见

  • 建议批准。六步执行参数逐条落实,分叉条款没有触发(没有任何调用方默认「全部仓」)。dev 故意改坏两处代码,测试都变红,说明测试确实在守这几条规则。
  • 请你在批准时顺带确认三点(复核记录 6137353215 的第③节;不回复即按 PR 现状落地):
    1. 规则 14 里 Tier H 的括号,从逐个列出改成「除 .claude/** 外的每一行」,因为行宽装不下逐个列出。要逐个列出的话说一声,席位照改。
    2. check-governed-merges --test / --branch 现在按 --root 那份检出的仓来判(stderr 上会写明判的是哪个仓);fork 或没有 origin 的检出直接拒绝。文件自己的规定是不许加新参数,所以没有加 --repo。
    3. 合入后,pm-dispatch 的车道表会让工作流 PR 同时落在两条车道:SKILL.md 第 164 行按锚定例外把 .github/workflows/(接线)给 domain:spec,第 165 行把 governed 面全量给 domain:skills。席位会在合入后另立一张卡来理清,不放进本 PR。

你要做的


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 13, 2026 03:15
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit d206d08 Oct 13, 2026
49 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-23048-workflows-governed branch October 13, 2026 03:31
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/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants