# 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 (`platform/`) change — no API route, handler, migration,
  or env var added.
- No canvas (`canvas/`) change.
- No workspace-template (`workspace-template/`) 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
  `platform/internal/handlers/admin_test_token.go`. 404s unless
  `MOLECULE_ENV != "production"` OR `MOLECULE_ENABLE_TEST_TOKENS=1`.
  Router wiring in `platform/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** — `platform/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-template/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/<name>/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
  `platform/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** — `platform/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** — `platform/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 (`platform/`) 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
  `platform/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
  `platform/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[<category>]`").
- +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.
- `platform/internal/handlers/org.go` — `OrgDefaults.CategoryRouting`,
  `OrgWorkspace.CategoryRouting`, `mergeCategoryRouting`,
  `renderCategoryRoutingYAML`, `appendYAMLBlock`, `orgImportScheduleSQL`
  const, schedules upsert wired to the const.
- `platform/internal/handlers/schedules.go` — `scheduleResponse.Source`,
  `Create` inserts with `source='runtime'`, `List` reads `source`.
- `platform/internal/handlers/schedules_test.go` — new file.
- `platform/internal/handlers/org_test.go` — `TestCategoryRouting_*`
  + `TestAppendYAMLBlock_NewlineGuard`.
- `platform/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 `platform/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 `platform/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=<tenant>;instance=<machine_id>` 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.
- `platform/internal/middleware/tenant_guard.go` — new.
- `platform/internal/middleware/tenant_guard_test.go` — new.
- `platform/internal/router/router.go` — wired middleware.
