@dunnewold-labs/mr-manager 0.4.44 → 0.4.48
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 +226 -10
- 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.48",
|
|
189
189
|
description: "Mr. Manager - Task and project management CLI",
|
|
190
190
|
bin: {
|
|
191
191
|
mr: "./dist/index.mjs"
|
|
@@ -550,7 +550,7 @@ var contextCommand = new Command8("context").description("Output project context
|
|
|
550
550
|
import { Command as Command9 } from "commander";
|
|
551
551
|
import { spawn as spawn4, exec } from "child_process";
|
|
552
552
|
import { randomUUID } from "crypto";
|
|
553
|
-
import { resolve as resolve2 } from "path";
|
|
553
|
+
import { resolve as resolve2, extname } from "path";
|
|
554
554
|
import { readFileSync as readFileSync5, readdirSync, unlinkSync, existsSync as existsSync7, statSync, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, rmdirSync } from "fs";
|
|
555
555
|
import * as readline from "readline";
|
|
556
556
|
|
|
@@ -1826,6 +1826,20 @@ function checkPrStatus(prUrl, repoDir, vcs = "github") {
|
|
|
1826
1826
|
});
|
|
1827
1827
|
});
|
|
1828
1828
|
}
|
|
1829
|
+
function mergePrViaCli(prUrl, repoDir, vcs = "github") {
|
|
1830
|
+
const cmd = vcs === "gitlab" ? `glab mr merge "${prUrl}" --yes --squash --remove-source-branch 2>&1` : `gh pr merge "${prUrl}" --squash --delete-branch 2>&1`;
|
|
1831
|
+
return new Promise((resolve9) => {
|
|
1832
|
+
exec(cmd, { cwd: repoDir }, (err, stdout, stderr) => {
|
|
1833
|
+
if (err) {
|
|
1834
|
+
const raw = (stderr || stdout || err.message).toString().trim();
|
|
1835
|
+
const tail = raw.split("\n").slice(-3).join(" ").trim();
|
|
1836
|
+
resolve9({ ok: false, error: tail || err.message });
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
resolve9({ ok: true });
|
|
1840
|
+
});
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1829
1843
|
function buildPrototypeSection(protoRefs, workingDir) {
|
|
1830
1844
|
if (protoRefs.length === 0) return "";
|
|
1831
1845
|
const sections = [
|
|
@@ -2327,12 +2341,94 @@ function describeVariance(variance) {
|
|
|
2327
2341
|
]
|
|
2328
2342
|
};
|
|
2329
2343
|
}
|
|
2344
|
+
var EXEMPLAR_POOLS = [
|
|
2345
|
+
["Linear", "Vercel", "Arc", "Things 3"],
|
|
2346
|
+
["Stripe", "Notion", "Figma", "Cron"],
|
|
2347
|
+
["Raycast", "Superhuman", "Height", "Campsite"],
|
|
2348
|
+
["Apple", "Readymag", "Family", "Stripe Press"],
|
|
2349
|
+
[
|
|
2350
|
+
"Awwwards Site of the Day winners",
|
|
2351
|
+
"indie SaaS",
|
|
2352
|
+
"editorial / magazine sites",
|
|
2353
|
+
"design-studio portfolios"
|
|
2354
|
+
]
|
|
2355
|
+
];
|
|
2356
|
+
function pickExemplars(seed) {
|
|
2357
|
+
let hash = 0;
|
|
2358
|
+
for (let i = 0; i < seed.length; i++) {
|
|
2359
|
+
hash = hash * 31 + seed.charCodeAt(i) | 0;
|
|
2360
|
+
}
|
|
2361
|
+
return EXEMPLAR_POOLS[Math.abs(hash) % EXEMPLAR_POOLS.length].join(", ");
|
|
2362
|
+
}
|
|
2363
|
+
function buildReferenceSection(referenceImagePaths) {
|
|
2364
|
+
if (referenceImagePaths.length === 0) return [];
|
|
2365
|
+
return [
|
|
2366
|
+
`## Reference Designs (CRITICAL \u2014 these define the visual direction)`,
|
|
2367
|
+
``,
|
|
2368
|
+
`The user attached ${referenceImagePaths.length} reference image(s), saved on disk at:`,
|
|
2369
|
+
...referenceImagePaths.map((p) => `- \`${p}\``),
|
|
2370
|
+
``,
|
|
2371
|
+
`Before designing anything, open and study each reference image (read the files at the paths above with your image-reading capability).`,
|
|
2372
|
+
`The references define the intended look and feel: layout structure, color palette, typography, spacing, component styling, density, and overall mood. Match them closely.`,
|
|
2373
|
+
`Where the references conflict with any generic style guidance below, THE REFERENCES WIN \u2014 do not override the user's attached direction with a default "design-led SaaS" look.`,
|
|
2374
|
+
`Treat multiple references as one combined moodboard: extract the consistent visual language across them.`,
|
|
2375
|
+
`You may still vary layout and composition across variants per the Design Variance section \u2014 but every variant must clearly belong to the reference designs' visual world.`,
|
|
2376
|
+
``
|
|
2377
|
+
];
|
|
2378
|
+
}
|
|
2379
|
+
async function downloadReferenceImages(proto, baseDir, prefix) {
|
|
2380
|
+
const refs = proto.referenceImages;
|
|
2381
|
+
if (!Array.isArray(refs) || refs.length === 0) return { paths: [], dir: null };
|
|
2382
|
+
const dir = resolve2(baseDir, `.mr-proto-refs-${shortId(proto.id)}`);
|
|
2383
|
+
try {
|
|
2384
|
+
mkdirSync3(dir, { recursive: true });
|
|
2385
|
+
} catch {
|
|
2386
|
+
return { paths: [], dir: null };
|
|
2387
|
+
}
|
|
2388
|
+
const paths = [];
|
|
2389
|
+
for (let i = 0; i < refs.length; i++) {
|
|
2390
|
+
const ref = refs[i];
|
|
2391
|
+
if (!ref || typeof ref.url !== "string") continue;
|
|
2392
|
+
try {
|
|
2393
|
+
const res = await fetch(ref.url);
|
|
2394
|
+
if (!res.ok) {
|
|
2395
|
+
logWarn(prefix, `Reference image ${i + 1} \u2192 HTTP ${res.status}, skipping`);
|
|
2396
|
+
continue;
|
|
2397
|
+
}
|
|
2398
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
2399
|
+
let ext = extname(new URL(ref.url).pathname).toLowerCase();
|
|
2400
|
+
if (!ext || ext.length > 5) ext = ".png";
|
|
2401
|
+
const dest = resolve2(dir, `reference-${i + 1}${ext}`);
|
|
2402
|
+
writeFileSync3(dest, buf);
|
|
2403
|
+
paths.push(dest);
|
|
2404
|
+
} catch (err) {
|
|
2405
|
+
logWarn(prefix, `Failed to download reference image ${i + 1}: ${err.message}`);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return { paths, dir: paths.length > 0 ? dir : null };
|
|
2409
|
+
}
|
|
2410
|
+
function cleanupRefDir(dir) {
|
|
2411
|
+
if (!dir) return;
|
|
2412
|
+
try {
|
|
2413
|
+
for (const f of readdirSync(dir)) {
|
|
2414
|
+
try {
|
|
2415
|
+
unlinkSync(resolve2(dir, f));
|
|
2416
|
+
} catch {
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
rmdirSync(dir);
|
|
2420
|
+
} catch {
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2330
2423
|
function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
2331
2424
|
const prototypeType = proto.prototypeType ?? "web_app";
|
|
2332
2425
|
const variantsToProduce = options.variantsToProduce ?? proto.variantCount;
|
|
2333
2426
|
const startIndex = options.variantStartIndex ?? 1;
|
|
2334
2427
|
const variance = typeof proto.designVariance === "number" ? proto.designVariance : 70;
|
|
2335
2428
|
const varianceInfo = describeVariance(variance);
|
|
2429
|
+
const referenceImagePaths = options.referenceImagePaths ?? [];
|
|
2430
|
+
const hasReferences = referenceImagePaths.length > 0;
|
|
2431
|
+
const exemplars = pickExemplars(proto.id);
|
|
2336
2432
|
const variantSteps = [];
|
|
2337
2433
|
for (let i = 0; i < variantsToProduce; i++) {
|
|
2338
2434
|
const idx = startIndex + i;
|
|
@@ -2340,7 +2436,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2340
2436
|
variantSteps.push(
|
|
2341
2437
|
`### Variant ${idx}: ${filename}`,
|
|
2342
2438
|
`1. ${varianceInfo.perVariantDirective}`,
|
|
2343
|
-
`2. Spend a moment composing a real, opinionated design before you start typing HTML. Imagine the screen
|
|
2439
|
+
`2. Spend a moment composing a real, opinionated design before you start typing HTML. ${hasReferences ? "Imagine the screen sitting alongside the reference designs the user attached \u2014 it should look like it belongs in the same set." : `Imagine the screen on Dribbble, holding its own next to ${exemplars}.`}`,
|
|
2344
2440
|
`3. Write the complete self-contained HTML to \`${repoDir}/${filename}\` using the Write tool.`,
|
|
2345
2441
|
`4. Verify the file was created by reading the first few lines of \`${repoDir}/${filename}\`.`,
|
|
2346
2442
|
``
|
|
@@ -2353,7 +2449,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2353
2449
|
const sharedQualityBar = [
|
|
2354
2450
|
`## Craft & Polish Bar (this is the most important section \u2014 read carefully)`,
|
|
2355
2451
|
``,
|
|
2356
|
-
`These are PORTFOLIO-QUALITY prototypes, not throwaway sketches.
|
|
2452
|
+
hasReferences ? `These are PORTFOLIO-QUALITY prototypes, not throwaway sketches. They must match the visual direction of the reference designs the user attached (see the Reference Designs section above) while hitting a high craft bar. Study the references first \u2014 everything below is secondary to them.` : `These are PORTFOLIO-QUALITY prototypes, not throwaway sketches. The reviewer will judge them at a glance against the craft level of ${exemplars}, and indie SaaS that wins design awards. Your output must look like it belongs in that company \u2014 but interpret the brief on its own terms, don't imitate any one of them.`,
|
|
2357
2453
|
``,
|
|
2358
2454
|
`**Composition & layout**`,
|
|
2359
2455
|
`- Use a real grid with intentional spacing (multiples of 4 or 8). No cramped or arbitrary padding.`,
|
|
@@ -2362,12 +2458,12 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2362
2458
|
`- Consider "uncommon" navigation: side-scrolling, vertical headers, command-palette-first interfaces, or minimalist "zen" modes.`,
|
|
2363
2459
|
``,
|
|
2364
2460
|
`**Typography**`,
|
|
2365
|
-
`- Pair a real display face with a clean text face
|
|
2461
|
+
hasReferences ? `- Match the typography of the reference designs \u2014 identify the display and text faces they use (or the closest Google Fonts equivalents) and pull them in via \`<link>\`. Tailwind CDN does NOT ship custom fonts \u2014 link them yourself.` : `- Pair a real display face with a clean text face, pulled from Google Fonts via \`<link>\`. Good starting points: Inter, Geist, Space Grotesk, Instrument Serif, Fraunces, JetBrains Mono, IBM Plex, Manrope, DM Sans, Playfair Display, Bricolage Grotesque \u2014 but don't feel limited to this list; pick faces that fit the brief and vary them across variants. Tailwind CDN does NOT ship custom fonts \u2014 link them yourself.`,
|
|
2366
2462
|
`- Use real type scales (e.g. 12 / 14 / 16 / 20 / 28 / 40 / 64). Tighten tracking on display sizes (-0.02em to -0.05em). Use weight contrast (300 vs 600), not just size.`,
|
|
2367
2463
|
`- Pay attention to line-height (1.5-1.6 for body, 1.1-1.2 for headers).`,
|
|
2368
2464
|
``,
|
|
2369
2465
|
`**Color & material**`,
|
|
2370
|
-
`-
|
|
2466
|
+
hasReferences ? `- Derive the palette from the reference designs \u2014 sample their actual colors (backgrounds, ink, accents) rather than defaulting to a generic scheme. Avoid default Tailwind blue-500 / gray-900 unless the references use them.` : `- Pick a deliberate palette per variant. Avoid default Tailwind blue-500 / gray-900. Off-whites, warm neutrals, deep inks, accent pops, gradients, and noise textures are all fair game \u2014 specific values like #F9F9F9 or #0A0A0B are illustrations, not a required palette. Commit to a distinct color story per variant.`,
|
|
2371
2467
|
`- Use depth: soft shadows (e.g. \`0 10px 30px rgba(0,0,0,0.05)\`), subtle borders (1px @ low opacity), inner glows, glassmorphism (backdrop-blur), grain overlays, or mesh gradients. No flat-default look unless that's the chosen aesthetic.`,
|
|
2372
2468
|
`- Use "subtle motion" even in static colors: very soft gradients that feel like lighting, not just a color ramp.`,
|
|
2373
2469
|
``,
|
|
@@ -2391,7 +2487,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2391
2487
|
];
|
|
2392
2488
|
const typeConfig = {
|
|
2393
2489
|
web_app: {
|
|
2394
|
-
role: "You are a senior product designer at a top-tier design-led startup, comfortable in Figma and shipping production HTML/CSS.
|
|
2490
|
+
role: "You are a senior product designer at a top-tier design-led startup, comfortable in Figma and shipping production HTML/CSS.",
|
|
2395
2491
|
guidelines: [
|
|
2396
2492
|
`- **Layout DNA**: bento grid, editorial split, command-bar driven, three-pane mail-style, dashboard with sidebar + canvas, marketing landing with pinned hero, terminal/CLI-inspired, doc-style long-form.`,
|
|
2397
2493
|
`- **Aesthetic**: Swiss minimal, brutalist editorial, glassmorphism, neumorphic, retro 90s web revival, hand-drawn / sketchy, dark IDE, pastel friendly, monochrome high-contrast, neo-skeuomorphic, "soft-ui" with deep shadows.`,
|
|
@@ -2420,7 +2516,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2420
2516
|
]
|
|
2421
2517
|
},
|
|
2422
2518
|
desktop_app: {
|
|
2423
|
-
role: "You are a senior desktop product designer who
|
|
2519
|
+
role: "You are a senior desktop product designer who ships polished, best-in-class, native-feeling apps and doesn't settle for generic.",
|
|
2424
2520
|
guidelines: [
|
|
2425
2521
|
`- **Window chrome**: include realistic traffic-light controls or Windows controls, an integrated title bar, sidebar/main split, optional inspector pane.`,
|
|
2426
2522
|
`- **Layout DNA**: three-pane mail-style, sidebar + canvas + inspector, command-bar driven (kbar / Raycast), tabbed workspace, ribbon toolbar, split-pane editor, agenda + detail, "spatial" layout with floating windows.`,
|
|
@@ -2459,6 +2555,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2459
2555
|
logo: "Logo"
|
|
2460
2556
|
};
|
|
2461
2557
|
const agentNote = options.agentLabel ? `You are one of several models collaborating on this prototype set. You are the "${options.agentLabel}" track, responsible for variants ${startIndex} through ${startIndex + variantsToProduce - 1}. Bring YOUR creative perspective \u2014 don't try to be a generic averaged designer.` : `You are the sole designer on this prototype set \u2014 bring a strong, opinionated point of view.`;
|
|
2558
|
+
const creativeAnchor = hasReferences ? `The user attached reference designs for this prototype \u2014 they are the single most important input. Treat them as the definitive brief for look and feel (details in the Reference Designs section below).` : `Aim for the craft level of ${exemplars} \u2014 but interpret this brief on its own terms, don't imitate any one product.`;
|
|
2462
2559
|
const creativeDirection = [
|
|
2463
2560
|
`## Creative Direction & Polish`,
|
|
2464
2561
|
`- **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.`,
|
|
@@ -2472,6 +2569,8 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2472
2569
|
``,
|
|
2473
2570
|
agentNote,
|
|
2474
2571
|
``,
|
|
2572
|
+
creativeAnchor,
|
|
2573
|
+
``,
|
|
2475
2574
|
`Working directory: ${repoDir}`,
|
|
2476
2575
|
``,
|
|
2477
2576
|
`## Prototype Request`,
|
|
@@ -2482,6 +2581,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2482
2581
|
`## Design Prompt`,
|
|
2483
2582
|
`${proto.prompt}`,
|
|
2484
2583
|
``,
|
|
2584
|
+
...buildReferenceSection(referenceImagePaths),
|
|
2485
2585
|
`## Instructions`,
|
|
2486
2586
|
``,
|
|
2487
2587
|
`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.`,
|
|
@@ -2522,6 +2622,7 @@ function buildPrototypePrompt(proto, repoDir, options = {}) {
|
|
|
2522
2622
|
].join("\n");
|
|
2523
2623
|
}
|
|
2524
2624
|
function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
|
|
2625
|
+
const referenceImagePaths = options.referenceImagePaths ?? [];
|
|
2525
2626
|
const variantsToProduce = options.variantsToProduce ?? proto.variantCount;
|
|
2526
2627
|
const startIndex = options.variantStartIndex ?? 1;
|
|
2527
2628
|
const variance = typeof proto.designVariance === "number" ? proto.designVariance : 70;
|
|
@@ -2586,6 +2687,7 @@ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
|
|
|
2586
2687
|
`## Original Design Prompt`,
|
|
2587
2688
|
`${proto.prompt}`,
|
|
2588
2689
|
``,
|
|
2690
|
+
...buildReferenceSection(referenceImagePaths),
|
|
2589
2691
|
`## User Feedback (IMPORTANT \u2014 this is what you must address)`,
|
|
2590
2692
|
`${proto.refinementFeedback}`,
|
|
2591
2693
|
``,
|
|
@@ -3194,6 +3296,15 @@ var watchCommand = new Command9("watch").description(
|
|
|
3194
3296
|
...prUrl ? { link: prUrl } : {}
|
|
3195
3297
|
});
|
|
3196
3298
|
logSuccess(prefix, `"${paint("bold", task.title)}" marked ready for review`);
|
|
3299
|
+
if (prUrl && currentTask.autoMergeEnabled) {
|
|
3300
|
+
try {
|
|
3301
|
+
await api.post(`/api/tasks/${task.id}/auto-merge`, {});
|
|
3302
|
+
await api.patch(`/api/tasks/${task.id}`, { autoMergeEnabled: false });
|
|
3303
|
+
logDispatch(prefix, `Auto-merge preference detected \u2014 sanity review queued`);
|
|
3304
|
+
} catch (err) {
|
|
3305
|
+
logError(prefix, `Failed to queue auto-merge: ${err.message}`);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3197
3308
|
if (noMrRequested) {
|
|
3198
3309
|
await postTaskUpdate(task.id, `No ${prLabel} required: ${noMrDescription}`, "system");
|
|
3199
3310
|
}
|
|
@@ -3385,6 +3496,14 @@ var watchCommand = new Command9("watch").description(
|
|
|
3385
3496
|
} catch {
|
|
3386
3497
|
}
|
|
3387
3498
|
}
|
|
3499
|
+
const { paths: refImagePaths, dir: refImageDir } = await downloadReferenceImages(
|
|
3500
|
+
proto,
|
|
3501
|
+
repoDir,
|
|
3502
|
+
prefix
|
|
3503
|
+
);
|
|
3504
|
+
if (refImagePaths.length > 0) {
|
|
3505
|
+
logDispatch(prefix, `${refImagePaths.length} reference image(s) attached`);
|
|
3506
|
+
}
|
|
3388
3507
|
const validAgents = ["claude", "codex", "gemini"];
|
|
3389
3508
|
const requested = Array.isArray(proto.selectedAgents) ? proto.selectedAgents.filter(
|
|
3390
3509
|
(a) => validAgents.includes(a)
|
|
@@ -3405,13 +3524,15 @@ var watchCommand = new Command9("watch").description(
|
|
|
3405
3524
|
return buildRefinementPrompt(proto, parentFiles, sliceDir, {
|
|
3406
3525
|
variantStartIndex: startIndex,
|
|
3407
3526
|
variantsToProduce,
|
|
3408
|
-
agentLabel
|
|
3527
|
+
agentLabel,
|
|
3528
|
+
referenceImagePaths: refImagePaths
|
|
3409
3529
|
});
|
|
3410
3530
|
}
|
|
3411
3531
|
return buildPrototypePrompt(proto, sliceDir, {
|
|
3412
3532
|
variantStartIndex: startIndex,
|
|
3413
3533
|
variantsToProduce,
|
|
3414
|
-
agentLabel
|
|
3534
|
+
agentLabel,
|
|
3535
|
+
referenceImagePaths: refImagePaths
|
|
3415
3536
|
});
|
|
3416
3537
|
};
|
|
3417
3538
|
if (dedupedRequested.length <= 1) {
|
|
@@ -3421,6 +3542,7 @@ var watchCommand = new Command9("watch").description(
|
|
|
3421
3542
|
logError(prefix, `No available agents found for ${preferred}`);
|
|
3422
3543
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" }).catch(() => {
|
|
3423
3544
|
});
|
|
3545
|
+
cleanupRefDir(refImageDir);
|
|
3424
3546
|
queued.delete(key);
|
|
3425
3547
|
return;
|
|
3426
3548
|
}
|
|
@@ -3505,6 +3627,7 @@ var watchCommand = new Command9("watch").description(
|
|
|
3505
3627
|
}
|
|
3506
3628
|
}
|
|
3507
3629
|
} finally {
|
|
3630
|
+
cleanupRefDir(refImageDir);
|
|
3508
3631
|
queued.delete(key);
|
|
3509
3632
|
finishing.delete(key);
|
|
3510
3633
|
}
|
|
@@ -3540,6 +3663,7 @@ var watchCommand = new Command9("watch").description(
|
|
|
3540
3663
|
logError(prefix, `Could not assign any variants to selected agents`);
|
|
3541
3664
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" }).catch(() => {
|
|
3542
3665
|
});
|
|
3666
|
+
cleanupRefDir(refImageDir);
|
|
3543
3667
|
queued.delete(key);
|
|
3544
3668
|
return;
|
|
3545
3669
|
}
|
|
@@ -3661,6 +3785,7 @@ var watchCommand = new Command9("watch").description(
|
|
|
3661
3785
|
} catch {
|
|
3662
3786
|
}
|
|
3663
3787
|
} finally {
|
|
3788
|
+
cleanupRefDir(refImageDir);
|
|
3664
3789
|
queued.delete(key);
|
|
3665
3790
|
finishing.delete(key);
|
|
3666
3791
|
}
|
|
@@ -4370,6 +4495,97 @@ ${divider}`);
|
|
|
4370
4495
|
} catch (err) {
|
|
4371
4496
|
logError(prefix, `Failed to re-queue task for conflict resolution: ${err.message}`);
|
|
4372
4497
|
}
|
|
4498
|
+
continue;
|
|
4499
|
+
}
|
|
4500
|
+
if (task.autoMergeEnabled) {
|
|
4501
|
+
try {
|
|
4502
|
+
await api.post(`/api/tasks/${task.id}/auto-merge`, {});
|
|
4503
|
+
await api.patch(`/api/tasks/${task.id}`, { autoMergeEnabled: false });
|
|
4504
|
+
logDispatch(prefix, `Auto-merge preference detected \u2014 sanity review queued`);
|
|
4505
|
+
} catch (err) {
|
|
4506
|
+
logError(prefix, `Failed to queue auto-merge: ${err.message}`);
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
let completedAutoMergeReviews = [];
|
|
4511
|
+
try {
|
|
4512
|
+
completedAutoMergeReviews = await api.get("/api/reviews?status=completed&limit=20");
|
|
4513
|
+
} catch (err) {
|
|
4514
|
+
logError(watchTag(), `Failed to fetch completed reviews: ${err.message}`);
|
|
4515
|
+
}
|
|
4516
|
+
for (const review of completedAutoMergeReviews) {
|
|
4517
|
+
if (!review.autoMergeTaskId || review.autoMergeStatus !== "pending") continue;
|
|
4518
|
+
const taskId = review.autoMergeTaskId;
|
|
4519
|
+
const sid = shortId(review.id);
|
|
4520
|
+
const prefix = `${paint("blue", `[auto-merge:${sid}]`)}`;
|
|
4521
|
+
const repoDir = findDirectoryForProject(config, review.projectId, rootDir);
|
|
4522
|
+
if (!repoDir) {
|
|
4523
|
+
logWarn(prefix, `No local repo for project ${review.projectId} \u2014 cannot auto-merge`);
|
|
4524
|
+
try {
|
|
4525
|
+
await api.patch(`/api/reviews/${review.id}`, { autoMergeStatus: "blocked" });
|
|
4526
|
+
await postTaskUpdate(taskId, `Auto-merge blocked: no local checkout found for this project`, "system");
|
|
4527
|
+
} catch {
|
|
4528
|
+
}
|
|
4529
|
+
continue;
|
|
4530
|
+
}
|
|
4531
|
+
const findings = review.findings ?? [];
|
|
4532
|
+
const blocking = findings.filter((f) => f.severity === "critical" || f.severity === "high");
|
|
4533
|
+
if (blocking.length > 0) {
|
|
4534
|
+
logWarn(prefix, `Auto-merge blocked \u2014 ${blocking.length} ${blocking.length === 1 ? "issue" : "issues"} flagged`);
|
|
4535
|
+
const lines = blocking.slice(0, 10).map((f) => `- **${f.severity}**: ${f.title}${f.file ? ` (\`${f.file}${f.line ? `:${f.line}` : ""}\`)` : ""}`);
|
|
4536
|
+
const summaryBlock = review.summary ? `${review.summary}
|
|
4537
|
+
|
|
4538
|
+
` : "";
|
|
4539
|
+
const message = `Auto-merge blocked \u2014 sanity review flagged ${blocking.length} blocking issue${blocking.length === 1 ? "" : "s"}.
|
|
4540
|
+
|
|
4541
|
+
${summaryBlock}${lines.join("\n")}`;
|
|
4542
|
+
try {
|
|
4543
|
+
await api.patch(`/api/reviews/${review.id}`, { autoMergeStatus: "blocked" });
|
|
4544
|
+
await postTaskUpdate(taskId, message, "system");
|
|
4545
|
+
} catch (err) {
|
|
4546
|
+
logError(prefix, `Failed to record auto-merge block: ${err.message}`);
|
|
4547
|
+
}
|
|
4548
|
+
continue;
|
|
4549
|
+
}
|
|
4550
|
+
if (!review.branchUrl) {
|
|
4551
|
+
logWarn(prefix, `No PR/MR URL on review \u2014 cannot auto-merge`);
|
|
4552
|
+
try {
|
|
4553
|
+
await api.patch(`/api/reviews/${review.id}`, { autoMergeStatus: "blocked" });
|
|
4554
|
+
await postTaskUpdate(taskId, `Auto-merge blocked: review is missing the PR/MR URL`, "system");
|
|
4555
|
+
} catch {
|
|
4556
|
+
}
|
|
4557
|
+
continue;
|
|
4558
|
+
}
|
|
4559
|
+
const vcs = review.branchUrl.includes("gitlab") ? "gitlab" : "github";
|
|
4560
|
+
const prLabel = vcs === "gitlab" ? "MR" : "PR";
|
|
4561
|
+
const preMergeStatus = await checkPrStatus(review.branchUrl, repoDir, vcs);
|
|
4562
|
+
if (preMergeStatus?.merged) {
|
|
4563
|
+
logSuccess(prefix, `${prLabel} already merged \u2014 recording auto-merge as complete`);
|
|
4564
|
+
try {
|
|
4565
|
+
await api.patch(`/api/reviews/${review.id}`, { autoMergeStatus: "merged" });
|
|
4566
|
+
await postTaskUpdate(taskId, `Auto-merge complete \u2014 ${prLabel} was already merged`, "system");
|
|
4567
|
+
} catch (err) {
|
|
4568
|
+
logError(prefix, `Failed to record auto-merge success: ${err.message}`);
|
|
4569
|
+
}
|
|
4570
|
+
continue;
|
|
4571
|
+
}
|
|
4572
|
+
logDispatch(prefix, `Sanity review clean \u2014 merging ${prLabel} ${paint("cyan", review.branchUrl)}`);
|
|
4573
|
+
const mergeResult = await mergePrViaCli(review.branchUrl, repoDir, vcs);
|
|
4574
|
+
if (!mergeResult.ok) {
|
|
4575
|
+
logError(prefix, `Auto-merge failed: ${mergeResult.error}`);
|
|
4576
|
+
try {
|
|
4577
|
+
await api.patch(`/api/reviews/${review.id}`, { autoMergeStatus: "failed" });
|
|
4578
|
+
await postTaskUpdate(taskId, `Auto-merge failed: ${mergeResult.error}`, "system");
|
|
4579
|
+
} catch {
|
|
4580
|
+
}
|
|
4581
|
+
continue;
|
|
4582
|
+
}
|
|
4583
|
+
try {
|
|
4584
|
+
await api.patch(`/api/reviews/${review.id}`, { autoMergeStatus: "merged" });
|
|
4585
|
+
await postTaskUpdate(taskId, `Auto-merge complete \u2014 sanity review passed and ${prLabel} merged`, "system");
|
|
4586
|
+
logSuccess(prefix, `Auto-merge complete for ${prLabel}`);
|
|
4587
|
+
} catch (err) {
|
|
4588
|
+
logError(prefix, `Failed to record auto-merge success: ${err.message}`);
|
|
4373
4589
|
}
|
|
4374
4590
|
}
|
|
4375
4591
|
} finally {
|