fix(harness): Studio never creates a planner session; the map is a read-only view [SAP-3143] - #837
fix(harness): Studio never creates a planner session; the map is a read-only view [SAP-3143]#837gwitwer wants to merge 5 commits into
Conversation
…ad-only view [SAP-3143] Selecting a project or its Plan Agents row opened a session automatically: a `map-planner` session, launched behind the user's back, that owned the centre pane. There is no planning session type. The map is a view of durable state, and a project's sessions are ordinary sessions the user starts. - Opening a project or its Agent Map row reads the workspace and starts nothing. The centre says which project has no running session and offers a Start that goes through the same createSession path as the + tab. - Sessions persisted with planner metadata load as ordinary sessions; the stale key is dropped on registry load, and a persisted `map-planner` identity is no longer honored on resume. - The planner routes (create, message, greeting retry) answer 410 with `planner_sessions_removed`. - Closing a tab now prefers another live session in the same folder, which is what the planner-specific fallback expressed for planner sessions only. - Deleted: planner-greeting, planning-session, the agent-map-planner profile and their tests; the planner client API, its mock, and the client planning state. The Agent Map store, MCP tools and renderer are untouched. Mirrors the deletions in #826, which supersedes this. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DUq2aqxS5DQtG6aPvoYtn7
Review — PR #837 (round 1)Deleting the planner is the right call and the deletion itself is clean: the 410 route, the MCP-identity narrowing and the e2e rewrites all have tests. Three findings, ordered by severity. 1. Breaking removals from a released public surface, shipped as
|
…he state migration Review round 1: 1. The changeset now says Breaking and carries the migration note. The eight planner analytics event types stay in AnalyticsEventType for one release, deprecated and no longer emitted, so a consumer's exhaustive switch still compiles; five of them shipped 0.14.0 already marked deprecated. 2. The registry-load migration has a test: a 0.14.0-shaped session with planner metadata and a map-planner identity loads as an ordinary session, the rewrite is durable, and it resumes as an agent-builder. Mutation-tested. 3. The map-altitude Start spinner keys to the project's own root, not to any start in flight, so two projects starting concurrently do not disable each other's button. Hygiene: one shared rule for a Studio project's root (studioProjectRoot, beside mostSpecificStudioScope) so the tab strip, the Start target and the conversation subject cannot key to different folders under nested scopes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DUq2aqxS5DQtG6aPvoYtn7
Review — PR #837 (round 2, follow-up)Delta since 1. Round-1 #2 is only half covered — the server-side half still asserts a fake
2. New: the inserted function stole
|
Review round 2: 1. The resolver that runs against a user's persisted sessions.json is now core/project-session-identity.ts, called by the server, with its own tests: a persisted map-planner row is never honored, a server-authored planned assignment is, and a row from another session, project, or principal is re-issued. Mutation-tested against dropping the assignment.kind clause, which is the regression the review named. 2. mostSpecificStudioScope has its doc comment back; studioProjectRoot keeps only its own. Nit: the conversation subject keying to the innermost scope while the tabs key to the project root is deliberate, and now says so where the root is derived. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DUq2aqxS5DQtG6aPvoYtn7
…menu The ⋮ on every project row opened a 248px card to hold one item. Its create item is suppressed wherever the Agent Map owns creation, which is every plan-first project, so on the product path the popover existed to carry a single "Remove <project> from the rail" — a card, an anchor and a min-width whose stated purpose in the CSS was fitting that one sentence on one line. Remove is now an X beside the session shortcut, hover-revealed like every other row action, opening the same RemoveProjectConfirm as before: the project named, the count of running sessions it ends, and the statement that nothing on disk is touched. The legacy create action, where a server still offers one, keeps its own glyph rather than a menu row. design-eng D33 (2026-09-04) already ruled this way and was not followed: "a project row's verbs are hover actions on the header ... a per-row menu would be a new idiom", rejecting "a ⋯ menu on every project row". Reversibility: low. Focus survives the change rather than degrading. The menu item that opened the confirm unmounted with its popover, so focus was released to <body>; the X is still mounted when the dialog closes, so focus returns to the control that opened it. dialog-shell's CreateAgentDialog case gains the trigger it could not have before. The collapsed-row exclusion in styles.css goes with the menu: it existed only because a menu trigger always carries aria-expanded, which made every collapsed project row wear a standing ⋮. Plain row actions carry no such attribute. The hover-reveal spec now asserts the contract across every action on the row instead of a hard-coded pair, so adding or removing a verb cannot leave one standing at full strength unnoticed. Verified: 544 playwright mock specs pass; web typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KQCZymBi59bqzQ1dEKQYbw
Creating an agent inside an existing project had no door left. `mapOwnsCreation`
is `studioProject != null`, and the comment above it says current servers issue
a durable Studio project for every scope — so it is true everywhere. It gated
both the row's create action and the empty project's create row, on the stated
grounds that "its pinned Agent Map is the only route to generating agents".
AgentMapPane has no create control. The route was the planner session driving
the map's MCP tools, which SAP-3143 deletes in this same branch. A project that
already holds agents was therefore left with nothing: the row offered a session
and a remove, the empty-project row never rendered, and the rail's top CTA opens
the composer with no project context, so it cannot create INTO a project.
The `+` is New agent again, which is also what the design has said since
2026-09-04 — design-eng IA.md 219 and D34(a) put `project-create-agent-{label}`
on the row itself, hover-revealed. A bare project keeps its distinct scaffold
verb, which grows an agent inside the session already running there.
`project-start-session-{label}` comes off the row. D34(e) and D35 item 6 give a
plain session to the tab strip, and SAP-3143 added the project pane's own Start
a session, which is the map-altitude path. `startProjectSession` in App.tsx is
untouched and still serves both.
The empty project still gets no create row of its own: D36 makes its Agent Map
row the CTA, and that gate is the one thing `mapOwnsCreation` still owns.
Verified: 544 playwright mock specs pass; web typecheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQCZymBi59bqzQ1dEKQYbw
Primary change type
Problem and motivation
Selecting a project, or its Plan Agents row, opened a session automatically: a
map-plannersession launched behind the user's back that owned the centre pane and could not build anything (#835 unblocked what that session can do). There is no planning session type. The Agent Map is a view of durable state, and a project's sessions are ordinary sessions the user starts.Summary and scope
createSessionAtpath as the tab strip's plus.planningkey, and a persistedmap-planneridentity is no longer honored on resume, so those sessions come back as ordinaryagent-buildersessions. No data loss: the session, its cwd, and its vendor conversation id are untouched.POST /api/projects/:id/planner-sessionsand its message and greeting-retry children returnplanner_sessions_removedwith a sentence telling a stale client to start an ordinary session. The boot-token gate still comes first.handleSelectWorkspaceresolves a session for the focus first; a Studio project with none shows its map and waits.Out of scope: the Agent Map itself, proposals, and the E2 store.
deleted
src/core/planner-greeting.tssrc/core/planner-greeting.test.tssrc/core/planning-session.tssrc/core/planning-session.test.tssrc/profiles/agent-map-planner.tsAlso removed in place: the planner routes and their schemas in
server/agent-map.ts, the planner wiring inserver/index.ts, the threeplanner_session_requires_scoped_routefences inserver/rest.ts,setPlanningMetadataand the trustedplanningoptions incore/session-manager.ts,HarnessSession.planning, the planner analytics event types, the planner-only types inshared/agent-map.ts,paths.plannerSessions, the clientopenPlannerSession/sendPlannerMessage/retryPlannerGreetingand their mock, and the planner pane inuse-agent-map-entry.ts. Net non-test change: 254 added, 3421 removed.PlanningSessionIdentityand themap-planneractor role stay: durable proposal history records the actor that wrote it, and old rows name that role.Related work
Related issue or discussion: SAP-3143. Follows #835 (the stopgap that made the planner session able to build). Superseded by Yash's #820 / #826 stack; the deletion list mirrors #826's.
Validation
Real server,
node dist/cli/bin.js <tree> --port 4131 --state-root <scratch>, Playwright withpage.on("pageerror")attached, zero page errors in every run.Before (branch
gwitwer/planner-sessions-are-ordinary): clicking Plan Agents created a session unasked. Measured through/api/state: 0 sessions at load, 1 session after the click, rolemap-planner.After (this branch): 0 sessions at load, 0 sessions after the click. The map is on the right, the honest empty state in the centre.
After, explicit Start: one session,
planning: null,agentMapIdentity.role: agent-builder,assignment.kind: unplanned. It scaffolded, typechecked and locally checked a new agent throughsapiom-devon request, and the agent appeared in the rail. Nothing was deployed.Tests and documentation
agent-map-planning.spec.tsrenamed toagent-map.spec.tsand re-pointed test by test: first open starts no session and the explicit Start creates exactly one, a project session keeps the ordinary chrome (Copy path and Open in editor, which the planner hid), ending the last session returns to the empty state.open-project,project-axisandnew-session-composerre-pointed the same way. No test deleted except the ones whose subject no longer exists: the fourrest.test.tsplanner-fence cases, the twoagent-map.test.tsplanner-route cases (replaced by one asserting the 410), theingest.test.tsplanner-redaction case, and theapi.test.tsplanner-mutation case.Documentation: changeset only.
Compatibility and release impact
@sapiom/harnessminor, marked Breaking with the migration note.HarnessSession.planningis removed from the published types; the eight plannerAnalyticsEventTypemembers stay in the union for one release, deprecated and no longer emitted (five of them shipped 0.14.0 already deprecated), and are removed in the next minor.Security
will follow the
Security Policy for
private reporting.
AI assistance
Claude Code (Fable 5.1) made the change and re-pointed the tests under direction, ran the real-server verification, and drafted this body. Verified by the build, typecheck, lint, unit and mock UI suites, and the before/after measurements above.
Checklist
CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.any N/A checks above.
Review rounds
Round 1.
All three findings addressed in
ba1ce3f8, plus the advisory hygiene item. The changeset now says Breaking and carries the migration note, and the deprecated analytics members stay for one release. The registry-load migration has the mutation-tested unit test described above. The map-altitude Start spinner keys to the project's own root rather than to any start in flight. A Studio project's root now comes from one shared rule,studioProjectRoot, next tomostSpecificStudioScope, so the tab strip and the Start target cannot key to different folders under nested scopes.Round 2, verdict approve with comments. Both addressed in
a49b5207. The identity predicate that runs against a user's persisted sessions on first boot is nowcore/project-session-identity.ts, called by the server, with its own tests: a persistedmap-plannerrow is never honored, a server-authored planned assignment is, and a row from another session, project or principal is re-issued. It is mutation-tested against dropping theassignment.kindclause, which is the regression the review named; the earlier session-manager test asserted a hand-written mock's branch instead.mostSpecificStudioScopehas its doc comment back. On the nit: the conversation subject keys to the innermost scope while the tabs key to the project root deliberately, and now says so where the root is derived.did_not_work
pnpm testis not green on this machine even on unmodifiedorigin/main(12 to 13 watcher-test failures, count varies by run). Reported, not investigated here.sessions.jsonand asserts the session comes back ordinary and resumable, and that test fails when the delete is removed. I still have not run a real upgrade over a user's live state root: every real-server run used a fresh one.?mockGreetingand?mockPlannerfixtures are dead now. I left the query-string handling removed with the mock rather than keeping stubs, so a stale bookmark carrying them simply opens a normal project.🤖 Generated with Claude Code
https://claude.ai/code/session_01DUq2aqxS5DQtG6aPvoYtn7