Personas: requires_folder/subagents/scheduling traits replace family and the workspace enum

Manifest keeps a legacy family shim; telemetry wire fields unchanged.
No behavior change; spec in ocw-context/docs/workspace-scratch-design.md.
This commit is contained in:
Rohit C Prasad
2026-08-20 21:19:56 -07:00
parent 9735d0c6e7
commit 1299760400
40 changed files with 192 additions and 205 deletions
+2 -2
View File
@@ -25,7 +25,7 @@ def test_code_persona_matches_builder(tmp_path):
reg = PersonaRegistry()
ctx = _ctx(tmp_path)
assert _names(reg.agent("code"), ctx) == _names(code_agent(), ctx)
assert reg.agent("code").family == "code"
assert reg.agent("code").requires_folder and reg.agent("code").subagents
def test_cowork_persona_matches_builder(tmp_path):
@@ -42,7 +42,7 @@ def test_ops_persona_composes_knowledge_toolset(tmp_path):
# Ops uses the same capability list as Cowork (files/search/shell/todo).
assert _names(reg.agent("ops"), ctx) == _names(cowork_agent(), ctx)
a = reg.agent("ops")
assert a.family == "knowledge" and a.messaging and a.connectors
assert not a.requires_folder and a.scheduling and a.messaging and a.connectors
assert "read_file_lines" in _names(a, ctx) # multi-root knowledge files
+1 -3
View File
@@ -66,9 +66,7 @@ def test_persona_detail_endpoint(tmp_path, monkeypatch):
assert detail["id"] == "ops"
assert detail["name"] == "Ops Coworker"
assert detail["enabled"] is True # builtins ship enabled (UX-029)
assert (
detail["workspace"] == "deliverable"
) # §16 collapse: ops is a scratch persona now
assert detail["requires_folder"] is False # ops is a scratch persona
assert detail["default_permission_mode"] == "interactive"
assert "anthropic:claude-opus-4-8" in detail["recommended_models"]
assert set(detail["tools"]) == {"files", "search", "shell", "todo"}
+2 -2
View File
@@ -54,7 +54,7 @@ def test_install_from_dir_lands_disabled_pending_consent(tmp_path):
reg.set_enabled("acme-ops", True)
reg.set_surfaced("acme-ops", True)
assert "acme-ops" in [e["name"] for e in reg.sidebar()]
assert reg.agent("acme-ops").family == "knowledge"
assert reg.agent("acme-ops").requires_folder is False
def test_installed_persona_persists_across_restart(tmp_path):
@@ -131,7 +131,7 @@ def test_install_snapshots_independently_of_source(tmp_path):
shutil.rmtree(src) # source gone
reg2 = PersonaRegistry(state_path=tmp_path / "personas.json")
assert "acme-ops" in reg2.ids() and reg2.is_enabled("acme-ops")
assert reg2.agent("acme-ops").family == "knowledge"
assert reg2.agent("acme-ops").requires_folder is False
def test_adding_a_connector_grows_capabilities_and_forces_reconsent(tmp_path):
+14 -10
View File
@@ -27,10 +27,9 @@ def test_parse_valid():
m = parse_manifest(VALID)
assert m.id == "demo" and m.name == "Demo Coworker"
assert m.tools == ["files", "search", "shell", "todo"]
assert m.family == "knowledge" and m.workspace == "deliverable"
assert m.requires_folder is False and m.scheduling is True
assert m.messaging is True and m.connectors == ("github",)
assert m.recommended_models == ["anthropic:claude-opus-4-8"]
assert m.needs_workspace is True
assert m.system_prompt.startswith("You are a demo coworker")
@@ -81,7 +80,7 @@ def test_to_agent_carries_traits_and_tools(tmp_path):
from coworker.tools.todo import TodoList
agent = parse_manifest(VALID).to_agent()
assert agent.name == "demo" and agent.family == "knowledge"
assert agent.name == "demo" and agent.requires_folder is False
assert agent.messaging and agent.connectors
ctx = AgentContext(workspace=tmp_path, executor=object(), todo=TodoList())
names = {getattr(t, "__name__", "") for t in agent.build_tools(ctx)}
@@ -93,10 +92,10 @@ def test_list_field_accepts_comma_string():
assert parse_manifest(text).tools == ["files", "search"]
def test_workspace_key_is_accepted_but_derived_from_family():
# §16 collapse: the old enum still parses (back-compat + typo detection) but behavior
# derives from family — knowledge → scratch ("deliverable"), code → "git". A manifest
# can no longer demand a folder gate (`project`) or opt out of a workspace (`none`).
def test_legacy_family_key_maps_to_traits():
# Legacy shim (workspace-scratch-design.md): pre-trait bundles declared
# `family: code|knowledge` (plus a dead `workspace:` enum, ignored). `family: code`
# maps to the folder-gated profile; explicit new keys always win.
text = """---
id: opsy
workspace: project
@@ -105,12 +104,18 @@ tools: [files, search, shell, todo]
Operate things.
"""
m = parse_manifest(text)
assert m.workspace == "deliverable" and m.needs_workspace is True
assert m.requires_folder is False and m.subagents is False and m.scheduling is True
coded = parse_manifest(
"---\nid: dev\nfamily: code\nworkspace: none\ntools: [git]\n---\nCode."
)
assert coded.workspace == "git" and coded.needs_workspace is True
assert coded.requires_folder and coded.subagents and not coded.scheduling
# New keys override the shim.
mixed = parse_manifest(
"---\nid: dev2\nfamily: code\nrequires_folder: false\ntools: [git]\n---\nCode."
)
assert mixed.requires_folder is False and mixed.subagents is True
@pytest.mark.parametrize(
@@ -122,7 +127,6 @@ Operate things.
("---\nid: x\ntools: [files]\n---\n", "no body"),
("---\nid: x\ntools: [nope]\n---\nbody", "unknown tool"),
("---\nid: x\nfamily: alien\ntools: []\n---\nbody", "family"),
("---\nid: x\nworkspace: cloud\ntools: []\n---\nbody", "workspace"),
(
"---\nid: x\ndefault_permission_mode: yolo\ntools: []\n---\nbody",
"permission",
+9 -9
View File
@@ -137,20 +137,20 @@ def test_set_default_enables_and_persists(tmp_path):
def test_agent_resolution(tmp_path):
reg = _reg(tmp_path)
assert reg.agent("ops").family == "knowledge"
assert reg.agent("code").family == "code"
assert reg.agent("ops").requires_folder is False
assert reg.agent("code").requires_folder is True
# Unknown id → default persona.
assert reg.agent("does-not-exist").name == reg.default_id()
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.
def test_list_all_carries_requires_folder(tmp_path, internal):
# The workspace enum collapsed into the requires_folder trait
# (workspace-scratch-design.md): Code gates a folder; scratch personas don't.
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["ops"] == "deliverable"
gated = {p["id"]: p["requires_folder"] for p in reg.list_all()}
assert gated["code"] is True
assert gated["cowork"] is False
assert gated["ops"] is False
def test_set_unknown_persona_raises(tmp_path):
+2 -2
View File
@@ -38,10 +38,10 @@ def test_bundles_register_as_enabled_code_builtins(tmp_path):
for pid in BUNDLES:
entry = reg.get(pid)
assert entry is not None and entry.builtin
assert entry.family == "code" # folder pick at send, like Code
assert entry.requires_folder # folder pick at send, like Code
assert reg.is_enabled(pid) is True # in the picker out of the box
agent = reg.agent(pid) # catalog-expanded tools materialize
assert agent.family == "code" and agent.needs_workspace
assert agent.requires_folder and agent.subagents
def test_bundle_skill_folders_match_their_manifests(tmp_path):
+2 -2
View File
@@ -40,8 +40,8 @@ def test_code_agent_tools(tmp_path):
def test_chat_agent_has_no_workspace_tools():
assert chat_agent().build_tools(AgentContext()) == []
assert chat_agent().needs_workspace is False
assert code_agent().needs_workspace is True
assert chat_agent().requires_folder is False
assert code_agent().requires_folder is True
def test_get_agent_fallback():