Skip to content
Draft
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
50 changes: 50 additions & 0 deletions .github/workflows/cli-seer-platform.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Seer CLI platforms

on:
pull_request:
paths:
- "packages/cli/**"
- "packages/toolkit-core/**"
- "packages/mcp-core/**"
- "packages/mcp-server/**"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "package.json"
- ".github/workflows/cli-seer-platform.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
seer-fix:
name: Seer fix (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
env:
TZ: UTC
SENTRY_CLIENT_ID: test-client-id
SENTRY_CLI_NO_TELEMETRY: "1"
SENTRY_CLI_NO_UPDATE_CHECK: "1"
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "24"
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build CLI bundle
run: pnpm --filter sentry run bundle
- name: Test source Seer flow and patch safety
run: pnpm --filter sentry exec vitest run seer-fix seer-patch sentry-client.request-policy --maxWorkers=2
- name: Test bundled CLI Seer flow
env:
SENTRY_CLI_BINARY: ${{ github.workspace }}/packages/cli/dist/bin.cjs
run: pnpm --filter sentry exec vitest run test/e2e/seer-fix.test.ts --maxWorkers=1
2 changes: 1 addition & 1 deletion apps/cli-docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ toolkit/
│ │ │ ├── dsn/ # list
│ │ │ ├── event/ # list, send, view
│ │ │ ├── feedback/ # list, resolve, spam, unresolve, view
│ │ │ ├── issue/ # archive, events, explain, link, list, merge, plan, resolve, unlink, unresolve, view
│ │ │ ├── issue/ # archive, events, explain, fix, link, list, merge, plan, resolve, unlink, unresolve, view
│ │ │ ├── local/ # run, serve
│ │ │ ├── log/ # list, view
│ │ │ ├── monitor/ # list, run
Expand Down
67 changes: 67 additions & 0 deletions apps/cli-docs/src/fragments/commands/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,3 +418,70 @@ is also accepted. The organization's “Let Members Delete Events” setting doe
not restrict unlinking on updated Sentry versions.

Granting a token more scopes does not override project-access policy.

### Generate and review a fix

Use Seer to generate code changes and preview their diffs:

```bash
sentry issue fix my-org/FRONT-123
sentry issue fix @latest --instruction "Keep the existing API compatible"
```

The chat asks Seer to investigate the issue and generate code changes for
review. In a terminal, the CLI shows new messages,
asks Seer's questions, and lets you approve or reject each proposed file
change. Approval lets Seer continue generating the fix. Completed fixes preview
all generated diffs; local files are not modified and no pull request is created.

The organization needs Seer coding access, linked repositories, and code
mappings. Interactive updates require a signed-in Sentry user. Use
`sentry auth login` if a token has no associated user identity.

Each invocation starts a new interactive Seer chat. Use `--run-id` to resume a
specific chat. The issue argument resolves the organization and provides context
for new chats; the explicit run ID selects the chat when resuming.
`--instruction` supplies context for a new run and cannot be combined with
`--run-id`. `--fresh` also refreshes issue resolution; run polling always fetches
fresh state.

### Resume a fix from JSON output

JSON output and sessions without a terminal never prompt. A paused result
contains `run_id`, `status`, `pending_user_input`, `patches`, `pr_urls`, and a
`next_action` hint. Supply the returned run ID to continue that exact run:

```bash
sentry issue fix my-org/FRONT-123 --json
sentry issue fix my-org/FRONT-123 --run-id <run-id> --json
sentry issue fix my-org/FRONT-123 --run-id <run-id> --response answer.json --json
```

For `ask_user_question`, `answer.json` contains one answer for each question:

```json
{"input_id":"<pending-input-id>","response_data":{"answers":["Keep the current behavior"]}}
```

For `file_change_approval`, include one boolean for each proposed patch:

```json
{"input_id":"<pending-input-id>","response_data":{"decisions":[true,false]}}
```

The CLI validates the input ID, response type, and answer count against the
current pending input before submitting. `--response` requires `--run-id`.
Seer questions and file change approvals work in the CLI. Browser-session
permission elevation (`agent_write_approval`), provider reconnection, and unknown
input types pause with guidance to continue in Sentry. Permission elevation
requires the browser approval flow before the run can continue. The handoff
hint links directly to the selected chat, preserving its run ID. The CLI never
answers these requests automatically.

The default automated wait is six minutes. Change it with `--timeout <seconds>`.
A terminal prompt stays open until you respond or cancel; an answer entered
after the deadline is not submitted. A timeout
returns the run ID and resume command because Seer may still be working. Runs
that fail, cancel, or time out return a nonzero exit status with their JSON result.
A resumed completed run without patches explains that no code changes were
produced and offers a new full fix.
1 change: 1 addition & 0 deletions packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ Manage Sentry issues
- `sentry issue events <issue>` — List events for a specific issue
- `sentry issue explain <issue...>` — Analyze one or more issues using Seer AI
- `sentry issue plan <issue>` — Generate a solution plan using Seer AI
- `sentry issue fix <issue>` — Generate and review code changes using Seer AI
- `sentry issue view <issue...>` — View details of one or more issues
- `sentry issue resolve <issue>` — Mark an issue as resolved
- `sentry issue unresolve <issue>` — Reopen a resolved issue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,28 @@ sentry issue plan 123456789
sentry issue plan 123456789 --force
```

### `sentry issue fix <issue>`

Generate and review code changes using Seer AI

**Flags:**
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `--instruction <value> - Context for a new fix`
- `--run-id <value> - Resume this Seer run`
- `--response <value> - JSON file with input_id and response_data (requires --run-id)`
- `--timeout <value> - Maximum wait in seconds - (default: "360")`

**Examples:**

```bash
sentry issue fix my-org/FRONT-123
sentry issue fix @latest --instruction "Keep the existing API compatible"

sentry issue fix my-org/FRONT-123 --json
sentry issue fix my-org/FRONT-123 --run-id <run-id> --json
sentry issue fix my-org/FRONT-123 --run-id <run-id> --response answer.json --json
```

### `sentry issue view <issue...>`

View details of one or more issues
Expand Down
158 changes: 158 additions & 0 deletions packages/cli/src/commands/issue/fix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/** Generate and review code changes with Seer; resume structured input safely. */
import { readFile } from "node:fs/promises";
import { isatty } from "node:tty";
import { safeParse } from "valibot";
import type { SentryContext } from "../../context.js";
import {
UserInputResponseSchema,
type UserInputResponse,
} from "../../types/seer-fix.js";
import { buildCommand } from "../../lib/command.js";
import { ApiError, OutputError, ValidationError } from "../../lib/errors.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { handleSeerApiError } from "../../lib/formatters/seer.js";
import {
createSeerBlockRenderer,
formatSeerFix,
promptSeerInput,
} from "../../lib/formatters/seer-fix.js";
import {
applyFreshFlag,
FRESH_ALIASES,
FRESH_FLAG,
} from "../../lib/list-command.js";
import { runSeerFix } from "../../lib/seer-fix.js";
import { issueIdPositional, resolveOrgAndIssueId } from "./utils.js";

type FixFlags = {
readonly json: boolean;
readonly fresh: boolean;
readonly instruction?: string;
readonly "run-id"?: string;
readonly response?: string;
readonly timeout: number;
readonly fields?: string[];
};

async function readResponse(
path: string | undefined,
): Promise<UserInputResponse | undefined> {
if (path === undefined) {
return;
}
let value: unknown;
try {
value = JSON.parse(await readFile(path, "utf8"));
} catch {
throw new ValidationError(
"Unable to read --response as a JSON file.",
"--response",
);
}
const parsed = safeParse(UserInputResponseSchema, value);
if (!parsed.success) {
throw new ValidationError(
"--response must contain input_id and response_data with answers or decisions.",
"--response",
);
}
return parsed.output;
}

function parseTimeout(value: string): number {
const seconds = Number(value);
if (!(Number.isFinite(seconds) && seconds > 0 && seconds <= 86_400)) {
throw new ValidationError(
"--timeout must be a positive number of seconds, up to 86400.",
"--timeout",
);
}
return seconds;
}

export const fixCommand = buildCommand({
docs: {
brief: "Generate and review code changes using Seer AI",
fullDescription:
"Generate code changes for an issue using Seer AI. Review questions and proposed file changes in an interactive terminal.\n\nJSON and non-interactive sessions pause with a run ID and pending input. Resume using --run-id and --response with a JSON file containing input_id and response_data.\n\nCompleted fixes preview diffs. No files are changed locally. Requires Seer coding access, linked repositories, and a signed-in Sentry user.\n\nEach invocation starts a new interactive chat. Use --run-id to resume a specific chat; the issue argument resolves its organization. --instruction supplies context for a new fix.\n\nExamples:\n sentry issue fix @latest\n sentry issue fix my-org/CLI-G --instruction 'Preserve compatibility'\n sentry issue fix my-org/CLI-G --json\n sentry issue fix my-org/CLI-G --run-id <run> --response answer.json --json",
},
output: { human: formatSeerFix },
parameters: {
positional: issueIdPositional,
flags: {
fresh: FRESH_FLAG,
instruction: {
kind: "parsed",
parse: String,
brief: "Context for a new fix",
optional: true,
},
"run-id": {
kind: "parsed",
parse: String,
brief: "Resume this Seer run",
optional: true,
},
response: {
kind: "parsed",
parse: String,
brief: "JSON file with input_id and response_data (requires --run-id)",
optional: true,
},
timeout: {
kind: "parsed",
parse: parseTimeout,
brief: "Maximum wait in seconds",
default: "360",
},
},
aliases: FRESH_ALIASES,
},
async *func(this: SentryContext, flags: FixFlags, issueArg: string) {
applyFreshFlag(flags);
const runId = flags["run-id"];
if (flags.response && !runId) {
throw new ValidationError("--response requires --run-id.", "--response");
}
const response = await readResponse(flags.response);
const interactive = !flags.json && isatty(0) && isatty(2);
let org: string | undefined;
const abort = new AbortController();
const onInterrupt = () => abort.abort();
process.once("SIGINT", onInterrupt);
try {
const resolved = await resolveOrgAndIssueId({
issueArg,
cwd: this.cwd,
command: "fix",
});
org = resolved.org;
const result = await runSeerFix({
org,
issueId: resolved.issueId,
issueArg,
runId,
response,
instruction: flags.instruction,
timeoutMs: flags.timeout * 1000,
signal: abort.signal,
interactive,
prompt: (input) => promptSeerInput(input, this.stderr),
onBlocks: interactive
? createSeerBlockRenderer(this.stderr)
: undefined,
});
if (["ERROR", "CANCELLED", "TIMEOUT"].includes(result.status)) {
throw new OutputError(result);
}
yield new CommandOutput(result);
} catch (error) {
if (error instanceof ApiError) {
throw handleSeerApiError(error.status, error.detail, org);
}
throw error;
} finally {
process.removeListener("SIGINT", onInterrupt);
}
},
});
5 changes: 4 additions & 1 deletion packages/cli/src/commands/issue/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { buildRouteMap } from "../../lib/route-map.js";
import { archiveCommand } from "./archive.js";
import { eventsCommand } from "./events.js";
import { fixCommand } from "./fix.js";
import { explainCommand } from "./explain.js";
import { linkCommand } from "./link.js";
import { listCommand } from "./list.js";
Expand All @@ -17,6 +18,7 @@ export const issueRoute = buildRouteMap({
events: eventsCommand,
explain: explainCommand,
plan: planCommand,
fix: fixCommand,
view: viewCommand,
resolve: resolveCommand,
unresolve: unresolveCommand,
Expand All @@ -38,13 +40,14 @@ export const issueRoute = buildRouteMap({
" view View details of one or more issues\n" +
" explain Analyze one or more issues using Seer AI\n" +
" plan Generate a solution plan using Seer AI\n" +
" fix Generate and review code changes using Seer AI\n" +
" resolve Mark an issue as resolved (optionally in a release)\n" +
" unresolve Reopen a resolved issue (alias: reopen)\n" +
" archive Archive/ignore an issue (alias: ignore)\n" +
" merge Merge 2+ issues into a single group\n" +
" link Link an existing external issue\n" +
" unlink Remove an external issue link\n\n" +
"Magic selectors (available for view, events, explain, plan, resolve, unresolve, archive):\n" +
"Magic selectors (available for view, events, explain, plan, fix, resolve, unresolve, archive):\n" +
" @latest Most recent unresolved issue\n" +
" @most_frequent Issue with the highest event frequency\n\n" +
"Examples:\n" +
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ export {
} from "./api/repositories.js";
export {
getAutofixState,
getSeerExplorerState,
triggerIssueFix,
respondToSeerUserInput,
triggerRootCauseAnalysis,
triggerSolutionPlanning,
} from "./api/seer.js";
Expand Down
Loading
Loading