@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.
- package/dist/index.mjs +851 -277
- 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
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
2250
|
-
const
|
|
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 ${
|
|
2253
|
-
`1.
|
|
2254
|
-
`2.
|
|
2255
|
-
`3.
|
|
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(
|
|
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
|
|
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**:
|
|
2265
|
-
`- **
|
|
2266
|
-
`- **Color
|
|
2267
|
-
`- **Interaction
|
|
2268
|
-
`- **
|
|
2269
|
-
`- **Navigation**:
|
|
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
|
|
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
|
|
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
|
-
`- **
|
|
2279
|
-
`- **Layout**:
|
|
2280
|
-
`- **
|
|
2281
|
-
`- **
|
|
2282
|
-
`- **
|
|
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
|
|
2288
|
-
`- Use
|
|
2289
|
-
`- Each file
|
|
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
|
|
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
|
-
`- **
|
|
2296
|
-
`- **Layout**:
|
|
2297
|
-
`- **
|
|
2298
|
-
`- **
|
|
2299
|
-
`- **Interaction**:
|
|
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
|
-
`-
|
|
2304
|
-
`- Each file
|
|
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
|
|
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
|
-
`- **
|
|
2311
|
-
`- **Visual language**:
|
|
2312
|
-
`- **Color
|
|
2313
|
-
`- **Typography**:
|
|
2314
|
-
`- **Symbolism**:
|
|
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
|
|
2318
|
-
`- The
|
|
2319
|
-
`- Show the
|
|
2320
|
-
`- Include a
|
|
2321
|
-
`- The SVG must be clean and production-ready \u2014 no
|
|
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
|
-
`
|
|
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
|
-
|
|
2489
|
+
...sharedQualityBar,
|
|
2490
|
+
``,
|
|
2491
|
+
...creativeDirection,
|
|
2492
|
+
``,
|
|
2493
|
+
`## Design Variance: ${varianceInfo.label} (${variance}/100)`,
|
|
2494
|
+
``,
|
|
2495
|
+
varianceInfo.summary,
|
|
2350
2496
|
``,
|
|
2351
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
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
|
-
`-
|
|
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 ${
|
|
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 =
|
|
2374
|
-
const
|
|
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 ${
|
|
2377
|
-
`1. Redesign variant ${
|
|
2378
|
-
`2.
|
|
2379
|
-
`3.
|
|
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(
|
|
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
|
|
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
|
-
`-
|
|
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
|
-
|
|
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
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
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
|
-
|
|
3247
|
-
const
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
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
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3647
|
+
for (const f of readdirSync(slice.dir)) {
|
|
3283
3648
|
try {
|
|
3284
|
-
|
|
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
|
-
|
|
3319
|
-
|
|
3320
|
-
finishing.delete(key);
|
|
3653
|
+
rmdirSync(slice.dir);
|
|
3654
|
+
} catch {
|
|
3321
3655
|
}
|
|
3322
|
-
}
|
|
3323
|
-
}
|
|
3324
|
-
|
|
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
|
|
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/
|
|
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
|
|
4241
|
-
new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4679
|
-
var updateCommand = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5524
|
+
import { Command as Command22 } from "commander";
|
|
5166
5525
|
import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
|
|
5167
|
-
var testCommand = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
let
|
|
5475
|
-
let
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
if (
|
|
5479
|
-
|
|
5480
|
-
|
|
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
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
log(paint8("
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
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
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
}
|
|
5517
|
-
} catch {
|
|
5518
|
-
logErr(
|
|
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
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
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
|
|
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
|
-
|
|
5542
|
-
|
|
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
|
-
|
|
5550
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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();
|