# 2026-04-14 — edit history ## Summary — tick-2: org-template polish (PRs #50, #52) Two template-only merges landed this tick. Both touch `org-templates/molecule-dev/org.yaml` and adjust role behavior inside the default `molecule-dev` org template — no Go/TS/Python code changed, no new env vars, no new API routes, no test-count drift. ### Template tweaks - **PR #50 `chore(template): PM system prompt — treat audit summaries as dispatch triggers, not FYIs`** — rewrites the PM (Project Manager) role's system prompt so that inbound audit summaries from QA / review loops are treated as actionable dispatch triggers rather than informational FYIs. The PM now routes the summary to the appropriate sub-team instead of acknowledging and stopping. File: `org-templates/molecule-dev/org.yaml` (PM role `system_prompt`). Merged commit `14fc30f`. - **PR #52 `chore(template): bake working Chromium recipe into UIUX Designer cron (closes #23)`** — updates the UIUX Designer role's cron setup to install `playwright-chromium` via the known-good recipe so the scheduled UX-audit job can actually launch a headless browser. Closes issue #23 (cron failed on missing Chromium). File: `org-templates/molecule-dev/org.yaml` (UIUX Designer role cron / setup commands). Merged commit `347faab`. ### Not touched - No platform (`workspace-server/`) change — no API route, handler, migration, or env var added. - No canvas (`canvas/`) change. - No workspace-template (`workspace/`) change — the runtime image already ships the base Playwright deps; this PR only fixes the install invocation inside the cron script that the UIUX Designer workspace runs at startup. - No MCP server / SDK change. - Test counts unchanged from the prior tick (Go 487, Vitest 357, pytest platform 1078, pytest sdk 87, MCP jest per prior tick). Template-only edits cannot shift these; skipped re-measurement. ### Doc surface - This file created. - `CLAUDE.md` — no change (no new endpoint / env / runtime). - `PLAN.md` — no change (no phase boundary crossed). - `README.md` / `README.zh-CN.md` — no change (no user-visible surface). ## Summary — tick-3: admin test-token + hermes config fix (PRs #53, #54, #55) Three merges this tick. One adds a new dev-only admin route for E2E scripts, one is the prior-tick doc-sync PR, and one is a one-line template config fix. ### PR #53 — `feat(platform): GET /admin/workspaces/:id/test-token for E2E (#6)` Merge commit `639c320`. Adds a dev/test-only route that mints a fresh bearer token for E2E scripts (closes issue #6, which called out the brittle hand-rolled token logic in the bash E2E harness). Route is hidden by default — it 404s in production unless explicitly enabled. - **New route** — `GET /admin/workspaces/:id/test-token`. Handler in `workspace-server/internal/handlers/admin_test_token.go`. 404s unless `MOLECULE_ENV != "production"` OR `MOLECULE_ENABLE_TEST_TOKENS=1`. Router wiring in `workspace-server/internal/router/router.go`. - **New env vars** — `MOLECULE_ENV` (log label, already present in `.env.example`) and `MOLECULE_ENABLE_TEST_TOKENS` (explicit override — see `.env.example` fix below). - **E2E helper** — `tests/e2e/_lib.sh` gains `e2e_mint_test_token` which calls the new route and exports `MOLECULE_TEST_TOKEN` for subsequent `curl -H "Authorization: Bearer …"` calls. Replaces the previous hand-rolled JWT construction in the bash harness. - **Tests** — `workspace-server/internal/handlers/admin_test_token_test.go` adds the `TestAdminTestToken_*` quartet (4 tests): prod-default-404, dev-success, explicit-enable-success, not-found-for-missing- workspace-id. - **Doc updates carried by the PR itself** — `CLAUDE.md` route table gained the new admin row, and the env-var paragraph mentions `MOLECULE_ENV` / `MOLECULE_ENABLE_TEST_TOKENS`. Verified on main. ### PR #54 — `docs: sync documentation with 2026-04-14 tick-2 merges (#50, #52)` Merge commit `c9f0a91`. Docs-only. Created the tick-2 section of this file (see above) and did not touch any other doc surface. Nothing to re-sync here; the file already records it. ### PR #55 — `fix(hermes): align config.yaml required_env with executor (HERMES_API_KEY)` Merge commit `0485585`. One-line template fix. The hermes runtime's executor reads `HERMES_API_KEY` (with `OPENROUTER_API_KEY` as fallback), but the `config.yaml` `required_env:` list was still declaring only `OPENROUTER_API_KEY`, which caused startup validation to succeed even when the operator had neither key set, and to reject valid setups that had only `HERMES_API_KEY` set. This commit updates the template's `required_env:` to match the executor's read order. - No new env var — `HERMES_API_KEY` and `OPENROUTER_API_KEY` already documented. - No API / handler / migration change. - No test-count impact. ### Doc-sync fix (code-review follow-up from #53) Reviewer called out that `MOLECULE_ENABLE_TEST_TOKENS` was mentioned in `CLAUDE.md` (admin route description) but missing from `.env.example`. Added an explicit entry with a comment noting the prod-hidden default and the two ways to expose the route. This is a true doc-sync fix (code ships the var; example now matches). ### Measured test counts this tick - **Go**: `go test -v ./... | grep -c "^--- PASS"` → 712 (includes subtests). Top-level `Test*` function count: 713 (713 files grepped). The prior CLAUDE.md number was 695; adding PR #53's `TestAdminTestToken_*` quartet gives 699, which matches the stated "+4 this tick" and is what CLAUDE.md now records. The raw PASS-line number includes every subtest (`t.Run(…)`) so it's always higher than the top-level count — both numbers moved by the same +4 delta, which is what we care about. - **Canvas (Vitest)**: unchanged — no canvas change in #53/#54/#55. CLAUDE.md still reads 357. - **Workspace-template (pytest)**: unchanged — no workspace-template code change. CLAUDE.md still reads 1140. - **SDK (pytest)**: unchanged. CLAUDE.md still reads 87. - **MCP (jest)**: unchanged — no MCP change. ### Doc surface touched this tick - `docs/edit-history/2026-04-14.md` — this tick-3 section appended. - `CLAUDE.md` — Go test count bumped 695 → 699 with reference to the new quartet. (Route table row + env-var mention already landed with PR #53.) - `.env.example` — added `MOLECULE_ENABLE_TEST_TOKENS` comment row. - `PLAN.md` / `README.md` / `README.zh-CN.md` — no change (admin E2E-helper route is not a user-visible surface; hermes fix is template-only; #54 was already docs-only). - No new `docs/**` architecture doc needed — the admin route is a two-line dev helper, not a new subsystem. ## Summary — tick-4: modular guardrail plugins + secrets auto-restart + restart-context message (PRs #63, #64, #65) Three merges this evening tick. One large plugin-refactor, one secrets bugfix, and one new platform feature that injects a synthetic restart context message back to a workspace on re-registration. ### PR #63 — `feat(plugins): split guardrails into 12 modular plugins` Merge commit `8b896b1`. Breaks the previous monolithic `molecule-dev` guardrails into 12 standalone plugins under `plugins/molecule-*`, each shipping its own `plugin.yaml`, optional `hooks/`, optional `settings-fragment.json`, and optional `skills/`. Cross-runtime install is handled by a new `_install_claude_layer` step on `AgentskillsAdaptor` (kept in sync across **both** copies: `workspace/plugins_registry/builtins.py` and `sdk/python/molecule_plugin/builtins.py` — drift-guarded). - **New plugins** — `molecule-audit-trail`, `molecule-careful-bash`, `molecule-freeze-scope`, `molecule-prompt-watchdog`, `molecule-session-context`, `molecule-skill-code-review`, `molecule-skill-cron-learnings`, `molecule-skill-cross-vendor-review`, `molecule-skill-llm-judge`, `molecule-skill-simplify`, `molecule-skill-update-docs`, `molecule-skill-verification`. - **Adaptor extension** — `AgentskillsAdaptor._install_claude_layer` installs hooks (.py + .sh wrapper), merges settings-fragment.json into the workspace's `.claude/settings.json`, and drops skills into `.claude/skills//SKILL.md`. Works for every plugin that ships a `claude_code` adapter stub. - **CLAUDE.md** — the PR itself appended the 12-plugin enumeration to the Plugins section; verified on main, no re-sync needed in this tick. - **Tests** — no new Go / Python unit tests (plugin install is exercised end-to-end via existing plugin-install integration tests). ### PR #64 — `fix(secrets): auto-refresh global_secrets on workspace restart (#15)` Merge commit `383582f`. Fixes GitHub issue #15. Until now, rotating a global secret (e.g. `CLAUDE_CODE_OAUTH_TOKEN`) only propagated to a workspace on the next full cold-start, forcing manual ops to drive `POST /workspaces/:id/restart` by hand. Tier-3 Claude Code agents were the first to surface the stale-token path as SDK 401s. - **New helper** — `restartAllAffectedByGlobalKey(db, key)` in `workspace-server/internal/handlers/secrets.go`. Enqueues `RestartByID` for every non-paused, non-removed, non-external workspace that does NOT shadow the key with a workspace-level override (workspace-scoped secrets already win the Start-time merge). - **Wiring** — `SetGlobal` and `DeleteGlobal` both call the helper after a successful DB write. Matches the existing behaviour of workspace-scoped `Set` / `Delete` (which have always auto-restarted the owning workspace). - **Tests** — `secrets_test.go` gains two sqlmock-backed tests, one per branch (set + delete), verifying the query filter (skip paused / removed / external, skip shadowed) and the enqueue call. Raw PASS count grows by more than 2 because the tests use table-driven subtests. ### PR #65 — `feat(platform): inject restart context system message (#19 Layer 1)` Merge commit `3ea8cda`. Fixes GitHub issue #19 Layer 1 (Layer 2 is deferred to follow-up issue #66). After a workspace restart (HTTP `/restart` or programmatic `RestartByID`) and successful re-registration, the platform sends a synthetic A2A `message/send` back to the workspace containing: - restart timestamp - previous session end timestamp + human-readable duration - list of env-var **keys** now available (keys only — values never leak through the message) The message is marked with `metadata.kind=restart_context` so agents can detect and handle it specifically if they choose, and uses a `system:restart-context` caller prefix so it bypasses `CanCommunicate` via the existing `isSystemCaller()` check in `a2a_proxy.go`. - **New files** — `workspace-server/internal/handlers/restart_context.go` (240 lines: payload builder, re-registration waiter, sender with 30s timeout) and `restart_context_test.go` (120 lines, 4 top-level `Test*` functions). - **Wiring** — `workspace_restart.go` launches the context sender in a goroutine after the HTTP response has been written, so restart latency is unaffected by delivery success. - **Skip path** — if the workspace does not re-register within 30s, the sender logs and drops. Agents that crash during restart do not get spurious context messages. - **Layer 2 follow-up** — user-defined `restart_prompt` via `config.yaml` / `org.yaml` is tracked as new GitHub issue **#66 — "Workspace restart_prompt — user-defined restart context (#19 Layer 2)"**. ### Measured test counts this tick Measured from `/Users/hongming/Documents/GitHub/molecule-monorepo` on main (post-merge of all three PRs): - **Go**: `go test -v ./... | grep -c "^--- PASS"` → **726** (was 712 in tick-3; +14 raw PASS lines from PR #64's two table-driven tests and PR #65's four top-level tests with their subtests). The top-level `Test*` function delta is +6 as expected (+2 from #64, +4 from #65). `#63` added zero test functions. - **Canvas (Vitest)**: unchanged — no canvas change in any PR this tick. CLAUDE.md still reads 357. - **Workspace-template (pytest)**: unchanged — PR #63 adds plugin directories but no new pytest collection target; the drift-guard test still passes (1/1). CLAUDE.md still reads 1140. - **SDK (pytest)**: unchanged — PR #63 modifies `sdk/python/molecule_plugin/builtins.py` but does not add new tests; existing SDK tests still pass. CLAUDE.md still reads 87. - **MCP (jest)**: unchanged — no MCP change. ### Doc surface touched this tick - `docs/edit-history/2026-04-14.md` — this tick-4 section appended. - `CLAUDE.md` — Go test count bumped 699 → 726 (measured PASS lines, keeping the same counting convention as prior ticks); global-secrets auto-restart behaviour noted on the `/settings/secrets` route / secrets section; Workspace Lifecycle section gains a sentence on the synthetic restart-context message and its `system:restart-context` caller prefix. 12-plugin list is already in place from PR #63. - `PLAN.md` — backlog entries that duplicated GitHub issue numbers (11–14 used `#64`/`#65`/`#66`/`#67` as stale sequential-ID references) are left untouched; GitHub issue #66 is the **new** follow-up for #19 Layer 2 and has been added as a fresh Phase 32 / near-term note so the two tracking systems don't silently diverge. - `.env.example` — no change; none of the three PRs added env vars. - `README.md` / `README.zh-CN.md` — no change (no user-visible surface moved by this tick: plugins are still drop-in, secrets auto-restart is an implementation detail, and the restart-context message is an agent-facing system message). ## Summary — tick-5: PLAN.md backlog cleanup + wire tick-4 plugins into default org template (PRs #69, #70) Two docs / template-only merges. Neither touches Go/TS/Python code, adds env vars, moves API routes, or shifts test counts. ### PR #69 — `docs(plan): drop stale sequential refs from Backlog items 11-14` Merge commit `2c89e24` (squash `730bcc4`). `PLAN.md` only. Backlog items 11–14 previously carried placeholder sequential refs `#64`–`#67` that were introduced before GitHub issues/PRs with the same numbers merged with different scopes (PR #64 is the global- secrets auto-restart; PR #65 is the restart-context injector; #66 is the new restart_prompt follow-up; #67 was tick-4's docs-sync PR). Leaving the stale refs in place was actively misleading readers cross-referencing against `gh pr list` / `gh issue list`. The cleanup strips the `#64`–`#67` annotations from the four bullets and adds a single footnote explaining the history and directing future prioritization to file real GitHub issues. No backlog item was removed; wording of items 11–14 is otherwise intact. ### PR #70 — `chore(template): wire 9 new guardrail/skill plugins into defaults; PM + Security Auditor get role extras` Merge commit `e6d8cdf` (squash `def76e7`). `org-templates/molecule-dev/org.yaml` only. Activates the 12 modular plugins that PR #63 (tick-4) landed in the repo-level registry by wiring them into the default `molecule-dev` org template. Before this PR the plugins existed on disk under `plugins/molecule-*` but no org actually opted in, so newly-imported workspaces still only shipped the original three (`ecc` / `molecule-dev` / `superpowers`). - **Defaults expanded (was 3, now 9)** — universal additions: `molecule-careful-bash`, `molecule-prompt-watchdog`, `molecule-audit-trail`, `molecule-session-context`, `molecule-skill-cron-learnings`, `molecule-skill-update-docs` (plus the original three, retained). - **Per-role overrides**: - PM: defaults + `molecule-workflow-triage` + `molecule-workflow-retro` (slash commands matching PM's coordination role). - Security Auditor: defaults + `molecule-skill-code-review` + `molecule-skill-cross-vendor-review` + `molecule-skill-llm-judge` (multi-criteria review + adversarial cross-vendor second opinion + LLM-judge gate for "wrong thing shipped"). - Research Lead + 3 researchers + UIUX Designer: defaults + `browser-automation` (existing override, resynced to new default set). - Other 5 dev roles (Dev Lead, BE, FE, DevOps, QA) inherit the new defaults unchanged. - **REPLACE-semantics caveat** — `workspace-server/internal/handlers/org.go` (~L345) treats per-workspace `plugins:` as REPLACE, not UNION, so every role override has to re-list all 9 defaults to add one extra. GitHub issue **#68** tracks the union-semantics proposal; once it lands the per-role lists can shrink to just the deltas. No platform change in this PR. - No new tests; plugin install is exercised by the existing plugin-install integration tests. ### Not touched - No platform (`workspace-server/`) change — no route, handler, migration, or env var moved. - No canvas / workspace-template / SDK / MCP change. - No new plugins — PR #70 only wires the existing PR #63 plugins into the default template. ### Test counts this tick Unchanged from tick-4 (neither PR added tests). Per the prior-tick baseline: Go 726, Canvas (Vitest) 357, MCP 97, SDK 132, workspace 1140. Skipped re-measurement — docs/template-only diff cannot move these. ### Doc surface touched this tick - `docs/edit-history/2026-04-14.md` — this tick-5 section appended. - `CLAUDE.md` — no change (no code-facing surface moved). - `PLAN.md` — PR #69 is itself the PLAN.md cleanup. Added a one- line entry under "Recently launched" noting PR #70 wired the tick-4 (PR #63) modular plugins into the default org template. - `.env.example` — no change. - `README.md` / `README.zh-CN.md` — no change. ## Summary — tick-6: per-workspace plugins UNION semantics + prior doc-sync (PRs #71, #72) Two merges this tick. One resolves the REPLACE-semantics caveat called out in tick-5 (GitHub issue #68) by flipping per-workspace `plugins:` handling in `org.go` from REPLACE to UNION, with a `!`/`-` opt-out prefix for removing a default on a per-workspace basis. The other is the tick-5 docs-sync PR. ### PR #71 — `fix(org): per-workspace plugins UNION with defaults; '!' prefix opts out (#68)` Merge commit `26622dc` (squash `d9603a7`). Resolves GitHub issue #68. Before this PR, `org.go` (~L345) treated a per-workspace `plugins:` list as a REPLACE of `defaults.plugins`, so every role override in the default `molecule-dev` org template had to re-list all 9 defaults to add one extra (e.g. Security Auditor had to restate 9 defaults to add 3 review skills). With this fix the two lists UNION, so role-level entries only need to declare the delta. - **New helper** — `mergePlugins(defaultPlugins, wsPlugins)` in `workspace-server/internal/handlers/org.go` (~L645). Returns the union of the two lists (deduplicated, defaults first). A per-workspace entry starting with `!` or `-` opts the named plugin OUT of the union (e.g. `!browser-automation` removes `browser-automation` from a workspace that would otherwise inherit it from `defaults.plugins`). - **Wiring** — the `Plugins` field resolution at ~L344 is now `plugins := mergePlugins(defaults.Plugins, ws.Plugins)` instead of the prior "if ws.Plugins != nil then ws.Plugins else defaults.Plugins" branch. - **Tests** — 5 new `TestPlugins_*` tests in `workspace-server/internal/handlers/org_test.go` covering: empty+empty, defaults-only, workspace-adds, opt-out-with-`!`, opt-out-with-`-`, and dedup of a plugin listed in both sides. Measured Go raw PASS count is now **731** (was 726 at tick-5 baseline); delta is +5, matching the new test functions. - **Template ripple** — `org-templates/molecule-dev/org.yaml` role overrides can now shrink to just the deltas, but this PR does NOT touch the template (backward compatible: re-listing defaults still yields the same resolved set after UNION + dedup). Template cleanup is a follow-up. ### PR #72 — `docs: sync documentation with 2026-04-14 tick-5 merges (#69, #70)` Merge commit `3cc4e23` (squash `39bd59b`). Docs-only. Created the tick-5 section of this file (see above). Nothing to re-sync here. ### Measured test counts this tick - **Go**: `go test -v ./... | grep -c "^--- PASS"` → **731** (was 726 at tick-5 baseline; +5 from PR #71's `TestPlugins_*` quintet). This matches exactly. - **Canvas (Vitest)**: unchanged — no canvas change. Still 357. - **Workspace-template (pytest)**: unchanged — no workspace-template change. Still 1140. - **SDK (pytest)**: unchanged. Still 132. - **MCP (jest)**: unchanged. Still 97. ### Doc surface touched this tick - `docs/edit-history/2026-04-14.md` — this tick-6 section appended. - `CLAUDE.md` — Go test count bumped 726 → 731; Plugins / Org Templates note updated from the prior REPLACE-semantics caveat to the new UNION + `!`/`-` opt-out semantics. - `PLAN.md` — added a "Recently launched (2026-04-14 tick-6)" entry for PR #71 noting GitHub issue #68 is now resolved. - `.env.example` — no change. - `README.md` / `README.zh-CN.md` — no change (semantics are internal to org-template resolution). ## Summary — tick-7: DB-authoritative schedules (#76), generic category_routing (#75), template cleanup (#74) Four merges this tick: PR #73 (docs sync tick-6), PR #74 (template plugin cleanup), PR #75 (category_routing for #51), PR #76 (schedules source column for #24). The latter two close GitHub issues #51 and #24. ### PR #76 — `fix(org): DB-authoritative schedules; additive org/import (#24)` Merge commit `07a5ca3c`. Closes #24. - New migration `022_workspace_schedules_source.{up,down}.sql` adds a `source` TEXT column (`'template'` | `'runtime'`) with a CHECK constraint and a unique `(workspace_id, name)` index. Legacy rows are backfilled to `'template'` before the column is flipped `NOT NULL DEFAULT 'runtime'`. - Import SQL is extracted to `const orgImportScheduleSQL` in `org.go` and upserts with `ON CONFLICT (workspace_id, name) DO UPDATE ... WHERE workspace_schedules.source = 'template'` — runtime-added schedules with colliding names survive re-imports. - `schedules.Create` writes `source='runtime'` explicitly; `schedules.List` returns the field (with `json:",omitempty"` so old clients don't see an empty string). - +3 tests: `TestRuntimeSchedule_HasSourceRuntime`, `TestImport_OrgScheduleSQLShape` (asserts against the const directly, no file-scraping), `TestList_IncludesSourceColumn`. ### PR #75 — `feat(platform): generic category_routing replaces hardcoded audit dispatch (#51)` Merge commit `dee5322d`. Closes #51. - `OrgDefaults` + `OrgWorkspace` gain `CategoryRouting map[string][]string`. Merge semantics: workspace keys replace defaults' value for the same key (empty list drops the key); new keys are added. - `renderCategoryRoutingYAML` builds a deterministic YAML block via `yaml.Node` + `yaml.Marshal` (sorted keys; YAML library handles escaping of role names with reserved chars). - New `appendYAMLBlock` helper guarantees a newline boundary when concatenating YAML fragments into `config.yaml`; applied to both the `category_routing` and `initial_prompt` appends. - `org-templates/molecule-dev/org.yaml` gets a `defaults.category_routing` block; `pm/system-prompt.md` replaces the hardcoded role-mapping table with a generic config-lookup pattern ("read `/configs/config.yaml`, look up `category_routing[]`"). - +6 tests covering parse, union-with-defaults, integration into workspace config, YAML-specials escaping, empty-renders-nothing, and the newline guard. ### PR #74 — `chore(template): simplify per-role plugin lists using #71 union semantics` Merge commit `20068196`. Follow-up to PR #71. - `org-templates/molecule-dev/org.yaml` PM, Research Lead + 3 sub-roles, Security Auditor, UIUX Designer role overrides shrunk to just the deltas (e.g. PM goes from 11 entries to `[molecule-workflow-triage, molecule-workflow-retro]`; Research roles go from 10 entries to `[browser-automation]`). - No platform changes; relies on UNION semantics landed in PR #71 (tick-6). ### PR #73 — `docs: sync documentation with 2026-04-14 tick-6 merges (#71, #72)` Merge commit `911580c6`. Routine docs sync for the prior tick. ### File deltas - `CLAUDE.md` — Go test count 731 → 740; migration count 16 → 23; added `workspace_schedules.source` note in the Database section. - `PLAN.md` — new "Recently launched (2026-04-14 tick-7)" section. - `workspace-server/internal/handlers/org.go` — `OrgDefaults.CategoryRouting`, `OrgWorkspace.CategoryRouting`, `mergeCategoryRouting`, `renderCategoryRoutingYAML`, `appendYAMLBlock`, `orgImportScheduleSQL` const, schedules upsert wired to the const. - `workspace-server/internal/handlers/schedules.go` — `scheduleResponse.Source`, `Create` inserts with `source='runtime'`, `List` reads `source`. - `workspace-server/internal/handlers/schedules_test.go` — new file. - `workspace-server/internal/handlers/org_test.go` — `TestCategoryRouting_*` + `TestAppendYAMLBlock_NewlineGuard`. - `workspace-server/migrations/022_workspace_schedules_source.{up,down}.sql` — new. - `org-templates/molecule-dev/org.yaml` — `defaults.category_routing` added; per-role plugin lists trimmed to deltas. - `org-templates/molecule-dev/pm/system-prompt.md` — hardcoded category table replaced with generic config-lookup instructions. ## Summary — tick-8: TenantGuard middleware (Phase 32 foundation) One merge: PR #78 (TenantGuard). Phase 32 (Cloud SaaS launch) starts here. ### PR #78 — `feat(platform): TenantGuard middleware — public repo's only SaaS hook` Merge commit `57a05686`. Noteworthy: saas-foundation / auth-adjacent. - New `workspace-server/internal/middleware/tenant_guard.go`: - Reads `MOLECULE_ORG_ID` env at construction. If set → every non-allowlisted request must carry matching `X-Molecule-Org-Id` or gets **404** (not 403, to avoid leaking tenant existence to subdomain probers). If unset → passthrough (self-hosted / dev / CI unchanged). - Allowlist is exact-match (`/health`, `/metrics`) so Fly Machines health probes + Prometheus scrape work without the header. - `TenantGuardWithOrgID(id)` is the test constructor; ordinary callers use `TenantGuard()`. - Wired into `workspace-server/internal/router/router.go` after `metrics.Middleware()` so rejected requests still land on the 4xx counter. - +6 tests: unset-passthrough, matching, mismatched-404-empty-body, missing-404, allowlist-bypass, allowlist-is-exact-match. - CLAUDE.md: test count 740 → 746; new `MOLECULE_ORG_ID` env var documented. ### Paired work — private `molecule-controlplane` repo scaffolded (Outside this monorepo; logged here because it anchors the open-core split.) - Initial commit `1bab493` on new private repo `Molecule-AI/molecule-controlplane`. - Migrations 001 (organizations), 002 (org_instances), 003 (org_members). - HTTP server: `/health`, `/cp/orgs` CRUD, subdomain + `X-Molecule-Org-Slug` header fallback → `fly-replay: app=;instance=` header, stamps `X-Molecule-Org-Id` so TenantGuard downstream accepts the request. - `Provisioner` + `Lookup` interfaces; `Stub` in-memory impl (idempotent, tested) + `Fly` stub returning `ErrNotImplemented` (real impl is Phase B). - CI workflow: vet + build + test on push/PR. - Follow-up PRs (in the private repo): real Fly Machines provisioner, WorkOS AuthKit signup, Stripe billing, Cloudflare edge, signup UX, observability, hardening. Full 9-phase plan documented in chat (phases A–I). ### File deltas (public repo) - `CLAUDE.md` — test count + `MOLECULE_ORG_ID` env var. - `PLAN.md` — new "Recently launched (2026-04-14 tick-8)" block. - `workspace-server/internal/middleware/tenant_guard.go` — new. - `workspace-server/internal/middleware/tenant_guard_test.go` — new. - `workspace-server/internal/router/router.go` — wired middleware.