diff --git a/CLAUDE.md b/CLAUDE.md
index a86b93ee..7341923b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -495,6 +495,15 @@ fix flows to every page automatically; two hand-written HTML pages never will.
`Studio._urlSuppress`. URL writing is NOT a protocol mutation — never route
it through `pushUndo`. Any NEW shareable state must flow through
`encodeApp()` + `Studio.updateUrl` (never hand-build `location.search`).
+ **`?repo=` alone is a tab-scoped repo override** (LAB-227 Phase 4): `Studio.sessionRepo`
+ is parsed in the classic shell and `courseSettings()` returns it ahead of the stored
+ `studio_gh_repo` and the default (`Studio.DEFAULT_COURSE_REPO` = the private
+ `reiserlab/arena-experiments`; the Pattern Designer's `DEFAULT_DATA_REPO` and the
+ dashboard's `DEFAULT_REPO` carry the same string, pinned by `tests/test-studio-shell.js`).
+ Every repo consumer reads `courseSettings().repo` — never `localStorage` directly — so the
+ override wins for previews, Open from Repo, vocab, replay AND saves; it is never written to
+ storage, `encodeApp({sessionRepo})` keeps it across the canonical rewrite, and typing a
+ repo in ⚙ Settings ends it.
- **Top bar: Protocol ▾ vs ⚙ Settings.** `Protocol ▾` (`#fileMenu`, renamed from File)
holds protocol ACTIONS only (new/open/replay/save/promote/copy/reset/soak). `⚙ Settings`
(`#settingsMenu`) holds the Studio/bench settings kept in the browser, not the YAML: the
diff --git a/arena_studio.html b/arena_studio.html
index 883d0026..3678e5fa 100644
--- a/arena_studio.html
+++ b/arena_studio.html
@@ -2390,7 +2390,7 @@
drives stays visible here (clicking opens ⚙ Settings). Text/title set by syncRigContext(). -->
rig —⚠ bench ≠ protocol
- ⚠ bench ≠ roster
+ ⚠ bench ≠ rosterQwiic
@@ -2420,7 +2420,7 @@
Open protocol
-
+
Save & share
@@ -2472,12 +2472,12 @@
-
+
-
-
+
+
-
+
→ writes a local .yaml file (sign in to GitHub to open a PR instead)
@@ -2681,7 +2681,7 @@
Sequence · test any single condition ▶ Clear
-
+
@@ -3551,22 +3551,47 @@
Import error
Studio.ghToken = function () {
return sessionStorage.getItem('studio_gh_pat') || localStorage.getItem('studio_gh_pat') || null;
};
- // Default course-data repo — pre-filled in the sign-in Repo field and used
- // by courseSettings when the bench hasn't set its own, so the course flow
- // (save/open-course/runlogs/roster/genotypes) targets it out of the box.
- // Overridable: type another owner/name and it's stored in localStorage.
- Studio.DEFAULT_COURSE_REPO = 'reiserlab/cshl-2026-course';
+ // Default data repo (LAB-227, 2026-11): the lab's PRIVATE reiserlab/arena-experiments —
+ // pre-filled in the Repo field and used by courseSettings when this browser has not
+ // stored its own, so saves / Open from Repo / run logs / roster / vocab target it out
+ // of the box. Overridable: type another owner/name (the course benches and rig03-sr
+ // still write to reiserlab/cshl-2026-course) and it is stored in localStorage. The
+ // Pattern Designer (DEFAULT_DATA_REPO) and the dashboard (DEFAULT_REPO) carry the
+ // same string; tests/test-studio-shell.js pins the three equal.
+ Studio.DEFAULT_COURSE_REPO = 'reiserlab/arena-experiments';
+ // ?repo=owner/name is a SESSION override of the stored repo for THIS TAB only (never
+ // written to localStorage — the Pattern Designer has the same rule). Parsed here, in
+ // the classic shell, so it is in force before the page-start listings run (ww18
+ // 2026-10-29: signed out, `?repo=` wired the DEFAULT repo's shared pattern previews
+ // because the override existed only for `?repo=&p=` loads). Every consumer of the
+ // "course repo" goes through courseSettings(), so the override wins everywhere —
+ // reads AND writes, signed in or out. Typing a repo in ⚙ Settings ends it.
+ Studio.sessionRepo = (function () {
+ try {
+ const r = new URLSearchParams(location.search).get('repo');
+ return r && /^([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})$/.test(r) && !r.includes('..')
+ ? r
+ : null;
+ } catch (_) {
+ return null;
+ }
+ })();
Studio.courseSettings = function () {
- const raw = (localStorage.getItem('studio_gh_repo') || Studio.DEFAULT_COURSE_REPO).trim();
+ const raw = (
+ Studio.sessionRepo ||
+ localStorage.getItem('studio_gh_repo') ||
+ Studio.DEFAULT_COURSE_REPO
+ ).trim();
const m = /^([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})$/.exec(raw);
return {
repo: m && !raw.includes('..') ? { owner: m[1], name: m[2], full: raw } : null,
direct: localStorage.getItem('studio_gh_direct') === '1',
archivePatterns: localStorage.getItem('studio_archive_patterns') === '1',
- benchId: (localStorage.getItem('studio_bench_id') || '').trim()
+ benchId: (localStorage.getItem('studio_bench_id') || '').trim(),
+ repoFromUrl: !!Studio.sessionRepo // this tab follows its ?repo= link
};
};
- // RIG-ID: the instructor-set bench id — namespaces course-repo protocol
+ // RIG-ID: the rig id (localStorage key `studio_bench_id` keeps its historical name) — namespaces course-repo protocol
// saves and run-log commits. Identity comes from THIS (not the controller
// MAC, which only cross-checks it against roster.yaml at connect).
Object.defineProperty(Studio, 'rigId', {
@@ -4035,7 +4060,7 @@
Import error
// outcome is shown in the modal so a push is never silent/mysterious.
showUploadModal('Uploading run log…', { busy: true, msg: 'preparing…' });
if (!cs.benchId) {
- showUploadModal('Run log not committed', { error: true, msg: 'Bench id not set — see instructor (⚙ Settings → Bench id).' });
+ showUploadModal('Run log not committed', { error: true, msg: 'Rig id not set — set it in ⚙ Settings → Rig id (🔓 to unlock).' });
return;
}
const bridge = Studio.session && Studio.session.bridge;
@@ -4683,6 +4708,9 @@
Import error
// validated ?repo= load or an in-app course-repo open/save.
repo: repoRef ? repoRef.repo : null,
repoPath: repoRef ? repoRef.path : null,
+ // A bare ?repo= (tab-scoped repo override, see courseSettings) survives the
+ // canonical rewrite so a refresh keeps the tab on the requested repo.
+ sessionRepo: Studio.sessionRepo || null,
// #135: only an EXPLICIT session-rig selection is shareable; a rig
// derived from the loaded protocol is redundant with `p` (clean URL).
rigKey: Studio.currentRig && Studio.currentRig.explicit ? Studio.currentRig.name : null,
@@ -8872,14 +8900,14 @@
Import error
if (!cs.benchId) return;
const mine = entries.find((p) => p && p.rig_id === cs.benchId);
if (!mine) {
- Studio.rawLog('course roster: no entry for bench id "' + cs.benchId + '" — experimenter not pre-filled', 'warn');
+ Studio.rawLog('course roster: no entry for rig id "' + cs.benchId + '" — experimenter not pre-filled', 'warn');
return;
}
// Pre-fill (typo-proofing) — never clobber something already typed.
const exp = $('mExp');
if (exp && !exp.value.trim() && (mine.id || mine.name)) {
exp.value = mine.id || mine.name;
- Studio.rawLog('experimenter pre-filled from roster: ' + exp.value + ' (bench ' + cs.benchId + ')', 'info');
+ Studio.rawLog('experimenter pre-filled from roster: ' + exp.value + ' (rig ' + cs.benchId + ')', 'info');
Studio.updateRunGate();
}
// MAC cross-check — bench id claims bench03 but the controller matches
@@ -8890,8 +8918,8 @@
Import error
if (norm(mac) !== norm(mine.mac)) {
chip.style.display = 'inline-block';
chip.title = 'Controller MAC ' + mac + ' ≠ roster entry for ' + cs.benchId + ' (' + mine.mac +
- ') — a wrong bench id would namespace saves/run-logs under another bench. See instructor.';
- Studio.rawLog('⚠ roster: bench id ' + cs.benchId + ' lists MAC ' + mine.mac + ' but the controller reports ' + mac, 'warn');
+ ') — a wrong rig id would namespace saves/run-logs under another rig.';
+ Studio.rawLog('⚠ roster: rig id ' + cs.benchId + ' lists MAC ' + mine.mac + ' but the controller reports ' + mac, 'warn');
}
}
}
@@ -8901,17 +8929,36 @@
Import error
// `genotypes:` list, same shape as configs/metadata/genotypes.yaml) becomes
// the fly-genotype vocabulary for the session — replacing the lab-wide list
// and repointing the "↗ source" link. Independent of bench id / roster.
+// Per-rig vocabularies (LAB-227 Phase 4): a rig may keep its own list at
+// protocols// (its protocol folder, already a readable prefix in
+// js/studio-github.js); the repo ROOT file is the lab-wide fallback. A missing
+// rig-level file is the normal case and stays silent — only a root miss is
+// reported by the caller. Returns {ok, path, bytes} or {ok:false, status}.
+async function readCourseVocabFile(cs, token, file) {
+ const paths = [];
+ if (cs.benchId && /^[A-Za-z0-9._-]+$/.test(cs.benchId)) paths.push('protocols/' + cs.benchId + '/' + file);
+ paths.push(file);
+ let last = null;
+ for (const path of paths) {
+ const res = await GH.runBytes(fetch, GH.reqGetContentsRaw(cs.repo.owner, cs.repo.name, path, null, token));
+ if (res.ok) return { ok: true, path: path, bytes: res.bytes };
+ last = res;
+ }
+ return { ok: false, status: last ? last.status : 0 };
+}
async function fetchCourseGenotypes() {
const cs = Studio.courseSettings();
const token = ghToken();
- if (!cs.repo || !token) return; // no course repo — keep the lab-wide list
+ if (!cs.repo || !token) return; // no data repo — keep the lab-wide list
let list;
+ let src;
try {
- const res = await GH.runBytes(fetch, GH.reqGetContentsRaw(cs.repo.owner, cs.repo.name, 'genotypes.yaml', null, token));
+ const res = await readCourseVocabFile(cs, token, 'genotypes.yaml');
if (!res.ok) {
Studio.rawLog('course genotypes: no genotypes.yaml in ' + cs.repo.full + ' (HTTP ' + res.status + ') — using the lab list', 'warn');
return;
}
+ src = res.path;
const data = YAML.parse(new TextDecoder().decode(res.bytes));
list = (data && data.genotypes) || [];
} catch (e) {
@@ -8922,10 +8969,10 @@
Import error
renderGenoVocab(list);
const link = $('mGenoSrc');
if (link) {
- link.href = courseFileUrl(cs, 'genotypes.yaml');
- link.title = 'Course genotype list (' + cs.repo.full + '/genotypes.yaml)';
+ link.href = courseFileUrl(cs, src);
+ link.title = 'Genotype list (' + cs.repo.full + '/' + src + ')';
}
- Studio.rawLog('genotype list from the course repo (' + cs.repo.full + '/genotypes.yaml, ' + list.length + ' entries)', 'info');
+ Studio.rawLog('genotype list from the data repo (' + cs.repo.full + '/' + src + ', ' + list.length + ' entries)', 'info');
}
// ---- generic course-repo vocab (CLAUDE.md rule: connected repo is the source of
@@ -8940,12 +8987,14 @@
Import error
const token = ghToken();
if (!cs.repo || !token) return; // not connected to a repo — keep the lab list
let list;
+ let src;
try {
- const res = await GH.runBytes(fetch, GH.reqGetContentsRaw(cs.repo.owner, cs.repo.name, file, null, token));
+ const res = await readCourseVocabFile(cs, token, file); // protocols// first, then root
if (!res.ok) {
Studio.rawLog('course ' + key + ': no ' + file + ' in ' + cs.repo.full + ' (HTTP ' + res.status + ') — using the lab list', 'warn');
return;
}
+ src = res.path;
const data = YAML.parse(new TextDecoder().decode(res.bytes));
list = (data && data[key]) || [];
} catch (e) {
@@ -8956,10 +9005,10 @@
Import error
apply(list);
const link = $(srcId);
if (link) {
- link.href = courseFileUrl(cs, file);
- link.title = 'Course ' + key + ' list (' + cs.repo.full + '/' + file + ')';
+ link.href = courseFileUrl(cs, src);
+ link.title = key + ' list (' + cs.repo.full + '/' + src + ')';
}
- Studio.rawLog(key + ' list from the course repo (' + cs.repo.full + '/' + file + ', ' + list.length + ' entries)', 'info');
+ Studio.rawLog(key + ' list from the data repo (' + cs.repo.full + '/' + src + ', ' + list.length + ' entries)', 'info');
}
// ---- pattern set / name picker (Console) + Run pattern resolution ----
@@ -11471,7 +11520,7 @@
Import error
if (!token) { Studio.showBanner('Sign in to GitHub first.'); return; }
if (!cs.repo) { Studio.showBanner('Set the target repo (owner/name) in ⚙ Settings first.'); return; }
if (!cs.benchId) {
- Studio.showBanner('Course-repo save blocked: bench id not set — see instructor (⚙ Settings → Bench id).');
+ Studio.showBanner('Repo save blocked: rig id not set — set it in ⚙ Settings → Rig id (🔓 to unlock).');
return;
}
if (!ensureSaveFilename(doc)) return;
@@ -11685,7 +11734,7 @@
Import error
const saveAs = $('fmSaveAs');
if (direct) {
$('ghDest').textContent = '→ commits straight to ' + cs.repo.full + ' /protocols/' +
- (cs.benchId || '⚠ set bench id') + '/ (default branch, no PR)';
+ (cs.benchId || '⚠ set rig id') + '/ (default branch, no PR)';
$('fmSave').textContent = 'Save → course repo';
if (saveAs) saveAs.textContent = 'Save as → course repo…';
} else if (pr) {
@@ -11737,19 +11786,27 @@
Import error
const direct = $('ghDirect');
const bench = $('ghBenchId');
if (!repoIn || !direct || !bench) return;
- repoIn.value = localStorage.getItem('studio_gh_repo') || Studio.DEFAULT_COURSE_REPO;
+ repoIn.value = Studio.sessionRepo || localStorage.getItem('studio_gh_repo') || Studio.DEFAULT_COURSE_REPO;
+ if (Studio.sessionRepo) {
+ repoIn.title += ' — this tab follows its ?repo= link (' + Studio.sessionRepo + '); typing another repo ends that';
+ }
direct.checked = localStorage.getItem('studio_gh_direct') === '1';
bench.value = localStorage.getItem('studio_bench_id') || '';
repoIn.addEventListener('change', () => {
const v = repoIn.value.trim();
if (v && !(window.StudioUrlState && window.StudioUrlState.isSafeRepo(v))) {
- Studio.showBanner('Repo must be owner/name — e.g. reiserlab/cshl-2026-course.');
- repoIn.value = localStorage.getItem('studio_gh_repo') || Studio.DEFAULT_COURSE_REPO;
+ Studio.showBanner('Repo must be owner/name — e.g. reiserlab/arena-experiments.');
+ repoIn.value = Studio.sessionRepo || localStorage.getItem('studio_gh_repo') || Studio.DEFAULT_COURSE_REPO;
return;
}
// '' (not removeItem): an explicit clear must survive reloads — only a
- // never-set key gets the course-repo default seed above.
+ // never-set key gets the data-repo default seed above.
localStorage.setItem('studio_gh_repo', v);
+ // Typing a repo ends this tab's ?repo= override (and drops it from the URL).
+ if (Studio.sessionRepo) {
+ Studio.sessionRepo = null;
+ if (Studio.updateUrl) Studio.updateUrl({ push: false });
+ }
updateSaveLabel();
// Repo changed → re-pull that repo's metadata (roster/genotypes/links).
if (Studio.refreshCourseMeta) Studio.refreshCourseMeta();
@@ -11794,13 +11851,13 @@
Import error
if (signIn) {
signIn.classList.toggle('gh-signin-while-locked', locked);
signIn.title = locked
- ? 'Sign in for course-repo read access; repo, bench, and write settings remain instructor-locked'
+ ? 'Sign in for data-repo read access; repo, rig id, and write settings remain locked (🔓 to edit)'
: 'Sign in to GitHub with a personal access token (fine-grained for org members; classic for the shared course account) — see docs/development/data-repo-token-runbook.md';
}
if (ghLock) {
ghLock.textContent = locked ? '🔒' : '🔓';
ghLock.title = locked
- ? 'Bench GitHub settings are LOCKED (kiosk-safe) — click to unlock and edit the token / repo / bench id'
+ ? 'Bench GitHub settings are LOCKED (kiosk-safe) — click to unlock and edit the token / repo / rig id'
: 'Bench GitHub settings UNLOCKED — set them, then click to re-lock (locks again on reload)';
}
}
@@ -12235,7 +12292,15 @@
Import error
try {
const res = await GH.run(fetch, GH.reqGetContents(repo.owner, repo.name, 'patterns', null, token));
if (gen !== sharedPreviewGen) return; // a newer listing (repo change / sign-in) was requested
- if (!res.ok || !Array.isArray(res.data)) return;
+ if (!res.ok || !Array.isArray(res.data)) {
+ // Signed out, a private repo (the lab default) answers 404 — say so once, so a
+ // missing thumbnail is traceable to the sign-in state, not to the pattern.
+ if (!token && res && [401, 403, 404].includes(res.status)) {
+ Studio.rawLog('shared pattern library: ' + repo.full + '/patterns/ is not readable signed out (HTTP ' +
+ res.status + ') — sign in if this repository is private', 'info');
+ }
+ return;
+ }
const entries = res.data
.filter((f) => f && f.type === 'file' && /\.pat$/i.test(f.name))
.map((f) => [f.name, () => repoFileBytes(repo, f.path)]);
@@ -12246,7 +12311,7 @@
Import error
const sel = $('cPatName');
if (sel && sel.value) renderPatThumb(sel.value);
if (Studio.editor && Studio.editor.repaintLibrary) Studio.editor.repaintLibrary();
- Studio.rawLog('course repo: ' + n + ' shared pattern preview(s) wired from patterns/', 'info');
+ Studio.rawLog('shared pattern library: ' + n + ' preview(s) wired from ' + repo.full + '/patterns/', 'info');
}
} catch (_) { /* missing/private/unavailable shared library — previews stay best-effort */ }
}
@@ -12489,7 +12554,7 @@
Import error
const token = ghToken();
const doc = Studio.currentDoc;
if (!cs.repo || !token) { Studio.showBanner('Course repo + GitHub sign-in required to promote.'); return; }
- if (!cs.benchId) { Studio.showBanner('Promote blocked: bench id not set — see instructor.'); return; }
+ if (!cs.benchId) { Studio.showBanner('Promote blocked: rig id not set (⚙ Settings → Rig id).'); return; }
if (!doc || Studio.isUntitledDoc(doc)) { Studio.showBanner('Open (and save) a protocol first.'); return; }
const base = GH.slug((doc.filename || 'protocol.yaml').replace(/\.ya?ml$/i, ''));
const srcYaml = 'protocols/' + cs.benchId + '/' + base + '.yaml';
@@ -18085,7 +18150,7 @@
Import error
psEl.commitBtn.disabled = n === 0 || invalid > 0 || !dir;
psEl.commitBtn.title = dir
? 'Direct-commit the set to ' + dir + '/ in the course repo (identical files skipped; a different file under an existing name refuses)'
- : 'Save the protocol to the course repo first — the set commits into its _patterns/ folder (needs sign-in + bench id)';
+ : 'Save the protocol to the course repo first — the set commits into its _patterns/ folder (needs sign-in + rig id)';
}
}
@@ -18131,7 +18196,7 @@
Import error
const token = ghToken();
if (!token || !cs.repo) { showError('Not signed in', 'Sign in to GitHub and set the course repo (⚙ Settings) first.'); return; }
const dir = psProtoDir();
- if (!dir) { showError('No destination', 'Save the protocol to the course repo first — the set commits next to its YAML (needs a bench id).'); return; }
+ if (!dir) { showError('No destination', 'Save the protocol to the course repo first — the set commits next to its YAML (needs a rig id).'); return; }
let bundle;
try { bundle = window.PatternSet.buildBundle(psState.set); }
catch (e) { showError('Set not exportable', e.message || String(e)); return; }
@@ -18640,7 +18705,7 @@
Import error
// The two save-mode checkboxes: put the help on the whole label row.
(function(){ var pr=document.getElementById('ghPR'), di=document.getElementById('ghDirect');
if(pr&&pr.closest('label')) pr.closest('label').setAttribute('data-help','Saving opens a Pull Request for review instead of writing directly.');
- if(di&&di.closest('label')) di.closest('label').setAttribute('data-help','Saving commits straight to the repo’s main branch — the usual course setup (each bench writes only its own files).');
+ if(di&&di.closest('label')) di.closest('label').setAttribute('data-help','Saving commits straight to the repo’s main branch — the usual rig setup (each rig writes only its own files).');
})();
// Panel titles: student text beside the always-on engineer hover.
var PANEL_HELP = {
@@ -18731,7 +18796,10 @@
Import error
var q=[];
var arena=window.Studio && Studio.currentRig && Studio.currentRig.arenaConfig;
if(arena) q.push('arena='+encodeURIComponent(arena));
- var repo=(localStorage.getItem('studio_gh_repo')||'').trim();
+ // The effective data repo — default, stored, or this tab's ?repo= override — so the
+ // Designer's "Open from Repo" lists the same repo the Studio is on.
+ var cs=window.Studio && Studio.courseSettings ? Studio.courseSettings() : null;
+ var repo=cs && cs.repo ? cs.repo.full : '';
if(repo) q.push('repo='+encodeURIComponent(repo));
return 'pattern_editor.html'+(q.length?'?'+q.join('&'):'');
}
diff --git a/dashboard/data-browser/README.md b/dashboard/data-browser/README.md
index 44010916..07157e87 100644
--- a/dashboard/data-browser/README.md
+++ b/dashboard/data-browser/README.md
@@ -128,9 +128,11 @@ relative-heading pages instead of failing import.
## Public page, private data
-The dashboard itself is public. Runlogs live in `reiserlab/cshl-2026-course`,
-which was private during the course and has been public since 2026-08 (reads work
-signed-out; writes still need a token).
+The dashboard itself is public. Runlogs live in the lab's private data repo
+`reiserlab/arena-experiments` (the default in the repo field since LAB-227 Phase 4;
+every read needs a token). The 2026 course's logs stay in `reiserlab/cshl-2026-course`,
+public since 2026-08 (reads work signed-out) — type that repo into the field to browse
+them.
The sign-in flow uses:
diff --git a/dashboard/data-browser/app.js b/dashboard/data-browser/app.js
index 8242ac58..a3e16528 100644
--- a/dashboard/data-browser/app.js
+++ b/dashboard/data-browser/app.js
@@ -289,7 +289,7 @@ function updateGithubUi(user) {
const repo = G.parseRepo(els.githubRepoInput.value);
els.courseRepoLink.href = `https://github.com/${repo.full}`;
} catch (_) {
- els.courseRepoLink.href = 'https://github.com/reiserlab/cshl-2026-course';
+ els.courseRepoLink.href = `https://github.com/${G.DEFAULT_REPO}`;
}
}
@@ -951,7 +951,7 @@ async function browseGithub() {
function normalizeDirectoryUrl(value) {
const url = new URL(
- A.safeText(value).trim() || '/cshl-2026-course/runlogs/',
+ A.safeText(value).trim() || '/arena-experiments/runlogs/',
window.location.href
);
if (!url.pathname.endsWith('/')) url.pathname += '/';
diff --git a/dashboard/data-browser/github-repo.js b/dashboard/data-browser/github-repo.js
index fd3b1ada..aa2f70b5 100644
--- a/dashboard/data-browser/github-repo.js
+++ b/dashboard/data-browser/github-repo.js
@@ -7,7 +7,10 @@
const REPO_KEY = 'studio_gh_repo';
const BENCH_KEY = 'studio_bench_id';
const FOLDERS_KEY_PREFIX = 'dashboard_runlog_folders:';
- const DEFAULT_REPO = 'reiserlab/cshl-2026-course';
+ // The lab's private data repo (LAB-227) — same default as Arena Studio and the
+ // Pattern Designer; the 2026 course's logs stay in reiserlab/cshl-2026-course
+ // (type it into the repo field). tests/test-studio-shell.js pins the three equal.
+ const DEFAULT_REPO = 'reiserlab/arena-experiments';
// Run logs are committed as `.jsonl.gz` since Studio v0.72; fetchRaw reads
// BYTES and lets the shared format module inflate (magic-detected, so a raw
// `.jsonl` still works). Vendored byte-identical copy of js/runlog-format.js.
diff --git a/dashboard/data-browser/index.html b/dashboard/data-browser/index.html
index 46a89c53..a6a226d2 100644
--- a/dashboard/data-browser/index.html
+++ b/dashboard/data-browser/index.html
@@ -21,7 +21,7 @@
diff --git a/docs/development/data-repo-token-runbook.md b/docs/development/data-repo-token-runbook.md
index 5549bb17..79025e23 100644
--- a/docs/development/data-repo-token-runbook.md
+++ b/docs/development/data-repo-token-runbook.md
@@ -1,9 +1,13 @@
# Data-repo tokens — renewal and setup runbook
Arena Studio, the Pattern Designer and the data-browser dashboard write protocols,
-patterns and run logs to a GitHub **data repo** (the CSHL course repo today; the lab's
-`reiserlab/arena-experiments` next) using a personal access token pasted once per
-browser (⚙ Settings → GitHub → **Sign in…**). The token is stored in that browser only
+patterns and run logs to a GitHub **data repo** — the lab's private
+`reiserlab/arena-experiments` by default (LAB-227 Phase 4, 2026-11); the CSHL course
+benches and `rig03-sr` type `reiserlab/cshl-2026-course` into the Repo field instead —
+using a personal access token pasted once per browser (⚙ Settings → GitHub →
+**Sign in…**). A `?repo=owner/name` link overrides the stored repo for that tab only.
+The rig's folder name is its **Rig id** (⚙ Settings; the course benches use
+`bench01`…`bench07`). The token is stored in that browser only
(`studio_gh_pat`, sessionStorage or localStorage) and sent only as an
`Authorization` header. This page is the operational how-to; design background is in
`studio-github-save-proposal.md`, the course-week checklist in
@@ -38,9 +42,10 @@ the stored token; a save or run-log commit made with a dead token falls back to
`ghp_…` value once; store it in the lab password manager.
4. On each bench browser (served from `http://127.0.0.1:8000/`, not `file://`):
⚙ Settings → **Sign in…** → paste → **YES** to "Remember this token" → 🛡 advanced
- (instructor password) → 🔓 in the GitHub block → **Repo** `reiserlab/cshl-2026-course`,
- **Bench id** `bench01`…`bench07` (must match `roster.yaml`), tick **Commit
- directly to default branch** → 🔒. Connect the arena; the roster pre-fills the
+ (instructor password) → 🔓 in the GitHub block → **Repo** `reiserlab/cshl-2026-course`
+ (the field is pre-filled with the lab repo — overwrite it), **Rig id**
+ `bench01`…`bench07` (must match `roster.yaml`), tick **Commit directly to default
+ branch** → 🔒. Connect the arena; the roster pre-fills the
experimenter.
5. Check: the run log shows `ages list from the course repo (… 5 entries)` and no
"unreadable" lines; a test save lands in `protocols//`.
@@ -63,8 +68,10 @@ the stored token; a save or run-log commit made with a dead token falls back to
6. In the Studio: ⚙ Settings → **Sign in…** → paste. Answer **YES** to "Remember" only on a
rig computer you control or your own laptop; answer **NO** (session-only) on a
shared or borrowed machine. **Sign out** clears the token everywhere.
-7. Set **Repo** and your **Rig id** once (🔓 needed); saves go to `protocols//`,
- run logs to `runlogs//`, and your commits carry your GitHub identity.
+7. Set your **Rig id** once (🔓 needed; **Repo** is already `reiserlab/arena-experiments`);
+ saves go to `protocols//`, run logs to `runlogs//`, and your commits
+ carry your GitHub identity. A rig may keep its own genotype / age / sex / fly-number
+ lists at `protocols//.yaml`; the Studio reads those before the root files.
Sharing across the lab needs no extra permission: everyone with a token sees every
rig's folder, **Protocol ▾ → Promote to shared…** copies a protocol and its patterns into
diff --git a/js/studio-replay.js b/js/studio-replay.js
index 66f4233f..9da649fc 100644
--- a/js/studio-replay.js
+++ b/js/studio-replay.js
@@ -1246,7 +1246,18 @@
setProtoLine('Downloading ' + name + ' from ' + repo.full + '…');
const bytes = await readRepoBytes(repo, path);
if (token !== R.loadToken) return false;
- if (!bytes) throw new Error(path + ' was not found in ' + repo.full + '.');
+ // Signed out, GitHub answers 404 for a private repo exactly as for a
+ // missing file — say so (the lab's default data repo is private).
+ if (!bytes) {
+ throw new Error(
+ path +
+ ' was not found in ' +
+ repo.full +
+ (ghToken()
+ ? '.'
+ : ' (HTTP 404) — sign in if this repository is private.')
+ );
+ }
const text = Fmt
? await Fmt.readRunlogText(bytes)
: new TextDecoder().decode(bytes);
@@ -2111,7 +2122,11 @@
if (!hit) {
if (Studio.showBanner)
Studio.showBanner(
- 'Replay: run ' + s.runId + ' was not found in ' + repo.full + '.'
+ 'Replay: run ' +
+ s.runId +
+ ' was not found in ' +
+ repo.full +
+ (ghToken() ? '.' : ' — sign in if this repository is private.')
);
return false;
}
diff --git a/js/studio-url-state.js b/js/studio-url-state.js
index 25fb8800..c649979d 100644
--- a/js/studio-url-state.js
+++ b/js/studio-url-state.js
@@ -240,8 +240,9 @@
* is bench identity, NOT doc state — emitted whenever present (the caller
* only passes an explicit selection), regardless of doc locality. `repo`
* is emitted ONLY alongside a valid repo-path `p` (a repo ref without a
- * document to point at is bench localStorage config, not URL state).
- * @param {object} state {mode, p, lib, set, rig, repo, source?: 'local'|'committed'}
+ * document to point at is bench localStorage config, not URL state) —
+ * except `sessionRepo`, the tab-scoped override a bare ?repo= link set.
+ * @param {object} state {mode, p, lib, set, rig, repo, sessionRepo, source?: 'local'|'committed'}
*/
function encode(state) {
const s = state || {};
@@ -261,6 +262,11 @@
} else if (!local && isSafeKey(s.p)) {
params.set('p', s.p);
}
+ // sessionRepo: the tab-scoped repo override (a bare ?repo=, Studio.sessionRepo) —
+ // kept so a refresh keeps the tab on the requested repo; only when no repo-bound
+ // doc or replay already names the repo above.
+ if (!replayMode && !repoMode && isSafeRepo(s.sessionRepo))
+ params.set('repo', s.sessionRepo);
if (!replayMode && isSafeKey(s.lib)) params.set('lib', s.lib);
if (!replayMode && !local && isSafeKey(s.set)) params.set('set', s.set);
if (isSafeKey(s.rig)) params.set('rig', s.rig);
@@ -293,7 +299,10 @@
* `advanced` must be true ONLY when advanced mode is active AND was
* URL-requested (the caller enforces this; a remembered-unlocked browser
* passes false to keep a clean URL, mirroring the derived-rig rule).
- * @param {object} app {mode, protocolKey, rigKey, repo, repoPath, advanced}
+ * `sessionRepo` is the tab-scoped repo override (Studio.sessionRepo, from a
+ * bare ?repo= link): emitted as `repo` only when no repo-bound doc or replay
+ * owns the link.
+ * @param {object} app {mode, protocolKey, rigKey, repo, repoPath, sessionRepo, advanced}
* @returns {string} query string (leading '?', or '' when all defaults)
*/
function encodeApp(app) {
@@ -323,6 +332,7 @@
return encode({
mode: a.mode,
p: a.protocolKey || undefined,
+ sessionRepo: a.sessionRepo || undefined,
rig: a.rigKey || undefined,
advanced: a.advanced || undefined,
soak: a.soak || undefined,
diff --git a/pattern_editor.html b/pattern_editor.html
index 934cba83..8d67ec6f 100644
--- a/pattern_editor.html
+++ b/pattern_editor.html
@@ -2551,7 +2551,7 @@
Save to Repo
A protocol's pattern folder
- protocols/<bench-id>/<protocol>_patterns/ — travels with that protocol (needs a bench id)
+ protocols/<rig-id>/<protocol>_patterns/ — travels with that protocol (needs a rig id)
@@ -3582,9 +3582,13 @@
Save to Repo
// ============================================================
let sessionRepo = null; // ?repo= override for this tab
let _urlArenaLock = false; // ?arena= handover locks the config after initUI
+ // Same default as the Studio's DEFAULT_COURSE_REPO (LAB-227): the lab's private
+ // data repo, so a fresh browser lists it in Open from Repo… / Save to Repo…
+ // without a Studio visit; a stored repo or a ?repo= link overrides it.
+ const DEFAULT_DATA_REPO = 'reiserlab/arena-experiments';
function repoSettings() {
- const raw = (sessionRepo || localStorage.getItem('studio_gh_repo') || '').trim();
+ const raw = (sessionRepo || localStorage.getItem('studio_gh_repo') || DEFAULT_DATA_REPO).trim();
const rm = /^([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})$/.exec(raw);
const ok = rm && !raw.includes('..');
return {
@@ -3791,13 +3795,13 @@
Save to Repo
if (!built) return;
_pendingRepoSave = built;
document.getElementById('repoSaveRepoLine').textContent =
- 'Repo: ' + cs.repoRaw + (cs.benchId ? ' · bench ' + cs.benchId : '');
+ 'Repo: ' + cs.repoRaw + (cs.benchId ? ' · rig ' + cs.benchId : '');
const protoRadio = document.getElementById('destProtocol');
const protoInput = document.getElementById('destProtoName');
protoRadio.disabled = !cs.benchId;
protoRadio.closest('.dest-row').title = cs.benchId
? 'Commits next to a protocol so they travel together'
- : 'Needs a bench id (set in Arena Studio → ⚙ Settings) — course benches only';
+ : 'Needs a rig id (set in Arena Studio → ⚙ Settings → Rig id)';
if (!cs.benchId) document.getElementById('destLibrary').checked = true;
protoInput.disabled = !protoRadio.checked;
updateRepoSavePath();
diff --git a/tests/test-studio-shell.js b/tests/test-studio-shell.js
index 793049e2..f5c7792d 100644
--- a/tests/test-studio-shell.js
+++ b/tests/test-studio-shell.js
@@ -17,6 +17,9 @@ const path = require('path');
const root = path.join(__dirname, '..');
const studio = fs.readFileSync(path.join(root, 'arena_studio.html'), 'utf8');
const replay = fs.readFileSync(path.join(root, 'js/studio-replay.js'), 'utf8');
+const designer = fs.readFileSync(path.join(root, 'pattern_editor.html'), 'utf8');
+const dashRepo = fs.readFileSync(path.join(root, 'dashboard/data-browser/github-repo.js'), 'utf8');
+const dashIndex = fs.readFileSync(path.join(root, 'dashboard/data-browser/index.html'), 'utf8');
let totalChecks = 0;
let failures = 0;
@@ -407,6 +410,76 @@ check(
!/:root\[data-ui-theme="night"\] (img|canvas)\b/.test(studio) &&
studio.includes('filter:url(#uiNightRed)')
);
+console.log('=== data repo defaults + rig id (LAB-227 Phase 4) ===');
+// ONE default string across the three tools: the lab's private data repo. The course
+// repo stays reachable by typing it (course benches + rig03-sr still write there).
+const DATA_REPO = 'reiserlab/arena-experiments';
+check(
+ 'Studio, Pattern Designer and dashboard default to the lab data repo',
+ studio.includes("Studio.DEFAULT_COURSE_REPO = '" + DATA_REPO + "';") &&
+ designer.includes("const DEFAULT_DATA_REPO = '" + DATA_REPO + "';") &&
+ dashRepo.includes("const DEFAULT_REPO = '" + DATA_REPO + "';")
+);
+check(
+ 'Studio Repo field + dashboard repo field show the data repo, not the course repo',
+ /id="ghRepoInput" placeholder="reiserlab\/arena-experiments"/.test(studio) &&
+ /id="githubRepoInput" type="text" value="reiserlab\/arena-experiments"/.test(dashIndex) &&
+ dashIndex.includes('href="https://github.com/reiserlab/arena-experiments"')
+);
+check(
+ 'the Pattern Designer falls back to the same default when no repo is stored',
+ designer.includes(
+ "(sessionRepo || localStorage.getItem('studio_gh_repo') || DEFAULT_DATA_REPO).trim()"
+ )
+);
+check(
+ '?repo= is a tab-scoped override applied in courseSettings (wins signed in or out)',
+ studio.includes('Studio.sessionRepo = (function () {') &&
+ studio.includes(
+ "Studio.sessionRepo ||\n localStorage.getItem('studio_gh_repo') ||\n Studio.DEFAULT_COURSE_REPO"
+ ) &&
+ studio.includes('sessionRepo: Studio.sessionRepo || null,') &&
+ !studio.includes("localStorage.setItem('studio_gh_repo', Studio.sessionRepo")
+);
+check(
+ 'typing a repo in ⚙ Settings ends the ?repo= override',
+ studio.includes('Studio.sessionRepo = null;')
+);
+check(
+ 'the shared-pattern listing names the repo it came from',
+ studio.includes(
+ "'shared pattern library: ' + n + ' preview(s) wired from ' + repo.full + '/patterns/'"
+ ) && !studio.includes("'course repo: ' + n + ' shared pattern preview(s)")
+);
+check(
+ 'Patterns ↗ passes the effective repo (default, stored, or ?repo=)',
+ studio.includes(
+ 'var cs=window.Studio && Studio.courseSettings ? Studio.courseSettings() : null;'
+ ) && studio.includes("var repo=cs && cs.repo ? cs.repo.full : '';")
+);
+check(
+ 'UI says "Rig id" (the storage key studio_bench_id keeps its name)',
+ /