Skip to content

feat(cursor): support workspace/open event routes as fire-and-forget observation - #298

Merged
ScriptedAlchemy merged 3 commits into
mainfrom
wave/cursor-workspace-open
Sep 2, 2026
Merged

feat(cursor): support workspace/open event routes as fire-and-forget observation#298
ScriptedAlchemy merged 3 commits into
mainfrom
wave/cursor-workspace-open

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Summary

Re-verification of #221's workspace/open ruling against Cursor's current hooks reference, triggered by the challenge that Cursor does support a workspace-open hook. Outcome: partially expressible — implemented as fire-and-forget observation on the Cursor target.

The #221 reason had two claims:

  1. "Sessionless" — still true. https://cursor.com/docs/hooks documents that app lifecycle hooks (workspaceOpen) fire outside any agent session; the request omits conversation_id, generation_id, model, session_id, and transcript_path and carries hook_event_name, cursor_version, workspace_roots, user_email. But this was a defect of our wrapper's own session_id validation, not a native inexpressibility.
  2. "Its response must return pluginPaths" — wrong per current docs. The output field table marks pluginPaths as string[] (optional): an empty/no-output response is legal and the workspace open continues.

Since the canonical workspace/open family is observation-shaped (projection emits no native output), Cursor can honestly support it: the generated event-route wrapper validates the real sessionless envelope, renders the route, and emits nothing. The native pluginPaths return channel is deliberately not modeled and never emitted — no code path can fabricate plugin loads.

Native proof (live binary, 2026-09-02 UTC)

Captured on Cursor CLI cursor-agent 2026.08.31-4057e58 (trusted project hooks, non-interactive -p run): workspaceOpen fired with exactly the documented sessionless envelope

{"hook_event_name":"workspaceOpen","cursor_version":"2026.08.31-4057e58","workspace_roots":["<workspace>"],"user_email":"<account email>"}

while the same run's sessionStart carried session_id/conversation_id. The capture hook exited 0 with no stdout and the workspace open plus agent session proceeded — proving the pluginPaths-omitted empty response on the real binary, independent of agent-session wrappers (the #189 acceptance bar). Pinned as a dated evidence bullet in the capability table.

Changes

  • events/projection.ts: validateNativeEventEnvelope gets a cursor workspace/open branch (requires nonempty workspace_roots string array + cursor_version, accepts user_email string|null, requires no session fields); projectEventDocument explicitly rejects deny/updatedInput (observation-only) and context text (no output channel) and returns no output otherwise.
  • adapters/hook-contract.ts: the cursor workspace/open lifecycle starter is the real sessionless envelope.
  • adapters/capabilities/cursor-2026-08-28.json: workspace/open flipped to supported; stale "must return pluginPaths" evidence replaced with dated doc citation + live CLI capture; observedAt bumped to 2026-09-02.
  • Plain hooks.events handler hooks stay unavailable (diagnostic preserved): the session-scoped handler vocabulary genuinely cannot express the sessionless envelope. Composite plugin target stays unavailable (Claude ∩ Codex have no such event); its merged reason no longer mentions pluginPaths.
  • Tests: G10 matrix, capability-table sha repin, route-graph (cursor-restricted workspace/open route now inspects ready), envelope validation, projection rejections through real Flight render, generated-wrapper execution with empty stdout, and feat(dev): semantic lifecycle replay (#105 stage 3) #279 lifecycle fixture replay asserting nativeResponse undefined.
  • examples/rsc-agent-runtime/README.md event matrix row updated (Cursor: supported observe-only; Claude/Codex unchanged).
  • Changeset: minor.

Gates (local, after rebase onto 3c08e6e)

  • pnpm typecheck clean, pnpm lint clean (0/0 across 978 files)
  • pnpm test:route-unit, pnpm test:projection: all pass
  • Scoped unit suites (adapter-capability-states, adapter-metadata, event-project, host-adapters, route-graph, hooks, target-hook-contract, cursor-adapter, lifecycle-replay-service/routes/dev-server): all pass, no failures

Closes the #189 workspaceOpen checklist row ("Correct workspaceOpen input/output (pluginPaths) and prove it independently of agent-session wrappers").

Accept Cursor's sessionless lifecycle envelope while deliberately projecting no native output, preserving pluginPaths as an unmodeled host channel.
@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d8d9753

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ScriptedAlchemy
ScriptedAlchemy merged commit 69a413b into main Sep 2, 2026
4 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T07:13:48.826245Z d8d9753 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8d9753207

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +58 to +62
if (canonicalEvent === 'workspace/open') {
if (
!Array.isArray(native.workspace_roots)
|| native.workspace_roots.length === 0
|| !native.workspace_roots.every((root) => typeof root === 'string' && root.trim() !== '')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate request workspace from the validated roots

For every Cursor workspaceOpen invocation, the newly accepted envelope contains workspace_roots but no cwd; however, both generated execution paths still derive the runAgentRequest workspace exclusively from native.cwd (hook-contract.ts for standalone and mcp-server-runtime.ts for shared). Consequently, (await agent()).workspace is reported as unavailable for this route despite the host supplying workspace identity, which also prevents workspace-scoped providers/notices from using the opened workspace. Thread an appropriate root from this validated field into both execution paths, with explicit semantics for multi-root workspaces.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a45992e: both shared and standalone event execution now prefer native cwd and fall back to workspace_roots[0]; the scalar first-root multi-root rule is documented. The #298 envelope, projection, lifecycle replay, and real shared-route tests pass.

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Post-merge regression note: the new Cursor workspace/open test was already red at its introducing commit 69a413b62: it wrote and ran the generated wrapper without compiling/emitting its standalone worker. Converting the fixture to compile the real wrapper/worker pair exposed a product defect where Rspack tried to bundle the separately emitted hooks-flight.mjs. #328 keeps that URL runtime-relative and verifies the compiled pair; merged as 3368b5b6a485506cc466ce4bb4920078a49db432.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant