@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.
- package/dist/index.mjs +255 -67
- 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
|
|
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.
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
|
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: ${
|
|
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
|
-
...
|
|
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.
|
|
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 = [
|
|
1912
|
+
const args = [];
|
|
1789
1913
|
if (mode === "execute") {
|
|
1790
|
-
args.push("-a", "never"
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2057
|
-
|
|
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}),
|
|
2141
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
3619
|
-
var BROWSE_DIR2 =
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
4363
|
+
walk(join10(dir, entry.name), routePath);
|
|
4176
4364
|
continue;
|
|
4177
4365
|
}
|
|
4178
|
-
walk(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
4421
|
+
walk(join10(dir, entry.name));
|
|
4234
4422
|
} else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
|
|
4235
|
-
components.push(relative(projectPath,
|
|
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(
|
|
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(
|
|
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(
|
|
4457
|
+
searchDir(join10(projectPath, candidate));
|
|
4270
4458
|
}
|
|
4271
4459
|
for (const candidate of ["components", "src/components"]) {
|
|
4272
|
-
searchDir(
|
|
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
|
|
5313
|
+
import { join as join11 } from "path";
|
|
5126
5314
|
async function checkConfigExists() {
|
|
5127
|
-
const configPath2 =
|
|
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 =
|
|
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"]);
|