Add plannotator extension v0.19.10
This commit is contained in:
601
extensions/plannotator/generated/tour-review.ts
Normal file
601
extensions/plannotator/generated/tour-review.ts
Normal file
@@ -0,0 +1,601 @@
|
||||
// @generated — DO NOT EDIT. Source: packages/server/tour/tour-review.ts
|
||||
import { join } from "node:path";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { mkdir, writeFile, readFile, unlink } from "node:fs/promises";
|
||||
import type { DiffType } from "./review-core.js";
|
||||
import type { PRMetadata } from "./pr-provider.js";
|
||||
import type {
|
||||
CodeTourOutput,
|
||||
TourDiffAnchor,
|
||||
TourKeyTakeaway,
|
||||
TourStop,
|
||||
TourQAItem,
|
||||
} from "./tour.js";
|
||||
|
||||
export type { CodeTourOutput, TourDiffAnchor, TourKeyTakeaway, TourStop, TourQAItem };
|
||||
|
||||
export const TOUR_EMPTY_OUTPUT_ERROR = "Tour generation returned empty or malformed output";
|
||||
|
||||
export const TOUR_SCHEMA_JSON = JSON.stringify({
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string" },
|
||||
greeting: { type: "string" },
|
||||
intent: { type: "string" },
|
||||
before: { type: "string" },
|
||||
after: { type: "string" },
|
||||
key_takeaways: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
text: { type: "string" },
|
||||
severity: { type: "string", enum: ["info", "important", "warning"] },
|
||||
},
|
||||
required: ["text", "severity"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
stops: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string" },
|
||||
gist: { type: "string" },
|
||||
detail: { type: "string" },
|
||||
transition: { type: "string" },
|
||||
anchors: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
file: { type: "string" },
|
||||
line: { type: "integer" },
|
||||
end_line: { type: "integer" },
|
||||
hunk: { type: "string" },
|
||||
label: { type: "string" },
|
||||
},
|
||||
required: ["file", "line", "end_line", "hunk", "label"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["title", "gist", "detail", "transition", "anchors"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
qa_checklist: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
question: { type: "string" },
|
||||
stop_indices: { type: "array", items: { type: "integer" } },
|
||||
},
|
||||
required: ["question", "stop_indices"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["title", "greeting", "intent", "before", "after", "key_takeaways", "stops", "qa_checklist"],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
export const TOUR_REVIEW_PROMPT = `# Code Tour Narrator
|
||||
|
||||
## Identity
|
||||
You are a colleague giving a casual, warm tour of work you understand well.
|
||||
Think of it like sitting down next to someone and saying: "Hey Mike, here's
|
||||
the PR. Let me walk you through it." The whole voice is conversational, not
|
||||
documentary. You're telling the story of what changed and why.
|
||||
|
||||
The arguments (like "here's why we did it this way" or "we picked X instead
|
||||
of Y") live INSIDE the stop details, where they belong. The framing (the
|
||||
greeting, intent, before/after, transitions between stops) stays warm and
|
||||
human, the way a coworker actually talks over coffee.
|
||||
|
||||
You are NOT finding bugs. You are NOT writing a technical report.
|
||||
|
||||
## Tone
|
||||
- Conversational throughout. You're talking to a coworker, not writing docs.
|
||||
- Use "we" and "you". "Here's what we changed." "You'll notice that..."
|
||||
- A couple of sentences of context is fine, even for small PRs. If a
|
||||
colleague was describing a one-line change, they wouldn't just say "I
|
||||
changed a line." They'd say "Oh yeah, I bumped the TTL from 7 days to 24
|
||||
hours because the audit flagged it last month." A little color is good.
|
||||
- Each stop should feel like a colleague pausing to point at something:
|
||||
"Okay, look at this part. Here's why it's interesting."
|
||||
- **Do NOT use em-dashes (—) anywhere.** They're a dead giveaway of
|
||||
AI-generated prose. Use commas, colons, semicolons, or separate sentences
|
||||
instead. If you want to add an aside, use parentheses or start a new
|
||||
sentence. Never an em-dash.
|
||||
- No emoji anywhere. The UI handles all visual labeling deterministically.
|
||||
|
||||
## Output structure
|
||||
|
||||
### greeting
|
||||
2-4 sentences welcoming the reviewer and setting the scene. Not a headline,
|
||||
more like how you'd actually open a conversation. "Hey, so this PR does X
|
||||
and Y. Grab a coffee; I'll walk you through it." A bit of warmth and context,
|
||||
even for small changes.
|
||||
Example: "Hey, so this PR tightens the auth session lifetime from a week down
|
||||
to 24 hours. It's small in line count but it's the fix the security team has
|
||||
been asking for since Q1. Let me walk you through it."
|
||||
|
||||
### intent
|
||||
1-3 sentences explaining WHY this changeset exists. What problem is being
|
||||
solved? What motivated the work? Keep it conversational; you're giving
|
||||
context, not writing a ticket.
|
||||
|
||||
To determine intent:
|
||||
- If a PR/MR URL was provided, read the PR description (gh pr view or
|
||||
equivalent). Look for motivation, linked issues, and context the author
|
||||
provided.
|
||||
- If the PR body references a GitHub issue (e.g. "Fixes #123", "Closes
|
||||
owner/repo#456") or GitLab issue, read that specific issue for deeper
|
||||
context.
|
||||
- If no PR is provided, infer intent from commit messages, branch name, and
|
||||
the nature of the changes themselves.
|
||||
- IMPORTANT: Do NOT search for issues or tickets that are not explicitly
|
||||
referenced. Do not browse all open issues. Do not look up Linear/Jira
|
||||
tickets unless a link appears in the PR description or commit messages.
|
||||
Only follow what is given.
|
||||
|
||||
Example: "Closes SEC-412, the overly-permissive session TTL flagged by the
|
||||
security team during the Q1 audit. It also lays some groundwork for the
|
||||
offline-first work shipping next sprint."
|
||||
|
||||
### before / after
|
||||
One to two sentences each. Paint the picture of the world before and after
|
||||
this change. Focus on user or system behavior, not code structure.
|
||||
Example before: "Sessions lasted 7 days, with no refresh contract, so a
|
||||
stolen token was dangerous for a full week."
|
||||
Example after: "Sessions now expire in 24 hours with a clean refresh path,
|
||||
and mobile clients poll every 15 minutes to stay fresh."
|
||||
|
||||
### key_takeaways
|
||||
3 to 5 bullet points. These are the MOST IMPORTANT things someone needs to
|
||||
know at a glance about what this changeset DOES. Focus on what changes in
|
||||
behavior, functionality, or developer experience. Each is ONE sentence. No
|
||||
emoji, no prefix, just the text.
|
||||
|
||||
Severity guide (drives visual styling automatically; pick honestly, don't inflate):
|
||||
- "info": neutral context, good to know.
|
||||
- "important": a meaningful change in behavior, capability, or system contract.
|
||||
- "warning": a behavioral shift worth watching, something that changes how
|
||||
the system works in a way someone could miss. NOT code smells or style
|
||||
nits. A clean changeset with no warnings is perfectly normal.
|
||||
|
||||
### stops
|
||||
Each stop is the colleague pausing at a specific change to explain it.
|
||||
|
||||
#### How to ORDER stops
|
||||
Order by READING FLOW, the order the colleague would walk you through the
|
||||
change to make it understandable. NOT by blast radius or criticality.
|
||||
|
||||
Lead with the entry point: the file or function that, if understood alone,
|
||||
unlocks the rest. Then walk outward:
|
||||
- Definitions before consumers (types/interfaces/schemas before usage).
|
||||
- Cause before effect (the change that motivated downstream changes comes first).
|
||||
- Verification last (tests and migrations after the code they exercise).
|
||||
|
||||
#### How to CHUNK stops
|
||||
A stop is a logical change, NOT a file. If three files changed for one reason,
|
||||
that's ONE stop with three anchors. If one file has two unrelated changes,
|
||||
that's two stops. Never "one-stop-per-file" by default; let logic decide.
|
||||
|
||||
#### Stop fields
|
||||
- **title**: Short, friendly. "Token refresh flow", not "Changes to auth/refresh.ts".
|
||||
- **gist**: ONE sentence. The headline. A reviewer who reads nothing else should
|
||||
understand this stop from the gist alone.
|
||||
- **detail**: This is where the colleague pauses to explain. Supports basic markdown.
|
||||
- Start with 1-2 sentences describing the situation or problem this stop addresses.
|
||||
- Then make the argument: WHY did we change this? WHY does the new code look the
|
||||
way it does? If a non-obvious choice was made (data structure, error strategy,
|
||||
sync vs async, where the logic lives), surface it. "We did X instead of Y
|
||||
because Z" is exactly what the reviewer wants.
|
||||
- Use ### headings (e.g. "### Why this shape") to highlight critical sub-sections.
|
||||
- Use > [!IMPORTANT], > [!WARNING], or > [!NOTE] callout blocks for context
|
||||
that helps the reader understand non-obvious decisions or behavioral shifts
|
||||
(e.g., a new default value, a changed error path, a contract that callers
|
||||
now depend on). These are not for flagging code smells.
|
||||
- Use - bullet points for multi-part changes or parallel considerations.
|
||||
- Keep total length reasonable, around 3-6 sentences equivalent. Don't write
|
||||
an essay.
|
||||
- **transition**: A short connective phrase to the next stop, in the colleague's
|
||||
voice. Examples: "Building on that...", "On a related note...", "To support
|
||||
that change...". Empty string for the last stop.
|
||||
- **anchors**: The specific diff hunks shown inline below the detail narrative.
|
||||
Each anchor MUST have a non-empty "hunk" field containing the actual unified
|
||||
diff text extracted from the changeset. The hunk must include the @@ line.
|
||||
|
||||
Valid hunk format (REQUIRED; every anchor needs this):
|
||||
|
||||
@@ -42,7 +42,9 @@
|
||||
function processRequest(req) {
|
||||
- const result = await fetch(url);
|
||||
- return result.json();
|
||||
+ const result = await fetch(url, { timeout: 5000 });
|
||||
+ if (!result.ok) throw new Error("HTTP " + result.status);
|
||||
+ return result.json();
|
||||
}
|
||||
|
||||
The label should be a substantive 1-sentence explanation of what this code
|
||||
section does or why it matters, not a filename paraphrase.
|
||||
E.g. "Adds a 5-second timeout and explicit error check to prevent silent hangs",
|
||||
not "Changes to request.ts".
|
||||
|
||||
### qa_checklist
|
||||
4 to 8 verification questions a HUMAN can actually answer. Two valid channels:
|
||||
|
||||
1. By READING the code (e.g., "Did we update both call sites of \`legacyAuth()\`?",
|
||||
"Are all uses of the old token format migrated?", "Does the error handler
|
||||
cover the new throw paths?").
|
||||
2. By manually USING the product (e.g., "Sign in, restart the browser, and
|
||||
confirm the session persists.", "Trigger a 503 from the API and confirm the
|
||||
retry banner appears.").
|
||||
|
||||
NOT machine-runnable test ideas. NOT generic "smoke test" framing. The reviewer
|
||||
is a person; what would THEY do to gain confidence?
|
||||
|
||||
Reference which stops each question relates to via stop_indices. Every question
|
||||
should reference at least one stop.
|
||||
|
||||
## Pipeline
|
||||
|
||||
1. Read the full diff (git diff or inlined patch).
|
||||
2. Read CLAUDE.md and README.md for project context.
|
||||
3. Read commit messages (git log --oneline) and PR title/body if available.
|
||||
4. Identify logical groupings of change (cross-file when appropriate). These
|
||||
become stops.
|
||||
5. Determine reading flow order: entry point first, then outward. Definitions
|
||||
before consumers, cause before effect.
|
||||
6. Write the greeting, intent, before/after, takeaways, stops, and checklist
|
||||
in the voice of a coworker walking you through the work.
|
||||
7. Return structured JSON matching the schema.
|
||||
|
||||
## Hard constraints
|
||||
- Every anchor MUST have a non-empty "hunk" field. An anchor with an empty hunk
|
||||
is broken; it will show "diff not available" to the reviewer. Extract the
|
||||
real unified diff text from the input patch. Do not leave hunk blank.
|
||||
- Never fabricate line numbers. Extract them from the diff.
|
||||
- Gist must be ONE sentence. Not two. Not a run-on. One.
|
||||
- Detail supports markdown. Use it when it makes the explanation clearer, not
|
||||
for decoration. Plain prose is fine when the change is simple.
|
||||
- Anchor labels must explain the code's purpose or the change's impact, not
|
||||
just describe the filename.
|
||||
- key_takeaways: 3 to 5 items, each ONE sentence.
|
||||
- Stops are LOGICAL units, not files. Cross-file grouping is expected.
|
||||
- Stop ORDER is reading flow: entry point first, definitions before consumers,
|
||||
cause before effect, verification last.
|
||||
- Combine trivial changes (renames, imports, formatting) into one "Housekeeping"
|
||||
stop at the end, or omit entirely.
|
||||
- QA questions must be answerable by a human, either by reading code or by
|
||||
using the product. Never frame them as automated tests.
|
||||
- NEVER use em-dashes (—) anywhere in the output. Use commas, colons,
|
||||
semicolons, parentheses, or separate sentences. This is a hard constraint.
|
||||
|
||||
## Calibration: tour, not review
|
||||
Your job is to EXPLAIN the changeset, not to critique it. If you genuinely
|
||||
spot a real bug or a meaningful behavioral concern while reading the code,
|
||||
surface it naturally in the relevant stop detail or as a warning takeaway.
|
||||
That's the colleague noticing something worth mentioning. But don't hunt for
|
||||
problems. Most clean changesets should have zero warnings and zero [!WARNING]
|
||||
callouts. The primary question is "what does this change do and why?" not
|
||||
"what's wrong with this code?"`;
|
||||
|
||||
function buildTourUserMessage(
|
||||
patch: string,
|
||||
diffType: DiffType,
|
||||
options?: { defaultBranch?: string; hasLocalAccess?: boolean; prDiffScope?: string },
|
||||
prMetadata?: PRMetadata,
|
||||
): string {
|
||||
if (prMetadata) {
|
||||
if (options?.prDiffScope === "full-stack") {
|
||||
return [
|
||||
`Full-stack tour of ${prMetadata.url}`,
|
||||
"",
|
||||
"This is a stacked PR. The diff below shows ALL accumulated changes from the repository default branch through this PR's head (not just this PR's own layer).",
|
||||
"Walk the reviewer through the complete changeset as a guided tour.",
|
||||
"",
|
||||
"```diff",
|
||||
patch,
|
||||
"```",
|
||||
].join("\n");
|
||||
}
|
||||
if (options?.hasLocalAccess) {
|
||||
return [
|
||||
prMetadata.url,
|
||||
"",
|
||||
"You are in a local worktree checked out at the PR head. The code is available locally.",
|
||||
`To see the PR changes, diff against the remote base branch: git diff origin/${prMetadata.baseBranch}...HEAD`,
|
||||
"Do NOT diff against the local `main` branch; it may be stale. Always use origin/.",
|
||||
"",
|
||||
"Walk the reviewer through this changeset as a guided tour.",
|
||||
].join("\n");
|
||||
}
|
||||
return [prMetadata.url, "", "Walk the reviewer through this PR as a guided tour."].join("\n");
|
||||
}
|
||||
|
||||
const effectiveDiffType = diffType.startsWith("worktree:")
|
||||
? diffType.split(":").pop() || "uncommitted"
|
||||
: diffType;
|
||||
|
||||
switch (effectiveDiffType) {
|
||||
case "uncommitted":
|
||||
return "Walk the reviewer through the current code changes (staged, unstaged, and untracked files) as a guided tour.";
|
||||
case "staged":
|
||||
return "Walk the reviewer through the currently staged code changes (`git diff --staged`) as a guided tour.";
|
||||
case "unstaged":
|
||||
return "Walk the reviewer through the unstaged code changes (tracked modifications and untracked files) as a guided tour.";
|
||||
case "last-commit":
|
||||
return "Walk the reviewer through the code changes introduced in the last commit (`git diff HEAD~1..HEAD`) as a guided tour.";
|
||||
case "branch": {
|
||||
const base = options?.defaultBranch || "main";
|
||||
return `Walk the reviewer through the code changes against the base branch '${base}' as a guided tour. Run \`git diff ${base}..HEAD\` to inspect the changes.`;
|
||||
}
|
||||
case "merge-base": {
|
||||
const base = options?.defaultBranch || "main";
|
||||
return `Walk the reviewer through the PR-style diff against base '${base}' as a guided tour. First find the common ancestor with \`git merge-base ${base} HEAD\`, then run \`git diff <merge-base>..HEAD\` using that commit to inspect only the changes introduced on this branch (matches GitHub's PR view).`;
|
||||
}
|
||||
case "all":
|
||||
return "Walk the reviewer through every file in the repository as a guided tour. All files are shown as additions (diffed against an empty tree).";
|
||||
default:
|
||||
return [
|
||||
"Walk the reviewer through the following code changes as a guided tour.",
|
||||
"",
|
||||
"```diff",
|
||||
patch,
|
||||
"```",
|
||||
].join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
export interface TourClaudeCommandResult {
|
||||
command: string[];
|
||||
stdinPrompt: string;
|
||||
}
|
||||
|
||||
export function buildTourClaudeCommand(prompt: string, model: string = "sonnet", effort?: string): TourClaudeCommandResult {
|
||||
const allowedTools = [
|
||||
"Agent", "Read", "Glob", "Grep",
|
||||
"Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)",
|
||||
"Bash(git show:*)", "Bash(git blame:*)", "Bash(git branch:*)",
|
||||
"Bash(git grep:*)", "Bash(git ls-remote:*)", "Bash(git ls-tree:*)",
|
||||
"Bash(git merge-base:*)", "Bash(git remote:*)", "Bash(git rev-parse:*)",
|
||||
"Bash(git show-ref:*)",
|
||||
"Bash(gh pr view:*)", "Bash(gh pr diff:*)", "Bash(gh pr list:*)",
|
||||
"Bash(gh api repos/*/*/pulls/*)", "Bash(gh api repos/*/*/pulls/*/files*)",
|
||||
// The tour prompt follows linked issues (`Fixes #123`, `Closes owner/repo#456`),
|
||||
// so the allowlist has to permit the issue-read commands.
|
||||
"Bash(gh issue view:*)", "Bash(gh api repos/*/*/issues/*)",
|
||||
"Bash(glab mr view:*)", "Bash(glab mr diff:*)",
|
||||
"Bash(glab issue view:*)",
|
||||
"Bash(wc:*)",
|
||||
].join(",");
|
||||
|
||||
const disallowedTools = [
|
||||
"Edit", "Write", "NotebookEdit", "WebFetch", "WebSearch",
|
||||
"Bash(python:*)", "Bash(python3:*)", "Bash(node:*)", "Bash(npx:*)",
|
||||
"Bash(bun:*)", "Bash(bunx:*)", "Bash(sh:*)", "Bash(bash:*)", "Bash(zsh:*)",
|
||||
"Bash(curl:*)", "Bash(wget:*)",
|
||||
].join(",");
|
||||
|
||||
return {
|
||||
command: [
|
||||
"claude", "-p",
|
||||
"--permission-mode", "dontAsk",
|
||||
"--output-format", "stream-json",
|
||||
"--verbose",
|
||||
"--json-schema", TOUR_SCHEMA_JSON,
|
||||
"--no-session-persistence",
|
||||
"--model", model,
|
||||
...(effort ? ["--effort", effort] : []),
|
||||
"--tools", "Agent,Bash,Read,Glob,Grep",
|
||||
"--allowedTools", allowedTools,
|
||||
"--disallowedTools", disallowedTools,
|
||||
],
|
||||
stdinPrompt: prompt,
|
||||
};
|
||||
}
|
||||
|
||||
const TOUR_SCHEMA_DIR = join(homedir(), ".plannotator");
|
||||
const TOUR_SCHEMA_FILE = join(TOUR_SCHEMA_DIR, "tour-schema.json");
|
||||
let tourSchemaMaterialized = false;
|
||||
|
||||
async function ensureTourSchemaFile(): Promise<string> {
|
||||
if (!tourSchemaMaterialized) {
|
||||
await mkdir(TOUR_SCHEMA_DIR, { recursive: true });
|
||||
await writeFile(TOUR_SCHEMA_FILE, TOUR_SCHEMA_JSON);
|
||||
tourSchemaMaterialized = true;
|
||||
}
|
||||
return TOUR_SCHEMA_FILE;
|
||||
}
|
||||
|
||||
export function generateTourOutputPath(): string {
|
||||
return join(tmpdir(), `plannotator-tour-${crypto.randomUUID()}.json`);
|
||||
}
|
||||
|
||||
export async function buildTourCodexCommand(options: {
|
||||
cwd: string;
|
||||
outputPath: string;
|
||||
prompt: string;
|
||||
model?: string;
|
||||
reasoningEffort?: string;
|
||||
fastMode?: boolean;
|
||||
}): Promise<string[]> {
|
||||
const { cwd, outputPath, prompt, model, reasoningEffort, fastMode } = options;
|
||||
const schemaPath = await ensureTourSchemaFile();
|
||||
|
||||
const command = [
|
||||
"codex",
|
||||
// Global flags must precede the "exec" subcommand for the Codex CLI.
|
||||
...(model ? ["-m", model] : []),
|
||||
...(reasoningEffort ? ["-c", `model_reasoning_effort=${reasoningEffort}`] : []),
|
||||
...(fastMode ? ["-c", "service_tier=fast"] : []),
|
||||
"exec",
|
||||
"--output-schema", schemaPath,
|
||||
"-o", outputPath,
|
||||
"--full-auto", "--ephemeral",
|
||||
"-C", cwd,
|
||||
prompt,
|
||||
];
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
export function parseTourStreamOutput(stdout: string): CodeTourOutput | null {
|
||||
if (!stdout.trim()) return null;
|
||||
|
||||
const lines = stdout.trim().split('\n');
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
if (event.type === 'result') {
|
||||
if (event.is_error) return null;
|
||||
const output = event.structured_output;
|
||||
// A tour with no stops isn't a tour — treat as invalid so the UI
|
||||
// error state fires instead of rendering an empty walkthrough.
|
||||
if (!output || !Array.isArray(output.stops) || output.stops.length === 0) return null;
|
||||
return output as CodeTourOutput;
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON — skip
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function parseTourFileOutput(outputPath: string): Promise<CodeTourOutput | null> {
|
||||
try {
|
||||
const text = await readFile(outputPath, "utf-8");
|
||||
try { await unlink(outputPath); } catch { /* ignore */ }
|
||||
if (!text.trim()) return null;
|
||||
const parsed = JSON.parse(text);
|
||||
// A tour with no stops isn't a tour — treat as invalid so the UI
|
||||
// error state fires instead of rendering an empty walkthrough.
|
||||
if (!parsed || !Array.isArray(parsed.stops) || parsed.stops.length === 0) return null;
|
||||
return parsed as CodeTourOutput;
|
||||
} catch {
|
||||
try { await unlink(outputPath); } catch { /* ignore */ }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TourSessionBuildCommandOptions {
|
||||
cwd: string;
|
||||
patch: string;
|
||||
diffType: DiffType;
|
||||
options?: { defaultBranch?: string; hasLocalAccess?: boolean };
|
||||
prMetadata?: PRMetadata;
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TourSessionBuildCommandResult {
|
||||
command: string[];
|
||||
outputPath?: string;
|
||||
captureStdout?: boolean;
|
||||
stdinPrompt?: string;
|
||||
cwd?: string;
|
||||
label?: string;
|
||||
prompt?: string;
|
||||
engine: "claude" | "codex";
|
||||
model: string;
|
||||
effort?: string;
|
||||
reasoningEffort?: string;
|
||||
fastMode?: boolean;
|
||||
}
|
||||
|
||||
export interface TourSessionJobSummary {
|
||||
correctness: string;
|
||||
explanation: string;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export interface TourSessionJobRef {
|
||||
id: string;
|
||||
engine?: string;
|
||||
}
|
||||
|
||||
export interface TourSessionOnJobCompleteOptions {
|
||||
job: TourSessionJobRef;
|
||||
meta: { outputPath?: string; stdout?: string };
|
||||
}
|
||||
|
||||
export interface TourSession {
|
||||
tourResults: Map<string, CodeTourOutput>;
|
||||
tourChecklists: Map<string, boolean[]>;
|
||||
buildCommand(opts: TourSessionBuildCommandOptions): Promise<TourSessionBuildCommandResult>;
|
||||
onJobComplete(opts: TourSessionOnJobCompleteOptions): Promise<{ summary: TourSessionJobSummary | null }>;
|
||||
getTour(jobId: string): (CodeTourOutput & { checklist: boolean[] }) | null;
|
||||
saveChecklist(jobId: string, checked: boolean[]): void;
|
||||
}
|
||||
|
||||
export function createTourSession(): TourSession {
|
||||
const tourResults = new Map<string, CodeTourOutput>();
|
||||
const tourChecklists = new Map<string, boolean[]>();
|
||||
|
||||
return {
|
||||
tourResults,
|
||||
tourChecklists,
|
||||
|
||||
async buildCommand({ cwd, patch, diffType, options, prMetadata, config }) {
|
||||
const engine = (typeof config?.engine === "string" ? config.engine : "claude") as "claude" | "codex";
|
||||
const explicitModel = typeof config?.model === "string" && config.model ? config.model : null;
|
||||
// "sonnet" is a Claude model, so we must NOT pass it to Codex when no model
|
||||
// is explicitly selected. Leave Codex model blank and let its CLI default pick.
|
||||
const model = explicitModel ?? (engine === "codex" ? "" : "sonnet");
|
||||
const reasoningEffort = typeof config?.reasoningEffort === "string" && config.reasoningEffort ? config.reasoningEffort : undefined;
|
||||
const effort = typeof config?.effort === "string" && config.effort ? config.effort : undefined;
|
||||
const fastMode = config?.fastMode === true;
|
||||
const userMessage = buildTourUserMessage(patch, diffType, options, prMetadata);
|
||||
const prompt = TOUR_REVIEW_PROMPT + "\n\n---\n\n" + userMessage;
|
||||
|
||||
if (engine === "codex") {
|
||||
const outputPath = generateTourOutputPath();
|
||||
const command = await buildTourCodexCommand({ cwd, outputPath, prompt, model: model || undefined, reasoningEffort, fastMode });
|
||||
return { command, outputPath, prompt, label: "Code Tour", engine: "codex", model, reasoningEffort, fastMode: fastMode || undefined };
|
||||
}
|
||||
|
||||
const { command, stdinPrompt } = buildTourClaudeCommand(prompt, model, effort);
|
||||
return { command, stdinPrompt, prompt, cwd, label: "Code Tour", captureStdout: true, engine: "claude", model, effort };
|
||||
},
|
||||
|
||||
async onJobComplete({ job, meta }) {
|
||||
let output: CodeTourOutput | null = null;
|
||||
if (job.engine === "codex" && meta.outputPath) {
|
||||
output = await parseTourFileOutput(meta.outputPath);
|
||||
} else if (meta.stdout) {
|
||||
output = parseTourStreamOutput(meta.stdout);
|
||||
}
|
||||
|
||||
if (!output) {
|
||||
console.error(`[tour] Failed to parse output for job ${job.id}`);
|
||||
return { summary: null };
|
||||
}
|
||||
|
||||
tourResults.set(job.id, output);
|
||||
const summary: TourSessionJobSummary = {
|
||||
correctness: "Tour Generated",
|
||||
explanation: `${output.stops.length} stop${output.stops.length !== 1 ? "s" : ""}, ${output.qa_checklist.length} QA item${output.qa_checklist.length !== 1 ? "s" : ""}`,
|
||||
confidence: 1.0,
|
||||
};
|
||||
return { summary };
|
||||
},
|
||||
|
||||
getTour(jobId) {
|
||||
const tour = tourResults.get(jobId);
|
||||
if (!tour) return null;
|
||||
return { ...tour, checklist: tourChecklists.get(jobId) ?? [] };
|
||||
},
|
||||
|
||||
saveChecklist(jobId, checked) {
|
||||
tourChecklists.set(jobId, checked);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user