@dunnewold-labs/mr-manager 0.4.2 → 0.4.3

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 +253 -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.3",
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 };
@@ -1904,7 +2032,8 @@ var watchCommand = new Command8("watch").description(
1904
2032
  `stall-timeout=${paint("cyan", formatTimeoutMinutes(taskStallTimeoutMs))}`,
1905
2033
  ...planApproval ? [paint("yellow", "plan-approval")] : [],
1906
2034
  ...dryRun ? [paint("yellow", "dry-run")] : [],
1907
- ...scanAt ? [`scan-at=${paint("cyan", scanAt)}`] : []
2035
+ ...scanAt ? [`scan-at=${paint("cyan", scanAt)}`] : [],
2036
+ `hung-timeout=${paint("cyan", `${hungTaskTimeoutMinutes}m`)}`
1908
2037
  ].join(" ");
1909
2038
  const banner = [
1910
2039
  ``,
@@ -1918,6 +2047,17 @@ var watchCommand = new Command8("watch").description(
1918
2047
  console.log(banner);
1919
2048
  console.log(` ${flags}
1920
2049
  `);
2050
+ async function moveTaskToError(task, prefix, reason) {
2051
+ try {
2052
+ await api.patch(`/api/tasks/${task.id}`, { status: "error" });
2053
+ } catch (err) {
2054
+ logError(prefix, `Failed to mark task as error: ${err.message}`);
2055
+ }
2056
+ try {
2057
+ await postTaskUpdate(task.id, reason, "system");
2058
+ } catch {
2059
+ }
2060
+ }
1921
2061
  async function dispatchTask(task, repoDir) {
1922
2062
  const sid = shortId(task.id);
1923
2063
  const slug = slugify(task.title);
@@ -1969,19 +2109,34 @@ var watchCommand = new Command8("watch").description(
1969
2109
  }
1970
2110
  } catch {
1971
2111
  }
1972
- const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs);
2112
+ const hasFeedback = feedbackUpdates.length > 0;
2113
+ const desiredWorktreePath = hasFeedback ? worktreePath(`${sid}-fb`) : worktreePath(sid);
2114
+ let executionDir = repoDir;
2115
+ let cleanupWorktreePath;
2116
+ if (isGitRepo(repoDir)) {
2117
+ executionDir = createWorktree(
2118
+ repoDir,
2119
+ branchName,
2120
+ worktreeNameFromPath(desiredWorktreePath)
2121
+ );
2122
+ cleanupWorktreePath = executionDir;
2123
+ logInfo(prefix, `Prepared worktree ${paint("cyan", executionDir)}`);
2124
+ }
2125
+ const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs, executionDir);
1973
2126
  const sessionId = agent === "claude" ? randomUUID() : void 0;
1974
2127
  const activeEntry = {
1975
2128
  process: void 0,
1976
2129
  title: task.title,
1977
- repoDir,
2130
+ repoDir: executionDir,
2131
+ cleanupRepoDir: cleanupWorktreePath ? repoDir : void 0,
2132
+ cleanupWorktreePath,
1978
2133
  startedAt: Date.now(),
1979
2134
  lastActivityAt: Date.now()
1980
2135
  };
1981
2136
  const touchActivity = () => {
1982
2137
  activeEntry.lastActivityAt = Date.now();
1983
2138
  };
1984
- const child = spawnAgent(agent, repoDir, prompt2, prefix, touchActivity, sessionId, task.title);
2139
+ const child = spawnAgent(agent, executionDir, prompt2, prefix, touchActivity, sessionId, task.title);
1985
2140
  activeEntry.process = child;
1986
2141
  if (sessionId) {
1987
2142
  api.patch(`/api/tasks/${task.id}`, { claudeSessionId: sessionId }).catch(() => {
@@ -1995,7 +2150,7 @@ var watchCommand = new Command8("watch").description(
1995
2150
  try {
1996
2151
  if (code === 0) {
1997
2152
  try {
1998
- const researchPath = resolve2(repoDir, "research.md");
2153
+ const researchPath = resolve2(executionDir, "research.md");
1999
2154
  if (existsSync7(researchPath)) {
2000
2155
  try {
2001
2156
  const researchContent = readFileSync5(researchPath, "utf-8");
@@ -2018,7 +2173,7 @@ var watchCommand = new Command8("watch").description(
2018
2173
  logWarn(prefix, `Failed to upload research resource: ${err.message}`);
2019
2174
  }
2020
2175
  }
2021
- const noMrPath = resolve2(repoDir, ".mr-no-mr");
2176
+ const noMrPath = resolve2(executionDir, ".mr-no-mr");
2022
2177
  const noMrRequested = existsSync7(noMrPath);
2023
2178
  let noMrDescription;
2024
2179
  if (noMrRequested) {
@@ -2053,10 +2208,14 @@ var watchCommand = new Command8("watch").description(
2053
2208
  logError(prefix, `Failed to update task: ${err.message}`);
2054
2209
  }
2055
2210
  } 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");
2211
+ const reason = `Agent failed with exit code ${code} \u2014 task moved to error`;
2212
+ logError(prefix, `"${paint("bold", task.title)}" failed (exit ${code}), moving task to error`);
2213
+ await moveTaskToError(task, prefix, reason);
2058
2214
  }
2059
2215
  } finally {
2216
+ if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
2217
+ removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
2218
+ }
2060
2219
  queued.delete(task.id);
2061
2220
  finishing.delete(task.id);
2062
2221
  }
@@ -2091,8 +2250,8 @@ var watchCommand = new Command8("watch").description(
2091
2250
  "system"
2092
2251
  );
2093
2252
  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 });
2253
+ const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, void 0, task.title);
2254
+ active.set(task.id, { process: child, title: task.title, repoDir, startedAt: Date.now() });
2096
2255
  child.on("exit", async (code) => {
2097
2256
  active.delete(task.id);
2098
2257
  finishing.add(task.id);
@@ -2137,8 +2296,13 @@ var watchCommand = new Command8("watch").description(
2137
2296
  logError(prefix, `Failed to update task: ${err.message}`);
2138
2297
  }
2139
2298
  } 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");
2299
+ logError(prefix, `"${paint("bold", task.title)}" PRD generation failed (exit ${code}), marked as error`);
2300
+ try {
2301
+ await api.patch(`/api/tasks/${task.id}`, { status: "error" });
2302
+ } catch (err) {
2303
+ logError(prefix, `Failed to mark task as error: ${err.message}`);
2304
+ }
2305
+ await postTaskUpdate(task.id, `PRD generation failed with exit code ${code} \u2014 task moved to error`, "system");
2142
2306
  }
2143
2307
  } finally {
2144
2308
  queued.delete(task.id);
@@ -2171,7 +2335,7 @@ var watchCommand = new Command8("watch").description(
2171
2335
  prompt2 = buildPrototypePrompt(proto, repoDir);
2172
2336
  }
2173
2337
  const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, proto.title);
2174
- active.set(`proto-${proto.id}`, { process: child, title: proto.title, repoDir });
2338
+ active.set(`proto-${proto.id}`, { process: child, title: proto.title, repoDir, startedAt: Date.now() });
2175
2339
  child.on("exit", async (code) => {
2176
2340
  const key = `proto-${proto.id}`;
2177
2341
  active.delete(key);
@@ -2228,7 +2392,7 @@ var watchCommand = new Command8("watch").description(
2228
2392
  }
2229
2393
  const prompt2 = buildRepoCreationPrompt(project, workDir);
2230
2394
  const child = spawnAgent(agent, workDir, prompt2, prefix, void 0, project.name);
2231
- active.set(`repo-${project.id}`, { process: child, title: project.name, repoDir: workDir });
2395
+ active.set(`repo-${project.id}`, { process: child, title: project.name, repoDir: workDir, startedAt: Date.now() });
2232
2396
  child.on("exit", async (code) => {
2233
2397
  const key = `repo-${project.id}`;
2234
2398
  active.delete(key);
@@ -2261,7 +2425,7 @@ var watchCommand = new Command8("watch").description(
2261
2425
  }
2262
2426
  const prompt2 = buildIdeaPrompt(idea, repoDir);
2263
2427
  const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, idea.title);
2264
- active.set(`idea-${idea.id}`, { process: child, title: idea.title, repoDir });
2428
+ active.set(`idea-${idea.id}`, { process: child, title: idea.title, repoDir, startedAt: Date.now() });
2265
2429
  child.on("exit", async (code) => {
2266
2430
  const key = `idea-${idea.id}`;
2267
2431
  active.delete(key);
@@ -2363,7 +2527,7 @@ var watchCommand = new Command8("watch").description(
2363
2527
  queued.delete(key);
2364
2528
  failed.set(key, err.message);
2365
2529
  });
2366
- active.set(key, { process: scanProc, title: `scan-${scan.id.slice(0, 8)}`, repoDir: rootDir });
2530
+ active.set(key, { process: scanProc, title: `scan-${scan.id.slice(0, 8)}`, repoDir: rootDir, startedAt: Date.now() });
2367
2531
  scanProc.stdout?.on("data", (d) => {
2368
2532
  const lines = d.toString().trim().split("\n");
2369
2533
  for (const line of lines) {
@@ -2440,7 +2604,29 @@ ${divider}`);
2440
2604
  }
2441
2605
  const nonTestQueued = queuedTasks.filter((t) => t.mode !== "testing");
2442
2606
  const nonTestDelegated = delegatedTasks.filter((t) => t.mode !== "testing");
2443
- const tasks = [...nonTestQueued, ...nonTestDelegated];
2607
+ const staleDelegatedTasks = nonTestDelegated.filter((task) => {
2608
+ if (!task.inProgressSince) return false;
2609
+ return Date.now() - new Date(task.inProgressSince).getTime() >= hungTaskTimeoutMs;
2610
+ });
2611
+ for (const task of staleDelegatedTasks) {
2612
+ const prefix = taskTag(shortId(task.id));
2613
+ const running = active.get(task.id);
2614
+ if (running) {
2615
+ logWarn(prefix, `Task exceeded hang timeout after ${formatElapsed(Date.now() - running.startedAt)}, terminating agent\u2026`);
2616
+ running.process.kill("SIGTERM");
2617
+ active.delete(task.id);
2618
+ }
2619
+ queued.delete(task.id);
2620
+ finishing.delete(task.id);
2621
+ const elapsed = task.inProgressSince ? formatElapsed(Date.now() - new Date(task.inProgressSince).getTime()) : `${hungTaskTimeoutMinutes}m`;
2622
+ await moveTaskToError(
2623
+ task,
2624
+ prefix,
2625
+ `Agent appears hung after ${elapsed} without finishing \u2014 task moved to error`
2626
+ );
2627
+ }
2628
+ const staleTaskIds = new Set(staleDelegatedTasks.map((task) => task.id));
2629
+ const tasks = [...nonTestQueued, ...nonTestDelegated.filter((task) => !staleTaskIds.has(task.id))];
2444
2630
  const config = loadConfig();
2445
2631
  const activeTaskIds = new Set(tasks.map((t) => t.id));
2446
2632
  for (const task of tasks) {
@@ -3433,7 +3619,7 @@ var updateCommand = new Command15("update").description("Post a status update to
3433
3619
  // cli/commands/screenshot.ts
3434
3620
  import { Command as Command16 } from "commander";
3435
3621
  import { readFileSync as readFileSync6, existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
3436
- import { join as join8 } from "path";
3622
+ import { join as join7 } from "path";
3437
3623
  import { tmpdir } from "os";
3438
3624
  var screenshotCommand = new Command16("screenshot").description(
3439
3625
  "Take or attach a screenshot to a task update (agents use this to show their work)"
@@ -3441,7 +3627,7 @@ var screenshotCommand = new Command16("screenshot").description(
3441
3627
  let filePath = file;
3442
3628
  let tempFile = null;
3443
3629
  if (!filePath) {
3444
- tempFile = join8(tmpdir(), `mr-screenshot-${Date.now()}.png`);
3630
+ tempFile = join7(tmpdir(), `mr-screenshot-${Date.now()}.png`);
3445
3631
  try {
3446
3632
  const config2 = loadConfig();
3447
3633
  let targetUrl = opts.url;
@@ -3615,8 +3801,8 @@ var resumeCommand = new Command17("resume").description("Resume an interactive C
3615
3801
  import { Command as Command18 } from "commander";
3616
3802
  import { execSync as execSync5, spawn as spawn6 } from "child_process";
3617
3803
  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");
3804
+ import { join as join8 } from "path";
3805
+ var BROWSE_DIR2 = join8(import.meta.dirname, "..", "..", "browse");
3620
3806
  function isProcessAlive(pid) {
3621
3807
  try {
3622
3808
  process.kill(pid, 0);
@@ -3657,7 +3843,7 @@ async function ensureDevServer() {
3657
3843
  const devProc = spawn6("npm", ["run", "dev", "--", "--port", String(port)], {
3658
3844
  stdio: ["ignore", "pipe", "pipe"],
3659
3845
  detached: true,
3660
- cwd: join9(import.meta.dirname, "..", ".."),
3846
+ cwd: join8(import.meta.dirname, "..", ".."),
3661
3847
  env: { ...process.env }
3662
3848
  });
3663
3849
  devProc.unref();
@@ -4084,7 +4270,7 @@ import { spawn as spawn7 } from "child_process";
4084
4270
 
4085
4271
  // lib/scanner/config.ts
4086
4272
  import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
4087
- import { join as join10 } from "path";
4273
+ import { join as join9 } from "path";
4088
4274
  var ALL_FINDING_TYPES = [
4089
4275
  "idea",
4090
4276
  "bug",
@@ -4100,7 +4286,7 @@ var DEFAULTS = {
4100
4286
  findingTypes: ALL_FINDING_TYPES
4101
4287
  };
4102
4288
  function loadScanConfig(projectPath) {
4103
- const configPath2 = join10(projectPath, ".mr-scan.json");
4289
+ const configPath2 = join9(projectPath, ".mr-scan.json");
4104
4290
  if (!existsSync13(configPath2)) {
4105
4291
  return { ...DEFAULTS };
4106
4292
  }
@@ -4148,11 +4334,11 @@ async function authenticateBrowseSession(magicUrl, runBrowse) {
4148
4334
 
4149
4335
  // lib/scanner/codebase-analysis.ts
4150
4336
  import { readdirSync as readdirSync2, readFileSync as readFileSync11, existsSync as existsSync14 } from "fs";
4151
- import { join as join11, relative } from "path";
4337
+ import { join as join10, relative } from "path";
4152
4338
  import { execSync as execSync6 } from "child_process";
4153
4339
  function resolveDir(projectPath, candidates) {
4154
4340
  for (const candidate of candidates) {
4155
- const dir = join11(projectPath, candidate);
4341
+ const dir = join10(projectPath, candidate);
4156
4342
  if (existsSync14(dir)) return dir;
4157
4343
  }
4158
4344
  return null;
@@ -4172,10 +4358,10 @@ function discoverRoutes(projectPath) {
4172
4358
  segment = `:${segment.slice(1, -1)}`;
4173
4359
  }
4174
4360
  if (segment.startsWith("(")) {
4175
- walk(join11(dir, entry.name), routePath);
4361
+ walk(join10(dir, entry.name), routePath);
4176
4362
  continue;
4177
4363
  }
4178
- walk(join11(dir, entry.name), `${routePath}/${segment}`);
4364
+ walk(join10(dir, entry.name), `${routePath}/${segment}`);
4179
4365
  }
4180
4366
  if (entry.name === "page.tsx" || entry.name === "page.ts") {
4181
4367
  routes.push(routePath || "/");
@@ -4186,7 +4372,7 @@ function discoverRoutes(projectPath) {
4186
4372
  return routes;
4187
4373
  }
4188
4374
  function extractModels(projectPath) {
4189
- const schemaPath = join11(projectPath, "prisma", "schema.prisma");
4375
+ const schemaPath = join10(projectPath, "prisma", "schema.prisma");
4190
4376
  if (existsSync14(schemaPath)) {
4191
4377
  const content = readFileSync11(schemaPath, "utf-8");
4192
4378
  const models2 = [];
@@ -4200,14 +4386,14 @@ function extractModels(projectPath) {
4200
4386
  const models = [];
4201
4387
  const drizzleDirs = ["src/db", "src/schema", "db", "drizzle"];
4202
4388
  for (const dir of drizzleDirs) {
4203
- const fullDir = join11(projectPath, dir);
4389
+ const fullDir = join10(projectPath, dir);
4204
4390
  if (!existsSync14(fullDir)) continue;
4205
4391
  try {
4206
4392
  const entries = readdirSync2(fullDir, { withFileTypes: true });
4207
4393
  for (const entry of entries) {
4208
4394
  if (!entry.isFile() || !entry.name.endsWith(".ts") && !entry.name.endsWith(".js")) continue;
4209
4395
  try {
4210
- const content = readFileSync11(join11(fullDir, entry.name), "utf-8");
4396
+ const content = readFileSync11(join10(fullDir, entry.name), "utf-8");
4211
4397
  const tableRegex = /(?:pg|mysql|sqlite)Table\(\s*["'](\w+)["']/g;
4212
4398
  let match;
4213
4399
  while ((match = tableRegex.exec(content)) !== null) {
@@ -4230,9 +4416,9 @@ function discoverComponents(projectPath) {
4230
4416
  for (const entry of entries) {
4231
4417
  if (entry.isDirectory()) {
4232
4418
  if (entry.name === "ui") continue;
4233
- walk(join11(dir, entry.name));
4419
+ walk(join10(dir, entry.name));
4234
4420
  } else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
4235
- components.push(relative(projectPath, join11(dir, entry.name)));
4421
+ components.push(relative(projectPath, join10(dir, entry.name)));
4236
4422
  }
4237
4423
  }
4238
4424
  }
@@ -4247,10 +4433,10 @@ function extractInternalLinks(projectPath) {
4247
4433
  for (const entry of entries) {
4248
4434
  if (entry.isDirectory()) {
4249
4435
  if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "ui") continue;
4250
- searchDir(join11(dir, entry.name));
4436
+ searchDir(join10(dir, entry.name));
4251
4437
  } else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
4252
4438
  try {
4253
- const content = readFileSync11(join11(dir, entry.name), "utf-8");
4439
+ const content = readFileSync11(join10(dir, entry.name), "utf-8");
4254
4440
  const hrefRegex = /href=["'`](\/[^"'`]*?)["'`]/g;
4255
4441
  const pushRegex = /router\.push\(["'`](\/[^"'`]*?)["'`]\)/g;
4256
4442
  let match;
@@ -4266,10 +4452,10 @@ function extractInternalLinks(projectPath) {
4266
4452
  }
4267
4453
  }
4268
4454
  for (const candidate of ["app", "src/app"]) {
4269
- searchDir(join11(projectPath, candidate));
4455
+ searchDir(join10(projectPath, candidate));
4270
4456
  }
4271
4457
  for (const candidate of ["components", "src/components"]) {
4272
- searchDir(join11(projectPath, candidate));
4458
+ searchDir(join10(projectPath, candidate));
4273
4459
  }
4274
4460
  return Array.from(links);
4275
4461
  }
@@ -5122,9 +5308,9 @@ var ideaCommand = new Command25("idea").description("Manage ideas \u2014 brainst
5122
5308
  import { Command as Command26 } from "commander";
5123
5309
  import { existsSync as existsSync15 } from "fs";
5124
5310
  import { homedir as homedir2 } from "os";
5125
- import { join as join12 } from "path";
5311
+ import { join as join11 } from "path";
5126
5312
  async function checkConfigExists() {
5127
- const configPath2 = join12(homedir2(), ".mr-manager", "config.json");
5313
+ const configPath2 = join11(homedir2(), ".mr-manager", "config.json");
5128
5314
  const exists = existsSync15(configPath2);
5129
5315
  if (!exists) {
5130
5316
  return {
@@ -5210,7 +5396,7 @@ var doctorCommand = new Command26("doctor").description("Diagnose Mr. Manager CL
5210
5396
  });
5211
5397
 
5212
5398
  // cli/index.ts
5213
- var configPath = join13(homedir3(), ".mr-manager", "config.json");
5399
+ var configPath = join12(homedir3(), ".mr-manager", "config.json");
5214
5400
  var isFirstRun = !existsSync16(configPath);
5215
5401
  var userArgs = process.argv.slice(2);
5216
5402
  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.3",
4
4
  "description": "Mr. Manager - Task and project management CLI",
5
5
  "bin": {
6
6
  "mr": "./dist/index.mjs"