Skip to main content

Reproduce a Lab

Reproduce This Lab is the product moment behind every 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.
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.

Path A — Verify (anonymous, zero-cost)

No account needed. This path never touches Convex:
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.

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 for what that means for non-web-app callers.
1

Verify the user exists

Looked up by userId. If not found, the action returns { ok: false, reason: "no-user" } immediately.
2

Create the lab record

A new labs row is created (status: "active", isPublic: false), owned by the calling user. Title becomes "<source title> (reproduction)".
3

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.
4

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.
5

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).
6

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.

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

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:
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.
The generated lab.yaml itself always looks like this, regardless of what the source reported:
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: 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.