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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursor/rules/git.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This Cursor window’s default cwd is often `ethereumjs-monorepo`. Nested FYP ru
- Do **not** commit, push, or open a PR unless the human asks.
- Working-tree **inspection** (branch, dirty files) is fine. Do not stash, checkout, create, rename, or push branches unless the human asks (including round-trip phase 2 — branches are human-led or explicit instruction).
- **Exception:** [round-trip-close](../skills/round-trip-close/SKILL.md) at merge GO (merge PRs, delete remote `eip-NNNN`, checkout / pull default, safe-delete local). Do not run close outside phase 6.
- **Exception:** [Unicorn merge 🦄](../skills/unicorn-merge/SKILL.md) **only** when the human explicitly says that phrase — coordinated commit/push/PR/CI-wait/merge/cleanup/new branches for session FYP work. Not a synonym for round-trip or generic “merge my PRs”.
- A **push** ask includes the [pre-push CI](#before-a-push) loop: fix failures and commit those fixes without a second ask, then push.

## Scope
Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/repo-structure.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Fleet overview for humans: [docs.feelyourprotocol.org](https://docs.feelyourprot
| **Rules — protocol data** (always-on) | `eip-canonical-data.mdc` | Twin + lab data homes, when to update, how to retrieve (website + MCP). Engine `website-relation.mdc` points here — do not fork the map |
| **Rules — task invariants** (glob-scoped) | `explorations-website.mdc` on `src/explorations/**`, `src/eComponents/**`; `design.mdc` on shared Vue/chrome; `exploration-design.mdc` on exploration Vue, UICs, and cover/layout files; `mcp-docs-design.mdc` on `mcp-docs/**` | Failure modes for one domain — not a fourth primitive |
| **Rules — specialized** | `video-recording.mdc`, `og-images.mdc` | Long capture/setup playbooks; do not fold into primitives |
| **Skills** | `.cursor/skills/round-trip-protocol-change/`, `add-exploration`, `update-ethereumjs`, `cover-image`, `bro-bruh-comic`, `x-engagement` | Ordered workflows; point at website-docs + source, do not duplicate catalogs |
| **Skills** | `.cursor/skills/round-trip-protocol-change/`, `unicorn-merge`, `add-exploration`, `update-ethereumjs`, `cover-image`, `bro-bruh-comic`, `x-engagement` | Ordered workflows; point at website-docs + source, do not duplicate catalogs |

New work: invariants → rule or glob rule; executable procedure → skill; concepts and API tables → website-docs only.

Expand Down
2 changes: 1 addition & 1 deletion .cursor/skills/round-trip-close/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description: >-

# Round-trip close

**Only** after [round-trip](../round-trip-protocol-change/SKILL.md) comic and video (or skip of those asks). Not a general git habit. Standalone widget / engine / comic / video work must not run this.
**Only** after [round-trip](../round-trip-protocol-change/SKILL.md) comic and video (or skip of those asks). Not a general git habit. Standalone widget / engine / comic / video work must not run this. For non–round-trip multi-repo ship/merge, the human uses **[Unicorn merge 🦄](../unicorn-merge/SKILL.md)** (explicit trigger only).

Target: the confirmed id (`eip-NNNN`). The three siblings: `website`, `mcp-execution-engine`, `mcp-gateway`. Git cwd rules: [git.mdc](../../rules/git.mdc).

Expand Down
107 changes: 107 additions & 0 deletions .cursor/skills/unicorn-merge/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
name: unicorn-merge
description: >-
Unicorn merge 🦄 only — when the human explicitly asks for that phrase (with
or without the emoji). Ship session FYP branches to merged PRs, clean default
branches, and fresh working branches. Not exploration round-trip close.
---

# Unicorn merge 🦄

**Trigger:** the human says **Unicorn merge** or **Unicorn merge 🦄** (or clearly names this skill). Do **not** run from vague “merge my PRs”, “ship it”, or “round-trip close” — those are different workflows ([round-trip-close](../round-trip-close/SKILL.md) is EIP phase 6 only).

**Goal:** move from open feature branches on the FYP repos involved in **this session** to merged PRs, updated `main`, deleted feature branches, and new local working branches.

**Repos:** `website`, `mcp-execution-engine`, `mcp-gateway` — only siblings the human named or that hold unpushed/ahead work on the **same coordinated effort** this session. Skip clean unrelated checkouts. `server-config` only when this session touched it.

**Git mechanics:** [git.mdc](../../rules/git.mdc) — `working_directory`, `required_permissions: ["all"]`, **one signed op at a time**, commit messages, [pre-push CI](#before-a-push) (in git.mdc), PR title/body rules. Multi-repo push/PR order when all three move: **engine → gateway → website**.

Human may stop after any step; report status and what is left.

---

## 1 — Commit if needed

Per in-scope repo: `git status`. If dirty or untracked work belongs to this effort, commit (human already asked via Unicorn merge). One-line subject, no body. If nothing to commit, note clean.

Do **not** start Unicorn merge without the explicit trigger, even if trees are dirty.

---

## 2 — Push

Per in-scope repo, in turn (engine → gateway → website when all three):

1. Run that repo’s pre-push CI from git.mdc; fix red, commit fixes, re-run.
2. `git push` (set upstream if needed) → wait success.

---

## 3 — Open PRs

Per repo, after that repo’s push, one at a time: `gh pr view <branch>`.

- **PR exists:** refresh title/body if branch essence changed (git.mdc § Pull requests).
- **No PR:** `gh pr create` with Related links to sibling PRs when coordinated.

Report each PR URL.

---

## 4 — Wait for green CI

Per repo with a PR: `gh pr checks` (and `gh pr view` for state).

| Result | Action |
| --- | --- |
| Pending / queued / in progress | **Stop here.** Report “waiting on CI” with PR links. Human re-invokes Unicorn merge 🦄 when checks finish. |
| Failed / cancelled | Report job + short cause. Ask fix vs retry. Do not merge. |
| Success / skipped / neutral (all required) | Repo is green. |

Proceed to §5 only when **every** in-scope PR is green. Trees should match remote (not ahead/behind); if not, say so before merge.

---

## 5 — Merge PRs and delete remote feature branches

Only when §4 is fully green for all in-scope PRs.

Per repo, in turn: `gh pr merge <number> --delete-branch` (add `--merge` if `gh` requires a merge method). On failure, **stop** — do not merge the rest until resolved.

---

## 6 — Update local default and delete local feature branches

After **all** merges in §5 succeeded, per repo in turn:

1. `git fetch origin`
2. Checkout default (`main` / `origin/HEAD`)
3. `git pull --ff-only origin <default>`
4. `git branch -d <old-feature-branch>` (safe delete only — not `-D` unless human asks)

Report default @ short SHA; local feature branch gone.

---

## 7 — New working branches

Per in-scope repo, in turn (after §6 for that repo):

`git checkout -b <name>` where `<name>` is the branch name the human gave, or **`new-work`** if they did not name one.

If the human wanted **one shared name** across repos, use the same name on each. If names differ per repo, they will say so.

**Stop** with a short summary: merged PR URLs, default SHAs, new branch names, optional deploy reminder (gateway/hosted MCP, website, metrics) when this session changed runtime surfaces.

---

## Distinction from round-trip close

| | Unicorn merge 🦄 | Round-trip close |
| --- | --- | --- |
| Trigger | Explicit “Unicorn merge” | Round-trip phase 6 GO |
| Branch names | Any session feature branch | `eip-NNNN` on three siblings |
| Extra | — | YouTube, announcement tweets |
| Scope | Session-coordinated FYP repos | Always website + engine + gateway for that EIP |

Reuse merge/checkout/delete **patterns** from round-trip-close §3; do not run round-trip-close marketing/YouTube unless the human also asked for phase 6.
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Tool-agnostic entrypoint for coding agents (Cursor, Claude Code, Codex, etc.) in
| Task | Read first |
| --- | --- |
| **Full round-trip** (EIP → exploration → MCP → optional comic/video → close) | [`.cursor/skills/round-trip-protocol-change/SKILL.md`](.cursor/skills/round-trip-protocol-change/SKILL.md) |
| **Unicorn merge 🦄** (explicit phrase only — session branches → merged PRs → fresh `new-work`) | [`.cursor/skills/unicorn-merge/SKILL.md`](.cursor/skills/unicorn-merge/SKILL.md) — not round-trip close |
| **Brief a protocol change** (round-trip phase 1) | [`.cursor/skills/brief-protocol-change/SKILL.md`](.cursor/skills/brief-protocol-change/SKILL.md) — includes working-tree prep (observe git only) |
| **Create or change an exploration** (round-trip phase 2) | [`.cursor/skills/add-exploration/SKILL.md`](.cursor/skills/add-exploration/SKILL.md) → [adding-an-exploration.md](website-docs/contributing/adding-an-exploration.md) |
| **EthereumJS / EST bump** (re-pin EIP specs) | [`.cursor/skills/update-ethereumjs/SKILL.md`](.cursor/skills/update-ethereumjs/SKILL.md) → [eip-canonical-data.mdc](.cursor/rules/eip-canonical-data.mdc) § Spec versioning |
Expand Down
2 changes: 1 addition & 1 deletion mcp-docs/use/forks/fusaka.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Current mainnet EL — Fusaka
# 🦓 Current mainnet EL — Fusaka

> **Status:** **Live on the public MCP.** **`baselineForkId`** in the probe — today’s mainnet execution-layer rules.

Expand Down
2 changes: 1 addition & 1 deletion mcp-docs/use/forks/glamsterdam.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Amsterdam now
# 🐻‍❄️ Amsterdam now (Glamsterdam)

::: info Naming
**Glamsterdam** is the catalog id; **`amsterdam`** is the Ethereum execution-layer alias. Both mean the same fork here. Default when you omit `fork`.
Expand Down
29 changes: 21 additions & 8 deletions mcp-docs/use/forks/historical-forks.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,29 @@ Examples (compare prompts):
- **PUSH0** (3855) → **`shapella`** → compare **`paris`** then **`shapella`** (`0x5f00` on bytecode).
- **ModExp repricing** (7883) → **`fusaka`** → compare **`pectra`** then **`fusaka`** ([EIP-7883](/use/eips/eip-7883)).

## Upgrade mascots

From **Paris (The Merge)** on, Ethereum upgrades often have a community mascot ([EIP-8066](https://eips.ethereum.org/EIPS/eip-8066)). The probe exposes them on `namedForks[].mascotEmoji` and on run results as `provenance.forkLabel` / `forkMascotEmoji`. Berlin and London predate that process.

| Fork | Mascot |
| --- | --- |
| Paris (Merge) | 🐼 |
| Shapella | 🦉 |
| Dencun | 🐡 |
| Pectra | 🦒 |
| Fusaka | 🦓 |
| Glamsterdam | 🐻‍❄️ |

## Lineage table (historical rows)

| Order | `baseHardfork` | Aliases | Notable activations (full lists in probe) |
| --- | --- | --- | --- |
| 0 | `berlin` | — | 2565 ModExp, 2718 typed txs, 2929 cold/warm gas, 2930 access lists |
| 1 | `london` | — | **1559** fee market, 3198 BASEFEE, 3529 refunds, 3541 initcode prefix |
| 2 | `paris` | `merge`, `the-merge` | 3675 Merge (not replayable here), 4399 PREVRANDAO |
| 3 | `shapella` | `shanghai` | 3855 PUSH0, 3860 initcode, 4895 withdrawals (limited in lab) |
| 4 | `dencun` | `cancun` | 1153 transient storage, 5656 MCOPY, 4844 blobs (no blob txs in lab) |
| 5 | `pectra` | `prague` | 7702 set-code (`authorizationList` on run_transaction), 2537 BLS precompiles, 7623 calldata |
| Order | `baseHardfork` | Mascot | Aliases | Notable activations (full lists in probe) |
| --- | --- | --- | --- | --- |
| 0 | `berlin` | — | — | 2565 ModExp, 2718 typed txs, 2929 cold/warm gas, 2930 access lists |
| 1 | `london` | — | — | **1559** fee market, 3198 BASEFEE, 3529 refunds, 3541 initcode prefix |
| 2 | `paris` | 🐼 | `merge`, `the-merge` | 3675 Merge (not replayable here), 4399 PREVRANDAO |
| 3 | `shapella` | 🦉 | `shanghai` | 3855 PUSH0, 3860 initcode, 4895 withdrawals (limited in lab) |
| 4 | `dencun` | 🐡 | `cancun` | 1153 transient storage, 5656 MCOPY, 4844 blobs (no blob txs in lab) |
| 5 | `pectra` | 🦒 | `prague` | 7702 set-code (`authorizationList` on run_transaction), 2537 BLS precompiles, 7623 calldata |

**Floor:** **Berlin** — Istanbul and earlier hardforks are out of scope for this MCP round.

Expand Down
6 changes: 3 additions & 3 deletions mcp-docs/use/tools/describe-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ None required. Pass `{}` or omit arguments.
| `ceilings` | `maxGasLimit`, `maxTransactionGasLimit`, `defaultGasLimit`, `maxBytecodeBytes`, `maxTraceSteps`, `maxTxsPerBlock` |
| `baselineForkId` | Current mainnet EL (`fusaka`) — probe label for mainnet-today; run the fork the user asked for |
| `queryShapes` | Dictionary: catalog `id` (`simulate`, `generate`, …) → `mcpTool` (`run_bytecode`, `generate_artifact`, …). Agents call `mcpTool`. |
| `namedForks` | Berlin→Glamsterdam lineage — `order`, `predecessorId`, `successorId`, `role`, `activatedEips`, advertised `relatedEips`, **`tools`** (MCP names) |
| `namedForks` | Berlin→Glamsterdam lineage — `order`, `predecessorId`, `successorId`, `role`, `mascotEmoji` (when assigned), `activatedEips`, advertised `relatedEips`, **`tools`** (MCP names) |
| `eipIntroductions` | When each EIP activated. **`coverage`** is `twin` (runnable module), `supported` (hardfork already applies it — omit that id from `eips` and use the fork tools), `unshown` (execution-layer, in the bundle, and this lab does not demonstrate it — do not put it in `eips`; a generic fork run does not stand in for it), `consensus` (consensus-layer — this lab does not execute it; do not put it in `eips`), `networking` (wire protocol — this lab does not speak it; do not put it in `eips`), `informational` (analysis or a schedule — this lab does not execute it; do not put it in `eips`), or `listed` (name and fork only). **`observableTools`** are MCP names; `supported`, `unshown`, `consensus`, `networking`, and `informational` rows omit them |
| `eips` | Runnable modules — optional `comparison` lookup from `eipIntroductions` (use when the user asks for before/after); **`tools`** lists MCP names to call; each row includes `specUrl`, `specDate`, `status`, and optional `testReleaseUrl` / `testReleaseName` (or a live `eips.ethereum.org` page when unpinned) |
| `allowedBaseHardforks` | Lineage forks (`berlin` … `glamsterdam`) plus aliases; glacier/BPO ids rejected |
Expand All @@ -61,8 +61,8 @@ _Output (abbreviated):_
{ "id": "simulate", "mcpTool": "run_bytecode", "summary": "…" }
],
"namedForks": [
{ "id": "fusaka", "role": "current", "aliases": ["osaka", "mainnet-el"], "relatedEips": [7883, 7951], "tools": ["run_bytecode", "run_transaction", "run_block"], "…": "…" },
{ "id": "glamsterdam", "role": "preview", "aliases": ["amsterdam"], "relatedEips": [2780, 7708, 7778, 7843, 7928, 7954, 7976, 8024, 8037, 8038], "tools": ["run_bytecode", "run_transaction", "run_block"], "…": "…" }
{ "id": "fusaka", "role": "current", "mascotEmoji": "🦓", "aliases": ["osaka", "mainnet-el"], "relatedEips": [7883, 7951], "tools": ["run_bytecode", "run_transaction", "run_block"], "…": "…" },
{ "id": "glamsterdam", "role": "preview", "mascotEmoji": "🐻‍❄️", "aliases": ["amsterdam"], "relatedEips": [2780, 7708, 7778, 7843, 7928, 7954, 7976, 8024, 8037, 8038], "tools": ["run_bytecode", "run_transaction", "run_block"], "…": "…" }
],
"eips": [{
"eip": 8024,
Expand Down
8 changes: 5 additions & 3 deletions src/explorations/ExplorationMetaPills.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ const props = withDefaults(
const pillLinkClass =
'exploration-meta-pill inline-flex items-center justify-center font-mono rounded-full border no-underline cursor-pointer transition-colors hover:bg-slate-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400 text-xs px-3 py-2 min-h-11 sm:min-h-0 sm:px-2 sm:py-0.5 sm:text-[0.65rem]'

const timelineLabel = computed(
() => TIMELINE[props.exploration.timeline]?.title ?? props.exploration.timeline,
)
const timelineLabel = computed(() => {
const entry = TIMELINE[props.exploration.timeline]
if (!entry) return props.exploration.timeline
return `${entry.emoji} ${entry.title}`
})
const timelineBrowseTo = computed(() => ({
path: '/all',
query: { timeline: props.exploration.timeline },
Expand Down
6 changes: 4 additions & 2 deletions src/explorations/TIMELINE.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FORK_MASCOT_EMOJI } from '@/explorations/forkCatalog'

/**
* Website catalog timeline — forks that have explorations.
*
Expand Down Expand Up @@ -27,13 +29,13 @@ export const TIMELINE: Timeline = {
fusaka: {
title: 'Fusaka',
role: 'current',
emoji: '🏯',
emoji: FORK_MASCOT_EMOJI.fusaka!,
order: 100,
},
glamsterdam: {
title: 'Glamsterdam',
role: 'preview',
emoji: '🌷',
emoji: FORK_MASCOT_EMOJI.glamsterdam!,
order: 110,
},
}
Expand Down
30 changes: 30 additions & 0 deletions src/explorations/__tests__/forkCatalog.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest'

import {
FORK_MASCOT_EMOJI,
forkDisplayLabel,
fypHardforkToggleOptions,
MCP_FORK_IDS,
} from '@/explorations/forkCatalog'
import { TIMELINE } from '@/explorations/TIMELINE'

describe('forkCatalog mascots', () => {
it('keeps TIMELINE emoji aligned with FORK_MASCOT_EMOJI', () => {
for (const [id, entry] of Object.entries(TIMELINE)) {
expect(entry.emoji).toBe(FORK_MASCOT_EMOJI[id as keyof typeof FORK_MASCOT_EMOJI])
}
})

it('lists zebra and polar bear on hardfork toggles', () => {
const labels = fypHardforkToggleOptions().map((row) => row.label)
expect(labels).toEqual(['🐻‍❄️ Glamsterdam', '🦓 Fusaka'])
})

it('covers every mascot id in MCP_FORK_IDS', () => {
const mascotIds = Object.keys(FORK_MASCOT_EMOJI).sort()
expect(
mascotIds.every((id) => MCP_FORK_IDS.includes(id as (typeof MCP_FORK_IDS)[number])),
).toBe(true)
expect(forkDisplayLabel('pectra', 'Pectra')).toBe('🦒 Pectra')
})
})
6 changes: 2 additions & 4 deletions src/explorations/eip-2780/MyC.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ScenarioStepNavUIC from '@/eComponents/ui/ScenarioStepNavUIC.vue'
import SegmentedToggleUIC from '@/eComponents/ui/SegmentedToggleUIC.vue'
import WidgetChromeUIC from '@/eComponents/ui/WidgetChromeUIC.vue'
import ExplorationC from '@/explorations/ExplorationC.vue'
import { fypHardforkToggleOptions } from '@/explorations/forkCatalog'
import { TOPICS } from '@/explorations/TOPICS'
import { resolveInitialExample } from '@/libs/exampleFromQuery'
import { useExplorationExampleQuery } from '@/libs/useExplorationExampleQuery'
Expand Down Expand Up @@ -75,10 +76,7 @@ function onHardforkInput(value: string) {
if (value === 'glamsterdam' || value === 'fusaka') setHardfork(value)
}

const hardforkOptions = [
{ value: 'glamsterdam', label: 'Glamsterdam', testId: 'hardfork-glamsterdam' },
{ value: 'fusaka', label: 'Fusaka', testId: 'hardfork-fusaka' },
]
const hardforkOptions = fypHardforkToggleOptions()

async function runTxAction(): Promise<void> {
if (!isShapeId(example.value)) return
Expand Down
6 changes: 3 additions & 3 deletions src/explorations/eip-2780/tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ describe('EIP-2780 intrinsic transaction gas exploration', () => {

it('shows a flat 21,000 when Fusaka is selected', async () => {
const wrapper = await mountWidget()
const fusaka = wrapper.findAll('button').find((button) => button.text() === 'Fusaka')
expect(fusaka).toBeDefined()
await fusaka!.trigger('click')
const fusaka = wrapper.find('[data-testid="hardfork-fusaka"]')
expect(fusaka.exists()).toBe(true)
await fusaka.trigger('click')
expect(wrapper.find('[data-testid="piece-ledger"]').attributes('data-split')).toBe('false')
expect(wrapper.find('[data-testid="piece-total"]').text()).toBe('21,000')
expect(wrapper.find('[data-testid="piece-base"]').text()).toContain('flat 21,000')
Expand Down
6 changes: 2 additions & 4 deletions src/explorations/eip-7708/MyC.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ScenarioStepNavUIC from '@/eComponents/ui/ScenarioStepNavUIC.vue'
import SegmentedToggleUIC from '@/eComponents/ui/SegmentedToggleUIC.vue'
import WidgetChromeUIC from '@/eComponents/ui/WidgetChromeUIC.vue'
import ExplorationC from '@/explorations/ExplorationC.vue'
import { fypHardforkToggleOptions } from '@/explorations/forkCatalog'
import { TOPICS } from '@/explorations/TOPICS'
import { useCompanionStatusPublisher } from '@/libs/companionStatus'
import { resolveInitialExample } from '@/libs/exampleFromQuery'
Expand Down Expand Up @@ -68,10 +69,7 @@ function onHardforkInput(value: string) {
if (value === 'glamsterdam' || value === 'fusaka') setHardfork(value)
}

const hardforkOptions = [
{ value: 'glamsterdam', label: 'Glamsterdam', testId: 'hardfork-glamsterdam' },
{ value: 'fusaka', label: 'Fusaka', testId: 'hardfork-fusaka' },
]
const hardforkOptions = fypHardforkToggleOptions()

async function runBlock(): Promise<void> {
if (example.value === '') return
Expand Down
Loading
Loading