@dunnewold-labs/mr-manager 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +1557 -725
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // cli/index.ts
4
4
  import { Command as Command27 } from "commander";
5
- import { existsSync as existsSync16 } from "fs";
5
+ import { existsSync as existsSync17 } from "fs";
6
6
  import { homedir as homedir3 } from "os";
7
7
  import { join as join12 } from "path";
8
8
 
@@ -132,7 +132,7 @@ Remote login \u2014 visit this URL in any browser:
132
132
  }
133
133
  async function loginWithLocalServer(apiUrl) {
134
134
  const port = getRandomPort();
135
- return new Promise((resolve7, reject) => {
135
+ return new Promise((resolve8, reject) => {
136
136
  const server = createServer((req, res) => {
137
137
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
138
138
  const key = url.searchParams.get("key");
@@ -144,7 +144,7 @@ async function loginWithLocalServer(apiUrl) {
144
144
  </body></html>
145
145
  `);
146
146
  server.close();
147
- if (key) resolve7(key);
147
+ if (key) resolve8(key);
148
148
  else reject(new Error("No key received from server"));
149
149
  });
150
150
  server.listen(port, () => {
@@ -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.4",
188
+ version: "0.4.6",
189
189
  description: "Mr. Manager - Task and project management CLI",
190
190
  bin: {
191
191
  mr: "./dist/index.mjs"
@@ -356,10 +356,10 @@ function detectVcs(cwd) {
356
356
  // cli/commands/link.ts
357
357
  function prompt(question) {
358
358
  const rl = createInterface({ input: process.stdin, output: process.stdout });
359
- return new Promise((resolve7) => {
359
+ return new Promise((resolve8) => {
360
360
  rl.question(question, (answer) => {
361
361
  rl.close();
362
- resolve7(answer.trim());
362
+ resolve8(answer.trim());
363
363
  });
364
364
  });
365
365
  }
@@ -492,7 +492,7 @@ import { Command as Command8 } from "commander";
492
492
  import { spawn as spawn4, exec } from "child_process";
493
493
  import { randomUUID } from "crypto";
494
494
  import { resolve as resolve2 } from "path";
495
- import { readFileSync as readFileSync5, readdirSync, unlinkSync, existsSync as existsSync7, statSync } from "fs";
495
+ import { readFileSync as readFileSync5, readdirSync, unlinkSync, existsSync as existsSync7, statSync, writeFileSync as writeFileSync4 } from "fs";
496
496
  import * as readline from "readline";
497
497
 
498
498
  // lib/test-runner.ts
@@ -512,25 +512,89 @@ function tryExec(command, cwd) {
512
512
  return false;
513
513
  }
514
514
  }
515
+ function resolveWorktreeStartPoint(hasRemoteBranch, hasLocalBranch, hasOriginMain) {
516
+ if (hasRemoteBranch && !hasLocalBranch) {
517
+ return "origin-branch";
518
+ }
519
+ if (hasLocalBranch) {
520
+ return "local-branch";
521
+ }
522
+ if (hasOriginMain) {
523
+ return "origin-main";
524
+ }
525
+ return null;
526
+ }
527
+ function parseGitWorktreePorcelain(output) {
528
+ const entries = [];
529
+ const blocks = output.trim().split(/\n(?=worktree )/);
530
+ for (const block of blocks) {
531
+ if (!block.trim()) continue;
532
+ let path = null;
533
+ let branch = null;
534
+ for (const line of block.split("\n")) {
535
+ if (line.startsWith("worktree ")) {
536
+ path = line.slice("worktree ".length).trim();
537
+ } else if (line.startsWith("branch ")) {
538
+ const ref = line.slice("branch ".length).trim();
539
+ branch = ref.replace(/^refs\/heads\//, "");
540
+ }
541
+ }
542
+ if (path) {
543
+ entries.push({ path, branch });
544
+ }
545
+ }
546
+ return entries;
547
+ }
548
+ function findExistingWorktreeForBranch(repoDir, branch) {
549
+ try {
550
+ const output = execSync2("git worktree list --porcelain", {
551
+ cwd: repoDir,
552
+ stdio: "pipe"
553
+ }).toString();
554
+ const match = parseGitWorktreePorcelain(output).find((entry) => entry.branch === branch);
555
+ return match?.path ?? null;
556
+ } catch {
557
+ return null;
558
+ }
559
+ }
515
560
  function createWorktree(repoDir, branch, worktreeName) {
516
561
  const wtPath = join4(repoDir, ".mr-worktrees", worktreeName);
517
562
  if (existsSync4(wtPath)) {
518
563
  execSync2(`git checkout ${branch}`, { cwd: wtPath, stdio: "pipe" });
519
- return wtPath;
564
+ return {
565
+ path: wtPath,
566
+ created: false,
567
+ reusedBranchWorktree: false
568
+ };
569
+ }
570
+ const existingBranchWorktree = findExistingWorktreeForBranch(repoDir, branch);
571
+ if (existingBranchWorktree) {
572
+ return {
573
+ path: existingBranchWorktree,
574
+ created: false,
575
+ reusedBranchWorktree: true
576
+ };
520
577
  }
521
578
  tryExec(`git fetch origin ${branch}`, repoDir);
579
+ const hasOriginMain = tryExec(`git fetch origin main`, repoDir) && tryExec(`git rev-parse --verify "origin/main"`, repoDir);
522
580
  const hasRemoteBranch = tryExec(`git rev-parse --verify "origin/${branch}"`, repoDir);
523
581
  const hasLocalBranch = tryExec(`git rev-parse --verify "${branch}"`, repoDir);
524
- if (hasRemoteBranch && !hasLocalBranch) {
582
+ const startPoint = resolveWorktreeStartPoint(hasRemoteBranch, hasLocalBranch, hasOriginMain);
583
+ if (startPoint === "origin-branch") {
525
584
  execSync2(`git worktree add -b "${branch}" "${wtPath}" "origin/${branch}"`, {
526
585
  cwd: repoDir,
527
586
  stdio: "pipe"
528
587
  });
529
- } else if (hasLocalBranch) {
588
+ } else if (startPoint === "local-branch") {
530
589
  execSync2(`git worktree add "${wtPath}" "${branch}"`, {
531
590
  cwd: repoDir,
532
591
  stdio: "pipe"
533
592
  });
593
+ } else if (startPoint === "origin-main") {
594
+ execSync2(`git worktree add -b "${branch}" "${wtPath}" "origin/main"`, {
595
+ cwd: repoDir,
596
+ stdio: "pipe"
597
+ });
534
598
  } else {
535
599
  execSync2(`git worktree add -b "${branch}" "${wtPath}"`, {
536
600
  cwd: repoDir,
@@ -543,7 +607,11 @@ function createWorktree(repoDir, branch, worktreeName) {
543
607
  copyFileSync(src, join4(wtPath, envFile));
544
608
  }
545
609
  }
546
- return wtPath;
610
+ return {
611
+ path: wtPath,
612
+ created: true,
613
+ reusedBranchWorktree: false
614
+ };
547
615
  }
548
616
  function removeWorktree(repoDir, wtPath) {
549
617
  try {
@@ -777,7 +845,7 @@ async function runTest(options) {
777
845
  }
778
846
  log2(`Branch: ${branch}`);
779
847
  log2("Creating git worktree...");
780
- wtPath = createWorktree(localPath, branch, worktreeName);
848
+ wtPath = createWorktree(localPath, branch, worktreeName).path;
781
849
  log2(`Worktree created at ${wtPath}`);
782
850
  log2("Installing dependencies...");
783
851
  try {
@@ -890,6 +958,55 @@ async function runTest(options) {
890
958
  return result;
891
959
  }
892
960
 
961
+ // lib/agent-fallback.ts
962
+ var AGENT_FALLBACK_ORDER = ["claude", "codex", "gemini"];
963
+ function getAgentFallbackChain(agent) {
964
+ const startIndex = AGENT_FALLBACK_ORDER.indexOf(agent);
965
+ if (startIndex === -1) {
966
+ return [];
967
+ }
968
+ return AGENT_FALLBACK_ORDER.slice(startIndex);
969
+ }
970
+ function getAvailableAgentFallbackChain(agent, availability) {
971
+ return getAgentFallbackChain(agent).filter((candidate) => availability[candidate] !== false);
972
+ }
973
+
974
+ // lib/task-workflow.ts
975
+ function normalizeWhitespace(value) {
976
+ return value.replace(/\s+/g, " ").trim();
977
+ }
978
+ var NON_CODE_TASK_KEYWORDS = [
979
+ "no code",
980
+ "no-code",
981
+ "without code",
982
+ "research",
983
+ "document",
984
+ "documentation",
985
+ "docs",
986
+ "prd",
987
+ "test plan",
988
+ "resource",
989
+ "write up",
990
+ "writeup",
991
+ "config via the ui",
992
+ "configuration via the ui",
993
+ "ui configuration",
994
+ "upload"
995
+ ];
996
+ var NON_CODE_TASK_PATTERNS = [
997
+ /\b(?:doesn'?t|does not|don't|do not)\s+(?:involve|require|need)\s+(?:writing\s+)?(?:any\s+)?code\b/,
998
+ /\b(?:without|avoids?)\s+(?:writing\s+)?(?:any\s+)?code\b/,
999
+ /\b(?:no need|skip)\s+to\s+(?:spin up|create|prepare)\s+(?:an?\s+)?worktree\b/
1000
+ ];
1001
+ function taskLikelyDoesNotNeedCodeChanges(task) {
1002
+ if (task.mode && task.mode !== "development") return true;
1003
+ const haystack = normalizeWhitespace(
1004
+ [task.title, task.notes, task.prdContent].filter(Boolean).join(" ").toLowerCase()
1005
+ );
1006
+ if (!haystack) return false;
1007
+ return NON_CODE_TASK_KEYWORDS.some((keyword) => haystack.includes(keyword)) || NON_CODE_TASK_PATTERNS.some((pattern) => pattern.test(haystack));
1008
+ }
1009
+
893
1010
  // cli/browse-runner.ts
894
1011
  import { execSync as execSync4, spawn as spawn3 } from "child_process";
895
1012
  import { existsSync as existsSync6 } from "fs";
@@ -916,7 +1033,7 @@ Run the setup script: cd browse && ./setup`
916
1033
  async function runBrowseCommand2(browseArgs) {
917
1034
  const runner = getBrowseRunner();
918
1035
  const fullArgs = [...runner.args, ...browseArgs];
919
- return new Promise((resolve7) => {
1036
+ return new Promise((resolve8) => {
920
1037
  const proc = spawn3(runner.cmd, fullArgs, {
921
1038
  stdio: ["pipe", "pipe", "pipe"],
922
1039
  env: { ...process.env }
@@ -933,21 +1050,156 @@ async function runBrowseCommand2(browseArgs) {
933
1050
  if (stderr && code !== 0) {
934
1051
  process.stderr.write(stderr);
935
1052
  }
936
- resolve7({ stdout: stdout.trim(), exitCode: code || 0 });
1053
+ resolve8({ stdout: stdout.trim(), exitCode: code || 0 });
937
1054
  });
938
1055
  proc.on("error", () => {
939
- resolve7({ stdout: "", exitCode: 1 });
1056
+ resolve8({ stdout: "", exitCode: 1 });
940
1057
  });
941
1058
  });
942
1059
  }
943
1060
 
1061
+ // cli/utils/token-estimate.ts
1062
+ function estimateTokens(text) {
1063
+ return Math.ceil(text.length / 4);
1064
+ }
1065
+ function formatTokenCount(tokens) {
1066
+ if (tokens < 1e3) return String(tokens);
1067
+ if (tokens < 1e4) return `~${(tokens / 1e3).toFixed(1)}k`;
1068
+ return `~${Math.round(tokens / 1e3)}k`;
1069
+ }
1070
+ function tokenLogLine(jobType, identifier, prompt2, systemPrompt) {
1071
+ const promptTokens = estimateTokens(prompt2);
1072
+ const systemTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;
1073
+ const total = promptTokens + systemTokens;
1074
+ const parts = [`[tokens] ${jobType}:${identifier} prompt=${formatTokenCount(promptTokens)}`];
1075
+ if (systemTokens > 0) {
1076
+ parts[0] += ` system=${formatTokenCount(systemTokens)}`;
1077
+ }
1078
+ parts[0] += ` total=${formatTokenCount(total)}`;
1079
+ return parts[0];
1080
+ }
1081
+
944
1082
  // cli/commands/watch.ts
945
1083
  var FEATURES_FILE2 = ".mr-features.md";
946
- function readFeaturesDoc(repoDir) {
947
- const path = resolve2(repoDir, FEATURES_FILE2);
948
- if (!existsSync7(path)) return null;
949
- return readFileSync5(path, "utf-8");
1084
+ var SYSTEM_SECTION_STATUS_UPDATES = `## Status Updates
1085
+
1086
+ As you work, post brief status updates so progress is visible in the UI.
1087
+ Use: mr update <task-id> "your status message here"
1088
+
1089
+ The system automatically posts updates for: agent dispatch and task completion. Do NOT duplicate these \u2014 focus your updates on what you're actually doing:
1090
+ - What you found while exploring (e.g. "Found the auth logic in src/auth.ts, needs refactoring")
1091
+ - What specific changes you're making (e.g. "Adding validation to the signup form component")
1092
+ - Problems you hit and how you solved them (e.g. "Fixed circular dependency between User and Order models")
1093
+ - What files/components you modified (e.g. "Updated api/routes.ts and middleware/auth.ts")
1094
+
1095
+ Be specific about your changes \u2014 don't post vague messages like "working on the task" or "exploring the codebase". Each update should tell the user something concrete about what changed or what you learned.
1096
+
1097
+ Keep messages short (1 sentence). Post 3-5 updates total.`;
1098
+ var SYSTEM_SECTION_SCREENSHOTS = `## Screenshots
1099
+
1100
+ Before you finish, take a screenshot of your work to attach to the task. This helps the reviewer see what changed visually.
1101
+ Use: mr screenshot <task-id> <path-to-image> -m "Description of what the screenshot shows"
1102
+
1103
+ You can take a screenshot on macOS (no file arg needed) or provide a path to an existing image file.
1104
+ When no file is provided, the command uses the browse daemon (headless Chromium) to screenshot the app's project page automatically.
1105
+ You can also specify a custom URL: mr screenshot <task-id> --url "http://localhost:3000/some-page" -m "description"
1106
+ If your changes are visual (UI changes), try to capture the result. If they are purely backend/logic changes, you can skip this step.`;
1107
+ var SYSTEM_SECTION_TEST_PLAN = `## Test Plan
1108
+
1109
+ After pushing your MR/PR, create a structured test plan so the reviewer can run automated browser tests against your changes.
1110
+ Save the test plan as a TaskResource by calling:
1111
+ mr update <task-id> --resource test-plan "Test plan for <task-title>" '<json>'
1112
+
1113
+ Or write it to a file and upload it. The test plan is a JSON array of browse commands:
1114
+
1115
+ \`\`\`json
1116
+ [
1117
+ { "command": "goto", "args": ["/login"], "description": "Navigate to login page" },
1118
+ { "command": "fill", "args": ["input[type=email]", "test@example.com"], "description": "Enter email" },
1119
+ { "command": "fill", "args": ["input[type=password]", "test-password"], "description": "Enter password" },
1120
+ { "command": "click", "args": ["button[type=submit]"], "description": "Submit login" },
1121
+ { "command": "wait", "args": ["2000"], "description": "Wait for redirect" },
1122
+ { "command": "goto", "args": ["/dashboard"], "description": "Navigate to changed page" },
1123
+ { "command": "screenshot", "description": "Capture dashboard" },
1124
+ { "command": "assertVisible", "args": [".dashboard-widget"], "description": "Verify widget renders" },
1125
+ { "command": "assertText", "args": ["h1", "Dashboard"], "description": "Verify page title" }
1126
+ ]
1127
+ \`\`\`
1128
+
1129
+ Supported commands: goto, click, fill, wait, screenshot, assertVisible, assertText, assertUrl.
1130
+ Include auth steps (login/signup URLs) if the feature requires authentication.
1131
+ If your changes don't have a testable UI flow, you can skip this step.`;
1132
+ var SYSTEM_SECTION_NO_MR = `## Tasks That Don't Need an MR/PR
1133
+
1134
+ If the task does not require code changes (e.g. research, documentation uploaded as a resource, configuration via the UI, etc.), you can signal this instead of creating a pull request:
1135
+ mr no-mr <task-id> "Brief description of what was done instead"
1136
+
1137
+ This tells the watch system to skip looking for a PR and records what action was taken. You should still clean up any worktrees and exit normally.`;
1138
+ var SYSTEM_SECTION_FEATURES_WORKFLOW = `## Features & Goals Document Workflow
1139
+
1140
+ After completing your work, check if the project has a .mr-features.md file. If changes are relevant, update it.
1141
+ Write the updated content to a temp file and run: mr features --file /tmp/mr-features-update.md
1142
+ Only update the sections relevant to your changes. Preserve existing content that is still accurate.
1143
+ If the project does not yet have .mr-features.md, create one capturing the current state of features grouped by area.`;
1144
+ var SYSTEM_SECTION_PRD_FORMAT = `### PRD Format Conventions
1145
+
1146
+ Follow these conventions so PRDs are scannable by humans and parseable by agents.
1147
+ All sections are optional \u2014 include only what's relevant for this task type (greenfield, feature, bug fix, refactor). Omit sections that would be boilerplate.
1148
+
1149
+ **TL;DR Block** \u2014 Start the PRD with a \`> \` blockquote summary (2-3 sentences) immediately after the \`# PRD: Title\` heading.
1150
+
1151
+ **Requirement IDs** \u2014 Every requirement gets a unique ID prefix and a single-sentence summary on the first line:
1152
+ \`**F1: Name** \u2014 One-sentence summary.\` followed by optional \`- Detail bullet\` lines.
1153
+ Use \`F\` prefix for functional requirements, \`NF\` for non-functional. Number sequentially within each category.
1154
+
1155
+ **Success Metrics as JSON** \u2014 Use a fenced \`json\` code block containing an array of objects with \`Metric\`, \`Target\`, and \`Type\` fields.
1156
+
1157
+ **Affected Components as JSON** \u2014 Use the same JSON table format for the technical approach section.
1158
+
1159
+ **Implementation Steps as Phased Checklists** \u2014 Use \`### Phase N: Name\` headers with markdown checklists (\`- [ ]\`).
1160
+
1161
+ **Edge Cases as Tagged List** \u2014 Each edge case leads with a bold tag: \`- **Tag** \u2014 Description.\`
1162
+
1163
+ **No Frontmatter** \u2014 Do not include YAML frontmatter. Metadata lives in the database.`;
1164
+ var SYSTEM_SECTION_PRD_OPEN_QUESTIONS = `### Open Questions Format
1165
+
1166
+ The "Open Questions" section of the PRD MUST end with a fenced JSON code block tagged \`open-questions\`.
1167
+
1168
+ Format:
1169
+ \`\`\`open-questions
1170
+ [
1171
+ {
1172
+ "id": "q1",
1173
+ "question": "Which approach should we use for X?",
1174
+ "context": "One sentence explaining why this decision matters.",
1175
+ "options": [
1176
+ "Option A \u2014 brief description",
1177
+ "Option B \u2014 brief description"
1178
+ ]
1179
+ }
1180
+ ]
1181
+ \`\`\`
1182
+
1183
+ Rules:
1184
+ - Each question MUST have 2-4 concrete options.
1185
+ - Options should be actionable choices.
1186
+ - The "context" field should be a single sentence.
1187
+ - Use sequential IDs: q1, q2, q3, etc.
1188
+ - Place the JSON block at the very end of the "Open Questions" section.`;
1189
+ var SYSTEM_SECTIONS = {
1190
+ "status-updates": SYSTEM_SECTION_STATUS_UPDATES,
1191
+ "screenshots": SYSTEM_SECTION_SCREENSHOTS,
1192
+ "test-plan": SYSTEM_SECTION_TEST_PLAN,
1193
+ "no-mr": SYSTEM_SECTION_NO_MR,
1194
+ "features-workflow": SYSTEM_SECTION_FEATURES_WORKFLOW,
1195
+ "prd-format": SYSTEM_SECTION_PRD_FORMAT,
1196
+ "prd-open-questions": SYSTEM_SECTION_PRD_OPEN_QUESTIONS
1197
+ };
1198
+ function composeSystemPrompt(sections) {
1199
+ return sections.map((s) => SYSTEM_SECTIONS[s]).join("\n\n");
950
1200
  }
1201
+ var EXECUTION_SYSTEM_SECTIONS = ["status-updates", "screenshots", "test-plan", "no-mr", "features-workflow"];
1202
+ var PRD_SYSTEM_SECTIONS = ["prd-format", "prd-open-questions"];
951
1203
  var c = {
952
1204
  reset: "\x1B[0m",
953
1205
  bold: "\x1B[1m",
@@ -1026,6 +1278,12 @@ function slugify(title) {
1026
1278
  function shortId(id) {
1027
1279
  return id.slice(0, 8);
1028
1280
  }
1281
+ function ownerPrefix(task) {
1282
+ const name = task.user?.name;
1283
+ if (!name) return "mr";
1284
+ const first = name.split(/\s+/)[0].toLowerCase().replace(/[^a-z0-9]/g, "");
1285
+ return first || "mr";
1286
+ }
1029
1287
  function formatElapsed(ms) {
1030
1288
  const totalMinutes = Math.max(1, Math.floor(ms / 6e4));
1031
1289
  const hours = Math.floor(totalMinutes / 60);
@@ -1035,13 +1293,13 @@ function formatElapsed(ms) {
1035
1293
  }
1036
1294
  return `${totalMinutes}m`;
1037
1295
  }
1038
- function worktreePath(sid) {
1039
- return `.mr-worktrees/mr-${sid}`;
1296
+ function worktreePath(name) {
1297
+ return `.mr-worktrees/${name}`;
1040
1298
  }
1041
1299
  function worktreeNameFromPath(wtPath) {
1042
1300
  return wtPath.replace(/^\.mr-worktrees\//, "");
1043
1301
  }
1044
- function normalizeWhitespace(value) {
1302
+ function normalizeWhitespace2(value) {
1045
1303
  return value.replace(/\s+/g, " ").trim();
1046
1304
  }
1047
1305
  function extractFirstMeaningfulParagraph(markdown) {
@@ -1066,7 +1324,7 @@ function extractFirstMeaningfulParagraph(markdown) {
1066
1324
  if (quoteLines.length > 0) break;
1067
1325
  }
1068
1326
  if (quoteLines.length > 0) {
1069
- const text2 = normalizeWhitespace(quoteLines.join(" "));
1327
+ const text2 = normalizeWhitespace2(quoteLines.join(" "));
1070
1328
  return text2 || null;
1071
1329
  }
1072
1330
  const paragraphLines = [];
@@ -1089,7 +1347,7 @@ function extractFirstMeaningfulParagraph(markdown) {
1089
1347
  paragraphLines.push(line);
1090
1348
  }
1091
1349
  if (paragraphLines.length === 0) return null;
1092
- const text = normalizeWhitespace(paragraphLines.join(" "));
1350
+ const text = normalizeWhitespace2(paragraphLines.join(" "));
1093
1351
  return text || null;
1094
1352
  }
1095
1353
  function truncateText(value, maxLength) {
@@ -1127,25 +1385,6 @@ function buildPrBodyTemplate(task, subtasks, protoRefs = [], feedbackUpdates = [
1127
1385
  "_Replace every placeholder above before creating the PR. Remove bullets or sections that do not apply._"
1128
1386
  ].join("\n");
1129
1387
  }
1130
- function pullLatestMain(repoDir, prefix) {
1131
- return new Promise((resolve7) => {
1132
- exec(
1133
- "git pull origin main --ff-only",
1134
- { cwd: repoDir },
1135
- (err, stdout, stderr) => {
1136
- if (err) {
1137
- logWarn(prefix, `git pull failed (proceeding anyway): ${stderr.trim() || err.message}`);
1138
- } else {
1139
- const msg = stdout.trim();
1140
- if (msg && msg !== "Already up to date.") {
1141
- logInfo(prefix, `pulled latest main: ${paint("gray", msg.split("\n")[0])}`);
1142
- }
1143
- }
1144
- resolve7();
1145
- }
1146
- );
1147
- });
1148
- }
1149
1388
  function buildPlanningPrompt(task, repoDir) {
1150
1389
  const notes = task.notes ? `
1151
1390
 
@@ -1171,17 +1410,109 @@ ${task.notes}` : "";
1171
1410
  }
1172
1411
  function findPrUrl(branchName, repoDir, vcs = "github") {
1173
1412
  const cmd = vcs === "gitlab" ? `glab mr view "${branchName}" --output json 2>/dev/null | jq -r '.web_url // empty'` : `gh pr view "${branchName}" --json url -q .url`;
1174
- return new Promise((resolve7) => {
1413
+ return new Promise((resolve8) => {
1175
1414
  exec(
1176
1415
  cmd,
1177
1416
  { cwd: repoDir },
1178
1417
  (err, stdout) => {
1179
- if (err) resolve7(null);
1180
- else resolve7(stdout.trim() || null);
1418
+ if (err) resolve8(null);
1419
+ else resolve8(stdout.trim() || null);
1181
1420
  }
1182
1421
  );
1183
1422
  });
1184
1423
  }
1424
+ function buildAutomaticPrBody(task, branchName, vcs, subtasks, protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = []) {
1425
+ const contextSource = task.prdContent ?? task.notes ?? "";
1426
+ const taskContext = extractFirstMeaningfulParagraph(contextSource);
1427
+ const contextBullets = [
1428
+ `- Resolves MR Manager task ${task.id}.`,
1429
+ `- Task: ${task.title}.`,
1430
+ ...taskContext ? [`- Goal/context: ${truncateText(taskContext, 220)}.`] : [],
1431
+ ...subtasks.map((subtask) => `- Completed subtask: ${subtask.title}.`),
1432
+ ...protoRefs.map((ref) => `- Related prototype: ${ref.prototype.title}${ref.selectedVariants?.length ? ` (variants ${ref.selectedVariants.join(", ")})` : ""}.`),
1433
+ ...feedbackUpdates.slice(0, 3).map((update) => `- Addresses feedback from ${new Date(update.createdAt).toLocaleDateString("en-US")}.`),
1434
+ ...existingResources.slice(0, 3).map((resource) => `- Referenced resource: ${resource.name}.${resource.kind === "link" && resource.url ? ` (${resource.url})` : ""}`),
1435
+ ...skillRefs.slice(0, 3).map((ref) => `- Applied skill guidance: ${ref.skill.name}.`)
1436
+ ];
1437
+ return [
1438
+ "## Summary",
1439
+ `- Auto-created by \`mr watch\` because no existing ${detectPrLabelFromVcs(vcs)} was found for branch \`${branchName}\` after the agent finished.`,
1440
+ `- Captures the implementation for MR Manager task \`${task.id}\` while preserving the task title as the review title.`,
1441
+ "",
1442
+ "## Context",
1443
+ ...contextBullets,
1444
+ "",
1445
+ "## Testing",
1446
+ "- [ ] Automated tests: Not run by `mr watch`; see task updates or follow-up commits for validation details.",
1447
+ "- [ ] Manual verification completed by reviewer."
1448
+ ].join("\n");
1449
+ }
1450
+ function detectPrLabelFromVcs(vcs) {
1451
+ return vcs === "gitlab" ? "MR" : "PR";
1452
+ }
1453
+ function extractPrUrlFromText(value) {
1454
+ const match = value.match(/https?:\/\/(?:github\.com\/[^\s]+\/pull\/\d+|gitlab\.[^\s]+\/merge_requests\/\d+)/);
1455
+ return match ? match[0] : null;
1456
+ }
1457
+ function commandSucceeds(command, cwd) {
1458
+ return new Promise((resolve8) => {
1459
+ exec(command, { cwd }, (err) => resolve8(!err));
1460
+ });
1461
+ }
1462
+ async function createPrInRepo(task, branchName, repoDir, vcs, subtasks, protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = []) {
1463
+ const hasLocalBranch = await commandSucceeds(`git show-ref --verify --quiet refs/heads/${JSON.stringify(branchName)}`, repoDir);
1464
+ const hasRemoteBranch = await commandSucceeds(`git ls-remote --exit-code --heads origin ${JSON.stringify(branchName)}`, repoDir);
1465
+ if (!hasLocalBranch && !hasRemoteBranch) {
1466
+ return null;
1467
+ }
1468
+ const bodyPath = resolve2("/tmp", `mr-auto-pr-${randomUUID()}.md`);
1469
+ const body = buildAutomaticPrBody(task, branchName, vcs, subtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
1470
+ writeFileSync4(bodyPath, `${body}
1471
+ `, "utf-8");
1472
+ const createCommand2 = vcs === "gitlab" ? `glab mr create --source-branch ${JSON.stringify(branchName)} --title ${JSON.stringify(task.title)} --description-file ${JSON.stringify(bodyPath)} --yes` : `gh pr create --head ${JSON.stringify(branchName)} --title ${JSON.stringify(task.title)} --body-file ${JSON.stringify(bodyPath)}`;
1473
+ try {
1474
+ const output = await new Promise((resolve8, reject) => {
1475
+ exec(createCommand2, { cwd: repoDir }, (err, stdout, stderr) => {
1476
+ if (err) {
1477
+ reject(new Error(stderr.trim() || stdout.trim() || err.message));
1478
+ return;
1479
+ }
1480
+ resolve8(`${stdout}
1481
+ ${stderr}`.trim());
1482
+ });
1483
+ });
1484
+ const createdUrl = extractPrUrlFromText(output);
1485
+ return createdUrl ?? await findPrUrl(branchName, repoDir, vcs);
1486
+ } catch {
1487
+ return null;
1488
+ } finally {
1489
+ if (existsSync7(bodyPath)) {
1490
+ unlinkSync(bodyPath);
1491
+ }
1492
+ }
1493
+ }
1494
+ async function createPrAcrossRepos(task, branchName, repoDir, vcs, subtasks, protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = []) {
1495
+ if (isGitRepo(repoDir)) {
1496
+ return createPrInRepo(task, branchName, repoDir, vcs, subtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
1497
+ }
1498
+ const childRepos = findChildGitRepos(repoDir);
1499
+ for (const repo of childRepos) {
1500
+ const childVcs = detectVcs(repo)?.provider ?? vcs;
1501
+ const url = await createPrInRepo(
1502
+ task,
1503
+ branchName,
1504
+ repo,
1505
+ childVcs,
1506
+ subtasks,
1507
+ protoRefs,
1508
+ feedbackUpdates,
1509
+ existingResources,
1510
+ skillRefs
1511
+ );
1512
+ if (url) return url;
1513
+ }
1514
+ return null;
1515
+ }
1185
1516
  function isGitRepo(dir) {
1186
1517
  try {
1187
1518
  return existsSync7(resolve2(dir, ".git"));
@@ -1227,13 +1558,41 @@ async function extractPrUrlFromUpdates(taskId) {
1227
1558
  }
1228
1559
  return null;
1229
1560
  }
1230
- function buildPrototypeSection(protoRefs) {
1561
+ function checkPrStatus(prUrl, repoDir, vcs = "github") {
1562
+ const cmd = vcs === "gitlab" ? `glab mr view "${prUrl}" --output json 2>/dev/null` : `gh pr view "${prUrl}" --json merged,mergeable 2>/dev/null`;
1563
+ return new Promise((resolve8) => {
1564
+ exec(cmd, { cwd: repoDir }, (err, stdout) => {
1565
+ if (err || !stdout.trim()) {
1566
+ resolve8(null);
1567
+ return;
1568
+ }
1569
+ try {
1570
+ const data = JSON.parse(stdout.trim());
1571
+ if (vcs === "gitlab") {
1572
+ resolve8({
1573
+ merged: data.state === "merged",
1574
+ hasConflicts: data.has_conflicts === true
1575
+ });
1576
+ } else {
1577
+ resolve8({
1578
+ merged: data.merged === true,
1579
+ hasConflicts: data.mergeable === "CONFLICTING"
1580
+ });
1581
+ }
1582
+ } catch {
1583
+ resolve8(null);
1584
+ }
1585
+ });
1586
+ });
1587
+ }
1588
+ function buildPrototypeSection(protoRefs, workingDir) {
1231
1589
  if (protoRefs.length === 0) return "";
1232
1590
  const sections = [
1233
1591
  ``,
1234
1592
  `## Referenced Prototypes`,
1235
1593
  ``,
1236
1594
  `The following prototype designs have been linked to this task as reference. Use them to guide the implementation.`,
1595
+ `Read the referenced files when you need to see the HTML content.`,
1237
1596
  ``
1238
1597
  ];
1239
1598
  for (const ref of protoRefs) {
@@ -1251,10 +1610,19 @@ function buildPrototypeSection(protoRefs) {
1251
1610
  for (let i = 0; i < selectedFiles.length; i++) {
1252
1611
  const file = selectedFiles[i];
1253
1612
  const variantLabel = selected.length < files.length ? `Variant ${selected[i] + 1} (selected)` : `Variant ${i + 1}`;
1254
- sections.push(`#### ${variantLabel}: ${file.name}`);
1255
- sections.push(`\`\`\`html`);
1256
- sections.push(file.content);
1257
- sections.push(`\`\`\``);
1613
+ const tmpDir = workingDir ?? "/tmp";
1614
+ const safeProtoId = proto.id.slice(0, 8);
1615
+ const tmpPath = resolve2(tmpDir, `.mr-proto-${safeProtoId}-v${selected[i] ?? i}.html`);
1616
+ try {
1617
+ writeFileSync4(tmpPath, file.content, "utf-8");
1618
+ sections.push(`#### ${variantLabel}: ${file.name}`);
1619
+ sections.push(`File: \`${tmpPath}\` \u2014 read this file to see the full HTML content.`);
1620
+ } catch {
1621
+ sections.push(`#### ${variantLabel}: ${file.name}`);
1622
+ sections.push(`\`\`\`html`);
1623
+ sections.push(file.content);
1624
+ sections.push(`\`\`\``);
1625
+ }
1258
1626
  sections.push(``);
1259
1627
  }
1260
1628
  }
@@ -1320,8 +1688,9 @@ function buildFeedbackSection(updates) {
1320
1688
  return lines.join("\n");
1321
1689
  }
1322
1690
  function buildFeaturesSection(repoDir) {
1323
- const content = readFeaturesDoc(repoDir);
1324
- if (!content) {
1691
+ const featuresPath = resolve2(repoDir, FEATURES_FILE2);
1692
+ const exists = existsSync7(featuresPath);
1693
+ if (!exists) {
1325
1694
  return [
1326
1695
  ``,
1327
1696
  `## Features & Goals Document`,
@@ -1341,11 +1710,8 @@ function buildFeaturesSection(repoDir) {
1341
1710
  ``,
1342
1711
  `## Features & Goals Document`,
1343
1712
  ``,
1344
- `The project maintains a features & goals document at \`.mr-features.md\`. Here is its current content:`,
1345
- ``,
1346
- "```markdown",
1347
- content,
1348
- "```",
1713
+ `The project maintains a features & goals document at \`${featuresPath}\`.`,
1714
+ `Read this file if you need to understand existing features or update it after your work.`,
1349
1715
  ``,
1350
1716
  `After completing your work, update this document to reflect any changes. Write the updated content to a temp file and run:`,
1351
1717
  `\`mr features --file /tmp/mr-features-update.md\``,
@@ -1354,11 +1720,11 @@ function buildFeaturesSection(repoDir) {
1354
1720
  ``
1355
1721
  ].join("\n");
1356
1722
  }
1357
- function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = [], executionDir) {
1358
- const sid = shortId(task.id);
1723
+ function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = [], executionDir, startWithoutWorktree = false) {
1359
1724
  const slug = slugify(task.title);
1360
- const branchName = `mr/${sid}/${slug}`;
1361
- const wtPath = worktreePath(sid);
1725
+ const owner = ownerPrefix(task);
1726
+ const branchName = `${owner}/${slug}`;
1727
+ const wtPath = worktreePath(`${owner}-${slug}`);
1362
1728
  const workingDir = executionDir ?? repoDir;
1363
1729
  const prBodyPath = "/tmp/mr-pr-body.md";
1364
1730
  const notes = task.prdContent ? `
@@ -1381,7 +1747,6 @@ ${task.notes}` : "";
1381
1747
  ``
1382
1748
  ].join("\n") : "";
1383
1749
  const hasFeedback = feedbackUpdates.length > 0;
1384
- const feedbackWtPath = hasFeedback ? worktreePath(`${sid}-fb`) : wtPath;
1385
1750
  const prBodyTemplate = buildPrBodyTemplate(task, pendingSubtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
1386
1751
  const prCreateCmd = vcs === "gitlab" ? `glab mr create --title "${task.title}" --description-file ${prBodyPath} --yes` : `gh pr create --title "${task.title}" --body-file ${prBodyPath}`;
1387
1752
  return [
@@ -1393,7 +1758,7 @@ ${task.notes}` : "";
1393
1758
  `ID: ${task.id}${notes}`,
1394
1759
  subtaskSection,
1395
1760
  buildResourcesSection(existingResources),
1396
- buildPrototypeSection(protoRefs),
1761
+ buildPrototypeSection(protoRefs, workingDir),
1397
1762
  buildSkillsSection(skillRefs),
1398
1763
  buildFeedbackSection(feedbackUpdates),
1399
1764
  `## Instructions`,
@@ -1408,9 +1773,16 @@ ${task.notes}` : "";
1408
1773
  ] : hasFeedback ? [
1409
1774
  `2. Set up your working environment:`,
1410
1775
  ` - Fetch the latest from origin: \`git fetch origin\``,
1411
- ` - Check out the existing branch in a worktree: \`git worktree add ${feedbackWtPath} origin/${branchName}\``,
1412
- ` - If the origin branch doesn't exist yet, fall back to: \`git worktree add -b ${branchName} ${feedbackWtPath}\``,
1413
- ` - Work in the worktree directory: ${feedbackWtPath}`
1776
+ ` - Reuse the existing task branch/worktree if it is still present.`,
1777
+ ` - Otherwise check out the existing branch in a worktree: \`git worktree add ${wtPath} origin/${branchName}\``,
1778
+ ` - If the origin branch doesn't exist yet, fall back to: \`git worktree add -b ${branchName} ${wtPath}\``,
1779
+ ` - Work in the worktree directory: ${wtPath}`
1780
+ ] : startWithoutWorktree ? [
1781
+ `2. Start in the linked project directory first:`,
1782
+ ` - Current working directory: ${repoDir}`,
1783
+ ` - This task appears likely to be a no-code workflow (research, documentation, UI configuration, or resource updates), so no worktree has been pre-created.`,
1784
+ ` - If you discover you need to edit tracked files or write code after all, create a worktree before making changes: \`git worktree add -b ${branchName} ${wtPath}\``,
1785
+ ` - If the task spans multiple repos, create matching worktrees only in the repo(s) where edits are actually needed.`
1414
1786
  ] : [
1415
1787
  `2. Set up an isolated working environment:`,
1416
1788
  ` - If ${repoDir} is a git repo, create a worktree: \`git worktree add -b ${branchName} ${wtPath}\``,
@@ -1446,21 +1818,6 @@ ${task.notes}` : "";
1446
1818
  ``,
1447
1819
  `This tells the watch system to skip looking for a ${vcs === "gitlab" ? "MR" : "PR"} and records what action was taken. You should still clean up any worktrees and exit normally.`,
1448
1820
  ``,
1449
- `## Status Updates`,
1450
- ``,
1451
- `As you work, post brief status updates so progress is visible in the UI:`,
1452
- `\`mr update ${task.id} "your status message here"\``,
1453
- ``,
1454
- `The system automatically posts updates for: agent dispatch and task completion. Do NOT duplicate these \u2014 focus your updates on what you're actually doing:`,
1455
- `- What you found while exploring (e.g. "Found the auth logic in src/auth.ts, needs refactoring")`,
1456
- `- What specific changes you're making (e.g. "Adding validation to the signup form component")`,
1457
- `- Problems you hit and how you solved them (e.g. "Fixed circular dependency between User and Order models")`,
1458
- `- What files/components you modified (e.g. "Updated api/routes.ts and middleware/auth.ts")`,
1459
- ``,
1460
- `Be specific about your changes \u2014 don't post vague messages like "working on the task" or "exploring the codebase". Each update should tell the user something concrete about what changed or what you learned.`,
1461
- ``,
1462
- `Keep messages short (1 sentence). Post 3-5 updates total.`,
1463
- ``,
1464
1821
  ...hasFeedback ? [] : [
1465
1822
  `## PR Description Template`,
1466
1823
  ``,
@@ -1472,41 +1829,6 @@ ${task.notes}` : "";
1472
1829
  "```",
1473
1830
  ``
1474
1831
  ],
1475
- `## Screenshots`,
1476
- ``,
1477
- `Before you finish, take a screenshot of your work to attach to the task. This helps the reviewer see what changed visually.`,
1478
- `\`mr screenshot ${task.id} <path-to-image> -m "Description of what the screenshot shows"\``,
1479
- ``,
1480
- `You can take a screenshot on macOS (no file arg needed) or provide a path to an existing image file.`,
1481
- `When no file is provided, the command uses the browse daemon (headless Chromium) to screenshot the app's project page automatically.`,
1482
- `You can also specify a custom URL: \`mr screenshot ${task.id} --url "http://localhost:3000/some-page" -m "description"\``,
1483
- `If your changes are visual (UI changes), try to capture the result. If they are purely backend/logic changes, you can skip this step.`,
1484
- ``,
1485
- `## Test Plan`,
1486
- ``,
1487
- `After pushing your MR/PR, create a structured test plan so the reviewer can run automated browser tests against your changes.`,
1488
- `Save the test plan as a TaskResource by calling:`,
1489
- `\`mr update ${task.id} --resource test-plan "Test plan for ${task.title}" '<json>'\``,
1490
- ``,
1491
- `Or write it to a file and upload it. The test plan is a JSON array of browse commands:`,
1492
- ``,
1493
- "```json",
1494
- `[`,
1495
- ` { "command": "goto", "args": ["/login"], "description": "Navigate to login page" },`,
1496
- ` { "command": "fill", "args": ["input[type=email]", "test@example.com"], "description": "Enter email" },`,
1497
- ` { "command": "fill", "args": ["input[type=password]", "test-password"], "description": "Enter password" },`,
1498
- ` { "command": "click", "args": ["button[type=submit]"], "description": "Submit login" },`,
1499
- ` { "command": "wait", "args": ["2000"], "description": "Wait for redirect" },`,
1500
- ` { "command": "goto", "args": ["/dashboard"], "description": "Navigate to changed page" },`,
1501
- ` { "command": "screenshot", "description": "Capture dashboard" },`,
1502
- ` { "command": "assertVisible", "args": [".dashboard-widget"], "description": "Verify widget renders" },`,
1503
- ` { "command": "assertText", "args": ["h1", "Dashboard"], "description": "Verify page title" }`,
1504
- `]`,
1505
- "```",
1506
- ``,
1507
- `Supported commands: goto, click, fill, wait, screenshot, assertVisible, assertText, assertUrl.`,
1508
- `Include auth steps (login/signup URLs) if the feature requires authentication.`,
1509
- `If your changes don't have a testable UI flow, you can skip this step.`,
1510
1832
  buildFeaturesSection(repoDir),
1511
1833
  `Complete all steps autonomously without asking for confirmation. Exit with code 0 when done.`
1512
1834
  ].join("\n");
@@ -1517,16 +1839,31 @@ function buildPrdPrompt(task, repoDir, existingPrd, feedbackUpdates = []) {
1517
1839
  Task notes:
1518
1840
  ${task.notes}` : "";
1519
1841
  const isRevision = !!(existingPrd && feedbackUpdates.length > 0);
1520
- const feedbackSection = isRevision ? [
1521
- ``,
1522
- `## Existing PRD`,
1523
- ``,
1524
- `The following PRD was previously generated. You must revise it based on the user's feedback below \u2014 do NOT start from scratch.`,
1525
- ``,
1526
- existingPrd,
1527
- ``,
1528
- buildFeedbackSection(feedbackUpdates)
1529
- ].join("\n") : "";
1842
+ let feedbackSection = "";
1843
+ if (isRevision) {
1844
+ const prdTmpPath = resolve2(repoDir, ".mr-existing-prd.md");
1845
+ try {
1846
+ writeFileSync4(prdTmpPath, existingPrd, "utf-8");
1847
+ } catch {
1848
+ }
1849
+ const prdSummaryLines = [];
1850
+ for (const line of (existingPrd ?? "").split("\n")) {
1851
+ const trimmed = line.trim();
1852
+ if (/^#{1,3}\s/.test(trimmed)) prdSummaryLines.push(trimmed);
1853
+ else if (/^\*\*[FN]+\d+:/.test(trimmed)) prdSummaryLines.push(` ${trimmed.split("\u2014")[0].trim()}`);
1854
+ }
1855
+ feedbackSection = [
1856
+ ``,
1857
+ `## Existing PRD`,
1858
+ ``,
1859
+ `A PRD was previously generated. You must revise it based on the user's feedback below \u2014 do NOT start from scratch.`,
1860
+ ``,
1861
+ `**Full PRD file:** \`${prdTmpPath}\` \u2014 read this file for the complete existing PRD.`,
1862
+ ``,
1863
+ ...prdSummaryLines.length > 0 ? [`**PRD structure summary:**`, ...prdSummaryLines, ``] : [],
1864
+ buildFeedbackSection(feedbackUpdates)
1865
+ ].join("\n");
1866
+ }
1530
1867
  const revisionInstructions = isRevision ? [
1531
1868
  `1. Read the existing PRD and user feedback above carefully.`,
1532
1869
  `2. Revise the PRD to address every piece of feedback. Keep sections that are still valid \u2014 only modify what the feedback requires.`,
@@ -1557,77 +1894,16 @@ ${task.notes}` : "";
1557
1894
  ` - Technical approach and affected components`,
1558
1895
  ` - Edge cases`,
1559
1896
  ` - Implementation steps / breakdown`,
1560
- ` - **Open Questions** \u2014 this section MUST use the structured format below`,
1561
- ``,
1562
- `### PRD Format Conventions`,
1563
- ``,
1564
- `Follow these conventions so PRDs are scannable by humans and parseable by agents.`,
1565
- `All sections are optional \u2014 include only what's relevant for this task type (greenfield, feature, bug fix, refactor). Omit sections that would be boilerplate.`,
1566
- ``,
1567
- `**TL;DR Block** \u2014 Start the PRD with a \`> \` blockquote summary (2-3 sentences) immediately after the \`# PRD: Title\` heading. This gives humans instant orientation and agents a compact context summary.`,
1568
- ``,
1569
- `**Requirement IDs** \u2014 Every requirement gets a unique ID prefix and a single-sentence summary on the first line:`,
1570
- `\`**F1: Name** \u2014 One-sentence summary.\` followed by optional \`- Detail bullet\` lines.`,
1571
- `Use \`F\` prefix for functional requirements, \`NF\` for non-functional. Number sequentially within each category.`,
1572
- `The summary line must be a complete description \u2014 an agent should be able to extract just summary lines to build a work plan.`,
1573
- ...isRevision ? [`When revising, preserve existing requirement IDs. Add new requirements by continuing the numbering sequence \u2014 never restart at F1.`] : [],
1574
- ``,
1575
- `**Success Metrics as JSON** \u2014 Use a fenced \`json\` code block containing an array of objects. The UI auto-renders these as tables.`,
1576
- `Each object should have \`Metric\`, \`Target\`, and \`Type\` fields. Example:`,
1577
- "````json",
1578
- `[`,
1579
- ` { "Metric": "API response time under load", "Target": "< 200ms p95", "Type": "Performance" }`,
1580
- `]`,
1581
- "````",
1582
- ``,
1583
- `**Affected Components as JSON** \u2014 Use the same JSON table format for the technical approach section:`,
1584
- "````json",
1585
- `[`,
1586
- ` { "Component": "path/to/file.ts", "Change": "What changes", "Scope": "~N lines" }`,
1587
- `]`,
1588
- "````",
1589
- ``,
1590
- `**Implementation Steps as Phased Checklists** \u2014 Use \`### Phase N: Name\` headers with markdown checklists (\`- [ ]\`).`,
1591
- `Gives humans a timeline overview at heading level; gives agents discrete work items.`,
1897
+ ` - **Open Questions** \u2014 this section MUST use the structured format described in the system prompt`,
1592
1898
  ``,
1593
- `**Edge Cases as Tagged List** \u2014 Each edge case leads with a bold tag: \`- **Tag** \u2014 Description.\``,
1594
- ``,
1595
- `**No Frontmatter** \u2014 Do not include YAML frontmatter. Metadata lives in the database.`,
1596
- ``,
1597
- `### Open Questions Format`,
1598
- ``,
1599
- `The "Open Questions" section of the PRD MUST end with a fenced JSON code block`,
1600
- `tagged \`open-questions\`. This allows the UI to present them as answerable cards.`,
1601
- ``,
1602
- `Format:`,
1603
- "```",
1604
- "```open-questions",
1605
- `[`,
1606
- ` {`,
1607
- ` "id": "q1",`,
1608
- ` "question": "Which approach should we use for X?",`,
1609
- ` "context": "One sentence explaining why this decision matters.",`,
1610
- ` "options": [`,
1611
- ` "Option A \u2014 brief description",`,
1612
- ` "Option B \u2014 brief description",`,
1613
- ` "Option C \u2014 brief description"`,
1614
- ` ]`,
1615
- ` }`,
1616
- `]`,
1617
- "```",
1618
- "```",
1619
- ``,
1620
- `Rules for open questions:`,
1621
- `- Each question MUST have 2-4 concrete options.`,
1622
- `- Options should be actionable choices, not vague ("Yes"/"No" is fine when appropriate).`,
1623
- `- The "context" field should be a single sentence explaining why the decision matters.`,
1624
- `- Use sequential IDs: q1, q2, q3, etc.`,
1625
- `- Place the JSON block at the very end of the "Open Questions" section.`,
1626
- `- You may include a brief prose introduction before the JSON block, but the block itself must be last.`,
1627
1899
  ...isRevision ? [
1628
- `- If the user's feedback answered an open question, incorporate their answer and remove that question.`,
1629
- `- Only include questions that are still genuinely open after considering the feedback.`
1630
- ] : [],
1900
+ `Follow the PRD Format Conventions and Open Questions Format from the system prompt.`,
1901
+ `When revising, preserve existing requirement IDs. Add new requirements by continuing the numbering sequence \u2014 never restart at F1.`,
1902
+ `If the user's feedback answered an open question, incorporate their answer and remove that question.`,
1903
+ `Only include questions that are still genuinely open after considering the feedback.`
1904
+ ] : [
1905
+ `Follow the PRD Format Conventions and Open Questions Format from the system prompt.`
1906
+ ],
1631
1907
  `${nextStep(4)}. Save the PRD as a Markdown file in the working directory:`,
1632
1908
  ` - File name: \`prd.md\``,
1633
1909
  ` - Write the full PRD content to this file.`,
@@ -1784,13 +2060,23 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
1784
2060
  );
1785
2061
  }
1786
2062
  const variantList = Array.from({ length: proto.variantCount }, (_, i) => `prototype-${i + 1}.html`);
1787
- const existingVariants = parentFiles.map((f, i) => [
1788
- `### Previous Variant ${i + 1} (${f.name})`,
1789
- `\`\`\`html`,
1790
- f.content,
1791
- `\`\`\``,
1792
- ``
1793
- ].join("\n")).join("\n");
2063
+ const existingVariantLines = [];
2064
+ for (let i = 0; i < parentFiles.length; i++) {
2065
+ const f = parentFiles[i];
2066
+ const tmpPath = resolve2(repoDir, `.mr-parent-variant-${i + 1}.html`);
2067
+ try {
2068
+ writeFileSync4(tmpPath, f.content, "utf-8");
2069
+ existingVariantLines.push(`### Previous Variant ${i + 1} (${f.name})`);
2070
+ existingVariantLines.push(`File: \`${tmpPath}\` \u2014 read this file to see the full HTML content.`);
2071
+ } catch {
2072
+ existingVariantLines.push(`### Previous Variant ${i + 1} (${f.name})`);
2073
+ existingVariantLines.push(`\`\`\`html`);
2074
+ existingVariantLines.push(f.content);
2075
+ existingVariantLines.push(`\`\`\``);
2076
+ }
2077
+ existingVariantLines.push(``);
2078
+ }
2079
+ const existingVariants = existingVariantLines.join("\n");
1794
2080
  return [
1795
2081
  `You are a UI designer and frontend engineer. Your job is to REFINE an existing prototype based on user feedback.`,
1796
2082
  ``,
@@ -1907,7 +2193,7 @@ function buildIdeaPrompt(idea, repoDir) {
1907
2193
  `- Do NOT exit until both files have been written and verified`
1908
2194
  ].join("\n");
1909
2195
  }
1910
- function buildAgentArgs(agent, prompt2, mode, sessionId, name) {
2196
+ function buildAgentArgs(agent, prompt2, mode, sessionId, name, resumeSession = false, systemPrompt) {
1911
2197
  if (agent === "codex") {
1912
2198
  const args = [];
1913
2199
  if (mode === "execute") {
@@ -1917,26 +2203,40 @@ function buildAgentArgs(agent, prompt2, mode, sessionId, name) {
1917
2203
  if (mode === "execute") {
1918
2204
  args.push("-s", "danger-full-access");
1919
2205
  }
1920
- args.push(prompt2);
2206
+ const fullPrompt = systemPrompt ? `${prompt2}
2207
+
2208
+ ${systemPrompt}` : prompt2;
2209
+ args.push(fullPrompt);
1921
2210
  return { bin: "codex", args };
1922
2211
  }
1923
2212
  if (agent === "gemini") {
1924
- const args = ["-p", prompt2];
2213
+ const fullPrompt = systemPrompt ? `${prompt2}
2214
+
2215
+ ${systemPrompt}` : prompt2;
2216
+ const args = ["-p", fullPrompt];
1925
2217
  if (mode === "execute") {
1926
2218
  args.push("--yolo");
1927
2219
  }
1928
2220
  return { bin: "gemini", args };
1929
2221
  }
1930
- const sessionArgs = sessionId ? ["--session-id", sessionId] : [];
2222
+ const sessionArgs = sessionId ? resumeSession ? ["--resume", sessionId] : ["--session-id", sessionId] : [];
1931
2223
  const nameArgs = name ? ["--name", name] : [];
2224
+ const systemArgs = systemPrompt ? ["--append-system-prompt", systemPrompt] : [];
1932
2225
  if (mode === "plan") {
1933
- return { bin: "claude", args: [...sessionArgs, ...nameArgs, "--permission-mode", "plan", "-p", prompt2] };
2226
+ return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, "--permission-mode", "plan", "-p", prompt2] };
1934
2227
  }
1935
- return { bin: "claude", args: [...sessionArgs, ...nameArgs, "-p", "--dangerously-skip-permissions", prompt2] };
2228
+ return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, "-p", "--dangerously-skip-permissions", prompt2] };
2229
+ }
2230
+ function commandExists(cmd) {
2231
+ return new Promise((resolve8) => {
2232
+ exec(`command -v ${cmd}`, (err) => resolve8(!err));
2233
+ });
1936
2234
  }
1937
2235
  function runPlanningPhase(task, repoDir, agent) {
1938
2236
  return new Promise((res, reject) => {
1939
- const { bin, args } = buildAgentArgs(agent, buildPlanningPrompt(task, repoDir), "plan", void 0, task.title);
2237
+ const planPrompt = buildPlanningPrompt(task, repoDir);
2238
+ console.log(`${timestamp()} ${taskTag(shortId(task.id))} ${paint("dim", tokenLogLine("plan", shortId(task.id), planPrompt))}`);
2239
+ const { bin, args } = buildAgentArgs(agent, planPrompt, "plan", void 0, task.title);
1940
2240
  const child = spawn4(bin, args, { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] });
1941
2241
  child.on("error", (err) => {
1942
2242
  reject(new Error(`Failed to spawn ${agent}: ${err.message}`));
@@ -1969,25 +2269,28 @@ ${output.trim()}`));
1969
2269
  });
1970
2270
  }
1971
2271
  function askYesNo(question) {
1972
- return new Promise((resolve7) => {
2272
+ return new Promise((resolve8) => {
1973
2273
  const rl = readline.createInterface({
1974
2274
  input: process.stdin,
1975
2275
  output: process.stdout
1976
2276
  });
1977
2277
  rl.question(question, (answer) => {
1978
2278
  rl.close();
1979
- resolve7(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
2279
+ resolve8(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
1980
2280
  });
1981
2281
  });
1982
2282
  }
1983
- function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name) {
1984
- const { bin, args } = buildAgentArgs(agent, prompt2, "execute", sessionId, name);
2283
+ function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name, resumeSession = false, onSpawnError, systemPrompt) {
2284
+ const jobLabel = name ?? "unknown";
2285
+ console.log(`${timestamp()} ${prefix} ${paint("dim", tokenLogLine("agent", jobLabel, prompt2, systemPrompt))}`);
2286
+ const { bin, args } = buildAgentArgs(agent, prompt2, "execute", sessionId, name, resumeSession, systemPrompt);
1985
2287
  const child = spawn4(bin, args, { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] });
1986
2288
  child.on("error", (err) => {
1987
2289
  logError(prefix, `Failed to spawn ${agent}: ${err.message}`);
1988
2290
  if (err.code === "ENOENT") {
1989
2291
  logError(prefix, `Check that "${bin}" is on PATH and "${repoDir}" exists`);
1990
2292
  }
2293
+ onSpawnError?.(err);
1991
2294
  });
1992
2295
  if (agent === "codex") {
1993
2296
  child.stdout?.on("data", () => onActivity?.());
@@ -2027,6 +2330,7 @@ var watchCommand = new Command8("watch").description(
2027
2330
  let pollRunning = false;
2028
2331
  const approvalQueue = [];
2029
2332
  let approvalRunning = false;
2333
+ const agentAvailability = /* @__PURE__ */ new Map();
2030
2334
  const flags = [
2031
2335
  `interval=${paint("cyan", opts.interval + "s")}`,
2032
2336
  `root=${paint("cyan", rootDir)}`,
@@ -2049,6 +2353,22 @@ var watchCommand = new Command8("watch").description(
2049
2353
  console.log(banner);
2050
2354
  console.log(` ${flags}
2051
2355
  `);
2356
+ async function isAgentAvailable(candidate) {
2357
+ const cached = agentAvailability.get(candidate);
2358
+ if (cached !== void 0) {
2359
+ return cached;
2360
+ }
2361
+ const available = await commandExists(candidate);
2362
+ agentAvailability.set(candidate, available);
2363
+ return available;
2364
+ }
2365
+ async function resolveAgentChain(preferred) {
2366
+ const availability = {};
2367
+ for (const candidate of ["claude", "codex", "gemini"]) {
2368
+ availability[candidate] = await isAgentAvailable(candidate);
2369
+ }
2370
+ return getAvailableAgentFallbackChain(preferred, availability);
2371
+ }
2052
2372
  async function moveTaskToError(task, prefix, reason) {
2053
2373
  try {
2054
2374
  await api.patch(`/api/tasks/${task.id}`, { status: "error" });
@@ -2063,12 +2383,13 @@ var watchCommand = new Command8("watch").description(
2063
2383
  async function dispatchTask(task, repoDir) {
2064
2384
  const sid = shortId(task.id);
2065
2385
  const slug = slugify(task.title);
2066
- const branchName = `mr/${sid}/${slug}`;
2386
+ const owner = ownerPrefix(task);
2387
+ const branchName = `${owner}/${slug}`;
2388
+ const legacyBranchName = `mr/${sid}/${slug}`;
2067
2389
  const prefix = taskTag(sid);
2068
2390
  const vcs = detectVcs(repoDir)?.provider ?? "github";
2069
2391
  logDispatch(prefix, `"${paint("bold", task.title)}" ${paint("gray", repoDir)} ${paint("dim", `[${vcs}]`)}`);
2070
2392
  await postTaskUpdate(task.id, `Agent dispatched \u2014 starting work on "${task.title}"`, "system");
2071
- await pullLatestMain(repoDir, prefix);
2072
2393
  let subtasks = [];
2073
2394
  try {
2074
2395
  subtasks = await api.get(`/api/tasks/${task.id}/subtasks`);
@@ -2112,20 +2433,27 @@ var watchCommand = new Command8("watch").description(
2112
2433
  } catch {
2113
2434
  }
2114
2435
  const hasFeedback = feedbackUpdates.length > 0;
2115
- const desiredWorktreePath = hasFeedback ? worktreePath(`${sid}-fb`) : worktreePath(sid);
2436
+ const wtName = `${owner}-${slug}`;
2437
+ const desiredWorktreePath = worktreePath(wtName);
2438
+ const startWithoutWorktree = !hasFeedback && taskLikelyDoesNotNeedCodeChanges(task);
2116
2439
  let executionDir = repoDir;
2117
2440
  let cleanupWorktreePath;
2118
- if (isGitRepo(repoDir)) {
2119
- executionDir = createWorktree(
2441
+ if (startWithoutWorktree) {
2442
+ logInfo(prefix, `Skipping pre-created worktree; task looks like a no-code workflow`);
2443
+ } else if (isGitRepo(repoDir)) {
2444
+ const worktree = createWorktree(
2120
2445
  repoDir,
2121
2446
  branchName,
2122
2447
  worktreeNameFromPath(desiredWorktreePath)
2123
2448
  );
2124
- cleanupWorktreePath = executionDir;
2125
- logInfo(prefix, `Prepared worktree ${paint("cyan", executionDir)}`);
2449
+ executionDir = worktree.path;
2450
+ cleanupWorktreePath = worktree.created ? executionDir : void 0;
2451
+ logInfo(
2452
+ prefix,
2453
+ worktree.reusedBranchWorktree ? `Reusing existing worktree ${paint("cyan", executionDir)} for branch ${paint("dim", branchName)}` : `Prepared worktree ${paint("cyan", executionDir)}`
2454
+ );
2126
2455
  }
2127
- const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs, executionDir);
2128
- const sessionId = agent === "claude" ? randomUUID() : void 0;
2456
+ const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs, executionDir, startWithoutWorktree);
2129
2457
  const activeEntry = {
2130
2458
  process: void 0,
2131
2459
  title: task.title,
@@ -2138,90 +2466,181 @@ var watchCommand = new Command8("watch").description(
2138
2466
  const touchActivity = () => {
2139
2467
  activeEntry.lastActivityAt = Date.now();
2140
2468
  };
2141
- const child = spawnAgent(agent, executionDir, prompt2, prefix, touchActivity, sessionId, task.title);
2142
- activeEntry.process = child;
2143
- if (sessionId) {
2144
- api.patch(`/api/tasks/${task.id}`, { claudeSessionId: sessionId }).catch(() => {
2145
- });
2146
- logInfo(prefix, `Claude session: ${paint("dim", sessionId)}`);
2469
+ const attemptOrder = await resolveAgentChain(agent);
2470
+ if (attemptOrder.length === 0) {
2471
+ logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
2472
+ await moveTaskToError(task, prefix, `No available agent found for fallback chain starting at ${agent}`);
2473
+ if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
2474
+ removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
2475
+ }
2476
+ queued.delete(task.id);
2477
+ return;
2147
2478
  }
2148
- active.set(task.id, activeEntry);
2149
- child.on("exit", async (code) => {
2150
- active.delete(task.id);
2151
- finishing.add(task.id);
2152
- try {
2153
- if (code === 0) {
2154
- try {
2155
- const researchPath = resolve2(executionDir, "research.md");
2156
- if (existsSync7(researchPath)) {
2157
- try {
2158
- const researchContent = readFileSync5(researchPath, "utf-8");
2159
- const existingResearch = existingResources.find((r) => r.type === "research");
2160
- if (existingResearch) {
2161
- await api.patch(`/api/tasks/${task.id}/resources/${existingResearch.id}`, {
2162
- content: researchContent
2163
- });
2164
- logSuccess(prefix, `Updated existing research resource`);
2165
- } else {
2166
- await api.post(`/api/tasks/${task.id}/resources`, {
2167
- type: "research",
2168
- title: `Research \u2014 ${task.title}`,
2169
- content: researchContent
2170
- });
2171
- logSuccess(prefix, `Uploaded research.md as task resource`);
2479
+ let attemptIndex = 0;
2480
+ const launchAttempt = async (attemptAgent) => {
2481
+ let spawnFailureReason = null;
2482
+ const shouldResumeClaudeSession = attemptAgent === "claude" && hasFeedback && !!task.claudeSessionId;
2483
+ const sessionId = attemptAgent === "claude" ? task.claudeSessionId ?? randomUUID() : void 0;
2484
+ const executionSystemPrompt = composeSystemPrompt(EXECUTION_SYSTEM_SECTIONS);
2485
+ const child = spawnAgent(
2486
+ attemptAgent,
2487
+ executionDir,
2488
+ prompt2,
2489
+ prefix,
2490
+ touchActivity,
2491
+ sessionId,
2492
+ task.title,
2493
+ shouldResumeClaudeSession,
2494
+ (err) => {
2495
+ spawnFailureReason = err.message;
2496
+ },
2497
+ executionSystemPrompt
2498
+ );
2499
+ activeEntry.process = child;
2500
+ activeEntry.currentAgent = attemptAgent;
2501
+ active.set(task.id, activeEntry);
2502
+ if (attemptAgent === "claude" && sessionId) {
2503
+ api.patch(`/api/tasks/${task.id}`, { claudeSessionId: sessionId }).catch(() => {
2504
+ });
2505
+ logInfo(
2506
+ prefix,
2507
+ shouldResumeClaudeSession ? `Resuming Claude session: ${paint("dim", sessionId)}` : `Claude session: ${paint("dim", sessionId)}`
2508
+ );
2509
+ } else {
2510
+ api.patch(`/api/tasks/${task.id}`, { claudeSessionId: null }).catch(() => {
2511
+ });
2512
+ }
2513
+ child.on("exit", async (code) => {
2514
+ if (active.get(task.id)?.process === child) {
2515
+ active.delete(task.id);
2516
+ }
2517
+ const failedAttempt = code !== 0 || spawnFailureReason !== null;
2518
+ if (failedAttempt && !activeEntry.terminatedForError) {
2519
+ const nextAgent = attemptOrder[attemptIndex + 1];
2520
+ if (nextAgent) {
2521
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
2522
+ logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying with ${nextAgent}`);
2523
+ await postTaskUpdate(
2524
+ task.id,
2525
+ `${attemptAgent} failed (${failureDetail}) \u2014 retrying with ${nextAgent}`,
2526
+ "system"
2527
+ );
2528
+ attemptIndex += 1;
2529
+ await launchAttempt(nextAgent);
2530
+ return;
2531
+ }
2532
+ }
2533
+ finishing.add(task.id);
2534
+ try {
2535
+ if (code === 0) {
2536
+ try {
2537
+ const researchPath = resolve2(executionDir, "research.md");
2538
+ if (existsSync7(researchPath)) {
2539
+ try {
2540
+ const researchContent = readFileSync5(researchPath, "utf-8");
2541
+ const existingResearch = existingResources.find((r) => r.type === "research");
2542
+ if (existingResearch) {
2543
+ await api.patch(`/api/tasks/${task.id}/resources/${existingResearch.id}`, {
2544
+ content: researchContent
2545
+ });
2546
+ logSuccess(prefix, `Updated existing research resource`);
2547
+ } else {
2548
+ await api.post(`/api/tasks/${task.id}/resources`, {
2549
+ type: "research",
2550
+ title: `Research \u2014 ${task.title}`,
2551
+ content: researchContent
2552
+ });
2553
+ logSuccess(prefix, `Uploaded research.md as task resource`);
2554
+ }
2555
+ unlinkSync(researchPath);
2556
+ } catch (err) {
2557
+ logWarn(prefix, `Failed to upload research resource: ${err.message}`);
2172
2558
  }
2173
- unlinkSync(researchPath);
2174
- } catch (err) {
2175
- logWarn(prefix, `Failed to upload research resource: ${err.message}`);
2176
2559
  }
2177
- }
2178
- const noMrPath = resolve2(executionDir, ".mr-no-mr");
2179
- const noMrRequested = existsSync7(noMrPath);
2180
- let noMrDescription;
2181
- if (noMrRequested) {
2182
- noMrDescription = readFileSync5(noMrPath, "utf-8").trim();
2183
- unlinkSync(noMrPath);
2184
- logSuccess(prefix, `No ${vcs === "gitlab" ? "MR" : "PR"} needed \u2014 ${noMrDescription}`);
2185
- }
2186
- const prLabel = vcs === "gitlab" ? "MR" : "PR";
2187
- let prUrl = null;
2188
- if (!noMrRequested) {
2189
- prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
2190
- if (!prUrl) {
2191
- prUrl = await extractPrUrlFromUpdates(task.id);
2560
+ const noMrPath = resolve2(executionDir, ".mr-no-mr");
2561
+ const noMrRequested = existsSync7(noMrPath);
2562
+ let noMrDescription;
2563
+ if (noMrRequested) {
2564
+ noMrDescription = readFileSync5(noMrPath, "utf-8").trim();
2565
+ unlinkSync(noMrPath);
2566
+ logSuccess(prefix, `No ${vcs === "gitlab" ? "MR" : "PR"} needed \u2014 ${noMrDescription}`);
2567
+ }
2568
+ const prLabel = vcs === "gitlab" ? "MR" : "PR";
2569
+ let prUrl = null;
2570
+ if (!noMrRequested) {
2571
+ prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
2572
+ if (!prUrl) {
2573
+ prUrl = await findPrUrlAcrossRepos(legacyBranchName, repoDir, vcs);
2574
+ }
2575
+ if (!prUrl) {
2576
+ prUrl = await extractPrUrlFromUpdates(task.id);
2577
+ if (prUrl) {
2578
+ logInfo(prefix, `Found ${prLabel} URL from agent updates: ${paint("cyan", prUrl)}`);
2579
+ }
2580
+ }
2581
+ if (!prUrl) {
2582
+ prUrl = await createPrAcrossRepos(
2583
+ task,
2584
+ branchName,
2585
+ repoDir,
2586
+ vcs,
2587
+ subtasks,
2588
+ protoRefs,
2589
+ feedbackUpdates,
2590
+ existingResources,
2591
+ skillRefs
2592
+ );
2593
+ if (!prUrl) {
2594
+ prUrl = await createPrAcrossRepos(
2595
+ task,
2596
+ legacyBranchName,
2597
+ repoDir,
2598
+ vcs,
2599
+ subtasks,
2600
+ protoRefs,
2601
+ feedbackUpdates,
2602
+ existingResources,
2603
+ skillRefs
2604
+ );
2605
+ }
2606
+ if (prUrl) {
2607
+ logInfo(prefix, `Created missing ${prLabel} for branch ${paint("cyan", branchName)}`);
2608
+ await postTaskUpdate(task.id, `Watch created the missing ${prLabel} for branch ${branchName}`, "system");
2609
+ }
2610
+ }
2192
2611
  if (prUrl) {
2193
- logInfo(prefix, `Found ${prLabel} URL from agent updates: ${paint("cyan", prUrl)}`);
2612
+ logSuccess(prefix, `${prLabel} ready: ${paint("cyan", prUrl)}`);
2613
+ } else {
2614
+ logWarn(prefix, `No ${prLabel} found for branch ${paint("cyan", branchName)}`);
2615
+ await postTaskUpdate(task.id, `Agent finished \u2014 no ${prLabel} found for branch ${branchName}`, "system");
2194
2616
  }
2195
2617
  }
2196
- if (prUrl) {
2197
- logSuccess(prefix, `${prLabel} ready: ${paint("cyan", prUrl)}`);
2198
- } else {
2199
- logWarn(prefix, `No ${prLabel} found for branch ${paint("cyan", branchName)}`);
2200
- await postTaskUpdate(task.id, `Agent finished \u2014 no ${prLabel} found for branch ${branchName}`, "system");
2201
- }
2618
+ await api.patch(`/api/tasks/${task.id}`, {
2619
+ status: "review",
2620
+ claudeSessionId: activeEntry.currentAgent === "claude" ? void 0 : null,
2621
+ ...prUrl ? { link: prUrl } : {}
2622
+ });
2623
+ logSuccess(prefix, `"${paint("bold", task.title)}" marked ready for review`);
2624
+ await postTaskUpdate(task.id, noMrRequested ? `Task marked ready for review \u2014 no ${prLabel} needed: ${noMrDescription}` : "Task marked ready for review", "system");
2625
+ } catch (err) {
2626
+ logError(prefix, `Failed to update task: ${err.message}`);
2202
2627
  }
2203
- await api.patch(`/api/tasks/${task.id}`, {
2204
- status: "review",
2205
- ...prUrl ? { link: prUrl } : {}
2206
- });
2207
- logSuccess(prefix, `"${paint("bold", task.title)}" marked ready for review`);
2208
- await postTaskUpdate(task.id, noMrRequested ? `Task marked ready for review \u2014 no ${prLabel} needed: ${noMrDescription}` : "Task marked ready for review", "system");
2209
- } catch (err) {
2210
- logError(prefix, `Failed to update task: ${err.message}`);
2628
+ } else if (!activeEntry.terminatedForError) {
2629
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
2630
+ const reason = `${attemptAgent} failed (${failureDetail}) \u2014 task moved to error`;
2631
+ logError(prefix, `"${paint("bold", task.title)}" failed (${failureDetail}), moving task to error`);
2632
+ await moveTaskToError(task, prefix, reason);
2211
2633
  }
2212
- } else if (!activeEntry.terminatedForError) {
2213
- const reason = `Agent failed with exit code ${code} \u2014 task moved to error`;
2214
- logError(prefix, `"${paint("bold", task.title)}" failed (exit ${code}), moving task to error`);
2215
- await moveTaskToError(task, prefix, reason);
2216
- }
2217
- } finally {
2218
- if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
2219
- removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
2634
+ } finally {
2635
+ if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
2636
+ removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
2637
+ }
2638
+ queued.delete(task.id);
2639
+ finishing.delete(task.id);
2220
2640
  }
2221
- queued.delete(task.id);
2222
- finishing.delete(task.id);
2223
- }
2224
- });
2641
+ });
2642
+ };
2643
+ await launchAttempt(attemptOrder[attemptIndex]);
2225
2644
  }
2226
2645
  async function dispatchPlanModeTask(task, repoDir) {
2227
2646
  const sid = shortId(task.id);
@@ -2252,65 +2671,122 @@ var watchCommand = new Command8("watch").description(
2252
2671
  "system"
2253
2672
  );
2254
2673
  const prompt2 = buildPrdPrompt(task, repoDir, existingPlanResource?.content, feedbackUpdates);
2255
- const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, void 0, task.title);
2256
- active.set(task.id, { process: child, title: task.title, repoDir, startedAt: Date.now() });
2257
- child.on("exit", async (code) => {
2258
- active.delete(task.id);
2259
- finishing.add(task.id);
2260
- try {
2261
- if (code === 0) {
2262
- try {
2263
- const prdPath = resolve2(repoDir, "prd.md");
2264
- let prdContent;
2674
+ const attemptOrder = await resolveAgentChain(agent);
2675
+ if (attemptOrder.length === 0) {
2676
+ logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
2677
+ await api.patch(`/api/tasks/${task.id}`, { status: "error" }).catch((err) => {
2678
+ logError(prefix, `Failed to mark task as error: ${err.message}`);
2679
+ });
2680
+ await postTaskUpdate(task.id, `PRD generation could not start \u2014 no available agent found for fallback chain starting at ${agent}`, "system");
2681
+ queued.delete(task.id);
2682
+ return;
2683
+ }
2684
+ const activeEntry = {
2685
+ process: void 0,
2686
+ title: task.title,
2687
+ repoDir,
2688
+ startedAt: Date.now(),
2689
+ lastActivityAt: Date.now()
2690
+ };
2691
+ let attemptIndex = 0;
2692
+ const launchAttempt = async (attemptAgent) => {
2693
+ let spawnFailureReason = null;
2694
+ const prdSystemPrompt = composeSystemPrompt(PRD_SYSTEM_SECTIONS);
2695
+ const child = spawnAgent(
2696
+ attemptAgent,
2697
+ repoDir,
2698
+ prompt2,
2699
+ prefix,
2700
+ void 0,
2701
+ void 0,
2702
+ task.title,
2703
+ false,
2704
+ (err) => {
2705
+ spawnFailureReason = err.message;
2706
+ },
2707
+ prdSystemPrompt
2708
+ );
2709
+ activeEntry.process = child;
2710
+ activeEntry.currentAgent = attemptAgent;
2711
+ active.set(task.id, activeEntry);
2712
+ child.on("exit", async (code) => {
2713
+ if (active.get(task.id)?.process === child) {
2714
+ active.delete(task.id);
2715
+ }
2716
+ const failedAttempt = code !== 0 || spawnFailureReason !== null;
2717
+ if (failedAttempt) {
2718
+ const nextAgent = attemptOrder[attemptIndex + 1];
2719
+ if (nextAgent) {
2720
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
2721
+ logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying PRD generation with ${nextAgent}`);
2722
+ await postTaskUpdate(
2723
+ task.id,
2724
+ `${attemptAgent} failed (${failureDetail}) \u2014 retrying PRD generation with ${nextAgent}`,
2725
+ "system"
2726
+ );
2727
+ attemptIndex += 1;
2728
+ await launchAttempt(nextAgent);
2729
+ return;
2730
+ }
2731
+ }
2732
+ finishing.add(task.id);
2733
+ try {
2734
+ if (code === 0) {
2265
2735
  try {
2266
- prdContent = readFileSync5(prdPath, "utf-8");
2267
- logInfo(prefix, `Read PRD from ${paint("cyan", "prd.md")} (${prdContent.length} chars)`);
2268
- unlinkSync(prdPath);
2269
- } catch {
2270
- logWarn(prefix, `No prd.md file found in ${repoDir} \u2014 PRD may have been posted inline`);
2271
- }
2272
- if (prdContent) {
2736
+ const prdPath = resolve2(repoDir, "prd.md");
2737
+ let prdContent;
2273
2738
  try {
2274
- if (existingPlanResource) {
2275
- await api.patch(`/api/tasks/${task.id}/resources/${existingPlanResource.id}`, {
2276
- content: prdContent
2277
- });
2278
- logSuccess(prefix, `Updated existing PRD resource`);
2279
- } else {
2280
- await api.post(`/api/tasks/${task.id}/resources`, {
2281
- type: "plan",
2282
- title: `PRD \u2014 ${task.title}`,
2283
- content: prdContent
2284
- });
2285
- logSuccess(prefix, `Uploaded PRD as task resource`);
2739
+ prdContent = readFileSync5(prdPath, "utf-8");
2740
+ logInfo(prefix, `Read PRD from ${paint("cyan", "prd.md")} (${prdContent.length} chars)`);
2741
+ unlinkSync(prdPath);
2742
+ } catch {
2743
+ logWarn(prefix, `No prd.md file found in ${repoDir} \u2014 PRD may have been posted inline`);
2744
+ }
2745
+ if (prdContent) {
2746
+ try {
2747
+ if (existingPlanResource) {
2748
+ await api.patch(`/api/tasks/${task.id}/resources/${existingPlanResource.id}`, {
2749
+ content: prdContent
2750
+ });
2751
+ logSuccess(prefix, `Updated existing PRD resource`);
2752
+ } else {
2753
+ await api.post(`/api/tasks/${task.id}/resources`, {
2754
+ type: "plan",
2755
+ title: `PRD \u2014 ${task.title}`,
2756
+ content: prdContent
2757
+ });
2758
+ logSuccess(prefix, `Uploaded PRD as task resource`);
2759
+ }
2760
+ } catch (err) {
2761
+ logWarn(prefix, `Failed to upload PRD resource: ${err.message}`);
2286
2762
  }
2287
- } catch (err) {
2288
- logWarn(prefix, `Failed to upload PRD resource: ${err.message}`);
2289
2763
  }
2764
+ await api.patch(`/api/tasks/${task.id}`, {
2765
+ status: "review",
2766
+ ...prdContent ? { prdContent } : {}
2767
+ });
2768
+ logSuccess(prefix, `"${paint("bold", task.title)}" PRD generated and marked ready for review`);
2769
+ await postTaskUpdate(task.id, "PRD generated \u2014 ready for review", "system");
2770
+ } catch (err) {
2771
+ logError(prefix, `Failed to update task: ${err.message}`);
2290
2772
  }
2291
- await api.patch(`/api/tasks/${task.id}`, {
2292
- status: "review",
2293
- ...prdContent ? { prdContent } : {}
2294
- });
2295
- logSuccess(prefix, `"${paint("bold", task.title)}" PRD generated and marked ready for review`);
2296
- await postTaskUpdate(task.id, "PRD generated \u2014 ready for review", "system");
2297
- } catch (err) {
2298
- logError(prefix, `Failed to update task: ${err.message}`);
2299
- }
2300
- } else {
2301
- logError(prefix, `"${paint("bold", task.title)}" PRD generation failed (exit ${code}), marked as error`);
2302
- try {
2303
- await api.patch(`/api/tasks/${task.id}`, { status: "error" });
2304
- } catch (err) {
2305
- logError(prefix, `Failed to mark task as error: ${err.message}`);
2773
+ } else {
2774
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
2775
+ logError(prefix, `"${paint("bold", task.title)}" PRD generation failed (${failureDetail}), marked as error`);
2776
+ try {
2777
+ await api.patch(`/api/tasks/${task.id}`, { status: "error" });
2778
+ } catch (err) {
2779
+ logError(prefix, `Failed to mark task as error: ${err.message}`);
2780
+ }
2781
+ await postTaskUpdate(task.id, `PRD generation failed via ${attemptAgent} (${failureDetail}) \u2014 task moved to error`, "system");
2306
2782
  }
2307
- await postTaskUpdate(task.id, `PRD generation failed with exit code ${code} \u2014 task moved to error`, "system");
2783
+ } finally {
2784
+ queued.delete(task.id);
2785
+ finishing.delete(task.id);
2308
2786
  }
2309
- } finally {
2310
- queued.delete(task.id);
2311
- finishing.delete(task.id);
2312
- }
2313
- });
2787
+ });
2788
+ };
2789
+ await launchAttempt(attemptOrder[attemptIndex]);
2314
2790
  }
2315
2791
  async function dispatchPrototypeJob(proto, repoDir) {
2316
2792
  const sid = shortId(proto.id);
@@ -2336,84 +2812,186 @@ var watchCommand = new Command8("watch").description(
2336
2812
  } else {
2337
2813
  prompt2 = buildPrototypePrompt(proto, repoDir);
2338
2814
  }
2339
- const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, proto.title);
2340
- active.set(`proto-${proto.id}`, { process: child, title: proto.title, repoDir, startedAt: Date.now() });
2341
- child.on("exit", async (code) => {
2342
- const key = `proto-${proto.id}`;
2343
- active.delete(key);
2344
- finishing.add(key);
2345
- try {
2346
- if (code === 0) {
2347
- try {
2348
- const protoPattern = /^prototype-\d+\.html$/;
2349
- const found = readdirSync(repoDir).filter((f) => protoPattern.test(f)).sort();
2350
- const files = found.map((f) => ({
2351
- name: f,
2352
- content: readFileSync5(resolve2(repoDir, f), "utf-8")
2353
- }));
2354
- if (files.length === 0) {
2355
- logError(prefix, `No prototype HTML files found in ${repoDir}`);
2356
- await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
2357
- return;
2358
- }
2359
- await api.patch(`/api/prototypes/${proto.id}`, { status: "completed", files });
2360
- logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${files.length} file(s)`);
2361
- for (const file of files) {
2815
+ const key = `proto-${proto.id}`;
2816
+ const attemptOrder = await resolveAgentChain(agent);
2817
+ if (attemptOrder.length === 0) {
2818
+ logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
2819
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" }).catch(() => {
2820
+ });
2821
+ queued.delete(key);
2822
+ return;
2823
+ }
2824
+ const activeEntry = {
2825
+ process: void 0,
2826
+ title: proto.title,
2827
+ repoDir,
2828
+ startedAt: Date.now(),
2829
+ lastActivityAt: Date.now()
2830
+ };
2831
+ let attemptIndex = 0;
2832
+ const launchAttempt = async (attemptAgent) => {
2833
+ let spawnFailureReason = null;
2834
+ const child = spawnAgent(
2835
+ attemptAgent,
2836
+ repoDir,
2837
+ prompt2,
2838
+ prefix,
2839
+ void 0,
2840
+ void 0,
2841
+ proto.title,
2842
+ false,
2843
+ (err) => {
2844
+ spawnFailureReason = err.message;
2845
+ }
2846
+ );
2847
+ activeEntry.process = child;
2848
+ activeEntry.currentAgent = attemptAgent;
2849
+ active.set(key, activeEntry);
2850
+ child.on("exit", async (code) => {
2851
+ if (active.get(key)?.process === child) {
2852
+ active.delete(key);
2853
+ }
2854
+ const failedAttempt = code !== 0 || spawnFailureReason !== null;
2855
+ if (failedAttempt) {
2856
+ const nextAgent = attemptOrder[attemptIndex + 1];
2857
+ if (nextAgent) {
2858
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
2859
+ logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying prototype generation with ${nextAgent}`);
2860
+ attemptIndex += 1;
2861
+ await launchAttempt(nextAgent);
2862
+ return;
2863
+ }
2864
+ }
2865
+ finishing.add(key);
2866
+ try {
2867
+ if (code === 0) {
2868
+ try {
2869
+ const protoPattern = /^prototype-\d+\.html$/;
2870
+ const found = readdirSync(repoDir).filter((f) => protoPattern.test(f)).sort();
2871
+ const files = found.map((f) => ({
2872
+ name: f,
2873
+ content: readFileSync5(resolve2(repoDir, f), "utf-8")
2874
+ }));
2875
+ if (files.length === 0) {
2876
+ logError(prefix, `No prototype HTML files found in ${repoDir}`);
2877
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
2878
+ return;
2879
+ }
2880
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "completed", files });
2881
+ logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${files.length} file(s)`);
2882
+ for (const file of files) {
2883
+ try {
2884
+ unlinkSync(resolve2(repoDir, file.name));
2885
+ } catch {
2886
+ }
2887
+ }
2888
+ } catch (err) {
2889
+ logError(prefix, `Failed to upload prototype: ${err.message}`);
2362
2890
  try {
2363
- unlinkSync(resolve2(repoDir, file.name));
2891
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
2364
2892
  } catch {
2365
2893
  }
2366
2894
  }
2367
- } catch (err) {
2368
- logError(prefix, `Failed to upload prototype: ${err.message}`);
2895
+ } else {
2896
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
2897
+ logError(prefix, `"${paint("bold", proto.title)}" prototype failed via ${attemptAgent} (${failureDetail})`);
2898
+ try {
2899
+ await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
2900
+ } catch {
2901
+ }
2902
+ }
2903
+ } finally {
2904
+ queued.delete(key);
2905
+ finishing.delete(key);
2906
+ }
2907
+ });
2908
+ };
2909
+ await launchAttempt(attemptOrder[attemptIndex]);
2910
+ }
2911
+ async function dispatchRepoCreation(project, workDir) {
2912
+ const sid = shortId(project.id);
2913
+ const prefix = repoTag(sid);
2914
+ logDispatch(prefix, `"${paint("bold", project.name)}" ${paint("gray", workDir)}`);
2915
+ try {
2916
+ await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "creating" });
2917
+ } catch {
2918
+ }
2919
+ const prompt2 = buildRepoCreationPrompt(project, workDir);
2920
+ const key = `repo-${project.id}`;
2921
+ const attemptOrder = await resolveAgentChain(agent);
2922
+ if (attemptOrder.length === 0) {
2923
+ logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
2924
+ queued.delete(key);
2925
+ try {
2926
+ await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "failed" });
2927
+ } catch {
2928
+ }
2929
+ return;
2930
+ }
2931
+ const activeEntry = {
2932
+ process: void 0,
2933
+ title: project.name,
2934
+ repoDir: workDir,
2935
+ startedAt: Date.now(),
2936
+ lastActivityAt: Date.now()
2937
+ };
2938
+ let attemptIndex = 0;
2939
+ const launchAttempt = async (attemptAgent) => {
2940
+ let spawnFailureReason = null;
2941
+ const child = spawnAgent(
2942
+ attemptAgent,
2943
+ workDir,
2944
+ prompt2,
2945
+ prefix,
2946
+ void 0,
2947
+ void 0,
2948
+ project.name,
2949
+ false,
2950
+ (err) => {
2951
+ spawnFailureReason = err.message;
2952
+ }
2953
+ );
2954
+ activeEntry.process = child;
2955
+ activeEntry.currentAgent = attemptAgent;
2956
+ active.set(key, activeEntry);
2957
+ child.on("exit", async (code) => {
2958
+ if (active.get(key)?.process === child) {
2959
+ active.delete(key);
2960
+ }
2961
+ const failedAttempt = code !== 0 || spawnFailureReason !== null;
2962
+ if (failedAttempt) {
2963
+ const nextAgent = attemptOrder[attemptIndex + 1];
2964
+ if (nextAgent) {
2965
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
2966
+ logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying repo creation with ${nextAgent}`);
2967
+ attemptIndex += 1;
2968
+ await launchAttempt(nextAgent);
2969
+ return;
2970
+ }
2971
+ }
2972
+ finishing.add(key);
2973
+ try {
2974
+ if (code === 0) {
2975
+ logSuccess(prefix, `"${paint("bold", project.name)}" repo creation complete`);
2976
+ try {
2977
+ await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "created" });
2978
+ } catch {
2979
+ }
2980
+ } else {
2981
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
2982
+ logError(prefix, `"${paint("bold", project.name)}" repo creation failed via ${attemptAgent} (${failureDetail})`);
2369
2983
  try {
2370
- await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
2984
+ await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "failed" });
2371
2985
  } catch {
2372
2986
  }
2373
2987
  }
2374
- } else {
2375
- logError(prefix, `"${paint("bold", proto.title)}" prototype failed (exit ${code})`);
2376
- try {
2377
- await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
2378
- } catch {
2379
- }
2380
- }
2381
- } finally {
2382
- queued.delete(key);
2383
- finishing.delete(key);
2384
- }
2385
- });
2386
- }
2387
- async function dispatchRepoCreation(project, workDir) {
2388
- const sid = shortId(project.id);
2389
- const prefix = repoTag(sid);
2390
- logDispatch(prefix, `"${paint("bold", project.name)}" ${paint("gray", workDir)}`);
2391
- try {
2392
- await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "creating" });
2393
- } catch {
2394
- }
2395
- const prompt2 = buildRepoCreationPrompt(project, workDir);
2396
- const child = spawnAgent(agent, workDir, prompt2, prefix, void 0, project.name);
2397
- active.set(`repo-${project.id}`, { process: child, title: project.name, repoDir: workDir, startedAt: Date.now() });
2398
- child.on("exit", async (code) => {
2399
- const key = `repo-${project.id}`;
2400
- active.delete(key);
2401
- finishing.add(key);
2402
- try {
2403
- if (code === 0) {
2404
- logSuccess(prefix, `"${paint("bold", project.name)}" repo creation complete`);
2405
- } else {
2406
- logError(prefix, `"${paint("bold", project.name)}" repo creation failed (exit ${code})`);
2407
- try {
2408
- await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "failed" });
2409
- } catch {
2410
- }
2988
+ } finally {
2989
+ queued.delete(key);
2990
+ finishing.delete(key);
2411
2991
  }
2412
- } finally {
2413
- queued.delete(key);
2414
- finishing.delete(key);
2415
- }
2416
- });
2992
+ });
2993
+ };
2994
+ await launchAttempt(attemptOrder[attemptIndex]);
2417
2995
  }
2418
2996
  async function dispatchIdeaJob(idea, repoDir) {
2419
2997
  const sid = shortId(idea.id);
@@ -2426,96 +3004,146 @@ var watchCommand = new Command8("watch").description(
2426
3004
  }
2427
3005
  }
2428
3006
  const prompt2 = buildIdeaPrompt(idea, repoDir);
2429
- const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, idea.title);
2430
- active.set(`idea-${idea.id}`, { process: child, title: idea.title, repoDir, startedAt: Date.now() });
2431
- child.on("exit", async (code) => {
2432
- const key = `idea-${idea.id}`;
2433
- active.delete(key);
2434
- finishing.add(key);
3007
+ const key = `idea-${idea.id}`;
3008
+ const attemptOrder = await resolveAgentChain(agent);
3009
+ if (attemptOrder.length === 0) {
3010
+ logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
3011
+ queued.delete(key);
2435
3012
  try {
2436
- if (code === 0) {
2437
- try {
2438
- let plan;
2439
- let protoHtml;
2440
- let followUpTasks;
2441
- const planPath = resolve2(repoDir, "idea-plan.md");
2442
- const tasksPath = resolve2(repoDir, "idea-tasks.json");
2443
- const protoPath = resolve2(repoDir, "idea-prototype.html");
2444
- try {
2445
- plan = readFileSync5(planPath, "utf-8");
2446
- } catch {
2447
- }
2448
- try {
2449
- protoHtml = readFileSync5(protoPath, "utf-8");
2450
- } catch {
2451
- }
3013
+ await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
3014
+ } catch {
3015
+ }
3016
+ return;
3017
+ }
3018
+ const activeEntry = {
3019
+ process: void 0,
3020
+ title: idea.title,
3021
+ repoDir,
3022
+ startedAt: Date.now(),
3023
+ lastActivityAt: Date.now()
3024
+ };
3025
+ let attemptIndex = 0;
3026
+ const launchAttempt = async (attemptAgent) => {
3027
+ let spawnFailureReason = null;
3028
+ const child = spawnAgent(
3029
+ attemptAgent,
3030
+ repoDir,
3031
+ prompt2,
3032
+ prefix,
3033
+ void 0,
3034
+ void 0,
3035
+ idea.title,
3036
+ false,
3037
+ (err) => {
3038
+ spawnFailureReason = err.message;
3039
+ }
3040
+ );
3041
+ activeEntry.process = child;
3042
+ activeEntry.currentAgent = attemptAgent;
3043
+ active.set(key, activeEntry);
3044
+ child.on("exit", async (code) => {
3045
+ if (active.get(key)?.process === child) {
3046
+ active.delete(key);
3047
+ }
3048
+ const failedAttempt = code !== 0 || spawnFailureReason !== null;
3049
+ if (failedAttempt) {
3050
+ const nextAgent = attemptOrder[attemptIndex + 1];
3051
+ if (nextAgent) {
3052
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
3053
+ logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying idea generation with ${nextAgent}`);
3054
+ attemptIndex += 1;
3055
+ await launchAttempt(nextAgent);
3056
+ return;
3057
+ }
3058
+ }
3059
+ finishing.add(key);
3060
+ try {
3061
+ if (code === 0) {
2452
3062
  try {
2453
- const tasksRaw = readFileSync5(tasksPath, "utf-8");
2454
- const parsed = JSON.parse(tasksRaw);
2455
- if (Array.isArray(parsed)) {
2456
- followUpTasks = parsed.filter((t) => t && typeof t === "object" && "title" in t);
3063
+ let plan;
3064
+ let protoHtml;
3065
+ let followUpTasks;
3066
+ const planPath = resolve2(repoDir, "idea-plan.md");
3067
+ const tasksPath = resolve2(repoDir, "idea-tasks.json");
3068
+ const protoPath = resolve2(repoDir, "idea-prototype.html");
3069
+ try {
3070
+ plan = readFileSync5(planPath, "utf-8");
3071
+ } catch {
2457
3072
  }
2458
- } catch {
2459
- }
2460
- if (!plan && !protoHtml) {
2461
- logError(prefix, `No output files found in ${repoDir}`);
2462
- await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
2463
- return;
2464
- }
2465
- const updateData = { status: "generated" };
2466
- if (plan) updateData.plan = plan;
2467
- if (followUpTasks) updateData.followUpTasks = followUpTasks;
2468
- if (protoHtml) {
2469
3073
  try {
2470
- const proto = await api.post("/api/prototypes", {
2471
- title: `${idea.title} \u2014 Idea Prototype`,
2472
- prompt: idea.description || idea.title,
2473
- variantCount: 1,
2474
- projectId: idea.projectId ?? null
2475
- });
2476
- await api.patch(`/api/prototypes/${proto.id}`, {
2477
- status: "completed",
2478
- files: [{ name: "idea-prototype.html", content: protoHtml }]
2479
- });
2480
- updateData.generatedPrototypeId = proto.id;
2481
- logSuccess(prefix, `Prototype created: ${paint("gray", proto.id.slice(0, 8))}`);
2482
- } catch (err) {
2483
- logError(prefix, `Failed to create prototype: ${err.message}`);
3074
+ protoHtml = readFileSync5(protoPath, "utf-8");
3075
+ } catch {
3076
+ }
3077
+ try {
3078
+ const tasksRaw = readFileSync5(tasksPath, "utf-8");
3079
+ const parsed = JSON.parse(tasksRaw);
3080
+ if (Array.isArray(parsed)) {
3081
+ followUpTasks = parsed.filter((t) => t && typeof t === "object" && "title" in t);
3082
+ }
3083
+ } catch {
3084
+ }
3085
+ if (!plan && !protoHtml) {
3086
+ logError(prefix, `No output files found in ${repoDir}`);
3087
+ await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
3088
+ return;
3089
+ }
3090
+ const updateData = { status: "generated" };
3091
+ if (plan) updateData.plan = plan;
3092
+ if (followUpTasks) updateData.followUpTasks = followUpTasks;
3093
+ if (protoHtml) {
3094
+ try {
3095
+ const proto = await api.post("/api/prototypes", {
3096
+ title: `${idea.title} \u2014 Idea Prototype`,
3097
+ prompt: idea.description || idea.title,
3098
+ variantCount: 1,
3099
+ projectId: idea.projectId ?? null
3100
+ });
3101
+ await api.patch(`/api/prototypes/${proto.id}`, {
3102
+ status: "completed",
3103
+ files: [{ name: "idea-prototype.html", content: protoHtml }]
3104
+ });
3105
+ updateData.generatedPrototypeId = proto.id;
3106
+ logSuccess(prefix, `Prototype created: ${paint("gray", proto.id.slice(0, 8))}`);
3107
+ } catch (err) {
3108
+ logError(prefix, `Failed to create prototype: ${err.message}`);
3109
+ }
3110
+ }
3111
+ await api.patch(`/api/ideas/${idea.id}`, updateData);
3112
+ logSuccess(prefix, `"${paint("bold", idea.title)}" generation complete`);
3113
+ try {
3114
+ unlinkSync(planPath);
3115
+ } catch {
3116
+ }
3117
+ try {
3118
+ unlinkSync(tasksPath);
3119
+ } catch {
3120
+ }
3121
+ try {
3122
+ unlinkSync(protoPath);
3123
+ } catch {
3124
+ }
3125
+ } catch (err) {
3126
+ logError(prefix, `Failed to upload idea output: ${err.message}`);
3127
+ try {
3128
+ await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
3129
+ } catch {
2484
3130
  }
2485
3131
  }
2486
- await api.patch(`/api/ideas/${idea.id}`, updateData);
2487
- logSuccess(prefix, `"${paint("bold", idea.title)}" generation complete`);
2488
- try {
2489
- unlinkSync(planPath);
2490
- } catch {
2491
- }
2492
- try {
2493
- unlinkSync(tasksPath);
2494
- } catch {
2495
- }
2496
- try {
2497
- unlinkSync(protoPath);
2498
- } catch {
2499
- }
2500
- } catch (err) {
2501
- logError(prefix, `Failed to upload idea output: ${err.message}`);
3132
+ } else {
3133
+ const failureDetail = spawnFailureReason ?? `exit code ${code}`;
3134
+ logError(prefix, `"${paint("bold", idea.title)}" generation failed via ${attemptAgent} (${failureDetail})`);
2502
3135
  try {
2503
3136
  await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
2504
3137
  } catch {
2505
3138
  }
2506
3139
  }
2507
- } else {
2508
- logError(prefix, `"${paint("bold", idea.title)}" generation failed (exit ${code})`);
2509
- try {
2510
- await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
2511
- } catch {
2512
- }
3140
+ } finally {
3141
+ queued.delete(key);
3142
+ finishing.delete(key);
2513
3143
  }
2514
- } finally {
2515
- queued.delete(key);
2516
- finishing.delete(key);
2517
- }
2518
- });
3144
+ });
3145
+ };
3146
+ await launchAttempt(attemptOrder[attemptIndex]);
2519
3147
  }
2520
3148
  function dispatchScan(scan, prefix, key) {
2521
3149
  logDispatch(prefix, `Running scan for project ${paint("cyan", scan.projectId.slice(0, 8))}`);
@@ -2529,7 +3157,13 @@ var watchCommand = new Command8("watch").description(
2529
3157
  queued.delete(key);
2530
3158
  failed.set(key, err.message);
2531
3159
  });
2532
- active.set(key, { process: scanProc, title: `scan-${scan.id.slice(0, 8)}`, repoDir: rootDir, startedAt: Date.now() });
3160
+ active.set(key, {
3161
+ process: scanProc,
3162
+ title: `scan-${scan.id.slice(0, 8)}`,
3163
+ repoDir: rootDir,
3164
+ startedAt: Date.now(),
3165
+ lastActivityAt: Date.now()
3166
+ });
2533
3167
  scanProc.stdout?.on("data", (d) => {
2534
3168
  const lines = d.toString().trim().split("\n");
2535
3169
  for (const line of lines) {
@@ -2561,7 +3195,28 @@ var watchCommand = new Command8("watch").description(
2561
3195
  const prefix = taskTag(sid);
2562
3196
  try {
2563
3197
  logSpinner(prefix, `Generating plan for "${paint("bold", task.title)}"\u2026`);
2564
- const plan = await runPlanningPhase(task, repoDir, agent);
3198
+ const attemptOrder = await resolveAgentChain(agent);
3199
+ if (attemptOrder.length === 0) {
3200
+ throw new Error(`No available agent found for fallback chain starting at ${agent}`);
3201
+ }
3202
+ let plan;
3203
+ let lastError;
3204
+ for (let idx = 0; idx < attemptOrder.length; idx += 1) {
3205
+ const attemptAgent = attemptOrder[idx];
3206
+ try {
3207
+ plan = await runPlanningPhase(task, repoDir, attemptAgent);
3208
+ break;
3209
+ } catch (err) {
3210
+ lastError = err;
3211
+ const nextAgent = attemptOrder[idx + 1];
3212
+ if (nextAgent) {
3213
+ logWarn(prefix, `${attemptAgent} planning failed (${lastError.message}) \u2014 retrying with ${nextAgent}`);
3214
+ }
3215
+ }
3216
+ }
3217
+ if (!plan) {
3218
+ throw lastError ?? new Error("Planning failed");
3219
+ }
2565
3220
  const width = 64;
2566
3221
  const divider = paint("gray", "\u2500".repeat(width));
2567
3222
  const header = `${paint("bold", "Plan")} "${paint("bold", task.title)}" ${taskTag(sid)}`;
@@ -2754,14 +3409,16 @@ ${divider}`);
2754
3409
  if (failed.has(key)) continue;
2755
3410
  const sid = shortId(proto.id);
2756
3411
  const prefix = protoTag(sid);
2757
- if (!proto.projectId) {
2758
- logWarn(prefix, `"${proto.title}": no projectId \u2014 skipping`);
2759
- continue;
2760
- }
2761
- const repoDir = findDirectoryForProject(config, proto.projectId, rootDir);
2762
- if (!repoDir) {
2763
- logWarn(prefix, `"${proto.title}": no linked directory found \u2014 skipping`);
2764
- continue;
3412
+ let repoDir;
3413
+ if (proto.projectId) {
3414
+ const dir = findDirectoryForProject(config, proto.projectId, rootDir);
3415
+ if (!dir) {
3416
+ logWarn(prefix, `"${proto.title}": no linked directory found \u2014 skipping`);
3417
+ continue;
3418
+ }
3419
+ repoDir = dir;
3420
+ } else {
3421
+ repoDir = rootDir;
2765
3422
  }
2766
3423
  if (!existsSync7(repoDir)) {
2767
3424
  logError(prefix, `"${proto.title}": linked directory "${repoDir}" does not exist \u2014 skipping`);
@@ -2779,16 +3436,25 @@ ${divider}`);
2779
3436
  }
2780
3437
  dispatchPrototypeJob(proto, repoDir);
2781
3438
  }
3439
+ const MAX_CONCURRENT_TESTS = 2;
2782
3440
  const testTasks = queuedTasks.filter((t) => t.mode === "testing");
2783
3441
  for (const task of testTasks) {
2784
3442
  const key = `test-${task.id}`;
2785
3443
  if (queued.has(key)) continue;
2786
3444
  if (finishing.has(key)) continue;
2787
3445
  if (failed.has(key)) continue;
3446
+ const activeTests = [...queued].filter((k) => k.startsWith("test-")).length;
3447
+ if (activeTests >= MAX_CONCURRENT_TESTS) break;
2788
3448
  const sid = shortId(task.id);
2789
3449
  const prefix = testTag(sid);
2790
3450
  if (!task.link) {
2791
3451
  logWarn(prefix, `"${task.title}": no MR/PR link \u2014 skipping`);
3452
+ failed.set(key, "no MR/PR link");
3453
+ try {
3454
+ await api.patch(`/api/tasks/${task.id}`, { status: "error" });
3455
+ await postTaskUpdate(task.id, "Test skipped: task has no MR/PR link", "system");
3456
+ } catch {
3457
+ }
2792
3458
  continue;
2793
3459
  }
2794
3460
  queued.add(key);
@@ -2835,9 +3501,9 @@ ${divider}`);
2835
3501
  browseRunner: runBrowseCommand2,
2836
3502
  uploadScreenshot: async (screenshotPath, message) => {
2837
3503
  try {
2838
- const { readFileSync: readFileSync12 } = await import("fs");
3504
+ const { readFileSync: readFileSync13 } = await import("fs");
2839
3505
  const cfg = loadConfig();
2840
- const imageBuffer = readFileSync12(screenshotPath);
3506
+ const imageBuffer = readFileSync13(screenshotPath);
2841
3507
  const fileName = screenshotPath.split("/").pop() || "test-screenshot.png";
2842
3508
  const formData = new FormData();
2843
3509
  const blob = new Blob([imageBuffer], { type: "image/png" });
@@ -2977,6 +3643,55 @@ ${divider}`);
2977
3643
  }
2978
3644
  dispatchScan(scan, prefix, key);
2979
3645
  }
3646
+ let reviewTasks = [];
3647
+ try {
3648
+ reviewTasks = await api.get("/api/tasks?status=review");
3649
+ } catch (err) {
3650
+ logError(watchTag(), `Failed to fetch review tasks: ${err.message}`);
3651
+ }
3652
+ for (const task of reviewTasks) {
3653
+ if (!task.link) continue;
3654
+ if (queued.has(task.id) || finishing.has(task.id) || active.has(task.id)) continue;
3655
+ const sid = shortId(task.id);
3656
+ const prefix = taskTag(sid);
3657
+ const prLabel = task.link.includes("gitlab") ? "MR" : "PR";
3658
+ const vcs = task.link.includes("gitlab") ? "gitlab" : "github";
3659
+ const repoDir = findDirectoryForProject(config, task.projectId, rootDir);
3660
+ if (!repoDir) continue;
3661
+ const status = await checkPrStatus(task.link, repoDir, vcs);
3662
+ if (!status) continue;
3663
+ if (status.merged) {
3664
+ logSuccess(prefix, `${prLabel} merged \u2014 auto-completing "${paint("bold", task.title)}"`);
3665
+ try {
3666
+ await api.patch(`/api/tasks/${task.id}`, {
3667
+ status: "completed",
3668
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
3669
+ inProgressSince: null
3670
+ });
3671
+ await postTaskUpdate(task.id, `${prLabel} merged \u2014 task automatically completed`, "system");
3672
+ } catch (err) {
3673
+ logError(prefix, `Failed to auto-complete task: ${err.message}`);
3674
+ }
3675
+ continue;
3676
+ }
3677
+ if (status.hasConflicts) {
3678
+ logWarn(prefix, `${prLabel} has merge conflicts \u2014 re-dispatching agent for "${paint("bold", task.title)}"`);
3679
+ try {
3680
+ await api.patch(`/api/tasks/${task.id}`, { status: "queued" });
3681
+ await api.post(`/api/tasks/${task.id}/updates`, {
3682
+ message: `Your ${prLabel} has merge conflicts with the base branch. Please resolve the conflicts by rebasing or merging main into your branch, then push the updated branch.`,
3683
+ source: "user"
3684
+ });
3685
+ await postTaskUpdate(
3686
+ task.id,
3687
+ `${prLabel} has merge conflicts \u2014 re-dispatching agent to resolve`,
3688
+ "system"
3689
+ );
3690
+ } catch (err) {
3691
+ logError(prefix, `Failed to re-queue task for conflict resolution: ${err.message}`);
3692
+ }
3693
+ }
3694
+ }
2980
3695
  } finally {
2981
3696
  pollRunning = false;
2982
3697
  }
@@ -3214,22 +3929,21 @@ var prototypeCommand = new Command13("prototype").description("Manage prototypes
3214
3929
  }
3215
3930
  })
3216
3931
  ).addCommand(
3217
- new Command13("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)").option("--variants <count>", "Number of variants to generate (1-50)", "5").action(async (title, opts) => {
3932
+ new Command13("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").action(async (title, opts) => {
3218
3933
  const projectId = opts.project ?? getLinkedProjectId();
3219
- if (!projectId) {
3220
- console.error('No project linked. Run "mr link <project-id>" or use --project.');
3221
- process.exit(1);
3222
- }
3223
3934
  const variantCount = Math.max(1, Math.min(50, parseInt(opts.variants, 10) || 5));
3224
3935
  const prototype = await api.post("/api/prototypes", {
3225
3936
  title,
3226
3937
  prompt: opts.prompt,
3227
3938
  variantCount,
3228
- projectId
3939
+ projectId: projectId ?? null
3229
3940
  });
3230
3941
  console.log();
3231
3942
  console.log(` ${paint4("green", "\u2713")} Created prototype: ${paint4("bold", prototype.title)}`);
3232
3943
  console.log(` ${paint4("gray", "ID:")} ${prototype.id}`);
3944
+ if (!prototype.projectId) {
3945
+ console.log(` ${paint4("gray", "Project:")} none (will generate in the active watch directory)`);
3946
+ }
3233
3947
  console.log(` ${paint4("cyan", "\u27F3")} Generation will begin automatically via the watch agent.`);
3234
3948
  console.log();
3235
3949
  })
@@ -3272,20 +3986,20 @@ var c5 = {
3272
3986
  function paint5(color, text) {
3273
3987
  return `${c5[color]}${text}${c5.reset}`;
3274
3988
  }
3275
- function commandExists(cmd) {
3276
- return new Promise((resolve7) => {
3277
- exec2(`which ${cmd}`, (err) => resolve7(!err));
3989
+ function commandExists2(cmd) {
3990
+ return new Promise((resolve8) => {
3991
+ exec2(`which ${cmd}`, (err) => resolve8(!err));
3278
3992
  });
3279
3993
  }
3280
3994
  function execQuiet(cmd) {
3281
- return new Promise((resolve7) => {
3995
+ return new Promise((resolve8) => {
3282
3996
  exec2(cmd, (err, stdout, stderr) => {
3283
- resolve7({ ok: !err, stdout: stdout.trim(), stderr: stderr.trim() });
3997
+ resolve8({ ok: !err, stdout: stdout.trim(), stderr: stderr.trim() });
3284
3998
  });
3285
3999
  });
3286
4000
  }
3287
4001
  async function checkGitInstalled() {
3288
- const exists = await commandExists("git");
4002
+ const exists = await commandExists2("git");
3289
4003
  if (!exists) {
3290
4004
  return {
3291
4005
  name: "Git",
@@ -3325,7 +4039,7 @@ async function checkNodeVersion() {
3325
4039
  };
3326
4040
  }
3327
4041
  async function checkGhInstalled() {
3328
- const exists = await commandExists("gh");
4042
+ const exists = await commandExists2("gh");
3329
4043
  return {
3330
4044
  name: "GitHub CLI (gh)",
3331
4045
  ok: exists,
@@ -3334,7 +4048,7 @@ async function checkGhInstalled() {
3334
4048
  };
3335
4049
  }
3336
4050
  async function checkGhAuth() {
3337
- const exists = await commandExists("gh");
4051
+ const exists = await commandExists2("gh");
3338
4052
  if (!exists) {
3339
4053
  return {
3340
4054
  name: "GitHub CLI auth",
@@ -3352,7 +4066,7 @@ async function checkGhAuth() {
3352
4066
  };
3353
4067
  }
3354
4068
  async function checkClaudeInstalled() {
3355
- const exists = await commandExists("claude");
4069
+ const exists = await commandExists2("claude");
3356
4070
  return {
3357
4071
  name: "Claude Code (claude)",
3358
4072
  ok: exists,
@@ -3361,7 +4075,7 @@ async function checkClaudeInstalled() {
3361
4075
  };
3362
4076
  }
3363
4077
  async function checkClaudeAuth() {
3364
- const exists = await commandExists("claude");
4078
+ const exists = await commandExists2("claude");
3365
4079
  if (!exists) {
3366
4080
  return {
3367
4081
  name: "Claude Code auth",
@@ -3379,7 +4093,7 @@ async function checkClaudeAuth() {
3379
4093
  };
3380
4094
  }
3381
4095
  async function checkCodexInstalled() {
3382
- const exists = await commandExists("codex");
4096
+ const exists = await commandExists2("codex");
3383
4097
  return {
3384
4098
  name: "Codex CLI (codex)",
3385
4099
  ok: exists,
@@ -3388,7 +4102,7 @@ async function checkCodexInstalled() {
3388
4102
  };
3389
4103
  }
3390
4104
  async function checkCodexAuth() {
3391
- const exists = await commandExists("codex");
4105
+ const exists = await commandExists2("codex");
3392
4106
  if (!exists) {
3393
4107
  return {
3394
4108
  name: "Codex CLI auth",
@@ -3405,7 +4119,7 @@ async function checkCodexAuth() {
3405
4119
  };
3406
4120
  }
3407
4121
  async function checkGeminiInstalled() {
3408
- const exists = await commandExists("gemini");
4122
+ const exists = await commandExists2("gemini");
3409
4123
  return {
3410
4124
  name: "Gemini CLI (gemini)",
3411
4125
  ok: exists,
@@ -3414,7 +4128,7 @@ async function checkGeminiInstalled() {
3414
4128
  };
3415
4129
  }
3416
4130
  async function checkGeminiAuth() {
3417
- const exists = await commandExists("gemini");
4131
+ const exists = await commandExists2("gemini");
3418
4132
  if (!exists) {
3419
4133
  return {
3420
4134
  name: "Gemini CLI auth",
@@ -3431,7 +4145,7 @@ async function checkGeminiAuth() {
3431
4145
  };
3432
4146
  }
3433
4147
  async function checkGlabInstalled() {
3434
- const exists = await commandExists("glab");
4148
+ const exists = await commandExists2("glab");
3435
4149
  return {
3436
4150
  name: "GitLab CLI (glab)",
3437
4151
  ok: exists,
@@ -3441,7 +4155,7 @@ async function checkGlabInstalled() {
3441
4155
  };
3442
4156
  }
3443
4157
  async function checkGlabAuth() {
3444
- const exists = await commandExists("glab");
4158
+ const exists = await commandExists2("glab");
3445
4159
  if (!exists) {
3446
4160
  return {
3447
4161
  name: "GitLab CLI auth",
@@ -3461,7 +4175,7 @@ async function checkGlabAuth() {
3461
4175
  };
3462
4176
  }
3463
4177
  async function checkJqInstalled() {
3464
- const exists = await commandExists("jq");
4178
+ const exists = await commandExists2("jq");
3465
4179
  return {
3466
4180
  name: "jq (optional)",
3467
4181
  ok: exists,
@@ -3535,26 +4249,26 @@ async function autoFix(checks, agent) {
3535
4249
  if (claudeCheck && !claudeCheck.ok && agent === "claude") {
3536
4250
  console.log(paint5("cyan", " Installing Claude Code..."));
3537
4251
  console.log(paint5("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
3538
- await new Promise((resolve7) => {
4252
+ await new Promise((resolve8) => {
3539
4253
  const child = spawn8("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], { stdio: "inherit" });
3540
- child.on("exit", () => resolve7());
4254
+ child.on("exit", () => resolve8());
3541
4255
  });
3542
4256
  console.log("");
3543
4257
  }
3544
4258
  if (ghInstalled && !ghAuthed) {
3545
4259
  console.log(paint5("cyan", " Running gh auth login..."));
3546
- await new Promise((resolve7) => {
4260
+ await new Promise((resolve8) => {
3547
4261
  const child = spawn8("gh", ["auth", "login"], { stdio: "inherit" });
3548
- child.on("exit", () => resolve7());
4262
+ child.on("exit", () => resolve8());
3549
4263
  });
3550
4264
  console.log("");
3551
4265
  }
3552
4266
  if (!mrAuthed) {
3553
4267
  console.log(paint5("cyan", " Running mr login..."));
3554
4268
  const entry = process.argv[1];
3555
- await new Promise((resolve7) => {
4269
+ await new Promise((resolve8) => {
3556
4270
  const child = spawn8(process.execPath, [entry, "login"], { stdio: "inherit" });
3557
- child.on("exit", () => resolve7());
4271
+ child.on("exit", () => resolve8());
3558
4272
  });
3559
4273
  console.log("");
3560
4274
  }
@@ -3802,7 +4516,7 @@ var resumeCommand = new Command17("resume").description("Resume an interactive C
3802
4516
  // cli/commands/browse.ts
3803
4517
  import { Command as Command18 } from "commander";
3804
4518
  import { execSync as execSync5, spawn as spawn6 } from "child_process";
3805
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
4519
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
3806
4520
  import { join as join8 } from "path";
3807
4521
  var BROWSE_DIR2 = join8(import.meta.dirname, "..", "..", "browse");
3808
4522
  function isProcessAlive(pid) {
@@ -3849,7 +4563,7 @@ async function ensureDevServer() {
3849
4563
  env: { ...process.env }
3850
4564
  });
3851
4565
  devProc.unref();
3852
- writeFileSync4(
4566
+ writeFileSync5(
3853
4567
  devStateFile,
3854
4568
  JSON.stringify({ pid: devProc.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString() }),
3855
4569
  { mode: 384 }
@@ -4091,7 +4805,7 @@ var testCommand = new Command20("test").description("Run automated browser test
4091
4805
 
4092
4806
  // cli/commands/features.ts
4093
4807
  import { Command as Command21 } from "commander";
4094
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync12 } from "fs";
4808
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync12 } from "fs";
4095
4809
  import { resolve as resolve5, sep as sep2 } from "path";
4096
4810
  var FEATURES_FILE3 = ".mr-features.md";
4097
4811
  var c7 = {
@@ -4132,13 +4846,13 @@ var featuresCommand = new Command21("features").description("View or update the
4132
4846
  if (opts.file) {
4133
4847
  const content2 = readFileSync9(resolve5(opts.file), "utf-8");
4134
4848
  const featuresPath = getFeaturesPath();
4135
- writeFileSync5(featuresPath, content2);
4849
+ writeFileSync6(featuresPath, content2);
4136
4850
  console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)} from ${paint7("cyan", opts.file)}`);
4137
4851
  return;
4138
4852
  }
4139
4853
  if (opts.update) {
4140
4854
  const featuresPath = getFeaturesPath();
4141
- writeFileSync5(featuresPath, opts.update);
4855
+ writeFileSync6(featuresPath, opts.update);
4142
4856
  console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)}`);
4143
4857
  return;
4144
4858
  }
@@ -4153,12 +4867,12 @@ var featuresCommand = new Command21("features").description("View or update the
4153
4867
 
4154
4868
  // cli/commands/no-mr.ts
4155
4869
  import { Command as Command22 } from "commander";
4156
- import { writeFileSync as writeFileSync6 } from "fs";
4870
+ import { writeFileSync as writeFileSync7 } from "fs";
4157
4871
  import { resolve as resolve6 } from "path";
4158
4872
  var NO_MR_FILE = ".mr-no-mr";
4159
4873
  var noMrCommand = new Command22("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) => {
4160
4874
  const filePath = resolve6(process.cwd(), NO_MR_FILE);
4161
- writeFileSync6(filePath, description, "utf-8");
4875
+ writeFileSync7(filePath, description, "utf-8");
4162
4876
  await api.post(`/api/tasks/${taskId}/updates`, {
4163
4877
  message: `No MR/PR needed \u2014 ${description}`,
4164
4878
  source: "agent"
@@ -4167,105 +4881,8 @@ var noMrCommand = new Command22("no-mr").description("Signal that a task does no
4167
4881
  console.log(` Reason: ${description}`);
4168
4882
  });
4169
4883
 
4170
- // cli/commands/mobile.ts
4171
- import { Command as Command23 } from "commander";
4172
- function paint8(color, text) {
4173
- const colors = {
4174
- cyan: "\x1B[36m",
4175
- green: "\x1B[32m",
4176
- yellow: "\x1B[33m",
4177
- red: "\x1B[31m",
4178
- dim: "\x1B[2m",
4179
- reset: "\x1B[0m"
4180
- };
4181
- return `${colors[color] ?? ""}${text}${colors.reset}`;
4182
- }
4183
- var mobileCommand = new Command23("mobile").description(
4184
- "Start the Web-to-Mobile Conversion Wizard for the linked project"
4185
- ).argument("[project-id]", "Project ID (defaults to linked project)").option("--framework <framework>", "Framework: react-native or native").option("--url <url>", "Web app URL for analysis").action(
4186
- async (projectIdArg, opts) => {
4187
- const projectId = projectIdArg || getLinkedProjectId();
4188
- if (!projectId) {
4189
- console.error(
4190
- 'No project specified. Provide a project ID or run "mr link <project-id>" first.'
4191
- );
4192
- process.exit(1);
4193
- }
4194
- console.log(
4195
- paint8("cyan", "mobile") + paint8("dim", " \u2014 starting conversion wizard")
4196
- );
4197
- const task = await api.post("/api/tasks", {
4198
- title: "Convert to Mobile App",
4199
- projectId,
4200
- status: "in_progress"
4201
- });
4202
- console.log(` Created task: ${task.title} (${task.id})`);
4203
- console.log(" Analyzing web app...");
4204
- const analysis = await api.post("/api/wizard/mobile/analyze", {
4205
- projectId,
4206
- parentTaskId: task.id,
4207
- webAppUrl: opts.url
4208
- });
4209
- console.log(
4210
- ` Found ${analysis.analysis.screens.length} screens (${analysis.analysis.framework})`
4211
- );
4212
- if (opts.framework) {
4213
- console.log(
4214
- ` Generating architecture plan for ${opts.framework}...`
4215
- );
4216
- await api.post("/api/wizard/mobile/generate-plan", {
4217
- projectId,
4218
- parentTaskId: task.id,
4219
- framework: opts.framework,
4220
- analysisResourceId: analysis.resourceId
4221
- });
4222
- console.log(" Architecture plan created.");
4223
- }
4224
- console.log(
4225
- `
4226
- ${paint8("green", "\u2713")} Wizard initialized. Open the web UI to continue:
4227
- \u2192 Task ID: ${task.id}
4228
- \u2192 Use the "Convert to Mobile" button on the project page`
4229
- );
4230
- }
4231
- );
4232
- var statusSubcommand = new Command23("status").description("Show mobile conversion status for a task").argument("<task-id>", "Parent conversion task ID").action(async (taskId) => {
4233
- const resources = await api.get(
4234
- `/api/tasks/${taskId}/resources`
4235
- );
4236
- const wizardState = resources.find(
4237
- (r) => r.title === "Wizard State" && r.type === "plan"
4238
- );
4239
- if (!wizardState) {
4240
- console.log("No wizard state found for this task.");
4241
- return;
4242
- }
4243
- try {
4244
- const state = JSON.parse(wizardState.content);
4245
- console.log(paint8("cyan", "Mobile Conversion Status"));
4246
- console.log(` Phase: ${state.phase}`);
4247
- if (state.framework) {
4248
- console.log(` Framework: ${state.framework}`);
4249
- }
4250
- if (state.screenDesigns?.length) {
4251
- const completed = state.screenDesigns.filter(
4252
- (d) => d.status === "complete"
4253
- ).length;
4254
- console.log(
4255
- ` Screens: ${completed}/${state.screenDesigns.length} complete`
4256
- );
4257
- }
4258
- if (state.mobileRepoUrl) {
4259
- console.log(` Repo: ${state.mobileRepoUrl}`);
4260
- }
4261
- } catch {
4262
- console.log("Could not parse wizard state.");
4263
- }
4264
- });
4265
- mobileCommand.addCommand(statusSubcommand);
4266
-
4267
4884
  // cli/commands/scan.ts
4268
- import { Command as Command24 } from "commander";
4885
+ import { Command as Command23 } from "commander";
4269
4886
 
4270
4887
  // lib/scanner/index.ts
4271
4888
  import { spawn as spawn7 } from "child_process";
@@ -4494,8 +5111,8 @@ function matchesIgnorePattern(route, patterns) {
4494
5111
  }
4495
5112
  async function uploadScreenshot(imagePath, apiUrl, apiKey) {
4496
5113
  try {
4497
- const { readFileSync: readFileSync12 } = await import("fs");
4498
- const imageBuffer = readFileSync12(imagePath);
5114
+ const { readFileSync: readFileSync13 } = await import("fs");
5115
+ const imageBuffer = readFileSync13(imagePath);
4499
5116
  const blob = new Blob([imageBuffer], { type: "image/webp" });
4500
5117
  const formData = new FormData();
4501
5118
  formData.append("file", blob, `scan-${Date.now()}.webp`);
@@ -4702,22 +5319,19 @@ function buildSynthesisPrompt(config, context, codebaseAnalysis, crawlResults, p
4702
5319
  const promotedFindings = priorFindings.filter((f) => f.status === "promoted");
4703
5320
  const crawlSummary = crawlResults.map((r) => {
4704
5321
  let summary = `Route: ${r.route}
4705
- Title: ${r.pageTitle}
4706
- Headings: ${r.headings.join(", ") || "none"}`;
5322
+ Title: ${r.pageTitle}`;
4707
5323
  if (r.consoleErrors.length > 0) {
4708
5324
  summary += `
4709
- Console Errors: ${r.consoleErrors.slice(0, 5).join("; ")}`;
5325
+ Console Errors: ${r.consoleErrors.slice(0, 3).join("; ")}`;
4710
5326
  }
4711
5327
  if (r.consoleWarnings.length > 0) {
4712
5328
  summary += `
4713
- Console Warnings: ${r.consoleWarnings.slice(0, 3).join("; ")}`;
5329
+ Console Warnings: ${r.consoleWarnings.slice(0, 2).join("; ")}`;
4714
5330
  }
4715
- if (r.screenshotUrl) {
5331
+ if (r.loadTimeMs > 1e3) {
4716
5332
  summary += `
4717
- Screenshot: ${r.screenshotUrl}`;
5333
+ Load time: ${r.loadTimeMs}ms (slow)`;
4718
5334
  }
4719
- summary += `
4720
- Load time: ${r.loadTimeMs}ms`;
4721
5335
  return summary;
4722
5336
  }).join("\n\n");
4723
5337
  return `You are an autonomous product manager reviewing a web application. Your job is to produce actionable, specific findings that a developer can use to improve their product.
@@ -4734,16 +5348,16 @@ ${codebaseAnalysis.routes.map((r) => `- ${r}`).join("\n")}
4734
5348
  ${codebaseAnalysis.prismaModels.map((m) => `- ${m}`).join("\n")}
4735
5349
 
4736
5350
  **Components:**
4737
- ${codebaseAnalysis.components.slice(0, 30).map((c10) => `- ${c10}`).join("\n")}
5351
+ ${codebaseAnalysis.components.slice(0, 15).map((c10) => `- ${c10}`).join("\n")}
4738
5352
 
4739
5353
  **Recent Git Commits:**
4740
- ${codebaseAnalysis.recentCommits.slice(0, 15).map((c10) => `- ${c10}`).join("\n")}
5354
+ ${codebaseAnalysis.recentCommits.slice(0, 8).map((c10) => `- ${c10}`).join("\n")}
4741
5355
 
4742
5356
  **Completed Tasks:**
4743
- ${context.completedTasks.slice(0, 20).map((t) => `- ${t.title}`).join("\n") || "None"}
5357
+ ${context.completedTasks.slice(0, 10).map((t) => `- ${t.title}`).join("\n") || "None"}
4744
5358
 
4745
5359
  **Open Tasks:**
4746
- ${context.openTasks.slice(0, 10).map((t) => `- ${t.title}${t.notes ? ` (${t.notes})` : ""}`).join("\n") || "None"}
5360
+ ${context.openTasks.slice(0, 5).map((t) => `- ${t.title}`).join("\n") || "None"}
4747
5361
 
4748
5362
  ${config.focusAreas.length > 0 ? `**Focus Areas (user-specified):**
4749
5363
  ${config.focusAreas.map((a) => `- ${a}`).join("\n")}` : ""}
@@ -4754,11 +5368,9 @@ ${crawlResults.length > 0 ? crawlSummary : "Live crawl was not performed (app ma
4754
5368
 
4755
5369
  ## Prior Findings Context
4756
5370
 
4757
- ${dismissedFindings.length > 0 ? `**Previously Dismissed (do NOT re-suggest these):**
4758
- ${dismissedFindings.map((f) => `- [${f.type}] ${f.title}`).join("\n")}` : "No dismissed findings."}
5371
+ ${dismissedFindings.length > 0 ? `**Previously Dismissed (do NOT re-suggest):** ${dismissedFindings.map((f) => f.title).join("; ")}` : "No dismissed findings."}
4759
5372
 
4760
- ${promotedFindings.length > 0 ? `**Already Being Worked On (reference as "still present" if detected):**
4761
- ${promotedFindings.map((f) => `- [${f.type}] ${f.title}`).join("\n")}` : "No promoted findings."}
5373
+ ${promotedFindings.length > 0 ? `**Already Being Worked On:** ${promotedFindings.map((f) => f.title).join("; ")}` : "No promoted findings."}
4762
5374
 
4763
5375
  ## Instructions
4764
5376
 
@@ -4946,7 +5558,7 @@ async function fetchScanContext(opts) {
4946
5558
  };
4947
5559
  }
4948
5560
  function runClaude(prompt2) {
4949
- return new Promise((resolve7, reject) => {
5561
+ return new Promise((resolve8, reject) => {
4950
5562
  const child = spawn7("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
4951
5563
  stdio: ["ignore", "pipe", "pipe"]
4952
5564
  });
@@ -4959,7 +5571,7 @@ function runClaude(prompt2) {
4959
5571
  errOutput += d.toString();
4960
5572
  });
4961
5573
  child.on("exit", (code) => {
4962
- if (code === 0) resolve7(output.trim());
5574
+ if (code === 0) resolve8(output.trim());
4963
5575
  else reject(new Error(`claude exited with code ${code}
4964
5576
  ${errOutput.trim()}`));
4965
5577
  });
@@ -5014,25 +5626,25 @@ var c8 = {
5014
5626
  magenta: "\x1B[35m",
5015
5627
  gray: "\x1B[90m"
5016
5628
  };
5017
- function paint9(color, text) {
5629
+ function paint8(color, text) {
5018
5630
  return `${c8[color]}${text}${c8.reset}`;
5019
5631
  }
5020
5632
  function timestamp2() {
5021
- return paint9("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
5633
+ return paint8("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
5022
5634
  }
5023
5635
  function scanTag() {
5024
- return paint9("magenta", "[scan]");
5636
+ return paint8("magenta", "[scan]");
5025
5637
  }
5026
5638
  function log(msg) {
5027
5639
  console.log(`${timestamp2()} ${scanTag()} ${msg}`);
5028
5640
  }
5029
5641
  function logOk(msg) {
5030
- console.log(`${timestamp2()} ${scanTag()} ${paint9("green", "\u2713")} ${msg}`);
5642
+ console.log(`${timestamp2()} ${scanTag()} ${paint8("green", "\u2713")} ${msg}`);
5031
5643
  }
5032
5644
  function logErr(msg) {
5033
- console.error(`${timestamp2()} ${scanTag()} ${paint9("red", "\u2717")} ${msg}`);
5645
+ console.error(`${timestamp2()} ${scanTag()} ${paint8("red", "\u2717")} ${msg}`);
5034
5646
  }
5035
- var scanCommand = new Command24("scan").description("Run a product scan on the current project \u2014 analyzes codebase, crawls the app, and surfaces findings").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing scan report ID (created by UI trigger)").option("--no-crawl", "Skip live crawl (codebase analysis only)").action(async (opts) => {
5647
+ var scanCommand = new Command23("scan").description("Run a product scan on the current project \u2014 analyzes codebase, crawls the app, and surfaces findings").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing scan report ID (created by UI trigger)").option("--no-crawl", "Skip live crawl (codebase analysis only)").action(async (opts) => {
5036
5648
  const config = loadConfig();
5037
5649
  if (!config.apiKey) {
5038
5650
  logErr('Not authenticated. Run "mr login" first.');
@@ -5040,11 +5652,11 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
5040
5652
  }
5041
5653
  const banner = [
5042
5654
  ``,
5043
- paint9("magenta", ` \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2557\u2554`),
5044
- paint9("magenta", ` \u255A\u2550\u2557\u2551 \u2560\u2550\u2563\u2551\u2551\u2551`),
5045
- paint9("magenta", ` \u255A\u2550\u255D\u255A\u2550\u255D\u2569 \u2569\u255D\u255A\u255D`),
5046
- paint9("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
5047
- paint9("dim", ` autonomous product scanner`),
5655
+ paint8("magenta", ` \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2557\u2554`),
5656
+ paint8("magenta", ` \u255A\u2550\u2557\u2551 \u2560\u2550\u2563\u2551\u2551\u2551`),
5657
+ paint8("magenta", ` \u255A\u2550\u255D\u255A\u2550\u255D\u2569 \u2569\u255D\u255A\u255D`),
5658
+ paint8("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
5659
+ paint8("dim", ` autonomous product scanner`),
5048
5660
  ``
5049
5661
  ].join("\n");
5050
5662
  console.log(banner);
@@ -5060,7 +5672,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
5060
5672
  logErr(`Failed to fetch project ${projectId}`);
5061
5673
  process.exit(1);
5062
5674
  }
5063
- log(`Scanning project: ${paint9("cyan", project.name)}`);
5675
+ log(`Scanning project: ${paint8("cyan", project.name)}`);
5064
5676
  let projectPath = project.localPath;
5065
5677
  if (!projectPath) {
5066
5678
  for (const [dir, pid] of Object.entries(config.directories)) {
@@ -5076,7 +5688,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
5076
5688
  let reportId;
5077
5689
  if (opts.report) {
5078
5690
  reportId = opts.report;
5079
- log(`Using existing scan report ${paint9("yellow", reportId.slice(0, 8))}`);
5691
+ log(`Using existing scan report ${paint8("yellow", reportId.slice(0, 8))}`);
5080
5692
  } else {
5081
5693
  try {
5082
5694
  const scans = await api.get(`/api/scans?projectId=${projectId}&status=processing`);
@@ -5092,7 +5704,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
5092
5704
  status: "pending"
5093
5705
  });
5094
5706
  reportId = report.id;
5095
- log(`Created scan report ${paint9("yellow", reportId.slice(0, 8))}`);
5707
+ log(`Created scan report ${paint8("yellow", reportId.slice(0, 8))}`);
5096
5708
  } catch (err) {
5097
5709
  logErr(`Failed to create scan report: ${err.message}`);
5098
5710
  process.exit(1);
@@ -5105,7 +5717,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
5105
5717
  try {
5106
5718
  const current = await api.get(`/api/scans/${reportId}`);
5107
5719
  if (current.status === "cancelled") {
5108
- log(paint9("yellow", "Scan was cancelled \u2014 aborting."));
5720
+ log(paint8("yellow", "Scan was cancelled \u2014 aborting."));
5109
5721
  process.exit(0);
5110
5722
  }
5111
5723
  } catch {
@@ -5121,7 +5733,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
5121
5733
  runBrowse: runBrowseCommand2,
5122
5734
  onLog: log,
5123
5735
  onProgress: (phase, detail) => {
5124
- log(`${paint9("dim", `[${phase}]`)} ${detail}`);
5736
+ log(`${paint8("dim", `[${phase}]`)} ${detail}`);
5125
5737
  }
5126
5738
  });
5127
5739
  let wasCancelled = false;
@@ -5133,7 +5745,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
5133
5745
  } catch {
5134
5746
  }
5135
5747
  if (wasCancelled) {
5136
- log(paint9("yellow", "Scan was cancelled by user \u2014 discarding results."));
5748
+ log(paint8("yellow", "Scan was cancelled by user \u2014 discarding results."));
5137
5749
  process.exit(0);
5138
5750
  }
5139
5751
  await api.patch(`/api/scans/${reportId}`, {
@@ -5144,32 +5756,32 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
5144
5756
  scanDurationMs: result.scanDurationMs,
5145
5757
  routesCrawled: result.routesCrawled
5146
5758
  });
5147
- logOk(`Scan complete \u2014 ${paint9("cyan", String(result.findings.length))} findings`);
5759
+ logOk(`Scan complete \u2014 ${paint8("cyan", String(result.findings.length))} findings`);
5148
5760
  console.log("");
5149
- console.log(` ${paint9("bold", "Summary:")} ${result.summary}`);
5761
+ console.log(` ${paint8("bold", "Summary:")} ${result.summary}`);
5150
5762
  console.log("");
5151
5763
  const high = result.findings.filter((f) => f.priority === "high");
5152
5764
  const medium = result.findings.filter((f) => f.priority === "medium");
5153
5765
  const low = result.findings.filter((f) => f.priority === "low");
5154
5766
  if (high.length > 0) {
5155
- console.log(` ${paint9("bold", paint9("red", `High Priority (${high.length})`))}`);
5767
+ console.log(` ${paint8("bold", paint8("red", `High Priority (${high.length})`))}`);
5156
5768
  for (const f of high) {
5157
- console.log(` ${paint9("red", "\u25CF")} [${f.type}] ${f.title}`);
5158
- console.log(` ${paint9("dim", f.description.slice(0, 120))}`);
5769
+ console.log(` ${paint8("red", "\u25CF")} [${f.type}] ${f.title}`);
5770
+ console.log(` ${paint8("dim", f.description.slice(0, 120))}`);
5159
5771
  }
5160
5772
  console.log("");
5161
5773
  }
5162
5774
  if (medium.length > 0) {
5163
- console.log(` ${paint9("bold", paint9("yellow", `Medium Priority (${medium.length})`))}`);
5775
+ console.log(` ${paint8("bold", paint8("yellow", `Medium Priority (${medium.length})`))}`);
5164
5776
  for (const f of medium) {
5165
- console.log(` ${paint9("yellow", "\u25CF")} [${f.type}] ${f.title}`);
5777
+ console.log(` ${paint8("yellow", "\u25CF")} [${f.type}] ${f.title}`);
5166
5778
  }
5167
5779
  console.log("");
5168
5780
  }
5169
5781
  if (low.length > 0) {
5170
- console.log(` ${paint9("dim", `Low Priority (${low.length})`)} `);
5782
+ console.log(` ${paint8("dim", `Low Priority (${low.length})`)} `);
5171
5783
  for (const f of low) {
5172
- console.log(` ${paint9("dim", `\u25CB [${f.type}] ${f.title}`)}`);
5784
+ console.log(` ${paint8("dim", `\u25CB [${f.type}] ${f.title}`)}`);
5173
5785
  }
5174
5786
  console.log("");
5175
5787
  }
@@ -5187,7 +5799,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
5187
5799
  });
5188
5800
 
5189
5801
  // cli/commands/idea.ts
5190
- import { Command as Command25 } from "commander";
5802
+ import { Command as Command24 } from "commander";
5191
5803
  var c9 = {
5192
5804
  reset: "\x1B[0m",
5193
5805
  bold: "\x1B[1m",
@@ -5200,27 +5812,27 @@ var c9 = {
5200
5812
  gray: "\x1B[90m",
5201
5813
  magenta: "\x1B[35m"
5202
5814
  };
5203
- function paint10(color, text) {
5815
+ function paint9(color, text) {
5204
5816
  return `${c9[color]}${text}${c9.reset}`;
5205
5817
  }
5206
5818
  function statusBadge2(status) {
5207
5819
  switch (status) {
5208
5820
  case "draft":
5209
- return paint10("gray", "\u25CB draft");
5821
+ return paint9("gray", "\u25CB draft");
5210
5822
  case "generating":
5211
- return paint10("cyan", "\u27F3 generating");
5823
+ return paint9("cyan", "\u27F3 generating");
5212
5824
  case "generated":
5213
- return paint10("green", "\u2713 generated");
5825
+ return paint9("green", "\u2713 generated");
5214
5826
  case "promoted":
5215
- return paint10("magenta", "\u2191 promoted");
5827
+ return paint9("magenta", "\u2191 promoted");
5216
5828
  case "archived":
5217
- return paint10("dim", "\u2298 archived");
5829
+ return paint9("dim", "\u2298 archived");
5218
5830
  default:
5219
- return paint10("gray", status);
5831
+ return paint9("gray", status);
5220
5832
  }
5221
5833
  }
5222
- var ideaCommand = new Command25("idea").description("Manage ideas \u2014 brainstorm, generate prototypes & plans").addCommand(
5223
- new Command25("list").description("List ideas for the linked project").option("--all", "Show ideas for all projects").option("--status <status>", "Filter by status").action(async (opts) => {
5834
+ var ideaCommand = new Command24("idea").description("Manage ideas \u2014 brainstorm, generate prototypes & plans").addCommand(
5835
+ new Command24("list").description("List ideas for the linked project").option("--all", "Show ideas for all projects").option("--status <status>", "Filter by status").action(async (opts) => {
5224
5836
  const params = new URLSearchParams();
5225
5837
  if (!opts.all) {
5226
5838
  const projectId = getLinkedProjectId();
@@ -5231,23 +5843,23 @@ var ideaCommand = new Command25("idea").description("Manage ideas \u2014 brainst
5231
5843
  if (opts.status) params.set("status", opts.status);
5232
5844
  const ideas = await api.get(`/api/ideas?${params.toString()}`);
5233
5845
  if (ideas.length === 0) {
5234
- console.log(paint10("gray", "No ideas found."));
5846
+ console.log(paint9("gray", "No ideas found."));
5235
5847
  return;
5236
5848
  }
5237
5849
  console.log();
5238
5850
  for (const idea of ideas) {
5239
5851
  const date = new Date(idea.createdAt).toLocaleDateString();
5240
5852
  console.log(
5241
- ` ${paint10("bold", idea.title)} ${statusBadge2(idea.status)} ${paint10("gray", idea.id.slice(0, 8))} ${paint10("dim", date)}`
5853
+ ` ${paint9("bold", idea.title)} ${statusBadge2(idea.status)} ${paint9("gray", idea.id.slice(0, 8))} ${paint9("dim", date)}`
5242
5854
  );
5243
5855
  if (idea.description) {
5244
- console.log(` ${paint10("dim", idea.description.slice(0, 80) + (idea.description.length > 80 ? "\u2026" : ""))}`);
5856
+ console.log(` ${paint9("dim", idea.description.slice(0, 80) + (idea.description.length > 80 ? "\u2026" : ""))}`);
5245
5857
  }
5246
5858
  console.log();
5247
5859
  }
5248
5860
  })
5249
5861
  ).addCommand(
5250
- new Command25("create").description("Create a new idea").argument("<title>", "Title of the idea").option("--description <desc>", "Description of the idea").option("--project <projectId>", "Project ID (defaults to linked project)").option("--generate", "Immediately start generating plan & prototype").action(async (title, opts) => {
5862
+ new Command24("create").description("Create a new idea").argument("<title>", "Title of the idea").option("--description <desc>", "Description of the idea").option("--project <projectId>", "Project ID (defaults to linked project)").option("--generate", "Immediately start generating plan & prototype").action(async (title, opts) => {
5251
5863
  const projectId = opts.project ?? getLinkedProjectId() ?? null;
5252
5864
  const idea = await api.post("/api/ideas", {
5253
5865
  title,
@@ -5255,59 +5867,59 @@ var ideaCommand = new Command25("idea").description("Manage ideas \u2014 brainst
5255
5867
  projectId
5256
5868
  });
5257
5869
  console.log();
5258
- console.log(` ${paint10("green", "\u2713")} Created idea: ${paint10("bold", idea.title)}`);
5259
- console.log(` ${paint10("gray", "ID:")} ${idea.id}`);
5870
+ console.log(` ${paint9("green", "\u2713")} Created idea: ${paint9("bold", idea.title)}`);
5871
+ console.log(` ${paint9("gray", "ID:")} ${idea.id}`);
5260
5872
  if (opts.generate) {
5261
5873
  await api.post(`/api/ideas/${idea.id}/generate`);
5262
- console.log(` ${paint10("cyan", "\u27F3")} Generation will begin automatically via the watch agent.`);
5874
+ console.log(` ${paint9("cyan", "\u27F3")} Generation will begin automatically via the watch agent.`);
5263
5875
  }
5264
5876
  console.log();
5265
5877
  })
5266
5878
  ).addCommand(
5267
- new Command25("generate").description("Start generating plan & prototype for an idea").argument("<id>", "Idea ID").action(async (id) => {
5879
+ new Command24("generate").description("Start generating plan & prototype for an idea").argument("<id>", "Idea ID").action(async (id) => {
5268
5880
  const idea = await api.post(`/api/ideas/${id}/generate`);
5269
5881
  console.log();
5270
- console.log(` ${paint10("cyan", "\u27F3")} Generating: ${paint10("bold", idea.title)}`);
5271
- console.log(` ${paint10("gray", "The watch agent will pick this up shortly.")}`);
5882
+ console.log(` ${paint9("cyan", "\u27F3")} Generating: ${paint9("bold", idea.title)}`);
5883
+ console.log(` ${paint9("gray", "The watch agent will pick this up shortly.")}`);
5272
5884
  console.log();
5273
5885
  })
5274
5886
  ).addCommand(
5275
- new Command25("feedback").description("Send feedback to iterate on an idea's generated content").argument("<id>", "Idea ID").argument("<feedback>", "Feedback text").action(async (id, feedback) => {
5887
+ new Command24("feedback").description("Send feedback to iterate on an idea's generated content").argument("<id>", "Idea ID").argument("<feedback>", "Feedback text").action(async (id, feedback) => {
5276
5888
  const idea = await api.post(`/api/ideas/${id}/feedback`, { feedback });
5277
5889
  console.log();
5278
- console.log(` ${paint10("cyan", "\u27F3")} Feedback sent for: ${paint10("bold", idea.title)}`);
5279
- console.log(` ${paint10("gray", "The watch agent will re-generate with your feedback.")}`);
5890
+ console.log(` ${paint9("cyan", "\u27F3")} Feedback sent for: ${paint9("bold", idea.title)}`);
5891
+ console.log(` ${paint9("gray", "The watch agent will re-generate with your feedback.")}`);
5280
5892
  console.log();
5281
5893
  })
5282
5894
  ).addCommand(
5283
- new Command25("promote").description("Promote an idea to a task").argument("<id>", "Idea ID").action(async (id) => {
5895
+ new Command24("promote").description("Promote an idea to a task").argument("<id>", "Idea ID").action(async (id) => {
5284
5896
  const result = await api.post(`/api/ideas/${id}/promote`);
5285
5897
  console.log();
5286
- console.log(` ${paint10("green", "\u2713")} Promoted idea to task: ${paint10("bold", result.task.title)}`);
5287
- console.log(` ${paint10("gray", "Task ID:")} ${result.task.id}`);
5898
+ console.log(` ${paint9("green", "\u2713")} Promoted idea to task: ${paint9("bold", result.task.title)}`);
5899
+ console.log(` ${paint9("gray", "Task ID:")} ${result.task.id}`);
5288
5900
  console.log();
5289
5901
  })
5290
5902
  ).addCommand(
5291
- new Command25("spin-up").description("Spin up a new project with a GitHub repo from an idea").argument("<id>", "Idea ID").option("--name <name>", "Custom project name (defaults to idea title)").action(async (id, opts) => {
5903
+ new Command24("spin-up").description("Spin up a new project with a GitHub repo from an idea").argument("<id>", "Idea ID").option("--name <name>", "Custom project name (defaults to idea title)").action(async (id, opts) => {
5292
5904
  const body = {};
5293
5905
  if (opts.name) body.name = opts.name;
5294
5906
  const result = await api.post(`/api/ideas/${id}/spin-up`, body);
5295
5907
  console.log();
5296
- console.log(` ${paint10("green", "\u2713")} Spinning up project: ${paint10("bold", result.project.name)}`);
5297
- console.log(` ${paint10("gray", "Project ID:")} ${result.project.id}`);
5908
+ console.log(` ${paint9("green", "\u2713")} Spinning up project: ${paint9("bold", result.project.name)}`);
5909
+ console.log(` ${paint9("gray", "Project ID:")} ${result.project.id}`);
5298
5910
  if (result.tasks && result.tasks.length > 0) {
5299
- console.log(` ${paint10("green", "\u2713")} Created ${result.tasks.length} follow-up task(s):`);
5911
+ console.log(` ${paint9("green", "\u2713")} Created ${result.tasks.length} follow-up task(s):`);
5300
5912
  for (const task of result.tasks) {
5301
- console.log(` ${paint10("gray", "\u2022")} ${task.title}`);
5913
+ console.log(` ${paint9("gray", "\u2022")} ${task.title}`);
5302
5914
  }
5303
5915
  }
5304
- console.log(` ${paint10("cyan", "\u27F3")} Repo creation is queued \u2014 the watch daemon will pick it up.`);
5916
+ console.log(` ${paint9("cyan", "\u27F3")} Repo creation is queued \u2014 the watch daemon will pick it up.`);
5305
5917
  console.log();
5306
5918
  })
5307
5919
  );
5308
5920
 
5309
5921
  // cli/commands/doctor.ts
5310
- import { Command as Command26 } from "commander";
5922
+ import { Command as Command25 } from "commander";
5311
5923
  import { existsSync as existsSync15 } from "fs";
5312
5924
  import { homedir as homedir2 } from "os";
5313
5925
  import { join as join11 } from "path";
@@ -5355,7 +5967,7 @@ async function checkProjectLink() {
5355
5967
  optional: true
5356
5968
  };
5357
5969
  }
5358
- var doctorCommand = new Command26("doctor").description("Diagnose Mr. Manager CLI installation and environment").action(async () => {
5970
+ var doctorCommand = new Command25("doctor").description("Diagnose Mr. Manager CLI installation and environment").action(async () => {
5359
5971
  const banner = [
5360
5972
  ``,
5361
5973
  paint5("cyan", ` MR DOCTOR`),
@@ -5397,9 +6009,229 @@ var doctorCommand = new Command26("doctor").description("Diagnose Mr. Manager CL
5397
6009
  process.exit(1);
5398
6010
  });
5399
6011
 
6012
+ // cli/commands/prompt-audit.ts
6013
+ import { Command as Command26 } from "commander";
6014
+ import { resolve as resolve7 } from "path";
6015
+ import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
6016
+ function auditLine(label, tokens) {
6017
+ const bar = "\u2588".repeat(Math.min(60, Math.round(tokens / 200)));
6018
+ return ` ${label.padEnd(30)} ${formatTokenCount(tokens).padStart(8)} ${bar}`;
6019
+ }
6020
+ var promptAuditCommand = new Command26("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) => {
6021
+ const results = [];
6022
+ if (opts.task) {
6023
+ try {
6024
+ const task = await api.get(`/api/tasks/${opts.task}`);
6025
+ const [_subtasks, protoRefs, updates, resources, skills] = await Promise.all([
6026
+ api.get(`/api/tasks/${task.id}/subtasks`).catch(() => []),
6027
+ api.get(`/api/tasks/${task.id}/prototypes`).catch(() => []),
6028
+ api.get(`/api/tasks/${task.id}/updates`).catch(() => []),
6029
+ api.get(`/api/tasks/${task.id}/resources`).catch(() => []),
6030
+ api.get(`/api/tasks/${task.id}/skills`).catch(() => [])
6031
+ ]);
6032
+ const feedbackUpdates = updates.filter((u) => u.source === "user");
6033
+ const sections = [];
6034
+ const notesContent = task.prdContent ? `
6035
+
6036
+ ## PRD (Product Requirements Document)
6037
+
6038
+ ${task.prdContent}` : task.notes ? `
6039
+
6040
+ Task notes:
6041
+ ${task.notes}` : "";
6042
+ sections.push({ name: "task-notes/prd", tokens: estimateTokens(notesContent) });
6043
+ let protoContent = "";
6044
+ for (const ref of protoRefs) {
6045
+ const files = ref.prototype?.files ?? [];
6046
+ for (const f of files) {
6047
+ protoContent += f.content;
6048
+ }
6049
+ }
6050
+ sections.push({ name: "prototypes", tokens: estimateTokens(protoContent) });
6051
+ let skillContent = "";
6052
+ for (const s of skills) {
6053
+ skillContent += s.skill.content;
6054
+ }
6055
+ sections.push({ name: "skills", tokens: estimateTokens(skillContent) });
6056
+ let resourceContent = "";
6057
+ for (const r of resources) {
6058
+ resourceContent += r.content.slice(0, 8e3);
6059
+ }
6060
+ sections.push({ name: "resources", tokens: estimateTokens(resourceContent) });
6061
+ let feedbackContent = "";
6062
+ for (const f of feedbackUpdates) {
6063
+ feedbackContent += f.message;
6064
+ }
6065
+ sections.push({ name: "feedback", tokens: estimateTokens(feedbackContent) });
6066
+ const config = loadConfig();
6067
+ const repoDir = Object.entries(config.directories).find(([, pid]) => pid === task.projectId)?.[0];
6068
+ if (repoDir) {
6069
+ const featuresPath = resolve7(repoDir, ".mr-features.md");
6070
+ if (existsSync16(featuresPath)) {
6071
+ const featuresContent = readFileSync12(featuresPath, "utf-8");
6072
+ sections.push({ name: "features-doc", tokens: estimateTokens(featuresContent) });
6073
+ }
6074
+ }
6075
+ sections.push({ name: "static-instructions (system)", tokens: estimateTokens(
6076
+ "status-updates + screenshots + test-plan + no-mr + features-workflow"
6077
+ ) });
6078
+ const totalTokens = sections.reduce((sum, s) => sum + s.tokens, 0);
6079
+ const promptTokens = sections.filter((s) => !s.name.includes("system")).reduce((sum, s) => sum + s.tokens, 0);
6080
+ const systemTokens = totalTokens - promptTokens;
6081
+ results.push({
6082
+ jobType: "execution",
6083
+ identifier: task.id.slice(0, 8),
6084
+ promptTokens,
6085
+ systemTokens,
6086
+ totalTokens,
6087
+ sections
6088
+ });
6089
+ if (task.prdContent && feedbackUpdates.length > 0) {
6090
+ const prdSections = [
6091
+ { name: "task-notes", tokens: estimateTokens(task.notes ?? "") },
6092
+ { name: "existing-prd", tokens: estimateTokens(task.prdContent) },
6093
+ { name: "feedback", tokens: estimateTokens(feedbackContent) },
6094
+ { name: "prd-format (system)", tokens: estimateTokens("prd-format + open-questions") }
6095
+ ];
6096
+ const prdTotal = prdSections.reduce((sum, s) => sum + s.tokens, 0);
6097
+ results.push({
6098
+ jobType: "prd-revision",
6099
+ identifier: task.id.slice(0, 8),
6100
+ promptTokens: prdTotal,
6101
+ systemTokens: 0,
6102
+ totalTokens: prdTotal,
6103
+ sections: prdSections
6104
+ });
6105
+ }
6106
+ } catch (err) {
6107
+ console.error(`Failed to fetch task: ${err.message}`);
6108
+ process.exit(1);
6109
+ }
6110
+ } else if (opts.all) {
6111
+ const syntheticSections = [
6112
+ {
6113
+ jobType: "execution",
6114
+ description: "Task execution prompt (baseline without attachments)",
6115
+ sections: [
6116
+ { name: "core-prompt", tokens: 800 },
6117
+ { name: "instructions", tokens: 600 },
6118
+ { name: "pr-template", tokens: 400 },
6119
+ { name: "features-ref", tokens: 50 }
6120
+ ]
6121
+ },
6122
+ {
6123
+ jobType: "execution+prd",
6124
+ description: "Task execution with full PRD",
6125
+ sections: [
6126
+ { name: "core-prompt", tokens: 800 },
6127
+ { name: "prd-content", tokens: 4e3 },
6128
+ { name: "instructions", tokens: 600 }
6129
+ ]
6130
+ },
6131
+ {
6132
+ jobType: "prd-generation",
6133
+ description: "PRD generation prompt",
6134
+ sections: [
6135
+ { name: "core-prompt", tokens: 300 },
6136
+ { name: "task-notes", tokens: 500 },
6137
+ { name: "format-ref", tokens: 50 }
6138
+ ]
6139
+ },
6140
+ {
6141
+ jobType: "prd-revision",
6142
+ description: "PRD revision (delta mode)",
6143
+ sections: [
6144
+ { name: "core-prompt", tokens: 300 },
6145
+ { name: "prd-summary", tokens: 200 },
6146
+ { name: "feedback", tokens: 300 },
6147
+ { name: "prd-file-ref", tokens: 30 }
6148
+ ]
6149
+ },
6150
+ {
6151
+ jobType: "prototype",
6152
+ description: "Prototype generation prompt",
6153
+ sections: [
6154
+ { name: "core-prompt", tokens: 600 },
6155
+ { name: "variant-steps", tokens: 200 }
6156
+ ]
6157
+ },
6158
+ {
6159
+ jobType: "refinement",
6160
+ description: "Prototype refinement (file-ref mode)",
6161
+ sections: [
6162
+ { name: "core-prompt", tokens: 600 },
6163
+ { name: "parent-file-refs", tokens: 50 },
6164
+ { name: "feedback", tokens: 200 }
6165
+ ]
6166
+ },
6167
+ {
6168
+ jobType: "scanner-synthesis",
6169
+ description: "Scanner synthesis prompt (tightened caps)",
6170
+ sections: [
6171
+ { name: "context", tokens: 400 },
6172
+ { name: "routes+models", tokens: 300 },
6173
+ { name: "components", tokens: 150 },
6174
+ { name: "commits", tokens: 160 },
6175
+ { name: "crawl-results", tokens: 500 },
6176
+ { name: "prior-findings", tokens: 100 },
6177
+ { name: "instructions", tokens: 500 }
6178
+ ]
6179
+ },
6180
+ {
6181
+ jobType: "idea",
6182
+ description: "Idea generation prompt",
6183
+ sections: [
6184
+ { name: "core-prompt", tokens: 500 },
6185
+ { name: "idea-desc", tokens: 200 }
6186
+ ]
6187
+ },
6188
+ {
6189
+ jobType: "repo-creation",
6190
+ description: "Repository creation prompt",
6191
+ sections: [
6192
+ { name: "core-prompt", tokens: 400 },
6193
+ { name: "project-desc", tokens: 200 }
6194
+ ]
6195
+ }
6196
+ ];
6197
+ console.log("Estimated prompt sizes by job type (representative baselines)");
6198
+ console.log("\u2550".repeat(70));
6199
+ for (const entry of syntheticSections) {
6200
+ const total = entry.sections.reduce((sum, s) => sum + s.tokens, 0);
6201
+ console.log(`
6202
+ ${entry.jobType} \u2014 ${entry.description}`);
6203
+ console.log(` Total: ${formatTokenCount(total)} tokens`);
6204
+ for (const s of entry.sections) {
6205
+ console.log(auditLine(s.name, s.tokens));
6206
+ }
6207
+ }
6208
+ console.log("");
6209
+ return;
6210
+ } else {
6211
+ console.log("Usage: mr prompt-audit --task <id> or mr prompt-audit --all");
6212
+ console.log("\nDry-run prompt construction and report estimated token counts.");
6213
+ return;
6214
+ }
6215
+ if (opts.json) {
6216
+ console.log(JSON.stringify(results, null, 2));
6217
+ } else {
6218
+ console.log("Prompt Audit Report");
6219
+ console.log("\u2550".repeat(70));
6220
+ for (const r of results) {
6221
+ console.log(`
6222
+ ${r.jobType} [${r.identifier}]`);
6223
+ console.log(` Total: ${formatTokenCount(r.totalTokens)} tokens (prompt=${formatTokenCount(r.promptTokens)} system=${formatTokenCount(r.systemTokens)})`);
6224
+ for (const s of r.sections) {
6225
+ console.log(auditLine(s.name, s.tokens));
6226
+ }
6227
+ }
6228
+ console.log("");
6229
+ }
6230
+ });
6231
+
5400
6232
  // cli/index.ts
5401
6233
  var configPath = join12(homedir3(), ".mr-manager", "config.json");
5402
- var isFirstRun = !existsSync16(configPath);
6234
+ var isFirstRun = !existsSync17(configPath);
5403
6235
  var userArgs = process.argv.slice(2);
5404
6236
  var bypassCommands = /* @__PURE__ */ new Set(["login", "init", "auth", "help", "--help", "-h", "--version", "-V", "doctor", "setup"]);
5405
6237
  var shouldBypass = userArgs.length > 0 && bypassCommands.has(userArgs[0]);
@@ -5462,8 +6294,8 @@ program.addCommand(setPathCommand);
5462
6294
  program.addCommand(testCommand);
5463
6295
  program.addCommand(featuresCommand);
5464
6296
  program.addCommand(noMrCommand);
5465
- program.addCommand(mobileCommand);
5466
6297
  program.addCommand(scanCommand);
5467
6298
  program.addCommand(ideaCommand);
5468
6299
  program.addCommand(doctorCommand);
6300
+ program.addCommand(promptAuditCommand);
5469
6301
  program.parse();