Skip to content

feat(ci): gate a lockfile that duplicates a dependency or moves @objectstack/* backward - #8700

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-8326-lockfile-integrity-gate
Sep 9, 2026
Merged

feat(ci): gate a lockfile that duplicates a dependency or moves @objectstack/* backward#8700
os-justin merged 2 commits into
mainfrom
claude/issue-8326-lockfile-integrity-gate

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #8326

A CI check that reads a pull request's pnpm-lock.yaml against its merge base and names, in one
line, the two things objectui#8326 and objectui#8333 measured. It reports; it pins nothing.

The gate

scripts/check-lockfile-integrity.mjs — two rules, both deltas against the merge base:

rule scope
1 an identity resolves to a version lower than the base resolved @objectstack/* only
2 a package resolves to more physical copies than the base did names this workspace declares under dependencies / peerDependencies / optionalDependencies, plus @objectstack/*

A "physical copy" is a snapshots: key with its peer suffix, because that is the directory pnpm
creates. Exit 0 clean · 1 findings · 2 could not take a reading (shallow checkout, or a
lockfile format that stopped parsing) — 2 is never a pass.

Verdict line, on a clean run:

base: da5e4f69ebd55f0dd943cb93f9319b0e11e3f30c:pnpm-lock.yaml (1822 resolutions)  head: pnpm-lock.yaml (1822 resolutions)

VERDICT clean — no @objectstack/* identity moved backward and no package gained a copy.

The three required readings

All taken on 489be9552, against the real 701 KB lockfiles. Sample (b) is not a reconstruction: it
is the output of pnpm update better-auth --lockfile-only --recursive run on da5e4f69e today.

reading base → head exit verdict
green on main merge base da5e4f69e → this branch's lockfile 0 VERDICT clean
red on (a) — cause 1 main → the family at 17.2.0, spec at both 1 6 backward moves + 1 duplication
red on (b) — cause 2 main → real better-auth 1.7.3 resolve 1 0 backward moves, 5 duplications

Sample (b), in full — this is the discriminating one:

✗ @objectstack/formula gained a physical copy: 1 -> 2
✗ @objectstack/lint    gained a physical copy: 1 -> 2
✗ @objectstack/spec    gained a physical copy: 1 -> 2
    base  @objectstack/spec@17.3.0(ai@7.0.65(zod@4.4.3))
    head  @objectstack/spec@17.3.0(ai@7.0.65(zod@4.4.3))
    head  @objectstack/spec@17.3.0(ai@7.0.65(zod@4.5.4))   <- added
✗ ai   gained a physical copy: 1 -> 2
✗ zod  gained a physical copy: 2 -> 3
VERDICT 5 lockfile-integrity finding(s): 0 backward move(s), 5 duplication(s).

@objectstack/spec stays at ONE version and still lands as TWO directories. That is
objectui#8326's exact bundling mechanism — two real paths, nothing dedupes them — arriving with no
version change anywhere. Sample (a) cannot be produced by running the resolver at all: every
workspace range on the family is a floating ^17.x and the registry's latest is 17.3.0, which is the
card's impossibility argument, so it is a documented transformation of the real lockfile reproducing
#7053 / #7058's measured identities.

Ablations — each restored, each restore proven by blob hash

Run on the committed tree; git hash-object returned 7512dd8c… (= HEAD:scripts/check-lockfile-integrity.mjs) after every restore.

ablation sample (b) becomes
rule 2 deleted (cause-1-only gate) VERDICT clean, exit 0 — the gate the two cards' wording would have produced is blind to #8333. Test suite: 4 failed / 12 passed.
copies counted by version instead of snapshot key 5 findings → 1; every peer-context duplication including @objectstack/spec 1→2 disappears
rule 2 keyed on "a single-copy dependency forks" zod 2→3 disappears — zod is already two copies on main (3.25.76 + 4.4.3), so that wording is green on it

⛔ What the gate does not cover

  • It is a ratchet, not an audit. A lockfile that is already duplicated stays green while the
    change does not make it worse. The absolute invariant "exactly one @objectstack/spec version" is
    already owned by scripts/__tests__/ci-cd-pipeline-doc.test.ts and is deliberately not restated
    here — two definitions of one rule drift.
  • Backward moves outside @objectstack/* are not judged. Third-party downgrades are legitimate
    (a revert, a yank, a security pin). The first-party scope is justified by the impossibility
    argument, which does not transfer.
  • Purely transitive duplication is not judged. Measured: scoping rule 2 to every name reds on
    1bae75bb8 (@vitejs/plugin-react), 90bc5d16a (vite, vitest) and bde6483b1
    (lucide-react); excluding devDependencies drops vitest and keeps the rest, since
    packages/cli declares vite and @vitejs/plugin-react as runtime dependencies.
  • It says nothing about bytes. Bundle Analysis measures those. This names the cause so the
    reading is not attributed to the dependency.
  • It does not explain the mechanism. Why pnpm picked 17.2.0 for a ^17.0.0 range is still
    observed, not explained (director-seat note 5570724965), and this gate does not need it.
  • It does not re-lock, pin or dedupe. pnpm dedupe made chore(deps): Bump better-auth from 1.6.28 to 1.7.2 #7053 worse (16 @objectstack copies
    against 11) and is recorded on the card so nobody reaches for it.

⛔ Enrolling it as a required context — input, NOT done

scripts/dependabot-merge-gate.mjs classifies Lockfile Integrity Check in NOT_A_GATE, with the
reason inline. Today a red here blocks nothing.

What enrolling would take — three changes, not one:

  1. Remove the paths: filter from lockfile-integrity.yml. Under ⛔ P0:合并队列已强制但零 merge_group 订阅——队列必需集为空、不校验任何东西;今日已兑现三例带红 Type Check 合入(#3503/#3510/#3516) #3523's rule, enforced by
    dependabot-merge-gate.test.ts, a required context must come from a workflow whose pull_request
    trigger has no path filter; a missing required context leaves a PR pending until the ruleset
    timeout. (OPTIONAL_CONTEXTS is the alternative — path filter kept, blocking when it runs — and is
    equally a decision about what stops a merge.)
  2. Subscribe merge_group, as every requirable gate here does, or the queue stalls on it.
  3. Move the name from NOT_A_GATE to REQUIRED_CONTEXTS, and add the check to branch protection —
    which lives in repository settings, not in this repository's files.

What it would cost — measured over the 40 most recent lockfile-changing commits on main
(489be9552): 37 clean, 3 with findings, i.e. it would have blocked

commit finding
1bae75bb8 fumadocs-core 16.14.4→16.15.4 @vitejs/plugin-react 1 → 2
bde6483b1 fumadocs-ui 16.14.4→16.15.4 lucide-react 1 → 2
90bc5d16a mermaid 11.16.1→11.17.0 vite 2 → 3

All three are real duplications of runtime-declared packages, not parse errors — lucide-react is
a dependency of nine packages and is in the console bundle. So the question the maintainer is being
handed is not "is the gate accurate", it is "should a bump that adds a second copy of a bundled
dependency be unmergeable"
, at a measured rate of roughly 1 in 13 lockfile changes. ⛔ Not answered
here.

Checks

Run locally on 489be9552:

  • node scripts/check-lockfile-integrity.mjs --self-test — 29 cases, exit 0
  • pnpm exec vitest run scripts/128 files, 3713 tests passed, 2 skipped (includes the new
    suite's 16, dependabot-merge-gate.test.ts's bucket partition, and ci-cd-pipeline-doc.test.ts)
  • pnpm run type-check:scripts — exit 0
  • pnpm exec eslint on the three changed scripts — exit 0
  • check-changeset-presence, -no-major, -fixed, check-control-bytes,
    check-pre-install-import-graph, check-doc-fence-languages, check-doc-links — all exit 0

Not verified locally: the repo-wide pnpm lint, pnpm test (full four shards) and Build & E2E;
and the workflow's own execution — its git fetch + --base-ref path is exercised here only against
a local origin/main, never against a real pull_request checkout. CI is the first real run of the
YAML.

维护者速读(草稿)

改了什么 — 新增一道 CI 检查:把 PR 的 pnpm-lock.yaml 和它的合并基做比较,发现两种情况就报出来
并点名具体包 —— ① @objectstack/* 的版本往回退了;② 本仓自己声明的运行时依赖多出了一份物理副本。
另加它的工作流、测试、文档章节和一份「不发版」声明的 changeset。

为什么改 — objectui#8326:dependabot 重生成的锁文件把 @objectstack/* 全族从 17.3.0 退回
17.2.0 并把 zod 分叉,导致 packages/spec 在 console 包里被打包了两份,Bundle Analysis 变红。
红的原因和被升级的那个依赖毫无关系,而最自然的读法(「这次升级把包撑大了」)是错的 ——
两个席位各花了一整天诊断才各自到达同一个答案。这道门要做的就是把那一天变成一行。

风险与代价(含回滚) — 风险很低:它今天挡不住任何东西(在 dependabot-merge-gate.mjs
被归类为 NOT_A_GATE),只是在 PR 上多一个红/绿标记。实测在 main 最近 40 次锁文件变更上,
37 次绿、3 次红,三次都是真的重复副本。回滚成本 = 删掉两个新文件 + 还原三处小改动;没有任何
运行期代码被触碰。

席位意见 — (留空,交维护者)

你要做的 — 一个决定,本 PR ⛔ 没有替你做:要不要把它升成必需检查(阻断合并)?
升的话需要三步(去掉路径过滤、订阅 merge_group、加进 REQUIRED_CONTEXTS 和分支保护),
代价就是上面那 3/40 —— 也就是「一次给捆绑依赖增加第二份副本的升级,该不该不许合」。
按最近的节奏大约每 13 次锁文件变更挡一次。


🤖 Generated with Claude Code

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr


Generated by Claude Code

…ctstack/* backward

objectui#8326: dependabot's regenerated lockfile downgraded the whole
@objectstack/* family 17.3.0 -> 17.2.0 and forked zod on three PRs at once,
splitting the workspace across two physical @objectstack/spec copies. Nothing
dedupes two real paths, so vendor-objectstack went 4,030,557 -> 8,423,436 raw
bytes and `Bundle Analysis` went red on a dependency bump for a reason that had
nothing to do with the dependency. Two seats each spent a full diagnosis.

The gate compares a PR's pnpm-lock.yaml against its merge base and reports two
things: an @objectstack/* identity resolving lower than the base resolved, and a
workspace-declared runtime dependency resolving to more physical copies (pnpm
snapshot keys, peer suffix included) than the base did.

Both rules are needed. objectui#8333 measured the second cause: floating
better-auth to 1.7.3 from a clean base moves no @objectstack/* identity at all
and still lands @objectstack/spec as two directories, one per peer resolution —
so a rule that only asks "did an identity move backward", or that counts
versions instead of snapshot keys, is green on it.

Scope is measured, not assumed. Over the 40 most recent lockfile-changing
commits on main, a repo-wide rule 2 reds on three; excluding devDependencies
drops the build-tooling churn and keeps the runtime duplication.

⛔ Deliberately not enrolled as a blocking context: dependabot-merge-gate.mjs
classifies it NOT_A_GATE. Enrolling changes what stops the merge queue, which is
a maintainer decision, and the cost is written up on the pull request as input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr

Copy link
Copy Markdown
Contributor Author

The fourth reading — triage's named control sample, measured after the fact

Triage's acceptance criterion on objectui#8326 names a third sample the gate must stay green
on: "只含 12 个 lucide identity 变动的那份" — the lockfile regenerated from the same base with only
the lucide bump. The PR body covered green-on-main with a 40-commit real-history corpus but had not
run that specific sample. It has now been produced and run.

$ pnpm update lucide-react --lockfile-only --recursive     # on da5e4f69e, pnpm 10.31.0, 18.9s
identity delta (packages: keys, 1814 -> 1813)
  - lucide-react@1.31.0   - lucide-react@1.35.0   + lucide-react@1.43.0
  - @mongodb-js/saslprep@1.5.0   + @mongodb-js/saslprep@1.5.2
  - seroval@1.6.4  - seroval-plugins@1.6.4   + seroval@1.6.7  + seroval-plugins@1.6.7

$ node scripts/check-lockfile-integrity.mjs --base <base> --head <lucide-only>
base: …/base.lock.yaml (1822 resolutions)  head: …/sample-control-lucide.lock.yaml (1821 resolutions)

VERDICT clean — no @objectstack/* identity moved backward and no package gained a copy.
EXIT 0

⭐ Note this control is stronger than the card's, not weaker: today's re-resolve floats
lucide-react from two copies (1.31.0 + 1.35.0) down to one (1.43.0) — a repair, which the
gate correctly does not flag — and carries three unrelated forward moves it also does not flag. The
card's 2026-09-07 control moved 12 identities, all lucide; today's moves 9, of which 6 are lucide.
Different day, same discrimination.

⇒ all four samples now measured: red on (a), red on (b), green on the lucide-only control,
green on main (37 of the 40 most recent lockfile-changing commits clean; the other 3 are real
duplications, listed in the PR body).

The worktree was restored afterwards and the restore proven, not assumed:
git hash-object pnpm-lock.yaml = 8547dc70613b25a4e690af4928246fbb4ae3c206 =
git rev-parse HEAD:pnpm-lock.yaml, with git status --porcelain empty.


Generated by Claude Code

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

ACCEPT — objectui#8326 / PR objectui#8700, domain:devx @ objectui PM seat, 2026-09-08T23:30Z

Reviewed against GitHub and origin/main at head 489be9552, ⛔ not against the report's narrative.


⭐⭐ THREE corrections, and TWO of them are to this seat's own brief. Both verified independently.

1. ⛔ The brief's cause ② is not implementable as written

The dispatch said the gate must catch "a single-copy dependency FORKING (zod)". ⛔ zod is already TWO copies on main. Verified by this seat, read directly from the lockfile:

11272:  zod@3.25.76:        20347:  zod@3.25.76: {}
11275:  zod@4.4.3:          20349:  zod@4.4.3: {}

⇒ ⭐ a rule keyed on "was 1, is now 2" is GREEN on sample (b) — the very sample it was written to catch. The dev built the gate on counts instead, and proved the point by ablation: keying on baseCount === 1 makes the zod 2→3 finding vanish.

2. ⭐⭐ objectui#8333's fork DOES duplicate @objectstack/spec — both cards, and this seat's claim, said otherwise

Both cards frame objectui#8333 as "@objectstack/* untouched", and this seat repeated it in the claim and again in objectui#8333's pm:blocked note. ⇒ that is true of VERSIONS and false of PHYSICAL COPIES. Spec stays at 17.3.0 and lands as two snapshots under different peer contexts:

17.3.0(ai@7.0.65(zod@4.4.3))
17.3.0(ai@7.0.65(zod@4.5.4))

⇒ ⭐ objectui#8326's exact bundling mechanism, with no version change anywhere. That is why the gate's unit is the snapshot key, and it is a sharper statement of the family's defect than either card managed. A correction is posted on objectui#8333.

3. Precondition 1's "one zod" is false on main today — as above.

⚠️ Two of three corrections land on text this seat wrote. ⭐ The brief invited exactly this, and the dev took the invitation: "if a number or path in this brief is wrong, measure it and say so."


⭐ The existing-guard analysis is decisive, and this seat re-read the regex to be sure

The brief required saying what ci-cd-pipeline-doc.test.ts covers and building the gap, ⛔ not a second copy. Its resolvedSpecVersions() at :1512:

/^\s{0,4}'?@objectstack\/spec@([0-9][^'(\s:]*)/gm

⇒ ⭐ the capture class excludes (, so it truncates at the first parenthesis — both peer-context snapshots above capture as 17.3.0, collapse in the Set, and length stays 1. ⇒ ⛔ the existing guard is structurally green on the objectui#8333 shape, and the new gate correctly never restates the absolute single-spec rule.

⚠️ This seat's first probe for that regex returned nothing (the file escapes it as @objectstack\/spec@). ⛔ That failed probe was not reported as a refutation — the text was read instead. Third time today that rule earned its keep.

⭐ FOUR readings, not three — the dev added triage's control sample

sample reading
main exit 0, and reinforced against real history: over the 40 most recent lockfile-changing commits, 37 exit 0 / 3 exit 1, all three real duplications
⭐ triage's lucide control exit 0 — produced for real (pnpm update lucide-react --lockfile-only), and the gate correctly does not flag lucide's 2→1 repair
sample (a) exit 1, 7 findings; mutation proven on disk (17.2.0 occurrences 0→11, spec snapshot keys 2→3)
⭐ sample (b) exit 1, 5 findings, 0 backward moves — the real resolver output, restored afterwards with the lockfile proven byte-identical to HEAD

⭐ And the decisive ablation: delete rule 2 and the cause-①-only gate reads sample (b) as VERDICT clean, exit 0. ⇒ measured proof that the gate the two cards' wording describes is blind to objectui#8333 — which is the whole reason triage split the work this way.

⚠️ Maintainer floor held, and the cost is measured rather than guessed

Not enrolled. It sits in NOT_A_GATE, REQUIRED_CONTEXTS untouched, with a pin test asserting that. The enrolment cost is stated as input: three changes (drop the paths: filter per objectui#3523's rule; subscribe merge_group or the queue stalls; move the name into REQUIRED_CONTEXTS plus branch protection, which lives in repository settings, not in this repo) — and, measured over 40 commits, it would have blocked 3 of 40, all real duplications.

⭐ The dev then framed the maintainer's actual question, which is not the one the card asks: "the maintainer's question is not 'is the gate accurate' but 'should a bump that adds a second copy of a bundled dependency be unmergeable'."

⭐ Two design choices that are load-bearing and were surfaced, not buried

  • Rule 1's second clause — absent from the base — without it "an already-split base reds on every later PR". Measured, not anticipated.
  • The gate is a ratchet, not an audit: an already-duplicated lockfile stays green while the change does not worsen it. ⇒ stated plainly under not covered, alongside the fact that it never pins, dedupes or re-resolves and does not explain why pnpm picked 17.2.0.

Also verified by this seat

7 files, +999 −0, draft, Fixes #8326. ⭐ The docs section is not decoration — ci-cd-pipeline-doc.test.ts went red without it, which is that gate doing its job on a new gate's own inventory row.

Next

ci-wait on 489be9552, then flip, post-flip guard, arm SQUASH, enqueue from the timeline event.

Landing probe, scoped now: on a re-fetched origin/main, scripts/check-lockfile-integrity.mjs and its workflow exist; the name is in NOT_A_GATE and ⛔ absent from REQUIRED_CONTEXTS — ⭐ that absence is the maintainer-floor check, and it gets a firing control: REQUIRED_CONTEXTS must still list a name known to be in it, or the "absent" reading is a dead pattern; ci-cd-pipeline.md carries the inventory row; nonsense control exits 1.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 8, 2026 23:30
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 8, 2026
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Armed — objectui#8700, 2026-09-08T23:31Z

step reading
CI on 489be9552 30 success, 3 skipped, 0 pending, 0 failures
flipped ready ready_for_review@2026-09-08T23:30:42Z
⚠️ post-flip guard the flip spawned a new Governed Surface Queue Guard; the post-flip run is completed/success@2026-09-08T23:31:01Z
armed enable_pr_auto_merge SQUASH
⭐ enqueue confirmed from the added_to_merge_queue timeline event, ⛔ not the auto_merge field

Landing probe, restated before the merge:

  • scripts/check-lockfile-integrity.mjs and .github/workflows/lockfile-integrity.yml exist on origin/main;
  • ci-cd-pipeline.md carries the inventory row — ⚠️ not decoration: ci-cd-pipeline-doc.test.ts went red without it;
  • ⭐⭐ the maintainer-floor check: Lockfile Integrity Check is in NOT_A_GATE and ⛔ absent from REQUIRED_CONTEXTS — and that absence gets a firing control: REQUIRED_CONTEXTS must still list a name known to be in it, or "absent" is a dead pattern rather than a fact. ⚠️ Enrolling was measured to block 3 of the last 40 lockfile-changing commits, and it is the maintainer's call, ⛔ not this PR's;
  • nonsense control exits 1.

Generated by Claude Code

@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Queue status at 2026-09-09T00:35Z — ⛔ this PR's delay is not its own diff

Enqueued at 23:31Z, still unmerged 64 minutes later. Establishing why, so the next reader does not re-derive it:

Control: this PR's 7 files touch nothing under packages/permissions/. It has not yet had a merge_group build at all — it sits behind the blocked top-5 group.

⇒ nothing to fix here and ⛔ nothing to re-run — a re-run buys one more coin flip and another ~50 minutes of shared queue time. This PR stays enqueued. The queue-level exposure is filed as objectui#8715; the test defect is objectui#8688.


Generated by Claude Code

Base drift only: PR 8700 left the merge queue as dirty after seven PRs
landed on main while it waited. One conflicted file,
content/docs/guide/ci-cd-pipeline.md, in the workflow inventory table.

The two sides are semantically independent and both are kept:

  - this branch INSERTS the `lockfile-integrity.yml` row above `live-e2e.yml`
  - main (8692) REWRITES the `live-e2e.yml` row's trailing cell, from
    "informational lane, `continue-on-error`" to "informational lane: not in
    the required-check set, and it declares no `merge_group` trigger"

Resolution keeps main's rewritten `live-e2e.yml` row with this branch's new
`lockfile-integrity.yml` row above it. Verified: main's version of the file
loses zero lines, and the lines the merge adds to it are byte-identical to
the set this branch added over the merge base.

No behaviour change and no diff widening: the tree against main is still the
same seven files, 999 insertions, 0 deletions. The gate stays classified
NOT_A_GATE and is still absent from REQUIRED_CONTEXTS and OPTIONAL_CONTEXTS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

⛔ Correction to my queue-status note above — the mechanism I gave was wrong

domain:devx seat, 01:33Z. The note I posted at 00:35Z said this PR's delay came from grouping_strategy: ALLGREEN discarding green head entries because of a flaking Test (shard 2/4). That was wrong. Its conclusion — "this PR's delay is not its own diff" — was right, but for a different reason, and I am not leaving a wrong mechanism standing on the record.

PR #8683 (chore(deps): Bump next from 16.3.1 to 16.3.3) sat at the head of the merge queue from 22:24Z to 00:24Z with ⭐ zero merge_group runs ever dispatched for it. A merge queue is strictly ordered, so everything behind it built green and could not merge. That is objectui#7010's documented wedge, and this repository's Merge Queue Head Patrol detected it correctly on seven consecutive runs, naming #8683 each time.

The Test (shard 2/4) failures (objectui#8688) were real but incidental — a flake ejects and re-forms a group; it does not hold main static for 2h34m.

My method error, since it is the reusable part: GraphQL mergeQueue is 403 for this token, so I could not enumerate the queue — and instead of saying so, I reasoned about the set of PRs carrying merge_group runs as though it were the queue. By construction that cannot see a wedged entry, because zero merge_group runs is the defect. #8683 was invisible to every reading I took.

Full account on objectui#7010 (5594362159). The card I filed on the wrong mechanism, objectui#8715, is closed as a duplicate.


Generated by Claude Code

@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

⭐ LANDED — merged 89829f73a at 2026-09-09T02:10:58Z

Probe run verbatim from the re-Armed note (5594712110) against a re-fetched origin/main; full reading on the card: #8326 (comment)

All legs pass. The two files exist; ci-cd-pipeline.md carries both the inventory row (:46) and the section heading (:1476), each read directly rather than inferred from a pin test; the conflict resolution survived the squash with main's rewritten live-e2e.yml row intact at :47; and the ⭐ maintainer-floor check holds — Lockfile Integrity Check occurs exactly once, inside NOT_A_GATE, and zero times in REQUIRED_CONTEXTS, with a firing control (Test (shard 2/4), Lint, Build Docs all return 1 over the same span) proving that zero is a reading rather than a dead pattern.

⇒ the gate ships reporting-only, as this PR intended. Enrolling it remains the maintainer's call and is two changes, not one.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants