Add Project Update + Project description as agent surfaces (Workstream A)#1213
Open
alicejt wants to merge 6 commits into
Open
Add Project Update + Project description as agent surfaces (Workstream A)#1213alicejt wants to merge 6 commits into
alicejt wants to merge 6 commits into
Conversation
…m A) Cyrus previously only listened for Issue-level Linear events, so the Project surface (the Updates feed, the description body) was invisible to agents. This adds two capabilities: A1 — Project Updates as a conversation surface. An `@<agent>` mention inside a Project Update spawns a conversational agent session bound to that project and the agent replies as a follow-up Project Update. Reuses ChatSessionHandler, so the session runs in a plain workspace directory (no git worktree) — the surface is discussion, not code. The project is the thread: follow-up Updates resume the same session. A2 — Project description as standing context. On a `Project` webhook whose description changed, the description is cached; when an agent picks up any issue under that project, the cached description is injected into its system prompt as long-running context. Caching is opt-in via CYRUS_PROJECT_CACHE_URL / CYRUS_PROJECT_CACHE_TOKEN. Routing note: ProjectUpdate/Project webhooks are workspace-wide — every subscribed installation receives every event — so handlers self-filter. Each instance resolves its own Linear identity (config override, else the `viewer` query) and acts only when it is the agent mentioned, never on its own Updates (loop prevention). - core: ProjectUpdateWebhook / ProjectWebhook types + guards; optional linearAgentId / linearAgentName config fields. - linear-event-transport: fetchProject + createProjectUpdate helpers. - edge-worker: handleProjectUpdateWebhook / handleProjectWebhook wired into handleWebhook; LinearProjectChatAdapter; ProjectDescriptionCache client; project-mention parsing; self-identity resolution at startup. - Unit tests for the type guards, mention routing gate, adapter, and cache client. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A cached empty string would otherwise be treated as a hit, blocking the Linear back-fill forever — so a row written before a project had a description (or any stale empty row) could never self-heal. Treat an empty cached value the same as a miss: fall through to the back-fill. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
|
Validated end-to-end against a live Linear workspace with an isolated test instance:
All four test-plan items pass. Marking ready for review. |
… fetch (B1, N1) B1: mentionsAgent bound against the agent's full Linear viewer name (`tincture-mara`) so a human typing `@mara` silently never matched. Now matches both forms by stripping a configurable prefix (env `CYRUS_AGENT_NAME_PREFIX`, default `tincture-`) before matching. An optional `linearAgentShortName` config field takes precedence over the strip. `stripLinearSelfMention` is updated to strip both forms so the prompt handed to the runner stays clean. EdgeWorker now passes the full identity (not just `name`) to the adapter; the adapter accepts both shapes for back-compat. N1: `resolveSelfLinearIdentity` wraps the Linear `viewer` fetch in a 10-second timeout via `Promise.race`. On cloud, 13 instances hit the viewer endpoint at boot; a slow Linear API would otherwise hang startup indefinitely. The existing fallback path handles the degraded case. Test plan addendum #2 (Ray): added test cases asserting `@mara` matches against `name: "tincture-mara"`, prefix overrides work, and explicit `shortName` wins over the strip. Ref: workstream-a-review.md B1, N1 Co-Authored-By: Bob (Tincture) <noreply@anthropic.com>
…N5, N7, E1, E2) Several risks Ray flagged on the workspace-wide Project Update fan-out: B2 (rate limit): on cross-agent ping-pong there was no upper bound. Adds a per-project rolling-window reply rate-limiter (default 3 replies per 5-minute window per instance; env `CYRUS_PROJECT_REPLY_LIMIT_COUNT` / `CYRUS_PROJECT_REPLY_LIMIT_WINDOW_MS`). Plus a bonus loop guard: if the last two updates on a project look agent-authored — `self` user id, or a Linear user whose name begins with the configured agent prefix — skip + log. B3 (team routing gate): fetch the project once per qualified mention, intersect its team keys with the union of `teamKeys` across this instance's repositories. No intersection → warn + skip. Re-uses the same fetchProject call for N7. N4 (`update` action): only proceed when the mention was newly added. A typo-fix edit on an existing mention no longer re-fires the handler. N7 (persona pick): with project teamIds in hand, pick the repo whose `teamKeys` intersect (via the new `getRepositoryForProject` on `ChatRepositoryProvider`). Cross-team agents like Iris now reply in the mode of the team that owns the project, not whichever repo Map iteration returned first. The resolved project is threaded through the webhook on an internal `_resolvedProject` field so the adapter doesn't re-fetch. N2 (size cap on injected description): default 12,000 chars, override via `CYRUS_PROJECT_CONTEXT_MAX_CHARS`. Truncated bodies get a marker suffix. N3 (tag escape): replace `</` with `< /` in user-supplied description / update bodies before interpolation so reserved closing tags (`</project_context>`, `</project_description>`, `</recent_updates>`) inside content can't terminate the wrapper. N5 / E1: deferred per Ray. Added inline notes documenting the acceptable cost (concurrent back-fill wastage on first project pickup; N+1 author hydration in `fetchThreadContext`). E2 (efficiency): parse mentions once on the no-match debug branch instead of running the regex twice. Tests cover B2 cap + bonus loop guard, B3 intersect / non-intersect, N4 both directions, N7 persona switch, N3 escape on both injection sites. Also adds a unit test suite for the new `ProjectReplyRateLimiter`. Ref: workstream-a-review.md B2, B3, N2, N3, N4, N5, N7, E1, E2 Co-Authored-By: Bob (Tincture) <noreply@anthropic.com>
… E3) N6: ProjectDescriptionCache.get now returns both the description and the parsed `updated_at` timestamp from the edge function (which already emits it). EdgeWorker's `getCachedProjectDescription` treats a cached row older than the TTL as a miss and back-fills from Linear. Default TTL is 7 days, override via `CYRUS_PROJECT_CONTEXT_CACHE_TTL_DAYS`. Covers the case where a `Project` description-update webhook is dropped, leaving the cache stale-but-non-empty indefinitely. E3: switch the GET path from string concatenation to `new URL(...)` + `searchParams.set`, so a future bridge URL carrying a query string won't produce `?…?…` after the project id is appended. Tests cover the TTL freshness branch (stale → re-fetch; fresh → serve from cache), and that the GET path encodes special characters via searchParams. Ref: workstream-a-review.md N6, E3 Co-Authored-By: Bob (Tincture) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Cyrus today only listens for Issue-level Linear events, so the Project surface — the Updates feed and the description body — is invisible to agents. This adds two capabilities:
A1 — Project Updates as a conversation surface
An
@<agent>mention inside a Linear Project Update spawns a conversational agent session bound to that project, and the agent replies as a follow-up Project Update. It reusesChatSessionHandler(the Slack-chat engine), so the session runs in a plain workspace directory — no git worktree — because the surface is discussion, not code. The project is the thread: follow-up Updates on the same project resume the same session.A2 — Project description as standing context
On a
Projectwebhook whose description changed, the description is cached; when an agent later picks up any issue under that project, the cached description is injected into its system prompt as long-running context. Caching is opt-in viaCYRUS_PROJECT_CACHE_URL/CYRUS_PROJECT_CACHE_TOKENenv vars — when unset it's a no-op, so this is inert for installs that don't want it.Routing — self-filtering, not central routing
ProjectUpdateandProjectwebhooks are workspace-wide: every installation subscribed to them receives every event (unlike Issue/AgentSession webhooks, which are agent-targeted). So instead of a central router picking an agent, each instance self-filters — it resolves its own Linear identity (fromlinearAgentId/linearAgentNameconfig, else theviewerquery at startup) and acts only when it is the agent@-mentioned. It never replies to its own Updates (loop prevention).Changes by package
ProjectUpdateWebhook/ProjectWebhooktypes +isProjectUpdateWebhook/isProjectWebhook/isProjectDescriptionUpdateWebhookguards; optionallinearAgentId/linearAgentNameconfig fields.fetchProject+createProjectUpdatehelpers onLinearIssueTrackerService.handleProjectUpdateWebhook/handleProjectWebhookwired intohandleWebhook;LinearProjectChatAdapter(aChatPlatformAdapterfor Project Update conversations);ProjectDescriptionCacheclient;project-mentions.ts(mention parsing + the self-filter gate); self-identity resolution at startup;<project_context>injection into new-session system prompts.Subscription / config (operator-facing)
To enable, subscribe the Linear app to the
ProjectUpdateandProjectwebhook events.handleWebhookalready ignores unknown event types gracefully, so subscribing before/without this change is harmless. A2's cache is off unless the two env vars are set.Testing
core+edge-workersuites show no new failures (pre-existingscreenshot-upload-hooks/runner-selectionfailures onmainare unrelated).pnpm build+pnpm typecheckclean across the monorepo.🤖 Generated with Claude Code