@dunnewold-labs/mr-manager 0.4.48 → 0.4.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +970 -443
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -185,7 +185,7 @@ import { fileURLToPath } from "url";
|
|
|
185
185
|
// cli/package.json
|
|
186
186
|
var package_default = {
|
|
187
187
|
name: "@dunnewold-labs/mr-manager",
|
|
188
|
-
version: "0.4.
|
|
188
|
+
version: "0.4.52",
|
|
189
189
|
description: "Mr. Manager - Task and project management CLI",
|
|
190
190
|
bin: {
|
|
191
191
|
mr: "./dist/index.mjs"
|
|
@@ -561,7 +561,7 @@ import { join as join5 } from "path";
|
|
|
561
561
|
|
|
562
562
|
// lib/git-worktree.ts
|
|
563
563
|
import { execSync as execSync2 } from "child_process";
|
|
564
|
-
import { copyFileSync, existsSync as existsSync4 } from "fs";
|
|
564
|
+
import { copyFileSync, existsSync as existsSync4, rmSync } from "fs";
|
|
565
565
|
import { join as join4 } from "path";
|
|
566
566
|
function tryExec(command, cwd) {
|
|
567
567
|
try {
|
|
@@ -619,15 +619,21 @@ function findExistingWorktreeForBranch(repoDir, branch) {
|
|
|
619
619
|
function createWorktree(repoDir, branch, worktreeName, options = {}) {
|
|
620
620
|
const wtPath = join4(repoDir, ".mr-worktrees", worktreeName);
|
|
621
621
|
if (existsSync4(wtPath)) {
|
|
622
|
-
|
|
623
|
-
if (
|
|
624
|
-
|
|
622
|
+
const branchResolves = tryExec(`git rev-parse --verify "refs/heads/${branch}"`, repoDir);
|
|
623
|
+
if (branchResolves) {
|
|
624
|
+
execSync2(`git checkout "${branch}"`, { cwd: wtPath, stdio: "pipe" });
|
|
625
|
+
if (options.syncRemoteBranch) {
|
|
626
|
+
syncBranchWithRemote(repoDir, wtPath, branch);
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
path: wtPath,
|
|
630
|
+
created: false,
|
|
631
|
+
reusedBranchWorktree: false
|
|
632
|
+
};
|
|
625
633
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
reusedBranchWorktree: false
|
|
630
|
-
};
|
|
634
|
+
removeWorktree(repoDir, wtPath);
|
|
635
|
+
rmSync(wtPath, { recursive: true, force: true });
|
|
636
|
+
tryExec(`git worktree prune`, repoDir);
|
|
631
637
|
}
|
|
632
638
|
const existingBranchWorktree = findExistingWorktreeForBranch(repoDir, branch);
|
|
633
639
|
if (existingBranchWorktree) {
|
|
@@ -1133,7 +1139,7 @@ async function runTest(options) {
|
|
|
1133
1139
|
}
|
|
1134
1140
|
|
|
1135
1141
|
// lib/agent-fallback.ts
|
|
1136
|
-
var AGENT_FALLBACK_ORDER = ["claude", "codex", "
|
|
1142
|
+
var AGENT_FALLBACK_ORDER = ["claude", "codex", "antigravity"];
|
|
1137
1143
|
function getAgentFallbackChain(agent) {
|
|
1138
1144
|
const startIndex = AGENT_FALLBACK_ORDER.indexOf(agent);
|
|
1139
1145
|
if (startIndex === -1) {
|
|
@@ -1194,6 +1200,36 @@ function isPrOrMrUrl(input) {
|
|
|
1194
1200
|
}
|
|
1195
1201
|
return false;
|
|
1196
1202
|
}
|
|
1203
|
+
var MAX_SLUG_LENGTH = 30;
|
|
1204
|
+
function baseSlug(input) {
|
|
1205
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_SLUG_LENGTH);
|
|
1206
|
+
}
|
|
1207
|
+
function extractTicketKey(input) {
|
|
1208
|
+
const match = input.match(/\b([A-Z][A-Z0-9]+-\d+)\b/);
|
|
1209
|
+
return match ? match[1].toLowerCase() : null;
|
|
1210
|
+
}
|
|
1211
|
+
function slugifyTitle(title) {
|
|
1212
|
+
const raw = (title ?? "").trim();
|
|
1213
|
+
if (!raw) return "";
|
|
1214
|
+
const ticketKey = extractTicketKey(raw);
|
|
1215
|
+
if (ticketKey) return baseSlug(ticketKey);
|
|
1216
|
+
const urlPathSlug = urlToPathSlug(raw);
|
|
1217
|
+
if (urlPathSlug) return urlPathSlug;
|
|
1218
|
+
return baseSlug(raw);
|
|
1219
|
+
}
|
|
1220
|
+
function urlToPathSlug(input) {
|
|
1221
|
+
let url;
|
|
1222
|
+
try {
|
|
1223
|
+
url = new URL(input);
|
|
1224
|
+
} catch {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
if (!/^https?:$/.test(url.protocol)) return null;
|
|
1228
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
1229
|
+
if (segments.length === 0) return null;
|
|
1230
|
+
const slug = baseSlug(segments.slice(-2).join("-"));
|
|
1231
|
+
return slug || null;
|
|
1232
|
+
}
|
|
1197
1233
|
|
|
1198
1234
|
// cli/browse-runner.ts
|
|
1199
1235
|
import { execSync as execSync3, spawn as spawn3 } from "child_process";
|
|
@@ -1497,9 +1533,6 @@ function findDirectoryForProject(config, projectId, rootDir) {
|
|
|
1497
1533
|
}
|
|
1498
1534
|
return null;
|
|
1499
1535
|
}
|
|
1500
|
-
function slugify(title) {
|
|
1501
|
-
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30);
|
|
1502
|
-
}
|
|
1503
1536
|
function shortId(id) {
|
|
1504
1537
|
return id.slice(0, 8);
|
|
1505
1538
|
}
|
|
@@ -1514,7 +1547,8 @@ function taskBranchName(task) {
|
|
|
1514
1547
|
if (branch && !isPrOrMrUrl(branch)) {
|
|
1515
1548
|
return branch;
|
|
1516
1549
|
}
|
|
1517
|
-
|
|
1550
|
+
const slug = slugifyTitle(task.title) || shortId(task.id);
|
|
1551
|
+
return `${ownerPrefix(task)}/${slug}`;
|
|
1518
1552
|
}
|
|
1519
1553
|
function resolveBranchFromPrUrl(prUrl, repoDir, vcs) {
|
|
1520
1554
|
const cmd = vcs === "gitlab" ? `glab mr view "${prUrl}" --output json 2>/dev/null | jq -r '.source_branch // empty'` : `gh pr view "${prUrl}" --json headRefName -q .headRefName 2>/dev/null`;
|
|
@@ -1996,7 +2030,7 @@ function buildFeaturesSection(repoDir) {
|
|
|
1996
2030
|
].join("\n");
|
|
1997
2031
|
}
|
|
1998
2032
|
function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = [], executionDir, startWithoutWorktree = false, preparedBranchName) {
|
|
1999
|
-
const slug =
|
|
2033
|
+
const slug = slugifyTitle(task.title) || shortId(task.id);
|
|
2000
2034
|
const owner = ownerPrefix(task);
|
|
2001
2035
|
const generatedBranchName = `${owner}/${slug}`;
|
|
2002
2036
|
const branchName = taskBranchName(task);
|
|
@@ -2429,6 +2463,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2429
2463
|
const referenceImagePaths = options.referenceImagePaths ?? [];
|
|
2430
2464
|
const hasReferences = referenceImagePaths.length > 0;
|
|
2431
2465
|
const exemplars = pickExemplars(proto.id);
|
|
2466
|
+
const isLowFidelity = proto.fidelity === "low";
|
|
2432
2467
|
const variantSteps = [];
|
|
2433
2468
|
for (let i = 0; i < variantsToProduce; i++) {
|
|
2434
2469
|
const idx = startIndex + i;
|
|
@@ -2436,7 +2471,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2436
2471
|
variantSteps.push(
|
|
2437
2472
|
`### Variant ${idx}: ${filename}`,
|
|
2438
2473
|
`1. ${varianceInfo.perVariantDirective}`,
|
|
2439
|
-
`2. Spend a moment composing a real, opinionated design before you start typing HTML. ${hasReferences ? "Imagine the screen sitting alongside the reference designs the user attached \u2014 it should look like it belongs in the same set." : `Imagine the screen on Dribbble, holding its own next to ${exemplars}.`}`,
|
|
2474
|
+
isLowFidelity ? `2. Compose a layout focusing on structure and information flow. Keep it strictly grayscale, sketchy, and low-fidelity.` : `2. Spend a moment composing a real, opinionated design before you start typing HTML. ${hasReferences ? "Imagine the screen sitting alongside the reference designs the user attached \u2014 it should look like it belongs in the same set." : `Imagine the screen on Dribbble, holding its own next to ${exemplars}.`}`,
|
|
2440
2475
|
`3. Write the complete self-contained HTML to \`${repoDir}/${filename}\` using the Write tool.`,
|
|
2441
2476
|
`4. Verify the file was created by reading the first few lines of \`${repoDir}/${filename}\`.`,
|
|
2442
2477
|
``
|
|
@@ -2446,6 +2481,38 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2446
2481
|
{ length: variantsToProduce },
|
|
2447
2482
|
(_, i) => `prototype-${startIndex + i}.html`
|
|
2448
2483
|
);
|
|
2484
|
+
const wireframeQualityBar = [
|
|
2485
|
+
`## Wireframe & Sketch Guidelines (Balsamiq-Style)`,
|
|
2486
|
+
``,
|
|
2487
|
+
`This is an intentionally LOW-FIDELITY wireframe. The goal is to focus on structure, layout, and user flow, NOT visual polish. Follow these strict styling rules:`,
|
|
2488
|
+
``,
|
|
2489
|
+
`**Grayscale Aesthetic Only**`,
|
|
2490
|
+
`- Use ONLY black, white, and shades of gray.`,
|
|
2491
|
+
`- Absolutely NO color palette exploration, no accent colors, no gradients, and no color highlights.`,
|
|
2492
|
+
``,
|
|
2493
|
+
`**Linework & Borders**`,
|
|
2494
|
+
`- Borders must feel sketchy or hand-drawn. Use thick, solid borders (e.g., 2px solid black or dark gray) for elements.`,
|
|
2495
|
+
`- Keep shapes simple and flat. Avoid rounded corners unless they represent a device frame or button.`,
|
|
2496
|
+
`- Use flat gray boxes with diagonal lines or an "X" representing media/image placeholders.`,
|
|
2497
|
+
``,
|
|
2498
|
+
`**Typography (Default Font)**`,
|
|
2499
|
+
`- You MUST load the Shantell Sans font from Google Fonts via:`,
|
|
2500
|
+
` <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Shantell+Sans:wght@300..800&display=swap">`,
|
|
2501
|
+
`- Set the default typeface of the entire body to: 'Shantell Sans', 'Comic Sans MS', 'Comic Neue', cursive, sans-serif.`,
|
|
2502
|
+
`- Text should feel hand-drawn but readable, giving the app a sketch feeling.`,
|
|
2503
|
+
``,
|
|
2504
|
+
`**Placeholders & Annotations**`,
|
|
2505
|
+
`- Media (images, videos, illustrations) must render as obvious sketchy placeholders \u2014 never use real photos, Unsplash images, or polished vector illustrations. Use a rectangle with "X" drawn from corner to corner.`,
|
|
2506
|
+
`- Icons should be extremely simple/sketchy or represented as simple text placeholders (e.g. "[x]", "[+]", "[menu]"). Do NOT use polished icon packs if they look too clean. Standard text brackets or extremely basic SVG paths are preferred.`,
|
|
2507
|
+
`- Copy should be terse, greeked, or simple descriptive label annotations (e.g. "[Brand Logo]", "[Search Results: 10 items]", "[User Name]"). Avoid polished marketing copy or detailed UI prose.`,
|
|
2508
|
+
``,
|
|
2509
|
+
`**No Shadows, Gradients, or Motion**`,
|
|
2510
|
+
`- Absolutely NO box-shadows, text-shadows, gradients, backdrop-blur, or glassmorphism.`,
|
|
2511
|
+
`- Absolutely NO delight-oriented motion, fade-ins, slide-ups, marquee effects, or animated gradients. Everything must be static and flat.`,
|
|
2512
|
+
``,
|
|
2513
|
+
`**Device Framing (Sketch Form)**`,
|
|
2514
|
+
`${prototypeType === "mobile_app" ? `- Draw a sketchy mobile frame container (centered, e.g. 375x812px) using a 2px solid black border. Render a sketchy top status bar (e.g., "[9:41 AM Battery 100%]") and sketchy dynamic island/notch. Center the frame on a flat, light-gray canvas.` : prototypeType === "desktop_app" ? `- Draw a sketchy desktop window frame (e.g. 1024x768px or scaled container) with a 2px solid black border. Show a simple top bar with sketchy window controls (three tiny circles or text like "[o][o][o]"). Center the frame on a flat, light-gray canvas.` : `- For a web app, present the layout as a browser screen (with a sketchy address bar like "[https://app.com/]") styled with a 2px solid black border.`}`
|
|
2515
|
+
];
|
|
2449
2516
|
const sharedQualityBar = [
|
|
2450
2517
|
`## Craft & Polish Bar (this is the most important section \u2014 read carefully)`,
|
|
2451
2518
|
``,
|
|
@@ -2555,8 +2622,14 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2555
2622
|
logo: "Logo"
|
|
2556
2623
|
};
|
|
2557
2624
|
const agentNote = options.agentLabel ? `You are one of several models collaborating on this prototype set. You are the "${options.agentLabel}" track, responsible for variants ${startIndex} through ${startIndex + variantsToProduce - 1}. Bring YOUR creative perspective \u2014 don't try to be a generic averaged designer.` : `You are the sole designer on this prototype set \u2014 bring a strong, opinionated point of view.`;
|
|
2558
|
-
const creativeAnchor = hasReferences ? `The user attached reference designs for this prototype \u2014 they are the single most important input. Treat them as the definitive brief for look and feel (details in the Reference Designs section below).` : `Aim for the craft level of ${exemplars} \u2014 but interpret this brief on its own terms, don't imitate any one product.`;
|
|
2559
|
-
const creativeDirection = [
|
|
2625
|
+
const creativeAnchor = isLowFidelity ? hasReferences ? `The user attached reference designs for this prototype. Use them ONLY to guide structure, layout, and content hierarchy. Do NOT copy their visual style, colors, shadows, fonts, or polish. Keep the output strictly as a low-fidelity wireframe.` : `Generate a low-fidelity wireframe representing the layout, hierarchy, and flow described in the prompt.` : hasReferences ? `The user attached reference designs for this prototype \u2014 they are the single most important input. Treat them as the definitive brief for look and feel (details in the Reference Designs section below).` : `Aim for the craft level of ${exemplars} \u2014 but interpret this brief on its own terms, don't imitate any one product.`;
|
|
2626
|
+
const creativeDirection = isLowFidelity ? [
|
|
2627
|
+
`## Wireframe Direction`,
|
|
2628
|
+
`- **Focus on Hierarchy**: Make sure the most important elements have the largest text or thickest boundaries.`,
|
|
2629
|
+
`- **Vary the Layout Structure**: Across variants, use different ways of organizing information (e.g. one variant could be a sidebar layout, another a grid layout, another a single-column layout).`,
|
|
2630
|
+
`- **Use Shantell Sans**: Ensure the font successfully loads and applies globally to keep the Balsamiq sketch aesthetic consistent.`,
|
|
2631
|
+
`- **Terse Annotations**: Ensure copy is terse, annotated, or greeked.`
|
|
2632
|
+
] : [
|
|
2560
2633
|
`## Creative Direction & Polish`,
|
|
2561
2634
|
`- **Be Opinionated**: Do not design generic "safe" UIs. Imagine you are designing for a product that wants to win "Site of the Day" on Awwwards or be featured in a "Best of Linear-style design" gallery.`,
|
|
2562
2635
|
`- **Micro-interactions**: Use CSS keyframes for delightful details. A progress bar that has a shimmering gradient, a button that has a subtle "glint" effect, or a card that has a "magnetic" hover feel.`,
|
|
@@ -2564,6 +2637,15 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2564
2637
|
`- **Typography as Hero**: In at least one variant, let typography drive the entire layout. Use large, bold display faces with tight tracking and intentional overlapping.`,
|
|
2565
2638
|
`- **Negative Space**: Do not be afraid of "wasting" space. Generous margins and padding often signal luxury and quality.`
|
|
2566
2639
|
];
|
|
2640
|
+
const aestheticGuardrails = isLowFidelity ? [
|
|
2641
|
+
`- Absolutely NO colors besides black, white, and grays. No gradients, no shadows, no smooth animations.`,
|
|
2642
|
+
`- Do NOT use polished icon libraries (like Lucide CDN/images) if they look too crisp. Use simple text markers like "[+]" or standard hand-drawn style SVGs.`,
|
|
2643
|
+
`- Ensure the body uses the 'Shantell Sans' font stack for a sketchy comic feel.`
|
|
2644
|
+
] : [
|
|
2645
|
+
`- Do NOT default to a terminal / hacker / CRT / monospace-green-on-black aesthetic unless the user's prompt explicitly asks for it. This is a common failure mode \u2014 avoid it.`,
|
|
2646
|
+
`- Do NOT force any single "style label" (retro, futuristic, brutalist, etc.) onto the variants unless the user asked for it. Let the prompt drive the aesthetic.`,
|
|
2647
|
+
`- Choose aesthetics that fit the product and audience described in the user's prompt, not a generic developer-tool look.`
|
|
2648
|
+
];
|
|
2567
2649
|
return [
|
|
2568
2650
|
`${config.role}`,
|
|
2569
2651
|
``,
|
|
@@ -2586,7 +2668,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2586
2668
|
``,
|
|
2587
2669
|
`Generate exactly ${variantsToProduce} HTML file(s) covering variants ${startIndex} through ${startIndex + variantsToProduce - 1}: ${variantList.join(", ")}. Follow the steps below IN ORDER. Do NOT collapse or skip variants. Each file must be completely self-contained (inline all CSS and JS, plus any Google Fonts \`<link>\` you reference). Tailwind CDN is acceptable.`,
|
|
2588
2670
|
``,
|
|
2589
|
-
...sharedQualityBar,
|
|
2671
|
+
...isLowFidelity ? wireframeQualityBar : sharedQualityBar,
|
|
2590
2672
|
``,
|
|
2591
2673
|
...creativeDirection,
|
|
2592
2674
|
``,
|
|
@@ -2603,9 +2685,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2603
2685
|
``,
|
|
2604
2686
|
`## Aesthetic Guardrails`,
|
|
2605
2687
|
``,
|
|
2606
|
-
|
|
2607
|
-
`- Do NOT force any single "style label" (retro, futuristic, brutalist, etc.) onto the variants unless the user asked for it. Let the prompt drive the aesthetic.`,
|
|
2608
|
-
`- Choose aesthetics that fit the product and audience described in the user's prompt, not a generic developer-tool look.`,
|
|
2688
|
+
...aestheticGuardrails,
|
|
2609
2689
|
``,
|
|
2610
2690
|
...variantSteps,
|
|
2611
2691
|
`### Final verification`,
|
|
@@ -2627,6 +2707,8 @@ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
|
|
|
2627
2707
|
const startIndex = options.variantStartIndex ?? 1;
|
|
2628
2708
|
const variance = typeof proto.designVariance === "number" ? proto.designVariance : 70;
|
|
2629
2709
|
const varianceInfo = describeVariance(variance);
|
|
2710
|
+
const isLowFidelity = proto.fidelity === "low";
|
|
2711
|
+
const isPromotion = proto.fidelity === "high" && options.parentFidelity === "low";
|
|
2630
2712
|
const variantSteps = [];
|
|
2631
2713
|
for (let i = 0; i < variantsToProduce; i++) {
|
|
2632
2714
|
const idx = startIndex + i;
|
|
@@ -2634,7 +2716,7 @@ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
|
|
|
2634
2716
|
variantSteps.push(
|
|
2635
2717
|
`### Variant ${idx}: ${filename}`,
|
|
2636
2718
|
`1. Redesign variant ${idx} based on the feedback below, while keeping the core concept that worked.`,
|
|
2637
|
-
`2. Apply the same craft & polish bar as the original generation \u2014 portfolio quality, real microcopy, intentional typography, no Lorem ipsum, no broken images.`,
|
|
2719
|
+
isLowFidelity ? `2. Keep the low-fidelity Balsamiq-style sketch aesthetic (grayscale, Shantell Sans font, sketchy borders, flat placeholders, no shadows/motion).` : isPromotion ? `2. Keep the structure and elements from the parent wireframe, but completely upgrade it with professional colors, typography, gradients, shadows, polished images, and real copy according to the high-fidelity craft guidelines.` : `2. Apply the same craft & polish bar as the original generation \u2014 portfolio quality, real microcopy, intentional typography, no Lorem ipsum, no broken images.`,
|
|
2638
2720
|
`3. Write the complete self-contained HTML to \`${repoDir}/${filename}\` using the Write tool.`,
|
|
2639
2721
|
`4. Verify the file was created by reading the first few lines of \`${repoDir}/${filename}\`.`,
|
|
2640
2722
|
``
|
|
@@ -2674,6 +2756,34 @@ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
|
|
|
2674
2756
|
desktop_app: "Desktop App",
|
|
2675
2757
|
logo: "Logo"
|
|
2676
2758
|
};
|
|
2759
|
+
const wireframeGuidelines = isLowFidelity ? [
|
|
2760
|
+
`## Wireframe & Sketch Guidelines (Balsamiq-Style)`,
|
|
2761
|
+
`This is a low-fidelity wireframe refinement. Maintain these constraints:`,
|
|
2762
|
+
`- Grayscale only: Use ONLY black, white, and gray colors. No accents, gradients, or color fills.`,
|
|
2763
|
+
`- Shantell Sans font: Load 'Shantell Sans' from Google Fonts and set it globally as the default typeface.`,
|
|
2764
|
+
`- Borders/shapes: Use sketchy/thick solid black borders (e.g. 2px). Flat rectangles with an "X" for image placeholders.`,
|
|
2765
|
+
`- Annotations/terse copy: Avoid polished marketing copy; use brief labels, greeked text, or annotations.`,
|
|
2766
|
+
`- No polish: No shadows, no gradients, no glassmorphism, no transitions or motion.`
|
|
2767
|
+
] : isPromotion ? [
|
|
2768
|
+
`## High-Fidelity Promotion Guidelines`,
|
|
2769
|
+
`You are promoting this prototype from a low-fidelity wireframe to a high-fidelity design:`,
|
|
2770
|
+
`- Elevate the visual design using professional color palettes, modern typography (Google Fonts display/text pairings), layout depth (subtle shadows, borders), and realistic mock content/media.`,
|
|
2771
|
+
`- Do NOT copy the grayscale wireframe styling, comic-style font, or diagonal-line placeholders.`
|
|
2772
|
+
] : [
|
|
2773
|
+
`## High-Fidelity Craft & Polish Guidelines`,
|
|
2774
|
+
`- Composition: real grid, intentional spacing, distinct visual hierarchy.`,
|
|
2775
|
+
`- Typography: Google Fonts display + text face pairing (e.g. Inter, Geist, etc.).`,
|
|
2776
|
+
`- Colors & depth: deliberate color palette, soft shadows, inner glows, subtle motion/transitions.`,
|
|
2777
|
+
`- Real microcopy: no Lorem Ipsum, realistic data, realistic dates and names.`
|
|
2778
|
+
];
|
|
2779
|
+
const aestheticGuardrails = isLowFidelity ? [
|
|
2780
|
+
`- Absolutely NO colors besides black, white, and grays. No gradients, no shadows, no smooth animations.`,
|
|
2781
|
+
`- Do NOT use polished icon libraries (like Lucide CDN/images) if they look too crisp. Use simple text markers like "[+]" or standard hand-drawn style SVGs.`,
|
|
2782
|
+
`- Ensure the body uses the 'Shantell Sans' font stack for a sketchy comic feel.`
|
|
2783
|
+
] : [
|
|
2784
|
+
`- Do NOT default to a terminal / hacker / CRT / monospace-green-on-black aesthetic unless the user's prompt explicitly asks for it.`,
|
|
2785
|
+
`- Do NOT force any single "style label" onto the variants unless the user asked for it.`
|
|
2786
|
+
];
|
|
2677
2787
|
return [
|
|
2678
2788
|
`${typeRoleMap[prototypeType] ?? typeRoleMap.web_app}`,
|
|
2679
2789
|
``,
|
|
@@ -2696,32 +2806,33 @@ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
|
|
|
2696
2806
|
existingVariants,
|
|
2697
2807
|
`## Instructions`,
|
|
2698
2808
|
``,
|
|
2699
|
-
`You MUST generate exactly ${
|
|
2809
|
+
`You MUST generate exactly ${variantsToProduce} REFINED HTML file(s) that incorporate the user's feedback above. Follow the steps below IN ORDER.`,
|
|
2700
2810
|
``,
|
|
2701
2811
|
`Study the previous variants carefully, then apply the user's feedback to improve them. Keep what works, change what the user asked to change.`,
|
|
2702
2812
|
``,
|
|
2813
|
+
...wireframeGuidelines,
|
|
2814
|
+
``,
|
|
2703
2815
|
`## Design Variance: ${varianceInfo.label} (${variance}/100)`,
|
|
2704
2816
|
``,
|
|
2705
2817
|
varianceInfo.summary,
|
|
2706
2818
|
``,
|
|
2707
2819
|
`## Aesthetic Guardrails`,
|
|
2708
2820
|
``,
|
|
2709
|
-
|
|
2710
|
-
`- Do NOT force any single "style label" onto the variants unless the user asked for it.`,
|
|
2821
|
+
...aestheticGuardrails,
|
|
2711
2822
|
``,
|
|
2712
2823
|
`Each file must be completely self-contained (inline all CSS and JS \u2014 no external dependencies). Tailwind CDN is acceptable.`,
|
|
2713
2824
|
``,
|
|
2714
2825
|
...variantSteps,
|
|
2715
2826
|
`### Final verification`,
|
|
2716
|
-
`After generating ALL ${
|
|
2827
|
+
`After generating ALL ${variantsToProduce} variant(s), list the files in ${repoDir} and confirm that all ${variantsToProduce} file(s) exist: ${variantList.join(", ")}. If any are missing, go back and generate the missing ones.`,
|
|
2717
2828
|
``,
|
|
2718
2829
|
`IMPORTANT RULES:`,
|
|
2719
|
-
`- You MUST produce exactly
|
|
2830
|
+
`- You MUST produce exactly these file(s): ${variantList.join(", ")}`,
|
|
2720
2831
|
`- Generate them ONE AT A TIME \u2014 design each variant, write the file, then move to the next.`,
|
|
2721
2832
|
`- Respect the Design Variance level above.`,
|
|
2722
2833
|
`- Each file must be a complete, functional page \u2014 pure HTML/CSS/JS, no external libraries (Tailwind CDN is acceptable).`,
|
|
2723
2834
|
`- Do NOT upload or POST the files anywhere. The watch handler will upload them automatically after you exit.`,
|
|
2724
|
-
`- Do NOT exit until ALL ${
|
|
2835
|
+
`- Do NOT exit until ALL ${variantsToProduce} file(s) have been written and verified.`
|
|
2725
2836
|
].join("\n");
|
|
2726
2837
|
}
|
|
2727
2838
|
function buildAgentArgs(agent, prompt2, mode, sessionId, name, resumeSession = false, systemPrompt, maxTurns, claudeModel) {
|
|
@@ -2740,15 +2851,15 @@ ${systemPrompt}` : prompt2;
|
|
|
2740
2851
|
args.push(fullPrompt);
|
|
2741
2852
|
return { bin: "codex", args };
|
|
2742
2853
|
}
|
|
2743
|
-
if (agent === "
|
|
2854
|
+
if (agent === "antigravity") {
|
|
2744
2855
|
const fullPrompt = systemPrompt ? `${prompt2}
|
|
2745
2856
|
|
|
2746
2857
|
${systemPrompt}` : prompt2;
|
|
2747
2858
|
const args = ["-p", fullPrompt];
|
|
2748
2859
|
if (mode === "execute") {
|
|
2749
|
-
args.push("--
|
|
2860
|
+
args.push("--dangerously-skip-permissions");
|
|
2750
2861
|
}
|
|
2751
|
-
return { bin:
|
|
2862
|
+
return { bin: AGENT_BINARIES.antigravity, args };
|
|
2752
2863
|
}
|
|
2753
2864
|
const sessionArgs = sessionId ? resumeSession ? ["--resume", sessionId] : ["--session-id", sessionId] : [];
|
|
2754
2865
|
const nameArgs = name ? ["--name", name] : [];
|
|
@@ -2761,6 +2872,11 @@ ${systemPrompt}` : prompt2;
|
|
|
2761
2872
|
const permissionArgs = ["--dangerously-skip-permissions"];
|
|
2762
2873
|
return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, ...turnsArgs, ...modelArgs, ...permissionArgs, "-p", prompt2] };
|
|
2763
2874
|
}
|
|
2875
|
+
var AGENT_BINARIES = {
|
|
2876
|
+
claude: "claude",
|
|
2877
|
+
codex: "codex",
|
|
2878
|
+
antigravity: "agy"
|
|
2879
|
+
};
|
|
2764
2880
|
function commandExists(cmd) {
|
|
2765
2881
|
return new Promise((resolve9) => {
|
|
2766
2882
|
exec(`command -v ${cmd}`, (err) => resolve9(!err));
|
|
@@ -2777,8 +2893,11 @@ function resolveTaskAgentAndModel(delegatedModel, watchAgent) {
|
|
|
2777
2893
|
return { agent: "claude", claudeModel: "claude-haiku-4-5-20251001" };
|
|
2778
2894
|
case "codex":
|
|
2779
2895
|
return { agent: "codex" };
|
|
2896
|
+
case "antigravity":
|
|
2897
|
+
return { agent: "antigravity" };
|
|
2898
|
+
// Legacy alias: tasks delegated before the Antigravity migration.
|
|
2780
2899
|
case "gemini":
|
|
2781
|
-
return { agent: "
|
|
2900
|
+
return { agent: "antigravity" };
|
|
2782
2901
|
default:
|
|
2783
2902
|
return { agent: watchAgent };
|
|
2784
2903
|
}
|
|
@@ -2866,12 +2985,12 @@ function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name
|
|
|
2866
2985
|
}
|
|
2867
2986
|
var watchCommand = new Command9("watch").description(
|
|
2868
2987
|
"Watch for in-progress tasks and autonomously dispatch an AI coding agent to work on them"
|
|
2869
|
-
).option("--interval <seconds>", "Polling interval in seconds", "15").option("--dry-run", "Show what would be dispatched without spawning the agent", false).option("--plan-approval", "Show the agent's plan and ask for approval before executing", false).option("--root <dir>", "Root directory filter for linked repos (default: cwd)").option("--agent <agent>", "AI agent to use: claude, codex, or
|
|
2988
|
+
).option("--interval <seconds>", "Polling interval in seconds", "15").option("--dry-run", "Show what would be dispatched without spawning the agent", false).option("--plan-approval", "Show the agent's plan and ask for approval before executing", false).option("--root <dir>", "Root directory filter for linked repos (default: cwd)").option("--agent <agent>", "AI agent to use: claude, codex, or antigravity", "claude").option("--scan-at <HH:MM>", "Run a product scan daily at this time (e.g., 02:00)").action(async (opts) => {
|
|
2870
2989
|
const intervalMs = parseInt(opts.interval, 10) * 1e3;
|
|
2871
2990
|
const dryRun = opts.dryRun;
|
|
2872
2991
|
const planApproval = opts.planApproval;
|
|
2873
2992
|
const rootDir = opts.root ? resolve2(opts.root) : process.cwd();
|
|
2874
|
-
const agent = opts.agent === "codex" ? "codex" : opts.agent === "
|
|
2993
|
+
const agent = opts.agent === "codex" ? "codex" : opts.agent === "antigravity" ? "antigravity" : "claude";
|
|
2875
2994
|
const scanAt = opts.scanAt;
|
|
2876
2995
|
const taskStallTimeoutMs = getTaskStallTimeoutMs();
|
|
2877
2996
|
const hungTaskTimeoutMinutes = Math.max(5, parseInt(process.env.MR_WATCH_HUNG_TASK_TIMEOUT_MINUTES ?? "60", 10) || 60);
|
|
@@ -2913,16 +3032,16 @@ var watchCommand = new Command9("watch").description(
|
|
|
2913
3032
|
if (cached !== void 0) {
|
|
2914
3033
|
return cached;
|
|
2915
3034
|
}
|
|
2916
|
-
const available = await commandExists(candidate);
|
|
3035
|
+
const available = await commandExists(AGENT_BINARIES[candidate]);
|
|
2917
3036
|
agentAvailability.set(candidate, available);
|
|
2918
3037
|
return available;
|
|
2919
3038
|
}
|
|
2920
|
-
async function resolveAgentChain(
|
|
3039
|
+
async function resolveAgentChain(preferred2) {
|
|
2921
3040
|
const availability = {};
|
|
2922
|
-
for (const candidate of ["claude", "codex", "
|
|
3041
|
+
for (const candidate of ["claude", "codex", "antigravity"]) {
|
|
2923
3042
|
availability[candidate] = await isAgentAvailable(candidate);
|
|
2924
3043
|
}
|
|
2925
|
-
return getAvailableAgentFallbackChain(
|
|
3044
|
+
return getAvailableAgentFallbackChain(preferred2, availability);
|
|
2926
3045
|
}
|
|
2927
3046
|
async function moveTaskToError(task, prefix, reason) {
|
|
2928
3047
|
try {
|
|
@@ -3011,7 +3130,7 @@ var watchCommand = new Command9("watch").description(
|
|
|
3011
3130
|
}
|
|
3012
3131
|
async function dispatchTask(task, repoDir) {
|
|
3013
3132
|
const sid = shortId(task.id);
|
|
3014
|
-
const slug =
|
|
3133
|
+
const slug = slugifyTitle(task.title) || sid;
|
|
3015
3134
|
const owner = ownerPrefix(task);
|
|
3016
3135
|
let branchName = taskBranchName(task);
|
|
3017
3136
|
const legacyBranchName = `mr/${sid}/${slug}`;
|
|
@@ -3339,6 +3458,15 @@ var watchCommand = new Command9("watch").description(
|
|
|
3339
3458
|
};
|
|
3340
3459
|
await launchAttempt(attemptOrder[attemptIndex]);
|
|
3341
3460
|
}
|
|
3461
|
+
function dispatchTaskSafely(task, repoDir) {
|
|
3462
|
+
void dispatchTask(task, repoDir).catch(async (err) => {
|
|
3463
|
+
const prefix = taskTag(shortId(task.id));
|
|
3464
|
+
const reason = `Task setup failed: ${err.message}`;
|
|
3465
|
+
logError(prefix, reason);
|
|
3466
|
+
queued.delete(task.id);
|
|
3467
|
+
await moveTaskToError(task, prefix, reason);
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3342
3470
|
async function dispatchPlanModeTask(task, repoDir) {
|
|
3343
3471
|
const sid = shortId(task.id);
|
|
3344
3472
|
const prefix = taskTag(sid);
|
|
@@ -3504,17 +3632,19 @@ var watchCommand = new Command9("watch").description(
|
|
|
3504
3632
|
if (refImagePaths.length > 0) {
|
|
3505
3633
|
logDispatch(prefix, `${refImagePaths.length} reference image(s) attached`);
|
|
3506
3634
|
}
|
|
3507
|
-
const validAgents = ["claude", "codex", "
|
|
3635
|
+
const validAgents = ["claude", "codex", "antigravity"];
|
|
3508
3636
|
const requested = Array.isArray(proto.selectedAgents) ? proto.selectedAgents.filter(
|
|
3509
3637
|
(a) => validAgents.includes(a)
|
|
3510
3638
|
) : [];
|
|
3511
3639
|
const dedupedRequested = Array.from(new Set(requested));
|
|
3512
3640
|
const key = `proto-${proto.id}`;
|
|
3513
3641
|
let parentFiles = [];
|
|
3642
|
+
let parentFidelity;
|
|
3514
3643
|
if (proto.parentId && proto.refinementFeedback) {
|
|
3515
3644
|
try {
|
|
3516
3645
|
const parent = await api.get(`/api/prototypes/${proto.parentId}`);
|
|
3517
3646
|
parentFiles = parent.files ?? [];
|
|
3647
|
+
parentFidelity = parent.fidelity;
|
|
3518
3648
|
} catch (err) {
|
|
3519
3649
|
logError(prefix, `Failed to fetch parent prototype: ${err.message}`);
|
|
3520
3650
|
}
|
|
@@ -3525,7 +3655,8 @@ var watchCommand = new Command9("watch").description(
|
|
|
3525
3655
|
variantStartIndex: startIndex,
|
|
3526
3656
|
variantsToProduce,
|
|
3527
3657
|
agentLabel,
|
|
3528
|
-
referenceImagePaths: refImagePaths
|
|
3658
|
+
referenceImagePaths: refImagePaths,
|
|
3659
|
+
parentFidelity
|
|
3529
3660
|
});
|
|
3530
3661
|
}
|
|
3531
3662
|
return buildPrototypePrompt(proto, sliceDir, {
|
|
@@ -3535,251 +3666,191 @@ var watchCommand = new Command9("watch").description(
|
|
|
3535
3666
|
referenceImagePaths: refImagePaths
|
|
3536
3667
|
});
|
|
3537
3668
|
};
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3669
|
+
const totalVariants = Math.max(1, proto.variantCount);
|
|
3670
|
+
let agentChainForVariant;
|
|
3671
|
+
if (dedupedRequested.length >= 1) {
|
|
3672
|
+
agentChainForVariant = (variantIndex) => [
|
|
3673
|
+
dedupedRequested[(variantIndex - 1) % dedupedRequested.length]
|
|
3674
|
+
];
|
|
3675
|
+
logDispatch(
|
|
3676
|
+
prefix,
|
|
3677
|
+
`streaming ${totalVariants} variant(s) across ${dedupedRequested.join(", ")}`
|
|
3678
|
+
);
|
|
3679
|
+
} else {
|
|
3680
|
+
const chain = await resolveAgentChain(agent);
|
|
3681
|
+
if (chain.length === 0) {
|
|
3682
|
+
logError(prefix, `No available agents found for ${agent}`);
|
|
3543
3683
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" }).catch(() => {
|
|
3544
3684
|
});
|
|
3545
3685
|
cleanupRefDir(refImageDir);
|
|
3546
3686
|
queued.delete(key);
|
|
3547
3687
|
return;
|
|
3548
3688
|
}
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
process: void 0,
|
|
3552
|
-
title: proto.title,
|
|
3553
|
-
repoDir,
|
|
3554
|
-
startedAt: Date.now(),
|
|
3555
|
-
lastActivityAt: Date.now()
|
|
3556
|
-
};
|
|
3557
|
-
let attemptIndex = 0;
|
|
3558
|
-
const launchAttempt = async (attemptAgent) => {
|
|
3559
|
-
let spawnFailureReason = null;
|
|
3560
|
-
const child = spawnAgent(
|
|
3561
|
-
attemptAgent,
|
|
3562
|
-
repoDir,
|
|
3563
|
-
prompt2,
|
|
3564
|
-
prefix,
|
|
3565
|
-
void 0,
|
|
3566
|
-
void 0,
|
|
3567
|
-
proto.title,
|
|
3568
|
-
false,
|
|
3569
|
-
(err) => {
|
|
3570
|
-
spawnFailureReason = err.message;
|
|
3571
|
-
}
|
|
3572
|
-
);
|
|
3573
|
-
activeEntry2.process = child;
|
|
3574
|
-
activeEntry2.currentAgent = attemptAgent;
|
|
3575
|
-
active.set(key, activeEntry2);
|
|
3576
|
-
child.on("exit", async (code) => {
|
|
3577
|
-
if (active.get(key)?.process === child) {
|
|
3578
|
-
active.delete(key);
|
|
3579
|
-
}
|
|
3580
|
-
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
3581
|
-
if (failedAttempt && !activeEntry2.terminatedForError) {
|
|
3582
|
-
const nextAgent = attemptOrder[attemptIndex + 1];
|
|
3583
|
-
if (nextAgent) {
|
|
3584
|
-
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
3585
|
-
logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying prototype generation with ${nextAgent}`);
|
|
3586
|
-
attemptIndex += 1;
|
|
3587
|
-
await launchAttempt(nextAgent);
|
|
3588
|
-
return;
|
|
3589
|
-
}
|
|
3590
|
-
}
|
|
3591
|
-
finishing.add(key);
|
|
3592
|
-
try {
|
|
3593
|
-
if (code === 0) {
|
|
3594
|
-
try {
|
|
3595
|
-
const protoPattern = /^prototype-\d+\.html$/;
|
|
3596
|
-
const found = readdirSync(repoDir).filter((f) => protoPattern.test(f)).sort();
|
|
3597
|
-
const files = found.map((f) => ({
|
|
3598
|
-
name: f,
|
|
3599
|
-
content: readFileSync5(resolve2(repoDir, f), "utf-8")
|
|
3600
|
-
}));
|
|
3601
|
-
if (files.length === 0) {
|
|
3602
|
-
logError(prefix, `No prototype HTML files found in ${repoDir}`);
|
|
3603
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
3604
|
-
return;
|
|
3605
|
-
}
|
|
3606
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "completed", files });
|
|
3607
|
-
logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${files.length} file(s)`);
|
|
3608
|
-
for (const file of files) {
|
|
3609
|
-
try {
|
|
3610
|
-
unlinkSync(resolve2(repoDir, file.name));
|
|
3611
|
-
} catch {
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
} catch (err) {
|
|
3615
|
-
logError(prefix, `Failed to upload prototype: ${err.message}`);
|
|
3616
|
-
try {
|
|
3617
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
3618
|
-
} catch {
|
|
3619
|
-
}
|
|
3620
|
-
}
|
|
3621
|
-
} else {
|
|
3622
|
-
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
3623
|
-
logError(prefix, `"${paint("bold", proto.title)}" prototype failed via ${attemptAgent} (${failureDetail})`);
|
|
3624
|
-
try {
|
|
3625
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
3626
|
-
} catch {
|
|
3627
|
-
}
|
|
3628
|
-
}
|
|
3629
|
-
} finally {
|
|
3630
|
-
cleanupRefDir(refImageDir);
|
|
3631
|
-
queued.delete(key);
|
|
3632
|
-
finishing.delete(key);
|
|
3633
|
-
}
|
|
3634
|
-
});
|
|
3635
|
-
};
|
|
3636
|
-
await launchAttempt(attemptOrder[attemptIndex]);
|
|
3637
|
-
return;
|
|
3638
|
-
}
|
|
3639
|
-
const totalVariants = proto.variantCount;
|
|
3640
|
-
const slices = [];
|
|
3641
|
-
const baseShare = Math.floor(totalVariants / dedupedRequested.length);
|
|
3642
|
-
const remainder = totalVariants % dedupedRequested.length;
|
|
3643
|
-
let cursor = 1;
|
|
3644
|
-
for (let i = 0; i < dedupedRequested.length; i++) {
|
|
3645
|
-
const a = dedupedRequested[i];
|
|
3646
|
-
const count = baseShare + (i < remainder ? 1 : 0);
|
|
3647
|
-
if (count <= 0) continue;
|
|
3648
|
-
const dir = resolve2(repoDir, `.mr-proto-${a}`);
|
|
3649
|
-
try {
|
|
3650
|
-
mkdirSync3(dir, { recursive: true });
|
|
3651
|
-
} catch {
|
|
3652
|
-
}
|
|
3653
|
-
for (const f of readdirSync(dir).filter((f2) => stalePattern.test(f2))) {
|
|
3654
|
-
try {
|
|
3655
|
-
unlinkSync(resolve2(dir, f));
|
|
3656
|
-
} catch {
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
slices.push({ agentLabel: a, startIndex: cursor, count, dir });
|
|
3660
|
-
cursor += count;
|
|
3689
|
+
agentChainForVariant = () => chain;
|
|
3690
|
+
logDispatch(prefix, `streaming ${totalVariants} variant(s) via ${chain[0]}`);
|
|
3661
3691
|
}
|
|
3662
|
-
if (slices.length === 0) {
|
|
3663
|
-
logError(prefix, `Could not assign any variants to selected agents`);
|
|
3664
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" }).catch(() => {
|
|
3665
|
-
});
|
|
3666
|
-
cleanupRefDir(refImageDir);
|
|
3667
|
-
queued.delete(key);
|
|
3668
|
-
return;
|
|
3669
|
-
}
|
|
3670
|
-
logDispatch(
|
|
3671
|
-
prefix,
|
|
3672
|
-
`multi-agent dispatch: ${slices.map((s) => `${s.agentLabel}\xD7${s.count}`).join(", ")}`
|
|
3673
|
-
);
|
|
3674
3692
|
const activeEntry = {
|
|
3675
3693
|
process: void 0,
|
|
3676
3694
|
title: proto.title,
|
|
3677
3695
|
repoDir,
|
|
3678
3696
|
startedAt: Date.now(),
|
|
3679
3697
|
lastActivityAt: Date.now(),
|
|
3680
|
-
outputBytes: 0
|
|
3698
|
+
outputBytes: 0,
|
|
3699
|
+
children: /* @__PURE__ */ new Set()
|
|
3681
3700
|
};
|
|
3682
3701
|
active.set(key, activeEntry);
|
|
3683
|
-
const
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3702
|
+
const protoPattern = /^prototype-\d+\.html$/;
|
|
3703
|
+
const runVariant = (variantIndex) => {
|
|
3704
|
+
const chain = agentChainForVariant(variantIndex);
|
|
3705
|
+
const sliceDir = resolve2(repoDir, `.mr-proto-v${variantIndex}`);
|
|
3706
|
+
try {
|
|
3707
|
+
mkdirSync3(sliceDir, { recursive: true });
|
|
3708
|
+
} catch {
|
|
3709
|
+
}
|
|
3710
|
+
for (const f of readdirSync(sliceDir).filter((f2) => stalePattern.test(f2))) {
|
|
3711
|
+
try {
|
|
3712
|
+
unlinkSync(resolve2(sliceDir, f));
|
|
3713
|
+
} catch {
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
const prompt2 = buildPromptForSlice(variantIndex, 1, chain[0], sliceDir);
|
|
3717
|
+
return new Promise((res) => {
|
|
3718
|
+
let attemptIndex = 0;
|
|
3719
|
+
const finishSlice = (ok) => {
|
|
3720
|
+
try {
|
|
3721
|
+
for (const f of readdirSync(sliceDir)) {
|
|
3722
|
+
try {
|
|
3723
|
+
unlinkSync(resolve2(sliceDir, f));
|
|
3724
|
+
} catch {
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
rmdirSync(sliceDir);
|
|
3728
|
+
} catch {
|
|
3729
|
+
}
|
|
3730
|
+
res(ok);
|
|
3731
|
+
};
|
|
3732
|
+
const launch = (attemptAgent) => {
|
|
3687
3733
|
let spawnFailureReason = null;
|
|
3688
3734
|
const child = spawnAgent(
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3735
|
+
attemptAgent,
|
|
3736
|
+
sliceDir,
|
|
3737
|
+
prompt2,
|
|
3692
3738
|
prefix,
|
|
3739
|
+
() => {
|
|
3740
|
+
activeEntry.lastActivityAt = Date.now();
|
|
3741
|
+
},
|
|
3693
3742
|
void 0,
|
|
3694
|
-
|
|
3695
|
-
`${proto.title} [${slice.agentLabel}]`,
|
|
3743
|
+
`${proto.title} [variant ${variantIndex}/${totalVariants}]`,
|
|
3696
3744
|
false,
|
|
3697
3745
|
(err) => {
|
|
3698
3746
|
spawnFailureReason = err.message;
|
|
3699
3747
|
}
|
|
3700
3748
|
);
|
|
3701
3749
|
activeEntry.process = child;
|
|
3702
|
-
activeEntry.currentAgent =
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3750
|
+
activeEntry.currentAgent = attemptAgent;
|
|
3751
|
+
activeEntry.children?.add(child);
|
|
3752
|
+
child.on("exit", async (code) => {
|
|
3753
|
+
activeEntry.children?.delete(child);
|
|
3754
|
+
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
3755
|
+
if (failedAttempt && !activeEntry.terminatedForError) {
|
|
3756
|
+
const nextAgent = chain[attemptIndex + 1];
|
|
3757
|
+
if (nextAgent) {
|
|
3758
|
+
const detail = spawnFailureReason ?? `exit code ${code}`;
|
|
3759
|
+
logWarn(prefix, `variant ${variantIndex}: ${attemptAgent} failed (${detail}) \u2014 retrying with ${nextAgent}`);
|
|
3760
|
+
attemptIndex += 1;
|
|
3761
|
+
launch(nextAgent);
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
if (activeEntry.terminatedForError) {
|
|
3766
|
+
finishSlice(false);
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
if (code !== 0) {
|
|
3770
|
+
const detail = spawnFailureReason ?? `exit code ${code}`;
|
|
3771
|
+
logError(prefix, `variant ${variantIndex} failed via ${attemptAgent} (${detail})`);
|
|
3772
|
+
finishSlice(false);
|
|
3773
|
+
return;
|
|
3774
|
+
}
|
|
3775
|
+
try {
|
|
3776
|
+
const found = readdirSync(sliceDir).filter((f) => protoPattern.test(f)).sort();
|
|
3777
|
+
if (found.length === 0) {
|
|
3778
|
+
logError(prefix, `variant ${variantIndex}: no HTML file produced`);
|
|
3779
|
+
finishSlice(false);
|
|
3780
|
+
return;
|
|
3781
|
+
}
|
|
3782
|
+
const content = readFileSync5(resolve2(sliceDir, found[0]), "utf-8");
|
|
3783
|
+
const file = {
|
|
3784
|
+
name: `prototype-${variantIndex}.html`,
|
|
3785
|
+
content,
|
|
3786
|
+
agent: attemptAgent
|
|
3787
|
+
};
|
|
3788
|
+
await api.post(`/api/prototypes/${proto.id}/variants`, { file });
|
|
3789
|
+
logSuccess(prefix, `variant ${variantIndex}/${totalVariants} ready ${paint("gray", `(${attemptAgent})`)}`);
|
|
3790
|
+
finishSlice(true);
|
|
3791
|
+
} catch (err) {
|
|
3792
|
+
logError(prefix, `variant ${variantIndex}: failed to upload (${err.message})`);
|
|
3793
|
+
finishSlice(false);
|
|
3708
3794
|
}
|
|
3709
|
-
res({ agent: slice.agentLabel, ok, reason: spawnFailureReason });
|
|
3710
3795
|
});
|
|
3711
|
-
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3796
|
+
};
|
|
3797
|
+
launch(chain[attemptIndex]);
|
|
3798
|
+
});
|
|
3799
|
+
};
|
|
3800
|
+
const variantIndices = Array.from({ length: totalVariants }, (_, i) => i + 1);
|
|
3801
|
+
const VARIANT_CONCURRENCY = Math.min(3, totalVariants);
|
|
3802
|
+
let nextVariant = 0;
|
|
3803
|
+
let okCount = 0;
|
|
3804
|
+
const workers = Array.from({ length: VARIANT_CONCURRENCY }, async () => {
|
|
3805
|
+
while (true) {
|
|
3806
|
+
if (activeEntry.terminatedForError) return;
|
|
3807
|
+
const idx = nextVariant++;
|
|
3808
|
+
if (idx >= variantIndices.length) return;
|
|
3809
|
+
const ok = await runVariant(variantIndices[idx]);
|
|
3810
|
+
if (ok) okCount++;
|
|
3811
|
+
}
|
|
3812
|
+
});
|
|
3813
|
+
await Promise.all(workers);
|
|
3714
3814
|
if (active.get(key) === activeEntry) {
|
|
3715
3815
|
active.delete(key);
|
|
3716
3816
|
}
|
|
3817
|
+
if (activeEntry.terminatedForError) {
|
|
3818
|
+
cleanupRefDir(refImageDir);
|
|
3819
|
+
queued.delete(key);
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3717
3822
|
finishing.add(key);
|
|
3718
3823
|
try {
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
for (const slice of slices) {
|
|
3722
|
-
let found = [];
|
|
3723
|
-
try {
|
|
3724
|
-
found = readdirSync(slice.dir).filter((f) => protoPattern.test(f)).sort();
|
|
3725
|
-
} catch {
|
|
3726
|
-
}
|
|
3727
|
-
for (const f of found) {
|
|
3728
|
-
try {
|
|
3729
|
-
const content = readFileSync5(resolve2(slice.dir, f), "utf-8");
|
|
3730
|
-
collected.push({ name: f, content });
|
|
3731
|
-
} catch (err) {
|
|
3732
|
-
logError(prefix, `Failed reading ${f} from ${slice.dir}: ${err.message}`);
|
|
3733
|
-
}
|
|
3734
|
-
}
|
|
3735
|
-
}
|
|
3736
|
-
collected.sort((a, b) => {
|
|
3737
|
-
const na = parseInt(a.name.match(/\d+/)?.[0] ?? "0", 10);
|
|
3738
|
-
const nb = parseInt(b.name.match(/\d+/)?.[0] ?? "0", 10);
|
|
3739
|
-
return na - nb;
|
|
3740
|
-
});
|
|
3741
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3742
|
-
const finalFiles = [];
|
|
3743
|
-
let renumberCursor = 1;
|
|
3744
|
-
for (const f of collected) {
|
|
3745
|
-
let name = f.name;
|
|
3746
|
-
if (seen.has(name)) {
|
|
3747
|
-
while (seen.has(`prototype-${renumberCursor}.html`)) renumberCursor++;
|
|
3748
|
-
name = `prototype-${renumberCursor}.html`;
|
|
3749
|
-
}
|
|
3750
|
-
seen.add(name);
|
|
3751
|
-
finalFiles.push({ name, content: f.content });
|
|
3752
|
-
}
|
|
3753
|
-
const allOk = sliceResults.every((r) => r.ok);
|
|
3754
|
-
if (finalFiles.length === 0) {
|
|
3755
|
-
logError(prefix, `No prototype HTML files produced by any agent`);
|
|
3824
|
+
if (okCount === 0) {
|
|
3825
|
+
logError(prefix, `No prototype variants were produced`);
|
|
3756
3826
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
3757
3827
|
} else {
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3828
|
+
let finalFiles = [];
|
|
3829
|
+
try {
|
|
3830
|
+
const current = await api.get(`/api/prototypes/${proto.id}`);
|
|
3831
|
+
finalFiles = (current.files ?? []).slice();
|
|
3832
|
+
} catch (err) {
|
|
3833
|
+
logWarn(prefix, `Could not re-read files before completing: ${err.message}`);
|
|
3834
|
+
}
|
|
3835
|
+
finalFiles.sort((a, b) => {
|
|
3836
|
+
const na = parseInt(a.name.match(/\d+/)?.[0] ?? "0", 10);
|
|
3837
|
+
const nb = parseInt(b.name.match(/\d+/)?.[0] ?? "0", 10);
|
|
3838
|
+
return na - nb;
|
|
3761
3839
|
});
|
|
3762
|
-
|
|
3763
|
-
|
|
3840
|
+
const patch = {
|
|
3841
|
+
status: "completed",
|
|
3842
|
+
variantModels: finalFiles.map((f) => f.agent ?? null)
|
|
3843
|
+
};
|
|
3844
|
+
if (finalFiles.length > 0) patch.files = finalFiles;
|
|
3845
|
+
await api.patch(`/api/prototypes/${proto.id}`, patch);
|
|
3846
|
+
if (okCount === totalVariants) {
|
|
3847
|
+
logSuccess(prefix, `"${paint("bold", proto.title)}" completed \u2014 ${okCount} variant(s) streamed in`);
|
|
3764
3848
|
} else {
|
|
3765
|
-
|
|
3766
|
-
logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${finalFiles.length} file(s) (partial \u2014 ${failed2} failed)`);
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
for (const slice of slices) {
|
|
3770
|
-
try {
|
|
3771
|
-
for (const f of readdirSync(slice.dir)) {
|
|
3772
|
-
try {
|
|
3773
|
-
unlinkSync(resolve2(slice.dir, f));
|
|
3774
|
-
} catch {
|
|
3775
|
-
}
|
|
3776
|
-
}
|
|
3777
|
-
rmdirSync(slice.dir);
|
|
3778
|
-
} catch {
|
|
3849
|
+
logSuccess(prefix, `"${paint("bold", proto.title)}" completed \u2014 ${okCount}/${totalVariants} variant(s) (partial)`);
|
|
3779
3850
|
}
|
|
3780
3851
|
}
|
|
3781
3852
|
} catch (err) {
|
|
3782
|
-
logError(prefix, `Failed to
|
|
3853
|
+
logError(prefix, `Failed to finalize prototype: ${err.message}`);
|
|
3783
3854
|
try {
|
|
3784
3855
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
3785
3856
|
} catch {
|
|
@@ -4013,7 +4084,7 @@ ${divider}`);
|
|
|
4013
4084
|
`${paint("cyan", "?")} Approve task "${paint("bold", task.title)}"? [y/N] `
|
|
4014
4085
|
);
|
|
4015
4086
|
if (approved) {
|
|
4016
|
-
|
|
4087
|
+
dispatchTaskSafely(task, repoDir);
|
|
4017
4088
|
} else {
|
|
4018
4089
|
logWarn(prefix, `"${paint("bold", task.title)}" denied \u2014 will not retry`);
|
|
4019
4090
|
failed.set(task.id, "denied by user");
|
|
@@ -4176,7 +4247,7 @@ ${divider}`);
|
|
|
4176
4247
|
approvalQueue.push({ task, repoDir });
|
|
4177
4248
|
processApprovalQueue();
|
|
4178
4249
|
} else {
|
|
4179
|
-
|
|
4250
|
+
dispatchTaskSafely(task, repoDir);
|
|
4180
4251
|
}
|
|
4181
4252
|
}
|
|
4182
4253
|
let prototypes = [];
|
|
@@ -4190,7 +4261,11 @@ ${divider}`);
|
|
|
4190
4261
|
if (key.startsWith("proto-") && !inProgressProtoKeys.has(key)) {
|
|
4191
4262
|
logWarn(watchTag(), `Prototype ${paint("yellow", key)} no longer in_progress, terminating\u2026`);
|
|
4192
4263
|
entry.terminatedForError = true;
|
|
4193
|
-
entry.
|
|
4264
|
+
if (entry.children && entry.children.size > 0) {
|
|
4265
|
+
for (const child of entry.children) child.kill("SIGTERM");
|
|
4266
|
+
} else {
|
|
4267
|
+
entry.process.kill("SIGTERM");
|
|
4268
|
+
}
|
|
4194
4269
|
active.delete(key);
|
|
4195
4270
|
queued.delete(key);
|
|
4196
4271
|
}
|
|
@@ -4582,8 +4657,9 @@ ${summaryBlock}${lines.join("\n")}`;
|
|
|
4582
4657
|
}
|
|
4583
4658
|
try {
|
|
4584
4659
|
await api.patch(`/api/reviews/${review.id}`, { autoMergeStatus: "merged" });
|
|
4585
|
-
await
|
|
4586
|
-
|
|
4660
|
+
await api.patch(`/api/tasks/${taskId}`, { status: "completed" });
|
|
4661
|
+
await postTaskUpdate(taskId, `Auto-merge complete \u2014 sanity review passed, ${prLabel} merged, and task moved to Done`, "system");
|
|
4662
|
+
logSuccess(prefix, `Auto-merge complete for ${prLabel} \u2014 task moved to Done`);
|
|
4587
4663
|
} catch (err) {
|
|
4588
4664
|
logError(prefix, `Failed to record auto-merge success: ${err.message}`);
|
|
4589
4665
|
}
|
|
@@ -4838,23 +4914,35 @@ var prototypeCommand = new Command15("prototype").description("Manage prototypes
|
|
|
4838
4914
|
for (const p of prototypes) {
|
|
4839
4915
|
const date = new Date(p.createdAt).toLocaleDateString();
|
|
4840
4916
|
const typeLabel2 = typeLabels[p.prototypeType] ?? p.prototypeType ?? "web";
|
|
4917
|
+
const fidelityLabel = p.fidelity === "low" ? "wireframe" : "hi-fi";
|
|
4841
4918
|
console.log(
|
|
4842
|
-
` ${paint4("bold", p.title)} ${statusBadge(p.status)} ${paint4("blue", `[${typeLabel2}]`)} ${paint4("gray", p.id.slice(0, 8))} ${paint4("dim", date)}`
|
|
4919
|
+
` ${paint4("bold", p.title)} ${statusBadge(p.status)} ${paint4("blue", `[${typeLabel2}/${fidelityLabel}]`)} ${paint4("gray", p.id.slice(0, 8))} ${paint4("dim", date)}`
|
|
4843
4920
|
);
|
|
4844
4921
|
console.log(` ${paint4("dim", p.prompt.slice(0, 80) + (p.prompt.length > 80 ? "\u2026" : ""))}`);
|
|
4845
4922
|
console.log();
|
|
4846
4923
|
}
|
|
4847
4924
|
})
|
|
4848
4925
|
).addCommand(
|
|
4849
|
-
new Command15("create").description("Create a new prototype").argument("<title>", "Title of the prototype").requiredOption("--prompt <prompt>", "Design description / prompt").option("--project <projectId>", "Project ID (defaults to linked project, when available)").option("--variants <count>", "Number of variants to generate (1-50)", "5").option("--type <type>", "Prototype type: web_app, mobile_app, desktop_app, logo (default: web_app)", "web_app").action(async (title, opts) => {
|
|
4926
|
+
new Command15("create").description("Create a new prototype").argument("<title>", "Title of the prototype").requiredOption("--prompt <prompt>", "Design description / prompt").option("--project <projectId>", "Project ID (defaults to linked project, when available)").option("--variants <count>", "Number of variants to generate (1-50)", "5").option("--type <type>", "Prototype type: web_app, mobile_app, desktop_app, logo (default: web_app)", "web_app").option("--fidelity <fidelity>", "Fidelity level: low, high (default: high)", "high").action(async (title, opts) => {
|
|
4850
4927
|
const projectId = opts.project ?? getLinkedProjectId();
|
|
4851
4928
|
const variantCount = Math.max(1, Math.min(50, parseInt(opts.variants, 10) || 5));
|
|
4852
4929
|
const validTypes = ["web_app", "mobile_app", "desktop_app", "logo"];
|
|
4853
4930
|
const prototypeType = validTypes.includes(opts.type) ? opts.type : "web_app";
|
|
4931
|
+
const validFidelities = ["low", "high"];
|
|
4932
|
+
if (opts.fidelity && !validFidelities.includes(opts.fidelity)) {
|
|
4933
|
+
console.error(`Invalid fidelity value: ${opts.fidelity}. Supported values: ${validFidelities.join(", ")}`);
|
|
4934
|
+
process.exit(1);
|
|
4935
|
+
}
|
|
4936
|
+
const fidelity = opts.fidelity || "high";
|
|
4937
|
+
if (prototypeType === "logo" && fidelity === "low") {
|
|
4938
|
+
console.error("Logo prototypes do not support low fidelity.");
|
|
4939
|
+
process.exit(1);
|
|
4940
|
+
}
|
|
4854
4941
|
const prototype = await api.post("/api/prototypes", {
|
|
4855
4942
|
title,
|
|
4856
4943
|
prompt: opts.prompt,
|
|
4857
4944
|
prototypeType,
|
|
4945
|
+
fidelity,
|
|
4858
4946
|
variantCount,
|
|
4859
4947
|
projectId: projectId ?? null
|
|
4860
4948
|
});
|
|
@@ -4868,6 +4956,7 @@ var prototypeCommand = new Command15("prototype").description("Manage prototypes
|
|
|
4868
4956
|
console.log(` ${paint4("green", "\u2713")} Created prototype: ${paint4("bold", prototype.title)}`);
|
|
4869
4957
|
console.log(` ${paint4("gray", "ID:")} ${prototype.id}`);
|
|
4870
4958
|
console.log(` ${paint4("gray", "Type:")} ${typeLabels[prototype.prototypeType] ?? prototype.prototypeType}`);
|
|
4959
|
+
console.log(` ${paint4("gray", "Fidelity:")} ${prototype.fidelity === "low" ? "Low (wireframe)" : "High (hi-fi)"}`);
|
|
4871
4960
|
if (!prototype.projectId) {
|
|
4872
4961
|
console.log(` ${paint4("gray", "Project:")} none (will generate in the active watch directory)`);
|
|
4873
4962
|
}
|
|
@@ -5045,30 +5134,30 @@ async function checkCodexAuth() {
|
|
|
5045
5134
|
fix: hasKey ? void 0 : "Set OPENAI_API_KEY or CODEX_API_KEY environment variable"
|
|
5046
5135
|
};
|
|
5047
5136
|
}
|
|
5048
|
-
async function
|
|
5049
|
-
const exists = await commandExists2("
|
|
5137
|
+
async function checkAntigravityInstalled() {
|
|
5138
|
+
const exists = await commandExists2("agy");
|
|
5050
5139
|
return {
|
|
5051
|
-
name: "
|
|
5140
|
+
name: "Antigravity CLI (agy)",
|
|
5052
5141
|
ok: exists,
|
|
5053
5142
|
message: exists ? "installed" : "not found",
|
|
5054
|
-
fix: exists ? void 0 : "Install:
|
|
5143
|
+
fix: exists ? void 0 : "Install: curl -fsSL https://antigravity.google/cli/install.sh | bash"
|
|
5055
5144
|
};
|
|
5056
5145
|
}
|
|
5057
|
-
async function
|
|
5058
|
-
const exists = await commandExists2("
|
|
5146
|
+
async function checkAntigravityAuth() {
|
|
5147
|
+
const exists = await commandExists2("agy");
|
|
5059
5148
|
if (!exists) {
|
|
5060
5149
|
return {
|
|
5061
|
-
name: "
|
|
5150
|
+
name: "Antigravity CLI auth",
|
|
5062
5151
|
ok: false,
|
|
5063
|
-
message: "skipped (
|
|
5152
|
+
message: "skipped (agy not installed)"
|
|
5064
5153
|
};
|
|
5065
5154
|
}
|
|
5066
|
-
const hasKey = !!process.env.
|
|
5155
|
+
const hasKey = !!process.env.ANTIGRAVITY_API_KEY;
|
|
5067
5156
|
return {
|
|
5068
|
-
name: "
|
|
5157
|
+
name: "Antigravity CLI auth",
|
|
5069
5158
|
ok: hasKey,
|
|
5070
5159
|
message: hasKey ? "API key configured" : "no API key found",
|
|
5071
|
-
fix: hasKey ? void 0 : "Set
|
|
5160
|
+
fix: hasKey ? void 0 : "Set ANTIGRAVITY_API_KEY environment variable, or run `agy` once to sign in with Google"
|
|
5072
5161
|
};
|
|
5073
5162
|
}
|
|
5074
5163
|
async function checkGlabInstalled() {
|
|
@@ -5168,7 +5257,7 @@ function printResults(checks) {
|
|
|
5168
5257
|
return allOk;
|
|
5169
5258
|
}
|
|
5170
5259
|
async function autoFix(checks, agent) {
|
|
5171
|
-
const { spawn:
|
|
5260
|
+
const { spawn: spawn9 } = await import("child_process");
|
|
5172
5261
|
const ghInstalled = checks.find((c14) => c14.name === "GitHub CLI (gh)").ok;
|
|
5173
5262
|
const ghAuthed = checks.find((c14) => c14.name === "GitHub CLI auth").ok;
|
|
5174
5263
|
const mrAuthed = checks.find((c14) => c14.name === "Mr. Manager CLI auth").ok;
|
|
@@ -5177,7 +5266,7 @@ async function autoFix(checks, agent) {
|
|
|
5177
5266
|
console.log(paint5("cyan", " Installing Claude Code..."));
|
|
5178
5267
|
console.log(paint5("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
|
|
5179
5268
|
await new Promise((resolve9) => {
|
|
5180
|
-
const child =
|
|
5269
|
+
const child = spawn9("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], { stdio: "inherit" });
|
|
5181
5270
|
child.on("exit", () => resolve9());
|
|
5182
5271
|
});
|
|
5183
5272
|
console.log("");
|
|
@@ -5185,7 +5274,7 @@ async function autoFix(checks, agent) {
|
|
|
5185
5274
|
if (ghInstalled && !ghAuthed) {
|
|
5186
5275
|
console.log(paint5("cyan", " Running gh auth login..."));
|
|
5187
5276
|
await new Promise((resolve9) => {
|
|
5188
|
-
const child =
|
|
5277
|
+
const child = spawn9("gh", ["auth", "login"], { stdio: "inherit" });
|
|
5189
5278
|
child.on("exit", () => resolve9());
|
|
5190
5279
|
});
|
|
5191
5280
|
console.log("");
|
|
@@ -5194,14 +5283,14 @@ async function autoFix(checks, agent) {
|
|
|
5194
5283
|
console.log(paint5("cyan", " Running mr login..."));
|
|
5195
5284
|
const entry = process.argv[1];
|
|
5196
5285
|
await new Promise((resolve9) => {
|
|
5197
|
-
const child =
|
|
5286
|
+
const child = spawn9(process.execPath, [entry, "login"], { stdio: "inherit" });
|
|
5198
5287
|
child.on("exit", () => resolve9());
|
|
5199
5288
|
});
|
|
5200
5289
|
console.log("");
|
|
5201
5290
|
}
|
|
5202
5291
|
}
|
|
5203
|
-
var setupCommand = new Command16("setup").description("Check that all dependencies for mr watch are installed and configured").option("--fix", "Attempt to auto-fix issues where possible", false).option("--agent <agent>", "AI agent to check: claude, codex, or
|
|
5204
|
-
const agent = opts.agent === "codex" ? "codex" : opts.agent === "
|
|
5292
|
+
var setupCommand = new Command16("setup").description("Check that all dependencies for mr watch are installed and configured").option("--fix", "Attempt to auto-fix issues where possible", false).option("--agent <agent>", "AI agent to check: claude, codex, or antigravity (default: claude)", "claude").action(async (opts) => {
|
|
5293
|
+
const agent = opts.agent === "codex" ? "codex" : opts.agent === "antigravity" ? "antigravity" : "claude";
|
|
5205
5294
|
const banner = [
|
|
5206
5295
|
``,
|
|
5207
5296
|
paint5("cyan", ` \u2554\u2566\u2557\u2551\u2550\u2557 \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2566\u2557\u2551 \u2551\u2554\u2550\u2557`),
|
|
@@ -5212,7 +5301,7 @@ var setupCommand = new Command16("setup").description("Check that all dependenci
|
|
|
5212
5301
|
``
|
|
5213
5302
|
].join("\n");
|
|
5214
5303
|
console.log(banner);
|
|
5215
|
-
const agentChecks = agent === "codex" ? [checkCodexInstalled(), checkCodexAuth()] : agent === "
|
|
5304
|
+
const agentChecks = agent === "codex" ? [checkCodexInstalled(), checkCodexAuth()] : agent === "antigravity" ? [checkAntigravityInstalled(), checkAntigravityAuth()] : [checkClaudeInstalled(), checkClaudeAuth()];
|
|
5216
5305
|
const checks = await Promise.all([
|
|
5217
5306
|
checkGitInstalled(),
|
|
5218
5307
|
checkNodeVersion(),
|
|
@@ -5230,7 +5319,7 @@ var setupCommand = new Command16("setup").description("Check that all dependenci
|
|
|
5230
5319
|
if (allOk) {
|
|
5231
5320
|
console.log(paint5("green", " All checks passed! You're ready to run mr watch."));
|
|
5232
5321
|
if (agent === "claude") {
|
|
5233
|
-
console.log(paint5("dim", " Tip: check other agents with --agent codex or --agent
|
|
5322
|
+
console.log(paint5("dim", " Tip: check other agents with --agent codex or --agent antigravity"));
|
|
5234
5323
|
}
|
|
5235
5324
|
console.log("");
|
|
5236
5325
|
return;
|
|
@@ -5980,13 +6069,92 @@ var noMrCommand = new Command24("no-mr").description("Signal that a task does no
|
|
|
5980
6069
|
|
|
5981
6070
|
// cli/commands/review.ts
|
|
5982
6071
|
import { Command as Command26 } from "commander";
|
|
5983
|
-
import {
|
|
6072
|
+
import { execSync as execSync6 } from "child_process";
|
|
5984
6073
|
import { existsSync as existsSync14, statSync as statSync2 } from "fs";
|
|
5985
6074
|
|
|
5986
6075
|
// cli/commands/review-apply.ts
|
|
5987
6076
|
import { Command as Command25 } from "commander";
|
|
5988
|
-
import {
|
|
6077
|
+
import { execSync as execSync5 } from "child_process";
|
|
5989
6078
|
import { existsSync as existsSync13 } from "fs";
|
|
6079
|
+
|
|
6080
|
+
// lib/review/agent.ts
|
|
6081
|
+
import { spawn as spawn7, exec as exec3 } from "child_process";
|
|
6082
|
+
var AGENT_BINARIES2 = {
|
|
6083
|
+
claude: "claude",
|
|
6084
|
+
codex: "codex",
|
|
6085
|
+
antigravity: "agy"
|
|
6086
|
+
};
|
|
6087
|
+
function commandExists3(cmd) {
|
|
6088
|
+
return new Promise((resolve9) => {
|
|
6089
|
+
exec3(`command -v ${cmd}`, (err) => resolve9(!err));
|
|
6090
|
+
});
|
|
6091
|
+
}
|
|
6092
|
+
async function resolveReviewAgentChain(preferred2 = "claude") {
|
|
6093
|
+
const availability = {};
|
|
6094
|
+
for (const candidate of ["claude", "codex", "antigravity"]) {
|
|
6095
|
+
availability[candidate] = await commandExists3(AGENT_BINARIES2[candidate]);
|
|
6096
|
+
}
|
|
6097
|
+
return getAvailableAgentFallbackChain(preferred2, availability);
|
|
6098
|
+
}
|
|
6099
|
+
function buildArgs(agent, prompt2) {
|
|
6100
|
+
if (agent === "codex") {
|
|
6101
|
+
return ["-a", "never", "exec", "-s", "danger-full-access", prompt2];
|
|
6102
|
+
}
|
|
6103
|
+
if (agent === "antigravity") {
|
|
6104
|
+
return ["-p", prompt2, "--dangerously-skip-permissions"];
|
|
6105
|
+
}
|
|
6106
|
+
return ["-p", "--dangerously-skip-permissions", prompt2];
|
|
6107
|
+
}
|
|
6108
|
+
function runOnce(agent, prompt2, opts) {
|
|
6109
|
+
return new Promise((resolve9) => {
|
|
6110
|
+
const child = spawn7(AGENT_BINARIES2[agent], buildArgs(agent, prompt2), {
|
|
6111
|
+
cwd: opts.cwd,
|
|
6112
|
+
stdio: opts.capture ? ["ignore", "pipe", "pipe"] : ["ignore", "inherit", "inherit"]
|
|
6113
|
+
});
|
|
6114
|
+
let output = "";
|
|
6115
|
+
if (opts.capture) {
|
|
6116
|
+
child.stdout?.on("data", (d) => {
|
|
6117
|
+
output += d.toString();
|
|
6118
|
+
});
|
|
6119
|
+
child.stderr?.on("data", () => {
|
|
6120
|
+
});
|
|
6121
|
+
}
|
|
6122
|
+
child.on("error", () => {
|
|
6123
|
+
resolve9({ ok: false, output, spawnError: true });
|
|
6124
|
+
});
|
|
6125
|
+
child.on("exit", (code) => {
|
|
6126
|
+
resolve9({ ok: code === 0, output: output.trim(), spawnError: false });
|
|
6127
|
+
});
|
|
6128
|
+
});
|
|
6129
|
+
}
|
|
6130
|
+
async function runAgentCaptured(prompt2, opts) {
|
|
6131
|
+
if (opts.chain.length === 0) {
|
|
6132
|
+
throw new Error("No available agents (need one of: claude, codex, agy)");
|
|
6133
|
+
}
|
|
6134
|
+
let lastError = "";
|
|
6135
|
+
for (const agent of opts.chain) {
|
|
6136
|
+
const result = await runOnce(agent, prompt2, { cwd: opts.cwd, capture: true });
|
|
6137
|
+
if (result.ok && result.output) {
|
|
6138
|
+
return { output: result.output, agent };
|
|
6139
|
+
}
|
|
6140
|
+
lastError = result.spawnError ? `${agent} could not be spawned` : `${agent} exited without usable output`;
|
|
6141
|
+
}
|
|
6142
|
+
throw new Error(`All agents failed (${lastError})`);
|
|
6143
|
+
}
|
|
6144
|
+
async function runAgentInteractive(prompt2, opts) {
|
|
6145
|
+
if (opts.chain.length === 0) {
|
|
6146
|
+
throw new Error("No available agents (need one of: claude, codex, agy)");
|
|
6147
|
+
}
|
|
6148
|
+
let lastError = "";
|
|
6149
|
+
for (const agent of opts.chain) {
|
|
6150
|
+
const result = await runOnce(agent, prompt2, { cwd: opts.cwd, capture: false });
|
|
6151
|
+
if (result.ok) return { agent };
|
|
6152
|
+
lastError = result.spawnError ? `${agent} could not be spawned` : `${agent} exited non-zero`;
|
|
6153
|
+
}
|
|
6154
|
+
throw new Error(`All agents failed (${lastError})`);
|
|
6155
|
+
}
|
|
6156
|
+
|
|
6157
|
+
// cli/commands/review-apply.ts
|
|
5990
6158
|
var c8 = {
|
|
5991
6159
|
reset: "\x1B[0m",
|
|
5992
6160
|
dim: "\x1B[2m",
|
|
@@ -6016,7 +6184,7 @@ function logOk(msg) {
|
|
|
6016
6184
|
function logErr(msg) {
|
|
6017
6185
|
console.error(`${timestamp2()} ${tag()} ${paint8("red", "\u2717")} ${msg}`);
|
|
6018
6186
|
}
|
|
6019
|
-
var reviewApplyCommand = new Command25("apply").description("Apply review comments and findings using the Claude agent").argument("<id>", "Code review ID (the one shown in the Reviews UI)").option("--no-push", "Skip pushing the fix commit to the remote").option("--no-commit", "Apply changes but don't commit (for dry-run review)").action(async (id, opts) => {
|
|
6187
|
+
var reviewApplyCommand = new Command25("apply").description("Apply review comments and findings using the Claude agent").argument("<id>", "Code review ID (the one shown in the Reviews UI)").option("--no-push", "Skip pushing the fix commit to the remote").option("--no-commit", "Apply changes but don't commit (for dry-run review)").option("--in-place", "Apply edits directly in the linked checkout instead of an isolated worktree (legacy, unsafe)").action(async (id, opts) => {
|
|
6020
6188
|
const config = loadConfig();
|
|
6021
6189
|
if (!config.apiKey) {
|
|
6022
6190
|
logErr('Not authenticated. Run "mr login" first.');
|
|
@@ -6037,9 +6205,12 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6037
6205
|
process.exit(1);
|
|
6038
6206
|
}
|
|
6039
6207
|
const actionableComments = comments.filter((c14) => !excluded.has(c14.file));
|
|
6040
|
-
const
|
|
6208
|
+
const dismissedCount = findings.filter((f) => f.status === "dismissed").length;
|
|
6209
|
+
const actionableFindings = findings.filter(
|
|
6210
|
+
(f) => !excluded.has(f.file) && f.status !== "dismissed"
|
|
6211
|
+
);
|
|
6041
6212
|
if (actionableComments.length === 0 && actionableFindings.length === 0) {
|
|
6042
|
-
logErr("
|
|
6213
|
+
logErr("No selected comments or findings to act on (all excluded or dismissed).");
|
|
6043
6214
|
process.exit(1);
|
|
6044
6215
|
}
|
|
6045
6216
|
let project;
|
|
@@ -6064,19 +6235,59 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6064
6235
|
logErr(`Set the project's localPath or run from inside the repo.`);
|
|
6065
6236
|
process.exit(1);
|
|
6066
6237
|
}
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
}
|
|
6071
|
-
try {
|
|
6072
|
-
execSync5(`git checkout ${review.branch}`, { cwd: projectPath, stdio: "pipe" });
|
|
6073
|
-
} catch (err) {
|
|
6074
|
-
logErr(`Could not checkout branch ${review.branch}: ${err.message}`);
|
|
6238
|
+
const chain = await resolveReviewAgentChain("claude");
|
|
6239
|
+
if (chain.length === 0) {
|
|
6240
|
+
logErr("No available agent found (need one of: claude, codex, agy)");
|
|
6075
6241
|
process.exit(1);
|
|
6076
6242
|
}
|
|
6243
|
+
const touchedFiles = Array.from(
|
|
6244
|
+
new Set([
|
|
6245
|
+
...actionableComments.map((c14) => c14.file),
|
|
6246
|
+
...actionableFindings.map((f) => f.file)
|
|
6247
|
+
].filter(Boolean))
|
|
6248
|
+
);
|
|
6249
|
+
let workDir = projectPath;
|
|
6250
|
+
let cleanup = () => {
|
|
6251
|
+
};
|
|
6252
|
+
if (opts.inPlace) {
|
|
6253
|
+
try {
|
|
6254
|
+
execSync5(`git fetch origin ${review.branch}`, { cwd: projectPath, stdio: "ignore" });
|
|
6255
|
+
} catch {
|
|
6256
|
+
}
|
|
6257
|
+
try {
|
|
6258
|
+
execSync5(`git checkout ${review.branch}`, { cwd: projectPath, stdio: "pipe" });
|
|
6259
|
+
} catch (err) {
|
|
6260
|
+
logErr(`Could not checkout branch ${review.branch}: ${err.message}`);
|
|
6261
|
+
process.exit(1);
|
|
6262
|
+
}
|
|
6263
|
+
} else {
|
|
6264
|
+
try {
|
|
6265
|
+
const wt = createWorktree(projectPath, review.branch, `review-apply-${id}`, {
|
|
6266
|
+
syncRemoteBranch: true
|
|
6267
|
+
});
|
|
6268
|
+
workDir = wt.path;
|
|
6269
|
+
} catch (err) {
|
|
6270
|
+
logErr(`Could not create worktree for ${review.branch}: ${err.message}`);
|
|
6271
|
+
process.exit(1);
|
|
6272
|
+
}
|
|
6273
|
+
cleanup = () => removeWorktree(projectPath, workDir);
|
|
6274
|
+
}
|
|
6275
|
+
const failAndExit = async (message, extra = {}) => {
|
|
6276
|
+
logErr(message);
|
|
6277
|
+
try {
|
|
6278
|
+
await api.patch(`/api/reviews/${id}`, { fixStatus: "failed", fixErrorMessage: message, ...extra });
|
|
6279
|
+
} catch {
|
|
6280
|
+
}
|
|
6281
|
+
cleanup();
|
|
6282
|
+
process.exit(1);
|
|
6283
|
+
};
|
|
6077
6284
|
log(`Project: ${paint8("cyan", project.name)}`);
|
|
6078
6285
|
log(`Branch: ${paint8("cyan", review.branch)}`);
|
|
6286
|
+
log(`Workdir: ${paint8("dim", opts.inPlace ? `${workDir} (in-place)` : workDir)}`);
|
|
6079
6287
|
log(`Comments: ${paint8("yellow", String(actionableComments.length))}, findings: ${paint8("yellow", String(actionableFindings.length))}`);
|
|
6288
|
+
if (dismissedCount > 0) {
|
|
6289
|
+
log(`Dismissed findings skipped: ${paint8("dim", String(dismissedCount))}`);
|
|
6290
|
+
}
|
|
6080
6291
|
if (excluded.size > 0) {
|
|
6081
6292
|
log(`Excluded files: ${paint8("dim", [...excluded].join(", "))}`);
|
|
6082
6293
|
}
|
|
@@ -6091,19 +6302,14 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6091
6302
|
findings: actionableFindings,
|
|
6092
6303
|
excluded: [...excluded]
|
|
6093
6304
|
});
|
|
6094
|
-
log(
|
|
6305
|
+
log(`Asking ${paint8("cyan", chain[0])} to apply the changes\u2026`);
|
|
6095
6306
|
try {
|
|
6096
|
-
await
|
|
6307
|
+
const { agent } = await runAgentInteractive(prompt2, { cwd: workDir, chain });
|
|
6308
|
+
if (agent !== chain[0]) log(`Applied with fallback agent ${paint8("cyan", agent)}`);
|
|
6097
6309
|
} catch (err) {
|
|
6098
|
-
|
|
6099
|
-
logErr(`Agent fix failed: ${message}`);
|
|
6100
|
-
try {
|
|
6101
|
-
await api.patch(`/api/reviews/${id}`, { fixStatus: "failed", fixErrorMessage: message });
|
|
6102
|
-
} catch {
|
|
6103
|
-
}
|
|
6104
|
-
process.exit(1);
|
|
6310
|
+
await failAndExit(`Agent fix failed: ${err.message || "Unknown error"}`);
|
|
6105
6311
|
}
|
|
6106
|
-
const dirty = execSync5("git status --porcelain", { cwd:
|
|
6312
|
+
const dirty = execSync5("git status --porcelain", { cwd: workDir, encoding: "utf-8" }).trim();
|
|
6107
6313
|
if (!dirty) {
|
|
6108
6314
|
log(paint8("yellow", "Agent didn't change any files."));
|
|
6109
6315
|
try {
|
|
@@ -6113,10 +6319,11 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6113
6319
|
});
|
|
6114
6320
|
} catch {
|
|
6115
6321
|
}
|
|
6322
|
+
cleanup();
|
|
6116
6323
|
return;
|
|
6117
6324
|
}
|
|
6118
6325
|
if (opts.commit === false) {
|
|
6119
|
-
logOk(
|
|
6326
|
+
logOk(`Changes left unstaged for review (--no-commit) in ${paint8("dim", workDir)}.`);
|
|
6120
6327
|
try {
|
|
6121
6328
|
await api.patch(`/api/reviews/${id}`, { fixStatus: "completed" });
|
|
6122
6329
|
} catch {
|
|
@@ -6128,35 +6335,27 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6128
6335
|
findingsCount: actionableFindings.length
|
|
6129
6336
|
});
|
|
6130
6337
|
try {
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
try {
|
|
6137
|
-
await api.patch(`/api/reviews/${id}`, { fixStatus: "failed", fixErrorMessage: message });
|
|
6138
|
-
} catch {
|
|
6338
|
+
if (touchedFiles.length > 0) {
|
|
6339
|
+
const args = touchedFiles.map((f) => JSON.stringify(f)).join(" ");
|
|
6340
|
+
execSync5(`git add -- ${args}`, { cwd: workDir });
|
|
6341
|
+
} else {
|
|
6342
|
+
execSync5("git add -A", { cwd: workDir });
|
|
6139
6343
|
}
|
|
6140
|
-
|
|
6344
|
+
execSync5(`git commit -m ${JSON.stringify(commitMessage)}`, { cwd: workDir, stdio: "pipe" });
|
|
6345
|
+
} catch (err) {
|
|
6346
|
+
await failAndExit(`Commit failed: ${err.message || "Unknown error"}`);
|
|
6141
6347
|
}
|
|
6142
|
-
const sha = execSync5("git rev-parse HEAD", { cwd:
|
|
6348
|
+
const sha = execSync5("git rev-parse HEAD", { cwd: workDir, encoding: "utf-8" }).trim();
|
|
6143
6349
|
logOk(`Committed ${paint8("yellow", sha.slice(0, 10))}`);
|
|
6144
6350
|
if (opts.push !== false) {
|
|
6145
6351
|
try {
|
|
6146
|
-
execSync5(`git push origin ${review.branch}`, { cwd:
|
|
6352
|
+
execSync5(`git push origin ${review.branch}`, { cwd: workDir, stdio: "pipe" });
|
|
6147
6353
|
logOk(`Pushed to origin/${review.branch}`);
|
|
6148
6354
|
} catch (err) {
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
fixStatus: "failed",
|
|
6154
|
-
fixErrorMessage: `Committed ${sha.slice(0, 10)} but push failed: ${message}`,
|
|
6155
|
-
fixCommitSha: sha
|
|
6156
|
-
});
|
|
6157
|
-
} catch {
|
|
6158
|
-
}
|
|
6159
|
-
process.exit(1);
|
|
6355
|
+
await failAndExit(
|
|
6356
|
+
`Committed ${sha.slice(0, 10)} but push failed: ${err.message || "Unknown error"}`,
|
|
6357
|
+
{ fixCommitSha: sha }
|
|
6358
|
+
);
|
|
6160
6359
|
}
|
|
6161
6360
|
}
|
|
6162
6361
|
try {
|
|
@@ -6167,6 +6366,7 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6167
6366
|
});
|
|
6168
6367
|
} catch {
|
|
6169
6368
|
}
|
|
6369
|
+
cleanup();
|
|
6170
6370
|
logOk("Done.");
|
|
6171
6371
|
});
|
|
6172
6372
|
function buildApplyPrompt(args) {
|
|
@@ -6210,17 +6410,418 @@ function buildCommitMessage(args) {
|
|
|
6210
6410
|
|
|
6211
6411
|
Generated by mr review apply.`;
|
|
6212
6412
|
}
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6413
|
+
|
|
6414
|
+
// lib/review/dimensions.ts
|
|
6415
|
+
var REVIEW_DIMENSIONS = [
|
|
6416
|
+
{
|
|
6417
|
+
key: "security",
|
|
6418
|
+
title: "Security",
|
|
6419
|
+
allowedTypes: ["security", "bug"],
|
|
6420
|
+
focus: [
|
|
6421
|
+
"Hunt exclusively for security problems:",
|
|
6422
|
+
"- Injection (SQL/NoSQL/command/path), XSS, SSRF, and unsafe deserialization",
|
|
6423
|
+
"- AuthN/AuthZ gaps: missing ownership checks, privilege escalation, IDOR",
|
|
6424
|
+
"- Secrets or credentials committed, logged, or returned to clients",
|
|
6425
|
+
"- Unsafe handling of user input, missing validation, or unescaped output",
|
|
6426
|
+
"- Insecure crypto, weak randomness, or Edge-runtime incompatible Node APIs",
|
|
6427
|
+
"Do NOT report style, performance, or generic refactor ideas here."
|
|
6428
|
+
].join("\n")
|
|
6429
|
+
},
|
|
6430
|
+
{
|
|
6431
|
+
key: "correctness",
|
|
6432
|
+
title: "Correctness",
|
|
6433
|
+
allowedTypes: ["bug"],
|
|
6434
|
+
focus: [
|
|
6435
|
+
"Hunt exclusively for correctness bugs and logical errors:",
|
|
6436
|
+
"- Off-by-one, null/undefined dereferences, incorrect conditionals",
|
|
6437
|
+
"- Broken contracts: a changed function signature whose other call sites are now wrong",
|
|
6438
|
+
"- Unhandled error paths, swallowed exceptions, race conditions",
|
|
6439
|
+
"- State that can desync, missing await, or incorrect async ordering",
|
|
6440
|
+
"Open neighboring files to confirm a change doesn't break an un-diffed caller.",
|
|
6441
|
+
"Do NOT report style, naming, or performance micro-optimizations here."
|
|
6442
|
+
].join("\n")
|
|
6443
|
+
},
|
|
6444
|
+
{
|
|
6445
|
+
key: "performance",
|
|
6446
|
+
title: "Performance",
|
|
6447
|
+
allowedTypes: ["performance", "bug"],
|
|
6448
|
+
focus: [
|
|
6449
|
+
"Hunt exclusively for performance problems:",
|
|
6450
|
+
"- N+1 queries, missing indexes, redundant network/database round-trips",
|
|
6451
|
+
"- Unnecessary re-renders, missing memoization, work done in hot loops",
|
|
6452
|
+
"- Large synchronous work on a request path, unbounded memory growth",
|
|
6453
|
+
"Only flag issues with a realistic, material impact. Do NOT report style or naming."
|
|
6454
|
+
].join("\n")
|
|
6455
|
+
},
|
|
6456
|
+
{
|
|
6457
|
+
key: "style",
|
|
6458
|
+
title: "Style & Maintainability",
|
|
6459
|
+
allowedTypes: ["style", "suggestion", "nitpick"],
|
|
6460
|
+
focus: [
|
|
6461
|
+
"Look for style, readability and maintainability improvements:",
|
|
6462
|
+
"- Naming, dead code, duplication, and inconsistent patterns",
|
|
6463
|
+
"- Missing types, unclear abstractions, or violations of repo conventions",
|
|
6464
|
+
"- Small suggestions and nitpicks (keep these low severity)",
|
|
6465
|
+
"Do NOT duplicate security/correctness/performance findings \u2014 stay in your lane."
|
|
6466
|
+
].join("\n")
|
|
6467
|
+
}
|
|
6468
|
+
];
|
|
6469
|
+
|
|
6470
|
+
// lib/review/parse.ts
|
|
6471
|
+
function extractJsonObjects(text) {
|
|
6472
|
+
const objects = [];
|
|
6473
|
+
let depth = 0;
|
|
6474
|
+
let start = -1;
|
|
6475
|
+
let inString = false;
|
|
6476
|
+
let escaped = false;
|
|
6477
|
+
for (let i = 0; i < text.length; i++) {
|
|
6478
|
+
const ch = text[i];
|
|
6479
|
+
if (inString) {
|
|
6480
|
+
if (escaped) {
|
|
6481
|
+
escaped = false;
|
|
6482
|
+
} else if (ch === "\\") {
|
|
6483
|
+
escaped = true;
|
|
6484
|
+
} else if (ch === '"') {
|
|
6485
|
+
inString = false;
|
|
6486
|
+
}
|
|
6487
|
+
continue;
|
|
6488
|
+
}
|
|
6489
|
+
if (ch === '"') {
|
|
6490
|
+
inString = true;
|
|
6491
|
+
} else if (ch === "{") {
|
|
6492
|
+
if (depth === 0) start = i;
|
|
6493
|
+
depth++;
|
|
6494
|
+
} else if (ch === "}") {
|
|
6495
|
+
if (depth > 0) {
|
|
6496
|
+
depth--;
|
|
6497
|
+
if (depth === 0 && start !== -1) {
|
|
6498
|
+
objects.push(text.slice(start, i + 1));
|
|
6499
|
+
start = -1;
|
|
6500
|
+
}
|
|
6501
|
+
}
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6504
|
+
return objects;
|
|
6505
|
+
}
|
|
6506
|
+
function parseReviewOutput(output) {
|
|
6507
|
+
const cleaned = output.replace(/```(?:json)?/gi, "").replace(/```/g, "").trim();
|
|
6508
|
+
const candidates = [cleaned, ...extractJsonObjects(cleaned)].sort(
|
|
6509
|
+
(a, b) => b.length - a.length
|
|
6510
|
+
);
|
|
6511
|
+
for (const candidate of candidates) {
|
|
6512
|
+
try {
|
|
6513
|
+
const parsed = JSON.parse(candidate);
|
|
6514
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.findings)) {
|
|
6515
|
+
return {
|
|
6516
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
6517
|
+
findings: parsed.findings,
|
|
6518
|
+
parseFailed: false
|
|
6519
|
+
};
|
|
6520
|
+
}
|
|
6521
|
+
} catch {
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6524
|
+
return { summary: "", findings: [], parseFailed: true };
|
|
6525
|
+
}
|
|
6526
|
+
|
|
6527
|
+
// lib/review/merge.ts
|
|
6528
|
+
var SEVERITY_RANK = {
|
|
6529
|
+
critical: 4,
|
|
6530
|
+
high: 3,
|
|
6531
|
+
medium: 2,
|
|
6532
|
+
low: 1
|
|
6533
|
+
};
|
|
6534
|
+
function tokenize(text) {
|
|
6535
|
+
return new Set(
|
|
6536
|
+
(text || "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter(Boolean)
|
|
6537
|
+
);
|
|
6538
|
+
}
|
|
6539
|
+
function titleSimilarity(a, b) {
|
|
6540
|
+
const ta = tokenize(a);
|
|
6541
|
+
const tb = tokenize(b);
|
|
6542
|
+
if (ta.size === 0 && tb.size === 0) return 1;
|
|
6543
|
+
if (ta.size === 0 || tb.size === 0) return 0;
|
|
6544
|
+
let intersection = 0;
|
|
6545
|
+
for (const t of ta) if (tb.has(t)) intersection++;
|
|
6546
|
+
const union = ta.size + tb.size - intersection;
|
|
6547
|
+
return intersection / union;
|
|
6548
|
+
}
|
|
6549
|
+
function severityRank(sev) {
|
|
6550
|
+
return SEVERITY_RANK[sev] ?? 0;
|
|
6551
|
+
}
|
|
6552
|
+
function isDuplicate(a, b) {
|
|
6553
|
+
if (a.file !== b.file) return false;
|
|
6554
|
+
const aLine = a.line;
|
|
6555
|
+
const bLine = b.line;
|
|
6556
|
+
if (aLine != null && bLine != null) {
|
|
6557
|
+
if (Math.abs(aLine - bLine) > 3) return false;
|
|
6558
|
+
} else if (aLine != null || bLine != null) {
|
|
6559
|
+
return titleSimilarity(a.title, b.title) >= 0.8;
|
|
6560
|
+
}
|
|
6561
|
+
return titleSimilarity(a.title, b.title) >= 0.5;
|
|
6562
|
+
}
|
|
6563
|
+
function preferred(a, b) {
|
|
6564
|
+
const aScore = severityRank(a.severity) * 10 + (a.confidence ?? 0);
|
|
6565
|
+
const bScore = severityRank(b.severity) * 10 + (b.confidence ?? 0);
|
|
6566
|
+
const winner = bScore > aScore ? b : a;
|
|
6567
|
+
const loser = winner === a ? b : a;
|
|
6568
|
+
const dims = [winner.dimension, loser.dimension].filter(Boolean);
|
|
6569
|
+
return {
|
|
6570
|
+
...winner,
|
|
6571
|
+
description: (loser.description?.length ?? 0) > (winner.description?.length ?? 0) ? loser.description : winner.description,
|
|
6572
|
+
suggestion: winner.suggestion || loser.suggestion,
|
|
6573
|
+
dimension: Array.from(new Set(dims)).join("+") || winner.dimension,
|
|
6574
|
+
confidence: Math.max(winner.confidence ?? 0, loser.confidence ?? 0) || void 0
|
|
6575
|
+
};
|
|
6576
|
+
}
|
|
6577
|
+
function mergeFindings(all) {
|
|
6578
|
+
const merged = [];
|
|
6579
|
+
for (const finding of all) {
|
|
6580
|
+
const existingIdx = merged.findIndex((m) => isDuplicate(m, finding));
|
|
6581
|
+
if (existingIdx === -1) {
|
|
6582
|
+
merged.push(finding);
|
|
6583
|
+
} else {
|
|
6584
|
+
merged[existingIdx] = preferred(merged[existingIdx], finding);
|
|
6585
|
+
}
|
|
6586
|
+
}
|
|
6587
|
+
merged.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));
|
|
6588
|
+
return merged.map((f, i) => ({ ...f, id: `f${i + 1}` }));
|
|
6589
|
+
}
|
|
6590
|
+
|
|
6591
|
+
// lib/review/prompt.ts
|
|
6592
|
+
var JSON_CONTRACT = `Return ONLY a JSON object (no markdown fences, no prose before or after) with this exact shape:
|
|
6593
|
+
{
|
|
6594
|
+
"summary": "1-2 sentence assessment for this concern",
|
|
6595
|
+
"findings": [
|
|
6596
|
+
{
|
|
6597
|
+
"id": "f1",
|
|
6598
|
+
"type": "bug",
|
|
6599
|
+
"severity": "high",
|
|
6600
|
+
"title": "Brief one-line title",
|
|
6601
|
+
"description": "Detailed explanation",
|
|
6602
|
+
"file": "path/to/file.ts",
|
|
6603
|
+
"line": 42,
|
|
6604
|
+
"suggestion": "Concrete fix, with code when possible",
|
|
6605
|
+
"confidence": 0.0
|
|
6606
|
+
}
|
|
6607
|
+
]
|
|
6608
|
+
}
|
|
6609
|
+
"confidence" is your own 0-1 estimate that this is a real, actionable issue.
|
|
6610
|
+
If there are no issues for this concern, return an empty findings array with a positive summary.`;
|
|
6611
|
+
function buildDimensionPrompt(args) {
|
|
6612
|
+
const lines = [];
|
|
6613
|
+
lines.push(
|
|
6614
|
+
`You are a senior code reviewer focused exclusively on ${args.dimension.title.toUpperCase()}.`,
|
|
6615
|
+
`Review the git diff for branch "${args.branch}" compared to "${args.baseBranch}".`,
|
|
6616
|
+
"",
|
|
6617
|
+
args.dimension.focus,
|
|
6618
|
+
"",
|
|
6619
|
+
`Only emit findings whose "type" is one of: ${args.dimension.allowedTypes.join(", ")}.`,
|
|
6620
|
+
""
|
|
6621
|
+
);
|
|
6622
|
+
if (args.canReadFiles) {
|
|
6623
|
+
lines.push(
|
|
6624
|
+
"You are running inside a checkout of this repository. You MAY open the changed files",
|
|
6625
|
+
"and their neighbors (callers, callees, related modules) to verify a finding before",
|
|
6626
|
+
"reporting it \u2014 this catches issues that are invisible from the diff alone, like a",
|
|
6627
|
+
"renamed function's other call sites. Prefer confirmed findings over speculation.",
|
|
6628
|
+
""
|
|
6629
|
+
);
|
|
6630
|
+
}
|
|
6631
|
+
if (args.fileScope && args.fileScope.length > 0) {
|
|
6632
|
+
lines.push(
|
|
6633
|
+
"Concentrate on these files (other diff context is provided only for reference):",
|
|
6634
|
+
...args.fileScope.map((f) => ` - ${f}`),
|
|
6635
|
+
""
|
|
6636
|
+
);
|
|
6637
|
+
}
|
|
6638
|
+
lines.push(JSON_CONTRACT, "", "Here is the diff to review:", "", "```diff", args.diff, "```");
|
|
6639
|
+
return lines.join("\n");
|
|
6640
|
+
}
|
|
6641
|
+
function buildVerificationPrompt(args) {
|
|
6642
|
+
const compact = args.findings.map((f) => ({
|
|
6643
|
+
id: f.id,
|
|
6644
|
+
type: f.type,
|
|
6645
|
+
severity: f.severity,
|
|
6646
|
+
title: f.title,
|
|
6647
|
+
description: f.description,
|
|
6648
|
+
file: f.file,
|
|
6649
|
+
line: f.line
|
|
6650
|
+
}));
|
|
6651
|
+
const lines = [];
|
|
6652
|
+
lines.push(
|
|
6653
|
+
`You are an adversarial reviewer verifying findings from an automated review of branch "${args.branch}" vs "${args.baseBranch}".`,
|
|
6654
|
+
"For EACH finding below, try to refute it. A finding should be kept only if it is a real,",
|
|
6655
|
+
"actionable issue grounded in the actual diff. Reject hallucinations, findings that",
|
|
6656
|
+
"reference code that isn't there, and pure speculation.",
|
|
6657
|
+
""
|
|
6658
|
+
);
|
|
6659
|
+
if (args.canReadFiles) {
|
|
6660
|
+
lines.push(
|
|
6661
|
+
"You are in a checkout of the repo \u2014 open files to confirm or refute each claim.",
|
|
6662
|
+
""
|
|
6663
|
+
);
|
|
6664
|
+
}
|
|
6665
|
+
lines.push(
|
|
6666
|
+
"Return ONLY a JSON object (no fences, no prose) of this shape:",
|
|
6667
|
+
`{ "verdicts": [ { "id": "f1", "keep": true, "confidence": 0.0, "reason": "why" } ] }`,
|
|
6668
|
+
"confidence is 0-1. Set keep=false when you cannot substantiate the finding.",
|
|
6669
|
+
"",
|
|
6670
|
+
"FINDINGS:",
|
|
6671
|
+
JSON.stringify(compact, null, 2),
|
|
6672
|
+
"",
|
|
6673
|
+
"DIFF:",
|
|
6674
|
+
"```diff",
|
|
6675
|
+
args.diff,
|
|
6676
|
+
"```"
|
|
6677
|
+
);
|
|
6678
|
+
return lines.join("\n");
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
// lib/review/orchestrate.ts
|
|
6682
|
+
function splitDiffByFile(diff) {
|
|
6683
|
+
const sections = diff.split(/(?=^diff --git )/m).filter((s) => s.trim());
|
|
6684
|
+
return sections.map((section) => {
|
|
6685
|
+
const m = section.match(/^diff --git a\/(.+?) b\//m);
|
|
6686
|
+
return { file: m?.[1] ?? "unknown", section };
|
|
6687
|
+
});
|
|
6688
|
+
}
|
|
6689
|
+
async function pool(items, limit) {
|
|
6690
|
+
const results = new Array(items.length);
|
|
6691
|
+
let cursor = 0;
|
|
6692
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
6693
|
+
while (cursor < items.length) {
|
|
6694
|
+
const idx = cursor++;
|
|
6695
|
+
results[idx] = await items[idx]();
|
|
6696
|
+
}
|
|
6697
|
+
});
|
|
6698
|
+
await Promise.all(workers);
|
|
6699
|
+
return results;
|
|
6700
|
+
}
|
|
6701
|
+
function buildJobs(opts) {
|
|
6702
|
+
const files = splitDiffByFile(opts.diff);
|
|
6703
|
+
const shardThreshold = opts.shardThreshold ?? 12;
|
|
6704
|
+
if (files.length >= shardThreshold) {
|
|
6705
|
+
const shardCount = Math.min(4, Math.ceil(files.length / 6));
|
|
6706
|
+
const shards = Array.from(
|
|
6707
|
+
{ length: shardCount },
|
|
6708
|
+
() => ({ diff: "", files: [] })
|
|
6709
|
+
);
|
|
6710
|
+
files.forEach((f, i) => {
|
|
6711
|
+
const s = shards[i % shardCount];
|
|
6712
|
+
s.diff += f.section;
|
|
6713
|
+
s.files.push(f.file);
|
|
6222
6714
|
});
|
|
6715
|
+
const jobs = [];
|
|
6716
|
+
for (const dim of REVIEW_DIMENSIONS) {
|
|
6717
|
+
for (const shard of shards) {
|
|
6718
|
+
jobs.push({ dimension: dim, diff: shard.diff, fileScope: shard.files });
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
return jobs;
|
|
6722
|
+
}
|
|
6723
|
+
return REVIEW_DIMENSIONS.map((dimension) => ({ dimension, diff: opts.diff }));
|
|
6724
|
+
}
|
|
6725
|
+
async function orchestrateReview(opts) {
|
|
6726
|
+
const log4 = opts.log ?? (() => {
|
|
6727
|
+
});
|
|
6728
|
+
const concurrency = opts.concurrency ?? 4;
|
|
6729
|
+
const canReadFiles = Boolean(opts.cwd);
|
|
6730
|
+
const runOpts = { cwd: opts.cwd, chain: opts.chain };
|
|
6731
|
+
const jobs = buildJobs(opts);
|
|
6732
|
+
log4(`Fanning out ${jobs.length} review subagent(s) across ${REVIEW_DIMENSIONS.length} dimensions`);
|
|
6733
|
+
const summaries = [];
|
|
6734
|
+
const collected = [];
|
|
6735
|
+
const jobResults = await pool(
|
|
6736
|
+
jobs.map((job) => async () => {
|
|
6737
|
+
const prompt2 = buildDimensionPrompt({
|
|
6738
|
+
dimension: job.dimension,
|
|
6739
|
+
branch: opts.branch,
|
|
6740
|
+
baseBranch: opts.baseBranch,
|
|
6741
|
+
diff: job.diff,
|
|
6742
|
+
canReadFiles,
|
|
6743
|
+
fileScope: job.fileScope
|
|
6744
|
+
});
|
|
6745
|
+
try {
|
|
6746
|
+
const { output } = await runAgentCaptured(prompt2, runOpts);
|
|
6747
|
+
const parsed = parseReviewOutput(output);
|
|
6748
|
+
return { job, parsed };
|
|
6749
|
+
} catch (err) {
|
|
6750
|
+
log4(` ${job.dimension.key} subagent failed: ${err.message}`);
|
|
6751
|
+
return { job, parsed: { summary: "", findings: [], parseFailed: true } };
|
|
6752
|
+
}
|
|
6753
|
+
}),
|
|
6754
|
+
concurrency
|
|
6755
|
+
);
|
|
6756
|
+
const dimensionsRun = /* @__PURE__ */ new Set();
|
|
6757
|
+
for (const { job, parsed } of jobResults) {
|
|
6758
|
+
if (parsed.summary) summaries.push(`${job.dimension.title}: ${parsed.summary}`);
|
|
6759
|
+
for (const f of parsed.findings) {
|
|
6760
|
+
dimensionsRun.add(job.dimension.key);
|
|
6761
|
+
collected.push({
|
|
6762
|
+
id: f.id || "f",
|
|
6763
|
+
type: f.type || job.dimension.allowedTypes[0],
|
|
6764
|
+
severity: f.severity || "medium",
|
|
6765
|
+
title: f.title || "Untitled finding",
|
|
6766
|
+
description: f.description || "",
|
|
6767
|
+
file: f.file || "unknown",
|
|
6768
|
+
line: typeof f.line === "number" ? f.line : void 0,
|
|
6769
|
+
endLine: typeof f.endLine === "number" ? f.endLine : void 0,
|
|
6770
|
+
suggestion: f.suggestion,
|
|
6771
|
+
status: "new",
|
|
6772
|
+
dimension: job.dimension.key,
|
|
6773
|
+
confidence: typeof f.confidence === "number" ? f.confidence : void 0
|
|
6774
|
+
});
|
|
6775
|
+
}
|
|
6776
|
+
}
|
|
6777
|
+
const rawCount = collected.length;
|
|
6778
|
+
let findings = mergeFindings(collected);
|
|
6779
|
+
log4(`Merged ${rawCount} raw findings into ${findings.length} after dedup`);
|
|
6780
|
+
if (opts.verify && findings.length > 0) {
|
|
6781
|
+
findings = await verifyFindings(findings, opts, runOpts, log4);
|
|
6782
|
+
}
|
|
6783
|
+
return {
|
|
6784
|
+
summary: summaries.join(" "),
|
|
6785
|
+
findings,
|
|
6786
|
+
dimensionsRun: [...dimensionsRun],
|
|
6787
|
+
rawCount
|
|
6788
|
+
};
|
|
6789
|
+
}
|
|
6790
|
+
async function verifyFindings(findings, opts, runOpts, log4) {
|
|
6791
|
+
const minConfidence = opts.minConfidence ?? 0.4;
|
|
6792
|
+
const prompt2 = buildVerificationPrompt({
|
|
6793
|
+
findings,
|
|
6794
|
+
branch: opts.branch,
|
|
6795
|
+
baseBranch: opts.baseBranch,
|
|
6796
|
+
diff: opts.diff,
|
|
6797
|
+
canReadFiles: Boolean(opts.cwd)
|
|
6223
6798
|
});
|
|
6799
|
+
let verdicts = [];
|
|
6800
|
+
try {
|
|
6801
|
+
const { output } = await runAgentCaptured(prompt2, runOpts);
|
|
6802
|
+
const cleaned = output.replace(/```(?:json)?/gi, "").replace(/```/g, "").trim();
|
|
6803
|
+
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
6804
|
+
if (match) {
|
|
6805
|
+
const parsed = JSON.parse(match[0]);
|
|
6806
|
+
verdicts = parsed.verdicts ?? [];
|
|
6807
|
+
}
|
|
6808
|
+
} catch (err) {
|
|
6809
|
+
log4(`Verification stage failed, keeping all findings: ${err.message}`);
|
|
6810
|
+
return findings;
|
|
6811
|
+
}
|
|
6812
|
+
const byId = new Map(verdicts.map((v) => [v.id, v]));
|
|
6813
|
+
const kept = findings.filter((f) => {
|
|
6814
|
+
const v = byId.get(f.id);
|
|
6815
|
+
if (!v) return true;
|
|
6816
|
+
if (v.keep === false) return false;
|
|
6817
|
+
if (typeof v.confidence === "number") {
|
|
6818
|
+
f.confidence = v.confidence;
|
|
6819
|
+
return v.confidence >= minConfidence;
|
|
6820
|
+
}
|
|
6821
|
+
return true;
|
|
6822
|
+
});
|
|
6823
|
+
log4(`Verification kept ${kept.length}/${findings.length} findings`);
|
|
6824
|
+
return kept.map((f, i) => ({ ...f, id: `f${i + 1}` }));
|
|
6224
6825
|
}
|
|
6225
6826
|
|
|
6226
6827
|
// cli/commands/review.ts
|
|
@@ -6290,6 +6891,7 @@ var reviewCommand = new Command26("review").description("Run an automated code r
|
|
|
6290
6891
|
const canUseRemote = remote !== null && (prNumber || remote.number) !== void 0;
|
|
6291
6892
|
let diff;
|
|
6292
6893
|
let branch = opts.branch;
|
|
6894
|
+
let reviewCwd;
|
|
6293
6895
|
if (canUseRemote && remote) {
|
|
6294
6896
|
const num = prNumber ?? remote.number;
|
|
6295
6897
|
if (!num) {
|
|
@@ -6339,6 +6941,7 @@ var reviewCommand = new Command26("review").description("Run an automated code r
|
|
|
6339
6941
|
process.exit(1);
|
|
6340
6942
|
}
|
|
6341
6943
|
log2(`Using project path: ${paint9("dim", projectPath)}`);
|
|
6944
|
+
reviewCwd = projectPath;
|
|
6342
6945
|
if (!branch) {
|
|
6343
6946
|
try {
|
|
6344
6947
|
branch = execSync6("git rev-parse --abbrev-ref HEAD", {
|
|
@@ -6417,17 +7020,39 @@ var reviewCommand = new Command26("review").description("Run an automated code r
|
|
|
6417
7020
|
} catch {
|
|
6418
7021
|
}
|
|
6419
7022
|
const startTime = Date.now();
|
|
6420
|
-
const
|
|
6421
|
-
|
|
7023
|
+
const chain = await resolveReviewAgentChain("claude");
|
|
7024
|
+
if (chain.length === 0) {
|
|
7025
|
+
const message = "No available agent found (need one of: claude, codex, agy)";
|
|
7026
|
+
logErr2(message);
|
|
7027
|
+
try {
|
|
7028
|
+
await api.patch(`/api/reviews/${reportId}`, { status: "failed", errorMessage: message });
|
|
7029
|
+
} catch {
|
|
7030
|
+
}
|
|
7031
|
+
process.exit(1);
|
|
7032
|
+
}
|
|
7033
|
+
log2(`Agent chain: ${paint9("cyan", chain.join(" \u2192 "))}`);
|
|
7034
|
+
const MAX_DIFF_CHARS = 4e5;
|
|
7035
|
+
const diffCharsTotal = diff.length;
|
|
7036
|
+
let reviewDiff = diff;
|
|
7037
|
+
let diffTruncated = false;
|
|
6422
7038
|
if (diff.length > MAX_DIFF_CHARS) {
|
|
6423
|
-
|
|
6424
|
-
|
|
7039
|
+
reviewDiff = diff.slice(0, MAX_DIFF_CHARS);
|
|
7040
|
+
diffTruncated = true;
|
|
7041
|
+
log2(paint9("yellow", `Diff truncated to ${MAX_DIFF_CHARS.toLocaleString()} of ${diffCharsTotal.toLocaleString()} chars`));
|
|
6425
7042
|
}
|
|
6426
7043
|
try {
|
|
6427
|
-
log2("Running code review
|
|
6428
|
-
const
|
|
6429
|
-
|
|
6430
|
-
|
|
7044
|
+
log2("Running fan-out code review...");
|
|
7045
|
+
const orchestrated = await orchestrateReview({
|
|
7046
|
+
branch,
|
|
7047
|
+
baseBranch,
|
|
7048
|
+
diff: reviewDiff,
|
|
7049
|
+
chain,
|
|
7050
|
+
cwd: reviewCwd,
|
|
7051
|
+
concurrency: 4,
|
|
7052
|
+
verify: true,
|
|
7053
|
+
log: (m) => log2(m)
|
|
7054
|
+
});
|
|
7055
|
+
const result = { summary: orchestrated.summary, findings: orchestrated.findings };
|
|
6431
7056
|
const duration = Date.now() - startTime;
|
|
6432
7057
|
let wasCancelled = false;
|
|
6433
7058
|
try {
|
|
@@ -6444,7 +7069,9 @@ var reviewCommand = new Command26("review").description("Run an automated code r
|
|
|
6444
7069
|
summary: result.summary,
|
|
6445
7070
|
findings: result.findings,
|
|
6446
7071
|
filesReviewed: filesChanged,
|
|
6447
|
-
reviewDurationMs: duration
|
|
7072
|
+
reviewDurationMs: duration,
|
|
7073
|
+
diffTruncated,
|
|
7074
|
+
diffCharsTotal
|
|
6448
7075
|
});
|
|
6449
7076
|
logOk2(`Review completed in ${paint9("cyan", formatDuration(duration))}`);
|
|
6450
7077
|
logOk2(`Found ${paint9("yellow", String(result.findings.length))} findings`);
|
|
@@ -6533,112 +7160,12 @@ function formatDuration(ms) {
|
|
|
6533
7160
|
if (s < 60) return `${s}s`;
|
|
6534
7161
|
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
6535
7162
|
}
|
|
6536
|
-
function buildReviewPrompt(branch, baseBranch, diff) {
|
|
6537
|
-
return `You are a senior code reviewer. Review the following git diff for branch "${branch}" compared to "${baseBranch}".
|
|
6538
|
-
|
|
6539
|
-
Analyze the code changes and produce a JSON response with your review findings.
|
|
6540
|
-
|
|
6541
|
-
Focus on:
|
|
6542
|
-
- Bugs and logical errors
|
|
6543
|
-
- Security vulnerabilities (XSS, injection, auth issues, secrets exposure)
|
|
6544
|
-
- Performance issues (N+1 queries, missing indexes, unnecessary re-renders)
|
|
6545
|
-
- Code style and best practices violations
|
|
6546
|
-
- Suggestions for improvement
|
|
6547
|
-
- Nitpicks (minor style/naming issues)
|
|
6548
|
-
|
|
6549
|
-
For each finding, provide:
|
|
6550
|
-
- A unique ID (e.g. "f1", "f2", etc.)
|
|
6551
|
-
- Type: "bug", "security", "performance", "style", "suggestion", or "nitpick"
|
|
6552
|
-
- Severity: "critical", "high", "medium", or "low"
|
|
6553
|
-
- Title: a brief one-line summary
|
|
6554
|
-
- Description: detailed explanation of the issue
|
|
6555
|
-
- File: the file path where the issue was found
|
|
6556
|
-
- Line: the approximate line number in the new code (optional)
|
|
6557
|
-
- Suggestion: suggested fix or improvement (optional, include actual code when possible)
|
|
6558
|
-
|
|
6559
|
-
Return ONLY a JSON object with this structure (no markdown, no explanation before/after):
|
|
6560
|
-
{
|
|
6561
|
-
"summary": "Brief overall assessment of the code changes (2-3 sentences)",
|
|
6562
|
-
"findings": [
|
|
6563
|
-
{
|
|
6564
|
-
"id": "f1",
|
|
6565
|
-
"type": "bug",
|
|
6566
|
-
"severity": "high",
|
|
6567
|
-
"title": "Brief title",
|
|
6568
|
-
"description": "Detailed description",
|
|
6569
|
-
"file": "path/to/file.ts",
|
|
6570
|
-
"line": 42,
|
|
6571
|
-
"suggestion": "Suggested fix code"
|
|
6572
|
-
}
|
|
6573
|
-
]
|
|
6574
|
-
}
|
|
6575
|
-
|
|
6576
|
-
If the code looks good with no issues, return an empty findings array with a positive summary.
|
|
6577
|
-
|
|
6578
|
-
Here is the diff to review:
|
|
6579
|
-
|
|
6580
|
-
\`\`\`diff
|
|
6581
|
-
${diff}
|
|
6582
|
-
\`\`\``;
|
|
6583
|
-
}
|
|
6584
|
-
function runClaude(prompt2) {
|
|
6585
|
-
return new Promise((resolve9, reject) => {
|
|
6586
|
-
const child = spawn8("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
6587
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
6588
|
-
});
|
|
6589
|
-
let output = "";
|
|
6590
|
-
let errOutput = "";
|
|
6591
|
-
child.stdout?.on("data", (d) => {
|
|
6592
|
-
output += d.toString();
|
|
6593
|
-
});
|
|
6594
|
-
child.stderr?.on("data", (d) => {
|
|
6595
|
-
errOutput += d.toString();
|
|
6596
|
-
});
|
|
6597
|
-
child.on("exit", (code) => {
|
|
6598
|
-
if (code === 0) resolve9(output.trim());
|
|
6599
|
-
else reject(new Error(`claude exited with code ${code}
|
|
6600
|
-
${errOutput.trim()}`));
|
|
6601
|
-
});
|
|
6602
|
-
});
|
|
6603
|
-
}
|
|
6604
|
-
function parseReviewOutput(output) {
|
|
6605
|
-
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
6606
|
-
if (!jsonMatch) {
|
|
6607
|
-
return {
|
|
6608
|
-
summary: "Failed to parse review output",
|
|
6609
|
-
findings: []
|
|
6610
|
-
};
|
|
6611
|
-
}
|
|
6612
|
-
try {
|
|
6613
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
6614
|
-
return {
|
|
6615
|
-
summary: parsed.summary || "",
|
|
6616
|
-
findings: (parsed.findings || []).map((f) => ({
|
|
6617
|
-
id: f.id || `f${Math.random().toString(36).slice(2, 8)}`,
|
|
6618
|
-
type: f.type || "suggestion",
|
|
6619
|
-
severity: f.severity || "medium",
|
|
6620
|
-
title: f.title || "Untitled finding",
|
|
6621
|
-
description: f.description || "",
|
|
6622
|
-
file: f.file || "unknown",
|
|
6623
|
-
line: f.line,
|
|
6624
|
-
endLine: f.endLine,
|
|
6625
|
-
suggestion: f.suggestion,
|
|
6626
|
-
status: "new"
|
|
6627
|
-
}))
|
|
6628
|
-
};
|
|
6629
|
-
} catch {
|
|
6630
|
-
return {
|
|
6631
|
-
summary: "Failed to parse review JSON",
|
|
6632
|
-
findings: []
|
|
6633
|
-
};
|
|
6634
|
-
}
|
|
6635
|
-
}
|
|
6636
7163
|
|
|
6637
7164
|
// cli/commands/scan.ts
|
|
6638
7165
|
import { Command as Command27 } from "commander";
|
|
6639
7166
|
|
|
6640
7167
|
// lib/scanner/index.ts
|
|
6641
|
-
import { spawn as
|
|
7168
|
+
import { spawn as spawn8 } from "child_process";
|
|
6642
7169
|
|
|
6643
7170
|
// lib/scanner/config.ts
|
|
6644
7171
|
import { readFileSync as readFileSync10, existsSync as existsSync15 } from "fs";
|
|
@@ -7256,7 +7783,7 @@ async function runScanPipeline(opts) {
|
|
|
7256
7783
|
context.priorFindings,
|
|
7257
7784
|
opts.customPrompt
|
|
7258
7785
|
);
|
|
7259
|
-
const synthesisResult = await
|
|
7786
|
+
const synthesisResult = await runClaude(prompt2);
|
|
7260
7787
|
const parsed = parseSynthesisOutput(synthesisResult);
|
|
7261
7788
|
const scanDurationMs = Date.now() - startTime;
|
|
7262
7789
|
opts.onLog(`Scan complete in ${Math.round(scanDurationMs / 1e3)}s \u2014 ${parsed.findings.length} findings`);
|
|
@@ -7317,9 +7844,9 @@ async function fetchScanContext(opts) {
|
|
|
7317
7844
|
priorFindings
|
|
7318
7845
|
};
|
|
7319
7846
|
}
|
|
7320
|
-
function
|
|
7847
|
+
function runClaude(prompt2) {
|
|
7321
7848
|
return new Promise((resolve9, reject) => {
|
|
7322
|
-
const child =
|
|
7849
|
+
const child = spawn8("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
7323
7850
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7324
7851
|
});
|
|
7325
7852
|
let output = "";
|