diff --git a/docs/architecture/02-webhook-pipeline.md b/docs/architecture/02-webhook-pipeline.md
index fda4e4ef3..214fd2070 100644
--- a/docs/architecture/02-webhook-pipeline.md
+++ b/docs/architecture/02-webhook-pipeline.md
@@ -85,6 +85,8 @@ interface ParsedWebhookEvent {
| `LinearRouterAdapter` | `src/router/adapters/linear.ts` | Linear team ID |
| `SentryRouterAdapter` | `src/router/adapters/sentry.ts` | CASCADE `projectId` (from URL) |
+Sentry keeps the route shape `/sentry/webhook/:projectId`. The project ID selects the Cascade project, whose Sentry config includes a paired `organizationSlug`/`projectSlug`. The adapter filters payloads by matching Sentry project identifiers against the configured `projectSlug`; organization-level deliveries whose payload project does not match are acknowledged but do not dispatch agents.
+
## The 12-Step Pipeline
`src/router/webhook-processor.ts` — `processRouterWebhook()`
diff --git a/docs/architecture/03-trigger-system.md b/docs/architecture/03-trigger-system.md
index c8812535b..9820e89b0 100644
--- a/docs/architecture/03-trigger-system.md
+++ b/docs/architecture/03-trigger-system.md
@@ -136,8 +136,9 @@ function registerBuiltInTriggers(registry: TriggerRegistry): void {
| Handler | Event | Agent |
|---------|-------|-------|
-| `AlertingIssueTrigger` | Sentry issue alert | `alerting` |
-| `AlertingMetricTrigger` | Sentry metric alert | `alerting` |
+| `AlertingIssueTrigger` | Sentry `event_alert` resource | `alerting` |
+| `AlertingMetricTrigger` | Sentry `metric_alert` resource | `alerting` |
+| `SentryIssueLifecycleTrigger` | Sentry `issue` lifecycle resource | `alerting` |
## Trigger Configuration
@@ -147,7 +148,7 @@ Triggers use category-prefixed events from `src/triggers/shared/events.ts`. `TRI
- PM: `pm:status-changed`, `pm:label-added`, `pm:comment-mention`
- SCM: `scm:check-suite-success`, `scm:check-suite-failure`, `scm:pr-review-submitted`, `scm:review-requested`, `scm:pr-opened`, `scm:pr-comment-mention`, `scm:pr-merged`, `scm:pr-ready-to-merge`, `scm:pr-conflict-detected`
-- Alerting: `alerting:issue-alert`, `alerting:metric-alert`
+- Alerting: `alerting:issue-alert`, `alerting:metric-alert`, `alerting:issue-lifecycle`
- Internal: `internal:auto-chain`
New handlers should import `TRIGGER_EVENTS` instead of adding raw string literals. The static guard in `tests/unit/triggers/trigger-event-consistency.test.ts` fails when a handler gates on one event string and emits a different `agentInput.triggerEvent`.
@@ -203,7 +204,7 @@ Each trigger in a YAML agent definition can declare a `contextPipeline` — an o
| `prepopulateTodos` | Pre-populate todo list from work item checklists |
| `prContext` | Fetch PR details, GitHub changed-file metadata, locally verified compact per-file diffs from `origin/...HEAD`, and CI checks; emit a `SKIPPED FILES` injection when files are omitted (over budget, deleted, binary/no patch, or local diff unavailable) |
| `prConversation` | Fetch PR comments and review threads |
-| `pipelineSnapshot` | Fetch CI pipeline status |
+| `pipelineSnapshot` | Fetch PM workflow/pipeline state and emit the single authoritative `PipelineSnapshotSummary` JSON context for backlog-manager |
| `alertingIssue` | Fetch Sentry issue and event details |
## Shared Agent Execution
diff --git a/docs/architecture/04-agent-system.md b/docs/architecture/04-agent-system.md
index b2b82ac4f..6b22a5c0b 100644
--- a/docs/architecture/04-agent-system.md
+++ b/docs/architecture/04-agent-system.md
@@ -212,6 +212,14 @@ Pipeline prompts receive separate PM identifiers for selection and creation:
For Trello, BACKLOG is a list, so `backlogStatusId` and `workItemCreateContainerId` are both the backlog list ID. For JIRA, `backlogStatusId` is `jira.statuses.backlog` and creation uses `jira.projectKey`. For Linear, `backlogStatusId` is `linear.statuses.backlog` and creation uses `linear.teamId`; backlog-manager must not use the Linear team ID to discover candidate backlog issues.
+### Backlog-manager pipeline context
+
+The `backlog-manager` agent requires the `pipelineSnapshot` context step on every run. That internal step key now emits exactly one context injection named `PipelineSnapshotSummary`; its `result` is structured JSON, not markdown. The JSON is the authoritative contract for active pipeline capacity, backlog ordering, per-status counts, `itemsById`, comments, checklists, labels, descriptions, attachments/media references, dependency signals, and provider or item-read errors.
+
+The previous human-readable markdown `PipelineSnapshot` context was intentionally removed as a clean contract break. Prompt policy should read `PipelineSnapshotSummary.statuses..itemIds` and `PipelineSnapshotSummary.itemsById` directly instead of parsing formatted work-item text.
+
+When capacity is available but every backlog item is blocked, backlog-manager posts the `Backlog Blocked` comment exactly once on the first item in `PipelineSnapshotSummary.statuses.backlog.itemIds` provider order. If BACKLOG is empty, it exits silently without posting that comment. Selected items are still moved only from BACKLOG to TODO with `MoveWorkItem.expectedSourceState` set to the configured backlog source.
+
### Alert task prompt context
Alerting task prompts can reference scalar alert fields passed through `AgentInput`:
diff --git a/docs/architecture/06-integration-layer.md b/docs/architecture/06-integration-layer.md
index d4ef73a73..45a805421 100644
--- a/docs/architecture/06-integration-layer.md
+++ b/docs/architecture/06-integration-layer.md
@@ -148,8 +148,8 @@ Each provider declares its credential roles — the mapping from logical role na
- `SentryAlertingIntegration` implements `AlertingIntegration`
- `sentryClient` — REST API client with Bearer token auth
-- Supports issue alerts, metric alerts, and issue lifecycle webhooks
-- Config: `organizationSlug` stored in `project_integrations.config` JSONB
+- Supports Sentry webhook resources `event_alert`, `metric_alert`, and `issue` lifecycle
+- Config: `organizationSlug` and `projectSlug` stored in `project_integrations.config` JSONB
## PM Abstraction
diff --git a/docs/architecture/08-config-credentials.md b/docs/architecture/08-config-credentials.md
index 3812c2f9c..6c376c1aa 100644
--- a/docs/architecture/08-config-credentials.md
+++ b/docs/architecture/08-config-credentials.md
@@ -77,6 +77,16 @@ Backlog selection and work-item creation use different native concepts:
For Linear, `teamId` is not a backlog state. It is the container used when creating or searching issues, and unfiltered team listing may include unmapped workflow states such as Ideas. Pipeline snapshot and backlog-manager paths use status-aware listing (`ListWorkItems --status backlog`) so only the configured `linear.statuses.backlog` state is eligible for selection.
+Backlog-manager receives pipeline state through the required `pipelineSnapshot`
+context step, which emits one structured `PipelineSnapshotSummary` JSON
+injection. The JSON includes provider-source status counts, active pipeline
+count, backlog item order, `itemsById`, comments, checklists, labels,
+descriptions, attachments/media references, dependency signals, and provider
+errors. The legacy markdown `PipelineSnapshot` context is intentionally not
+emitted. If all backlog items appear blocked, backlog-manager posts the blocked
+backlog comment only on the first BACKLOG item in `statuses.backlog.itemIds`
+provider order; when BACKLOG is empty it exits without posting.
+
`maxInFlightItems` is enforced at two points: (a) the `backlog-manager` chain
gates (won't auto-pull from BACKLOG when at capacity) and (b) the PM
`status-changed` triggers (won't fire `implementation` when a card is moved
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 6be0c1076..b0ef84090 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -307,7 +307,7 @@ node bin/cascade.js webhooks create my-project \
--callback-url https://your-tunnel.ngrok.io
```
-This creates webhooks on GitHub, Trello, and Jira when those integrations are configured, reusing existing hooks when the canonical callback URL already exists. Linear and Sentry are informational/manual setup paths: the dashboard and API show the correct callback URL and whether a signing secret is stored, but you create the webhook in the provider UI.
+This creates webhooks on GitHub, Trello, and Jira when those integrations are configured, reusing existing hooks when the canonical callback URL already exists. Linear and Sentry are informational/manual setup paths: the dashboard and API show the correct callback URL and whether a signing secret is stored, but you create the webhook in the provider UI. For Sentry, the URL remains `https://your-router-host/sentry/webhook/:projectId`; organization-level deliveries may reach that URL, but Cascade dispatches only payloads whose Sentry project matches the configured `projectSlug`.
| Provider | Setup behavior | Callback URL |
|----------|----------------|--------------|
@@ -315,7 +315,7 @@ This creates webhooks on GitHub, Trello, and Jira when those integrations are co
| Trello | Programmatic create/list/delete | `https://your-router-host/trello/webhook` |
| Jira | Programmatic create/list/delete plus label ensure | `https://your-router-host/jira/webhook` |
| Linear | Manual setup with optional `LINEAR_WEBHOOK_SECRET` | `https://your-router-host/linear/webhook` |
-| Sentry | Manual setup with optional Sentry webhook secret | `https://your-router-host/sentry/webhook/my-project` |
+| Sentry | Manual setup with optional Sentry webhook secret; paired with configured `organizationSlug`/`projectSlug` and filtered by payload project matching `projectSlug` | `https://your-router-host/sentry/webhook/my-project` |
---
diff --git a/src/agents/definitions/backlog-manager.yaml b/src/agents/definitions/backlog-manager.yaml
index 53cf66569..26f7692ca 100644
--- a/src/agents/definitions/backlog-manager.yaml
+++ b/src/agents/definitions/backlog-manager.yaml
@@ -58,7 +58,7 @@ triggers:
# Required context — runs for EVERY invocation regardless of trigger source.
# Manual `cascade runs trigger --agent-type backlog-manager` would otherwise
# skip the per-trigger contextPipeline (triggerEvent is undefined), and the
-# agent would freelance with no snapshot. Empty pipelineSnapshot result
+# agent would freelance with no structured PipelineSnapshotSummary JSON. Empty pipelineSnapshot result
# aborts the run with a structured error + Sentry capture under tag
# `context_pipeline_required_step_failed` (closes the 2026-04-29 incident
# where MNG-422 was pulled from SPLITTING into TODO).
@@ -70,10 +70,11 @@ prompts:
# The system prompt (.eta file) contains full instructions.
# This taskPrompt provides the per-run context and directive.
taskPrompt: |
- A Pipeline Snapshot has been pre-loaded into your context with the current state of all pipeline lists (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE, MERGED).
+ A PipelineSnapshotSummary JSON object has been pre-loaded into your context with the current state of all pipeline statuses (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE, MERGED).
- 1. Review the pre-loaded Pipeline Snapshot and count items currently in the active pipeline (TODO + IN PROGRESS + IN REVIEW).
- 2. If the count is below the capacity limit (see system prompt): use the pre-loaded BACKLOG data from the snapshot to select ALL eligible unblocked items to fill remaining capacity completely — always move the maximum number possible.
+ 1. Parse PipelineSnapshotSummary. If activeCapacityReliable is false, abort immediately — capacity count is unreliable due to a fetch error; do not move any items. Otherwise use activePipelineCount for items currently in the active pipeline (TODO + IN PROGRESS + IN REVIEW).
+ 2. If the count is below the capacity limit (see system prompt): use PipelineSnapshotSummary.statuses.backlog.itemIds in provider order plus itemsById details to select ALL eligible unblocked items to fill remaining capacity completely — always move the maximum number possible.
3. If already at or above capacity: exit immediately without taking action.
+ 4. If every BACKLOG item is blocked, post one Backlog Blocked comment on the first BACKLOG item in PipelineSnapshotSummary.statuses.backlog.itemIds order. If BACKLOG is empty, exit silently.
hint: Only act if pipeline has capacity (items in TODO + IN PROGRESS + IN REVIEW < maxInFlightItems). When acting, ALWAYS fill ALL remaining capacity.
diff --git a/src/agents/definitions/contextSteps.ts b/src/agents/definitions/contextSteps.ts
index 5f43beb8d..4d2021f19 100644
--- a/src/agents/definitions/contextSteps.ts
+++ b/src/agents/definitions/contextSteps.ts
@@ -7,7 +7,11 @@
import { formatCheckStatus } from '../../gadgets/github/core/getPRChecks.js';
import { ListDirectory } from '../../gadgets/ListDirectory.js';
-import { readWorkItem, readWorkItemWithMedia } from '../../gadgets/pm/core/readWorkItem.js';
+import {
+ readStructuredWorkItemDetails,
+ readWorkItemWithMedia,
+ type StructuredWorkItemDetails,
+} from '../../gadgets/pm/core/readWorkItem.js';
import { formatSentryEvent } from '../../gadgets/sentry/core/format.js';
import type { Todo } from '../../gadgets/todo/storage.js';
import {
@@ -18,6 +22,13 @@ import {
} from '../../gadgets/todo/storage.js';
import { githubClient } from '../../github/client.js';
import { getJiraConfig, getLinearConfig, getTrelloConfig } from '../../pm/config.js';
+import type {
+ Attachment,
+ Checklist,
+ MediaReference,
+ WorkItem,
+ WorkItemLabel,
+} from '../../pm/index.js';
import { getPMProviderOrNull } from '../../pm/index.js';
import { getSentryClient } from '../../sentry/client.js';
import type { AgentInput, ProjectConfig } from '../../types/index.js';
@@ -367,8 +378,69 @@ interface PipelineListResult {
error: string | null;
}
+type PipelineStatusKey = PipelineList['statusKey'];
+
+interface PipelineCommentSummary {
+ id: string;
+ authorName: string;
+ date: string;
+ text: string;
+}
+
+interface PipelineDependencySignal {
+ sourceType: 'description' | 'comment' | 'checklist' | 'attachment';
+ sourceId?: string;
+ text: string;
+ matches: string[];
+}
+
+interface PipelineStatusSummary {
+ statusKey: PipelineStatusKey;
+ statusName: string;
+ itemIds: string[];
+ count: number;
+ error?: string;
+}
+
+interface PipelineItemSummary {
+ id: string;
+ title: string;
+ url: string;
+ statusKey: PipelineStatusKey;
+ statusName: string;
+ providerStatus?: string;
+ providerStatusId?: string;
+ description?: string;
+ labels: WorkItemLabel[];
+ checklists: Checklist[];
+ comments: PipelineCommentSummary[];
+ attachments: Attachment[];
+ mediaReferences: MediaReference[];
+ dependencySignals: PipelineDependencySignal[];
+ error?: string;
+}
+
+interface PipelineSnapshotSummary {
+ schemaVersion: 1;
+ provider: string;
+ statuses: Partial>;
+ activePipelineCount: number;
+ /**
+ * `true` when all active-status list fetches (todo, inProgress, inReview) succeeded.
+ * `false` when any active-status fetch failed — the count is a lower bound, not authoritative.
+ * When false, the backlog-manager MUST abort without moving items.
+ */
+ activeCapacityReliable: boolean;
+ activeStatusKeys: PipelineStatusKey[];
+ itemsById: Record;
+ errors: Array<{ statusKey?: PipelineStatusKey; itemId?: string; message: string }>;
+}
+
const PIPELINE_DETAIL_LISTS = new Set(['BACKLOG', 'TODO', 'IN_PROGRESS', 'IN_REVIEW']);
const PIPELINE_DETAIL_CONCURRENCY = 5;
+const ACTIVE_PIPELINE_STATUS_KEYS: PipelineStatusKey[] = ['todo', 'inProgress', 'inReview'];
+const DEPENDENCY_SIGNAL_REGEX =
+ /\b(?:blocked by|depends on|waiting for|after|requires)\b|[A-Z][A-Z0-9]+-\d+|https?:\/\/\S+/gi;
function buildPipelineLists(project: ProjectConfig): PipelineList[] {
const trelloConfig = getTrelloConfig(project);
@@ -437,15 +509,15 @@ function collectItemsNeedingFullDetails(listResults: PipelineListResult[]): Arra
async function fetchFullPipelineDetails(
items: Array<{ id: string }>,
logWriter: LogWriter,
-): Promise