+
+
+
+ Proposed work items — {item.items.length}
+
+
+ {item.note &&
{item.note}
}
+ {visible.map((entry, i) => (
+
+ {i + 1}.
+
+ {entry.title}
+
+ Done when: {entry.criteria}
+
+
+
+ ))}
+ {hidden > 0 && (
+
+ )}
+
+
+ Reply to edit the split; approval creates these on the board.
+
+
+
+
+
+
+ );
+}
diff --git a/surfaces/gui/src/styles.css b/surfaces/gui/src/styles.css
index 8479978a..4f890678 100644
--- a/surfaces/gui/src/styles.css
+++ b/surfaces/gui/src/styles.css
@@ -1734,3 +1734,17 @@ html[data-platform="linux"] ::-webkit-scrollbar-thumb:hover { background-color:
.team-dot.in_progress { background: var(--ok-dot); }
.team-dot.blocked { background: var(--danger); }
.team-dot.review { background: var(--warn-ink); }
+
+/* Decomposition gate (proposed work items) */
+.itemsreq-card { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
+.itemsreq-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
+.itemsreq-title { font-weight: 600; font-size: 13px; color: var(--ink); }
+.itemsreq-note { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
+.itemsreq-item { display: flex; gap: 9px; padding: 7px 0; border-top: 1px solid var(--line); }
+.itemsreq-num { color: var(--faint); font-size: 12px; padding-top: 1px; }
+.itemsreq-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
+.itemsreq-item-title { font-size: 12.5px; color: var(--ink); }
+.itemsreq-ac { font-size: 11.5px; color: var(--muted); }
+.itemsreq-ac b { font-weight: 600; }
+.itemsreq-more { display: flex; align-items: center; gap: 5px; border: 0; background: transparent; color: var(--accent); font-size: 12px; cursor: pointer; padding: 6px 0; }
+.itemsreq-grant { font-size: 11.5px; color: var(--faint); }
diff --git a/surfaces/gui/src/types.ts b/surfaces/gui/src/types.ts
index b0b25a0c..7693e4c3 100644
--- a/surfaces/gui/src/types.ts
+++ b/surfaces/gui/src/types.ts
@@ -12,6 +12,7 @@ export type EventType =
| "question_requested"
| "plan_proposed"
| "team_proposed"
+ | "items_proposed"
| "tool_started"
| "tool_finished"
| "iteration_end"
@@ -164,6 +165,13 @@ export type Item =
note?: string;
resolved?: "approved" | "rejected";
}
+ | {
+ // The decomposition gate: a lead proposes work items; approval creates them.
+ kind: "itemsreq";
+ items: { title: string; criteria: string; description?: string }[];
+ note?: string;
+ resolved?: "approved" | "rejected";
+ }
| {
// A live ask_user prompt (attended sessions answer inline; unattended ones route to the Inbox).
kind: "question";
diff --git a/tests/test_team_wake.py b/tests/test_team_wake.py
index 909477c6..70ee36e1 100644
--- a/tests/test_team_wake.py
+++ b/tests/test_team_wake.py
@@ -231,3 +231,38 @@ def test_team_options_lists_only_enabled_workers(manager):
assert {"swe-worker", "design-worker", "test-worker"} <= workers
assert "swe-lead" not in workers # leads staff, they aren't staffed
assert "security" not in workers # solo coworkers are not team-eligible
+
+
+def test_turn_saves_never_detach_a_worker_from_its_team(manager, monkeypatch):
+ from coworker.agents.base import Agent
+ from coworker.sessions import SessionRecord
+
+ worker_agent = Agent(name="swe-worker", title="SWE", system_prompt="p", team="worker")
+ monkeypatch.setattr("coworker.server.manager.get_agent", lambda name: worker_agent)
+ manager.session_store.save(
+ SessionRecord(
+ session_id="lead-sid",
+ workspace=manager.default_workspace,
+ model="m",
+ mode="interactive",
+ messages=[],
+ agent="swe-lead",
+ )
+ )
+ result = manager.create_team("lead-sid", [{"persona": "swe-worker"}])
+ wid = result["workers"][0]["session_id"]
+ # A per-turn save rebuilds the record WITHOUT the team field (the engine doesn't
+ # carry it) — owner-hit 2026-08-16: this detached workers from the lead's entry.
+ record = manager.session_store.load(wid)
+ manager.session_store.save(
+ SessionRecord(
+ session_id=wid,
+ workspace=record.workspace,
+ model=record.model,
+ mode=record.mode,
+ messages=[{"role": "user", "content": "hi"}],
+ agent=record.agent,
+ )
+ )
+ assert manager.session_store.load(wid).team["lead_session"] == "lead-sid"
+ assert manager.session_store.load("lead-sid").team["role"] == "lead"