@dunnewold-labs/mr-manager 0.4.38 → 0.4.39

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.
Files changed (2) hide show
  1. package/dist/index.mjs +851 -277
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // cli/index.ts
4
- import { Command as Command31 } from "commander";
4
+ import { Command as Command33 } from "commander";
5
5
  import { existsSync as existsSync18 } from "fs";
6
6
  import { homedir as homedir3 } from "os";
7
7
  import { join as join12 } from "path";
@@ -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.38",
188
+ version: "0.4.39",
189
189
  description: "Mr. Manager - Task and project management CLI",
190
190
  bin: {
191
191
  mr: "./dist/index.mjs"
@@ -551,7 +551,7 @@ import { Command as Command9 } from "commander";
551
551
  import { spawn as spawn4, exec } from "child_process";
552
552
  import { randomUUID } from "crypto";
553
553
  import { resolve as resolve2 } from "path";
554
- import { readFileSync as readFileSync5, readdirSync, unlinkSync, existsSync as existsSync7, statSync, writeFileSync as writeFileSync3 } from "fs";
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
 
557
557
  // lib/test-runner.ts
@@ -1869,20 +1869,40 @@ function buildPrototypeSection(protoRefs, workingDir) {
1869
1869
  }
1870
1870
  return sections.join("\n");
1871
1871
  }
1872
- function buildSkillsSection(skillRefs) {
1872
+ function buildSkillsSection(skillRefs, taskId) {
1873
1873
  if (skillRefs.length === 0) return "";
1874
1874
  const sections = skillRefs.map(
1875
- (ref) => `### ${ref.skill.name}
1875
+ (ref) => `### ${ref.skill.name}
1876
+ *(skill id: \`${ref.skill.id}\`)*
1876
1877
 
1877
1878
  ${ref.skill.content}`
1878
1879
  );
1880
+ const proposalTitleExample = skillRefs[0]?.skill.name ? `Skill Improvement \u2014 ${skillRefs[0].skill.name}` : `Skill Improvement \u2014 <skill name>`;
1879
1881
  return [
1880
1882
  ``,
1881
1883
  `## Attached Skills / Playbooks`,
1882
1884
  ``,
1883
1885
  `The following playbooks describe tools, workflows, and key considerations for this type of work. Use them as contextual guidance \u2014 adapt to the specific task rather than following rigidly.`,
1884
1886
  ``,
1885
- sections.join("\n\n---\n\n")
1887
+ sections.join("\n\n---\n\n"),
1888
+ ``,
1889
+ `---`,
1890
+ ``,
1891
+ `### Improving These Skills`,
1892
+ ``,
1893
+ `As you use these skills, pay attention to gaps and rough edges: missing steps, stale commands, unclear guidance, or anything you had to figure out yourself that the skill should have told you. Capture those observations while the work is fresh.`,
1894
+ ``,
1895
+ `When you finish the task, decide per skill:`,
1896
+ ``,
1897
+ `- **Minor, clearly-correct improvements** (fixing a stale command, adding a missing step you just verified, clarifying wording): apply them directly with`,
1898
+ ` \`mr skill update <skill-id> --file /tmp/skill-<name>.md\``,
1899
+ ` The previous content is automatically saved as a revision, so the edit is reversible. Write the full updated markdown to the file first; the command replaces the skill content wholesale.`,
1900
+ `- **Larger rewrites, judgment calls, or anything you're unsure about**: propose the change for human review by attaching a note resource to this task instead of editing the skill:`,
1901
+ ` \`mr update ${taskId ?? "<task-id>"} --resource note "${proposalTitleExample}" '<markdown describing the proposed change, the skill id, and why>'\``,
1902
+ ` Include the target skill id, a concise diff or replacement block, and the reasoning so the user can review and apply it.`,
1903
+ ``,
1904
+ `If the attached skills are already accurate and complete for this task, do nothing \u2014 do not invent improvements for the sake of it.`,
1905
+ ``
1886
1906
  ].join("\n");
1887
1907
  }
1888
1908
  function buildResourcesSection(resources) {
@@ -1990,12 +2010,25 @@ ${task.notes}` : "";
1990
2010
  ``,
1991
2011
  `## Subtasks`,
1992
2012
  ``,
1993
- `The following subtasks need to be completed. As you finish each one, mark it done by running:`,
2013
+ `The following subtasks have already been planned for this task. Work through them in order, and as you finish each one mark it done by running:`,
1994
2014
  `\`mr subtask-complete ${task.id} <subtask-id>\``,
1995
2015
  ``,
1996
2016
  ...pendingSubtasks.map((s) => `- [ ] ${s.title} (id: ${s.id})`),
2017
+ ``,
2018
+ `If you discover additional steps along the way, add them with \`mr subtask-add ${task.id} "<short title>"\` so progress stays visible in the UI.`,
2019
+ ``
2020
+ ].join("\n") : [
2021
+ ``,
2022
+ `## Subtasks (you create these)`,
2023
+ ``,
2024
+ `This task has no subtasks yet. Before you start implementing, break the work into a short ordered checklist and create one subtask per step so progress is visible in the UI as you go.`,
2025
+ ``,
2026
+ `- Add each step with: \`mr subtask-add ${task.id} "<short imperative title>"\``,
2027
+ `- Aim for roughly 3\u20137 subtasks. Each should be a concrete unit of work (e.g. "Add API endpoint for X", "Wire up UI button", "Write migration"), not a vague phase.`,
2028
+ `- After creating each subtask the command prints its id. As you finish each step, mark it done immediately with \`mr subtask-complete ${task.id} <subtask-id>\` before moving on.`,
2029
+ `- If new work surfaces mid-task, add another subtask rather than silently expanding scope.`,
1997
2030
  ``
1998
- ].join("\n") : "";
2031
+ ].join("\n");
1999
2032
  const hasFeedback = feedbackUpdates.length > 0;
2000
2033
  const prBodyTemplate = buildPrBodyTemplate(task, pendingSubtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
2001
2034
  const prCreateCmd = vcs === "gitlab" ? `glab mr create --title "${task.title}" --description-file ${prBodyPath} --yes` : `gh pr create --title "${task.title}" --body-file ${prBodyPath}`;
@@ -2009,7 +2042,7 @@ ${task.notes}` : "";
2009
2042
  subtaskSection,
2010
2043
  buildResourcesSection(existingResources),
2011
2044
  buildPrototypeSection(protoRefs, workingDir),
2012
- buildSkillsSection(skillRefs),
2045
+ buildSkillsSection(skillRefs, task.id),
2013
2046
  buildFeedbackSection(feedbackUpdates),
2014
2047
  `## Instructions`,
2015
2048
  ``,
@@ -2045,7 +2078,9 @@ ${task.notes}` : "";
2045
2078
  ` - If you do significant research or investigation, save your findings as a resource using: \`mr update ${task.id} --resource research "Research \u2014 <short title>" '<markdown content>'\``,
2046
2079
  ...pendingSubtasks.length > 0 ? [
2047
2080
  ` - Work through each subtask in order. After completing each subtask, immediately run \`mr subtask-complete ${task.id} <subtask-id>\` to mark it done before moving on.`
2048
- ] : [],
2081
+ ] : [
2082
+ ` - Before writing code, create your subtask checklist (see the Subtasks section above) so the user can see your plan. Then work through each subtask in order, running \`mr subtask-complete ${task.id} <subtask-id>\` as soon as each one is done.`
2083
+ ],
2049
2084
  ``,
2050
2085
  `4. Once implementation is complete, for each repo that has changes:`,
2051
2086
  ` a. Commit all changes with a clear, descriptive message.`,
@@ -2243,82 +2278,176 @@ ${initPrompt}`] : [],
2243
2278
  `Exit with code 0 when done (success or failure \u2014 do not retry indefinitely).`
2244
2279
  ].join("\n");
2245
2280
  }
2246
- function buildPrototypePrompt(proto, repoDir) {
2281
+ function describeVariance(variance) {
2282
+ if (variance <= 15) {
2283
+ return {
2284
+ label: "Very Similar",
2285
+ summary: "All variants should feel like members of the same product family. Think of these as A/B tweaks of a single design \u2014 same overall layout, same type system, same general palette.",
2286
+ perVariantDirective: "Keep the same overall layout, type system, and palette as the other variants. Vary only subtle details (spacing, micro-copy, accent color shade, button style, minor component swaps).",
2287
+ axes: [
2288
+ "- **What to vary**: spacing, accent color shade, button shape/weight, icon set, micro-copy",
2289
+ "- **What to keep the same**: overall layout, typography scale, information architecture, core palette"
2290
+ ]
2291
+ };
2292
+ }
2293
+ if (variance <= 40) {
2294
+ return {
2295
+ label: "Slight Variation",
2296
+ summary: "Variants share the same overall direction but try different compositions and palettes. Same product personality, different executions.",
2297
+ perVariantDirective: "Keep the same general product personality and information architecture as the other variants, but change the composition, palette, and typography choices.",
2298
+ axes: [
2299
+ "- **What to vary**: layout composition, palette, typography pairing, component styling, density",
2300
+ "- **What to keep the same**: product personality, information architecture, tone"
2301
+ ]
2302
+ };
2303
+ }
2304
+ if (variance <= 70) {
2305
+ return {
2306
+ label: "Clearly Different",
2307
+ summary: "Each variant should feel like a distinct design direction. Different layouts, different palettes, different personalities \u2014 but all reasonable interpretations of the user's prompt.",
2308
+ perVariantDirective: "Pick a meaningfully different design direction from the other variants \u2014 different layout, different palette, different typographic voice, different overall personality.",
2309
+ axes: [
2310
+ "- **Layout**: genuinely different structural choices (grid vs. asymmetric vs. single-column vs. split-pane, etc.)",
2311
+ "- **Palette & mood**: distinct color stories and emotional tones",
2312
+ "- **Typography**: different type pairings and hierarchies",
2313
+ "- **Personality**: different audiences / brand voices can be implied"
2314
+ ]
2315
+ };
2316
+ }
2317
+ return {
2318
+ label: "Drastically Different",
2319
+ summary: "Push the variants as far apart as possible. Each one should feel like it came from a different designer with a different brief \u2014 radically different layouts, aesthetics, and interaction models. The only thing they share is fulfilling the user's core request.",
2320
+ perVariantDirective: "Deliberately choose a radically different aesthetic, layout, and interaction model from the other variants. Each variant should feel like it belongs to a different product universe.",
2321
+ axes: [
2322
+ "- **Layout**: invent a structurally distinct approach for each variant",
2323
+ "- **Aesthetic direction**: each variant should commit to a different visual identity (no two variants should feel related)",
2324
+ "- **Interaction model**: vary scroll vs. click vs. hover-driven experiences",
2325
+ "- **Information density**: span from sparse/editorial to dense/utilitarian",
2326
+ "- **Personality**: target different audiences and moods across variants"
2327
+ ]
2328
+ };
2329
+ }
2330
+ function buildPrototypePrompt(proto, repoDir, options = {}) {
2247
2331
  const prototypeType = proto.prototypeType ?? "web_app";
2332
+ const variantsToProduce = options.variantsToProduce ?? proto.variantCount;
2333
+ const startIndex = options.variantStartIndex ?? 1;
2334
+ const variance = typeof proto.designVariance === "number" ? proto.designVariance : 70;
2335
+ const varianceInfo = describeVariance(variance);
2248
2336
  const variantSteps = [];
2249
- for (let i = 1; i <= proto.variantCount; i++) {
2250
- const filename = `prototype-${i}.html`;
2337
+ for (let i = 0; i < variantsToProduce; i++) {
2338
+ const idx = startIndex + i;
2339
+ const filename = `prototype-${idx}.html`;
2251
2340
  variantSteps.push(
2252
- `### Variant ${i} of ${proto.variantCount}: ${filename}`,
2253
- `1. Design variant ${i} with a unique creative direction \u2014 different layout, color scheme, typography, and visual style from the other variants.`,
2254
- `2. Write the complete self-contained HTML to \`${repoDir}/${filename}\` using the Write tool.`,
2255
- `3. Verify the file was created by reading the first few lines of \`${repoDir}/${filename}\`.`,
2341
+ `### Variant ${idx}: ${filename}`,
2342
+ `1. ${varianceInfo.perVariantDirective}`,
2343
+ `2. Spend a moment composing a real, opinionated design before you start typing HTML. Imagine the screen on Dribbble or in a Linear / Stripe / Vercel / Arc / Things-style portfolio.`,
2344
+ `3. Write the complete self-contained HTML to \`${repoDir}/${filename}\` using the Write tool.`,
2345
+ `4. Verify the file was created by reading the first few lines of \`${repoDir}/${filename}\`.`,
2256
2346
  ``
2257
2347
  );
2258
2348
  }
2259
- const variantList = Array.from({ length: proto.variantCount }, (_, i) => `prototype-${i + 1}.html`);
2349
+ const variantList = Array.from(
2350
+ { length: variantsToProduce },
2351
+ (_, i) => `prototype-${startIndex + i}.html`
2352
+ );
2353
+ const sharedQualityBar = [
2354
+ `## Craft & Polish Bar (this is the most important section \u2014 read carefully)`,
2355
+ ``,
2356
+ `These are PORTFOLIO-QUALITY prototypes, not throwaway sketches. The reviewer will judge them at a glance against work from Linear, Vercel, Stripe, Arc, Things, Things 3, Notion, Figma, Apple, Cron, Raycast, Superhuman, and indie SaaS that wins design awards. Your output must look like it belongs in that company.`,
2357
+ ``,
2358
+ `**Composition & layout**`,
2359
+ `- Use a real grid with intentional spacing (multiples of 4 or 8). No cramped or arbitrary padding.`,
2360
+ `- Establish clear hierarchy: 1 hero element, 2-3 supporting beats, ample negative space. Most variants should breathe.`,
2361
+ `- Avoid the "centered hero + three feature cards" clich\xE9 unless you have a specific reason. Push for unexpected compositions: asymmetric splits, bento grids, editorial column layouts, full-bleed media, sticky-side panels, diagonal layouts, magazine-style pull quotes.`,
2362
+ `- Consider "uncommon" navigation: side-scrolling, vertical headers, command-palette-first interfaces, or minimalist "zen" modes.`,
2363
+ ``,
2364
+ `**Typography**`,
2365
+ `- Pair a real display face with a clean text face. Pull from Google Fonts via \`<link>\` (Inter, Geist, Space Grotesk, Instrument Serif, Fraunces, S\xF6hne-style fallbacks, JetBrains Mono, IBM Plex, Manrope, DM Sans, Playfair Display, Caladea, Bricolage Grotesque). Tailwind CDN does NOT ship custom fonts \u2014 link them yourself.`,
2366
+ `- 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
+ `- Pay attention to line-height (1.5-1.6 for body, 1.1-1.2 for headers).`,
2368
+ ``,
2369
+ `**Color & material**`,
2370
+ `- Pick a deliberate palette per variant. Avoid default Tailwind blue-500 / gray-900 unless that IS the design intent. Mix off-whites (#F9F9F9), warm neutrals, deep ink colors (#0A0A0B), accent pops, gradients, noise textures.`,
2371
+ `- 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
+ `- Use "subtle motion" even in static colors: very soft gradients that feel like lighting, not just a color ramp.`,
2373
+ ``,
2374
+ `**Content (this is where most prototypes fail)**`,
2375
+ `- Write specific, believable microcopy. NO "Lorem ipsum". NO "Feature One / Feature Two / Feature Three". NO placeholder names like "John Doe" or "Acme Inc."`,
2376
+ `- Invent realistic product copy, sample data, names, dates, numbers, statuses, and supporting metadata that match the prompt's domain.`,
2377
+ `- Populate lists, tables, and feeds with at least 6-12 distinct entries \u2014 empty-looking UIs feel fake.`,
2378
+ ``,
2379
+ `**Visual richness**`,
2380
+ `- Include real iconography: prefer inline SVG icons (Lucide, Heroicons, Phosphor \u2014 copy paths inline). Don't stub with emoji unless the design language calls for it.`,
2381
+ `- Use inline SVG illustrations, charts, sparklines, avatars (initials in colored circles, NOT broken \`<img>\` tags), badges, and status pills where they earn their place.`,
2382
+ `- Any \`<img>\` you include MUST resolve. Use \`https://images.unsplash.com/photo-...\` URLs you actually know exist, or replace with SVG. NEVER use \`/path/to/image.jpg\` or other broken paths.`,
2383
+ ``,
2384
+ `**Motion & interaction**`,
2385
+ `- Add tasteful CSS transitions on hover/focus (0.2s ease-out), subtle entrance animations (fade-in + slide-up), and at least one delightful detail per variant (animated gradient, hover lift, marquee, count-up, stagger animation). Keep it polished, not gimmicky.`,
2386
+ `- Use \`@keyframes\` for sophisticated micro-interactions: a button that "pulses" slightly when hovered, or a sidebar that slides in with a spring-like feel (cubic-bezier).`,
2387
+ ``,
2388
+ `**Code hygiene**`,
2389
+ `- Tailwind CDN is acceptable as the utility layer, but custom CSS in a \`<style>\` block is encouraged for the parts that make the design feel bespoke (custom fonts, gradients, keyframes, grain, etc.).`,
2390
+ `- Keep markup semantic. Use real \`<header>\`, \`<nav>\`, \`<main>\`, \`<section>\`, \`<aside>\`, \`<footer>\` where they apply.`
2391
+ ];
2260
2392
  const typeConfig = {
2261
2393
  web_app: {
2262
- role: "You are a UI designer and frontend engineer. Your job is to generate high-quality, visually distinct web app prototype variants based on the user's design request.",
2394
+ role: "You are a senior product designer at a top-tier design-led startup, comfortable in Figma and shipping production HTML/CSS. You design at the level of Linear, Vercel, Stripe, and Arc.",
2263
2395
  guidelines: [
2264
- `- **Layout**: Grid, list, card, single-page, multi-section`,
2265
- `- **Style**: Minimal, bold, playful, corporate, retro, futuristic`,
2266
- `- **Color scheme**: Light, dark, colorful, monochrome`,
2267
- `- **Interaction model**: Click-heavy, scroll-based, hover effects, animated`,
2268
- `- **Information density**: Sparse, balanced, dense`,
2269
- `- **Navigation**: Sidebar, top nav, bottom nav, hamburger, tabs`
2396
+ `- **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
+ `- **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.`,
2398
+ `- **Color story**: warm-cream + ink, deep navy + electric accent, off-white + serif + warm yellow, bold gradient + frosted glass, ultra-mono with one neon accent, earthy clay tones.`,
2399
+ `- **Interaction language**: hover-rich with micro-animations, scroll-driven storytelling, command-palette first, keyboard-shortcut overlay, drag-and-drop canvas, interactive data-viz.`,
2400
+ `- **Density**: from "Apple marketing whitespace" to "Bloomberg Terminal density" \u2014 push to opposite ends across variants.`,
2401
+ `- **Navigation**: floating dock, hidden sidebar that appears on hover, multi-tab workspace, breadcrumb-driven, tabbed canvas, kbar, persistent footer chrome, minimalist hamburger with full-screen menu.`
2270
2402
  ],
2271
2403
  rules: [
2272
- `- Each file must be a complete, functional page \u2014 pure HTML/CSS/JS, no external libraries (Tailwind CDN is acceptable).`
2404
+ `- Each file is a complete self-contained HTML page. Tailwind CDN allowed; you may also include Google Fonts via \`<link>\` and inline SVG.`
2273
2405
  ]
2274
2406
  },
2275
2407
  mobile_app: {
2276
- role: "You are a mobile UI designer and frontend engineer. Your job is to generate high-quality, visually distinct mobile app prototype variants based on the user's design request.",
2408
+ role: "You are a senior mobile product designer who ships work that gets shared on Mobbin and Designspiration. You think in Apple HIG and Material 3 fluently and break the rules where it earns the design.",
2277
2409
  guidelines: [
2278
- `- **Viewport**: Use a mobile viewport (375px wide max, 812px tall). Center the phone frame on a neutral background.`,
2279
- `- **Layout**: Bottom tabs, stack navigation, cards, lists, full-screen views`,
2280
- `- **Style**: iOS-inspired, Material Design, custom/brand-driven, minimal`,
2281
- `- **Color scheme**: Light mode, dark mode, brand-colored`,
2282
- `- **Touch patterns**: Large tap targets (44px+), swipe gestures indicated, thumb-friendly navigation`,
2283
- `- **Typography**: Mobile-scale fonts (14-18px body), clear hierarchy`,
2284
- `- **Navigation**: Bottom tab bar, top navigation bar, hamburger drawer, floating action button`
2410
+ `- **Frame**: Center a 375\xD7812 phone-sized canvas on a moody background (gradient, noisy off-white, soft tinted blur). Add a notch / Dynamic Island, status bar (time, signal, battery), and home indicator. Treat the surrounding canvas like a Dribbble shot \u2014 make IT feel designed too.`,
2411
+ `- **Layout DNA**: bottom tabs with floating action, list-detail with sticky header, card-stack swipe, full-bleed feed, modal sheet over content, native iOS settings-style grouped lists, Spotify-style hero + scroll, Things 3-style focused single column, circular "control wheel" UI.`,
2412
+ `- **Aesthetic**: glassy iOS, Material You expressive color, dark OLED app, soft pastel friendly, tactile retro, neo-skeuomorphic, Linear/Things minimal, brand-led editorial, "depth-first" with stacked cards.`,
2413
+ `- **Touch & motion**: large tap targets (44px+), suggested swipe affordances, sheet pull handles, tab bar with bouncy active indicator, haptic-feel hover transforms, page-turn transitions.`,
2414
+ `- **Typography**: SF Pro / Inter / Geist via Google Fonts; clear scale (11/13/15/17/22/28/34); use weight contrast.`
2285
2415
  ],
2286
2416
  rules: [
2287
- `- Render the app as a phone-sized frame (375\xD7812px) centered on a light gray page background.`,
2288
- `- Use a \`<meta name="viewport" content="width=375">\` tag.`,
2289
- `- Each file must be a complete, functional HTML page \u2014 pure HTML/CSS/JS, no external libraries (Tailwind CDN is acceptable).`
2417
+ `- Render the app as a 375\xD7812 device frame centered on a designed background canvas (NOT plain white).`,
2418
+ `- Use \`<meta name="viewport" content="width=device-width, initial-scale=1">\`. Inside, render the phone as a fixed-size container with realistic device chrome (notch, status bar, home indicator).`,
2419
+ `- Each file is a complete self-contained HTML page. Tailwind CDN + Google Fonts via \`<link>\` allowed.`
2290
2420
  ]
2291
2421
  },
2292
2422
  desktop_app: {
2293
- role: "You are a desktop UI designer and frontend engineer. Your job is to generate high-quality, visually distinct desktop application prototype variants based on the user's design request.",
2423
+ role: "You are a senior desktop product designer who has shipped work on Linear, Things 3, Cron, Raycast, Notion, and Arc. You design at the level of those apps and don't settle for generic.",
2294
2424
  guidelines: [
2295
- `- **Viewport**: Full-width desktop layout (1280px+). Use the full browser window.`,
2296
- `- **Layout**: Multi-panel, sidebar + main content, menubar, toolbar, status bar`,
2297
- `- **Style**: Native OS-inspired (macOS, Windows), Electron-style, productivity tool, pro/creative app`,
2298
- `- **Color scheme**: Light, dark, system-default`,
2299
- `- **Interaction**: Dense information layouts, keyboard shortcuts shown, right-click context menus, drag handles`,
2300
- `- **Navigation**: Left sidebar tree, top tabs, ribbon toolbar, split panes`
2425
+ `- **Window chrome**: include realistic traffic-light controls or Windows controls, an integrated title bar, sidebar/main split, optional inspector pane.`,
2426
+ `- **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.`,
2427
+ `- **Aesthetic**: macOS-translucent / vibrancy, Linear ink-on-cream, Things 3 calm minimal, Arc playful, Raycast utilitarian, dark IDE, brand-led pro tool, "paper" aesthetic with textured backgrounds.`,
2428
+ `- **Density**: from spacious / agenda-style to high-density data tables \u2014 push range across variants.`,
2429
+ `- **Interaction**: keyboard shortcut hints visible (\`\u2318K\` style), right-click menus rendered open in some variants, drag handles, resize affordances, persistent status bar, "peek" previews on hover.`
2301
2430
  ],
2302
2431
  rules: [
2303
- `- Design for 1280px+ wide layouts. Use the full browser viewport width.`,
2304
- `- Each file must be a complete, functional HTML page \u2014 pure HTML/CSS/JS, no external libraries (Tailwind CDN is acceptable).`
2432
+ `- Render a designed app window centered on a desktop-style background (subtle wallpaper / gradient / mesh). Window should feel like a screenshot, not a full-bleed page.`,
2433
+ `- Each file is a complete self-contained HTML page. Tailwind CDN + Google Fonts via \`<link>\` allowed.`
2305
2434
  ]
2306
2435
  },
2307
2436
  logo: {
2308
- role: "You are a graphic designer specializing in logo and brand identity. Your job is to generate high-quality, visually distinct logo variants based on the user's brand brief.",
2437
+ role: "You are a senior brand designer in the lineage of Pentagram, MetaLab, and Wolff Olins. Your logo work has been featured in Brand New and shows up on Awwwards-winning sites.",
2309
2438
  guidelines: [
2310
- `- **Style**: Wordmark, lettermark, icon + wordmark, abstract symbol, emblem/badge`,
2311
- `- **Visual language**: Minimal/geometric, organic/hand-drawn, bold/impactful, elegant/luxury, playful/friendly`,
2312
- `- **Color scheme**: Monochrome, duotone, full-color (max 3 colors), gradient`,
2313
- `- **Typography**: Serif, sans-serif, display/decorative, script`,
2314
- `- **Symbolism**: Use shapes, negative space, and iconography that reflect the brand concept`
2439
+ `- **Logo type**: wordmark, lettermark, monogram, icon + wordmark, abstract symbol, emblem/badge, dynamic mark.`,
2440
+ `- **Visual language**: geometric minimal, organic / hand-drawn, bold expressive, classical / serifed elegant, retro modernist, futuristic / tech, playful character.`,
2441
+ `- **Color**: pure monochrome, duotone, restrained 3-color palette, gradient (used sparingly), warm-paper + ink.`,
2442
+ `- **Typography**: pair the wordmark with a deliberate type choice \u2014 modernist sans, geometric grotesque, transitional serif, slab, display script.`,
2443
+ `- **Symbolism**: use negative space, geometric primitives, ligatures, or conceptual marks that evoke the brand. No clip-art clich\xE9s.`
2315
2444
  ],
2316
2445
  rules: [
2317
- `- Each file is an HTML page that displays the logo centered on a white background with a dark mode toggle.`,
2318
- `- The logo itself MUST be created as inline SVG \u2014 no raster images, no external assets.`,
2319
- `- Show the logo at three sizes: large (400px wide), medium (200px), small (80px) stacked vertically.`,
2320
- `- Include a dark background preview section below the logo to test contrast.`,
2321
- `- The SVG must be clean and production-ready \u2014 no lorem ipsum, placeholder paths, or broken shapes.`
2446
+ `- Each file is a single HTML page that presents the logo as a brand-system mini-showcase. Render it on a designed canvas, not a plain white box.`,
2447
+ `- The mark itself MUST be inline SVG \u2014 no raster images, no broken \`<img>\` paths.`,
2448
+ `- Show: (1) the primary mark large (~480px wide), (2) a wordmark / monogram lockup, (3) the mark at small sizes (200px and 64px), (4) a dark-background preview section showing the mark inverted, (5) a small color swatch row showing the palette.`,
2449
+ `- Include a tiny "About this direction" caption (1-2 sentences) explaining the design rationale.`,
2450
+ `- The SVG must be clean and production-ready \u2014 no placeholder paths, no broken shapes.`
2322
2451
  ]
2323
2452
  }
2324
2453
  };
@@ -2329,9 +2458,20 @@ function buildPrototypePrompt(proto, repoDir) {
2329
2458
  desktop_app: "Desktop App",
2330
2459
  logo: "Logo"
2331
2460
  };
2461
+ 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.`;
2462
+ const creativeDirection = [
2463
+ `## Creative Direction & Polish`,
2464
+ `- **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.`,
2465
+ `- **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.`,
2466
+ `- **Materiality**: Experiment with textures. Use a very subtle SVG grain filter on backgrounds. Use \`backdrop-filter: blur()\` for overlays. Use multiple layered box-shadows for a "diffused" light effect.`,
2467
+ `- **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.`,
2468
+ `- **Negative Space**: Do not be afraid of "wasting" space. Generous margins and padding often signal luxury and quality.`
2469
+ ];
2332
2470
  return [
2333
2471
  `${config.role}`,
2334
2472
  ``,
2473
+ agentNote,
2474
+ ``,
2335
2475
  `Working directory: ${repoDir}`,
2336
2476
  ``,
2337
2477
  `## Prototype Request`,
@@ -2344,43 +2484,65 @@ function buildPrototypePrompt(proto, repoDir) {
2344
2484
  ``,
2345
2485
  `## Instructions`,
2346
2486
  ``,
2347
- `You MUST generate exactly ${proto.variantCount} separate HTML files. Follow the steps below IN ORDER. Do NOT skip any variant. Do NOT stop early. Complete every single variant before exiting.`,
2487
+ `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.`,
2348
2488
  ``,
2349
- `Each file must be completely self-contained (inline all CSS and JS \u2014 no external dependencies). Tailwind CDN is acceptable.`,
2489
+ ...sharedQualityBar,
2490
+ ``,
2491
+ ...creativeDirection,
2492
+ ``,
2493
+ `## Design Variance: ${varianceInfo.label} (${variance}/100)`,
2494
+ ``,
2495
+ varianceInfo.summary,
2350
2496
  ``,
2351
- `## Variation Guidelines`,
2497
+ `Axes to vary across variants:`,
2498
+ ...varianceInfo.axes,
2499
+ ``,
2500
+ `## Variation Levers (push these aggressively across variants, scaled by the variance level above)`,
2352
2501
  ``,
2353
- `When generating multiple variants, vary these aspects:`,
2354
2502
  ...config.guidelines,
2355
2503
  ``,
2356
- `Variants should demonstrate different interpretations, not just color swaps. Vary from very similar to wildly different approaches.`,
2504
+ `## Aesthetic Guardrails`,
2505
+ ``,
2506
+ `- 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.`,
2507
+ `- 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.`,
2508
+ `- Choose aesthetics that fit the product and audience described in the user's prompt, not a generic developer-tool look.`,
2357
2509
  ``,
2358
2510
  ...variantSteps,
2359
2511
  `### Final verification`,
2360
- `After generating ALL ${proto.variantCount} variants, list the files in ${repoDir} and confirm that all ${proto.variantCount} files exist: ${variantList.join(", ")}. If any are missing, go back and generate the missing ones.`,
2512
+ `After generating ALL ${variantsToProduce} variants, list the files in ${repoDir} and confirm: ${variantList.join(", ")}. If any are missing or feel rushed, go back and redo them.`,
2361
2513
  ``,
2362
2514
  `IMPORTANT RULES:`,
2363
- `- You MUST produce exactly ${proto.variantCount} files: ${variantList.join(", ")}`,
2515
+ `- You MUST produce exactly these files: ${variantList.join(", ")}`,
2364
2516
  `- Generate them ONE AT A TIME \u2014 design each variant, write the file, then move to the next.`,
2365
- `- Each variant must look visually DISTINCT from the others.`,
2517
+ `- Respect the Design Variance level above \u2014 do not force drastic differences if the user asked for similar variants, and do not produce near-duplicates if they asked for drastic variance.`,
2518
+ `- NO Lorem ipsum, NO "Feature One/Two/Three", NO broken \`<img>\` paths, NO empty-looking UIs.`,
2366
2519
  ...config.rules,
2367
2520
  `- Do NOT upload or POST the files anywhere. The watch handler will upload them automatically after you exit.`,
2368
- `- Do NOT exit until ALL ${proto.variantCount} files have been written and verified.`
2521
+ `- Do NOT exit until ALL ${variantsToProduce} files have been written and verified.`
2369
2522
  ].join("\n");
2370
2523
  }
2371
- function buildRefinementPrompt(proto, parentFiles, repoDir) {
2524
+ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
2525
+ const variantsToProduce = options.variantsToProduce ?? proto.variantCount;
2526
+ const startIndex = options.variantStartIndex ?? 1;
2527
+ const variance = typeof proto.designVariance === "number" ? proto.designVariance : 70;
2528
+ const varianceInfo = describeVariance(variance);
2372
2529
  const variantSteps = [];
2373
- for (let i = 1; i <= proto.variantCount; i++) {
2374
- const filename = `prototype-${i}.html`;
2530
+ for (let i = 0; i < variantsToProduce; i++) {
2531
+ const idx = startIndex + i;
2532
+ const filename = `prototype-${idx}.html`;
2375
2533
  variantSteps.push(
2376
- `### Variant ${i} of ${proto.variantCount}: ${filename}`,
2377
- `1. Redesign variant ${i} based on the feedback below, while maintaining the core concept.`,
2378
- `2. Write the complete self-contained HTML to \`${repoDir}/${filename}\` using the Write tool.`,
2379
- `3. Verify the file was created by reading the first few lines of \`${repoDir}/${filename}\`.`,
2534
+ `### Variant ${idx}: ${filename}`,
2535
+ `1. Redesign variant ${idx} based on the feedback below, while keeping the core concept that worked.`,
2536
+ `2. Apply the same craft & polish bar as the original generation \u2014 portfolio quality, real microcopy, intentional typography, no Lorem ipsum, no broken images.`,
2537
+ `3. Write the complete self-contained HTML to \`${repoDir}/${filename}\` using the Write tool.`,
2538
+ `4. Verify the file was created by reading the first few lines of \`${repoDir}/${filename}\`.`,
2380
2539
  ``
2381
2540
  );
2382
2541
  }
2383
- const variantList = Array.from({ length: proto.variantCount }, (_, i) => `prototype-${i + 1}.html`);
2542
+ const variantList = Array.from(
2543
+ { length: variantsToProduce },
2544
+ (_, i) => `prototype-${startIndex + i}.html`
2545
+ );
2384
2546
  const existingVariantLines = [];
2385
2547
  for (let i = 0; i < parentFiles.length; i++) {
2386
2548
  const f = parentFiles[i];
@@ -2434,7 +2596,16 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
2434
2596
  ``,
2435
2597
  `You MUST generate exactly ${proto.variantCount} REFINED HTML files that incorporate the user's feedback above. Follow the steps below IN ORDER.`,
2436
2598
  ``,
2437
- `Study the previous variants carefully, then apply the user's feedback to improve them. Keep what works, change what the user asked to change. Each variant should still be visually distinct from the others.`,
2599
+ `Study the previous variants carefully, then apply the user's feedback to improve them. Keep what works, change what the user asked to change.`,
2600
+ ``,
2601
+ `## Design Variance: ${varianceInfo.label} (${variance}/100)`,
2602
+ ``,
2603
+ varianceInfo.summary,
2604
+ ``,
2605
+ `## Aesthetic Guardrails`,
2606
+ ``,
2607
+ `- Do NOT default to a terminal / hacker / CRT / monospace-green-on-black aesthetic unless the user's prompt explicitly asks for it.`,
2608
+ `- Do NOT force any single "style label" onto the variants unless the user asked for it.`,
2438
2609
  ``,
2439
2610
  `Each file must be completely self-contained (inline all CSS and JS \u2014 no external dependencies). Tailwind CDN is acceptable.`,
2440
2611
  ``,
@@ -2445,7 +2616,7 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
2445
2616
  `IMPORTANT RULES:`,
2446
2617
  `- You MUST produce exactly ${proto.variantCount} files: ${variantList.join(", ")}`,
2447
2618
  `- Generate them ONE AT A TIME \u2014 design each variant, write the file, then move to the next.`,
2448
- `- Each variant must look visually DISTINCT from the others.`,
2619
+ `- Respect the Design Variance level above.`,
2449
2620
  `- Each file must be a complete, functional page \u2014 pure HTML/CSS/JS, no external libraries (Tailwind CDN is acceptable).`,
2450
2621
  `- Do NOT upload or POST the files anywhere. The watch handler will upload them automatically after you exit.`,
2451
2622
  `- Do NOT exit until ALL ${proto.variantCount} files have been written and verified.`
@@ -3214,114 +3385,285 @@ var watchCommand = new Command9("watch").description(
3214
3385
  } catch {
3215
3386
  }
3216
3387
  }
3217
- let prompt2;
3388
+ const validAgents = ["claude", "codex", "gemini"];
3389
+ const requested = Array.isArray(proto.selectedAgents) ? proto.selectedAgents.filter(
3390
+ (a) => validAgents.includes(a)
3391
+ ) : [];
3392
+ const dedupedRequested = Array.from(new Set(requested));
3393
+ const key = `proto-${proto.id}`;
3394
+ let parentFiles = [];
3218
3395
  if (proto.parentId && proto.refinementFeedback) {
3219
- let parentFiles = [];
3220
3396
  try {
3221
3397
  const parent = await api.get(`/api/prototypes/${proto.parentId}`);
3222
3398
  parentFiles = parent.files ?? [];
3223
3399
  } catch (err) {
3224
3400
  logError(prefix, `Failed to fetch parent prototype: ${err.message}`);
3225
3401
  }
3226
- prompt2 = buildRefinementPrompt(proto, parentFiles, repoDir);
3227
- } else {
3228
- prompt2 = buildPrototypePrompt(proto, repoDir);
3229
3402
  }
3230
- const key = `proto-${proto.id}`;
3231
- const attemptOrder = await resolveAgentChain(agent);
3232
- if (attemptOrder.length === 0) {
3233
- logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
3403
+ const buildPromptForSlice = (startIndex, variantsToProduce, agentLabel, sliceDir) => {
3404
+ if (proto.parentId && proto.refinementFeedback) {
3405
+ return buildRefinementPrompt(proto, parentFiles, sliceDir, {
3406
+ variantStartIndex: startIndex,
3407
+ variantsToProduce,
3408
+ agentLabel
3409
+ });
3410
+ }
3411
+ return buildPrototypePrompt(proto, sliceDir, {
3412
+ variantStartIndex: startIndex,
3413
+ variantsToProduce,
3414
+ agentLabel
3415
+ });
3416
+ };
3417
+ if (dedupedRequested.length <= 1) {
3418
+ const preferred = dedupedRequested[0] ?? agent;
3419
+ const attemptOrder = dedupedRequested.length === 1 ? [preferred] : await resolveAgentChain(preferred);
3420
+ if (attemptOrder.length === 0) {
3421
+ logError(prefix, `No available agents found for ${preferred}`);
3422
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" }).catch(() => {
3423
+ });
3424
+ queued.delete(key);
3425
+ return;
3426
+ }
3427
+ const prompt2 = buildPromptForSlice(1, proto.variantCount, void 0, repoDir);
3428
+ const activeEntry2 = {
3429
+ process: void 0,
3430
+ title: proto.title,
3431
+ repoDir,
3432
+ startedAt: Date.now(),
3433
+ lastActivityAt: Date.now()
3434
+ };
3435
+ let attemptIndex = 0;
3436
+ const launchAttempt = async (attemptAgent) => {
3437
+ let spawnFailureReason = null;
3438
+ const child = spawnAgent(
3439
+ attemptAgent,
3440
+ repoDir,
3441
+ prompt2,
3442
+ prefix,
3443
+ void 0,
3444
+ void 0,
3445
+ proto.title,
3446
+ false,
3447
+ (err) => {
3448
+ spawnFailureReason = err.message;
3449
+ }
3450
+ );
3451
+ activeEntry2.process = child;
3452
+ activeEntry2.currentAgent = attemptAgent;
3453
+ active.set(key, activeEntry2);
3454
+ child.on("exit", async (code) => {
3455
+ if (active.get(key)?.process === child) {
3456
+ active.delete(key);
3457
+ }
3458
+ const failedAttempt = code !== 0 || spawnFailureReason !== null;
3459
+ if (failedAttempt && !activeEntry2.terminatedForError) {
3460
+ const nextAgent = attemptOrder[attemptIndex + 1];
3461
+ if (nextAgent) {
3462
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
3463
+ logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying prototype generation with ${nextAgent}`);
3464
+ attemptIndex += 1;
3465
+ await launchAttempt(nextAgent);
3466
+ return;
3467
+ }
3468
+ }
3469
+ finishing.add(key);
3470
+ try {
3471
+ if (code === 0) {
3472
+ try {
3473
+ const protoPattern = /^prototype-\d+\.html$/;
3474
+ const found = readdirSync(repoDir).filter((f) => protoPattern.test(f)).sort();
3475
+ const files = found.map((f) => ({
3476
+ name: f,
3477
+ content: readFileSync5(resolve2(repoDir, f), "utf-8")
3478
+ }));
3479
+ if (files.length === 0) {
3480
+ logError(prefix, `No prototype HTML files found in ${repoDir}`);
3481
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
3482
+ return;
3483
+ }
3484
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "completed", files });
3485
+ logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${files.length} file(s)`);
3486
+ for (const file of files) {
3487
+ try {
3488
+ unlinkSync(resolve2(repoDir, file.name));
3489
+ } catch {
3490
+ }
3491
+ }
3492
+ } catch (err) {
3493
+ logError(prefix, `Failed to upload prototype: ${err.message}`);
3494
+ try {
3495
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
3496
+ } catch {
3497
+ }
3498
+ }
3499
+ } else {
3500
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
3501
+ logError(prefix, `"${paint("bold", proto.title)}" prototype failed via ${attemptAgent} (${failureDetail})`);
3502
+ try {
3503
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
3504
+ } catch {
3505
+ }
3506
+ }
3507
+ } finally {
3508
+ queued.delete(key);
3509
+ finishing.delete(key);
3510
+ }
3511
+ });
3512
+ };
3513
+ await launchAttempt(attemptOrder[attemptIndex]);
3514
+ return;
3515
+ }
3516
+ const totalVariants = proto.variantCount;
3517
+ const slices = [];
3518
+ const baseShare = Math.floor(totalVariants / dedupedRequested.length);
3519
+ const remainder = totalVariants % dedupedRequested.length;
3520
+ let cursor = 1;
3521
+ for (let i = 0; i < dedupedRequested.length; i++) {
3522
+ const a = dedupedRequested[i];
3523
+ const count = baseShare + (i < remainder ? 1 : 0);
3524
+ if (count <= 0) continue;
3525
+ const dir = resolve2(repoDir, `.mr-proto-${a}`);
3526
+ try {
3527
+ mkdirSync3(dir, { recursive: true });
3528
+ } catch {
3529
+ }
3530
+ for (const f of readdirSync(dir).filter((f2) => stalePattern.test(f2))) {
3531
+ try {
3532
+ unlinkSync(resolve2(dir, f));
3533
+ } catch {
3534
+ }
3535
+ }
3536
+ slices.push({ agentLabel: a, startIndex: cursor, count, dir });
3537
+ cursor += count;
3538
+ }
3539
+ if (slices.length === 0) {
3540
+ logError(prefix, `Could not assign any variants to selected agents`);
3234
3541
  await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" }).catch(() => {
3235
3542
  });
3236
3543
  queued.delete(key);
3237
3544
  return;
3238
3545
  }
3546
+ logDispatch(
3547
+ prefix,
3548
+ `multi-agent dispatch: ${slices.map((s) => `${s.agentLabel}\xD7${s.count}`).join(", ")}`
3549
+ );
3239
3550
  const activeEntry = {
3240
3551
  process: void 0,
3241
3552
  title: proto.title,
3242
3553
  repoDir,
3243
3554
  startedAt: Date.now(),
3244
- lastActivityAt: Date.now()
3555
+ lastActivityAt: Date.now(),
3556
+ outputBytes: 0
3245
3557
  };
3246
- let attemptIndex = 0;
3247
- const launchAttempt = async (attemptAgent) => {
3248
- let spawnFailureReason = null;
3249
- const child = spawnAgent(
3250
- attemptAgent,
3251
- repoDir,
3252
- prompt2,
3253
- prefix,
3254
- void 0,
3255
- void 0,
3256
- proto.title,
3257
- false,
3258
- (err) => {
3259
- spawnFailureReason = err.message;
3260
- }
3261
- );
3262
- activeEntry.process = child;
3263
- activeEntry.currentAgent = attemptAgent;
3264
- active.set(key, activeEntry);
3265
- child.on("exit", async (code) => {
3266
- if (active.get(key)?.process === child) {
3267
- active.delete(key);
3558
+ active.set(key, activeEntry);
3559
+ const sliceResults = await Promise.all(
3560
+ slices.map((slice) => {
3561
+ return new Promise((res) => {
3562
+ const slicePrompt = buildPromptForSlice(slice.startIndex, slice.count, slice.agentLabel, slice.dir);
3563
+ let spawnFailureReason = null;
3564
+ const child = spawnAgent(
3565
+ slice.agentLabel,
3566
+ slice.dir,
3567
+ slicePrompt,
3568
+ prefix,
3569
+ void 0,
3570
+ void 0,
3571
+ `${proto.title} [${slice.agentLabel}]`,
3572
+ false,
3573
+ (err) => {
3574
+ spawnFailureReason = err.message;
3575
+ }
3576
+ );
3577
+ activeEntry.process = child;
3578
+ activeEntry.currentAgent = slice.agentLabel;
3579
+ child.on("exit", (code) => {
3580
+ const ok = code === 0 && spawnFailureReason === null;
3581
+ if (!ok) {
3582
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
3583
+ logWarn(prefix, `${slice.agentLabel} slice failed (${failureDetail})`);
3584
+ }
3585
+ res({ agent: slice.agentLabel, ok, reason: spawnFailureReason });
3586
+ });
3587
+ });
3588
+ })
3589
+ );
3590
+ if (active.get(key) === activeEntry) {
3591
+ active.delete(key);
3592
+ }
3593
+ finishing.add(key);
3594
+ try {
3595
+ const protoPattern = /^prototype-\d+\.html$/;
3596
+ const collected = [];
3597
+ for (const slice of slices) {
3598
+ let found = [];
3599
+ try {
3600
+ found = readdirSync(slice.dir).filter((f) => protoPattern.test(f)).sort();
3601
+ } catch {
3268
3602
  }
3269
- const failedAttempt = code !== 0 || spawnFailureReason !== null;
3270
- if (failedAttempt && !activeEntry.terminatedForError) {
3271
- const nextAgent = attemptOrder[attemptIndex + 1];
3272
- if (nextAgent) {
3273
- const failureDetail = spawnFailureReason ?? `exit code ${code}`;
3274
- logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying prototype generation with ${nextAgent}`);
3275
- attemptIndex += 1;
3276
- await launchAttempt(nextAgent);
3277
- return;
3603
+ for (const f of found) {
3604
+ try {
3605
+ const content = readFileSync5(resolve2(slice.dir, f), "utf-8");
3606
+ collected.push({ name: f, content });
3607
+ } catch (err) {
3608
+ logError(prefix, `Failed reading ${f} from ${slice.dir}: ${err.message}`);
3278
3609
  }
3279
3610
  }
3280
- finishing.add(key);
3611
+ }
3612
+ collected.sort((a, b) => {
3613
+ const na = parseInt(a.name.match(/\d+/)?.[0] ?? "0", 10);
3614
+ const nb = parseInt(b.name.match(/\d+/)?.[0] ?? "0", 10);
3615
+ return na - nb;
3616
+ });
3617
+ const seen = /* @__PURE__ */ new Set();
3618
+ const finalFiles = [];
3619
+ let renumberCursor = 1;
3620
+ for (const f of collected) {
3621
+ let name = f.name;
3622
+ if (seen.has(name)) {
3623
+ while (seen.has(`prototype-${renumberCursor}.html`)) renumberCursor++;
3624
+ name = `prototype-${renumberCursor}.html`;
3625
+ }
3626
+ seen.add(name);
3627
+ finalFiles.push({ name, content: f.content });
3628
+ }
3629
+ const allOk = sliceResults.every((r) => r.ok);
3630
+ if (finalFiles.length === 0) {
3631
+ logError(prefix, `No prototype HTML files produced by any agent`);
3632
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
3633
+ } else {
3634
+ await api.patch(`/api/prototypes/${proto.id}`, {
3635
+ status: "completed",
3636
+ files: finalFiles
3637
+ });
3638
+ if (allOk) {
3639
+ logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${finalFiles.length} file(s) from ${slices.length} agent(s)`);
3640
+ } else {
3641
+ const failed2 = sliceResults.filter((r) => !r.ok).map((r) => r.agent).join(", ");
3642
+ logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${finalFiles.length} file(s) (partial \u2014 ${failed2} failed)`);
3643
+ }
3644
+ }
3645
+ for (const slice of slices) {
3281
3646
  try {
3282
- if (code === 0) {
3647
+ for (const f of readdirSync(slice.dir)) {
3283
3648
  try {
3284
- const protoPattern = /^prototype-\d+\.html$/;
3285
- const found = readdirSync(repoDir).filter((f) => protoPattern.test(f)).sort();
3286
- const files = found.map((f) => ({
3287
- name: f,
3288
- content: readFileSync5(resolve2(repoDir, f), "utf-8")
3289
- }));
3290
- if (files.length === 0) {
3291
- logError(prefix, `No prototype HTML files found in ${repoDir}`);
3292
- await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
3293
- return;
3294
- }
3295
- await api.patch(`/api/prototypes/${proto.id}`, { status: "completed", files });
3296
- logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${files.length} file(s)`);
3297
- for (const file of files) {
3298
- try {
3299
- unlinkSync(resolve2(repoDir, file.name));
3300
- } catch {
3301
- }
3302
- }
3303
- } catch (err) {
3304
- logError(prefix, `Failed to upload prototype: ${err.message}`);
3305
- try {
3306
- await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
3307
- } catch {
3308
- }
3309
- }
3310
- } else {
3311
- const failureDetail = spawnFailureReason ?? `exit code ${code}`;
3312
- logError(prefix, `"${paint("bold", proto.title)}" prototype failed via ${attemptAgent} (${failureDetail})`);
3313
- try {
3314
- await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
3649
+ unlinkSync(resolve2(slice.dir, f));
3315
3650
  } catch {
3316
3651
  }
3317
3652
  }
3318
- } finally {
3319
- queued.delete(key);
3320
- finishing.delete(key);
3653
+ rmdirSync(slice.dir);
3654
+ } catch {
3321
3655
  }
3322
- });
3323
- };
3324
- await launchAttempt(attemptOrder[attemptIndex]);
3656
+ }
3657
+ } catch (err) {
3658
+ logError(prefix, `Failed to upload multi-agent prototype: ${err.message}`);
3659
+ try {
3660
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
3661
+ } catch {
3662
+ }
3663
+ } finally {
3664
+ queued.delete(key);
3665
+ finishing.delete(key);
3666
+ }
3325
3667
  }
3326
3668
  async function dispatchRepoCreation(project, workDir) {
3327
3669
  const sid = shortId(project.id);
@@ -3458,7 +3800,10 @@ var watchCommand = new Command9("watch").description(
3458
3800
  }
3459
3801
  function dispatchCodeReview(review, prefix, key) {
3460
3802
  logDispatch(prefix, `Running code review on branch ${paint("cyan", review.branch)}`);
3461
- const reviewProc = spawn4(process.execPath, [process.argv[1], "review", "--project", review.projectId, "--report", review.id, "--branch", review.branch, "--base", review.baseBranch], {
3803
+ const reviewArgs = [process.argv[1], "review", "--project", review.projectId, "--report", review.id, "--branch", review.branch, "--base", review.baseBranch];
3804
+ if (review.branchUrl) reviewArgs.push("--pr-url", review.branchUrl);
3805
+ if (review.prNumber != null) reviewArgs.push("--pr-number", String(review.prNumber));
3806
+ const reviewProc = spawn4(process.execPath, reviewArgs, {
3462
3807
  stdio: ["ignore", "pipe", "pipe"],
3463
3808
  cwd: rootDir
3464
3809
  });
@@ -4207,8 +4552,22 @@ var subtaskCompleteCommand = new Command13("subtask-complete").description("Mark
4207
4552
  console.log(`\u2713 Subtask completed: ${subtask.title}`);
4208
4553
  });
4209
4554
 
4210
- // cli/commands/prototype.ts
4555
+ // cli/commands/subtask-add.ts
4211
4556
  import { Command as Command14 } from "commander";
4557
+ var subtaskAddCommand = new Command14("subtask-add").description("Add a subtask to a task (used by the agent to plan work as it starts)").argument("<task-id>", "Parent task ID").argument("<title...>", "Subtask title").action(async (taskId, titleParts) => {
4558
+ const title = titleParts.join(" ").trim();
4559
+ if (!title) {
4560
+ console.error("Subtask title is required.");
4561
+ process.exit(1);
4562
+ }
4563
+ const subtask = await api.post(`/api/tasks/${taskId}/subtasks`, {
4564
+ title
4565
+ });
4566
+ console.log(`\u2713 Subtask added (${subtask.id}): ${subtask.title}`);
4567
+ });
4568
+
4569
+ // cli/commands/prototype.ts
4570
+ import { Command as Command15 } from "commander";
4212
4571
  var c4 = {
4213
4572
  reset: "\x1B[0m",
4214
4573
  bold: "\x1B[1m",
@@ -4237,8 +4596,8 @@ function statusBadge(status) {
4237
4596
  return paint4("gray", status);
4238
4597
  }
4239
4598
  }
4240
- var prototypeCommand = new Command14("prototype").description("Manage prototypes").addCommand(
4241
- new Command14("list").description("List prototypes for the linked project").option("--all", "Show prototypes for all projects").action(async (opts) => {
4599
+ var prototypeCommand = new Command15("prototype").description("Manage prototypes").addCommand(
4600
+ new Command15("list").description("List prototypes for the linked project").option("--all", "Show prototypes for all projects").action(async (opts) => {
4242
4601
  const params = new URLSearchParams();
4243
4602
  if (!opts.all) {
4244
4603
  const projectId = getLinkedProjectId();
@@ -4271,7 +4630,7 @@ var prototypeCommand = new Command14("prototype").description("Manage prototypes
4271
4630
  }
4272
4631
  })
4273
4632
  ).addCommand(
4274
- new Command14("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) => {
4633
+ 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) => {
4275
4634
  const projectId = opts.project ?? getLinkedProjectId();
4276
4635
  const variantCount = Math.max(1, Math.min(50, parseInt(opts.variants, 10) || 5));
4277
4636
  const validTypes = ["web_app", "mobile_app", "desktop_app", "logo"];
@@ -4300,7 +4659,7 @@ var prototypeCommand = new Command14("prototype").description("Manage prototypes
4300
4659
  console.log();
4301
4660
  })
4302
4661
  ).addCommand(
4303
- new Command14("start").description("Start prototype generation (sets status to in_progress)").argument("<id>", "Prototype ID").action(async (id) => {
4662
+ new Command15("start").description("Start prototype generation (sets status to in_progress)").argument("<id>", "Prototype ID").action(async (id) => {
4304
4663
  const prototype = await api.patch(`/api/prototypes/${id}`, {
4305
4664
  status: "in_progress"
4306
4665
  });
@@ -4310,7 +4669,7 @@ var prototypeCommand = new Command14("prototype").description("Manage prototypes
4310
4669
  console.log();
4311
4670
  })
4312
4671
  ).addCommand(
4313
- new Command14("retry").description("Retry a failed prototype").argument("<id>", "Prototype ID").action(async (id) => {
4672
+ new Command15("retry").description("Retry a failed prototype").argument("<id>", "Prototype ID").action(async (id) => {
4314
4673
  const prototype = await api.patch(`/api/prototypes/${id}`, {
4315
4674
  status: "in_progress",
4316
4675
  files: null
@@ -4322,7 +4681,7 @@ var prototypeCommand = new Command14("prototype").description("Manage prototypes
4322
4681
  );
4323
4682
 
4324
4683
  // cli/commands/setup.ts
4325
- import { Command as Command15 } from "commander";
4684
+ import { Command as Command16 } from "commander";
4326
4685
  import { exec as exec2 } from "child_process";
4327
4686
  var c5 = {
4328
4687
  reset: "\x1B[0m",
@@ -4625,7 +4984,7 @@ async function autoFix(checks, agent) {
4625
4984
  console.log("");
4626
4985
  }
4627
4986
  }
4628
- var setupCommand = new Command15("setup").description("Check that all dependencies for mr watch are installed and configured").option("--fix", "Attempt to auto-fix issues where possible", false).option("--agent <agent>", "AI agent to check: claude, codex, or gemini (default: claude)", "claude").action(async (opts) => {
4987
+ var setupCommand = new Command16("setup").description("Check that all dependencies for mr watch are installed and configured").option("--fix", "Attempt to auto-fix issues where possible", false).option("--agent <agent>", "AI agent to check: claude, codex, or gemini (default: claude)", "claude").action(async (opts) => {
4629
4988
  const agent = opts.agent === "codex" ? "codex" : opts.agent === "gemini" ? "gemini" : "claude";
4630
4989
  const banner = [
4631
4990
  ``,
@@ -4675,8 +5034,8 @@ var setupCommand = new Command15("setup").description("Check that all dependenci
4675
5034
  });
4676
5035
 
4677
5036
  // cli/commands/update.ts
4678
- import { Command as Command16 } from "commander";
4679
- var updateCommand = new Command16("update").description("Post a status update to a task, or attach a resource").argument("<task-id>", "Task ID").argument("[message-or-title]", "Status update message, or resource title when using --resource").argument("[content]", "Resource content (only used with --resource)").option("--source <source>", "Update source: agent, system, or user", "agent").option("--resource <type>", "Create a task resource (e.g. test-plan, note, plan, research)").action(async (taskId, messageOrTitle, content, opts) => {
5037
+ import { Command as Command17 } from "commander";
5038
+ var updateCommand = new Command17("update").description("Post a status update to a task, or attach a resource").argument("<task-id>", "Task ID").argument("[message-or-title]", "Status update message, or resource title when using --resource").argument("[content]", "Resource content (only used with --resource)").option("--source <source>", "Update source: agent, system, or user", "agent").option("--resource <type>", "Create a task resource (e.g. test-plan, note, plan, research)").action(async (taskId, messageOrTitle, content, opts) => {
4680
5039
  if (opts.resource) {
4681
5040
  if (!messageOrTitle || !content) {
4682
5041
  console.error(`Usage: mr update <task-id> --resource <type> "<title>" '<content>'`);
@@ -4702,11 +5061,11 @@ var updateCommand = new Command16("update").description("Post a status update to
4702
5061
  });
4703
5062
 
4704
5063
  // cli/commands/screenshot.ts
4705
- import { Command as Command17 } from "commander";
5064
+ import { Command as Command18 } from "commander";
4706
5065
  import { readFileSync as readFileSync6, existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
4707
5066
  import { join as join7 } from "path";
4708
5067
  import { tmpdir } from "os";
4709
- var screenshotCommand = new Command17("screenshot").description(
5068
+ var screenshotCommand = new Command18("screenshot").description(
4710
5069
  "Take or attach a screenshot to a task update (agents use this to show their work)"
4711
5070
  ).argument("<task-id>", "Task ID").argument("[file]", "Path to an image file (if omitted, uses headless browser to screenshot the app)").option("-m, --message <message>", "Optional message to include with the screenshot").option("-u, --url <url>", "Custom URL to screenshot (defaults to the task's project page)").action(async (taskId, file, opts) => {
4712
5071
  let filePath = file;
@@ -4797,7 +5156,7 @@ var screenshotCommand = new Command17("screenshot").description(
4797
5156
  });
4798
5157
 
4799
5158
  // cli/commands/resume.ts
4800
- import { Command as Command18 } from "commander";
5159
+ import { Command as Command19 } from "commander";
4801
5160
  import { spawn as spawn5 } from "child_process";
4802
5161
  import { resolve as resolve3 } from "path";
4803
5162
  var c6 = {
@@ -4814,7 +5173,7 @@ var c6 = {
4814
5173
  function paint6(color, text) {
4815
5174
  return `${c6[color]}${text}${c6.reset}`;
4816
5175
  }
4817
- var resumeCommand = new Command18("resume").description("Resume an interactive Claude session for a task (non-headless)").argument("<task-id>", "Task ID whose Claude session to resume").option("--dir <directory>", "Override the working directory for the session").action(async (taskId, opts) => {
5176
+ var resumeCommand = new Command19("resume").description("Resume an interactive Claude session for a task (non-headless)").argument("<task-id>", "Task ID whose Claude session to resume").option("--dir <directory>", "Override the working directory for the session").action(async (taskId, opts) => {
4818
5177
  const task = await api.get(`/api/tasks/${taskId}`);
4819
5178
  if (!task.claudeSessionId) {
4820
5179
  console.error(
@@ -4883,7 +5242,7 @@ var resumeCommand = new Command18("resume").description("Resume an interactive C
4883
5242
  });
4884
5243
 
4885
5244
  // cli/commands/browse.ts
4886
- import { Command as Command19 } from "commander";
5245
+ import { Command as Command20 } from "commander";
4887
5246
  import { execSync as execSync4, spawn as spawn6 } from "child_process";
4888
5247
  import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
4889
5248
  import { createHash } from "crypto";
@@ -4998,7 +5357,7 @@ async function ensureDevServer(options = {}) {
4998
5357
  }
4999
5358
  throw new Error(`Dev server failed to start within 60s. Command: ${devCmd} in ${projectCwd}`);
5000
5359
  }
5001
- var browseCommand = new Command19("browse").description("Control a headless browser for QA and testing").argument("[command]", "Browse command (goto, click, fill, screenshot, etc.)").argument("[args...]", "Command arguments").option(
5360
+ var browseCommand = new Command20("browse").description("Control a headless browser for QA and testing").argument("[command]", "Browse command (goto, click, fill, screenshot, etc.)").argument("[args...]", "Command arguments").option(
5002
5361
  "--task-id <id>",
5003
5362
  "Attach output to a task update (for screenshot and recording-stop commands)"
5004
5363
  ).option("--dev", "Auto-start local dev server before browsing").option("--dev-cwd <path>", "Working directory for the dev server (defaults to mr-manager root)").option("--dev-cmd <command>", "Dev server command to run (auto-detected from package.json if omitted)").option("--dev-port-flag <flag>", "CLI flag name used to set port (e.g. --port). Omit to use PORT env var.").allowUnknownOption(true).action(
@@ -5141,10 +5500,10 @@ var browseCommand = new Command19("browse").description("Control a headless brow
5141
5500
  );
5142
5501
 
5143
5502
  // cli/commands/set-path.ts
5144
- import { Command as Command20 } from "commander";
5503
+ import { Command as Command21 } from "commander";
5145
5504
  import { resolve as resolve5 } from "path";
5146
5505
  import { existsSync as existsSync10 } from "fs";
5147
- var setPathCommand = new Command20("set-path").description("Set or update the local repo path for a project").argument("<project-id>", "Project ID").argument("<path>", "Absolute or relative path to the local repo").action(async (projectId, pathArg) => {
5506
+ var setPathCommand = new Command21("set-path").description("Set or update the local repo path for a project").argument("<project-id>", "Project ID").argument("<path>", "Absolute or relative path to the local repo").action(async (projectId, pathArg) => {
5148
5507
  const absolutePath = resolve5(pathArg);
5149
5508
  if (!existsSync10(absolutePath)) {
5150
5509
  console.error(`Error: Path does not exist: ${absolutePath}`);
@@ -5162,9 +5521,9 @@ var setPathCommand = new Command20("set-path").description("Set or update the lo
5162
5521
  });
5163
5522
 
5164
5523
  // cli/commands/test.ts
5165
- import { Command as Command21 } from "commander";
5524
+ import { Command as Command22 } from "commander";
5166
5525
  import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
5167
- var testCommand = new Command21("test").description("Run automated browser test for a task's MR/PR").argument("<task-id>", "Task ID to test").option("--plan <file>", "Path to a custom test plan JSON file").option("--no-recording", "Disable proof recording for this run").action(async (taskId, opts) => {
5526
+ var testCommand = new Command22("test").description("Run automated browser test for a task's MR/PR").argument("<task-id>", "Task ID to test").option("--plan <file>", "Path to a custom test plan JSON file").option("--no-recording", "Disable proof recording for this run").action(async (taskId, opts) => {
5168
5527
  const config = loadConfig();
5169
5528
  console.log("[test] Fetching task...");
5170
5529
  let task;
@@ -5326,7 +5685,7 @@ var testCommand = new Command21("test").description("Run automated browser test
5326
5685
  });
5327
5686
 
5328
5687
  // cli/commands/features.ts
5329
- import { Command as Command22 } from "commander";
5688
+ import { Command as Command23 } from "commander";
5330
5689
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync12 } from "fs";
5331
5690
  import { resolve as resolve6, sep as sep2 } from "path";
5332
5691
  var FEATURES_FILE3 = ".mr-features.md";
@@ -5360,7 +5719,7 @@ function readFeatures2() {
5360
5719
  if (!existsSync12(path)) return null;
5361
5720
  return readFileSync9(path, "utf-8");
5362
5721
  }
5363
- var featuresCommand = new Command22("features").description("View or update the project features & goals document (.mr-features.md)").option("--update <content>", "Replace the features document with the given content").option("--file <path>", "Read content from a file and use it to update the features document").option("--path", "Print the path to the features file").action(async (opts) => {
5722
+ var featuresCommand = new Command23("features").description("View or update the project features & goals document (.mr-features.md)").option("--update <content>", "Replace the features document with the given content").option("--file <path>", "Read content from a file and use it to update the features document").option("--path", "Print the path to the features file").action(async (opts) => {
5364
5723
  if (opts.path) {
5365
5724
  console.log(getFeaturesPath());
5366
5725
  return;
@@ -5388,11 +5747,11 @@ var featuresCommand = new Command22("features").description("View or update the
5388
5747
  });
5389
5748
 
5390
5749
  // cli/commands/no-mr.ts
5391
- import { Command as Command23 } from "commander";
5750
+ import { Command as Command24 } from "commander";
5392
5751
  import { writeFileSync as writeFileSync6 } from "fs";
5393
5752
  import { resolve as resolve7 } from "path";
5394
5753
  var NO_MR_FILE = ".mr-no-mr";
5395
- var noMrCommand = new Command23("no-mr").description("Signal that a task does not require a merge/pull request and describe what was done instead").argument("<task-id>", "Task ID").argument("<description>", "Description of what was done instead of creating an MR/PR").action(async (taskId, description) => {
5754
+ var noMrCommand = new Command24("no-mr").description("Signal that a task does not require a merge/pull request and describe what was done instead").argument("<task-id>", "Task ID").argument("<description>", "Description of what was done instead of creating an MR/PR").action(async (taskId, description) => {
5396
5755
  const filePath = resolve7(process.cwd(), NO_MR_FILE);
5397
5756
  writeFileSync6(filePath, description, "utf-8");
5398
5757
  await api.post(`/api/tasks/${taskId}/updates`, {
@@ -5404,7 +5763,7 @@ var noMrCommand = new Command23("no-mr").description("Signal that a task does no
5404
5763
  });
5405
5764
 
5406
5765
  // cli/commands/review.ts
5407
- import { Command as Command24 } from "commander";
5766
+ import { Command as Command25 } from "commander";
5408
5767
  import { spawn as spawn7, execSync as execSync5 } from "child_process";
5409
5768
  import { existsSync as existsSync13, statSync as statSync2 } from "fs";
5410
5769
  var c8 = {
@@ -5437,7 +5796,7 @@ function logOk(msg) {
5437
5796
  function logErr(msg) {
5438
5797
  console.error(`${timestamp2()} ${tag()} ${paint8("red", "\u2717")} ${msg}`);
5439
5798
  }
5440
- var reviewCommand = new Command24("review").description("Run an automated code review on a branch").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing review report ID (created by UI trigger)").option("--branch <name>", "Branch to review (defaults to current branch)").option("--base <name>", "Base branch to diff against (defaults to main)").action(async (opts) => {
5799
+ var reviewCommand = new Command25("review").description("Run an automated code review on a branch").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing review report ID (created by UI trigger)").option("--branch <name>", "Branch to review (defaults to current branch)").option("--base <name>", "Base branch to diff against (defaults to main)").option("--pr-url <url>", "Pull/merge request URL; when set, diff is fetched via gh/glab and no local checkout is needed").option("--pr-number <n>", "Pull/merge request number (used with --pr-url)").action(async (opts) => {
5441
5800
  const config = loadConfig();
5442
5801
  if (!config.apiKey) {
5443
5802
  logErr('Not authenticated. Run "mr login" first.');
@@ -5465,100 +5824,102 @@ var reviewCommand = new Command24("review").description("Run an automated code r
5465
5824
  logErr(`Failed to fetch project ${projectId}`);
5466
5825
  process.exit(1);
5467
5826
  }
5468
- const candidates = [];
5469
- if (project.localPath) candidates.push({ path: project.localPath, source: "project.localPath" });
5470
- for (const [dir, pid] of Object.entries(config.directories)) {
5471
- if (pid === projectId) candidates.push({ path: dir, source: "linked directory" });
5472
- }
5473
- candidates.push({ path: process.cwd(), source: "cwd" });
5474
- let projectPath;
5475
- let pathSource = "";
5476
- const triedPaths = [];
5477
- for (const { path, source } of candidates) {
5478
- if (existsSync13(path)) {
5479
- projectPath = path;
5480
- pathSource = source;
5481
- break;
5827
+ const baseBranch = opts.base || "main";
5828
+ const prUrl = opts.prUrl;
5829
+ const prNumberRaw = opts.prNumber;
5830
+ const prNumber = prNumberRaw ? Number(prNumberRaw) : void 0;
5831
+ const remote = prUrl ? parsePrUrl(prUrl) : null;
5832
+ const canUseRemote = remote !== null && (prNumber || remote.number) !== void 0;
5833
+ let diff;
5834
+ let branch = opts.branch;
5835
+ if (canUseRemote && remote) {
5836
+ const num = prNumber ?? remote.number;
5837
+ if (!num) {
5838
+ logErr(`Could not determine PR number from URL: ${prUrl}`);
5839
+ process.exit(1);
5482
5840
  }
5483
- triedPaths.push(`${path} (${source})`);
5484
- }
5485
- if (!projectPath) {
5486
- logErr(`No valid project path found. Tried:`);
5487
- for (const p of triedPaths) logErr(` - ${p}`);
5488
- logErr(`Run "mr link" from the correct directory, or update the project's localPath.`);
5489
- process.exit(1);
5490
- }
5491
- if (triedPaths.length > 0) {
5492
- log(paint8("yellow", `Skipped stale path(s): ${triedPaths.join(", ")}`));
5493
- log(`Falling back to ${pathSource}`);
5494
- }
5495
- try {
5496
- if (!statSync2(projectPath).isDirectory()) {
5497
- logErr(`Project path is not a directory: ${projectPath}`);
5841
+ log(`Fetching diff from ${paint8("cyan", remote.host)} for ${paint8("yellow", `#${num}`)} in ${paint8("dim", `${remote.owner}/${remote.repo}`)}`);
5842
+ try {
5843
+ diff = fetchRemoteDiff(remote, num);
5844
+ } catch (err) {
5845
+ logErr(`Failed to fetch diff from ${remote.host}: ${err.message}`);
5846
+ logErr(`Ensure ${remote.host === "github" ? "`gh`" : "`glab`"} is installed and authenticated.`);
5847
+ process.exit(1);
5848
+ }
5849
+ if (!branch) branch = `pr-${num}`;
5850
+ log(`Reviewing PR ${paint8("cyan", `#${num}`)} against ${paint8("dim", baseBranch)}`);
5851
+ } else {
5852
+ let projectPath = project.localPath;
5853
+ if (!projectPath) {
5854
+ for (const [dir, pid] of Object.entries(config.directories)) {
5855
+ if (pid === projectId) {
5856
+ projectPath = dir;
5857
+ break;
5858
+ }
5859
+ }
5860
+ }
5861
+ if (!projectPath) {
5862
+ projectPath = process.cwd();
5863
+ }
5864
+ if (!existsSync13(projectPath)) {
5865
+ logErr(`Project path does not exist: ${projectPath}`);
5866
+ logErr(`Update the project's localPath, attach a PR URL to the review, or run "mr link" from the correct directory.`);
5498
5867
  process.exit(1);
5499
5868
  }
5500
- } catch (err) {
5501
- logErr(`Cannot stat project path ${projectPath}: ${err.message}`);
5502
- process.exit(1);
5503
- }
5504
- if (!existsSync13(`${projectPath}/.git`)) {
5505
- logErr(`Project path is not a git repository: ${projectPath}`);
5506
- logErr(`Update the project's localPath to point to the local checkout.`);
5507
- process.exit(1);
5508
- }
5509
- log(`Using project path: ${paint8("dim", projectPath)}`);
5510
- let branch = opts.branch;
5511
- if (!branch) {
5512
5869
  try {
5513
- branch = execSync5("git rev-parse --abbrev-ref HEAD", {
5514
- cwd: projectPath,
5515
- encoding: "utf-8"
5516
- }).trim();
5517
- } catch {
5518
- logErr("Could not determine current branch. Pass --branch <name>.");
5870
+ if (!statSync2(projectPath).isDirectory()) {
5871
+ logErr(`Project path is not a directory: ${projectPath}`);
5872
+ process.exit(1);
5873
+ }
5874
+ } catch (err) {
5875
+ logErr(`Cannot stat project path ${projectPath}: ${err.message}`);
5519
5876
  process.exit(1);
5520
5877
  }
5521
- }
5522
- const baseBranch = opts.base || "main";
5523
- log(`Reviewing branch: ${paint8("cyan", branch)} against ${paint8("dim", baseBranch)}`);
5524
- log(`Project: ${paint8("cyan", project.name)}`);
5525
- let diff;
5526
- try {
5527
- diff = execSync5(`git diff ${baseBranch}...${branch} -- . ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml'`, {
5528
- cwd: projectPath,
5529
- encoding: "utf-8",
5530
- maxBuffer: 10 * 1024 * 1024
5531
- }).trim();
5532
- } catch (err) {
5878
+ if (!existsSync13(`${projectPath}/.git`)) {
5879
+ logErr(`Project path is not a git repository: ${projectPath}`);
5880
+ logErr(`Update the project's localPath to point to the local checkout, or attach a PR URL to the review.`);
5881
+ process.exit(1);
5882
+ }
5883
+ log(`Using project path: ${paint8("dim", projectPath)}`);
5884
+ if (!branch) {
5885
+ try {
5886
+ branch = execSync5("git rev-parse --abbrev-ref HEAD", {
5887
+ cwd: projectPath,
5888
+ encoding: "utf-8"
5889
+ }).trim();
5890
+ } catch {
5891
+ logErr("Could not determine current branch. Pass --branch <name>.");
5892
+ process.exit(1);
5893
+ }
5894
+ }
5895
+ log(`Reviewing branch: ${paint8("cyan", branch)} against ${paint8("dim", baseBranch)}`);
5533
5896
  try {
5534
- execSync5(`git fetch origin ${baseBranch}`, { cwd: projectPath, encoding: "utf-8", stdio: "pipe" });
5535
- diff = execSync5(`git diff origin/${baseBranch}...${branch} -- . ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml'`, {
5897
+ diff = execSync5(`git diff ${baseBranch}...${branch} -- . ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml'`, {
5536
5898
  cwd: projectPath,
5537
5899
  encoding: "utf-8",
5538
5900
  maxBuffer: 10 * 1024 * 1024
5539
5901
  }).trim();
5540
- } catch {
5541
- logErr(`Failed to get diff between ${baseBranch} and ${branch}: ${err.message}`);
5542
- process.exit(1);
5902
+ } catch (err) {
5903
+ try {
5904
+ execSync5(`git fetch origin ${baseBranch}`, { cwd: projectPath, encoding: "utf-8", stdio: "pipe" });
5905
+ diff = execSync5(`git diff origin/${baseBranch}...${branch} -- . ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml'`, {
5906
+ cwd: projectPath,
5907
+ encoding: "utf-8",
5908
+ maxBuffer: 10 * 1024 * 1024
5909
+ }).trim();
5910
+ } catch {
5911
+ logErr(`Failed to get diff between ${baseBranch} and ${branch}: ${err.message}`);
5912
+ process.exit(1);
5913
+ }
5543
5914
  }
5544
5915
  }
5916
+ log(`Project: ${paint8("cyan", project.name)}`);
5545
5917
  if (!diff) {
5546
5918
  logOk("No changes found between branches. Nothing to review.");
5547
5919
  process.exit(0);
5548
5920
  }
5549
- const filesChanged = (() => {
5550
- try {
5551
- const stat = execSync5(`git diff --stat ${baseBranch}...${branch} -- . ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml'`, {
5552
- cwd: projectPath,
5553
- encoding: "utf-8"
5554
- }).trim();
5555
- const lines = stat.split("\n");
5556
- return Math.max(0, lines.length - 1);
5557
- } catch {
5558
- const fileHeaders = diff.match(/^diff --git/gm);
5559
- return fileHeaders?.length ?? 0;
5560
- }
5561
- })();
5921
+ diff = stripLockFileDiffs(diff);
5922
+ const filesChanged = (diff.match(/^diff --git/gm) ?? []).length;
5562
5923
  log(`Diff size: ${paint8("yellow", `${diff.length.toLocaleString()} chars`)}, ${paint8("yellow", `${filesChanged} files`)}`);
5563
5924
  let reportId;
5564
5925
  if (opts.report) {
@@ -5645,6 +6006,54 @@ var reviewCommand = new Command24("review").description("Run an automated code r
5645
6006
  process.exit(1);
5646
6007
  }
5647
6008
  });
6009
+ function parsePrUrl(url) {
6010
+ try {
6011
+ const u = new URL(url);
6012
+ const parts = u.pathname.replace(/^\/+|\/+$/g, "").split("/");
6013
+ if (u.hostname === "github.com" || u.hostname.endsWith(".github.com")) {
6014
+ const owner = parts[0];
6015
+ const repo = parts[1];
6016
+ const kind = parts[2];
6017
+ const number = parts[3] ? Number(parts[3]) : void 0;
6018
+ if (!owner || !repo || kind !== "pull") return null;
6019
+ return { host: "github", owner, repo, number: Number.isFinite(number) ? number : void 0 };
6020
+ }
6021
+ const dashIdx = parts.indexOf("-");
6022
+ if (dashIdx > 0 && parts[dashIdx + 1] === "merge_requests") {
6023
+ const owner = parts.slice(0, dashIdx - 1).join("/");
6024
+ const repo = parts[dashIdx - 1];
6025
+ const number = parts[dashIdx + 2] ? Number(parts[dashIdx + 2]) : void 0;
6026
+ if (!owner || !repo) return null;
6027
+ return { host: "gitlab", owner, repo, number: Number.isFinite(number) ? number : void 0 };
6028
+ }
6029
+ return null;
6030
+ } catch {
6031
+ return null;
6032
+ }
6033
+ }
6034
+ function fetchRemoteDiff(remote, number) {
6035
+ if (remote.host === "github") {
6036
+ return execSync5(`gh pr diff ${number} --repo ${remote.owner}/${remote.repo}`, {
6037
+ encoding: "utf-8",
6038
+ maxBuffer: 20 * 1024 * 1024
6039
+ }).trim();
6040
+ }
6041
+ const project = `${remote.owner}/${remote.repo}`;
6042
+ return execSync5(`glab mr diff ${number} --repo ${project} --raw`, {
6043
+ encoding: "utf-8",
6044
+ maxBuffer: 20 * 1024 * 1024
6045
+ }).trim();
6046
+ }
6047
+ function stripLockFileDiffs(diff) {
6048
+ const skipPatterns = [/\.lock$/, /(^|\/)package-lock\.json$/, /(^|\/)pnpm-lock\.yaml$/, /(^|\/)yarn\.lock$/];
6049
+ const sections = diff.split(/(?=^diff --git )/m);
6050
+ return sections.filter((section) => {
6051
+ const header = section.match(/^diff --git a\/(.+?) b\//m);
6052
+ if (!header) return true;
6053
+ const path = header[1];
6054
+ return !skipPatterns.some((p) => p.test(path));
6055
+ }).join("").trim();
6056
+ }
5648
6057
  function formatDuration(ms) {
5649
6058
  if (ms < 1e3) return `${ms}ms`;
5650
6059
  const s = Math.round(ms / 1e3);
@@ -5753,7 +6162,7 @@ function parseReviewOutput(output) {
5753
6162
  }
5754
6163
 
5755
6164
  // cli/commands/scan.ts
5756
- import { Command as Command25 } from "commander";
6165
+ import { Command as Command26 } from "commander";
5757
6166
 
5758
6167
  // lib/scanner/index.ts
5759
6168
  import { spawn as spawn8 } from "child_process";
@@ -6185,7 +6594,7 @@ var SeedDataManager = class {
6185
6594
  };
6186
6595
 
6187
6596
  // lib/scanner/prompts.ts
6188
- function buildSynthesisPrompt(config, context, codebaseAnalysis, crawlResults, priorFindings) {
6597
+ function buildSynthesisPrompt(config, context, codebaseAnalysis, crawlResults, priorFindings, customPrompt) {
6189
6598
  const dismissedFindings = priorFindings.filter((f) => f.status === "dismissed");
6190
6599
  const promotedFindings = priorFindings.filter((f) => f.status === "promoted");
6191
6600
  const crawlSummary = crawlResults.map((r) => {
@@ -6233,6 +6642,9 @@ ${context.openTasks.slice(0, 5).map((t) => `- ${t.title}`).join("\n") || "None"}
6233
6642
  ${config.focusAreas.length > 0 ? `**Focus Areas (user-specified):**
6234
6643
  ${config.focusAreas.map((a) => `- ${a}`).join("\n")}` : ""}
6235
6644
 
6645
+ ${customPrompt && customPrompt.trim().length > 0 ? `**User-Specified Scan Direction (take priority \u2014 focus the scan around this):**
6646
+ ${customPrompt.trim()}` : ""}
6647
+
6236
6648
  ## Live Crawl Results
6237
6649
 
6238
6650
  ${crawlResults.length > 0 ? crawlSummary : "Live crawl was not performed (app may not be running)."}
@@ -6360,12 +6772,16 @@ async function runScanPipeline(opts) {
6360
6772
  }
6361
6773
  }
6362
6774
  opts.onLog("Synthesizing findings with Claude...");
6775
+ if (opts.customPrompt && opts.customPrompt.trim().length > 0) {
6776
+ opts.onLog("Using custom scan direction from user");
6777
+ }
6363
6778
  const prompt2 = buildSynthesisPrompt(
6364
6779
  config,
6365
6780
  context,
6366
6781
  codebaseAnalysis,
6367
6782
  crawlResults,
6368
- context.priorFindings
6783
+ context.priorFindings,
6784
+ opts.customPrompt
6369
6785
  );
6370
6786
  const synthesisResult = await runClaude2(prompt2);
6371
6787
  const parsed = parseSynthesisOutput(synthesisResult);
@@ -6515,7 +6931,7 @@ function logOk2(msg) {
6515
6931
  function logErr2(msg) {
6516
6932
  console.error(`${timestamp3()} ${scanTag()} ${paint9("red", "\u2717")} ${msg}`);
6517
6933
  }
6518
- var scanCommand = new Command25("scan").description("Run a product scan on the current project \u2014 analyzes codebase, crawls the app, and surfaces findings").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing scan report ID (created by UI trigger)").option("--no-crawl", "Skip live crawl (codebase analysis only)").action(async (opts) => {
6934
+ var scanCommand = new Command26("scan").description("Run a product scan on the current project \u2014 analyzes codebase, crawls the app, and surfaces findings").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing scan report ID (created by UI trigger)").option("--prompt <prompt>", "Custom scan direction/prompt to focus the scan on").option("--no-crawl", "Skip live crawl (codebase analysis only)").action(async (opts) => {
6519
6935
  const config = loadConfig();
6520
6936
  if (!config.apiKey) {
6521
6937
  logErr2('Not authenticated. Run "mr login" first.');
@@ -6557,9 +6973,17 @@ var scanCommand = new Command25("scan").description("Run a product scan on the c
6557
6973
  projectPath = process.cwd();
6558
6974
  }
6559
6975
  let reportId;
6976
+ let customPrompt = typeof opts.prompt === "string" && opts.prompt.trim().length > 0 ? opts.prompt.trim() : null;
6560
6977
  if (opts.report) {
6561
6978
  reportId = opts.report;
6562
6979
  log2(`Using existing scan report ${paint9("yellow", reportId.slice(0, 8))}`);
6980
+ try {
6981
+ const existing = await api.get(`/api/scans/${reportId}`);
6982
+ if (!customPrompt && existing.customPrompt) {
6983
+ customPrompt = existing.customPrompt;
6984
+ }
6985
+ } catch {
6986
+ }
6563
6987
  } else {
6564
6988
  try {
6565
6989
  const scans = await api.get(`/api/scans?projectId=${projectId}&status=processing`);
@@ -6572,7 +6996,8 @@ var scanCommand = new Command25("scan").description("Run a product scan on the c
6572
6996
  try {
6573
6997
  const report = await api.post("/api/scans", {
6574
6998
  projectId,
6575
- status: "pending"
6999
+ status: "pending",
7000
+ customPrompt
6576
7001
  });
6577
7002
  reportId = report.id;
6578
7003
  log2(`Created scan report ${paint9("yellow", reportId.slice(0, 8))}`);
@@ -6605,7 +7030,8 @@ var scanCommand = new Command25("scan").description("Run a product scan on the c
6605
7030
  onLog: log2,
6606
7031
  onProgress: (phase, detail) => {
6607
7032
  log2(`${paint9("dim", `[${phase}]`)} ${detail}`);
6608
- }
7033
+ },
7034
+ customPrompt
6609
7035
  });
6610
7036
  let wasCancelled = false;
6611
7037
  try {
@@ -6670,7 +7096,7 @@ var scanCommand = new Command25("scan").description("Run a product scan on the c
6670
7096
  });
6671
7097
 
6672
7098
  // cli/commands/doctor.ts
6673
- import { Command as Command26 } from "commander";
7099
+ import { Command as Command27 } from "commander";
6674
7100
  import { existsSync as existsSync16 } from "fs";
6675
7101
  import { homedir as homedir2 } from "os";
6676
7102
  import { join as join11 } from "path";
@@ -6718,7 +7144,7 @@ async function checkProjectLink() {
6718
7144
  optional: true
6719
7145
  };
6720
7146
  }
6721
- var doctorCommand = new Command26("doctor").description("Diagnose Mr. Manager CLI installation and environment").action(async () => {
7147
+ var doctorCommand = new Command27("doctor").description("Diagnose Mr. Manager CLI installation and environment").action(async () => {
6722
7148
  const banner = [
6723
7149
  ``,
6724
7150
  paint5("cyan", ` MR DOCTOR`),
@@ -6761,14 +7187,14 @@ var doctorCommand = new Command26("doctor").description("Diagnose Mr. Manager CL
6761
7187
  });
6762
7188
 
6763
7189
  // cli/commands/prompt-audit.ts
6764
- import { Command as Command27 } from "commander";
7190
+ import { Command as Command28 } from "commander";
6765
7191
  import { resolve as resolve8 } from "path";
6766
7192
  import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
6767
7193
  function auditLine(label, tokens) {
6768
7194
  const bar = "\u2588".repeat(Math.min(60, Math.round(tokens / 200)));
6769
7195
  return ` ${label.padEnd(30)} ${formatTokenCount(tokens).padStart(8)} ${bar}`;
6770
7196
  }
6771
- var promptAuditCommand = new Command27("prompt-audit").description("Dry-run prompt construction and report estimated token counts by job type").option("--task <id>", "Audit prompts for a specific task ID").option("--all", "Audit all supported job types with representative data", false).option("--json", "Output as JSON instead of plain text", false).action(async (opts) => {
7197
+ var promptAuditCommand = new Command28("prompt-audit").description("Dry-run prompt construction and report estimated token counts by job type").option("--task <id>", "Audit prompts for a specific task ID").option("--all", "Audit all supported job types with representative data", false).option("--json", "Output as JSON instead of plain text", false).action(async (opts) => {
6772
7198
  const results = [];
6773
7199
  if (opts.task) {
6774
7200
  try {
@@ -6977,7 +7403,8 @@ ${r.jobType} [${r.identifier}]`);
6977
7403
  });
6978
7404
 
6979
7405
  // cli/commands/skill.ts
6980
- import { Command as Command28 } from "commander";
7406
+ import { Command as Command29 } from "commander";
7407
+ import { createInterface as createInterface3 } from "readline/promises";
6981
7408
  var c10 = {
6982
7409
  reset: "\x1B[0m",
6983
7410
  bold: "\x1B[1m",
@@ -6986,7 +7413,32 @@ var c10 = {
6986
7413
  green: "\x1B[32m",
6987
7414
  yellow: "\x1B[33m"
6988
7415
  };
6989
- var skillCommand = new Command28("skill").description("Manage skills \u2014 reusable playbooks for AI agents");
7416
+ async function resolveSkill(idOrName) {
7417
+ try {
7418
+ return await api.get(
7419
+ `/api/skills/${encodeURIComponent(idOrName)}`
7420
+ );
7421
+ } catch (err) {
7422
+ if (!(err instanceof ApiError) || err.status !== 404) throw err;
7423
+ }
7424
+ const skills = await api.get("/api/skills");
7425
+ const matches = skills.filter((s) => s.name === idOrName);
7426
+ if (matches.length === 0) {
7427
+ throw new Error(`No skill found with id or name "${idOrName}".`);
7428
+ }
7429
+ if (matches.length > 1) {
7430
+ throw new Error(
7431
+ `Multiple skills named "${idOrName}". Pass an id instead:
7432
+ ${matches.map((m) => ` ${m.id}`).join("\n")}`
7433
+ );
7434
+ }
7435
+ return await api.get(`/api/skills/${matches[0].id}`);
7436
+ }
7437
+ function formatSize(bytes) {
7438
+ if (bytes < 1024) return `${bytes}b`;
7439
+ return `${(bytes / 1024).toFixed(1)}kb`;
7440
+ }
7441
+ var skillCommand = new Command29("skill").description("Manage skills \u2014 reusable playbooks for AI agents");
6990
7442
  skillCommand.command("list").alias("ls").description("List all skills").option("--category <category>", "Filter by category").action(async (opts) => {
6991
7443
  const params = new URLSearchParams();
6992
7444
  if (opts.category) params.set("category", opts.category);
@@ -7043,6 +7495,97 @@ skillCommand.command("create").description("Create a new skill from a markdown f
7043
7495
  `${c10.green}Created skill:${c10.reset} ${c10.bold}${skill.name}${c10.reset} ${c10.dim}(${skill.id})${c10.reset}`
7044
7496
  );
7045
7497
  });
7498
+ skillCommand.command("update").description(
7499
+ "Update an existing skill's content, metadata, or scope. The previous content is saved as a revision."
7500
+ ).argument("<idOrName>", "Skill id or name").option("-f, --file <path>", "Replace content from a markdown file").option("--content <text>", "Inline replacement content").option("-d, --description <desc>", "Update description").option("-c, --category <cat>", "Update category").option("--name <name>", "Rename the skill").option("-p, --project", "Re-scope to the linked project").option("--global", "Re-scope to global (clears projectId)").action(async (idOrName, opts) => {
7501
+ if (opts.project && opts.global) {
7502
+ console.error("Pass only one of --project or --global, not both.");
7503
+ process.exit(1);
7504
+ }
7505
+ if (opts.file && opts.content !== void 0) {
7506
+ console.error("Pass only one of --file or --content, not both.");
7507
+ process.exit(1);
7508
+ }
7509
+ let existing;
7510
+ try {
7511
+ existing = await resolveSkill(idOrName);
7512
+ } catch (err) {
7513
+ console.error(err.message);
7514
+ process.exit(1);
7515
+ }
7516
+ const patch = {};
7517
+ if (opts.file) {
7518
+ const { readFileSync: readFileSync13 } = await import("fs");
7519
+ try {
7520
+ patch.content = readFileSync13(opts.file, "utf-8").trim();
7521
+ } catch (err) {
7522
+ console.error(`Failed to read file: ${err.message}`);
7523
+ process.exit(1);
7524
+ }
7525
+ } else if (opts.content !== void 0) {
7526
+ patch.content = String(opts.content).trim();
7527
+ }
7528
+ if (opts.description !== void 0) patch.description = opts.description;
7529
+ if (opts.category !== void 0) patch.category = opts.category;
7530
+ if (opts.name !== void 0) patch.name = opts.name;
7531
+ if (opts.project) {
7532
+ const projectId = getLinkedProjectId();
7533
+ if (!projectId) {
7534
+ console.error(
7535
+ 'No project linked to this directory. Run "mr link <project-id>" first.'
7536
+ );
7537
+ process.exit(1);
7538
+ }
7539
+ patch.projectId = projectId;
7540
+ } else if (opts.global) {
7541
+ patch.projectId = null;
7542
+ }
7543
+ if (Object.keys(patch).length === 0) {
7544
+ console.error(
7545
+ "Nothing to update. Pass at least one of --file, --content, --description, --category, --name, --project, --global."
7546
+ );
7547
+ process.exit(1);
7548
+ }
7549
+ const updated = await api.patch(
7550
+ `/api/skills/${existing.id}`,
7551
+ patch
7552
+ );
7553
+ const beforeContent = existing.content ?? "";
7554
+ const afterContent = typeof patch.content === "string" ? patch.content : beforeContent;
7555
+ const contentChanged = typeof patch.content === "string" && patch.content !== beforeContent;
7556
+ const sizeDiff = typeof patch.content === "string" ? ` ${c10.dim}(${formatSize(Buffer.byteLength(beforeContent, "utf-8"))} -> ${formatSize(Buffer.byteLength(afterContent, "utf-8"))})${c10.reset}` : "";
7557
+ const priorRevisions = existing.revisions?.length ?? 0;
7558
+ const revisionsAfter = priorRevisions + (contentChanged ? 1 : 0);
7559
+ const revisionNote = contentChanged ? ` ${c10.dim}kept ${revisionsAfter} prior revision${revisionsAfter === 1 ? "" : "s"}${c10.reset}` : "";
7560
+ console.log(
7561
+ `${c10.green}Updated skill:${c10.reset} ${c10.bold}${updated.name}${c10.reset}${sizeDiff}${revisionNote}`
7562
+ );
7563
+ console.log(` ${c10.dim}id: ${updated.id}${c10.reset}`);
7564
+ });
7565
+ skillCommand.command("delete").alias("rm").description("Delete a skill").argument("<idOrName>", "Skill id or name").option("--yes", "Skip confirmation prompt").action(async (idOrName, opts) => {
7566
+ let skill;
7567
+ try {
7568
+ skill = await resolveSkill(idOrName);
7569
+ } catch (err) {
7570
+ console.error(err.message);
7571
+ process.exit(1);
7572
+ }
7573
+ if (!opts.yes) {
7574
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
7575
+ const answer = (await rl.question(
7576
+ `Delete skill ${c10.bold}${skill.name}${c10.reset} ${c10.dim}(${skill.id})${c10.reset}? [y/N] `
7577
+ )).trim().toLowerCase();
7578
+ rl.close();
7579
+ if (answer !== "y" && answer !== "yes") {
7580
+ console.log("Aborted.");
7581
+ return;
7582
+ }
7583
+ }
7584
+ await api.del(`/api/skills/${skill.id}`);
7585
+ console.log(
7586
+ `${c10.yellow}Deleted skill:${c10.reset} ${c10.bold}${skill.name}${c10.reset} ${c10.dim}(${skill.id})${c10.reset}`
7587
+ );
7588
+ });
7046
7589
  skillCommand.command("generate").alias("gen").description("Generate a new skill using AI from a text prompt").argument("<prompt>", "Describe the skill to generate").option("-p, --project", "Scope to the linked project").action(async (prompt2, opts) => {
7047
7590
  let projectId = null;
7048
7591
  if (opts.project) {
@@ -7077,7 +7620,7 @@ skillCommand.command("generate").alias("gen").description("Generate a new skill
7077
7620
  });
7078
7621
 
7079
7622
  // cli/commands/resource.ts
7080
- import { Command as Command29 } from "commander";
7623
+ import { Command as Command30 } from "commander";
7081
7624
  var c11 = {
7082
7625
  reset: "\x1B[0m",
7083
7626
  bold: "\x1B[1m",
@@ -7097,7 +7640,7 @@ function typeLabel(type) {
7097
7640
  const color = TYPE_COLORS[type] ?? c11.dim;
7098
7641
  return `${color}${type}${c11.reset}`;
7099
7642
  }
7100
- var resourceCommand = new Command29("resource").description("Manage resources \u2014 documents, plans, research, and notes");
7643
+ var resourceCommand = new Command30("resource").description("Manage resources \u2014 documents, plans, research, and notes");
7101
7644
  resourceCommand.command("list").alias("ls").description("List resources for the linked project (or all)").option("--all", "List all resources across projects").action(async (opts) => {
7102
7645
  const params = new URLSearchParams();
7103
7646
  if (opts.all) {
@@ -7195,13 +7738,13 @@ resourceCommand.command("generate").alias("gen").description("Generate a resourc
7195
7738
  });
7196
7739
 
7197
7740
  // cli/commands/tests.ts
7198
- import { Command as Command30 } from "commander";
7741
+ import { Command as Command31 } from "commander";
7199
7742
  var c12 = {
7200
7743
  reset: "\x1B[0m",
7201
7744
  dim: "\x1B[2m",
7202
7745
  yellow: "\x1B[33m"
7203
7746
  };
7204
- var testsCommand = new Command30("tests").description("List MR Test scenarios for the linked project").action(async () => {
7747
+ var testsCommand = new Command31("tests").description("List MR Test scenarios for the linked project").action(async () => {
7205
7748
  const projectId = getLinkedProjectId();
7206
7749
  if (!projectId) {
7207
7750
  console.error(
@@ -7232,6 +7775,35 @@ var testsCommand = new Command30("tests").description("List MR Test scenarios fo
7232
7775
  }
7233
7776
  });
7234
7777
 
7778
+ // cli/commands/walkthrough.ts
7779
+ import { Command as Command32 } from "commander";
7780
+ var SKILL_NAME = "Generate Walkthrough Video";
7781
+ var walkthroughCommand = new Command32("walkthrough").description("Attach the Generate Walkthrough Video skill to a task and queue it").argument("<task-id>", "Task ID to generate a walkthrough for").option("--prod", "Allow recording against production (requires explicit confirmation)").action(async (taskId, opts) => {
7782
+ const skills = await api.get("/api/skills");
7783
+ const skill = skills.find((s) => s.name === SKILL_NAME);
7784
+ if (!skill) {
7785
+ console.error(
7786
+ `Skill "${SKILL_NAME}" not found. Run \`mr setup\` to seed starter skills.`
7787
+ );
7788
+ process.exit(1);
7789
+ }
7790
+ try {
7791
+ await api.post(`/api/tasks/${taskId}/skills`, { skillId: skill.id });
7792
+ } catch (err) {
7793
+ const msg = err.message;
7794
+ if (!msg.includes("already attached")) {
7795
+ console.error(`Failed to attach skill: ${msg}`);
7796
+ process.exit(1);
7797
+ }
7798
+ }
7799
+ await api.post(`/api/tasks/${taskId}/updates`, {
7800
+ message: opts.prod ? "Walkthrough video queued (production target \u2014 confirm via watcher)" : "Walkthrough video queued",
7801
+ source: "agent"
7802
+ });
7803
+ console.log(`\u2713 Attached "${SKILL_NAME}" to task ${taskId}`);
7804
+ console.log(" Run `mr watch` to process the queue.");
7805
+ });
7806
+
7235
7807
  // cli/index.ts
7236
7808
  var configPath = join12(homedir3(), ".mr-manager", "config.json");
7237
7809
  var isFirstRun = !existsSync18(configPath);
@@ -7270,7 +7842,7 @@ if (isFirstRun && !shouldBypass) {
7270
7842
  console.log("");
7271
7843
  process.exit(0);
7272
7844
  }
7273
- var program = new Command31();
7845
+ var program = new Command33();
7274
7846
  program.name("mr").description("Mr. Manager - Task and project management CLI").version(CLI_VERSION);
7275
7847
  program.addCommand(initCommand);
7276
7848
  program.addCommand(authCommand);
@@ -7288,6 +7860,7 @@ program.addCommand(undelegateCommand);
7288
7860
  program.addCommand(createCommand);
7289
7861
  program.addCommand(completeCommand);
7290
7862
  program.addCommand(subtaskCompleteCommand);
7863
+ program.addCommand(subtaskAddCommand);
7291
7864
  program.addCommand(prototypeCommand);
7292
7865
  program.addCommand(setupCommand);
7293
7866
  program.addCommand(updateCommand);
@@ -7305,4 +7878,5 @@ program.addCommand(promptAuditCommand);
7305
7878
  program.addCommand(skillCommand);
7306
7879
  program.addCommand(resourceCommand);
7307
7880
  program.addCommand(testsCommand);
7881
+ program.addCommand(walkthroughCommand);
7308
7882
  program.parse();