> ## Documentation Index
> Fetch the complete documentation index at: https://hubify.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Reproduce a Lab

> How Reproduce This Lab works: the two-path flow, the reproducedFrom provenance record, and the hard honesty rule enforced in code.

# Reproduce a Lab

**Reproduce This Lab** is the product moment behind every [lab manifest](/docs/concepts/lab-manifest): a way for a human or agent to inherit a lab's research. It has two paths, both implemented in `ReproduceSidepeek.tsx`, both honest about what they actually do.

<Warning>
  **Core rule, enforced in code, not just documented:** a reproduction inherits *provenance* — where it came from — and **never** inherits *verified state* — how far the source got. The source lab's `lab.yaml` might report `reproducibility.level: 2` with several levels `complete`. None of that transfers. The new lab's `lab.yaml` starts every level at `not_started` and headline `level: 0`, unconditionally, because the fresh copy has not independently verified anything yet.
</Warning>

## Path A — Verify (anonymous, zero-cost)

No account needed. This path never touches Convex:

```bash theme={null}
# 1. Clone the reproduction repo
git clone https://github.com/Hubify-Projects/bigbounce-lab

# 2. Verify committed artifacts against the Level 0 manifest
python3 tools/verify_artifacts.py

# Optional — rebuild the manifest against your own checkout
python3 tools/build_manifest.py
```

`verify_artifacts.py` exits nonzero on divergence. A divergence is surfaced, never silently regenerated — the script reports what changed, not a new "verified" hash. This is exactly the Level 2 (`artifacts_verified`) evidence described in [Reproducibility Levels](/docs/concepts/reproducibility-levels).

## Path B — Create my copy (signed-in)

Materializes a brand-new, user-owned lab from another lab's manifest. Signed-in only; the UI shows a sign-in prompt otherwise. Implemented as the Convex action `reproduceLabFromManifest` in `convex/functions/reproduce.ts`, called directly from the client via `useAction` — see [MCP and API access patterns](#mcp-and-api-access-patterns) for what that means for non-web-app callers.

<Steps>
  <Step title="Verify the user exists">
    Looked up by `userId`. If not found, the action returns `{ ok: false, reason: "no-user" }` immediately.
  </Step>

  <Step title="Create the lab record">
    A new `labs` row is created (`status: "active"`, `isPublic: false`), owned by the calling user. Title becomes `"<source title> (reproduction)"`.
  </Step>

  <Step title="Record provenance immediately">
    The `reproducedFrom` block is written to the new lab's Convex row right after creation — even if every later step (repo creation, scaffold write) fails. The lab honestly records where it was supposed to come from regardless of what happens next.
  </Step>

  <Step title="Create the GitHub repo">
    Same fallback chain as ordinary lab creation: the user's own repo-scoped GitHub token if connected (Pro), else the `GITHUB_ACCESS_TOKEN` `Hubify-Projects` org PAT. If neither exists, the action returns `{ ok: false, reason: "no-github-token", labId, slug }` — the lab record is kept (so the user doesn't lose their spot), and the UI shows an explicit "connect GitHub to finish" state rather than a fake success.
  </Step>

  <Step title="Bind the repo, then scaffold + write files">
    The standard lab scaffold is built (`buildScaffoldFiles`), plus a reproduction-specific `lab.yaml` (see below), then written to the new GitHub repo in one batch (`writeFileSetToGithub`).
  </Step>

  <Step title="Log the event">
    A `repo.scaffolded` event is appended to the lab's `agentEvents` ledger, with `reproducedFromSlug` and write counts in the payload. Severity is `warn` if any file failed to write, else `info`.
  </Step>
</Steps>

### Slug generation

The new lab's slug is `slugify("<sourceSlug>-repro")`. On collision, a random 4-character suffix is appended; on a second collision, a timestamp-derived suffix replaces it.

### Result shape

```ts theme={null}
{
  ok: boolean;
  reason?: string;       // set on any failure — see below
  labId?: Id<"labs">;
  slug?: string;
  repoUrl?: string;
  appUrl?: string;       // "/app?lab=<slug>"
  scaffold?: { written: number; updated: number; unchanged: number; errors: number };
}
```

| `reason` value                        | Meaning                                                  |
| ------------------------------------- | -------------------------------------------------------- |
| `no-user`                             | `userId` doesn't resolve to a real user.                 |
| `lab-create-failed: <msg>`            | The initial `labs` row insert failed.                    |
| `no-github-token`                     | Lab row exists; no usable GitHub token to create a repo. |
| `github-repo-create-<status>: <body>` | GitHub API rejected repo creation.                       |
| `bad-repo-url-returned-by-github`     | GitHub returned a repo URL that didn't parse.            |
| `scaffold-write-errors: <n>`          | Repo exists; some scaffold files failed to write.        |

## What `reproducedFrom` records

`reproducedFrom` is a purely additive, fully optional block on the `labs` Convex table (`convex/schema.ts`) — every field is `v.optional`, and it's only ever set on labs created through this flow:

| Field                 | Type   | Notes                                                                                                         |
| --------------------- | ------ | ------------------------------------------------------------------------------------------------------------- |
| `sourceSlug`          | string | `labs-registry` slug of the source lab, e.g. `"bigbounce"`.                                                   |
| `sourceTitle`         | string | Source lab's display title at the time of reproduction.                                                       |
| `sourceRepo`          | string | Source manifest's `source.authoritative_repo`.                                                                |
| `baselineCommit`      | string | Source manifest's `source.baseline_commit` at the time of reproduction.                                       |
| `sourceManifestLevel` | number | Source's `reproducibility.level` **at the time of reproduction — record only, never applied to the new lab.** |
| `reproducedAt`        | number | Unix ms timestamp.                                                                                            |

<Tip>
  The same provenance is also written into the *generated* `lab.yaml` inside the new repo, under a `reproduced_from:` block — but it isn't identical. The generated YAML's `reproduced_from` carries `lab_slug`, `lab_title`, `reproduced_at`, and `via: hubify.com "Reproduce This Lab"`; `baseline_commit` lands in that file's `source:` block instead, and `sourceManifestLevel` isn't written into the YAML at all — only into the Convex row, since the YAML's own `reproducibility.level` is always freshly `0`. If you're reconciling the two records, don't expect a field-for-field match.
</Tip>

The generated `lab.yaml` itself always looks like this, regardless of what the source reported:

```yaml theme={null}
hubify_lab: 0.1

lab:
  slug: <new-slug>
  title: <source title> (reproduction)
  tagline: >-
    <source tagline, or "A Hubify reproduction of <source title>." if none given>
  homepage: <new repo URL>
  author: Hubify captain
  license: TBD

source:
  authoritative_repo: <same upstream repo the source lab points at>
  baseline_commit: <source's baseline_commit>

reproduced_from:
  lab_slug: <source slug>
  lab_title: <source title>
  reproduced_at: <ISO 8601 timestamp>
  via: hubify.com "Reproduce This Lab"

reproducibility:
  level: 0
  levels:
    0: { name: archived, status: not_started, note: "Not started — fresh reproduction; nothing independently verified yet." }
    # ...one entry per level name from the source manifest, all not_started

programs: []

surfaces:
  api: { status: not_configured }
  mcp: { status: not_configured }
  skills: { status: not_configured }
  site: { status: not_deployed, url: "" }
```

`source.authoritative_repo` points at the **same ultimate upstream repo** the source lab points at — the reproduction traces back to the original science, not to the intermediate sanitized `-lab` clone repo the user actually cloned from in Path A.

## MCP and API access patterns

This is the surface with the fewest integration points in Hubify today. Documenting it accurately means documenting the gaps:

* **No MCP tool wraps `reproduceLabFromManifest`.** The MCP server's `lab_create` tool exists, but it calls a different Convex action (`functions/github:createLabWithRepo`) — an ordinary new lab, unrelated to any manifest or source lab. There is no MCP tool that reads a `lab.yaml`, and no MCP tool that reproduces one.
* **No CLI command triggers reproduction.** The CLI's `hubify lab create` (interactive prompt) also calls the plain `functions/labs:create` mutation — same gap as the MCP tool above.
* **No public REST endpoint calls it either.** `reproduceLabFromManifest` is invoked directly as a Convex action from the browser (`useAction(api.functions.reproduce.reproduceLabFromManifest)` in `ReproduceSidepeek.tsx`). It is, today, a signed-in, web-app-only surface.
* **`lab.yaml` itself has no serving surface at all.** `loadLabManifest`/`listRegisteredLabs` are server-side-only Next.js functions reading `labs-registry/` off disk; there is no API route, MCP tool, or CLI command that returns manifest content. Reading a manifest today means cloning or browsing the `hubify` repo directly.

Once a lab *has* been reproduced, its provenance is only partially visible through existing read surfaces, because Convex queries return the full row with no field-stripping — but not every surface actually asks for the full row:

| Surface                                                          | Includes `reproducedFrom`? | Why                                                                                       |
| ---------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------- |
| `GET /api/v1/labs` (see [Labs API](/docs/api/labs))                   | Yes                        | Backed by `api.functions.labs.list` / `getBySlug`, which return the full `labs` document. |
| `api.functions.labs.getById` / `getBySlug` (direct Convex query) | Yes                        | Same — full document, no projection.                                                      |
| MCP `lab_list`                                                   | No                         | Only prints `slug` / `name` / `status` as formatted text.                                 |
| MCP `lab_status`                                                 | No                         | Calls `functions/stats:labStats`, a separate aggregate-stats query — not the raw lab row. |
| CLI `hubify labs` / `hubify lab info`                            | No                         | Same aggregate-stats path as MCP `lab_status`; doesn't request `reproducedFrom`.          |

So today, the only way to programmatically see whether a lab was reproduced — and from what — is `GET /api/v1/labs` (or a direct Convex query against `labs`). It is not visible through the MCP server or the CLI.
