Skip to content
Open
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
1 change: 0 additions & 1 deletion plugins/sentry-cli/skills/sentry-cli/references/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ sentry issue plan 123456789 --cause 0
Generate a solution plan using Seer AI

**Flags:**
- `--cause <value> - Root cause ID to plan (required if multiple causes exist)`
- `--force - Force new plan even if one exists`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`

Expand Down
139 changes: 22 additions & 117 deletions src/commands/issue/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import type { SentryContext } from "../../context.js";
import { triggerSolutionPlanning } from "../../lib/api-client.js";
import { buildCommand, numberParser } from "../../lib/command.js";
import { ApiError, ValidationError } from "../../lib/errors.js";
import { buildCommand } from "../../lib/command.js";
import { ApiError } from "../../lib/errors.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import {
formatSolution,
Expand All @@ -26,7 +26,6 @@ import {
extractNoSolutionReason,
extractRootCauses,
extractSolution,
type RootCause,
type SolutionArtifact,
} from "../../types/seer.js";
import {
Expand All @@ -37,78 +36,12 @@ import {
} from "./utils.js";

type PlanFlags = {
readonly cause?: number;
readonly json: boolean;
readonly force: boolean;
readonly fresh: boolean;
readonly fields?: string[];
};

/**
* Validate that the autofix state has root causes identified.
*
* @param state - Current autofix state (already ensured to exist)
* @returns Array of root causes
* @throws {ValidationError} If no root causes found
*/
function validateRootCauses(state: AutofixState): RootCause[] {
const causes = extractRootCauses(state);
if (causes.length === 0) {
throw new ValidationError(
"No root causes identified. Cannot create a plan without a root cause."
);
}
return causes;
}

/**
* Validate and resolve the cause selection for solution planning.
*
* @param causes - Array of available root causes
* @param selectedCause - User-specified cause index, or undefined for auto-select
* @param issueId - Issue ID for error message hints
* @returns Validated cause index (0-based)
* @throws {ValidationError} If multiple causes exist without selection, or if selection is out of range
*/
function validateCauseSelection(
causes: RootCause[],
selectedCause: number | undefined,
issueId: string
): number {
// If only one cause and none specified, use it
if (causes.length === 1 && selectedCause === undefined) {
return 0;
}

// If multiple causes and none specified, error with list
if (causes.length > 1 && selectedCause === undefined) {
const lines = [
"Multiple root causes found. Please specify one with --cause <id>:",
"",
];
for (let i = 0; i < causes.length; i++) {
const cause = causes[i];
if (cause) {
lines.push(` ${i}: ${cause.description.slice(0, 60)}...`);
}
}
lines.push("");
lines.push(`Example: sentry issue plan ${issueId} --cause 0`);
throw new ValidationError(lines.join("\n"));
}

const causeId = selectedCause ?? 0;

// Validate the cause ID is in range
if (causeId < 0 || causeId >= causes.length) {
throw new ValidationError(
`Invalid cause ID: ${causeId}. Valid range is 0-${causes.length - 1}.`
);
}

return causeId;
}

/** Context about why no solution was produced */
type NoSolutionContext = {
/** Seer's reason for not producing a solution (from the artifact) */
Expand Down Expand Up @@ -172,11 +105,10 @@ function formatPlanOutput(data: PlanData): string {
* Returns undefined when there's nothing useful to report.
*/
function buildNoSolutionContext(
state: AutofixState,
selectedCause?: RootCause
state: AutofixState
): NoSolutionContext | undefined {
const reason = extractNoSolutionReason(state);
const cause = selectedCause ?? extractRootCauses(state)[0];
const cause = extractRootCauses(state)[0];
const files = cause ? extractExaminedFiles([cause]) : [];

if (!(reason || cause?.description) && files.length === 0) {
Expand Down Expand Up @@ -206,10 +138,7 @@ function buildNoSolutionContext(
* API, root cause description, and files examined) so the user isn't left
* with a bare "no solution found" message.
*/
function buildPlanData(
state: AutofixState,
selectedCause?: RootCause
): PlanData {
function buildPlanData(state: AutofixState): PlanData {
const solution = extractSolution(state);
const data: PlanData = {
run_id: state.run_id,
Expand All @@ -218,7 +147,7 @@ function buildPlanData(
};

if (!solution) {
data.no_solution_context = buildNoSolutionContext(state, selectedCause);
data.no_solution_context = buildNoSolutionContext(state);
}

return data;
Expand All @@ -231,7 +160,6 @@ export const planCommand = buildCommand({
"Generate a solution plan for a Sentry issue using Seer AI.\n\n" +
"This command automatically runs root cause analysis if needed, then " +
"generates a solution plan with specific implementation steps to fix the issue.\n\n" +
"If multiple root causes are identified, use --cause to specify which one.\n" +
"Use --force to regenerate a plan even if one already exists.\n\n" +
"Issue formats:\n" +
" @latest - Most recent unresolved issue\n" +
Expand All @@ -246,10 +174,10 @@ export const planCommand = buildCommand({
" - GitHub integration configured for your organization\n" +
" - Code mappings set up for your project\n\n" +
"Examples:\n" +
" sentry issue plan @latest --cause 0\n" +
" sentry issue plan 123456789 --cause 0\n" +
" sentry issue plan sentry/EXTENSION-7 --cause 1\n" +
" sentry issue plan cli-G --cause 0\n" +
" sentry issue plan @latest\n" +
" sentry issue plan 123456789\n" +
" sentry issue plan sentry/EXTENSION-7\n" +
" sentry issue plan cli-G\n" +
" sentry issue plan 123456789 --force",
},
output: {
Expand All @@ -258,12 +186,6 @@ export const planCommand = buildCommand({
parameters: {
positional: issueIdPositional,
flags: {
cause: {
kind: "parsed",
parse: numberParser,
brief: "Root cause ID to plan (required if multiple causes exist)",
optional: true,
},
force: {
kind: "boolean",
brief: "Force new plan even if one exists",
Expand All @@ -277,11 +199,9 @@ export const planCommand = buildCommand({
applyFreshFlag(flags);
const { cwd } = this;

// Declare org outside try block so it's accessible in catch for error messages
let resolvedOrg: string | undefined;

try {
// Resolve org and issue ID
const { org, issueId: numericId } = await resolveOrgAndIssueId({
issueArg,
cwd,
Expand All @@ -296,41 +216,28 @@ export const planCommand = buildCommand({
json: flags.json,
});

// Validate we have root causes
const causes = validateRootCauses(state);

// Validate cause selection (always returns a valid index into causes)
const causeIndex = validateCauseSelection(causes, flags.cause, issueArg);
const selectedCause = causes.at(causeIndex);
if (!selectedCause) {
throw new ValidationError(
`Invalid cause index: ${causeIndex}. Valid range is 0-${causes.length - 1}.`
);
}

// Check if solution already exists (skip if --force)
if (!flags.force) {
const existingSolution = extractSolution(state);
if (existingSolution) {
return yield new CommandOutput(buildPlanData(state, selectedCause));
return yield new CommandOutput(buildPlanData(state));
}
}

// No solution exists, trigger planning
if (!flags.json) {
// Trigger solution planning
const causes = extractRootCauses(state);
if (!flags.json && causes.length > 0) {
const log = logger.withTag("issue.plan");
log.info(`Creating plan for cause #${causeIndex}...`);
log.info(`"${selectedCause.description}"`);
const cause = causes[0];
if (cause) {
log.info("Creating plan...");
log.info(`"${cause.description}"`);
}
}

await triggerSolutionPlanning(
org,
numericId,
state.run_id,
selectedCause.id
);
await triggerSolutionPlanning(org, numericId, state.run_id);

// Poll until solution is ready (NEED_MORE_INFORMATION) or terminal
// Poll until solution is ready or terminal
const finalState = await pollAutofixState({
orgSlug: org,
issueId: numericId,
Expand All @@ -343,7 +250,6 @@ export const planCommand = buildCommand({
` Or retry: sentry issue plan ${issueArg}`,
});

// Handle errors
if (finalState.status === "ERROR") {
throw new Error(
"Plan creation failed. Check the Sentry web UI for details."
Expand All @@ -354,9 +260,8 @@ export const planCommand = buildCommand({
throw new Error("Plan creation was cancelled.");
}

return yield new CommandOutput(buildPlanData(finalState, selectedCause));
return yield new CommandOutput(buildPlanData(finalState));
} catch (error) {
// Handle API errors with friendly messages
if (error instanceof ApiError) {
throw handleSeerApiError(error.status, error.detail, resolvedOrg);
}
Expand Down
Loading
Loading