The rank guard now derives needs_approval through the real _prepare_tool
on a bash call under an allowlisting persona instead of scripting the
flag, and asserts the approval gate actually fires. The row-shape guard's
source-grep is replaced with behavioral collector tests driving both
ws_created lanes (poll-diff and SSE relay), plus a proxy-forward twin and
a saved-list value assertion that would catch positional column mix-ups.
Receiving-side stamping gets its first HTTP coverage: create with an
explicit persona under workstreams.create only (selection needs no
persona perm), kind-mismatch and unknown-name 400s, omitted-persona
default stamping, the 503 on a failed default lookup, and the clean-None
legacy lane. Resume adoption is pinned end to end: a corrupt target stamp
leaves the session fully intact, an MCP-on stamp is refused when the
client was persona-gated at construction, and an MCP-off stamp drops the
live surface (listeners deregistered, toolsets reset). Soft-set
tool_search expansion recomposes the prompt exactly once; legacy sessions
never recompose.
Compaction legs run real flows now: spill plus the recall-pointer variant
under memory-off with recall visible vs hidden, and a full stamp
surviving compaction-then-resume. Migration 063 gains the downgrade
config-cleanup case (stamps removed, creative_mode preserved) and the
conversion idempotency case (already-stamped creative rows don't crash
the upgrade).
RBAC coverage goes cross-perm: read-only and write-only principals hit
every verb (a wrong-perm-name regression in any handler is now visible),
archive and default-flip succeed through PATCH, persona.* strings
round-trip the role editors and the overrides overlay, and the production
route table is asserted directly (no DELETE registered). Endpoint/storage
fixtures move off migration-seed names; storage hardening tests cover the
size caps, corrupt-row reads, the TypeError-to-ValueError ordering, the
duplicate-name race mapping, and the single-default backstop. Shell
asserts pin the new picker surfaces and drop the last persona-as-kind
wording.
Creation surfaces: the console launcher, the server webui new-workstream
dialog, and the dashboard composer all gain a Persona select fed by the
shared personas.js data layer (module cache + fingerprint + never-reject
fetch + window.TurnstonePersonas bridge, cloned from projects.js), kind-
filtered with the kind default preselected so a zero-touch launch is
byte-identical to today.
Authoring: a Personas tab in the console Manage surface (Governance
group, persona.read-gated) with a Service Hatch shelf exposing exactly
the four levers — base prompt, tool-visibility checklist (kind inventory
+ free-text row for MCP/dynamic names; tool_search membership decides
soft vs hard), MCP and memory toggles — plus kinds, default flip, and
archive. No delete action anywhere (archive-only lifecycle).
The workstream wears it: SavedColumns.persona() on both saved tables,
hover/aria labels on the rail rows (raw slug fallback keeps archived
personas labelling their workstreams), and the full row-emitter sweep —
storage projections (list_workstreams tail, get_workstreams_batch,
list_workstreams_with_history) on both backends, the server dict
builders and ws_created events, both _coordinator_rows lanes, the
collector delta + pseudo-node paths, and the console cluster-create
proxy that rebuilds its body.
Naming reclamation: the launcher kind-toggle ids/classes that squatted
on 'persona' (launcher-personas, persona-coordinator/-interactive,
.persona-btn/.persona-led/.persona-tag) are renamed to kind-* before
'persona' becomes user-facing vocabulary, along with the prompts-module
and test wording that used persona to mean kind.
Max-effort review findings on the visibility feature, worst first:
Leaks — the filter was sound where it ran, but several surfaces never
carried project_id to gate on:
- cluster_snapshot served the raw collector state with no filter at all;
it now gets the same per-request tenancy treatment as its siblings
- console pseudo-node coordinator rows + emit_console_ws_created,
the interactive-create ws_created event, and the poll-diff ws_created
now carry project_id/user_id (parity with their filtered siblings —
a missing field failed open, and a missing user_id over-hid the
creator's own workstreams)
- the SSE snapshot's overview total/state histogram is re-derived from
the filtered rows instead of leaking pre-filter counts
Correctness:
- saved list pages with OFFSET until it fills its 50-row window instead
of filtering after the LIMIT (a caller's own rows at position 51+
used to vanish behind other tenants' private rows); scan capped at 20
pages, logged when hit
- an INHERITED project_id whose project was since deleted no longer
400s coordinator child spawns — the dangling link is dropped; explicit
unknown ids still 400, revoked membership still 403s
- the SSE filter keeps a per-connection unresolved map: a storage blip
suppresses a row without pinning it hidden until reconnect (re-judged
on later events, rate-limited); definitive verdicts settle as before
- bypass principals (service / admin.cluster.inspect) get payloads
untouched — no row drops, no overview rewrite
Consistency and robustness:
- dashboard + saved-list visibility checks moved off the event loop
(executor), matching every sibling site
- list_project_attachments chunks its IN() at 500 ids per statement
- ws_visible/ensure_project_attachable now share one _project_grants
predicate so the tenancy rule can't diverge
- resolve_workstream_owner's docstring states the deliberate
fail-closed trade for project-attached rows during DB outages
- the workstreams-for-project ordering test asserts strict order on a
forced timestamp instead of a vacuous set fallback
Workstreams attached to a private project were listed and reachable for
every authenticated user — only the scope tier was checked. Add a
tenancy predicate (WorkstreamProjectVisibility: private → project
owner/members, the workstream's own creator, service scope, or
admin.cluster.inspect; public/dangling/no project → unchanged
trusted-team visibility; membership itself is the grant — deliberately
NOT gated on the project.read capability, which guards the management
API) and apply it at every surface:
- listings: saved sessions (project_id + owner tail-appended to
list_workstreams_with_history on both backends), active list, node
dashboard, console cluster list (pre-pagination via a collector
row_filter so totals stay honest), node detail
- console tier-1 SSE: per-connection snapshot filtering + a hidden-set
for sparse follow-up events; ws_created project lookups run on the
executor, membership changes take effect on reconnect
- row access: resolve_workstream_owner 403s private-project rows for
non-members, covering every interactive ws-scoped verb via
tenant_check (console coordinator lane stays on its privileged
admin.coordinator gate)
- create: ensure_project_attachable gates explicit and parent-inherited
project_id on both create validators (unknown project 400s instead of
minting a dangling link)
The dashboard body becomes a persona-unified launcher (start a coordinator OR an interactive session from one composer) and the saved list spans both kinds; the redundant active-coordinators table is dropped (the rail covers it now).
Backend — the console /v1/api/workstreams/saved now returns both kinds: session_routes.py extracts _collect_saved_rows (shared by the refactored, behaviour-preserving make_saved_handler) + adds make_unified_saved_handler (merges per-kind queries — run concurrently via asyncio.gather — sorted by updated desc). The operator gate (admin.coordinator) is applied once; no new exposure (operators already see every session). console/server.py mounts it with [coordinator, interactive] cfgs.
Frontend — a persona toggle routes submit by kind: coordinator -> console-local POST /v1/api/workstreams/new; interactive -> node-proxy POST /v1/api/cluster/workstreams/new (auto placement). Each option is scope-gated (admin.coordinator / workstreams.create); attachments stay coordinator-only. The saved list gains a KIND tag column + kind-routed activation (coordinator -> /open + /coordinator; interactive -> /node/{id}/?ws_id=) and stays operator-gated. The active-coordinators table + _renderHomeView/_activeCoordsFromClusterState are removed.
Tests: make_unified_saved_handler coverage (tests/test_saved_handler_unified.py, synthetic fixtures, no DB) + a console-launcher static guard (tests/test_shell_js.py). Reviewed via the multi-stage pipeline; findings applied (client/server gate match, concurrent queries, chip-CSS dedup, static guards, stale-comment cleanup).