Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ All notable user-visible changes to CASCADE are documented here. The format is l

### Fixed

- **Review-agent compact diffs now use local git as the authoritative patch source.** `GetPRDiffContext` is built from the checked-out PR workspace (`origin/<base>...HEAD`) instead of GitHub's potentially clipped `pulls.listFiles().patch` body, while GitHub remains the source of changed-file ordering and counts. Files without a locally verified patch are explicitly listed in `SKIPPED FILES`, skipped-file guidance now points to `cascade-tools scm get-pr-diff --prNumber <N> --path <path>`, and `PR context prepared` logs include patch-source counts, token counts, skip reasons, and bounded local-vs-GitHub hunk/size mismatches. See Linear issue [MNG-739](https://linear.app/issue/MNG-739).

- **Linear and JIRA inline checklist writes are now idempotent across provider/tool retries.** The shared markdown checklist engine now upserts by exact `### {Checklist Name}` heading and exact item text, merges duplicate inline sections into the first matching section, preserves non-checkbox prose from collapsed duplicate sections, preserves checked state when duplicate rows disagree, and keeps Trello on native checklist APIs. Linear also caches accepted description writes in-process so stale readback no longer overwrites or replays checklist appends into duplicate blocks. See Linear issue [MNG-741](https://linear.app/mongrel/issue/MNG-741/make-linear-checklist-mutations-idempotent-after-visibility-timeouts).

- **`cascade-tools scm reply-to-review-comment` now accepts `--body-file`, and `create-pr-review --comment` handles one inline comment object ergonomically.** `ReplyToReviewComment` now exposes the same generated body file-input contract as the other SCM comment commands. Array-of-object CLI params still prefer JSON arrays, but one top-level JSON object is normalized to a one-item array, while `null` and primitive JSON values fail early with the structured `json-parse` envelope instead of reaching the GitHub client. See Linear issues [MNG-736](https://linear.app/mongrel/issue/MNG-736/fix-cascade-tools-scm-review-command-cli-drift), [MNG-731](https://linear.app/mongrel/issue/MNG-731/friction-tooling-low-cascade-tools-reply-to-review-comment-rejects), and [MNG-729](https://linear.app/mongrel/issue/MNG-729/friction-tooling-low-create-pr-review-comment-accepted-an-object-but).
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,9 @@ The wedged-lock canary should never fire under normal operation. Its presence in

Review agent receives a **compact per-file diff context**, not full file contents. Each changed file is a `### <file> (<status>, +N -M)` section with a unified diff hunk. Budget: `REVIEW_DIFF_CONTEXT_TOKEN_LIMIT` = 200k tokens, per-file cap 10%.

Files that can't fit (deleted, binary, oversized patch, or budget exhausted) are injected as `SKIPPED FILES` with instructions to fetch on demand via `gh pr diff`, `Read`, or `Grep`.
GitHub's changed-file API is used for file enumeration and change counts, but compact patch bodies come from the checked-out PR workspace via `git diff origin/<base>...HEAD`. Files that can't fit or can't be locally verified (deleted, binary/no text patch, local diff failure/empty patch, oversized patch, or budget exhausted) are injected as `SKIPPED FILES` with instructions to fetch on demand via `cascade-tools scm get-pr-diff --prNumber <N> --path <path>`, `Read`, or `Grep`.

When review output misses something, check the `PR context prepared` log entry for `included` / `skipped` / `skipReasons` to confirm whether the file was visible to the agent.
When review output misses something, check the `PR context prepared` log entry for `included` / `skipped` / `skipReasons`, `patchSources`, `totalDiffTokens`, `perFileTokenCap`, and `localGitMismatches` to confirm whether the file was visible to the agent and whether GitHub's API patch differed from the local patch. Also check context offload logs if the diff context was written under `.cascade/context/`.

## Engines

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/03-trigger-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ Each trigger in a YAML agent definition can declare a `contextPipeline` — an o
| `contextFiles` | Read key project files (README, etc.) |
| `workItem` | Fetch work item details from PM tool |
| `prepopulateTodos` | Pre-populate todo list from work item checklists |
| `prContext` | Fetch PR details, compact per-file diffs, CI checks; emit a `SKIPPED FILES` injection when files are omitted (over budget, deleted, binary) |
| `prContext` | Fetch PR details, GitHub changed-file metadata, locally verified compact per-file diffs from `origin/<base>...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 |
| `alertingIssue` | Fetch Sentry issue and event details |
Expand Down
12 changes: 12 additions & 0 deletions docs/architecture/04-agent-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,18 @@ 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.

### Alert task prompt context

Alerting task prompts can reference scalar alert fields passed through `AgentInput`:

| Variable | Purpose |
|----------|---------|
| `alertTitle` | Provider-normalized alert title, with empty and stringified `undefined`/`null` candidates discarded |
| `alertIssueUrl` | Human-facing Sentry issue or alert permalink when available |
| `alertIssueId` | Sentry issue ID for issue/event alerts |
| `alertOrgId` | Sentry organization slug used for alerting API reads |
| `alertMetricKey` | Stable metric-alert key (`orgSlug:title`) used by worker-side materialization |

## Hooks

### Trailing hooks
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/07-gadgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ cascade-tools pm report-friction \
| `GetAlertingEventDetail` | `alerting:read` | Fetch Sentry issue-event details with stacktrace, tags, breadcrumbs, request data, and context |
| `ListAlertingEvents` | `alerting:read` | List recent events for an issue |

`GetAlertingEventDetail` accepts Sentry's issue-event response shape, including REST aliases from the [Retrieve an Issue Event API](https://docs.sentry.io/api/events/retrieve-an-issue-event/).
`GetAlertingEventDetail` accepts Sentry's issue-event response shape, including REST aliases from the [Retrieve an Issue Event API](https://docs.sentry.io/api/events/retrieve-an-issue-event/). It fetches issue metadata best-effort and includes `Sentry issue: <permalink>` near the top when Sentry returns a permalink; if that metadata request fails, the event details still render.

## cascade-tools CLI

Expand Down
24 changes: 15 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
"brace-expansion": "^2.0.3",
"axios": "^1.15.0"
"axios": "^1.15.0",
"protobufjs": "^7.5.8"
}
}
2 changes: 1 addition & 1 deletion src/agents/definitions/alerting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ strategies: {}

prompts:
taskPrompt: |
An alert has been triggered: "<%= it.alertTitle %>"
An alert has been triggered: "<%= it.alertTitle || it.workItemTitle || it.alertIssueId || 'Sentry alert' %>"

<% if (it.alertIssueUrl) { %>Issue: <%= it.alertIssueUrl %><% } %>

Expand Down
42 changes: 30 additions & 12 deletions src/agents/definitions/contextSteps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ import { getSentryClient } from '../../sentry/client.js';
import type { AgentInput, ProjectConfig } from '../../types/index.js';
import { parseRepoFullName } from '../../utils/repo.js';
import type { ContextInjection, LogWriter } from '../contracts/index.js';
import { sourceLocalPRDiffs } from '../shared/prDiffSource.js';
import {
countSkipsByReason,
extractPRDiffs,
formatPRComments,
formatPRDetails,
formatPRDiff,
formatPRDiffContext,
formatPRIssueComments,
formatPRReviews,
Expand Down Expand Up @@ -156,7 +156,6 @@ export async function fetchPRContextStep(params: FetchContextParams): Promise<Co
const checkStatus = await githubClient.getCheckSuiteStatus(owner, repo, prDetails.headSha);

const prDetailsFormatted = formatPRDetails(prDetails);
const diffFormatted = formatPRDiff(prDiff);
const checkStatusFormatted = formatCheckStatus(prNumber, checkStatus);

injections.push({
Expand All @@ -166,13 +165,6 @@ export async function fetchPRContextStep(params: FetchContextParams): Promise<Co
description: 'Pre-fetched PR details',
});

injections.push({
toolName: 'GetPRDiff',
params: { comment: 'Pre-fetching PR diff for code review', owner, repo, prNumber },
result: diffFormatted,
description: 'Pre-fetched PR diff',
});

injections.push({
toolName: 'GetPRChecks',
params: { comment: 'Pre-fetching CI check status for review', owner, repo, prNumber },
Expand All @@ -186,12 +178,30 @@ export async function fetchPRContextStep(params: FetchContextParams): Promise<Co
// Compact per-file diffs (scales with PR size, not repo size). Files that
// don't fit the budget or can't be diffed are surfaced in a separate
// SKIPPED FILES injection so the agent can decide whether to fetch them.
const diffContext = extractPRDiffs(prDiff);
// Use prDetails.baseRef (the PR's actual target branch) rather than the
// project base branch so stacked PRs targeting a feature branch don't
// include parent-branch commits in the diff context.
const baseBranch = prDetails.baseRef;
const localDiffSource = await sourceLocalPRDiffs({
files: prDiff,
repoDir: params.repoDir,
baseBranch,
logWriter: params.logWriter,
});
const diffContext = extractPRDiffs(localDiffSource.files);
const skipReasons = countSkipsByReason(diffContext.skipped);
const patchSources = localDiffSource.files.reduce<Record<string, number>>((acc, file) => {
acc[file.patchSource] = (acc[file.patchSource] ?? 0) + 1;
return acc;
}, {});
params.logWriter('INFO', 'PR context prepared', {
included: diffContext.included.length,
skipped: diffContext.skipped.length,
skipReasons,
patchSources,
totalDiffTokens: diffContext.totalDiffTokens,
perFileTokenCap: diffContext.perFileTokenCap,
localGitMismatches: localDiffSource.mismatches.slice(0, 20),
});

injections.push({
Expand Down Expand Up @@ -564,7 +574,7 @@ export async function fetchPipelineSnapshotStep(
export async function fetchAlertingIssueStep(
params: FetchContextParams,
): Promise<ContextInjection[]> {
const { alertIssueId, alertOrgId } = params.input;
const { alertIssueId, alertOrgId, alertIssueUrl, alertTitle } = params.input;
if (!alertIssueId || typeof alertIssueId !== 'string') return [];
if (!alertOrgId || typeof alertOrgId !== 'string') return [];

Expand All @@ -576,7 +586,15 @@ export async function fetchAlertingIssueStep(

const client = getSentryClient();
const event = await client.getIssueEvent(alertOrgId, alertIssueId, 'latest');
const result = formatSentryEvent(event);
const issue =
typeof alertIssueUrl === 'string' && alertIssueUrl.trim()
? {
id: alertIssueId,
permalink: alertIssueUrl,
title: typeof alertTitle === 'string' ? alertTitle : undefined,
}
: await client.getIssue(alertOrgId, alertIssueId).catch(() => undefined);
const result = formatSentryEvent(event, issue);

params.logWriter('INFO', 'fetchAlertingIssueStep: fetched alerting event successfully', {
issueId: alertIssueId,
Expand Down
13 changes: 9 additions & 4 deletions src/agents/definitions/review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,17 @@ prompts:
diff hunk. Only the changed lines are in your context; unchanged parts of
a file are not echoed. Use `Read <path>` if you need surrounding code.

GitHub is used to enumerate changed files, but compact patch bodies are
produced from the checked-out local PR workspace so the context is not
limited by GitHub's `pulls.listFiles().patch` truncation behavior.

If a **`SKIPPED FILES`** injection is present, those files were omitted from
the compact context (because they were deleted, binary, their patch was too
large, or the cumulative diff budget was reached). When any skipped file is
relevant to your review, fetch it on demand:
the compact context (because they were deleted, binary, could not be locally
diffed, their patch was too large, or the cumulative diff budget was
reached). When any skipped file is relevant to your review, fetch it on
demand:

- `gh pr diff <%= it.prNumber %> -- <path>` to read the file's patch
- `cascade-tools scm get-pr-diff --prNumber <%= it.prNumber %> --path <path>` to read the file's patch
- `Read <path>` to read the post-PR file content
- `Grep <pattern> <path>` to search inside the file

Expand Down
29 changes: 26 additions & 3 deletions src/agents/prompts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,16 @@ export interface TaskPromptInput {
* Null handling: all optional fields remain undefined when not present (no 'unknown' defaults).
*/
export function buildTaskPromptContext(input: TaskPromptInput): TaskPromptContext {
const context: TaskPromptContext = {};
for (const [key, value] of Object.entries(input)) {
if (key === 'project' || key === 'config') continue;
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
context[key] = value;
}
}

return {
workItemId: input.workItemId,
prNumber: input.prNumber,
prBranch: input.prBranch,
...context,
commentText: input.triggerCommentBody ?? input.triggerCommentText,
commentAuthor: input.triggerCommentAuthor,
commentBody: input.triggerCommentBody ?? input.triggerCommentText,
Expand Down Expand Up @@ -386,6 +392,23 @@ export function getTaskTemplateVariables(): Array<{
{ name: 'workItemId', group: 'Work Item', description: 'Work item ID (card or issue)' },
{ name: 'commentText', group: 'Comment', description: 'Comment text content (PM comments)' },
{ name: 'commentAuthor', group: 'Comment', description: 'Comment author username' },
{
name: 'alertTitle',
group: 'Alerting',
description: 'Alert title from the alerting provider',
},
{
name: 'alertIssueUrl',
group: 'Alerting',
description: 'Human-facing URL for the alerting issue or alert',
},
{ name: 'alertIssueId', group: 'Alerting', description: 'Alerting provider issue ID' },
{ name: 'alertOrgId', group: 'Alerting', description: 'Alerting provider organization ID' },
{
name: 'alertMetricKey',
group: 'Alerting',
description: 'Stable metric alert key used for metric alert materialization',
},
{ name: 'prNumber', group: 'PR', description: 'Pull request number' },
{ name: 'prBranch', group: 'PR', description: 'Pull request branch name' },
{ name: 'commentBody', group: 'PR Comment', description: 'PR comment body text' },
Expand Down
2 changes: 1 addition & 1 deletion src/agents/prompts/templates/resolve-conflicts.eta
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ You are an expert software engineer resolving merge conflicts on a pull request.

2. **Review PR details** - Pre-loaded for you:
- PR details (GetPRDetails) — includes base branch and head branch
- PR diff (GetPRDiff)
- PR diff context (GetPRDiffContext)
3. **Identify conflicting files** - Run `git status` to see which files have conflict markers
4. **Read codebase guidelines** (CLAUDE.md, README.md, etc.) for conventions

Expand Down
2 changes: 1 addition & 1 deletion src/agents/prompts/templates/respond-to-ci.eta
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ You are an expert software engineer fixing CI check failures on a pull request.

2. **Review CI failures** - Pre-loaded for you:
- PR details (GetPRDetails)
- PR diff (GetPRDiff)
- PR diff context (GetPRDiffContext)
- Check run status summary (GetPRChecks)
3. **Get failed check logs** - Use the `GetCIRunLogs` gadget with the PR's head SHA to fetch failed CI run logs
4. **Read codebase guidelines** (CLAUDE.md, README.md, etc.) for conventions
Expand Down
2 changes: 1 addition & 1 deletion src/agents/prompts/templates/respond-to-pr-comment.eta
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A user @mentioned you in a PR comment. Read their request and do exactly what th
- Line-specific review comments (GetPRComments)
- Review submissions (GetPRReviews)
- General PR comments (GetPRIssueComments)
- PR diff (GetPRDiff)
- PR diff context (GetPRDiffContext)
4. **Read codebase guidelines** (CLAUDE.md, README.md, etc.) to understand conventions

### Phase 3: Plan & Execute
Expand Down
2 changes: 1 addition & 1 deletion src/agents/prompts/templates/review.eta
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ Use CreatePRReview with:
- `body`: Summary of findings (or "LGTM" if no issues)
- `comments`: Inline comments for specific issues

**CRITICAL**: Inline comments can ONLY be placed on files in the PR diff. If you comment on a file path not in the diff, or use line numbers outside the changed ranges, the entire review will fail. Only add inline comments for files you saw in GetPRDiff output.
**CRITICAL**: Inline comments can ONLY be placed on files in the PR diff. If you comment on a file path not in the diff, or use line numbers outside the changed ranges, the entire review will fail. Only add inline comments for files you saw in GetPRDiffContext output or files you verified by fetching a skipped file on demand via `cascade-tools scm get-pr-diff --path`.

### Review Body Format

Expand Down
Loading
Loading