@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.
- package/dist/index.mjs +253 -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.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
|
-
|
|
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 };
|
|
@@ -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
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2057
|
-
|
|
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}),
|
|
2141
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
3619
|
-
var BROWSE_DIR2 =
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
4361
|
+
walk(join10(dir, entry.name), routePath);
|
|
4176
4362
|
continue;
|
|
4177
4363
|
}
|
|
4178
|
-
walk(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
4419
|
+
walk(join10(dir, entry.name));
|
|
4234
4420
|
} else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
|
|
4235
|
-
components.push(relative(projectPath,
|
|
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(
|
|
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(
|
|
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(
|
|
4455
|
+
searchDir(join10(projectPath, candidate));
|
|
4270
4456
|
}
|
|
4271
4457
|
for (const candidate of ["components", "src/components"]) {
|
|
4272
|
-
searchDir(
|
|
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
|
|
5311
|
+
import { join as join11 } from "path";
|
|
5126
5312
|
async function checkConfigExists() {
|
|
5127
|
-
const configPath2 =
|
|
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 =
|
|
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"]);
|