Shipping lineup: remove Chat, ship Code disabled, gate unshipped personas

Manifests carry ships/group; release builds hide ships:false coworkers (OPENWORKER_UNSHIPPED=1 restores them).
Persona detail serves bundle media; staffing gate reads the registry, not the filtered list.
This commit is contained in:
Rohit C Prasad
2026-08-20 17:59:48 -07:00
parent ff78ea9f35
commit 19e10f0e76
28 changed files with 220 additions and 77 deletions
+66 -22
View File
@@ -1,4 +1,6 @@
"""Phase 1 gate — persona registry lifecycle (installed → enabled → surfaced + default)."""
"""Phase 1 gate — persona registry lifecycle (installed → enabled → surfaced + default),
plus the shipping lineup (owner calls 2026-08-21): Chat removed, Code disabled by default,
ships:false personas hidden outside internal builds (OPENWORKER_UNSHIPPED=1)."""
from __future__ import annotations
@@ -11,26 +13,62 @@ def _reg(tmp_path) -> PersonaRegistry:
return PersonaRegistry(state_path=tmp_path / "personas.json")
@pytest.fixture
def internal(monkeypatch):
"""Internal build: ships:false personas (teams, ops, design…) are visible."""
monkeypatch.setenv("OPENWORKER_UNSHIPPED", "1")
def test_builtins_present(tmp_path):
reg = _reg(tmp_path)
assert {"code", "chat", "cowork", "ops"} <= set(reg.ids())
assert {"code", "cowork", "ops"} <= set(reg.ids())
assert "chat" not in reg.ids() # removed entirely, not just disabled
assert reg.get("ops").builtin is True
# Ops came from a markdown manifest; Code from a builder.
assert reg.get("ops").manifest is not None
assert reg.get("code").manifest is None
def test_sidebar_defaults_to_surfaced_builtins(tmp_path):
def test_release_lineup(tmp_path, monkeypatch):
# A release build (no flag) offers exactly OpenWorker + the security coworkers;
# Code is listed in Settings but disabled + unsurfaced (the recovery path).
monkeypatch.delenv("OPENWORKER_UNSHIPPED", raising=False)
reg = _reg(tmp_path)
assert [e["name"] for e in reg.sidebar()] == [
"cowork", "cloud-posture", "dep-audit", "security",
]
listed = {p["id"]: p for p in reg.list_all()}
assert set(listed) == {"cowork", "code", "cloud-posture", "dep-audit", "security"}
assert listed["code"]["enabled"] is False and listed["code"]["surfaced"] is False
assert listed["cloud-posture"]["group"] == "security"
assert listed["cowork"]["group"] == "general"
# Enabling Code from Settings puts it in the picker (enable implies surface).
reg.set_enabled("code", True)
assert "code" in [e["name"] for e in reg.sidebar()]
def test_unshipped_hidden_unless_enabled(tmp_path, monkeypatch):
monkeypatch.delenv("OPENWORKER_UNSHIPPED", raising=False)
reg = _reg(tmp_path)
assert "swe-lead" not in {p["id"] for p in reg.list_all()}
# Still resolvable (a session born on it keeps working)…
assert reg.agent("swe-lead").name == "swe-lead"
# …and an explicit user enable (made on an internal build) keeps it visible.
reg.set_enabled("swe-lead", True)
assert "swe-lead" in {p["id"] for p in reg.list_all()}
def test_sidebar_defaults_to_surfaced_builtins(tmp_path, internal):
reg = _reg(tmp_path)
sidebar = reg.sidebar()
ids = [e["name"] for e in sidebar]
# Built-ins ship enabled (UX-029: the coworker picker is their front door); Chat
# stays default-hidden via the surfaced axis. Installed personas remain opt-in.
# Built-ins ship enabled (UX-029: the coworker picker is their front door) except
# Code (owner 2026-08-21). Installed personas remain opt-in.
assert ids[0] == "cowork"
# Leads surface (the user's entry to a team — "the team IS the lead"); team
# workers never do.
assert set(ids) == {
"cowork", "code", "ops", "security", "cloud-posture", "dep-audit",
"cowork", "ops", "security", "cloud-posture", "dep-audit",
"swe-lead", "devsecops-lead", "devops-lead",
}
assert not any(
@@ -43,23 +81,30 @@ def test_sidebar_defaults_to_surfaced_builtins(tmp_path):
)
assert sidebar[0]["default"] is True
# An explicit disable removes a builtin from the picker.
reg.set_enabled("code", False)
assert "code" not in [e["name"] for e in reg.sidebar()]
reg.set_enabled("security", False)
assert "security" not in [e["name"] for e in reg.sidebar()]
def test_chat_retired_by_default_but_resolvable(tmp_path):
def test_code_ships_disabled_but_recoverable(tmp_path):
reg = _reg(tmp_path)
# Chat is retired (owner call 2026-08-11): disabled + unsurfaced out of the box,
# unlike the other builtins — Coworker covers quick Q&A.
assert reg.is_enabled("chat") is False
assert reg.is_surfaced("chat") is False
assert reg.agent("chat").name == "chat" # live sessions keep resolving
# Still recoverable from Settings ▸ Coworkers (enable implies surface).
reg.set_enabled("chat", True)
assert "chat" in [e["name"] for e in reg.sidebar()]
# Code ships disabled + unsurfaced (owner call 2026-08-21): OpenWorker leads the
# launch, Code stays one checkbox away as the plain work-in-my-repo persona.
assert reg.is_enabled("code") is False
assert reg.is_surfaced("code") is False
assert reg.agent("code").name == "code" # live sessions keep resolving
reg.set_enabled("code", True)
assert "code" in [e["name"] for e in reg.sidebar()]
def test_surface_toggle_filters_picker_but_keeps_resolvable(tmp_path):
def test_chat_gone_resolves_to_default(tmp_path):
reg = _reg(tmp_path)
# Chat is removed outright; a stray persona=chat session id falls back to the
# default persona instead of erroring.
assert reg.get("chat") is None
assert reg.agent("chat").name == reg.default_id()
def test_surface_toggle_filters_picker_but_keeps_resolvable(tmp_path, internal):
reg = _reg(tmp_path)
reg.set_surfaced("ops", False)
assert "ops" not in [e["name"] for e in reg.sidebar()]
@@ -98,14 +143,13 @@ def test_agent_resolution(tmp_path):
assert reg.agent("does-not-exist").name == reg.default_id()
def test_list_all_carries_workspace_enum(tmp_path):
# Post-§16 collapse: workspace derives from family — code → git, knowledge → deliverable
# (scratch). Only builder-registered Chat keeps "none". Ops is a scratch persona now.
def test_list_all_carries_workspace_enum(tmp_path, internal):
# Post-§16 collapse: workspace derives from family — code → git, knowledge →
# deliverable (scratch). Ops is a scratch persona now.
reg = _reg(tmp_path)
ws = {p["id"]: p["workspace"] for p in reg.list_all()}
assert ws["code"] == "git"
assert ws["cowork"] == "deliverable"
assert ws["chat"] == "none"
assert ws["ops"] == "deliverable"