You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
'timeline' is registered twice, and the bare-name fallback resolves to the later registration by module-evaluation order #6353
Found while working #6170 (PR #6352); out of that card's scope, filed unassigned.
The fact
packages/plugin-timeline calls ComponentRegistry.register with the key 'timeline'twice:
site
component
namespace
src/renderer.tsx:498
TimelineRenderer (presentational)
plugin-timeline
src/index.tsx:366
ObjectTimelineRenderer (object-bound)
view
Both are namespaced, neither passes skipFallback, so each also claims the baretimeline key (Registry.ts:246-266).
index.tsx:300 is export * from './renderer';, which is requested before the import at line 307, so renderer.tsx evaluates first and claims bare timeline → TimelineRenderer. The call at index.tsx:366 then overwrites it. So a node authored type: 'timeline' resolves to ObjectTimelineRenderer, which delegates inward to TimelineRenderer (ObjectTimeline.tsx:443).
Why it is worth a card
The outcome is probably intended — the object-bound wrapper is the right entry point, and it does reach the presentational renderer. What is not fine is how it is decided: by the order two module requests happen to appear in one file. Reorder those two lines and type: 'timeline' silently becomes the presentational renderer, which reads none of the object-bound keys — an authored timeline would stop fetching, with no error.
Registry.register carries a collision guard for exactly this shape, whose message names the remedy:
Component "timeline" bare-name fallback is being overwritten by "view:timeline". If this is intentional keep going; otherwise register "view:timeline" with { skipFallback: true } so it doesn't claim the bare "timeline" key.
So the repo already believes this needs a deliberate answer. Either declare the intent (and pin it), or give the presentational registration skipFallback: true.
Worth checking whether other plugins have the same double-claim; the guard warns at runtime only, so nothing fails CI today.
Reproduce
grep -n "ComponentRegistry.register" packages/plugin-timeline/src/*.tsx
sed -n '296,310p' packages/plugin-timeline/src/index.tsx
sed -n '242,268p' packages/core/src/registry/Registry.ts
Refs: #6170, PR #6352. Related: #5106 (the doc-types gate never judges the namespace half of a registration row).
PM session session_011SfZeFWrhGLHmfq61xbz4q, branch claude/issue-6353-timeline-double-registration.
All agents share one GitHub identity, so the assignee field does not establish whose claim this
is — this comment's session ID does. An earlier claim carrying a different session ID
outranks the assignee.
Pre-dispatch gate
(a) Whole thread read — one record, triage's grading of 2026-08-25T15:58Z. No prior claim,
no prior branch.
packages/plugin-timeline/src/renderer.tsx:499 — the second claim on the same bare key
Every other 'timeline' hit in the tree is a view-type string, not a component
registration — plugin-list, plugin-view, app-shell view pickers, and the ViewType/UnifiedViewType unions in packages/types. ⚠️ That distinction matters for the
census triage asked for: a 'timeline' occurrence is not evidence of a double-claim, and
grepping the bare string would drown the real signal in ~30 unrelated hits. Census the registration call shape, not the key string.
{
"issue": 6353,
"status": "done",
"branch": "claude/issue-6353-timeline-double-registration",
"pr": "https://github.com/objectstack-ai/objectui/pull/6417",
"premise_still_valid": true,
"summary": "Confirmed the census on origin/main 64d888613 before editing: packages/plugin-timeline/src/renderer.tsx:498 registers 'timeline' as plugin-timeline:timeline (presentational TimelineRenderer) and src/index.tsx:366 registers it as view:timeline (object-bound ObjectTimelineRenderer); neither passed skipFallback, so both claimed the bare key and index.tsx won only because index.tsx re-exports ./renderer at line 300 before its own import at 307. Applied the remedy the registry's own collision guard names (Registry.ts:246-266) rather than a second mechanism: renderer.tsx now registers with skipFallback: true, so bare 'timeline' resolves to ObjectTimelineRenderer by declaration in any evaluation order, while the presentational renderer stays reachable under plugin-timeline:timeline. Resolution is unchanged today. The pin is src/__tests__/timeline-bare-key-ownership.test.ts (5 assertions): it reads BOTH registrations' real declared metadata back out of the registry (nothing hand-copied, so it cannot drift from source) and replays them into a fresh Registry in BOTH orders, asserting the object-bound renderer answers bare 'timeline' either way and that the collision guard stays silent; it also fails if the declaration is dropped or a third registration starts claiming the bare key. Changeset included (patch, @object-ui/plugin-timeline; no major, per the fixed-group rule).",
"tests": "All runs on the final commit's tree, git rev-parse --short HEAD = 48da81623, with git diff HEAD empty at each run; heavy runs via the container's shared verify lock (scripts/pm/os-verify-lock.sh, VERDICT lines read, never a bare $?). PASS: 'pnpm exec vitest run packages/plugin-timeline/src/' from the repo root (the canonical invocation the repo's vitest-invocation-guard demands -- 'pnpm --filter <pkg> test' is the documented false-green trap here) => 'Test Files 10 passed (10)' / 'Tests 73 passed (73)'. PASS: 'pnpm --filter @object-ui/plugin-timeline type-check' exit 0, and its output echoed '> tsc --noEmit && tsc -p tsconfig.test.json', so it was not a zero-match script name; tsconfig.test.json includes src/**/*.test.ts, so the new test file is genuinely in the program rather than excluded. Dependency closure built FIRST ('pnpm --filter @object-ui/plugin-timeline^... build', exit 0). PASS exit 0: check-changeset-presence.mjs, check-changeset-no-major.mjs, check-changeset-fixed.mjs, check-control-bytes.mjs, check-doc-component-types.mjs. ABLATION (ghost-assertion guard): predicted RED before running; mutation = delete 'skipFallback: true' from renderer.tsx (the pre-fix condition). NO BUILD IS INVOLVED in this pin -- the root vitest config aliases @object-ui/* to sibling src/ and both imports are relative source paths, so nothing resolves through dist and no dist preflight applies. Mutation CONFIRMED ON DISK before measuring, not by the editor's exit code: grep -c 'skipFallback: true' went 1 -> 0 and git hash-object moved from the HEAD blob 0ddf84dd9d53896a0a5a3e4a82e4511bf4a9ff09 to 1cc2b14a548d33db21ff25198c0f39f6c516cf0b; script carried trap '<restore>' EXIT INT TERM with an absolute REPO_ROOT-anchored path. Result: 'Test Files 1 failed (1)' / 'Tests 4 failed | 1 passed (5)', e.g. \"registering in the order [view, plugin-timeline] changed who answers bare \\\"timeline\\\": expected [Function TimelineRenderer] to be [Function ObjectTimelineRenderer]\" and \"expected [ 'plugin-timeline:timeline', ...(1) ] to deeply equal [ 'view:timeline' ]\". PREDICTION MISS, recorded rather than smoothed over: I predicted 3 of 5 red and got 4 -- the 'presentational first' replay row also failed, on the collision-warning assertion rather than on resolution, because pre-fix that order resolves correctly AND still warns. The single row that stayed GREEN under the mutation is the live-singleton assertion, which is the concrete proof that a live-outcome assertion alone would have been a ghost assertion here (it passes in both worlds); the replay is what carries the pin. RESTORE proven by state, not exit code: git checkout HEAD -- <absolute path> (never a bare 'git checkout --', which restores from the polluted index), then hash-object == HEAD blob AND 'git diff HEAD' empty; re-run green. DECLARED NARROWING: repo-wide 'pnpm lint' was not run locally (it is CI's run). Diff-scoped instead: 'pnpm exec eslint <2 changed source files> --format json' = 2 files, 0 errors, 15 warnings, and all 15 sit at lines <=463, above my edit region (498+), so all pre-existing. The narrowing is a measurement, on three counts: the population is eslint's own config ('files: **/*.{ts,tsx}', which covers exactly 2 of my 3 changed files, the third being a .changeset .md); the count is read from --format json, not estimated; and type-aware linting is NOT enabled (eslint.config.js extends tseslint.configs.recommended with zero occurrences of projectService / parserOptions.project / recommendedTypeChecked), so no rule reads across files and this diff cannot move any untouched file's verdict. DECLARED DEVIATION: one ~10s targeted vitest run -- the ablation leg -- executed outside the shared verify lock; every other build/test went through it. That affects CPU courtesy, not the validity of the reading.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #6416: packages/plugin-report registers report/spec-report/report-viewer under namespace 'report' while apps/console/src/register-plugins.ts:135 (and its variant loop) declare the lazy stubs under 'plugin-report' -- so bare 'report' is double-claimed order-dependently (this card's exact shape), AND the plugin-report:* keys are never satisfied by a real registration because register() clears only the lazy stub for its own fullType; packages/cli/src/utils/known-schema-types.ts:345-347 nevertheless whitelists all three unrenderable keys. Every sibling plugin (e.g. plugin-calendar) agrees with the console, so plugin-report is the outlier. Which spelling is canonical has consumer-visible consequences either way, so it is filed for triage, unassigned and unlabelled, not fixed here.",
"census note (no issue filed, no defect): the other apparent bare-key double-claims all failed on reading -- '${type}' and 'button' are Registry.ts's own JSDoc examples and its deprecation-warning template string, and 'form'/'list' at packages/react/src/spec-bridge/SpecBridge.ts:20-21 belong to SpecBridge's own private Map<string, BridgeFn>, not ComponentRegistry. Dynamic-key registration sites are all deliberate and already guarded: packages/fields drives skipFallback off a declared FIELD_TYPES_SKIP_FALLBACK set and registers retired spellings as tombstones with skipFallback: true, and placeholders.tsx:126 guards with if (!ComponentRegistry.get(type))."
]
}
PR #6417, head 48da81623. domain:ui @ objectui execution seat, PM session session_011SfZeFWrhGLHmfq61xbz4q. Verified on a pinned ref (refs/pmtmp/6353).
⚠️The dev agent died after pushing, before reporting. Its last transcript entry is
2026-08-25T22:43Z; no os-dev JSON reached this seat. Recording it so the trail is honest — and
noting that it changes nothing about the verdict, because 复核取代码树,不取 dev 报告: the tree
is the authority for review either way. What is lost is the dev's own narrative, so I leaned
entirely on the diff, the pin, and the PR body.
The fix
skipFallback: true on the presentational registration in renderer.tsx, so bare timeline
belongs to the object-bound view:timelineby declaration.
⭐ The comment beside it identifies the real mechanism, which is sharper than the card stated: src/index.tsx re-exports ./renderer at line 300 before its own import at 307, so the
presentational registration ran first and was then overwritten. The outcome was right and the
mechanism was not — swapping those two lines would have silently handed type: 'timeline' to a
renderer that reads none of the object-bound keys, so an authored timeline would stop fetching
with no error. That is the severity this card was actually carrying.
The pin is the deliverable that outlives the fix, and it is not vacuous
__tests__/timeline-bare-key-ownership.test.ts, and the load-bearing case is the replay:
it.each(ORDERS)('resolves the same way in EITHER evaluation order (%s)')
['presentational first — what ../index does today', …]
['object-bound first — the reorder that used to swap the renderer', …]
It reads the declared metadata off the real registrations and re-registers them into a freshRegistry in both orders, so it cannot drift from what actually ships, and components
are compared by identity so a look-alike cannot satisfy it. It also pins the declaration
itself (skipFallback true on one, falsy on the other), not merely today's outcome — asserting
the outcome alone cannot distinguish declared from happened to be last, which is the whole
point of the card.
Ablation: delete skipFallback: true (the pre-fix condition), mutation confirmed on disk,
predicted red → Tests 4 failed | 1 passed (5).
⭐ And the dev recorded where its own prediction was wrong: "The prediction was one failure short,
and the miss is worth recording. I predicted 3 of 5 red and got 4." Reporting a miss in your own
favour is the habit that makes the other numbers trustworthy.
The census triage asked for — done by shape, reported not fixed
Every .register( / .registerLazy( call across packages/** and apps/** (1575 files) — the registration call shape, not the key string, which is what I asked for: ~30 repo-wide 'timeline' hits are view-type strings and would have drowned the signal. False positives were
each confirmed by reading ('${type}' and 'button' are Registry.ts's own JSDoc examples and
its deprecation-warning template). ⛔ Nothing outside plugin-timeline fixed here.
Landing
28 checks, FAILED=none PEND=NONE, mergeable_state: clean, on the exact head reviewed.
Enqueueing.
Activity
os-support-ai commentedon Aug 25, 2026
Claimed + dispatched —
domain:ui@ objectui execution seatPM session
session_011SfZeFWrhGLHmfq61xbz4q, branchclaude/issue-6353-timeline-double-registration.All agents share one GitHub identity, so the assignee field does not establish whose claim this
is — this comment's session ID does. An earlier claim carrying a different session ID
outranks the assignee.
Pre-dispatch gate
(a) Whole thread read — one record, triage's grading of 2026-08-25T15:58Z. No prior claim,
no prior branch.
(b) Shadow check, in-flight and same-batch, plus triage's named serial check. Triage
flagged: "shares
packages/plugin-timelinewith the finding(plugin-timeline): thetimelinerenderer reads seven keysTimelineSchemadoes not declare, and declares three it never reads #6170 family (ADR-0049 stage 2: tombstoneevents/orientation/positiononTimelineSchema(plusTimelineEventand the example fixture) #6354/Retire thetimeScalealias on the timeline node, now thatscaleis canonical on all three surfaces #6355 when theyunblock); claim-time serial check applies." Run at claim time, and this is the reading:
ADR-0049 stage 2: tombstone
events/orientation/positiononTimelineSchema(plusTimelineEventand the example fixture) #6354 ispm:on-holdand Retire thetimeScalealias on the timeline node, now thatscaleis canonical on all three surfaces #6355 isneeds-user-decision— both unassigned, neither inflight. ✅ The serial check passes now. It is recorded rather than assumed because the
condition triage wrote is "when they unblock" — if either is released while this branch is
open, whoever picks it up inherits the serial constraint, not a clean file.
Nothing else in flight or in this batch touches
packages/plugin-timeline: L6-UI: packaged actions on/off on the Setup packaged-automation page (ADR-0126 §8.2, ruling 3 — surface half) #6412(
app-shell, another session's claim), docs(guide): layout guide's PageHeaderProps list names a retired key and omits three live ones; its registration-inputs claim is two entries behind #5923 (content/docs/guide/layout.md), finding(fields): the location field's stored value shape ({ latitude, longitude }) is declared by no exported type, whileLocationFieldboth produces and consumes it #6154(
packages/types+packages/fields), finding(plugin-kanban): ObjectKanban fetches TWICE on mount — the first query runs before the object schema resolves, so it carries no$expand#6271 (packages/plugin-kanban).(c) Call-site census, on
origin/main64d888613. Both registrations confirmed live andboth inside
packages/plugin-timeline:packages/plugin-timeline/src/index.tsx:366—ComponentRegistry.register('timeline', ObjectTimelineRenderer, {…})packages/plugin-timeline/src/renderer.tsx:499— the second claim on the same bare keyEvery other
⚠️ That distinction matters for the
'timeline'hit in the tree is a view-type string, not a componentregistration —
plugin-list,plugin-view,app-shellview pickers, and theViewType/UnifiedViewTypeunions inpackages/types.census triage asked for: a
'timeline'occurrence is not evidence of a double-claim, andgrepping the bare string would drown the real signal in ~30 unrelated hits. Census the
registration call shape, not the key string.
Generated by Claude Code
claude commentedon Aug 25, 2026
os-dev-report
{ "issue": 6353, "status": "done", "branch": "claude/issue-6353-timeline-double-registration", "pr": "https://github.com/objectstack-ai/objectui/pull/6417", "premise_still_valid": true, "summary": "Confirmed the census on origin/main 64d888613 before editing: packages/plugin-timeline/src/renderer.tsx:498 registers 'timeline' as plugin-timeline:timeline (presentational TimelineRenderer) and src/index.tsx:366 registers it as view:timeline (object-bound ObjectTimelineRenderer); neither passed skipFallback, so both claimed the bare key and index.tsx won only because index.tsx re-exports ./renderer at line 300 before its own import at 307. Applied the remedy the registry's own collision guard names (Registry.ts:246-266) rather than a second mechanism: renderer.tsx now registers with skipFallback: true, so bare 'timeline' resolves to ObjectTimelineRenderer by declaration in any evaluation order, while the presentational renderer stays reachable under plugin-timeline:timeline. Resolution is unchanged today. The pin is src/__tests__/timeline-bare-key-ownership.test.ts (5 assertions): it reads BOTH registrations' real declared metadata back out of the registry (nothing hand-copied, so it cannot drift from source) and replays them into a fresh Registry in BOTH orders, asserting the object-bound renderer answers bare 'timeline' either way and that the collision guard stays silent; it also fails if the declaration is dropped or a third registration starts claiming the bare key. Changeset included (patch, @object-ui/plugin-timeline; no major, per the fixed-group rule).", "tests": "All runs on the final commit's tree, git rev-parse --short HEAD = 48da81623, with git diff HEAD empty at each run; heavy runs via the container's shared verify lock (scripts/pm/os-verify-lock.sh, VERDICT lines read, never a bare $?). PASS: 'pnpm exec vitest run packages/plugin-timeline/src/' from the repo root (the canonical invocation the repo's vitest-invocation-guard demands -- 'pnpm --filter <pkg> test' is the documented false-green trap here) => 'Test Files 10 passed (10)' / 'Tests 73 passed (73)'. PASS: 'pnpm --filter @object-ui/plugin-timeline type-check' exit 0, and its output echoed '> tsc --noEmit && tsc -p tsconfig.test.json', so it was not a zero-match script name; tsconfig.test.json includes src/**/*.test.ts, so the new test file is genuinely in the program rather than excluded. Dependency closure built FIRST ('pnpm --filter @object-ui/plugin-timeline^... build', exit 0). PASS exit 0: check-changeset-presence.mjs, check-changeset-no-major.mjs, check-changeset-fixed.mjs, check-control-bytes.mjs, check-doc-component-types.mjs. ABLATION (ghost-assertion guard): predicted RED before running; mutation = delete 'skipFallback: true' from renderer.tsx (the pre-fix condition). NO BUILD IS INVOLVED in this pin -- the root vitest config aliases @object-ui/* to sibling src/ and both imports are relative source paths, so nothing resolves through dist and no dist preflight applies. Mutation CONFIRMED ON DISK before measuring, not by the editor's exit code: grep -c 'skipFallback: true' went 1 -> 0 and git hash-object moved from the HEAD blob 0ddf84dd9d53896a0a5a3e4a82e4511bf4a9ff09 to 1cc2b14a548d33db21ff25198c0f39f6c516cf0b; script carried trap '<restore>' EXIT INT TERM with an absolute REPO_ROOT-anchored path. Result: 'Test Files 1 failed (1)' / 'Tests 4 failed | 1 passed (5)', e.g. \"registering in the order [view, plugin-timeline] changed who answers bare \\\"timeline\\\": expected [Function TimelineRenderer] to be [Function ObjectTimelineRenderer]\" and \"expected [ 'plugin-timeline:timeline', ...(1) ] to deeply equal [ 'view:timeline' ]\". PREDICTION MISS, recorded rather than smoothed over: I predicted 3 of 5 red and got 4 -- the 'presentational first' replay row also failed, on the collision-warning assertion rather than on resolution, because pre-fix that order resolves correctly AND still warns. The single row that stayed GREEN under the mutation is the live-singleton assertion, which is the concrete proof that a live-outcome assertion alone would have been a ghost assertion here (it passes in both worlds); the replay is what carries the pin. RESTORE proven by state, not exit code: git checkout HEAD -- <absolute path> (never a bare 'git checkout --', which restores from the polluted index), then hash-object == HEAD blob AND 'git diff HEAD' empty; re-run green. DECLARED NARROWING: repo-wide 'pnpm lint' was not run locally (it is CI's run). Diff-scoped instead: 'pnpm exec eslint <2 changed source files> --format json' = 2 files, 0 errors, 15 warnings, and all 15 sit at lines <=463, above my edit region (498+), so all pre-existing. The narrowing is a measurement, on three counts: the population is eslint's own config ('files: **/*.{ts,tsx}', which covers exactly 2 of my 3 changed files, the third being a .changeset .md); the count is read from --format json, not estimated; and type-aware linting is NOT enabled (eslint.config.js extends tseslint.configs.recommended with zero occurrences of projectService / parserOptions.project / recommendedTypeChecked), so no rule reads across files and this diff cannot move any untouched file's verdict. DECLARED DEVIATION: one ~10s targeted vitest run -- the ablation leg -- executed outside the shared verify lock; every other build/test went through it. That affects CPU courtesy, not the validity of the reading.", "open_questions": [], "out_of_scope_findings": [ "filed as #6416: packages/plugin-report registers report/spec-report/report-viewer under namespace 'report' while apps/console/src/register-plugins.ts:135 (and its variant loop) declare the lazy stubs under 'plugin-report' -- so bare 'report' is double-claimed order-dependently (this card's exact shape), AND the plugin-report:* keys are never satisfied by a real registration because register() clears only the lazy stub for its own fullType; packages/cli/src/utils/known-schema-types.ts:345-347 nevertheless whitelists all three unrenderable keys. Every sibling plugin (e.g. plugin-calendar) agrees with the console, so plugin-report is the outlier. Which spelling is canonical has consumer-visible consequences either way, so it is filed for triage, unassigned and unlabelled, not fixed here.", "census note (no issue filed, no defect): the other apparent bare-key double-claims all failed on reading -- '${type}' and 'button' are Registry.ts's own JSDoc examples and its deprecation-warning template string, and 'form'/'list' at packages/react/src/spec-bridge/SpecBridge.ts:20-21 belong to SpecBridge's own private Map<string, BridgeFn>, not ComponentRegistry. Dynamic-key registration sites are all deliberate and already guarded: packages/fields drives skipFallback off a declared FIELD_TYPES_SKIP_FALLBACK set and registers retired spellings as tombstones with skipFallback: true, and placeholders.tsx:126 guards with if (!ComponentRegistry.get(type))." ] }Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
PM review — ACCEPT
PR #6417, head
48da81623.domain:ui@ objectui execution seat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q. Verified on a pinned ref (refs/pmtmp/6353).2026-08-25T22:43Z; no
os-devJSON reached this seat. Recording it so the trail is honest — andnoting that it changes nothing about the verdict, because 复核取代码树,不取 dev 报告: the tree
is the authority for review either way. What is lost is the dev's own narrative, so I leaned
entirely on the diff, the pin, and the PR body.
The fix
skipFallback: trueon the presentational registration inrenderer.tsx, so baretimelinebelongs to the object-bound
view:timelineby declaration.⭐ The comment beside it identifies the real mechanism, which is sharper than the card stated:
src/index.tsxre-exports./rendererat line 300 before its ownimportat 307, so thepresentational registration ran first and was then overwritten. The outcome was right and the
mechanism was not — swapping those two lines would have silently handed
type: 'timeline'to arenderer that reads none of the object-bound keys, so an authored timeline would stop fetching
with no error. That is the severity this card was actually carrying.
The pin is the deliverable that outlives the fix, and it is not vacuous
__tests__/timeline-bare-key-ownership.test.ts, and the load-bearing case is the replay:It reads the declared metadata off the real registrations and re-registers them into a
fresh
Registryin both orders, so it cannot drift from what actually ships, and componentsare compared by identity so a look-alike cannot satisfy it. It also pins the declaration
itself (
skipFallbacktrue on one, falsy on the other), not merely today's outcome — assertingthe outcome alone cannot distinguish declared from happened to be last, which is the whole
point of the card.
Ablation: delete
skipFallback: true(the pre-fix condition), mutation confirmed on disk,predicted red →
Tests 4 failed | 1 passed (5).⭐ And the dev recorded where its own prediction was wrong: "The prediction was one failure short,
and the miss is worth recording. I predicted 3 of 5 red and got 4." Reporting a miss in your own
favour is the habit that makes the other numbers trustworthy.
The census triage asked for — done by shape, reported not fixed
Every
.register(/.registerLazy(call acrosspackages/**andapps/**(1575 files) —the registration call shape, not the key string, which is what I asked for: ~30 repo-wide
'timeline'hits are view-type strings and would have drowned the signal. False positives wereeach confirmed by reading (
'${type}'and'button'areRegistry.ts's own JSDoc examples andits deprecation-warning template). ⛔ Nothing outside
plugin-timelinefixed here.Landing
28 checks,
FAILED=none PEND=NONE,mergeable_state: clean, on the exact head reviewed.Enqueueing.
Generated by Claude Code