diff --git a/app/web/router.py b/app/web/router.py
index c8c68ed..cdffd49 100644
--- a/app/web/router.py
+++ b/app/web/router.py
@@ -1525,7 +1525,7 @@ async def marketplace_guide_curated(
):
ctx = _build_context(
request, user=user,
- guide_title="Submit a plugin to Curated Marketplace",
+ guide_title="Submit a skill or plugin to Curated Marketplace",
guide_kind="curated",
)
return templates.TemplateResponse(request, "marketplace_guide.html", ctx)
diff --git a/app/web/templates/marketplace.html b/app/web/templates/marketplace.html
index 2da00a0..48b00f9 100644
--- a/app/web/templates/marketplace.html
+++ b/app/web/templates/marketplace.html
@@ -440,7 +440,7 @@
-
Submit a plugin
+
Submit a skill or plugin
+ Upload
@@ -779,7 +779,7 @@ function fillEmptyState() {
if (state.tab === 'curated') {
t.textContent = 'No curated plugins available.';
b.textContent = 'Ask your admin to grant your group access.';
- c.innerHTML = `
Submit a plugin →`;
+ c.innerHTML = `
Submit a skill or plugin →`;
} else if (state.tab === 'flea') {
t.textContent = 'No community entities yet.';
b.textContent = 'Be the first to share something.';
diff --git a/app/web/templates/marketplace_guide.html b/app/web/templates/marketplace_guide.html
index 1602b7b..74de95c 100644
--- a/app/web/templates/marketplace_guide.html
+++ b/app/web/templates/marketplace_guide.html
@@ -71,6 +71,39 @@
/* Flea variant uses purple step bubbles (matches flea badge accent). */
.guide-page[data-kind="flea"] .guide-steps li::before { background: #6D28D9; }
+ /* Fast-path callout — visually distinct from the numbered steps so
+ readers spot "wait, there's a quicker option" without re-reading
+ the whole page. Lives only on the curated guide; flea has no
+ equivalent escape hatch (it's already the fast path). */
+ .guide-fastpath {
+ margin: 16px 0 28px;
+ padding: 18px 22px;
+ background: linear-gradient(135deg, rgba(109, 40, 217, 0.06), rgba(109, 40, 217, 0.02));
+ border: 1px solid rgba(109, 40, 217, 0.25);
+ border-left: 4px solid #6D28D9;
+ border-radius: 12px;
+ }
+ .guide-fastpath h3 {
+ margin: 0 0 8px;
+ font-size: 15px; font-weight: 700;
+ color: var(--text-primary);
+ }
+ .guide-fastpath p {
+ margin: 0 0 12px;
+ font-size: 14px; line-height: 1.6;
+ color: var(--text-secondary);
+ }
+ .guide-fastpath p strong { color: var(--text-primary); }
+ .guide-fastpath a.fastpath-cta {
+ display: inline-flex; align-items: center; gap: 6px;
+ padding: 8px 14px;
+ background: #6D28D9; color: #fff;
+ border-radius: 8px;
+ font-size: 13px; font-weight: 500;
+ text-decoration: none;
+ }
+ .guide-fastpath a.fastpath-cta:hover { background: #5B21B6; }
+
/* ── Section block ──────────────────────────────────────────────── */
.guide-section {
background: var(--card-bg);
@@ -130,32 +163,114 @@
{{ guide_title }}
{% if guide_kind == 'curated' %}
- {# Curated submission guide — body intentionally left minimal; full
- content to be authored separately. The CTA buttons at the bottom of
- the page still let the user jump back to the Curated tab or browse
- the Flea Market as a self-service alternative. #}
+ {# Curated submission guide — publish-by-curator flow. Named Curators
+ are the gatekeepers (security / telemetry hygiene / docs bar). The
+ fast-path callout points users at the Flea Market when they want
+ reach without the review wait. #}
- The Curated Marketplace is publish-by-curator only. To get your skill,
- agent, or plugin listed, hand it off to a curator who reviews and
- publishes it on your behalf.
+ Curated Marketplace submissions go through Named Curators
+ — folks accountable for keeping the bar (security, telemetry hygiene,
+ documentation) consistent across what ships on this shelf.
- {% else %}
- {# Flea publishing guide — body intentionally left minimal; full content
- to be authored separately. The CTA buttons at the bottom of the page
- still let the user jump to /store/new or browse the Flea Market. #}
-
- The Flea Market is self-service. Anyone in the company can upload a
- skill, agent, or plugin.
-
- {% endif %}
-
{% endblock %}
diff --git a/tests/test_web_marketplace_guide.py b/tests/test_web_marketplace_guide.py
new file mode 100644
index 0000000..bed4dad
--- /dev/null
+++ b/tests/test_web_marketplace_guide.py
@@ -0,0 +1,140 @@
+"""GET /marketplace/guide/{curated,flea} — submission flow guides.
+
+Both routes are authed (`get_current_user` dependency). The curated guide
+documents the Named Curator handoff and has a fast-path callout pointing
+at the flea self-service guide; the flea guide documents the /store/new
+upload flow. Together with the action-row CTA on /marketplace?tab=curated,
+this trio is the discovery surface for "how do I get my plugin published".
+"""
+
+from __future__ import annotations
+
+import tempfile
+import uuid
+
+import pytest
+
+
+@pytest.fixture
+def fresh_db(monkeypatch):
+ with tempfile.TemporaryDirectory() as tmp:
+ monkeypatch.setenv("DATA_DIR", tmp)
+ monkeypatch.setenv("TESTING", "1")
+ monkeypatch.setenv("JWT_SECRET_KEY", "test-jwt-secret-key-minimum-32-chars!!")
+ yield tmp
+
+
+def _make_user_and_session(conn, email="u@example.com"):
+ from src.repositories.users import UserRepository
+ from app.auth.jwt import create_access_token
+
+ uid = str(uuid.uuid4())
+ UserRepository(conn).create(id=uid, email=email, name=email.split("@")[0])
+ return uid, create_access_token(user_id=uid, email=email)
+
+
+def _client():
+ from fastapi.testclient import TestClient
+ from app.main import app
+
+ return TestClient(app)
+
+
+def test_marketplace_curated_tab_cta_text(fresh_db):
+ """The action-row CTA on /marketplace?tab=curated reads
+ 'Submit a skill or plugin' (renamed from 'Submit a plugin' so skills
+ aren't an afterthought) and links to the curated guide. Empty-state
+ fallback in JS uses the same string so both surfaces stay in sync."""
+ from src.db import get_system_db, close_system_db
+
+ conn = get_system_db()
+ try:
+ _, sess = _make_user_and_session(conn)
+ finally:
+ conn.close()
+ close_system_db()
+ body = _client().get(
+ "/marketplace?tab=curated", cookies={"access_token": sess}
+ ).text
+
+ # Action-row anchor — primary discovery path.
+ assert (
+ '
Submit a skill or plugin'
+ ) in body
+ # Empty-state JS innerHTML — same string, no drift.
+ assert "Submit a skill or plugin →" in body
+ # Old wording must be gone — guards against partial rename.
+ assert ">Submit a plugin<" not in body
+
+
+def test_marketplace_guide_curated_page(fresh_db):
+ """Curated guide page documents the Named Curator handoff. Three-step
+ flow (find → handoff → publish) lives inside `.guide-steps`. The
+ fast-path callout points users at the flea guide as the lighter
+ review-bar alternative; the primary CTA at the bottom does the same
+ so users who skim past the callout still see the escape hatch."""
+ from src.db import get_system_db, close_system_db
+
+ conn = get_system_db()
+ try:
+ _, sess = _make_user_and_session(conn)
+ finally:
+ conn.close()
+ close_system_db()
+ resp = _client().get(
+ "/marketplace/guide/curated", cookies={"access_token": sess}
+ )
+ assert resp.status_code == 200
+ body = resp.text
+
+ # Title carries the new 'skill or plugin' wording.
+ assert "Submit a skill or plugin to Curated Marketplace" in body
+ # Lede surfaces the gatekeeping concept.
+ assert "Named Curators" in body
+ # Three-step ordered list under `.guide-steps`.
+ assert '
' in body
+ assert "Find a Curator" in body
+ assert "Hand off your skill or plugin" in body
+ assert "Curator publishes" in body
+ # Fast-path callout exists and the CTA inside it points at the flea
+ # guide (NOT /store/new directly — we want users to read the flea
+ # context before they upload).
+ assert '' in body
+ assert 'href="/marketplace/guide/flea"' in body
+ # Primary CTA at the bottom also surfaces the flea path.
+ assert 'class="primary" href="/marketplace/guide/flea"' in body
+
+
+def test_marketplace_guide_flea_page(fresh_db):
+ """Flea guide documents the /store/new self-service flow. Four-step
+ body (package → upload → automated review → published) replaces the
+ earlier stub. Primary CTA goes directly to /store/new since users
+ landing on the flea guide have already chosen the self-service path."""
+ from src.db import get_system_db, close_system_db
+
+ conn = get_system_db()
+ try:
+ _, sess = _make_user_and_session(conn)
+ finally:
+ conn.close()
+ close_system_db()
+ resp = _client().get(
+ "/marketplace/guide/flea", cookies={"access_token": sess}
+ )
+ assert resp.status_code == 200
+ body = resp.text
+
+ assert "Upload to Flea Market" in body
+ # Four-step ordered list (no fast-path callout on flea — it IS the
+ # fast path, the curated guide is what links here).
+ assert '
' in body
+ assert "Package what you" in body
+ assert "Upload via the form" in body
+ assert "Automated review" in body
+ assert "Published" in body
+ # Primary CTA goes straight to /store/new (flea is one click away
+ # from being live, no intermediate handoff).
+ assert 'class="primary" href="/store/new"' in body
+ # No fast-path callout here — sanity check the asymmetry sticks.
+ assert '' not in body