@dunnewold-labs/mr-manager 0.4.2 → 0.4.4

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 +255 -67
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command as Command27 } from "commander";
5
5
  import { existsSync as existsSync16 } from "fs";
6
6
  import { homedir as homedir3 } from "os";
7
- import { join as join13 } from "path";
7
+ import { join as join12 } from "path";
8
8
 
9
9
  // cli/commands/init.ts
10
10
  import { Command } from "commander";
@@ -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.2",
188
+ version: "0.4.4",
189
189
  description: "Mr. Manager - Task and project management CLI",
190
190
  bin: {
191
191
  mr: "./dist/index.mjs"
@@ -504,33 +504,38 @@ import { join as join5 } from "path";
504
504
  import { execSync as execSync2 } from "child_process";
505
505
  import { copyFileSync, existsSync as existsSync4 } from "fs";
506
506
  import { join as join4 } from "path";
507
+ function tryExec(command, cwd) {
508
+ try {
509
+ execSync2(command, { cwd, stdio: "pipe" });
510
+ return true;
511
+ } catch {
512
+ return false;
513
+ }
514
+ }
507
515
  function createWorktree(repoDir, branch, worktreeName) {
508
516
  const wtPath = join4(repoDir, ".mr-worktrees", worktreeName);
509
517
  if (existsSync4(wtPath)) {
510
518
  execSync2(`git checkout ${branch}`, { cwd: wtPath, stdio: "pipe" });
511
519
  return wtPath;
512
520
  }
513
- try {
514
- execSync2(`git fetch origin ${branch}`, { cwd: repoDir, stdio: "pipe" });
515
- } catch {
516
- }
517
- try {
518
- execSync2(`git worktree add "${wtPath}" "origin/${branch}"`, {
521
+ tryExec(`git fetch origin ${branch}`, repoDir);
522
+ const hasRemoteBranch = tryExec(`git rev-parse --verify "origin/${branch}"`, repoDir);
523
+ const hasLocalBranch = tryExec(`git rev-parse --verify "${branch}"`, repoDir);
524
+ if (hasRemoteBranch && !hasLocalBranch) {
525
+ execSync2(`git worktree add -b "${branch}" "${wtPath}" "origin/${branch}"`, {
526
+ cwd: repoDir,
527
+ stdio: "pipe"
528
+ });
529
+ } else if (hasLocalBranch) {
530
+ execSync2(`git worktree add "${wtPath}" "${branch}"`, {
531
+ cwd: repoDir,
532
+ stdio: "pipe"
533
+ });
534
+ } else {
535
+ execSync2(`git worktree add -b "${branch}" "${wtPath}"`, {
519
536
  cwd: repoDir,
520
537
  stdio: "pipe"
521
538
  });
522
- } catch {
523
- try {
524
- execSync2(`git worktree add "${wtPath}" "${branch}"`, {
525
- cwd: repoDir,
526
- stdio: "pipe"
527
- });
528
- } catch {
529
- execSync2(`git worktree add -b "${branch}" "${wtPath}" "origin/${branch}"`, {
530
- cwd: repoDir,
531
- stdio: "pipe"
532
- });
533
- }
534
539
  }
535
540
  for (const envFile of [".env", ".env.local"]) {
536
541
  const src = join4(repoDir, envFile);
@@ -1021,9 +1026,107 @@ function slugify(title) {
1021
1026
  function shortId(id) {
1022
1027
  return id.slice(0, 8);
1023
1028
  }
1029
+ function formatElapsed(ms) {
1030
+ const totalMinutes = Math.max(1, Math.floor(ms / 6e4));
1031
+ const hours = Math.floor(totalMinutes / 60);
1032
+ const minutes = totalMinutes % 60;
1033
+ if (hours > 0) {
1034
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
1035
+ }
1036
+ return `${totalMinutes}m`;
1037
+ }
1024
1038
  function worktreePath(sid) {
1025
1039
  return `.mr-worktrees/mr-${sid}`;
1026
1040
  }
1041
+ function worktreeNameFromPath(wtPath) {
1042
+ return wtPath.replace(/^\.mr-worktrees\//, "");
1043
+ }
1044
+ function normalizeWhitespace(value) {
1045
+ return value.replace(/\s+/g, " ").trim();
1046
+ }
1047
+ function extractFirstMeaningfulParagraph(markdown) {
1048
+ const lines = markdown.split(/\r?\n/);
1049
+ const quoteLines = [];
1050
+ let inCodeFence = false;
1051
+ for (const rawLine of lines) {
1052
+ const line = rawLine.trim();
1053
+ if (line.startsWith("```")) {
1054
+ inCodeFence = !inCodeFence;
1055
+ continue;
1056
+ }
1057
+ if (inCodeFence) continue;
1058
+ if (!line) {
1059
+ if (quoteLines.length > 0) break;
1060
+ continue;
1061
+ }
1062
+ if (line.startsWith(">")) {
1063
+ quoteLines.push(line.replace(/^>\s?/, ""));
1064
+ continue;
1065
+ }
1066
+ if (quoteLines.length > 0) break;
1067
+ }
1068
+ if (quoteLines.length > 0) {
1069
+ const text2 = normalizeWhitespace(quoteLines.join(" "));
1070
+ return text2 || null;
1071
+ }
1072
+ const paragraphLines = [];
1073
+ inCodeFence = false;
1074
+ for (const rawLine of lines) {
1075
+ const line = rawLine.trim();
1076
+ if (line.startsWith("```")) {
1077
+ inCodeFence = !inCodeFence;
1078
+ continue;
1079
+ }
1080
+ if (inCodeFence) continue;
1081
+ if (!line) {
1082
+ if (paragraphLines.length > 0) break;
1083
+ continue;
1084
+ }
1085
+ if (/^(#{1,6}\s|[-*]\s|\d+\.\s|\|)/.test(line)) {
1086
+ if (paragraphLines.length > 0) break;
1087
+ continue;
1088
+ }
1089
+ paragraphLines.push(line);
1090
+ }
1091
+ if (paragraphLines.length === 0) return null;
1092
+ const text = normalizeWhitespace(paragraphLines.join(" "));
1093
+ return text || null;
1094
+ }
1095
+ function truncateText(value, maxLength) {
1096
+ if (value.length <= maxLength) return value;
1097
+ return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
1098
+ }
1099
+ function buildPrBodyTemplate(task, subtasks, protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = []) {
1100
+ const contextSource = task.prdContent ?? task.notes ?? "";
1101
+ const taskContext = extractFirstMeaningfulParagraph(contextSource);
1102
+ const contextBullets = [
1103
+ `- Resolves MR Manager task ${task.id}.`,
1104
+ `- Task: ${task.title}.`,
1105
+ ...taskContext ? [`- Goal/context: ${truncateText(taskContext, 220)}`] : [],
1106
+ ...subtasks.map((subtask) => `- Completed subtask: ${subtask.title}.`),
1107
+ ...protoRefs.map((ref) => `- Related prototype: ${ref.prototype.title}${ref.selectedVariants?.length ? ` (variants ${ref.selectedVariants.join(", ")})` : ""}.`),
1108
+ ...feedbackUpdates.slice(0, 3).map((update) => `- Addresses feedback from ${new Date(update.createdAt).toLocaleDateString("en-US")}.`),
1109
+ ...existingResources.slice(0, 3).map((resource) => `- Referenced resource: ${resource.name}.${resource.kind === "link" && resource.url ? ` (${resource.url})` : ""}`),
1110
+ ...skillRefs.slice(0, 3).map((ref) => `- Applied skill guidance: ${ref.skill.name}.`)
1111
+ ];
1112
+ const testingHint = task.mode === "development" ? "- [ ] Manual verification completed <describe the user flow or CLI behavior checked>." : "- [ ] Verification completed <describe what you validated>.";
1113
+ return [
1114
+ "## Summary",
1115
+ "- <Replace with 2-4 concise bullets describing the actual behavior or code changes in this branch.>",
1116
+ "- <Mention the main files, systems, or workflows that changed.>",
1117
+ "- <Call out any follow-up, rollout note, or migration detail if relevant. Remove this bullet if not needed.>",
1118
+ "",
1119
+ "## Context",
1120
+ ...contextBullets,
1121
+ "",
1122
+ "## Testing",
1123
+ "- [ ] Automated tests: <list the exact command(s) you ran, or replace with `Not run` plus a reason>.",
1124
+ testingHint,
1125
+ "- [ ] Additional verification: <note screenshots, logs, or why no UI/browser flow applies. Remove if not needed.>",
1126
+ "",
1127
+ "_Replace every placeholder above before creating the PR. Remove bullets or sections that do not apply._"
1128
+ ].join("\n");
1129
+ }
1027
1130
  function pullLatestMain(repoDir, prefix) {
1028
1131
  return new Promise((resolve7) => {
1029
1132
  exec(
@@ -1251,11 +1354,13 @@ function buildFeaturesSection(repoDir) {
1251
1354
  ``
1252
1355
  ].join("\n");
1253
1356
  }
1254
- function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = []) {
1357
+ function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = [], executionDir) {
1255
1358
  const sid = shortId(task.id);
1256
1359
  const slug = slugify(task.title);
1257
1360
  const branchName = `mr/${sid}/${slug}`;
1258
1361
  const wtPath = worktreePath(sid);
1362
+ const workingDir = executionDir ?? repoDir;
1363
+ const prBodyPath = "/tmp/mr-pr-body.md";
1259
1364
  const notes = task.prdContent ? `
1260
1365
 
1261
1366
  ## PRD (Product Requirements Document)
@@ -1277,10 +1382,11 @@ ${task.notes}` : "";
1277
1382
  ].join("\n") : "";
1278
1383
  const hasFeedback = feedbackUpdates.length > 0;
1279
1384
  const feedbackWtPath = hasFeedback ? worktreePath(`${sid}-fb`) : wtPath;
1280
- const prCreateCmd = vcs === "gitlab" ? `glab mr create --title "${task.title}" --description "Resolves MR Manager task ${task.id}" --yes` : `gh pr create --title "${task.title}" --body "Resolves MR Manager task ${task.id}"`;
1385
+ const prBodyTemplate = buildPrBodyTemplate(task, pendingSubtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
1386
+ const prCreateCmd = vcs === "gitlab" ? `glab mr create --title "${task.title}" --description-file ${prBodyPath} --yes` : `gh pr create --title "${task.title}" --body-file ${prBodyPath}`;
1281
1387
  return [
1282
1388
  `You are an autonomous agent working on a task from MR Manager.`,
1283
- `Working directory: ${repoDir}`,
1389
+ `Working directory: ${workingDir}`,
1284
1390
  ``,
1285
1391
  `## Task`,
1286
1392
  `Title: ${task.title}`,
@@ -1294,7 +1400,12 @@ ${task.notes}` : "";
1294
1400
  ``,
1295
1401
  `1. **Gather project context first.** Run \`mr context\` to get the project description, current task list, and the last 10 completed tasks. Use this to understand what's already been built, recent decisions, and the overall project direction before you start coding.`,
1296
1402
  ``,
1297
- ...hasFeedback ? [
1403
+ ...executionDir && executionDir !== repoDir ? [
1404
+ `2. Your isolated git worktree is already prepared and this session starts inside it.`,
1405
+ ` - Current working directory: ${executionDir}`,
1406
+ ` - Branch checked out in this worktree: ${branchName}`,
1407
+ ` - If the task spans additional repos, create matching worktrees there as needed.`
1408
+ ] : hasFeedback ? [
1298
1409
  `2. Set up your working environment:`,
1299
1410
  ` - Fetch the latest from origin: \`git fetch origin\``,
1300
1411
  ` - Check out the existing branch in a worktree: \`git worktree add ${feedbackWtPath} origin/${branchName}\``,
@@ -1319,7 +1430,9 @@ ${task.notes}` : "";
1319
1430
  ...hasFeedback ? [
1320
1431
  ` c. The existing ${vcs === "gitlab" ? "merge request" : "pull request"} will be updated automatically when you push to the branch. No need to create a new one.`
1321
1432
  ] : [
1322
- ` c. Open a ${vcs === "gitlab" ? "merge request" : "pull request"}: \`${prCreateCmd}\``
1433
+ ` c. Write a structured ${vcs === "gitlab" ? "merge request" : "pull request"} description to \`${prBodyPath}\` using the template below.`,
1434
+ ` Replace every placeholder with concrete implementation details before you create the ${vcs === "gitlab" ? "MR" : "PR"}.`,
1435
+ ` d. Open a ${vcs === "gitlab" ? "merge request" : "pull request"}: \`${prCreateCmd}\``
1323
1436
  ],
1324
1437
  ``,
1325
1438
  `5. Clean up any worktrees you created: \`git worktree remove --force <path>\``,
@@ -1348,6 +1461,17 @@ ${task.notes}` : "";
1348
1461
  ``,
1349
1462
  `Keep messages short (1 sentence). Post 3-5 updates total.`,
1350
1463
  ``,
1464
+ ...hasFeedback ? [] : [
1465
+ `## PR Description Template`,
1466
+ ``,
1467
+ `Write this template to \`${prBodyPath}\`, then replace the placeholders with the actual details from your implementation.`,
1468
+ `Delete any bullet or section that does not apply, but keep the final description specific and reviewer-friendly.`,
1469
+ ``,
1470
+ "```md",
1471
+ prBodyTemplate,
1472
+ "```",
1473
+ ``
1474
+ ],
1351
1475
  `## Screenshots`,
1352
1476
  ``,
1353
1477
  `Before you finish, take a screenshot of your work to attach to the task. This helps the reviewer see what changed visually.`,
@@ -1785,9 +1909,13 @@ function buildIdeaPrompt(idea, repoDir) {
1785
1909
  }
1786
1910
  function buildAgentArgs(agent, prompt2, mode, sessionId, name) {
1787
1911
  if (agent === "codex") {
1788
- const args = ["exec"];
1912
+ const args = [];
1789
1913
  if (mode === "execute") {
1790
- args.push("-a", "never", "-s", "danger-full-access");
1914
+ args.push("-a", "never");
1915
+ }
1916
+ args.push("exec");
1917
+ if (mode === "execute") {
1918
+ args.push("-s", "danger-full-access");
1791
1919
  }
1792
1920
  args.push(prompt2);
1793
1921
  return { bin: "codex", args };
@@ -1890,6 +2018,8 @@ var watchCommand = new Command8("watch").description(
1890
2018
  const agent = opts.agent === "codex" ? "codex" : opts.agent === "gemini" ? "gemini" : "claude";
1891
2019
  const scanAt = opts.scanAt;
1892
2020
  const taskStallTimeoutMs = getTaskStallTimeoutMs();
2021
+ const hungTaskTimeoutMinutes = Math.max(5, parseInt(process.env.MR_WATCH_HUNG_TASK_TIMEOUT_MINUTES ?? "60", 10) || 60);
2022
+ const hungTaskTimeoutMs = hungTaskTimeoutMinutes * 6e4;
1893
2023
  const active = /* @__PURE__ */ new Map();
1894
2024
  const failed = /* @__PURE__ */ new Map();
1895
2025
  const queued = /* @__PURE__ */ new Set();
@@ -1904,7 +2034,8 @@ var watchCommand = new Command8("watch").description(
1904
2034
  `stall-timeout=${paint("cyan", formatTimeoutMinutes(taskStallTimeoutMs))}`,
1905
2035
  ...planApproval ? [paint("yellow", "plan-approval")] : [],
1906
2036
  ...dryRun ? [paint("yellow", "dry-run")] : [],
1907
- ...scanAt ? [`scan-at=${paint("cyan", scanAt)}`] : []
2037
+ ...scanAt ? [`scan-at=${paint("cyan", scanAt)}`] : [],
2038
+ `hung-timeout=${paint("cyan", `${hungTaskTimeoutMinutes}m`)}`
1908
2039
  ].join(" ");
1909
2040
  const banner = [
1910
2041
  ``,
@@ -1918,6 +2049,17 @@ var watchCommand = new Command8("watch").description(
1918
2049
  console.log(banner);
1919
2050
  console.log(` ${flags}
1920
2051
  `);
2052
+ async function moveTaskToError(task, prefix, reason) {
2053
+ try {
2054
+ await api.patch(`/api/tasks/${task.id}`, { status: "error" });
2055
+ } catch (err) {
2056
+ logError(prefix, `Failed to mark task as error: ${err.message}`);
2057
+ }
2058
+ try {
2059
+ await postTaskUpdate(task.id, reason, "system");
2060
+ } catch {
2061
+ }
2062
+ }
1921
2063
  async function dispatchTask(task, repoDir) {
1922
2064
  const sid = shortId(task.id);
1923
2065
  const slug = slugify(task.title);
@@ -1969,19 +2111,34 @@ var watchCommand = new Command8("watch").description(
1969
2111
  }
1970
2112
  } catch {
1971
2113
  }
1972
- const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs);
2114
+ const hasFeedback = feedbackUpdates.length > 0;
2115
+ const desiredWorktreePath = hasFeedback ? worktreePath(`${sid}-fb`) : worktreePath(sid);
2116
+ let executionDir = repoDir;
2117
+ let cleanupWorktreePath;
2118
+ if (isGitRepo(repoDir)) {
2119
+ executionDir = createWorktree(
2120
+ repoDir,
2121
+ branchName,
2122
+ worktreeNameFromPath(desiredWorktreePath)
2123
+ );
2124
+ cleanupWorktreePath = executionDir;
2125
+ logInfo(prefix, `Prepared worktree ${paint("cyan", executionDir)}`);
2126
+ }
2127
+ const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs, executionDir);
1973
2128
  const sessionId = agent === "claude" ? randomUUID() : void 0;
1974
2129
  const activeEntry = {
1975
2130
  process: void 0,
1976
2131
  title: task.title,
1977
- repoDir,
2132
+ repoDir: executionDir,
2133
+ cleanupRepoDir: cleanupWorktreePath ? repoDir : void 0,
2134
+ cleanupWorktreePath,
1978
2135
  startedAt: Date.now(),
1979
2136
  lastActivityAt: Date.now()
1980
2137
  };
1981
2138
  const touchActivity = () => {
1982
2139
  activeEntry.lastActivityAt = Date.now();
1983
2140
  };
1984
- const child = spawnAgent(agent, repoDir, prompt2, prefix, touchActivity, sessionId, task.title);
2141
+ const child = spawnAgent(agent, executionDir, prompt2, prefix, touchActivity, sessionId, task.title);
1985
2142
  activeEntry.process = child;
1986
2143
  if (sessionId) {
1987
2144
  api.patch(`/api/tasks/${task.id}`, { claudeSessionId: sessionId }).catch(() => {
@@ -1995,7 +2152,7 @@ var watchCommand = new Command8("watch").description(
1995
2152
  try {
1996
2153
  if (code === 0) {
1997
2154
  try {
1998
- const researchPath = resolve2(repoDir, "research.md");
2155
+ const researchPath = resolve2(executionDir, "research.md");
1999
2156
  if (existsSync7(researchPath)) {
2000
2157
  try {
2001
2158
  const researchContent = readFileSync5(researchPath, "utf-8");
@@ -2018,7 +2175,7 @@ var watchCommand = new Command8("watch").description(
2018
2175
  logWarn(prefix, `Failed to upload research resource: ${err.message}`);
2019
2176
  }
2020
2177
  }
2021
- const noMrPath = resolve2(repoDir, ".mr-no-mr");
2178
+ const noMrPath = resolve2(executionDir, ".mr-no-mr");
2022
2179
  const noMrRequested = existsSync7(noMrPath);
2023
2180
  let noMrDescription;
2024
2181
  if (noMrRequested) {
@@ -2053,10 +2210,14 @@ var watchCommand = new Command8("watch").description(
2053
2210
  logError(prefix, `Failed to update task: ${err.message}`);
2054
2211
  }
2055
2212
  } else if (!activeEntry.terminatedForError) {
2056
- logError(prefix, `"${paint("bold", task.title)}" failed (exit ${code}), leaving status unchanged`);
2057
- await postTaskUpdate(task.id, `Agent failed with exit code ${code}`, "system");
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);
2058
2216
  }
2059
2217
  } finally {
2218
+ if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
2219
+ removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
2220
+ }
2060
2221
  queued.delete(task.id);
2061
2222
  finishing.delete(task.id);
2062
2223
  }
@@ -2091,8 +2252,8 @@ var watchCommand = new Command8("watch").description(
2091
2252
  "system"
2092
2253
  );
2093
2254
  const prompt2 = buildPrdPrompt(task, repoDir, existingPlanResource?.content, feedbackUpdates);
2094
- const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, task.title);
2095
- active.set(task.id, { process: child, title: task.title, repoDir });
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() });
2096
2257
  child.on("exit", async (code) => {
2097
2258
  active.delete(task.id);
2098
2259
  finishing.add(task.id);
@@ -2137,8 +2298,13 @@ var watchCommand = new Command8("watch").description(
2137
2298
  logError(prefix, `Failed to update task: ${err.message}`);
2138
2299
  }
2139
2300
  } else {
2140
- logError(prefix, `"${paint("bold", task.title)}" PRD generation failed (exit ${code}), leaving status unchanged`);
2141
- await postTaskUpdate(task.id, `PRD generation failed with exit code ${code}`, "system");
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}`);
2306
+ }
2307
+ await postTaskUpdate(task.id, `PRD generation failed with exit code ${code} \u2014 task moved to error`, "system");
2142
2308
  }
2143
2309
  } finally {
2144
2310
  queued.delete(task.id);
@@ -2171,7 +2337,7 @@ var watchCommand = new Command8("watch").description(
2171
2337
  prompt2 = buildPrototypePrompt(proto, repoDir);
2172
2338
  }
2173
2339
  const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, proto.title);
2174
- active.set(`proto-${proto.id}`, { process: child, title: proto.title, repoDir });
2340
+ active.set(`proto-${proto.id}`, { process: child, title: proto.title, repoDir, startedAt: Date.now() });
2175
2341
  child.on("exit", async (code) => {
2176
2342
  const key = `proto-${proto.id}`;
2177
2343
  active.delete(key);
@@ -2228,7 +2394,7 @@ var watchCommand = new Command8("watch").description(
2228
2394
  }
2229
2395
  const prompt2 = buildRepoCreationPrompt(project, workDir);
2230
2396
  const child = spawnAgent(agent, workDir, prompt2, prefix, void 0, project.name);
2231
- active.set(`repo-${project.id}`, { process: child, title: project.name, repoDir: workDir });
2397
+ active.set(`repo-${project.id}`, { process: child, title: project.name, repoDir: workDir, startedAt: Date.now() });
2232
2398
  child.on("exit", async (code) => {
2233
2399
  const key = `repo-${project.id}`;
2234
2400
  active.delete(key);
@@ -2261,7 +2427,7 @@ var watchCommand = new Command8("watch").description(
2261
2427
  }
2262
2428
  const prompt2 = buildIdeaPrompt(idea, repoDir);
2263
2429
  const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, idea.title);
2264
- active.set(`idea-${idea.id}`, { process: child, title: idea.title, repoDir });
2430
+ active.set(`idea-${idea.id}`, { process: child, title: idea.title, repoDir, startedAt: Date.now() });
2265
2431
  child.on("exit", async (code) => {
2266
2432
  const key = `idea-${idea.id}`;
2267
2433
  active.delete(key);
@@ -2363,7 +2529,7 @@ var watchCommand = new Command8("watch").description(
2363
2529
  queued.delete(key);
2364
2530
  failed.set(key, err.message);
2365
2531
  });
2366
- active.set(key, { process: scanProc, title: `scan-${scan.id.slice(0, 8)}`, repoDir: rootDir });
2532
+ active.set(key, { process: scanProc, title: `scan-${scan.id.slice(0, 8)}`, repoDir: rootDir, startedAt: Date.now() });
2367
2533
  scanProc.stdout?.on("data", (d) => {
2368
2534
  const lines = d.toString().trim().split("\n");
2369
2535
  for (const line of lines) {
@@ -2440,7 +2606,29 @@ ${divider}`);
2440
2606
  }
2441
2607
  const nonTestQueued = queuedTasks.filter((t) => t.mode !== "testing");
2442
2608
  const nonTestDelegated = delegatedTasks.filter((t) => t.mode !== "testing");
2443
- const tasks = [...nonTestQueued, ...nonTestDelegated];
2609
+ const staleDelegatedTasks = nonTestDelegated.filter((task) => {
2610
+ if (!task.inProgressSince) return false;
2611
+ return Date.now() - new Date(task.inProgressSince).getTime() >= hungTaskTimeoutMs;
2612
+ });
2613
+ for (const task of staleDelegatedTasks) {
2614
+ const prefix = taskTag(shortId(task.id));
2615
+ const running = active.get(task.id);
2616
+ if (running) {
2617
+ logWarn(prefix, `Task exceeded hang timeout after ${formatElapsed(Date.now() - running.startedAt)}, terminating agent\u2026`);
2618
+ running.process.kill("SIGTERM");
2619
+ active.delete(task.id);
2620
+ }
2621
+ queued.delete(task.id);
2622
+ finishing.delete(task.id);
2623
+ const elapsed = task.inProgressSince ? formatElapsed(Date.now() - new Date(task.inProgressSince).getTime()) : `${hungTaskTimeoutMinutes}m`;
2624
+ await moveTaskToError(
2625
+ task,
2626
+ prefix,
2627
+ `Agent appears hung after ${elapsed} without finishing \u2014 task moved to error`
2628
+ );
2629
+ }
2630
+ const staleTaskIds = new Set(staleDelegatedTasks.map((task) => task.id));
2631
+ const tasks = [...nonTestQueued, ...nonTestDelegated.filter((task) => !staleTaskIds.has(task.id))];
2444
2632
  const config = loadConfig();
2445
2633
  const activeTaskIds = new Set(tasks.map((t) => t.id));
2446
2634
  for (const task of tasks) {
@@ -3433,7 +3621,7 @@ var updateCommand = new Command15("update").description("Post a status update to
3433
3621
  // cli/commands/screenshot.ts
3434
3622
  import { Command as Command16 } from "commander";
3435
3623
  import { readFileSync as readFileSync6, existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
3436
- import { join as join8 } from "path";
3624
+ import { join as join7 } from "path";
3437
3625
  import { tmpdir } from "os";
3438
3626
  var screenshotCommand = new Command16("screenshot").description(
3439
3627
  "Take or attach a screenshot to a task update (agents use this to show their work)"
@@ -3441,7 +3629,7 @@ var screenshotCommand = new Command16("screenshot").description(
3441
3629
  let filePath = file;
3442
3630
  let tempFile = null;
3443
3631
  if (!filePath) {
3444
- tempFile = join8(tmpdir(), `mr-screenshot-${Date.now()}.png`);
3632
+ tempFile = join7(tmpdir(), `mr-screenshot-${Date.now()}.png`);
3445
3633
  try {
3446
3634
  const config2 = loadConfig();
3447
3635
  let targetUrl = opts.url;
@@ -3615,8 +3803,8 @@ var resumeCommand = new Command17("resume").description("Resume an interactive C
3615
3803
  import { Command as Command18 } from "commander";
3616
3804
  import { execSync as execSync5, spawn as spawn6 } from "child_process";
3617
3805
  import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
3618
- import { join as join9 } from "path";
3619
- var BROWSE_DIR2 = join9(import.meta.dirname, "..", "..", "browse");
3806
+ import { join as join8 } from "path";
3807
+ var BROWSE_DIR2 = join8(import.meta.dirname, "..", "..", "browse");
3620
3808
  function isProcessAlive(pid) {
3621
3809
  try {
3622
3810
  process.kill(pid, 0);
@@ -3657,7 +3845,7 @@ async function ensureDevServer() {
3657
3845
  const devProc = spawn6("npm", ["run", "dev", "--", "--port", String(port)], {
3658
3846
  stdio: ["ignore", "pipe", "pipe"],
3659
3847
  detached: true,
3660
- cwd: join9(import.meta.dirname, "..", ".."),
3848
+ cwd: join8(import.meta.dirname, "..", ".."),
3661
3849
  env: { ...process.env }
3662
3850
  });
3663
3851
  devProc.unref();
@@ -4084,7 +4272,7 @@ import { spawn as spawn7 } from "child_process";
4084
4272
 
4085
4273
  // lib/scanner/config.ts
4086
4274
  import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
4087
- import { join as join10 } from "path";
4275
+ import { join as join9 } from "path";
4088
4276
  var ALL_FINDING_TYPES = [
4089
4277
  "idea",
4090
4278
  "bug",
@@ -4100,7 +4288,7 @@ var DEFAULTS = {
4100
4288
  findingTypes: ALL_FINDING_TYPES
4101
4289
  };
4102
4290
  function loadScanConfig(projectPath) {
4103
- const configPath2 = join10(projectPath, ".mr-scan.json");
4291
+ const configPath2 = join9(projectPath, ".mr-scan.json");
4104
4292
  if (!existsSync13(configPath2)) {
4105
4293
  return { ...DEFAULTS };
4106
4294
  }
@@ -4148,11 +4336,11 @@ async function authenticateBrowseSession(magicUrl, runBrowse) {
4148
4336
 
4149
4337
  // lib/scanner/codebase-analysis.ts
4150
4338
  import { readdirSync as readdirSync2, readFileSync as readFileSync11, existsSync as existsSync14 } from "fs";
4151
- import { join as join11, relative } from "path";
4339
+ import { join as join10, relative } from "path";
4152
4340
  import { execSync as execSync6 } from "child_process";
4153
4341
  function resolveDir(projectPath, candidates) {
4154
4342
  for (const candidate of candidates) {
4155
- const dir = join11(projectPath, candidate);
4343
+ const dir = join10(projectPath, candidate);
4156
4344
  if (existsSync14(dir)) return dir;
4157
4345
  }
4158
4346
  return null;
@@ -4172,10 +4360,10 @@ function discoverRoutes(projectPath) {
4172
4360
  segment = `:${segment.slice(1, -1)}`;
4173
4361
  }
4174
4362
  if (segment.startsWith("(")) {
4175
- walk(join11(dir, entry.name), routePath);
4363
+ walk(join10(dir, entry.name), routePath);
4176
4364
  continue;
4177
4365
  }
4178
- walk(join11(dir, entry.name), `${routePath}/${segment}`);
4366
+ walk(join10(dir, entry.name), `${routePath}/${segment}`);
4179
4367
  }
4180
4368
  if (entry.name === "page.tsx" || entry.name === "page.ts") {
4181
4369
  routes.push(routePath || "/");
@@ -4186,7 +4374,7 @@ function discoverRoutes(projectPath) {
4186
4374
  return routes;
4187
4375
  }
4188
4376
  function extractModels(projectPath) {
4189
- const schemaPath = join11(projectPath, "prisma", "schema.prisma");
4377
+ const schemaPath = join10(projectPath, "prisma", "schema.prisma");
4190
4378
  if (existsSync14(schemaPath)) {
4191
4379
  const content = readFileSync11(schemaPath, "utf-8");
4192
4380
  const models2 = [];
@@ -4200,14 +4388,14 @@ function extractModels(projectPath) {
4200
4388
  const models = [];
4201
4389
  const drizzleDirs = ["src/db", "src/schema", "db", "drizzle"];
4202
4390
  for (const dir of drizzleDirs) {
4203
- const fullDir = join11(projectPath, dir);
4391
+ const fullDir = join10(projectPath, dir);
4204
4392
  if (!existsSync14(fullDir)) continue;
4205
4393
  try {
4206
4394
  const entries = readdirSync2(fullDir, { withFileTypes: true });
4207
4395
  for (const entry of entries) {
4208
4396
  if (!entry.isFile() || !entry.name.endsWith(".ts") && !entry.name.endsWith(".js")) continue;
4209
4397
  try {
4210
- const content = readFileSync11(join11(fullDir, entry.name), "utf-8");
4398
+ const content = readFileSync11(join10(fullDir, entry.name), "utf-8");
4211
4399
  const tableRegex = /(?:pg|mysql|sqlite)Table\(\s*["'](\w+)["']/g;
4212
4400
  let match;
4213
4401
  while ((match = tableRegex.exec(content)) !== null) {
@@ -4230,9 +4418,9 @@ function discoverComponents(projectPath) {
4230
4418
  for (const entry of entries) {
4231
4419
  if (entry.isDirectory()) {
4232
4420
  if (entry.name === "ui") continue;
4233
- walk(join11(dir, entry.name));
4421
+ walk(join10(dir, entry.name));
4234
4422
  } else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
4235
- components.push(relative(projectPath, join11(dir, entry.name)));
4423
+ components.push(relative(projectPath, join10(dir, entry.name)));
4236
4424
  }
4237
4425
  }
4238
4426
  }
@@ -4247,10 +4435,10 @@ function extractInternalLinks(projectPath) {
4247
4435
  for (const entry of entries) {
4248
4436
  if (entry.isDirectory()) {
4249
4437
  if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "ui") continue;
4250
- searchDir(join11(dir, entry.name));
4438
+ searchDir(join10(dir, entry.name));
4251
4439
  } else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
4252
4440
  try {
4253
- const content = readFileSync11(join11(dir, entry.name), "utf-8");
4441
+ const content = readFileSync11(join10(dir, entry.name), "utf-8");
4254
4442
  const hrefRegex = /href=["'`](\/[^"'`]*?)["'`]/g;
4255
4443
  const pushRegex = /router\.push\(["'`](\/[^"'`]*?)["'`]\)/g;
4256
4444
  let match;
@@ -4266,10 +4454,10 @@ function extractInternalLinks(projectPath) {
4266
4454
  }
4267
4455
  }
4268
4456
  for (const candidate of ["app", "src/app"]) {
4269
- searchDir(join11(projectPath, candidate));
4457
+ searchDir(join10(projectPath, candidate));
4270
4458
  }
4271
4459
  for (const candidate of ["components", "src/components"]) {
4272
- searchDir(join11(projectPath, candidate));
4460
+ searchDir(join10(projectPath, candidate));
4273
4461
  }
4274
4462
  return Array.from(links);
4275
4463
  }
@@ -5122,9 +5310,9 @@ var ideaCommand = new Command25("idea").description("Manage ideas \u2014 brainst
5122
5310
  import { Command as Command26 } from "commander";
5123
5311
  import { existsSync as existsSync15 } from "fs";
5124
5312
  import { homedir as homedir2 } from "os";
5125
- import { join as join12 } from "path";
5313
+ import { join as join11 } from "path";
5126
5314
  async function checkConfigExists() {
5127
- const configPath2 = join12(homedir2(), ".mr-manager", "config.json");
5315
+ const configPath2 = join11(homedir2(), ".mr-manager", "config.json");
5128
5316
  const exists = existsSync15(configPath2);
5129
5317
  if (!exists) {
5130
5318
  return {
@@ -5210,7 +5398,7 @@ var doctorCommand = new Command26("doctor").description("Diagnose Mr. Manager CL
5210
5398
  });
5211
5399
 
5212
5400
  // cli/index.ts
5213
- var configPath = join13(homedir3(), ".mr-manager", "config.json");
5401
+ var configPath = join12(homedir3(), ".mr-manager", "config.json");
5214
5402
  var isFirstRun = !existsSync16(configPath);
5215
5403
  var userArgs = process.argv.slice(2);
5216
5404
  var bypassCommands = /* @__PURE__ */ new Set(["login", "init", "auth", "help", "--help", "-h", "--version", "-V", "doctor", "setup"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunnewold-labs/mr-manager",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Mr. Manager - Task and project management CLI",
5
5
  "bin": {
6
6
  "mr": "./dist/index.mjs"