Context
The main motivation for this rail is that Plane offers a self-hosted Community Edition (the same codebase you can deploy on your own infra), whereas Linear is subscription-only and cloud-only. For operators who can't or won't use a SaaS issue tracker — homelab self-hosters, compliance/data-residency constraints, cost-sensitive teams — Linear is off the table. Plane CE is the natural alternative and unlocks Cyrus for that whole segment.
Plane's "Agents Beta" framework, however, only ships in Plane Commercial — Plane CE users can't use the Linear-Agent-SDK pattern. To make Cyrus work with self-hosted Plane CE I built a parallel transport package (cyrus-plane-event-transport) and wired it into the edge-worker, mirroring the GitHub/GitLab "own lane" pattern rather than touching the AgentEventTransportConfig discriminated union.
I'm opening this issue before submitting a PR to gauge whether the maintainers want this rail upstream, since it's a non-trivial chunk of code (~4.3k LOC across core, a new package, and edge-worker).
Approach (high-level)
- New package
packages/plane-event-transport/: webhook receiver (Fastify, HMAC X-Plane-Signature verify), payload translator (emits canonical PlaneAgentEvent matching Linear's shape), REST client (PlaneIssueTrackerService).
packages/edge-worker/src/ additions: PlaneSessionRunner (orchestrates ack → worktree → ClaudeRunner.startStreaming), PlaneClaudeActivityPoster (maps SDKMessage → Plane issue comments), PlaneSessionStore (persists last claudeSessionId per issue id to ~/.cyrus/plane-sessions.json for cross-restart resume), handlePlaneEvent dispatcher.
packages/core: a handful of optional RepositoryConfig fields (planeProjectId, planeAgentLabelIds, planeBypassPermissions, planeMaxTurns). Backwards-compatible — all .optional().
- Multi-turn parity with Linear's
handlePromptWithStreamingCheck: live runner + supportsStreamingInput → addStreamMessage; dead runner with persisted claudeSessionId → spawn new runner with resumeSessionId; no prior session → post "reassign to start" guidance.
Evidence
- Public fork: https://github.com/KevinLemaMartinez/cyrus (branch
feat/plane-event-transport, rebased on cyrusagents/cyrus@main v0.2.57).
- E2E smoke against real Plane CE 1.3.1 (self-hosted) passed end-to-end: assignment → ack → worktree → Claude session → live
addStreamMessage mid-session → session_completed (87 messages) → persistence write → resume after PR with resumeSessionId from the store. All three multi-turn branches verified in production logs.
- Tests: 25 in
cyrus-plane-event-transport, 17 net new in cyrus-edge-worker (PlaneSessionStore + PlaneSessionRunner extensions + label-filter helper). Full pnpm test:packages:run green.
- 28 commits, conventional commits, focused per concern (one runner refactor / one store / etc).
Open design questions for maintainers
planeBypassPermissions default: ships true because PlaneSessionRunner has no onAskUserQuestion plumbing — without bypass, the first Edit/Write/Bash hangs waiting for an approver. Acceptable for the POC; would you require false default + a permission-prompt-via-Plane-comment mechanism before considering merge?
- F1 test drive: CLAUDE.md mandates F1 for "major work". F1 is Linear-only today. Would a manual smoke checklist (against a self-hosted Plane CE) be acceptable, or do you want a Plane equivalent of F1?
- Scope split: 4.3k LOC in one PR is heavy. Happy to split into 3 sequential PRs (transport package only → edge-worker integration → hardening guards) if you prefer.
Status
I'd love to upstream this if there's interest. Happy to address whatever review feedback comes. If the rail doesn't fit the project direction, no hard feelings — I'll continue maintaining the fork.
Thanks!
Context
The main motivation for this rail is that Plane offers a self-hosted Community Edition (the same codebase you can deploy on your own infra), whereas Linear is subscription-only and cloud-only. For operators who can't or won't use a SaaS issue tracker — homelab self-hosters, compliance/data-residency constraints, cost-sensitive teams — Linear is off the table. Plane CE is the natural alternative and unlocks Cyrus for that whole segment.
Plane's "Agents Beta" framework, however, only ships in Plane Commercial — Plane CE users can't use the Linear-Agent-SDK pattern. To make Cyrus work with self-hosted Plane CE I built a parallel transport package (
cyrus-plane-event-transport) and wired it into the edge-worker, mirroring the GitHub/GitLab "own lane" pattern rather than touching theAgentEventTransportConfigdiscriminated union.I'm opening this issue before submitting a PR to gauge whether the maintainers want this rail upstream, since it's a non-trivial chunk of code (~4.3k LOC across
core, a new package, andedge-worker).Approach (high-level)
packages/plane-event-transport/: webhook receiver (Fastify, HMACX-Plane-Signatureverify), payload translator (emits canonicalPlaneAgentEventmatching Linear's shape), REST client (PlaneIssueTrackerService).packages/edge-worker/src/additions:PlaneSessionRunner(orchestrates ack → worktree →ClaudeRunner.startStreaming),PlaneClaudeActivityPoster(mapsSDKMessage→ Plane issue comments),PlaneSessionStore(persists lastclaudeSessionIdper issue id to~/.cyrus/plane-sessions.jsonfor cross-restart resume),handlePlaneEventdispatcher.packages/core: a handful of optionalRepositoryConfigfields (planeProjectId,planeAgentLabelIds,planeBypassPermissions,planeMaxTurns). Backwards-compatible — all.optional().handlePromptWithStreamingCheck: live runner +supportsStreamingInput→addStreamMessage; dead runner with persistedclaudeSessionId→ spawn new runner withresumeSessionId; no prior session → post "reassign to start" guidance.Evidence
feat/plane-event-transport, rebased oncyrusagents/cyrus@mainv0.2.57).addStreamMessagemid-session →session_completed(87 messages) → persistence write → resume after PR withresumeSessionIdfrom the store. All three multi-turn branches verified in production logs.cyrus-plane-event-transport, 17 net new incyrus-edge-worker(PlaneSessionStore + PlaneSessionRunner extensions + label-filter helper). Fullpnpm test:packages:rungreen.Open design questions for maintainers
planeBypassPermissionsdefault: shipstruebecausePlaneSessionRunnerhas noonAskUserQuestionplumbing — without bypass, the first Edit/Write/Bash hangs waiting for an approver. Acceptable for the POC; would you requirefalsedefault + a permission-prompt-via-Plane-comment mechanism before considering merge?Status
I'd love to upstream this if there's interest. Happy to address whatever review feedback comes. If the rail doesn't fit the project direction, no hard feelings — I'll continue maintaining the fork.
Thanks!