@dunnewold-labs/mr-manager 0.4.50 → 0.4.53
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 +929 -419
- 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.53",
|
|
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) {
|
|
@@ -1868,14 +1874,23 @@ function mergePrViaCli(prUrl, repoDir, vcs = "github") {
|
|
|
1868
1874
|
});
|
|
1869
1875
|
});
|
|
1870
1876
|
}
|
|
1871
|
-
function buildPrototypeSection(protoRefs, workingDir) {
|
|
1877
|
+
function buildPrototypeSection(protoRefs, workingDir, mode = "build") {
|
|
1872
1878
|
if (protoRefs.length === 0) return "";
|
|
1873
|
-
const sections = [
|
|
1879
|
+
const sections = mode === "prd" ? [
|
|
1874
1880
|
``,
|
|
1875
1881
|
`## Referenced Prototypes`,
|
|
1876
1882
|
``,
|
|
1877
|
-
`The following prototype designs have been linked to this task
|
|
1878
|
-
|
|
1883
|
+
`The following prototype designs have been linked to this task. They represent the intended product UI. **Read every referenced HTML file in full** (paths below) and base the PRD's UI/functional requirements on what they show: page/screen structure, layout, every component and element, all the states depicted (empty/loading/error/populated, hover/active/disabled, etc.), navigation, and the interactions the design implies. Enumerate these as concrete requirements so the implementation phase builds the complete prototype, not just a restyle.`,
|
|
1884
|
+
``
|
|
1885
|
+
] : [
|
|
1886
|
+
``,
|
|
1887
|
+
`## Referenced Prototypes`,
|
|
1888
|
+
``,
|
|
1889
|
+
`The following prototype designs have been linked to this task. They are not loose visual inspiration \u2014 they are the intended product. Your job is to BUILD what each prototype shows: its full page/screen structure, layout, all the UI components and elements, every state shown (empty/loading/error/populated, hover/active/disabled, etc.), navigation, and the interactions and behaviors the design implies. Do not stop at restyling or "rethemeing" existing UI \u2014 implement the complete structure and functionality, wired to real data and logic where the rest of the task requires it.`,
|
|
1890
|
+
``,
|
|
1891
|
+
`**Before you start implementing, read every referenced HTML file in full** (paths are given below). Do not skip this \u2014 the HTML is the source of truth for what to build. Inspect the markup to enumerate the components, sections, and states you need to create.`,
|
|
1892
|
+
``,
|
|
1893
|
+
`Where the prototype and the task PRD/notes conflict, the PRD/notes win; otherwise treat the prototype as the spec for the UI.`,
|
|
1879
1894
|
``
|
|
1880
1895
|
];
|
|
1881
1896
|
for (const ref of protoRefs) {
|
|
@@ -1883,8 +1898,13 @@ function buildPrototypeSection(protoRefs, workingDir) {
|
|
|
1883
1898
|
const files = proto.files ?? [];
|
|
1884
1899
|
const selected = ref.selectedVariants ?? Array.from({ length: proto.variantCount }, (_, i) => i);
|
|
1885
1900
|
const selectedFiles = files.filter((_, i) => selected.includes(i));
|
|
1901
|
+
const fidelity = (proto.fidelity ?? "high").toLowerCase();
|
|
1902
|
+
const isLowFi = fidelity === "low" || fidelity === "low_fidelity";
|
|
1903
|
+
const fidelityNote = isLowFi ? `This is a LOW-FIDELITY wireframe. Build the full structure, layout, components, and behavior it shows, but do NOT copy its placeholder visual style (greys, boxes, sketchy borders) \u2014 apply the project's real design system / existing styling conventions.` : `This is a HIGH-FIDELITY design. Treat it as the definitive brief for both structure/behavior AND look-and-feel \u2014 match its layout, spacing, colors, typography, and component styling faithfully.`;
|
|
1886
1904
|
sections.push(`### ${proto.title}`);
|
|
1887
1905
|
sections.push(``);
|
|
1906
|
+
sections.push(fidelityNote);
|
|
1907
|
+
sections.push(``);
|
|
1888
1908
|
if (selectedFiles.length === 0) {
|
|
1889
1909
|
sections.push(`(No variant files available)`);
|
|
1890
1910
|
sections.push(``);
|
|
@@ -1899,7 +1919,7 @@ function buildPrototypeSection(protoRefs, workingDir) {
|
|
|
1899
1919
|
try {
|
|
1900
1920
|
writeFileSync3(tmpPath, file.content, "utf-8");
|
|
1901
1921
|
sections.push(`#### ${variantLabel}: ${file.name}`);
|
|
1902
|
-
sections.push(`File: \`${tmpPath}\` \u2014 read this file
|
|
1922
|
+
sections.push(`File: \`${tmpPath}\` \u2014 read this file in full before implementing; it is the source of truth for what to build.`);
|
|
1903
1923
|
} catch {
|
|
1904
1924
|
sections.push(`#### ${variantLabel}: ${file.name}`);
|
|
1905
1925
|
sections.push(`\`\`\`html`);
|
|
@@ -2165,7 +2185,7 @@ ${task.notes}` : "";
|
|
|
2165
2185
|
`Complete all steps autonomously without asking for confirmation. Exit with code 0 when done.`
|
|
2166
2186
|
].join("\n");
|
|
2167
2187
|
}
|
|
2168
|
-
function buildPrdPrompt(task, repoDir, existingPrd, feedbackUpdates = []) {
|
|
2188
|
+
function buildPrdPrompt(task, repoDir, existingPrd, feedbackUpdates = [], protoRefs = []) {
|
|
2169
2189
|
const notes = task.notes ? `
|
|
2170
2190
|
|
|
2171
2191
|
Task notes:
|
|
@@ -2217,6 +2237,7 @@ ${task.notes}` : "";
|
|
|
2217
2237
|
`Title: ${task.title}`,
|
|
2218
2238
|
`ID: ${task.id}${notes}`,
|
|
2219
2239
|
feedbackSection,
|
|
2240
|
+
buildPrototypeSection(protoRefs, repoDir, "prd"),
|
|
2220
2241
|
`## Instructions`,
|
|
2221
2242
|
``,
|
|
2222
2243
|
...revisionInstructions,
|
|
@@ -2457,6 +2478,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2457
2478
|
const referenceImagePaths = options.referenceImagePaths ?? [];
|
|
2458
2479
|
const hasReferences = referenceImagePaths.length > 0;
|
|
2459
2480
|
const exemplars = pickExemplars(proto.id);
|
|
2481
|
+
const isLowFidelity = proto.fidelity === "low";
|
|
2460
2482
|
const variantSteps = [];
|
|
2461
2483
|
for (let i = 0; i < variantsToProduce; i++) {
|
|
2462
2484
|
const idx = startIndex + i;
|
|
@@ -2464,7 +2486,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2464
2486
|
variantSteps.push(
|
|
2465
2487
|
`### Variant ${idx}: ${filename}`,
|
|
2466
2488
|
`1. ${varianceInfo.perVariantDirective}`,
|
|
2467
|
-
`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}.`}`,
|
|
2489
|
+
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}.`}`,
|
|
2468
2490
|
`3. Write the complete self-contained HTML to \`${repoDir}/${filename}\` using the Write tool.`,
|
|
2469
2491
|
`4. Verify the file was created by reading the first few lines of \`${repoDir}/${filename}\`.`,
|
|
2470
2492
|
``
|
|
@@ -2474,6 +2496,38 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2474
2496
|
{ length: variantsToProduce },
|
|
2475
2497
|
(_, i) => `prototype-${startIndex + i}.html`
|
|
2476
2498
|
);
|
|
2499
|
+
const wireframeQualityBar = [
|
|
2500
|
+
`## Wireframe & Sketch Guidelines (Balsamiq-Style)`,
|
|
2501
|
+
``,
|
|
2502
|
+
`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:`,
|
|
2503
|
+
``,
|
|
2504
|
+
`**Grayscale Aesthetic Only**`,
|
|
2505
|
+
`- Use ONLY black, white, and shades of gray.`,
|
|
2506
|
+
`- Absolutely NO color palette exploration, no accent colors, no gradients, and no color highlights.`,
|
|
2507
|
+
``,
|
|
2508
|
+
`**Linework & Borders**`,
|
|
2509
|
+
`- Borders must feel sketchy or hand-drawn. Use thick, solid borders (e.g., 2px solid black or dark gray) for elements.`,
|
|
2510
|
+
`- Keep shapes simple and flat. Avoid rounded corners unless they represent a device frame or button.`,
|
|
2511
|
+
`- Use flat gray boxes with diagonal lines or an "X" representing media/image placeholders.`,
|
|
2512
|
+
``,
|
|
2513
|
+
`**Typography (Default Font)**`,
|
|
2514
|
+
`- You MUST load the Shantell Sans font from Google Fonts via:`,
|
|
2515
|
+
` <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Shantell+Sans:wght@300..800&display=swap">`,
|
|
2516
|
+
`- Set the default typeface of the entire body to: 'Shantell Sans', 'Comic Sans MS', 'Comic Neue', cursive, sans-serif.`,
|
|
2517
|
+
`- Text should feel hand-drawn but readable, giving the app a sketch feeling.`,
|
|
2518
|
+
``,
|
|
2519
|
+
`**Placeholders & Annotations**`,
|
|
2520
|
+
`- 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.`,
|
|
2521
|
+
`- 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.`,
|
|
2522
|
+
`- 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.`,
|
|
2523
|
+
``,
|
|
2524
|
+
`**No Shadows, Gradients, or Motion**`,
|
|
2525
|
+
`- Absolutely NO box-shadows, text-shadows, gradients, backdrop-blur, or glassmorphism.`,
|
|
2526
|
+
`- Absolutely NO delight-oriented motion, fade-ins, slide-ups, marquee effects, or animated gradients. Everything must be static and flat.`,
|
|
2527
|
+
``,
|
|
2528
|
+
`**Device Framing (Sketch Form)**`,
|
|
2529
|
+
`${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.`}`
|
|
2530
|
+
];
|
|
2477
2531
|
const sharedQualityBar = [
|
|
2478
2532
|
`## Craft & Polish Bar (this is the most important section \u2014 read carefully)`,
|
|
2479
2533
|
``,
|
|
@@ -2583,8 +2637,14 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2583
2637
|
logo: "Logo"
|
|
2584
2638
|
};
|
|
2585
2639
|
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.`;
|
|
2586
|
-
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.`;
|
|
2587
|
-
const creativeDirection = [
|
|
2640
|
+
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.`;
|
|
2641
|
+
const creativeDirection = isLowFidelity ? [
|
|
2642
|
+
`## Wireframe Direction`,
|
|
2643
|
+
`- **Focus on Hierarchy**: Make sure the most important elements have the largest text or thickest boundaries.`,
|
|
2644
|
+
`- **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).`,
|
|
2645
|
+
`- **Use Shantell Sans**: Ensure the font successfully loads and applies globally to keep the Balsamiq sketch aesthetic consistent.`,
|
|
2646
|
+
`- **Terse Annotations**: Ensure copy is terse, annotated, or greeked.`
|
|
2647
|
+
] : [
|
|
2588
2648
|
`## Creative Direction & Polish`,
|
|
2589
2649
|
`- **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.`,
|
|
2590
2650
|
`- **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.`,
|
|
@@ -2592,6 +2652,15 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2592
2652
|
`- **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.`,
|
|
2593
2653
|
`- **Negative Space**: Do not be afraid of "wasting" space. Generous margins and padding often signal luxury and quality.`
|
|
2594
2654
|
];
|
|
2655
|
+
const aestheticGuardrails = isLowFidelity ? [
|
|
2656
|
+
`- Absolutely NO colors besides black, white, and grays. No gradients, no shadows, no smooth animations.`,
|
|
2657
|
+
`- 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.`,
|
|
2658
|
+
`- Ensure the body uses the 'Shantell Sans' font stack for a sketchy comic feel.`
|
|
2659
|
+
] : [
|
|
2660
|
+
`- 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.`,
|
|
2661
|
+
`- 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.`,
|
|
2662
|
+
`- Choose aesthetics that fit the product and audience described in the user's prompt, not a generic developer-tool look.`
|
|
2663
|
+
];
|
|
2595
2664
|
return [
|
|
2596
2665
|
`${config.role}`,
|
|
2597
2666
|
``,
|
|
@@ -2614,7 +2683,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2614
2683
|
``,
|
|
2615
2684
|
`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.`,
|
|
2616
2685
|
``,
|
|
2617
|
-
...sharedQualityBar,
|
|
2686
|
+
...isLowFidelity ? wireframeQualityBar : sharedQualityBar,
|
|
2618
2687
|
``,
|
|
2619
2688
|
...creativeDirection,
|
|
2620
2689
|
``,
|
|
@@ -2631,9 +2700,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2631
2700
|
``,
|
|
2632
2701
|
`## Aesthetic Guardrails`,
|
|
2633
2702
|
``,
|
|
2634
|
-
|
|
2635
|
-
`- 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.`,
|
|
2636
|
-
`- Choose aesthetics that fit the product and audience described in the user's prompt, not a generic developer-tool look.`,
|
|
2703
|
+
...aestheticGuardrails,
|
|
2637
2704
|
``,
|
|
2638
2705
|
...variantSteps,
|
|
2639
2706
|
`### Final verification`,
|
|
@@ -2655,6 +2722,8 @@ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
|
|
|
2655
2722
|
const startIndex = options.variantStartIndex ?? 1;
|
|
2656
2723
|
const variance = typeof proto.designVariance === "number" ? proto.designVariance : 70;
|
|
2657
2724
|
const varianceInfo = describeVariance(variance);
|
|
2725
|
+
const isLowFidelity = proto.fidelity === "low";
|
|
2726
|
+
const isPromotion = proto.fidelity === "high" && options.parentFidelity === "low";
|
|
2658
2727
|
const variantSteps = [];
|
|
2659
2728
|
for (let i = 0; i < variantsToProduce; i++) {
|
|
2660
2729
|
const idx = startIndex + i;
|
|
@@ -2662,7 +2731,7 @@ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
|
|
|
2662
2731
|
variantSteps.push(
|
|
2663
2732
|
`### Variant ${idx}: ${filename}`,
|
|
2664
2733
|
`1. Redesign variant ${idx} based on the feedback below, while keeping the core concept that worked.`,
|
|
2665
|
-
`2. Apply the same craft & polish bar as the original generation \u2014 portfolio quality, real microcopy, intentional typography, no Lorem ipsum, no broken images.`,
|
|
2734
|
+
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.`,
|
|
2666
2735
|
`3. Write the complete self-contained HTML to \`${repoDir}/${filename}\` using the Write tool.`,
|
|
2667
2736
|
`4. Verify the file was created by reading the first few lines of \`${repoDir}/${filename}\`.`,
|
|
2668
2737
|
``
|
|
@@ -2702,6 +2771,34 @@ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
|
|
|
2702
2771
|
desktop_app: "Desktop App",
|
|
2703
2772
|
logo: "Logo"
|
|
2704
2773
|
};
|
|
2774
|
+
const wireframeGuidelines = isLowFidelity ? [
|
|
2775
|
+
`## Wireframe & Sketch Guidelines (Balsamiq-Style)`,
|
|
2776
|
+
`This is a low-fidelity wireframe refinement. Maintain these constraints:`,
|
|
2777
|
+
`- Grayscale only: Use ONLY black, white, and gray colors. No accents, gradients, or color fills.`,
|
|
2778
|
+
`- Shantell Sans font: Load 'Shantell Sans' from Google Fonts and set it globally as the default typeface.`,
|
|
2779
|
+
`- Borders/shapes: Use sketchy/thick solid black borders (e.g. 2px). Flat rectangles with an "X" for image placeholders.`,
|
|
2780
|
+
`- Annotations/terse copy: Avoid polished marketing copy; use brief labels, greeked text, or annotations.`,
|
|
2781
|
+
`- No polish: No shadows, no gradients, no glassmorphism, no transitions or motion.`
|
|
2782
|
+
] : isPromotion ? [
|
|
2783
|
+
`## High-Fidelity Promotion Guidelines`,
|
|
2784
|
+
`You are promoting this prototype from a low-fidelity wireframe to a high-fidelity design:`,
|
|
2785
|
+
`- 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.`,
|
|
2786
|
+
`- Do NOT copy the grayscale wireframe styling, comic-style font, or diagonal-line placeholders.`
|
|
2787
|
+
] : [
|
|
2788
|
+
`## High-Fidelity Craft & Polish Guidelines`,
|
|
2789
|
+
`- Composition: real grid, intentional spacing, distinct visual hierarchy.`,
|
|
2790
|
+
`- Typography: Google Fonts display + text face pairing (e.g. Inter, Geist, etc.).`,
|
|
2791
|
+
`- Colors & depth: deliberate color palette, soft shadows, inner glows, subtle motion/transitions.`,
|
|
2792
|
+
`- Real microcopy: no Lorem Ipsum, realistic data, realistic dates and names.`
|
|
2793
|
+
];
|
|
2794
|
+
const aestheticGuardrails = isLowFidelity ? [
|
|
2795
|
+
`- Absolutely NO colors besides black, white, and grays. No gradients, no shadows, no smooth animations.`,
|
|
2796
|
+
`- 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.`,
|
|
2797
|
+
`- Ensure the body uses the 'Shantell Sans' font stack for a sketchy comic feel.`
|
|
2798
|
+
] : [
|
|
2799
|
+
`- Do NOT default to a terminal / hacker / CRT / monospace-green-on-black aesthetic unless the user's prompt explicitly asks for it.`,
|
|
2800
|
+
`- Do NOT force any single "style label" onto the variants unless the user asked for it.`
|
|
2801
|
+
];
|
|
2705
2802
|
return [
|
|
2706
2803
|
`${typeRoleMap[prototypeType] ?? typeRoleMap.web_app}`,
|
|
2707
2804
|
``,
|
|
@@ -2724,32 +2821,33 @@ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
|
|
|
2724
2821
|
existingVariants,
|
|
2725
2822
|
`## Instructions`,
|
|
2726
2823
|
``,
|
|
2727
|
-
`You MUST generate exactly ${
|
|
2824
|
+
`You MUST generate exactly ${variantsToProduce} REFINED HTML file(s) that incorporate the user's feedback above. Follow the steps below IN ORDER.`,
|
|
2728
2825
|
``,
|
|
2729
2826
|
`Study the previous variants carefully, then apply the user's feedback to improve them. Keep what works, change what the user asked to change.`,
|
|
2730
2827
|
``,
|
|
2828
|
+
...wireframeGuidelines,
|
|
2829
|
+
``,
|
|
2731
2830
|
`## Design Variance: ${varianceInfo.label} (${variance}/100)`,
|
|
2732
2831
|
``,
|
|
2733
2832
|
varianceInfo.summary,
|
|
2734
2833
|
``,
|
|
2735
2834
|
`## Aesthetic Guardrails`,
|
|
2736
2835
|
``,
|
|
2737
|
-
|
|
2738
|
-
`- Do NOT force any single "style label" onto the variants unless the user asked for it.`,
|
|
2836
|
+
...aestheticGuardrails,
|
|
2739
2837
|
``,
|
|
2740
2838
|
`Each file must be completely self-contained (inline all CSS and JS \u2014 no external dependencies). Tailwind CDN is acceptable.`,
|
|
2741
2839
|
``,
|
|
2742
2840
|
...variantSteps,
|
|
2743
2841
|
`### Final verification`,
|
|
2744
|
-
`After generating ALL ${
|
|
2842
|
+
`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.`,
|
|
2745
2843
|
``,
|
|
2746
2844
|
`IMPORTANT RULES:`,
|
|
2747
|
-
`- You MUST produce exactly
|
|
2845
|
+
`- You MUST produce exactly these file(s): ${variantList.join(", ")}`,
|
|
2748
2846
|
`- Generate them ONE AT A TIME \u2014 design each variant, write the file, then move to the next.`,
|
|
2749
2847
|
`- Respect the Design Variance level above.`,
|
|
2750
2848
|
`- Each file must be a complete, functional page \u2014 pure HTML/CSS/JS, no external libraries (Tailwind CDN is acceptable).`,
|
|
2751
2849
|
`- Do NOT upload or POST the files anywhere. The watch handler will upload them automatically after you exit.`,
|
|
2752
|
-
`- Do NOT exit until ALL ${
|
|
2850
|
+
`- Do NOT exit until ALL ${variantsToProduce} file(s) have been written and verified.`
|
|
2753
2851
|
].join("\n");
|
|
2754
2852
|
}
|
|
2755
2853
|
function buildAgentArgs(agent, prompt2, mode, sessionId, name, resumeSession = false, systemPrompt, maxTurns, claudeModel) {
|
|
@@ -2953,12 +3051,12 @@ var watchCommand = new Command9("watch").description(
|
|
|
2953
3051
|
agentAvailability.set(candidate, available);
|
|
2954
3052
|
return available;
|
|
2955
3053
|
}
|
|
2956
|
-
async function resolveAgentChain(
|
|
3054
|
+
async function resolveAgentChain(preferred2) {
|
|
2957
3055
|
const availability = {};
|
|
2958
3056
|
for (const candidate of ["claude", "codex", "antigravity"]) {
|
|
2959
3057
|
availability[candidate] = await isAgentAvailable(candidate);
|
|
2960
3058
|
}
|
|
2961
|
-
return getAvailableAgentFallbackChain(
|
|
3059
|
+
return getAvailableAgentFallbackChain(preferred2, availability);
|
|
2962
3060
|
}
|
|
2963
3061
|
async function moveTaskToError(task, prefix, reason) {
|
|
2964
3062
|
try {
|
|
@@ -3375,6 +3473,15 @@ var watchCommand = new Command9("watch").description(
|
|
|
3375
3473
|
};
|
|
3376
3474
|
await launchAttempt(attemptOrder[attemptIndex]);
|
|
3377
3475
|
}
|
|
3476
|
+
function dispatchTaskSafely(task, repoDir) {
|
|
3477
|
+
void dispatchTask(task, repoDir).catch(async (err) => {
|
|
3478
|
+
const prefix = taskTag(shortId(task.id));
|
|
3479
|
+
const reason = `Task setup failed: ${err.message}`;
|
|
3480
|
+
logError(prefix, reason);
|
|
3481
|
+
queued.delete(task.id);
|
|
3482
|
+
await moveTaskToError(task, prefix, reason);
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3378
3485
|
async function dispatchPlanModeTask(task, repoDir) {
|
|
3379
3486
|
const sid = shortId(task.id);
|
|
3380
3487
|
const prefix = taskTag(sid);
|
|
@@ -3397,13 +3504,21 @@ var watchCommand = new Command9("watch").description(
|
|
|
3397
3504
|
}
|
|
3398
3505
|
} catch {
|
|
3399
3506
|
}
|
|
3507
|
+
let protoRefs = [];
|
|
3508
|
+
try {
|
|
3509
|
+
protoRefs = await api.get(`/api/tasks/${task.id}/prototypes`);
|
|
3510
|
+
if (protoRefs.length > 0) {
|
|
3511
|
+
logInfo(prefix, `${paint("cyan", String(protoRefs.length))} linked prototype(s)`);
|
|
3512
|
+
}
|
|
3513
|
+
} catch {
|
|
3514
|
+
}
|
|
3400
3515
|
const isRevision = !!(existingPlanResource && feedbackUpdates.length > 0);
|
|
3401
3516
|
await postTaskUpdate(
|
|
3402
3517
|
task.id,
|
|
3403
3518
|
isRevision ? "Agent dispatched in plan mode \u2014 revising PRD based on feedback" : "Agent dispatched in plan mode \u2014 generating PRD",
|
|
3404
3519
|
"system"
|
|
3405
3520
|
);
|
|
3406
|
-
const prompt2 = buildPrdPrompt(task, repoDir, existingPlanResource?.content, feedbackUpdates);
|
|
3521
|
+
const prompt2 = buildPrdPrompt(task, repoDir, existingPlanResource?.content, feedbackUpdates, protoRefs);
|
|
3407
3522
|
const attemptOrder = await resolveAgentChain(agent);
|
|
3408
3523
|
if (attemptOrder.length === 0) {
|
|
3409
3524
|
logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
|
|
@@ -3547,10 +3662,12 @@ var watchCommand = new Command9("watch").description(
|
|
|
3547
3662
|
const dedupedRequested = Array.from(new Set(requested));
|
|
3548
3663
|
const key = `proto-${proto.id}`;
|
|
3549
3664
|
let parentFiles = [];
|
|
3665
|
+
let parentFidelity;
|
|
3550
3666
|
if (proto.parentId && proto.refinementFeedback) {
|
|
3551
3667
|
try {
|
|
3552
3668
|
const parent = await api.get(`/api/prototypes/${proto.parentId}`);
|
|
3553
3669
|
parentFiles = parent.files ?? [];
|
|
3670
|
+
parentFidelity = parent.fidelity;
|
|
3554
3671
|
} catch (err) {
|
|
3555
3672
|
logError(prefix, `Failed to fetch parent prototype: ${err.message}`);
|
|
3556
3673
|
}
|
|
@@ -3561,7 +3678,8 @@ var watchCommand = new Command9("watch").description(
|
|
|
3561
3678
|
variantStartIndex: startIndex,
|
|
3562
3679
|
variantsToProduce,
|
|
3563
3680
|
agentLabel,
|
|
3564
|
-
referenceImagePaths: refImagePaths
|
|
3681
|
+
referenceImagePaths: refImagePaths,
|
|
3682
|
+
parentFidelity
|
|
3565
3683
|
});
|
|
3566
3684
|
}
|
|
3567
3685
|
return buildPrototypePrompt(proto, sliceDir, {
|
|
@@ -3571,254 +3689,191 @@ var watchCommand = new Command9("watch").description(
|
|
|
3571
3689
|
referenceImagePaths: refImagePaths
|
|
3572
3690
|
});
|
|
3573
3691
|
};
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3692
|
+
const totalVariants = Math.max(1, proto.variantCount);
|
|
3693
|
+
let agentChainForVariant;
|
|
3694
|
+
if (dedupedRequested.length >= 1) {
|
|
3695
|
+
agentChainForVariant = (variantIndex) => [
|
|
3696
|
+
dedupedRequested[(variantIndex - 1) % dedupedRequested.length]
|
|
3697
|
+
];
|
|
3698
|
+
logDispatch(
|
|
3699
|
+
prefix,
|
|
3700
|
+
`streaming ${totalVariants} variant(s) across ${dedupedRequested.join(", ")}`
|
|
3701
|
+
);
|
|
3702
|
+
} else {
|
|
3703
|
+
const chain = await resolveAgentChain(agent);
|
|
3704
|
+
if (chain.length === 0) {
|
|
3705
|
+
logError(prefix, `No available agents found for ${agent}`);
|
|
3579
3706
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" }).catch(() => {
|
|
3580
3707
|
});
|
|
3581
3708
|
cleanupRefDir(refImageDir);
|
|
3582
3709
|
queued.delete(key);
|
|
3583
3710
|
return;
|
|
3584
3711
|
}
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
process: void 0,
|
|
3588
|
-
title: proto.title,
|
|
3589
|
-
repoDir,
|
|
3590
|
-
startedAt: Date.now(),
|
|
3591
|
-
lastActivityAt: Date.now()
|
|
3592
|
-
};
|
|
3593
|
-
let attemptIndex = 0;
|
|
3594
|
-
const launchAttempt = async (attemptAgent) => {
|
|
3595
|
-
let spawnFailureReason = null;
|
|
3596
|
-
const child = spawnAgent(
|
|
3597
|
-
attemptAgent,
|
|
3598
|
-
repoDir,
|
|
3599
|
-
prompt2,
|
|
3600
|
-
prefix,
|
|
3601
|
-
void 0,
|
|
3602
|
-
void 0,
|
|
3603
|
-
proto.title,
|
|
3604
|
-
false,
|
|
3605
|
-
(err) => {
|
|
3606
|
-
spawnFailureReason = err.message;
|
|
3607
|
-
}
|
|
3608
|
-
);
|
|
3609
|
-
activeEntry2.process = child;
|
|
3610
|
-
activeEntry2.currentAgent = attemptAgent;
|
|
3611
|
-
active.set(key, activeEntry2);
|
|
3612
|
-
child.on("exit", async (code) => {
|
|
3613
|
-
if (active.get(key)?.process === child) {
|
|
3614
|
-
active.delete(key);
|
|
3615
|
-
}
|
|
3616
|
-
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
3617
|
-
if (failedAttempt && !activeEntry2.terminatedForError) {
|
|
3618
|
-
const nextAgent = attemptOrder[attemptIndex + 1];
|
|
3619
|
-
if (nextAgent) {
|
|
3620
|
-
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
3621
|
-
logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying prototype generation with ${nextAgent}`);
|
|
3622
|
-
attemptIndex += 1;
|
|
3623
|
-
await launchAttempt(nextAgent);
|
|
3624
|
-
return;
|
|
3625
|
-
}
|
|
3626
|
-
}
|
|
3627
|
-
finishing.add(key);
|
|
3628
|
-
try {
|
|
3629
|
-
if (code === 0) {
|
|
3630
|
-
try {
|
|
3631
|
-
const protoPattern = /^prototype-\d+\.html$/;
|
|
3632
|
-
const found = readdirSync(repoDir).filter((f) => protoPattern.test(f)).sort();
|
|
3633
|
-
const files = found.map((f) => ({
|
|
3634
|
-
name: f,
|
|
3635
|
-
content: readFileSync5(resolve2(repoDir, f), "utf-8"),
|
|
3636
|
-
agent: attemptAgent
|
|
3637
|
-
}));
|
|
3638
|
-
if (files.length === 0) {
|
|
3639
|
-
logError(prefix, `No prototype HTML files found in ${repoDir}`);
|
|
3640
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
3641
|
-
return;
|
|
3642
|
-
}
|
|
3643
|
-
const variantModels = files.map((f) => f.agent ?? null);
|
|
3644
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "completed", files, variantModels });
|
|
3645
|
-
logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${files.length} file(s)`);
|
|
3646
|
-
for (const file of files) {
|
|
3647
|
-
try {
|
|
3648
|
-
unlinkSync(resolve2(repoDir, file.name));
|
|
3649
|
-
} catch {
|
|
3650
|
-
}
|
|
3651
|
-
}
|
|
3652
|
-
} catch (err) {
|
|
3653
|
-
logError(prefix, `Failed to upload prototype: ${err.message}`);
|
|
3654
|
-
try {
|
|
3655
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
3656
|
-
} catch {
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
} else {
|
|
3660
|
-
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
3661
|
-
logError(prefix, `"${paint("bold", proto.title)}" prototype failed via ${attemptAgent} (${failureDetail})`);
|
|
3662
|
-
try {
|
|
3663
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
3664
|
-
} catch {
|
|
3665
|
-
}
|
|
3666
|
-
}
|
|
3667
|
-
} finally {
|
|
3668
|
-
cleanupRefDir(refImageDir);
|
|
3669
|
-
queued.delete(key);
|
|
3670
|
-
finishing.delete(key);
|
|
3671
|
-
}
|
|
3672
|
-
});
|
|
3673
|
-
};
|
|
3674
|
-
await launchAttempt(attemptOrder[attemptIndex]);
|
|
3675
|
-
return;
|
|
3676
|
-
}
|
|
3677
|
-
const totalVariants = proto.variantCount;
|
|
3678
|
-
const slices = [];
|
|
3679
|
-
const baseShare = Math.floor(totalVariants / dedupedRequested.length);
|
|
3680
|
-
const remainder = totalVariants % dedupedRequested.length;
|
|
3681
|
-
let cursor = 1;
|
|
3682
|
-
for (let i = 0; i < dedupedRequested.length; i++) {
|
|
3683
|
-
const a = dedupedRequested[i];
|
|
3684
|
-
const count = baseShare + (i < remainder ? 1 : 0);
|
|
3685
|
-
if (count <= 0) continue;
|
|
3686
|
-
const dir = resolve2(repoDir, `.mr-proto-${a}`);
|
|
3687
|
-
try {
|
|
3688
|
-
mkdirSync3(dir, { recursive: true });
|
|
3689
|
-
} catch {
|
|
3690
|
-
}
|
|
3691
|
-
for (const f of readdirSync(dir).filter((f2) => stalePattern.test(f2))) {
|
|
3692
|
-
try {
|
|
3693
|
-
unlinkSync(resolve2(dir, f));
|
|
3694
|
-
} catch {
|
|
3695
|
-
}
|
|
3696
|
-
}
|
|
3697
|
-
slices.push({ agentLabel: a, startIndex: cursor, count, dir });
|
|
3698
|
-
cursor += count;
|
|
3712
|
+
agentChainForVariant = () => chain;
|
|
3713
|
+
logDispatch(prefix, `streaming ${totalVariants} variant(s) via ${chain[0]}`);
|
|
3699
3714
|
}
|
|
3700
|
-
if (slices.length === 0) {
|
|
3701
|
-
logError(prefix, `Could not assign any variants to selected agents`);
|
|
3702
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" }).catch(() => {
|
|
3703
|
-
});
|
|
3704
|
-
cleanupRefDir(refImageDir);
|
|
3705
|
-
queued.delete(key);
|
|
3706
|
-
return;
|
|
3707
|
-
}
|
|
3708
|
-
logDispatch(
|
|
3709
|
-
prefix,
|
|
3710
|
-
`multi-agent dispatch: ${slices.map((s) => `${s.agentLabel}\xD7${s.count}`).join(", ")}`
|
|
3711
|
-
);
|
|
3712
3715
|
const activeEntry = {
|
|
3713
3716
|
process: void 0,
|
|
3714
3717
|
title: proto.title,
|
|
3715
3718
|
repoDir,
|
|
3716
3719
|
startedAt: Date.now(),
|
|
3717
3720
|
lastActivityAt: Date.now(),
|
|
3718
|
-
outputBytes: 0
|
|
3721
|
+
outputBytes: 0,
|
|
3722
|
+
children: /* @__PURE__ */ new Set()
|
|
3719
3723
|
};
|
|
3720
3724
|
active.set(key, activeEntry);
|
|
3721
|
-
const
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
+
const protoPattern = /^prototype-\d+\.html$/;
|
|
3726
|
+
const runVariant = (variantIndex) => {
|
|
3727
|
+
const chain = agentChainForVariant(variantIndex);
|
|
3728
|
+
const sliceDir = resolve2(repoDir, `.mr-proto-v${variantIndex}`);
|
|
3729
|
+
try {
|
|
3730
|
+
mkdirSync3(sliceDir, { recursive: true });
|
|
3731
|
+
} catch {
|
|
3732
|
+
}
|
|
3733
|
+
for (const f of readdirSync(sliceDir).filter((f2) => stalePattern.test(f2))) {
|
|
3734
|
+
try {
|
|
3735
|
+
unlinkSync(resolve2(sliceDir, f));
|
|
3736
|
+
} catch {
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
const prompt2 = buildPromptForSlice(variantIndex, 1, chain[0], sliceDir);
|
|
3740
|
+
return new Promise((res) => {
|
|
3741
|
+
let attemptIndex = 0;
|
|
3742
|
+
const finishSlice = (ok) => {
|
|
3743
|
+
try {
|
|
3744
|
+
for (const f of readdirSync(sliceDir)) {
|
|
3745
|
+
try {
|
|
3746
|
+
unlinkSync(resolve2(sliceDir, f));
|
|
3747
|
+
} catch {
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
rmdirSync(sliceDir);
|
|
3751
|
+
} catch {
|
|
3752
|
+
}
|
|
3753
|
+
res(ok);
|
|
3754
|
+
};
|
|
3755
|
+
const launch = (attemptAgent) => {
|
|
3725
3756
|
let spawnFailureReason = null;
|
|
3726
3757
|
const child = spawnAgent(
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3758
|
+
attemptAgent,
|
|
3759
|
+
sliceDir,
|
|
3760
|
+
prompt2,
|
|
3730
3761
|
prefix,
|
|
3762
|
+
() => {
|
|
3763
|
+
activeEntry.lastActivityAt = Date.now();
|
|
3764
|
+
},
|
|
3731
3765
|
void 0,
|
|
3732
|
-
|
|
3733
|
-
`${proto.title} [${slice.agentLabel}]`,
|
|
3766
|
+
`${proto.title} [variant ${variantIndex}/${totalVariants}]`,
|
|
3734
3767
|
false,
|
|
3735
3768
|
(err) => {
|
|
3736
3769
|
spawnFailureReason = err.message;
|
|
3737
3770
|
}
|
|
3738
3771
|
);
|
|
3739
3772
|
activeEntry.process = child;
|
|
3740
|
-
activeEntry.currentAgent =
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3773
|
+
activeEntry.currentAgent = attemptAgent;
|
|
3774
|
+
activeEntry.children?.add(child);
|
|
3775
|
+
child.on("exit", async (code) => {
|
|
3776
|
+
activeEntry.children?.delete(child);
|
|
3777
|
+
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
3778
|
+
if (failedAttempt && !activeEntry.terminatedForError) {
|
|
3779
|
+
const nextAgent = chain[attemptIndex + 1];
|
|
3780
|
+
if (nextAgent) {
|
|
3781
|
+
const detail = spawnFailureReason ?? `exit code ${code}`;
|
|
3782
|
+
logWarn(prefix, `variant ${variantIndex}: ${attemptAgent} failed (${detail}) \u2014 retrying with ${nextAgent}`);
|
|
3783
|
+
attemptIndex += 1;
|
|
3784
|
+
launch(nextAgent);
|
|
3785
|
+
return;
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
if (activeEntry.terminatedForError) {
|
|
3789
|
+
finishSlice(false);
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
if (code !== 0) {
|
|
3793
|
+
const detail = spawnFailureReason ?? `exit code ${code}`;
|
|
3794
|
+
logError(prefix, `variant ${variantIndex} failed via ${attemptAgent} (${detail})`);
|
|
3795
|
+
finishSlice(false);
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
try {
|
|
3799
|
+
const found = readdirSync(sliceDir).filter((f) => protoPattern.test(f)).sort();
|
|
3800
|
+
if (found.length === 0) {
|
|
3801
|
+
logError(prefix, `variant ${variantIndex}: no HTML file produced`);
|
|
3802
|
+
finishSlice(false);
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
const content = readFileSync5(resolve2(sliceDir, found[0]), "utf-8");
|
|
3806
|
+
const file = {
|
|
3807
|
+
name: `prototype-${variantIndex}.html`,
|
|
3808
|
+
content,
|
|
3809
|
+
agent: attemptAgent
|
|
3810
|
+
};
|
|
3811
|
+
await api.post(`/api/prototypes/${proto.id}/variants`, { file });
|
|
3812
|
+
logSuccess(prefix, `variant ${variantIndex}/${totalVariants} ready ${paint("gray", `(${attemptAgent})`)}`);
|
|
3813
|
+
finishSlice(true);
|
|
3814
|
+
} catch (err) {
|
|
3815
|
+
logError(prefix, `variant ${variantIndex}: failed to upload (${err.message})`);
|
|
3816
|
+
finishSlice(false);
|
|
3746
3817
|
}
|
|
3747
|
-
res({ agent: slice.agentLabel, ok, reason: spawnFailureReason });
|
|
3748
3818
|
});
|
|
3749
|
-
}
|
|
3750
|
-
|
|
3751
|
-
|
|
3819
|
+
};
|
|
3820
|
+
launch(chain[attemptIndex]);
|
|
3821
|
+
});
|
|
3822
|
+
};
|
|
3823
|
+
const variantIndices = Array.from({ length: totalVariants }, (_, i) => i + 1);
|
|
3824
|
+
const VARIANT_CONCURRENCY = Math.min(3, totalVariants);
|
|
3825
|
+
let nextVariant = 0;
|
|
3826
|
+
let okCount = 0;
|
|
3827
|
+
const workers = Array.from({ length: VARIANT_CONCURRENCY }, async () => {
|
|
3828
|
+
while (true) {
|
|
3829
|
+
if (activeEntry.terminatedForError) return;
|
|
3830
|
+
const idx = nextVariant++;
|
|
3831
|
+
if (idx >= variantIndices.length) return;
|
|
3832
|
+
const ok = await runVariant(variantIndices[idx]);
|
|
3833
|
+
if (ok) okCount++;
|
|
3834
|
+
}
|
|
3835
|
+
});
|
|
3836
|
+
await Promise.all(workers);
|
|
3752
3837
|
if (active.get(key) === activeEntry) {
|
|
3753
3838
|
active.delete(key);
|
|
3754
3839
|
}
|
|
3840
|
+
if (activeEntry.terminatedForError) {
|
|
3841
|
+
cleanupRefDir(refImageDir);
|
|
3842
|
+
queued.delete(key);
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3755
3845
|
finishing.add(key);
|
|
3756
3846
|
try {
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
for (const slice of slices) {
|
|
3760
|
-
let found = [];
|
|
3761
|
-
try {
|
|
3762
|
-
found = readdirSync(slice.dir).filter((f) => protoPattern.test(f)).sort();
|
|
3763
|
-
} catch {
|
|
3764
|
-
}
|
|
3765
|
-
for (const f of found) {
|
|
3766
|
-
try {
|
|
3767
|
-
const content = readFileSync5(resolve2(slice.dir, f), "utf-8");
|
|
3768
|
-
collected.push({ name: f, content, agent: slice.agentLabel });
|
|
3769
|
-
} catch (err) {
|
|
3770
|
-
logError(prefix, `Failed reading ${f} from ${slice.dir}: ${err.message}`);
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
}
|
|
3774
|
-
collected.sort((a, b) => {
|
|
3775
|
-
const na = parseInt(a.name.match(/\d+/)?.[0] ?? "0", 10);
|
|
3776
|
-
const nb = parseInt(b.name.match(/\d+/)?.[0] ?? "0", 10);
|
|
3777
|
-
return na - nb;
|
|
3778
|
-
});
|
|
3779
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3780
|
-
const finalFiles = [];
|
|
3781
|
-
let renumberCursor = 1;
|
|
3782
|
-
for (const f of collected) {
|
|
3783
|
-
let name = f.name;
|
|
3784
|
-
if (seen.has(name)) {
|
|
3785
|
-
while (seen.has(`prototype-${renumberCursor}.html`)) renumberCursor++;
|
|
3786
|
-
name = `prototype-${renumberCursor}.html`;
|
|
3787
|
-
}
|
|
3788
|
-
seen.add(name);
|
|
3789
|
-
finalFiles.push({ name, content: f.content, agent: f.agent });
|
|
3790
|
-
}
|
|
3791
|
-
const allOk = sliceResults.every((r) => r.ok);
|
|
3792
|
-
if (finalFiles.length === 0) {
|
|
3793
|
-
logError(prefix, `No prototype HTML files produced by any agent`);
|
|
3847
|
+
if (okCount === 0) {
|
|
3848
|
+
logError(prefix, `No prototype variants were produced`);
|
|
3794
3849
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
3795
3850
|
} else {
|
|
3796
|
-
|
|
3851
|
+
let finalFiles = [];
|
|
3852
|
+
try {
|
|
3853
|
+
const current = await api.get(`/api/prototypes/${proto.id}`);
|
|
3854
|
+
finalFiles = (current.files ?? []).slice();
|
|
3855
|
+
} catch (err) {
|
|
3856
|
+
logWarn(prefix, `Could not re-read files before completing: ${err.message}`);
|
|
3857
|
+
}
|
|
3858
|
+
finalFiles.sort((a, b) => {
|
|
3859
|
+
const na = parseInt(a.name.match(/\d+/)?.[0] ?? "0", 10);
|
|
3860
|
+
const nb = parseInt(b.name.match(/\d+/)?.[0] ?? "0", 10);
|
|
3861
|
+
return na - nb;
|
|
3862
|
+
});
|
|
3863
|
+
const patch = {
|
|
3797
3864
|
status: "completed",
|
|
3798
|
-
files: finalFiles,
|
|
3799
3865
|
variantModels: finalFiles.map((f) => f.agent ?? null)
|
|
3800
|
-
}
|
|
3801
|
-
if (
|
|
3802
|
-
|
|
3866
|
+
};
|
|
3867
|
+
if (finalFiles.length > 0) patch.files = finalFiles;
|
|
3868
|
+
await api.patch(`/api/prototypes/${proto.id}`, patch);
|
|
3869
|
+
if (okCount === totalVariants) {
|
|
3870
|
+
logSuccess(prefix, `"${paint("bold", proto.title)}" completed \u2014 ${okCount} variant(s) streamed in`);
|
|
3803
3871
|
} else {
|
|
3804
|
-
|
|
3805
|
-
logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${finalFiles.length} file(s) (partial \u2014 ${failed2} failed)`);
|
|
3806
|
-
}
|
|
3807
|
-
}
|
|
3808
|
-
for (const slice of slices) {
|
|
3809
|
-
try {
|
|
3810
|
-
for (const f of readdirSync(slice.dir)) {
|
|
3811
|
-
try {
|
|
3812
|
-
unlinkSync(resolve2(slice.dir, f));
|
|
3813
|
-
} catch {
|
|
3814
|
-
}
|
|
3815
|
-
}
|
|
3816
|
-
rmdirSync(slice.dir);
|
|
3817
|
-
} catch {
|
|
3872
|
+
logSuccess(prefix, `"${paint("bold", proto.title)}" completed \u2014 ${okCount}/${totalVariants} variant(s) (partial)`);
|
|
3818
3873
|
}
|
|
3819
3874
|
}
|
|
3820
3875
|
} catch (err) {
|
|
3821
|
-
logError(prefix, `Failed to
|
|
3876
|
+
logError(prefix, `Failed to finalize prototype: ${err.message}`);
|
|
3822
3877
|
try {
|
|
3823
3878
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
3824
3879
|
} catch {
|
|
@@ -4052,7 +4107,7 @@ ${divider}`);
|
|
|
4052
4107
|
`${paint("cyan", "?")} Approve task "${paint("bold", task.title)}"? [y/N] `
|
|
4053
4108
|
);
|
|
4054
4109
|
if (approved) {
|
|
4055
|
-
|
|
4110
|
+
dispatchTaskSafely(task, repoDir);
|
|
4056
4111
|
} else {
|
|
4057
4112
|
logWarn(prefix, `"${paint("bold", task.title)}" denied \u2014 will not retry`);
|
|
4058
4113
|
failed.set(task.id, "denied by user");
|
|
@@ -4215,7 +4270,7 @@ ${divider}`);
|
|
|
4215
4270
|
approvalQueue.push({ task, repoDir });
|
|
4216
4271
|
processApprovalQueue();
|
|
4217
4272
|
} else {
|
|
4218
|
-
|
|
4273
|
+
dispatchTaskSafely(task, repoDir);
|
|
4219
4274
|
}
|
|
4220
4275
|
}
|
|
4221
4276
|
let prototypes = [];
|
|
@@ -4229,7 +4284,11 @@ ${divider}`);
|
|
|
4229
4284
|
if (key.startsWith("proto-") && !inProgressProtoKeys.has(key)) {
|
|
4230
4285
|
logWarn(watchTag(), `Prototype ${paint("yellow", key)} no longer in_progress, terminating\u2026`);
|
|
4231
4286
|
entry.terminatedForError = true;
|
|
4232
|
-
entry.
|
|
4287
|
+
if (entry.children && entry.children.size > 0) {
|
|
4288
|
+
for (const child of entry.children) child.kill("SIGTERM");
|
|
4289
|
+
} else {
|
|
4290
|
+
entry.process.kill("SIGTERM");
|
|
4291
|
+
}
|
|
4233
4292
|
active.delete(key);
|
|
4234
4293
|
queued.delete(key);
|
|
4235
4294
|
}
|
|
@@ -4878,23 +4937,35 @@ var prototypeCommand = new Command15("prototype").description("Manage prototypes
|
|
|
4878
4937
|
for (const p of prototypes) {
|
|
4879
4938
|
const date = new Date(p.createdAt).toLocaleDateString();
|
|
4880
4939
|
const typeLabel2 = typeLabels[p.prototypeType] ?? p.prototypeType ?? "web";
|
|
4940
|
+
const fidelityLabel = p.fidelity === "low" ? "wireframe" : "hi-fi";
|
|
4881
4941
|
console.log(
|
|
4882
|
-
` ${paint4("bold", p.title)} ${statusBadge(p.status)} ${paint4("blue", `[${typeLabel2}]`)} ${paint4("gray", p.id.slice(0, 8))} ${paint4("dim", date)}`
|
|
4942
|
+
` ${paint4("bold", p.title)} ${statusBadge(p.status)} ${paint4("blue", `[${typeLabel2}/${fidelityLabel}]`)} ${paint4("gray", p.id.slice(0, 8))} ${paint4("dim", date)}`
|
|
4883
4943
|
);
|
|
4884
4944
|
console.log(` ${paint4("dim", p.prompt.slice(0, 80) + (p.prompt.length > 80 ? "\u2026" : ""))}`);
|
|
4885
4945
|
console.log();
|
|
4886
4946
|
}
|
|
4887
4947
|
})
|
|
4888
4948
|
).addCommand(
|
|
4889
|
-
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) => {
|
|
4949
|
+
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) => {
|
|
4890
4950
|
const projectId = opts.project ?? getLinkedProjectId();
|
|
4891
4951
|
const variantCount = Math.max(1, Math.min(50, parseInt(opts.variants, 10) || 5));
|
|
4892
4952
|
const validTypes = ["web_app", "mobile_app", "desktop_app", "logo"];
|
|
4893
4953
|
const prototypeType = validTypes.includes(opts.type) ? opts.type : "web_app";
|
|
4954
|
+
const validFidelities = ["low", "high"];
|
|
4955
|
+
if (opts.fidelity && !validFidelities.includes(opts.fidelity)) {
|
|
4956
|
+
console.error(`Invalid fidelity value: ${opts.fidelity}. Supported values: ${validFidelities.join(", ")}`);
|
|
4957
|
+
process.exit(1);
|
|
4958
|
+
}
|
|
4959
|
+
const fidelity = opts.fidelity || "high";
|
|
4960
|
+
if (prototypeType === "logo" && fidelity === "low") {
|
|
4961
|
+
console.error("Logo prototypes do not support low fidelity.");
|
|
4962
|
+
process.exit(1);
|
|
4963
|
+
}
|
|
4894
4964
|
const prototype = await api.post("/api/prototypes", {
|
|
4895
4965
|
title,
|
|
4896
4966
|
prompt: opts.prompt,
|
|
4897
4967
|
prototypeType,
|
|
4968
|
+
fidelity,
|
|
4898
4969
|
variantCount,
|
|
4899
4970
|
projectId: projectId ?? null
|
|
4900
4971
|
});
|
|
@@ -4908,6 +4979,7 @@ var prototypeCommand = new Command15("prototype").description("Manage prototypes
|
|
|
4908
4979
|
console.log(` ${paint4("green", "\u2713")} Created prototype: ${paint4("bold", prototype.title)}`);
|
|
4909
4980
|
console.log(` ${paint4("gray", "ID:")} ${prototype.id}`);
|
|
4910
4981
|
console.log(` ${paint4("gray", "Type:")} ${typeLabels[prototype.prototypeType] ?? prototype.prototypeType}`);
|
|
4982
|
+
console.log(` ${paint4("gray", "Fidelity:")} ${prototype.fidelity === "low" ? "Low (wireframe)" : "High (hi-fi)"}`);
|
|
4911
4983
|
if (!prototype.projectId) {
|
|
4912
4984
|
console.log(` ${paint4("gray", "Project:")} none (will generate in the active watch directory)`);
|
|
4913
4985
|
}
|
|
@@ -5208,7 +5280,7 @@ function printResults(checks) {
|
|
|
5208
5280
|
return allOk;
|
|
5209
5281
|
}
|
|
5210
5282
|
async function autoFix(checks, agent) {
|
|
5211
|
-
const { spawn:
|
|
5283
|
+
const { spawn: spawn9 } = await import("child_process");
|
|
5212
5284
|
const ghInstalled = checks.find((c14) => c14.name === "GitHub CLI (gh)").ok;
|
|
5213
5285
|
const ghAuthed = checks.find((c14) => c14.name === "GitHub CLI auth").ok;
|
|
5214
5286
|
const mrAuthed = checks.find((c14) => c14.name === "Mr. Manager CLI auth").ok;
|
|
@@ -5217,7 +5289,7 @@ async function autoFix(checks, agent) {
|
|
|
5217
5289
|
console.log(paint5("cyan", " Installing Claude Code..."));
|
|
5218
5290
|
console.log(paint5("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
|
|
5219
5291
|
await new Promise((resolve9) => {
|
|
5220
|
-
const child =
|
|
5292
|
+
const child = spawn9("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], { stdio: "inherit" });
|
|
5221
5293
|
child.on("exit", () => resolve9());
|
|
5222
5294
|
});
|
|
5223
5295
|
console.log("");
|
|
@@ -5225,7 +5297,7 @@ async function autoFix(checks, agent) {
|
|
|
5225
5297
|
if (ghInstalled && !ghAuthed) {
|
|
5226
5298
|
console.log(paint5("cyan", " Running gh auth login..."));
|
|
5227
5299
|
await new Promise((resolve9) => {
|
|
5228
|
-
const child =
|
|
5300
|
+
const child = spawn9("gh", ["auth", "login"], { stdio: "inherit" });
|
|
5229
5301
|
child.on("exit", () => resolve9());
|
|
5230
5302
|
});
|
|
5231
5303
|
console.log("");
|
|
@@ -5234,7 +5306,7 @@ async function autoFix(checks, agent) {
|
|
|
5234
5306
|
console.log(paint5("cyan", " Running mr login..."));
|
|
5235
5307
|
const entry = process.argv[1];
|
|
5236
5308
|
await new Promise((resolve9) => {
|
|
5237
|
-
const child =
|
|
5309
|
+
const child = spawn9(process.execPath, [entry, "login"], { stdio: "inherit" });
|
|
5238
5310
|
child.on("exit", () => resolve9());
|
|
5239
5311
|
});
|
|
5240
5312
|
console.log("");
|
|
@@ -6020,13 +6092,92 @@ var noMrCommand = new Command24("no-mr").description("Signal that a task does no
|
|
|
6020
6092
|
|
|
6021
6093
|
// cli/commands/review.ts
|
|
6022
6094
|
import { Command as Command26 } from "commander";
|
|
6023
|
-
import {
|
|
6095
|
+
import { execSync as execSync6 } from "child_process";
|
|
6024
6096
|
import { existsSync as existsSync14, statSync as statSync2 } from "fs";
|
|
6025
6097
|
|
|
6026
6098
|
// cli/commands/review-apply.ts
|
|
6027
6099
|
import { Command as Command25 } from "commander";
|
|
6028
|
-
import {
|
|
6100
|
+
import { execSync as execSync5 } from "child_process";
|
|
6029
6101
|
import { existsSync as existsSync13 } from "fs";
|
|
6102
|
+
|
|
6103
|
+
// lib/review/agent.ts
|
|
6104
|
+
import { spawn as spawn7, exec as exec3 } from "child_process";
|
|
6105
|
+
var AGENT_BINARIES2 = {
|
|
6106
|
+
claude: "claude",
|
|
6107
|
+
codex: "codex",
|
|
6108
|
+
antigravity: "agy"
|
|
6109
|
+
};
|
|
6110
|
+
function commandExists3(cmd) {
|
|
6111
|
+
return new Promise((resolve9) => {
|
|
6112
|
+
exec3(`command -v ${cmd}`, (err) => resolve9(!err));
|
|
6113
|
+
});
|
|
6114
|
+
}
|
|
6115
|
+
async function resolveReviewAgentChain(preferred2 = "claude") {
|
|
6116
|
+
const availability = {};
|
|
6117
|
+
for (const candidate of ["claude", "codex", "antigravity"]) {
|
|
6118
|
+
availability[candidate] = await commandExists3(AGENT_BINARIES2[candidate]);
|
|
6119
|
+
}
|
|
6120
|
+
return getAvailableAgentFallbackChain(preferred2, availability);
|
|
6121
|
+
}
|
|
6122
|
+
function buildArgs(agent, prompt2) {
|
|
6123
|
+
if (agent === "codex") {
|
|
6124
|
+
return ["-a", "never", "exec", "-s", "danger-full-access", prompt2];
|
|
6125
|
+
}
|
|
6126
|
+
if (agent === "antigravity") {
|
|
6127
|
+
return ["-p", prompt2, "--dangerously-skip-permissions"];
|
|
6128
|
+
}
|
|
6129
|
+
return ["-p", "--dangerously-skip-permissions", prompt2];
|
|
6130
|
+
}
|
|
6131
|
+
function runOnce(agent, prompt2, opts) {
|
|
6132
|
+
return new Promise((resolve9) => {
|
|
6133
|
+
const child = spawn7(AGENT_BINARIES2[agent], buildArgs(agent, prompt2), {
|
|
6134
|
+
cwd: opts.cwd,
|
|
6135
|
+
stdio: opts.capture ? ["ignore", "pipe", "pipe"] : ["ignore", "inherit", "inherit"]
|
|
6136
|
+
});
|
|
6137
|
+
let output = "";
|
|
6138
|
+
if (opts.capture) {
|
|
6139
|
+
child.stdout?.on("data", (d) => {
|
|
6140
|
+
output += d.toString();
|
|
6141
|
+
});
|
|
6142
|
+
child.stderr?.on("data", () => {
|
|
6143
|
+
});
|
|
6144
|
+
}
|
|
6145
|
+
child.on("error", () => {
|
|
6146
|
+
resolve9({ ok: false, output, spawnError: true });
|
|
6147
|
+
});
|
|
6148
|
+
child.on("exit", (code) => {
|
|
6149
|
+
resolve9({ ok: code === 0, output: output.trim(), spawnError: false });
|
|
6150
|
+
});
|
|
6151
|
+
});
|
|
6152
|
+
}
|
|
6153
|
+
async function runAgentCaptured(prompt2, opts) {
|
|
6154
|
+
if (opts.chain.length === 0) {
|
|
6155
|
+
throw new Error("No available agents (need one of: claude, codex, agy)");
|
|
6156
|
+
}
|
|
6157
|
+
let lastError = "";
|
|
6158
|
+
for (const agent of opts.chain) {
|
|
6159
|
+
const result = await runOnce(agent, prompt2, { cwd: opts.cwd, capture: true });
|
|
6160
|
+
if (result.ok && result.output) {
|
|
6161
|
+
return { output: result.output, agent };
|
|
6162
|
+
}
|
|
6163
|
+
lastError = result.spawnError ? `${agent} could not be spawned` : `${agent} exited without usable output`;
|
|
6164
|
+
}
|
|
6165
|
+
throw new Error(`All agents failed (${lastError})`);
|
|
6166
|
+
}
|
|
6167
|
+
async function runAgentInteractive(prompt2, opts) {
|
|
6168
|
+
if (opts.chain.length === 0) {
|
|
6169
|
+
throw new Error("No available agents (need one of: claude, codex, agy)");
|
|
6170
|
+
}
|
|
6171
|
+
let lastError = "";
|
|
6172
|
+
for (const agent of opts.chain) {
|
|
6173
|
+
const result = await runOnce(agent, prompt2, { cwd: opts.cwd, capture: false });
|
|
6174
|
+
if (result.ok) return { agent };
|
|
6175
|
+
lastError = result.spawnError ? `${agent} could not be spawned` : `${agent} exited non-zero`;
|
|
6176
|
+
}
|
|
6177
|
+
throw new Error(`All agents failed (${lastError})`);
|
|
6178
|
+
}
|
|
6179
|
+
|
|
6180
|
+
// cli/commands/review-apply.ts
|
|
6030
6181
|
var c8 = {
|
|
6031
6182
|
reset: "\x1B[0m",
|
|
6032
6183
|
dim: "\x1B[2m",
|
|
@@ -6056,7 +6207,7 @@ function logOk(msg) {
|
|
|
6056
6207
|
function logErr(msg) {
|
|
6057
6208
|
console.error(`${timestamp2()} ${tag()} ${paint8("red", "\u2717")} ${msg}`);
|
|
6058
6209
|
}
|
|
6059
|
-
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) => {
|
|
6210
|
+
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) => {
|
|
6060
6211
|
const config = loadConfig();
|
|
6061
6212
|
if (!config.apiKey) {
|
|
6062
6213
|
logErr('Not authenticated. Run "mr login" first.');
|
|
@@ -6077,9 +6228,12 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6077
6228
|
process.exit(1);
|
|
6078
6229
|
}
|
|
6079
6230
|
const actionableComments = comments.filter((c14) => !excluded.has(c14.file));
|
|
6080
|
-
const
|
|
6231
|
+
const dismissedCount = findings.filter((f) => f.status === "dismissed").length;
|
|
6232
|
+
const actionableFindings = findings.filter(
|
|
6233
|
+
(f) => !excluded.has(f.file) && f.status !== "dismissed"
|
|
6234
|
+
);
|
|
6081
6235
|
if (actionableComments.length === 0 && actionableFindings.length === 0) {
|
|
6082
|
-
logErr("
|
|
6236
|
+
logErr("No selected comments or findings to act on (all excluded or dismissed).");
|
|
6083
6237
|
process.exit(1);
|
|
6084
6238
|
}
|
|
6085
6239
|
let project;
|
|
@@ -6104,19 +6258,59 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6104
6258
|
logErr(`Set the project's localPath or run from inside the repo.`);
|
|
6105
6259
|
process.exit(1);
|
|
6106
6260
|
}
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
}
|
|
6111
|
-
try {
|
|
6112
|
-
execSync5(`git checkout ${review.branch}`, { cwd: projectPath, stdio: "pipe" });
|
|
6113
|
-
} catch (err) {
|
|
6114
|
-
logErr(`Could not checkout branch ${review.branch}: ${err.message}`);
|
|
6261
|
+
const chain = await resolveReviewAgentChain("claude");
|
|
6262
|
+
if (chain.length === 0) {
|
|
6263
|
+
logErr("No available agent found (need one of: claude, codex, agy)");
|
|
6115
6264
|
process.exit(1);
|
|
6116
6265
|
}
|
|
6266
|
+
const touchedFiles = Array.from(
|
|
6267
|
+
new Set([
|
|
6268
|
+
...actionableComments.map((c14) => c14.file),
|
|
6269
|
+
...actionableFindings.map((f) => f.file)
|
|
6270
|
+
].filter(Boolean))
|
|
6271
|
+
);
|
|
6272
|
+
let workDir = projectPath;
|
|
6273
|
+
let cleanup = () => {
|
|
6274
|
+
};
|
|
6275
|
+
if (opts.inPlace) {
|
|
6276
|
+
try {
|
|
6277
|
+
execSync5(`git fetch origin ${review.branch}`, { cwd: projectPath, stdio: "ignore" });
|
|
6278
|
+
} catch {
|
|
6279
|
+
}
|
|
6280
|
+
try {
|
|
6281
|
+
execSync5(`git checkout ${review.branch}`, { cwd: projectPath, stdio: "pipe" });
|
|
6282
|
+
} catch (err) {
|
|
6283
|
+
logErr(`Could not checkout branch ${review.branch}: ${err.message}`);
|
|
6284
|
+
process.exit(1);
|
|
6285
|
+
}
|
|
6286
|
+
} else {
|
|
6287
|
+
try {
|
|
6288
|
+
const wt = createWorktree(projectPath, review.branch, `review-apply-${id}`, {
|
|
6289
|
+
syncRemoteBranch: true
|
|
6290
|
+
});
|
|
6291
|
+
workDir = wt.path;
|
|
6292
|
+
} catch (err) {
|
|
6293
|
+
logErr(`Could not create worktree for ${review.branch}: ${err.message}`);
|
|
6294
|
+
process.exit(1);
|
|
6295
|
+
}
|
|
6296
|
+
cleanup = () => removeWorktree(projectPath, workDir);
|
|
6297
|
+
}
|
|
6298
|
+
const failAndExit = async (message, extra = {}) => {
|
|
6299
|
+
logErr(message);
|
|
6300
|
+
try {
|
|
6301
|
+
await api.patch(`/api/reviews/${id}`, { fixStatus: "failed", fixErrorMessage: message, ...extra });
|
|
6302
|
+
} catch {
|
|
6303
|
+
}
|
|
6304
|
+
cleanup();
|
|
6305
|
+
process.exit(1);
|
|
6306
|
+
};
|
|
6117
6307
|
log(`Project: ${paint8("cyan", project.name)}`);
|
|
6118
6308
|
log(`Branch: ${paint8("cyan", review.branch)}`);
|
|
6309
|
+
log(`Workdir: ${paint8("dim", opts.inPlace ? `${workDir} (in-place)` : workDir)}`);
|
|
6119
6310
|
log(`Comments: ${paint8("yellow", String(actionableComments.length))}, findings: ${paint8("yellow", String(actionableFindings.length))}`);
|
|
6311
|
+
if (dismissedCount > 0) {
|
|
6312
|
+
log(`Dismissed findings skipped: ${paint8("dim", String(dismissedCount))}`);
|
|
6313
|
+
}
|
|
6120
6314
|
if (excluded.size > 0) {
|
|
6121
6315
|
log(`Excluded files: ${paint8("dim", [...excluded].join(", "))}`);
|
|
6122
6316
|
}
|
|
@@ -6131,19 +6325,14 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6131
6325
|
findings: actionableFindings,
|
|
6132
6326
|
excluded: [...excluded]
|
|
6133
6327
|
});
|
|
6134
|
-
log(
|
|
6328
|
+
log(`Asking ${paint8("cyan", chain[0])} to apply the changes\u2026`);
|
|
6135
6329
|
try {
|
|
6136
|
-
await
|
|
6330
|
+
const { agent } = await runAgentInteractive(prompt2, { cwd: workDir, chain });
|
|
6331
|
+
if (agent !== chain[0]) log(`Applied with fallback agent ${paint8("cyan", agent)}`);
|
|
6137
6332
|
} catch (err) {
|
|
6138
|
-
|
|
6139
|
-
logErr(`Agent fix failed: ${message}`);
|
|
6140
|
-
try {
|
|
6141
|
-
await api.patch(`/api/reviews/${id}`, { fixStatus: "failed", fixErrorMessage: message });
|
|
6142
|
-
} catch {
|
|
6143
|
-
}
|
|
6144
|
-
process.exit(1);
|
|
6333
|
+
await failAndExit(`Agent fix failed: ${err.message || "Unknown error"}`);
|
|
6145
6334
|
}
|
|
6146
|
-
const dirty = execSync5("git status --porcelain", { cwd:
|
|
6335
|
+
const dirty = execSync5("git status --porcelain", { cwd: workDir, encoding: "utf-8" }).trim();
|
|
6147
6336
|
if (!dirty) {
|
|
6148
6337
|
log(paint8("yellow", "Agent didn't change any files."));
|
|
6149
6338
|
try {
|
|
@@ -6153,10 +6342,11 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6153
6342
|
});
|
|
6154
6343
|
} catch {
|
|
6155
6344
|
}
|
|
6345
|
+
cleanup();
|
|
6156
6346
|
return;
|
|
6157
6347
|
}
|
|
6158
6348
|
if (opts.commit === false) {
|
|
6159
|
-
logOk(
|
|
6349
|
+
logOk(`Changes left unstaged for review (--no-commit) in ${paint8("dim", workDir)}.`);
|
|
6160
6350
|
try {
|
|
6161
6351
|
await api.patch(`/api/reviews/${id}`, { fixStatus: "completed" });
|
|
6162
6352
|
} catch {
|
|
@@ -6168,35 +6358,27 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6168
6358
|
findingsCount: actionableFindings.length
|
|
6169
6359
|
});
|
|
6170
6360
|
try {
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
try {
|
|
6177
|
-
await api.patch(`/api/reviews/${id}`, { fixStatus: "failed", fixErrorMessage: message });
|
|
6178
|
-
} catch {
|
|
6361
|
+
if (touchedFiles.length > 0) {
|
|
6362
|
+
const args = touchedFiles.map((f) => JSON.stringify(f)).join(" ");
|
|
6363
|
+
execSync5(`git add -- ${args}`, { cwd: workDir });
|
|
6364
|
+
} else {
|
|
6365
|
+
execSync5("git add -A", { cwd: workDir });
|
|
6179
6366
|
}
|
|
6180
|
-
|
|
6367
|
+
execSync5(`git commit -m ${JSON.stringify(commitMessage)}`, { cwd: workDir, stdio: "pipe" });
|
|
6368
|
+
} catch (err) {
|
|
6369
|
+
await failAndExit(`Commit failed: ${err.message || "Unknown error"}`);
|
|
6181
6370
|
}
|
|
6182
|
-
const sha = execSync5("git rev-parse HEAD", { cwd:
|
|
6371
|
+
const sha = execSync5("git rev-parse HEAD", { cwd: workDir, encoding: "utf-8" }).trim();
|
|
6183
6372
|
logOk(`Committed ${paint8("yellow", sha.slice(0, 10))}`);
|
|
6184
6373
|
if (opts.push !== false) {
|
|
6185
6374
|
try {
|
|
6186
|
-
execSync5(`git push origin ${review.branch}`, { cwd:
|
|
6375
|
+
execSync5(`git push origin ${review.branch}`, { cwd: workDir, stdio: "pipe" });
|
|
6187
6376
|
logOk(`Pushed to origin/${review.branch}`);
|
|
6188
6377
|
} catch (err) {
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
fixStatus: "failed",
|
|
6194
|
-
fixErrorMessage: `Committed ${sha.slice(0, 10)} but push failed: ${message}`,
|
|
6195
|
-
fixCommitSha: sha
|
|
6196
|
-
});
|
|
6197
|
-
} catch {
|
|
6198
|
-
}
|
|
6199
|
-
process.exit(1);
|
|
6378
|
+
await failAndExit(
|
|
6379
|
+
`Committed ${sha.slice(0, 10)} but push failed: ${err.message || "Unknown error"}`,
|
|
6380
|
+
{ fixCommitSha: sha }
|
|
6381
|
+
);
|
|
6200
6382
|
}
|
|
6201
6383
|
}
|
|
6202
6384
|
try {
|
|
@@ -6207,6 +6389,7 @@ var reviewApplyCommand = new Command25("apply").description("Apply review commen
|
|
|
6207
6389
|
});
|
|
6208
6390
|
} catch {
|
|
6209
6391
|
}
|
|
6392
|
+
cleanup();
|
|
6210
6393
|
logOk("Done.");
|
|
6211
6394
|
});
|
|
6212
6395
|
function buildApplyPrompt(args) {
|
|
@@ -6250,17 +6433,418 @@ function buildCommitMessage(args) {
|
|
|
6250
6433
|
|
|
6251
6434
|
Generated by mr review apply.`;
|
|
6252
6435
|
}
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6436
|
+
|
|
6437
|
+
// lib/review/dimensions.ts
|
|
6438
|
+
var REVIEW_DIMENSIONS = [
|
|
6439
|
+
{
|
|
6440
|
+
key: "security",
|
|
6441
|
+
title: "Security",
|
|
6442
|
+
allowedTypes: ["security", "bug"],
|
|
6443
|
+
focus: [
|
|
6444
|
+
"Hunt exclusively for security problems:",
|
|
6445
|
+
"- Injection (SQL/NoSQL/command/path), XSS, SSRF, and unsafe deserialization",
|
|
6446
|
+
"- AuthN/AuthZ gaps: missing ownership checks, privilege escalation, IDOR",
|
|
6447
|
+
"- Secrets or credentials committed, logged, or returned to clients",
|
|
6448
|
+
"- Unsafe handling of user input, missing validation, or unescaped output",
|
|
6449
|
+
"- Insecure crypto, weak randomness, or Edge-runtime incompatible Node APIs",
|
|
6450
|
+
"Do NOT report style, performance, or generic refactor ideas here."
|
|
6451
|
+
].join("\n")
|
|
6452
|
+
},
|
|
6453
|
+
{
|
|
6454
|
+
key: "correctness",
|
|
6455
|
+
title: "Correctness",
|
|
6456
|
+
allowedTypes: ["bug"],
|
|
6457
|
+
focus: [
|
|
6458
|
+
"Hunt exclusively for correctness bugs and logical errors:",
|
|
6459
|
+
"- Off-by-one, null/undefined dereferences, incorrect conditionals",
|
|
6460
|
+
"- Broken contracts: a changed function signature whose other call sites are now wrong",
|
|
6461
|
+
"- Unhandled error paths, swallowed exceptions, race conditions",
|
|
6462
|
+
"- State that can desync, missing await, or incorrect async ordering",
|
|
6463
|
+
"Open neighboring files to confirm a change doesn't break an un-diffed caller.",
|
|
6464
|
+
"Do NOT report style, naming, or performance micro-optimizations here."
|
|
6465
|
+
].join("\n")
|
|
6466
|
+
},
|
|
6467
|
+
{
|
|
6468
|
+
key: "performance",
|
|
6469
|
+
title: "Performance",
|
|
6470
|
+
allowedTypes: ["performance", "bug"],
|
|
6471
|
+
focus: [
|
|
6472
|
+
"Hunt exclusively for performance problems:",
|
|
6473
|
+
"- N+1 queries, missing indexes, redundant network/database round-trips",
|
|
6474
|
+
"- Unnecessary re-renders, missing memoization, work done in hot loops",
|
|
6475
|
+
"- Large synchronous work on a request path, unbounded memory growth",
|
|
6476
|
+
"Only flag issues with a realistic, material impact. Do NOT report style or naming."
|
|
6477
|
+
].join("\n")
|
|
6478
|
+
},
|
|
6479
|
+
{
|
|
6480
|
+
key: "style",
|
|
6481
|
+
title: "Style & Maintainability",
|
|
6482
|
+
allowedTypes: ["style", "suggestion", "nitpick"],
|
|
6483
|
+
focus: [
|
|
6484
|
+
"Look for style, readability and maintainability improvements:",
|
|
6485
|
+
"- Naming, dead code, duplication, and inconsistent patterns",
|
|
6486
|
+
"- Missing types, unclear abstractions, or violations of repo conventions",
|
|
6487
|
+
"- Small suggestions and nitpicks (keep these low severity)",
|
|
6488
|
+
"Do NOT duplicate security/correctness/performance findings \u2014 stay in your lane."
|
|
6489
|
+
].join("\n")
|
|
6490
|
+
}
|
|
6491
|
+
];
|
|
6492
|
+
|
|
6493
|
+
// lib/review/parse.ts
|
|
6494
|
+
function extractJsonObjects(text) {
|
|
6495
|
+
const objects = [];
|
|
6496
|
+
let depth = 0;
|
|
6497
|
+
let start = -1;
|
|
6498
|
+
let inString = false;
|
|
6499
|
+
let escaped = false;
|
|
6500
|
+
for (let i = 0; i < text.length; i++) {
|
|
6501
|
+
const ch = text[i];
|
|
6502
|
+
if (inString) {
|
|
6503
|
+
if (escaped) {
|
|
6504
|
+
escaped = false;
|
|
6505
|
+
} else if (ch === "\\") {
|
|
6506
|
+
escaped = true;
|
|
6507
|
+
} else if (ch === '"') {
|
|
6508
|
+
inString = false;
|
|
6509
|
+
}
|
|
6510
|
+
continue;
|
|
6511
|
+
}
|
|
6512
|
+
if (ch === '"') {
|
|
6513
|
+
inString = true;
|
|
6514
|
+
} else if (ch === "{") {
|
|
6515
|
+
if (depth === 0) start = i;
|
|
6516
|
+
depth++;
|
|
6517
|
+
} else if (ch === "}") {
|
|
6518
|
+
if (depth > 0) {
|
|
6519
|
+
depth--;
|
|
6520
|
+
if (depth === 0 && start !== -1) {
|
|
6521
|
+
objects.push(text.slice(start, i + 1));
|
|
6522
|
+
start = -1;
|
|
6523
|
+
}
|
|
6524
|
+
}
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6527
|
+
return objects;
|
|
6528
|
+
}
|
|
6529
|
+
function parseReviewOutput(output) {
|
|
6530
|
+
const cleaned = output.replace(/```(?:json)?/gi, "").replace(/```/g, "").trim();
|
|
6531
|
+
const candidates = [cleaned, ...extractJsonObjects(cleaned)].sort(
|
|
6532
|
+
(a, b) => b.length - a.length
|
|
6533
|
+
);
|
|
6534
|
+
for (const candidate of candidates) {
|
|
6535
|
+
try {
|
|
6536
|
+
const parsed = JSON.parse(candidate);
|
|
6537
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.findings)) {
|
|
6538
|
+
return {
|
|
6539
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
6540
|
+
findings: parsed.findings,
|
|
6541
|
+
parseFailed: false
|
|
6542
|
+
};
|
|
6543
|
+
}
|
|
6544
|
+
} catch {
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
6547
|
+
return { summary: "", findings: [], parseFailed: true };
|
|
6548
|
+
}
|
|
6549
|
+
|
|
6550
|
+
// lib/review/merge.ts
|
|
6551
|
+
var SEVERITY_RANK = {
|
|
6552
|
+
critical: 4,
|
|
6553
|
+
high: 3,
|
|
6554
|
+
medium: 2,
|
|
6555
|
+
low: 1
|
|
6556
|
+
};
|
|
6557
|
+
function tokenize(text) {
|
|
6558
|
+
return new Set(
|
|
6559
|
+
(text || "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter(Boolean)
|
|
6560
|
+
);
|
|
6561
|
+
}
|
|
6562
|
+
function titleSimilarity(a, b) {
|
|
6563
|
+
const ta = tokenize(a);
|
|
6564
|
+
const tb = tokenize(b);
|
|
6565
|
+
if (ta.size === 0 && tb.size === 0) return 1;
|
|
6566
|
+
if (ta.size === 0 || tb.size === 0) return 0;
|
|
6567
|
+
let intersection = 0;
|
|
6568
|
+
for (const t of ta) if (tb.has(t)) intersection++;
|
|
6569
|
+
const union = ta.size + tb.size - intersection;
|
|
6570
|
+
return intersection / union;
|
|
6571
|
+
}
|
|
6572
|
+
function severityRank(sev) {
|
|
6573
|
+
return SEVERITY_RANK[sev] ?? 0;
|
|
6574
|
+
}
|
|
6575
|
+
function isDuplicate(a, b) {
|
|
6576
|
+
if (a.file !== b.file) return false;
|
|
6577
|
+
const aLine = a.line;
|
|
6578
|
+
const bLine = b.line;
|
|
6579
|
+
if (aLine != null && bLine != null) {
|
|
6580
|
+
if (Math.abs(aLine - bLine) > 3) return false;
|
|
6581
|
+
} else if (aLine != null || bLine != null) {
|
|
6582
|
+
return titleSimilarity(a.title, b.title) >= 0.8;
|
|
6583
|
+
}
|
|
6584
|
+
return titleSimilarity(a.title, b.title) >= 0.5;
|
|
6585
|
+
}
|
|
6586
|
+
function preferred(a, b) {
|
|
6587
|
+
const aScore = severityRank(a.severity) * 10 + (a.confidence ?? 0);
|
|
6588
|
+
const bScore = severityRank(b.severity) * 10 + (b.confidence ?? 0);
|
|
6589
|
+
const winner = bScore > aScore ? b : a;
|
|
6590
|
+
const loser = winner === a ? b : a;
|
|
6591
|
+
const dims = [winner.dimension, loser.dimension].filter(Boolean);
|
|
6592
|
+
return {
|
|
6593
|
+
...winner,
|
|
6594
|
+
description: (loser.description?.length ?? 0) > (winner.description?.length ?? 0) ? loser.description : winner.description,
|
|
6595
|
+
suggestion: winner.suggestion || loser.suggestion,
|
|
6596
|
+
dimension: Array.from(new Set(dims)).join("+") || winner.dimension,
|
|
6597
|
+
confidence: Math.max(winner.confidence ?? 0, loser.confidence ?? 0) || void 0
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
function mergeFindings(all) {
|
|
6601
|
+
const merged = [];
|
|
6602
|
+
for (const finding of all) {
|
|
6603
|
+
const existingIdx = merged.findIndex((m) => isDuplicate(m, finding));
|
|
6604
|
+
if (existingIdx === -1) {
|
|
6605
|
+
merged.push(finding);
|
|
6606
|
+
} else {
|
|
6607
|
+
merged[existingIdx] = preferred(merged[existingIdx], finding);
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
merged.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));
|
|
6611
|
+
return merged.map((f, i) => ({ ...f, id: `f${i + 1}` }));
|
|
6612
|
+
}
|
|
6613
|
+
|
|
6614
|
+
// lib/review/prompt.ts
|
|
6615
|
+
var JSON_CONTRACT = `Return ONLY a JSON object (no markdown fences, no prose before or after) with this exact shape:
|
|
6616
|
+
{
|
|
6617
|
+
"summary": "1-2 sentence assessment for this concern",
|
|
6618
|
+
"findings": [
|
|
6619
|
+
{
|
|
6620
|
+
"id": "f1",
|
|
6621
|
+
"type": "bug",
|
|
6622
|
+
"severity": "high",
|
|
6623
|
+
"title": "Brief one-line title",
|
|
6624
|
+
"description": "Detailed explanation",
|
|
6625
|
+
"file": "path/to/file.ts",
|
|
6626
|
+
"line": 42,
|
|
6627
|
+
"suggestion": "Concrete fix, with code when possible",
|
|
6628
|
+
"confidence": 0.0
|
|
6629
|
+
}
|
|
6630
|
+
]
|
|
6631
|
+
}
|
|
6632
|
+
"confidence" is your own 0-1 estimate that this is a real, actionable issue.
|
|
6633
|
+
If there are no issues for this concern, return an empty findings array with a positive summary.`;
|
|
6634
|
+
function buildDimensionPrompt(args) {
|
|
6635
|
+
const lines = [];
|
|
6636
|
+
lines.push(
|
|
6637
|
+
`You are a senior code reviewer focused exclusively on ${args.dimension.title.toUpperCase()}.`,
|
|
6638
|
+
`Review the git diff for branch "${args.branch}" compared to "${args.baseBranch}".`,
|
|
6639
|
+
"",
|
|
6640
|
+
args.dimension.focus,
|
|
6641
|
+
"",
|
|
6642
|
+
`Only emit findings whose "type" is one of: ${args.dimension.allowedTypes.join(", ")}.`,
|
|
6643
|
+
""
|
|
6644
|
+
);
|
|
6645
|
+
if (args.canReadFiles) {
|
|
6646
|
+
lines.push(
|
|
6647
|
+
"You are running inside a checkout of this repository. You MAY open the changed files",
|
|
6648
|
+
"and their neighbors (callers, callees, related modules) to verify a finding before",
|
|
6649
|
+
"reporting it \u2014 this catches issues that are invisible from the diff alone, like a",
|
|
6650
|
+
"renamed function's other call sites. Prefer confirmed findings over speculation.",
|
|
6651
|
+
""
|
|
6652
|
+
);
|
|
6653
|
+
}
|
|
6654
|
+
if (args.fileScope && args.fileScope.length > 0) {
|
|
6655
|
+
lines.push(
|
|
6656
|
+
"Concentrate on these files (other diff context is provided only for reference):",
|
|
6657
|
+
...args.fileScope.map((f) => ` - ${f}`),
|
|
6658
|
+
""
|
|
6659
|
+
);
|
|
6660
|
+
}
|
|
6661
|
+
lines.push(JSON_CONTRACT, "", "Here is the diff to review:", "", "```diff", args.diff, "```");
|
|
6662
|
+
return lines.join("\n");
|
|
6663
|
+
}
|
|
6664
|
+
function buildVerificationPrompt(args) {
|
|
6665
|
+
const compact = args.findings.map((f) => ({
|
|
6666
|
+
id: f.id,
|
|
6667
|
+
type: f.type,
|
|
6668
|
+
severity: f.severity,
|
|
6669
|
+
title: f.title,
|
|
6670
|
+
description: f.description,
|
|
6671
|
+
file: f.file,
|
|
6672
|
+
line: f.line
|
|
6673
|
+
}));
|
|
6674
|
+
const lines = [];
|
|
6675
|
+
lines.push(
|
|
6676
|
+
`You are an adversarial reviewer verifying findings from an automated review of branch "${args.branch}" vs "${args.baseBranch}".`,
|
|
6677
|
+
"For EACH finding below, try to refute it. A finding should be kept only if it is a real,",
|
|
6678
|
+
"actionable issue grounded in the actual diff. Reject hallucinations, findings that",
|
|
6679
|
+
"reference code that isn't there, and pure speculation.",
|
|
6680
|
+
""
|
|
6681
|
+
);
|
|
6682
|
+
if (args.canReadFiles) {
|
|
6683
|
+
lines.push(
|
|
6684
|
+
"You are in a checkout of the repo \u2014 open files to confirm or refute each claim.",
|
|
6685
|
+
""
|
|
6686
|
+
);
|
|
6687
|
+
}
|
|
6688
|
+
lines.push(
|
|
6689
|
+
"Return ONLY a JSON object (no fences, no prose) of this shape:",
|
|
6690
|
+
`{ "verdicts": [ { "id": "f1", "keep": true, "confidence": 0.0, "reason": "why" } ] }`,
|
|
6691
|
+
"confidence is 0-1. Set keep=false when you cannot substantiate the finding.",
|
|
6692
|
+
"",
|
|
6693
|
+
"FINDINGS:",
|
|
6694
|
+
JSON.stringify(compact, null, 2),
|
|
6695
|
+
"",
|
|
6696
|
+
"DIFF:",
|
|
6697
|
+
"```diff",
|
|
6698
|
+
args.diff,
|
|
6699
|
+
"```"
|
|
6700
|
+
);
|
|
6701
|
+
return lines.join("\n");
|
|
6702
|
+
}
|
|
6703
|
+
|
|
6704
|
+
// lib/review/orchestrate.ts
|
|
6705
|
+
function splitDiffByFile(diff) {
|
|
6706
|
+
const sections = diff.split(/(?=^diff --git )/m).filter((s) => s.trim());
|
|
6707
|
+
return sections.map((section) => {
|
|
6708
|
+
const m = section.match(/^diff --git a\/(.+?) b\//m);
|
|
6709
|
+
return { file: m?.[1] ?? "unknown", section };
|
|
6710
|
+
});
|
|
6711
|
+
}
|
|
6712
|
+
async function pool(items, limit) {
|
|
6713
|
+
const results = new Array(items.length);
|
|
6714
|
+
let cursor = 0;
|
|
6715
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
6716
|
+
while (cursor < items.length) {
|
|
6717
|
+
const idx = cursor++;
|
|
6718
|
+
results[idx] = await items[idx]();
|
|
6719
|
+
}
|
|
6720
|
+
});
|
|
6721
|
+
await Promise.all(workers);
|
|
6722
|
+
return results;
|
|
6723
|
+
}
|
|
6724
|
+
function buildJobs(opts) {
|
|
6725
|
+
const files = splitDiffByFile(opts.diff);
|
|
6726
|
+
const shardThreshold = opts.shardThreshold ?? 12;
|
|
6727
|
+
if (files.length >= shardThreshold) {
|
|
6728
|
+
const shardCount = Math.min(4, Math.ceil(files.length / 6));
|
|
6729
|
+
const shards = Array.from(
|
|
6730
|
+
{ length: shardCount },
|
|
6731
|
+
() => ({ diff: "", files: [] })
|
|
6732
|
+
);
|
|
6733
|
+
files.forEach((f, i) => {
|
|
6734
|
+
const s = shards[i % shardCount];
|
|
6735
|
+
s.diff += f.section;
|
|
6736
|
+
s.files.push(f.file);
|
|
6262
6737
|
});
|
|
6738
|
+
const jobs = [];
|
|
6739
|
+
for (const dim of REVIEW_DIMENSIONS) {
|
|
6740
|
+
for (const shard of shards) {
|
|
6741
|
+
jobs.push({ dimension: dim, diff: shard.diff, fileScope: shard.files });
|
|
6742
|
+
}
|
|
6743
|
+
}
|
|
6744
|
+
return jobs;
|
|
6745
|
+
}
|
|
6746
|
+
return REVIEW_DIMENSIONS.map((dimension) => ({ dimension, diff: opts.diff }));
|
|
6747
|
+
}
|
|
6748
|
+
async function orchestrateReview(opts) {
|
|
6749
|
+
const log4 = opts.log ?? (() => {
|
|
6750
|
+
});
|
|
6751
|
+
const concurrency = opts.concurrency ?? 4;
|
|
6752
|
+
const canReadFiles = Boolean(opts.cwd);
|
|
6753
|
+
const runOpts = { cwd: opts.cwd, chain: opts.chain };
|
|
6754
|
+
const jobs = buildJobs(opts);
|
|
6755
|
+
log4(`Fanning out ${jobs.length} review subagent(s) across ${REVIEW_DIMENSIONS.length} dimensions`);
|
|
6756
|
+
const summaries = [];
|
|
6757
|
+
const collected = [];
|
|
6758
|
+
const jobResults = await pool(
|
|
6759
|
+
jobs.map((job) => async () => {
|
|
6760
|
+
const prompt2 = buildDimensionPrompt({
|
|
6761
|
+
dimension: job.dimension,
|
|
6762
|
+
branch: opts.branch,
|
|
6763
|
+
baseBranch: opts.baseBranch,
|
|
6764
|
+
diff: job.diff,
|
|
6765
|
+
canReadFiles,
|
|
6766
|
+
fileScope: job.fileScope
|
|
6767
|
+
});
|
|
6768
|
+
try {
|
|
6769
|
+
const { output } = await runAgentCaptured(prompt2, runOpts);
|
|
6770
|
+
const parsed = parseReviewOutput(output);
|
|
6771
|
+
return { job, parsed };
|
|
6772
|
+
} catch (err) {
|
|
6773
|
+
log4(` ${job.dimension.key} subagent failed: ${err.message}`);
|
|
6774
|
+
return { job, parsed: { summary: "", findings: [], parseFailed: true } };
|
|
6775
|
+
}
|
|
6776
|
+
}),
|
|
6777
|
+
concurrency
|
|
6778
|
+
);
|
|
6779
|
+
const dimensionsRun = /* @__PURE__ */ new Set();
|
|
6780
|
+
for (const { job, parsed } of jobResults) {
|
|
6781
|
+
if (parsed.summary) summaries.push(`${job.dimension.title}: ${parsed.summary}`);
|
|
6782
|
+
for (const f of parsed.findings) {
|
|
6783
|
+
dimensionsRun.add(job.dimension.key);
|
|
6784
|
+
collected.push({
|
|
6785
|
+
id: f.id || "f",
|
|
6786
|
+
type: f.type || job.dimension.allowedTypes[0],
|
|
6787
|
+
severity: f.severity || "medium",
|
|
6788
|
+
title: f.title || "Untitled finding",
|
|
6789
|
+
description: f.description || "",
|
|
6790
|
+
file: f.file || "unknown",
|
|
6791
|
+
line: typeof f.line === "number" ? f.line : void 0,
|
|
6792
|
+
endLine: typeof f.endLine === "number" ? f.endLine : void 0,
|
|
6793
|
+
suggestion: f.suggestion,
|
|
6794
|
+
status: "new",
|
|
6795
|
+
dimension: job.dimension.key,
|
|
6796
|
+
confidence: typeof f.confidence === "number" ? f.confidence : void 0
|
|
6797
|
+
});
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
const rawCount = collected.length;
|
|
6801
|
+
let findings = mergeFindings(collected);
|
|
6802
|
+
log4(`Merged ${rawCount} raw findings into ${findings.length} after dedup`);
|
|
6803
|
+
if (opts.verify && findings.length > 0) {
|
|
6804
|
+
findings = await verifyFindings(findings, opts, runOpts, log4);
|
|
6805
|
+
}
|
|
6806
|
+
return {
|
|
6807
|
+
summary: summaries.join(" "),
|
|
6808
|
+
findings,
|
|
6809
|
+
dimensionsRun: [...dimensionsRun],
|
|
6810
|
+
rawCount
|
|
6811
|
+
};
|
|
6812
|
+
}
|
|
6813
|
+
async function verifyFindings(findings, opts, runOpts, log4) {
|
|
6814
|
+
const minConfidence = opts.minConfidence ?? 0.4;
|
|
6815
|
+
const prompt2 = buildVerificationPrompt({
|
|
6816
|
+
findings,
|
|
6817
|
+
branch: opts.branch,
|
|
6818
|
+
baseBranch: opts.baseBranch,
|
|
6819
|
+
diff: opts.diff,
|
|
6820
|
+
canReadFiles: Boolean(opts.cwd)
|
|
6821
|
+
});
|
|
6822
|
+
let verdicts = [];
|
|
6823
|
+
try {
|
|
6824
|
+
const { output } = await runAgentCaptured(prompt2, runOpts);
|
|
6825
|
+
const cleaned = output.replace(/```(?:json)?/gi, "").replace(/```/g, "").trim();
|
|
6826
|
+
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
6827
|
+
if (match) {
|
|
6828
|
+
const parsed = JSON.parse(match[0]);
|
|
6829
|
+
verdicts = parsed.verdicts ?? [];
|
|
6830
|
+
}
|
|
6831
|
+
} catch (err) {
|
|
6832
|
+
log4(`Verification stage failed, keeping all findings: ${err.message}`);
|
|
6833
|
+
return findings;
|
|
6834
|
+
}
|
|
6835
|
+
const byId = new Map(verdicts.map((v) => [v.id, v]));
|
|
6836
|
+
const kept = findings.filter((f) => {
|
|
6837
|
+
const v = byId.get(f.id);
|
|
6838
|
+
if (!v) return true;
|
|
6839
|
+
if (v.keep === false) return false;
|
|
6840
|
+
if (typeof v.confidence === "number") {
|
|
6841
|
+
f.confidence = v.confidence;
|
|
6842
|
+
return v.confidence >= minConfidence;
|
|
6843
|
+
}
|
|
6844
|
+
return true;
|
|
6263
6845
|
});
|
|
6846
|
+
log4(`Verification kept ${kept.length}/${findings.length} findings`);
|
|
6847
|
+
return kept.map((f, i) => ({ ...f, id: `f${i + 1}` }));
|
|
6264
6848
|
}
|
|
6265
6849
|
|
|
6266
6850
|
// cli/commands/review.ts
|
|
@@ -6330,6 +6914,7 @@ var reviewCommand = new Command26("review").description("Run an automated code r
|
|
|
6330
6914
|
const canUseRemote = remote !== null && (prNumber || remote.number) !== void 0;
|
|
6331
6915
|
let diff;
|
|
6332
6916
|
let branch = opts.branch;
|
|
6917
|
+
let reviewCwd;
|
|
6333
6918
|
if (canUseRemote && remote) {
|
|
6334
6919
|
const num = prNumber ?? remote.number;
|
|
6335
6920
|
if (!num) {
|
|
@@ -6379,6 +6964,7 @@ var reviewCommand = new Command26("review").description("Run an automated code r
|
|
|
6379
6964
|
process.exit(1);
|
|
6380
6965
|
}
|
|
6381
6966
|
log2(`Using project path: ${paint9("dim", projectPath)}`);
|
|
6967
|
+
reviewCwd = projectPath;
|
|
6382
6968
|
if (!branch) {
|
|
6383
6969
|
try {
|
|
6384
6970
|
branch = execSync6("git rev-parse --abbrev-ref HEAD", {
|
|
@@ -6457,17 +7043,39 @@ var reviewCommand = new Command26("review").description("Run an automated code r
|
|
|
6457
7043
|
} catch {
|
|
6458
7044
|
}
|
|
6459
7045
|
const startTime = Date.now();
|
|
6460
|
-
const
|
|
6461
|
-
|
|
7046
|
+
const chain = await resolveReviewAgentChain("claude");
|
|
7047
|
+
if (chain.length === 0) {
|
|
7048
|
+
const message = "No available agent found (need one of: claude, codex, agy)";
|
|
7049
|
+
logErr2(message);
|
|
7050
|
+
try {
|
|
7051
|
+
await api.patch(`/api/reviews/${reportId}`, { status: "failed", errorMessage: message });
|
|
7052
|
+
} catch {
|
|
7053
|
+
}
|
|
7054
|
+
process.exit(1);
|
|
7055
|
+
}
|
|
7056
|
+
log2(`Agent chain: ${paint9("cyan", chain.join(" \u2192 "))}`);
|
|
7057
|
+
const MAX_DIFF_CHARS = 4e5;
|
|
7058
|
+
const diffCharsTotal = diff.length;
|
|
7059
|
+
let reviewDiff = diff;
|
|
7060
|
+
let diffTruncated = false;
|
|
6462
7061
|
if (diff.length > MAX_DIFF_CHARS) {
|
|
6463
|
-
|
|
6464
|
-
|
|
7062
|
+
reviewDiff = diff.slice(0, MAX_DIFF_CHARS);
|
|
7063
|
+
diffTruncated = true;
|
|
7064
|
+
log2(paint9("yellow", `Diff truncated to ${MAX_DIFF_CHARS.toLocaleString()} of ${diffCharsTotal.toLocaleString()} chars`));
|
|
6465
7065
|
}
|
|
6466
7066
|
try {
|
|
6467
|
-
log2("Running code review
|
|
6468
|
-
const
|
|
6469
|
-
|
|
6470
|
-
|
|
7067
|
+
log2("Running fan-out code review...");
|
|
7068
|
+
const orchestrated = await orchestrateReview({
|
|
7069
|
+
branch,
|
|
7070
|
+
baseBranch,
|
|
7071
|
+
diff: reviewDiff,
|
|
7072
|
+
chain,
|
|
7073
|
+
cwd: reviewCwd,
|
|
7074
|
+
concurrency: 4,
|
|
7075
|
+
verify: true,
|
|
7076
|
+
log: (m) => log2(m)
|
|
7077
|
+
});
|
|
7078
|
+
const result = { summary: orchestrated.summary, findings: orchestrated.findings };
|
|
6471
7079
|
const duration = Date.now() - startTime;
|
|
6472
7080
|
let wasCancelled = false;
|
|
6473
7081
|
try {
|
|
@@ -6484,7 +7092,9 @@ var reviewCommand = new Command26("review").description("Run an automated code r
|
|
|
6484
7092
|
summary: result.summary,
|
|
6485
7093
|
findings: result.findings,
|
|
6486
7094
|
filesReviewed: filesChanged,
|
|
6487
|
-
reviewDurationMs: duration
|
|
7095
|
+
reviewDurationMs: duration,
|
|
7096
|
+
diffTruncated,
|
|
7097
|
+
diffCharsTotal
|
|
6488
7098
|
});
|
|
6489
7099
|
logOk2(`Review completed in ${paint9("cyan", formatDuration(duration))}`);
|
|
6490
7100
|
logOk2(`Found ${paint9("yellow", String(result.findings.length))} findings`);
|
|
@@ -6573,112 +7183,12 @@ function formatDuration(ms) {
|
|
|
6573
7183
|
if (s < 60) return `${s}s`;
|
|
6574
7184
|
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
6575
7185
|
}
|
|
6576
|
-
function buildReviewPrompt(branch, baseBranch, diff) {
|
|
6577
|
-
return `You are a senior code reviewer. Review the following git diff for branch "${branch}" compared to "${baseBranch}".
|
|
6578
|
-
|
|
6579
|
-
Analyze the code changes and produce a JSON response with your review findings.
|
|
6580
|
-
|
|
6581
|
-
Focus on:
|
|
6582
|
-
- Bugs and logical errors
|
|
6583
|
-
- Security vulnerabilities (XSS, injection, auth issues, secrets exposure)
|
|
6584
|
-
- Performance issues (N+1 queries, missing indexes, unnecessary re-renders)
|
|
6585
|
-
- Code style and best practices violations
|
|
6586
|
-
- Suggestions for improvement
|
|
6587
|
-
- Nitpicks (minor style/naming issues)
|
|
6588
|
-
|
|
6589
|
-
For each finding, provide:
|
|
6590
|
-
- A unique ID (e.g. "f1", "f2", etc.)
|
|
6591
|
-
- Type: "bug", "security", "performance", "style", "suggestion", or "nitpick"
|
|
6592
|
-
- Severity: "critical", "high", "medium", or "low"
|
|
6593
|
-
- Title: a brief one-line summary
|
|
6594
|
-
- Description: detailed explanation of the issue
|
|
6595
|
-
- File: the file path where the issue was found
|
|
6596
|
-
- Line: the approximate line number in the new code (optional)
|
|
6597
|
-
- Suggestion: suggested fix or improvement (optional, include actual code when possible)
|
|
6598
|
-
|
|
6599
|
-
Return ONLY a JSON object with this structure (no markdown, no explanation before/after):
|
|
6600
|
-
{
|
|
6601
|
-
"summary": "Brief overall assessment of the code changes (2-3 sentences)",
|
|
6602
|
-
"findings": [
|
|
6603
|
-
{
|
|
6604
|
-
"id": "f1",
|
|
6605
|
-
"type": "bug",
|
|
6606
|
-
"severity": "high",
|
|
6607
|
-
"title": "Brief title",
|
|
6608
|
-
"description": "Detailed description",
|
|
6609
|
-
"file": "path/to/file.ts",
|
|
6610
|
-
"line": 42,
|
|
6611
|
-
"suggestion": "Suggested fix code"
|
|
6612
|
-
}
|
|
6613
|
-
]
|
|
6614
|
-
}
|
|
6615
|
-
|
|
6616
|
-
If the code looks good with no issues, return an empty findings array with a positive summary.
|
|
6617
|
-
|
|
6618
|
-
Here is the diff to review:
|
|
6619
|
-
|
|
6620
|
-
\`\`\`diff
|
|
6621
|
-
${diff}
|
|
6622
|
-
\`\`\``;
|
|
6623
|
-
}
|
|
6624
|
-
function runClaude(prompt2) {
|
|
6625
|
-
return new Promise((resolve9, reject) => {
|
|
6626
|
-
const child = spawn8("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
6627
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
6628
|
-
});
|
|
6629
|
-
let output = "";
|
|
6630
|
-
let errOutput = "";
|
|
6631
|
-
child.stdout?.on("data", (d) => {
|
|
6632
|
-
output += d.toString();
|
|
6633
|
-
});
|
|
6634
|
-
child.stderr?.on("data", (d) => {
|
|
6635
|
-
errOutput += d.toString();
|
|
6636
|
-
});
|
|
6637
|
-
child.on("exit", (code) => {
|
|
6638
|
-
if (code === 0) resolve9(output.trim());
|
|
6639
|
-
else reject(new Error(`claude exited with code ${code}
|
|
6640
|
-
${errOutput.trim()}`));
|
|
6641
|
-
});
|
|
6642
|
-
});
|
|
6643
|
-
}
|
|
6644
|
-
function parseReviewOutput(output) {
|
|
6645
|
-
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
6646
|
-
if (!jsonMatch) {
|
|
6647
|
-
return {
|
|
6648
|
-
summary: "Failed to parse review output",
|
|
6649
|
-
findings: []
|
|
6650
|
-
};
|
|
6651
|
-
}
|
|
6652
|
-
try {
|
|
6653
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
6654
|
-
return {
|
|
6655
|
-
summary: parsed.summary || "",
|
|
6656
|
-
findings: (parsed.findings || []).map((f) => ({
|
|
6657
|
-
id: f.id || `f${Math.random().toString(36).slice(2, 8)}`,
|
|
6658
|
-
type: f.type || "suggestion",
|
|
6659
|
-
severity: f.severity || "medium",
|
|
6660
|
-
title: f.title || "Untitled finding",
|
|
6661
|
-
description: f.description || "",
|
|
6662
|
-
file: f.file || "unknown",
|
|
6663
|
-
line: f.line,
|
|
6664
|
-
endLine: f.endLine,
|
|
6665
|
-
suggestion: f.suggestion,
|
|
6666
|
-
status: "new"
|
|
6667
|
-
}))
|
|
6668
|
-
};
|
|
6669
|
-
} catch {
|
|
6670
|
-
return {
|
|
6671
|
-
summary: "Failed to parse review JSON",
|
|
6672
|
-
findings: []
|
|
6673
|
-
};
|
|
6674
|
-
}
|
|
6675
|
-
}
|
|
6676
7186
|
|
|
6677
7187
|
// cli/commands/scan.ts
|
|
6678
7188
|
import { Command as Command27 } from "commander";
|
|
6679
7189
|
|
|
6680
7190
|
// lib/scanner/index.ts
|
|
6681
|
-
import { spawn as
|
|
7191
|
+
import { spawn as spawn8 } from "child_process";
|
|
6682
7192
|
|
|
6683
7193
|
// lib/scanner/config.ts
|
|
6684
7194
|
import { readFileSync as readFileSync10, existsSync as existsSync15 } from "fs";
|
|
@@ -7296,7 +7806,7 @@ async function runScanPipeline(opts) {
|
|
|
7296
7806
|
context.priorFindings,
|
|
7297
7807
|
opts.customPrompt
|
|
7298
7808
|
);
|
|
7299
|
-
const synthesisResult = await
|
|
7809
|
+
const synthesisResult = await runClaude(prompt2);
|
|
7300
7810
|
const parsed = parseSynthesisOutput(synthesisResult);
|
|
7301
7811
|
const scanDurationMs = Date.now() - startTime;
|
|
7302
7812
|
opts.onLog(`Scan complete in ${Math.round(scanDurationMs / 1e3)}s \u2014 ${parsed.findings.length} findings`);
|
|
@@ -7357,9 +7867,9 @@ async function fetchScanContext(opts) {
|
|
|
7357
7867
|
priorFindings
|
|
7358
7868
|
};
|
|
7359
7869
|
}
|
|
7360
|
-
function
|
|
7870
|
+
function runClaude(prompt2) {
|
|
7361
7871
|
return new Promise((resolve9, reject) => {
|
|
7362
|
-
const child =
|
|
7872
|
+
const child = spawn8("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
7363
7873
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7364
7874
|
});
|
|
7365
7875
|
let output = "";
|