@dunnewold-labs/mr-manager 0.4.8 → 0.4.9
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 +1388 -556
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// cli/index.ts
|
|
4
|
-
import { Command as
|
|
5
|
-
import { existsSync as
|
|
4
|
+
import { Command as Command27 } from "commander";
|
|
5
|
+
import { existsSync as existsSync17 } from "fs";
|
|
6
6
|
import { homedir as homedir3 } from "os";
|
|
7
7
|
import { join as join12 } from "path";
|
|
8
8
|
|
|
@@ -132,7 +132,7 @@ Remote login \u2014 visit this URL in any browser:
|
|
|
132
132
|
}
|
|
133
133
|
async function loginWithLocalServer(apiUrl) {
|
|
134
134
|
const port = getRandomPort();
|
|
135
|
-
return new Promise((
|
|
135
|
+
return new Promise((resolve8, reject) => {
|
|
136
136
|
const server = createServer((req, res) => {
|
|
137
137
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
138
138
|
const key = url.searchParams.get("key");
|
|
@@ -144,7 +144,7 @@ async function loginWithLocalServer(apiUrl) {
|
|
|
144
144
|
</body></html>
|
|
145
145
|
`);
|
|
146
146
|
server.close();
|
|
147
|
-
if (key)
|
|
147
|
+
if (key) resolve8(key);
|
|
148
148
|
else reject(new Error("No key received from server"));
|
|
149
149
|
});
|
|
150
150
|
server.listen(port, () => {
|
|
@@ -185,7 +185,7 @@ import { fileURLToPath } from "url";
|
|
|
185
185
|
// cli/package.json
|
|
186
186
|
var package_default = {
|
|
187
187
|
name: "@dunnewold-labs/mr-manager",
|
|
188
|
-
version: "0.4.
|
|
188
|
+
version: "0.4.9",
|
|
189
189
|
description: "Mr. Manager - Task and project management CLI",
|
|
190
190
|
bin: {
|
|
191
191
|
mr: "./dist/index.mjs"
|
|
@@ -356,10 +356,10 @@ function detectVcs(cwd) {
|
|
|
356
356
|
// cli/commands/link.ts
|
|
357
357
|
function prompt(question) {
|
|
358
358
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
359
|
-
return new Promise((
|
|
359
|
+
return new Promise((resolve8) => {
|
|
360
360
|
rl.question(question, (answer) => {
|
|
361
361
|
rl.close();
|
|
362
|
-
|
|
362
|
+
resolve8(answer.trim());
|
|
363
363
|
});
|
|
364
364
|
});
|
|
365
365
|
}
|
|
@@ -492,7 +492,7 @@ import { Command as Command8 } from "commander";
|
|
|
492
492
|
import { spawn as spawn4, exec } from "child_process";
|
|
493
493
|
import { randomUUID } from "crypto";
|
|
494
494
|
import { resolve as resolve2 } from "path";
|
|
495
|
-
import { readFileSync as readFileSync5, readdirSync, unlinkSync, existsSync as existsSync7, statSync } from "fs";
|
|
495
|
+
import { readFileSync as readFileSync5, readdirSync, unlinkSync, existsSync as existsSync7, statSync, writeFileSync as writeFileSync4 } from "fs";
|
|
496
496
|
import * as readline from "readline";
|
|
497
497
|
|
|
498
498
|
// lib/test-runner.ts
|
|
@@ -512,25 +512,89 @@ function tryExec(command, cwd) {
|
|
|
512
512
|
return false;
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
|
+
function resolveWorktreeStartPoint(hasRemoteBranch, hasLocalBranch, hasOriginMain) {
|
|
516
|
+
if (hasRemoteBranch && !hasLocalBranch) {
|
|
517
|
+
return "origin-branch";
|
|
518
|
+
}
|
|
519
|
+
if (hasLocalBranch) {
|
|
520
|
+
return "local-branch";
|
|
521
|
+
}
|
|
522
|
+
if (hasOriginMain) {
|
|
523
|
+
return "origin-main";
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
function parseGitWorktreePorcelain(output) {
|
|
528
|
+
const entries = [];
|
|
529
|
+
const blocks = output.trim().split(/\n(?=worktree )/);
|
|
530
|
+
for (const block of blocks) {
|
|
531
|
+
if (!block.trim()) continue;
|
|
532
|
+
let path = null;
|
|
533
|
+
let branch = null;
|
|
534
|
+
for (const line of block.split("\n")) {
|
|
535
|
+
if (line.startsWith("worktree ")) {
|
|
536
|
+
path = line.slice("worktree ".length).trim();
|
|
537
|
+
} else if (line.startsWith("branch ")) {
|
|
538
|
+
const ref = line.slice("branch ".length).trim();
|
|
539
|
+
branch = ref.replace(/^refs\/heads\//, "");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (path) {
|
|
543
|
+
entries.push({ path, branch });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return entries;
|
|
547
|
+
}
|
|
548
|
+
function findExistingWorktreeForBranch(repoDir, branch) {
|
|
549
|
+
try {
|
|
550
|
+
const output = execSync2("git worktree list --porcelain", {
|
|
551
|
+
cwd: repoDir,
|
|
552
|
+
stdio: "pipe"
|
|
553
|
+
}).toString();
|
|
554
|
+
const match = parseGitWorktreePorcelain(output).find((entry) => entry.branch === branch);
|
|
555
|
+
return match?.path ?? null;
|
|
556
|
+
} catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
515
560
|
function createWorktree(repoDir, branch, worktreeName) {
|
|
516
561
|
const wtPath = join4(repoDir, ".mr-worktrees", worktreeName);
|
|
517
562
|
if (existsSync4(wtPath)) {
|
|
518
563
|
execSync2(`git checkout ${branch}`, { cwd: wtPath, stdio: "pipe" });
|
|
519
|
-
return
|
|
564
|
+
return {
|
|
565
|
+
path: wtPath,
|
|
566
|
+
created: false,
|
|
567
|
+
reusedBranchWorktree: false
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const existingBranchWorktree = findExistingWorktreeForBranch(repoDir, branch);
|
|
571
|
+
if (existingBranchWorktree) {
|
|
572
|
+
return {
|
|
573
|
+
path: existingBranchWorktree,
|
|
574
|
+
created: false,
|
|
575
|
+
reusedBranchWorktree: true
|
|
576
|
+
};
|
|
520
577
|
}
|
|
521
578
|
tryExec(`git fetch origin ${branch}`, repoDir);
|
|
579
|
+
const hasOriginMain = tryExec(`git fetch origin main`, repoDir) && tryExec(`git rev-parse --verify "origin/main"`, repoDir);
|
|
522
580
|
const hasRemoteBranch = tryExec(`git rev-parse --verify "origin/${branch}"`, repoDir);
|
|
523
581
|
const hasLocalBranch = tryExec(`git rev-parse --verify "${branch}"`, repoDir);
|
|
524
|
-
|
|
582
|
+
const startPoint = resolveWorktreeStartPoint(hasRemoteBranch, hasLocalBranch, hasOriginMain);
|
|
583
|
+
if (startPoint === "origin-branch") {
|
|
525
584
|
execSync2(`git worktree add -b "${branch}" "${wtPath}" "origin/${branch}"`, {
|
|
526
585
|
cwd: repoDir,
|
|
527
586
|
stdio: "pipe"
|
|
528
587
|
});
|
|
529
|
-
} else if (
|
|
588
|
+
} else if (startPoint === "local-branch") {
|
|
530
589
|
execSync2(`git worktree add "${wtPath}" "${branch}"`, {
|
|
531
590
|
cwd: repoDir,
|
|
532
591
|
stdio: "pipe"
|
|
533
592
|
});
|
|
593
|
+
} else if (startPoint === "origin-main") {
|
|
594
|
+
execSync2(`git worktree add -b "${branch}" "${wtPath}" "origin/main"`, {
|
|
595
|
+
cwd: repoDir,
|
|
596
|
+
stdio: "pipe"
|
|
597
|
+
});
|
|
534
598
|
} else {
|
|
535
599
|
execSync2(`git worktree add -b "${branch}" "${wtPath}"`, {
|
|
536
600
|
cwd: repoDir,
|
|
@@ -543,7 +607,11 @@ function createWorktree(repoDir, branch, worktreeName) {
|
|
|
543
607
|
copyFileSync(src, join4(wtPath, envFile));
|
|
544
608
|
}
|
|
545
609
|
}
|
|
546
|
-
return
|
|
610
|
+
return {
|
|
611
|
+
path: wtPath,
|
|
612
|
+
created: true,
|
|
613
|
+
reusedBranchWorktree: false
|
|
614
|
+
};
|
|
547
615
|
}
|
|
548
616
|
function removeWorktree(repoDir, wtPath) {
|
|
549
617
|
try {
|
|
@@ -777,7 +845,7 @@ async function runTest(options) {
|
|
|
777
845
|
}
|
|
778
846
|
log2(`Branch: ${branch}`);
|
|
779
847
|
log2("Creating git worktree...");
|
|
780
|
-
wtPath = createWorktree(localPath, branch, worktreeName);
|
|
848
|
+
wtPath = createWorktree(localPath, branch, worktreeName).path;
|
|
781
849
|
log2(`Worktree created at ${wtPath}`);
|
|
782
850
|
log2("Installing dependencies...");
|
|
783
851
|
try {
|
|
@@ -890,6 +958,55 @@ async function runTest(options) {
|
|
|
890
958
|
return result;
|
|
891
959
|
}
|
|
892
960
|
|
|
961
|
+
// lib/agent-fallback.ts
|
|
962
|
+
var AGENT_FALLBACK_ORDER = ["claude", "codex", "gemini"];
|
|
963
|
+
function getAgentFallbackChain(agent) {
|
|
964
|
+
const startIndex = AGENT_FALLBACK_ORDER.indexOf(agent);
|
|
965
|
+
if (startIndex === -1) {
|
|
966
|
+
return [];
|
|
967
|
+
}
|
|
968
|
+
return AGENT_FALLBACK_ORDER.slice(startIndex);
|
|
969
|
+
}
|
|
970
|
+
function getAvailableAgentFallbackChain(agent, availability) {
|
|
971
|
+
return getAgentFallbackChain(agent).filter((candidate) => availability[candidate] !== false);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// lib/task-workflow.ts
|
|
975
|
+
function normalizeWhitespace(value) {
|
|
976
|
+
return value.replace(/\s+/g, " ").trim();
|
|
977
|
+
}
|
|
978
|
+
var NON_CODE_TASK_KEYWORDS = [
|
|
979
|
+
"no code",
|
|
980
|
+
"no-code",
|
|
981
|
+
"without code",
|
|
982
|
+
"research",
|
|
983
|
+
"document",
|
|
984
|
+
"documentation",
|
|
985
|
+
"docs",
|
|
986
|
+
"prd",
|
|
987
|
+
"test plan",
|
|
988
|
+
"resource",
|
|
989
|
+
"write up",
|
|
990
|
+
"writeup",
|
|
991
|
+
"config via the ui",
|
|
992
|
+
"configuration via the ui",
|
|
993
|
+
"ui configuration",
|
|
994
|
+
"upload"
|
|
995
|
+
];
|
|
996
|
+
var NON_CODE_TASK_PATTERNS = [
|
|
997
|
+
/\b(?:doesn'?t|does not|don't|do not)\s+(?:involve|require|need)\s+(?:writing\s+)?(?:any\s+)?code\b/,
|
|
998
|
+
/\b(?:without|avoids?)\s+(?:writing\s+)?(?:any\s+)?code\b/,
|
|
999
|
+
/\b(?:no need|skip)\s+to\s+(?:spin up|create|prepare)\s+(?:an?\s+)?worktree\b/
|
|
1000
|
+
];
|
|
1001
|
+
function taskLikelyDoesNotNeedCodeChanges(task) {
|
|
1002
|
+
if (task.mode && task.mode !== "development") return true;
|
|
1003
|
+
const haystack = normalizeWhitespace(
|
|
1004
|
+
[task.title, task.notes, task.prdContent].filter(Boolean).join(" ").toLowerCase()
|
|
1005
|
+
);
|
|
1006
|
+
if (!haystack) return false;
|
|
1007
|
+
return NON_CODE_TASK_KEYWORDS.some((keyword) => haystack.includes(keyword)) || NON_CODE_TASK_PATTERNS.some((pattern) => pattern.test(haystack));
|
|
1008
|
+
}
|
|
1009
|
+
|
|
893
1010
|
// cli/browse-runner.ts
|
|
894
1011
|
import { execSync as execSync4, spawn as spawn3 } from "child_process";
|
|
895
1012
|
import { existsSync as existsSync6 } from "fs";
|
|
@@ -916,7 +1033,7 @@ Run the setup script: cd browse && ./setup`
|
|
|
916
1033
|
async function runBrowseCommand2(browseArgs) {
|
|
917
1034
|
const runner = getBrowseRunner();
|
|
918
1035
|
const fullArgs = [...runner.args, ...browseArgs];
|
|
919
|
-
return new Promise((
|
|
1036
|
+
return new Promise((resolve8) => {
|
|
920
1037
|
const proc = spawn3(runner.cmd, fullArgs, {
|
|
921
1038
|
stdio: ["pipe", "pipe", "pipe"],
|
|
922
1039
|
env: { ...process.env }
|
|
@@ -933,21 +1050,156 @@ async function runBrowseCommand2(browseArgs) {
|
|
|
933
1050
|
if (stderr && code !== 0) {
|
|
934
1051
|
process.stderr.write(stderr);
|
|
935
1052
|
}
|
|
936
|
-
|
|
1053
|
+
resolve8({ stdout: stdout.trim(), exitCode: code || 0 });
|
|
937
1054
|
});
|
|
938
1055
|
proc.on("error", () => {
|
|
939
|
-
|
|
1056
|
+
resolve8({ stdout: "", exitCode: 1 });
|
|
940
1057
|
});
|
|
941
1058
|
});
|
|
942
1059
|
}
|
|
943
1060
|
|
|
1061
|
+
// cli/utils/token-estimate.ts
|
|
1062
|
+
function estimateTokens(text) {
|
|
1063
|
+
return Math.ceil(text.length / 4);
|
|
1064
|
+
}
|
|
1065
|
+
function formatTokenCount(tokens) {
|
|
1066
|
+
if (tokens < 1e3) return String(tokens);
|
|
1067
|
+
if (tokens < 1e4) return `~${(tokens / 1e3).toFixed(1)}k`;
|
|
1068
|
+
return `~${Math.round(tokens / 1e3)}k`;
|
|
1069
|
+
}
|
|
1070
|
+
function tokenLogLine(jobType, identifier, prompt2, systemPrompt) {
|
|
1071
|
+
const promptTokens = estimateTokens(prompt2);
|
|
1072
|
+
const systemTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;
|
|
1073
|
+
const total = promptTokens + systemTokens;
|
|
1074
|
+
const parts = [`[tokens] ${jobType}:${identifier} prompt=${formatTokenCount(promptTokens)}`];
|
|
1075
|
+
if (systemTokens > 0) {
|
|
1076
|
+
parts[0] += ` system=${formatTokenCount(systemTokens)}`;
|
|
1077
|
+
}
|
|
1078
|
+
parts[0] += ` total=${formatTokenCount(total)}`;
|
|
1079
|
+
return parts[0];
|
|
1080
|
+
}
|
|
1081
|
+
|
|
944
1082
|
// cli/commands/watch.ts
|
|
945
1083
|
var FEATURES_FILE2 = ".mr-features.md";
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1084
|
+
var SYSTEM_SECTION_STATUS_UPDATES = `## Status Updates
|
|
1085
|
+
|
|
1086
|
+
As you work, post brief status updates so progress is visible in the UI.
|
|
1087
|
+
Use: mr update <task-id> "your status message here"
|
|
1088
|
+
|
|
1089
|
+
The system automatically posts updates for: agent dispatch and task completion. Do NOT duplicate these \u2014 focus your updates on what you're actually doing:
|
|
1090
|
+
- What you found while exploring (e.g. "Found the auth logic in src/auth.ts, needs refactoring")
|
|
1091
|
+
- What specific changes you're making (e.g. "Adding validation to the signup form component")
|
|
1092
|
+
- Problems you hit and how you solved them (e.g. "Fixed circular dependency between User and Order models")
|
|
1093
|
+
- What files/components you modified (e.g. "Updated api/routes.ts and middleware/auth.ts")
|
|
1094
|
+
|
|
1095
|
+
Be specific about your changes \u2014 don't post vague messages like "working on the task" or "exploring the codebase". Each update should tell the user something concrete about what changed or what you learned.
|
|
1096
|
+
|
|
1097
|
+
Keep messages short (1 sentence). Post 3-5 updates total.`;
|
|
1098
|
+
var SYSTEM_SECTION_SCREENSHOTS = `## Screenshots
|
|
1099
|
+
|
|
1100
|
+
Before you finish, take a screenshot of your work to attach to the task. This helps the reviewer see what changed visually.
|
|
1101
|
+
Use: mr screenshot <task-id> <path-to-image> -m "Description of what the screenshot shows"
|
|
1102
|
+
|
|
1103
|
+
You can take a screenshot on macOS (no file arg needed) or provide a path to an existing image file.
|
|
1104
|
+
When no file is provided, the command uses the browse daemon (headless Chromium) to screenshot the app's project page automatically.
|
|
1105
|
+
You can also specify a custom URL: mr screenshot <task-id> --url "http://localhost:3000/some-page" -m "description"
|
|
1106
|
+
If your changes are visual (UI changes), try to capture the result. If they are purely backend/logic changes, you can skip this step.`;
|
|
1107
|
+
var SYSTEM_SECTION_TEST_PLAN = `## Test Plan
|
|
1108
|
+
|
|
1109
|
+
After pushing your MR/PR, create a structured test plan so the reviewer can run automated browser tests against your changes.
|
|
1110
|
+
Save the test plan as a TaskResource by calling:
|
|
1111
|
+
mr update <task-id> --resource test-plan "Test plan for <task-title>" '<json>'
|
|
1112
|
+
|
|
1113
|
+
Or write it to a file and upload it. The test plan is a JSON array of browse commands:
|
|
1114
|
+
|
|
1115
|
+
\`\`\`json
|
|
1116
|
+
[
|
|
1117
|
+
{ "command": "goto", "args": ["/login"], "description": "Navigate to login page" },
|
|
1118
|
+
{ "command": "fill", "args": ["input[type=email]", "test@example.com"], "description": "Enter email" },
|
|
1119
|
+
{ "command": "fill", "args": ["input[type=password]", "test-password"], "description": "Enter password" },
|
|
1120
|
+
{ "command": "click", "args": ["button[type=submit]"], "description": "Submit login" },
|
|
1121
|
+
{ "command": "wait", "args": ["2000"], "description": "Wait for redirect" },
|
|
1122
|
+
{ "command": "goto", "args": ["/dashboard"], "description": "Navigate to changed page" },
|
|
1123
|
+
{ "command": "screenshot", "description": "Capture dashboard" },
|
|
1124
|
+
{ "command": "assertVisible", "args": [".dashboard-widget"], "description": "Verify widget renders" },
|
|
1125
|
+
{ "command": "assertText", "args": ["h1", "Dashboard"], "description": "Verify page title" }
|
|
1126
|
+
]
|
|
1127
|
+
\`\`\`
|
|
1128
|
+
|
|
1129
|
+
Supported commands: goto, click, fill, wait, screenshot, assertVisible, assertText, assertUrl.
|
|
1130
|
+
Include auth steps (login/signup URLs) if the feature requires authentication.
|
|
1131
|
+
If your changes don't have a testable UI flow, you can skip this step.`;
|
|
1132
|
+
var SYSTEM_SECTION_NO_MR = `## Tasks That Don't Need an MR/PR
|
|
1133
|
+
|
|
1134
|
+
If the task does not require code changes (e.g. research, documentation uploaded as a resource, configuration via the UI, etc.), you can signal this instead of creating a pull request:
|
|
1135
|
+
mr no-mr <task-id> "Brief description of what was done instead"
|
|
1136
|
+
|
|
1137
|
+
This tells the watch system to skip looking for a PR and records what action was taken. You should still clean up any worktrees and exit normally.`;
|
|
1138
|
+
var SYSTEM_SECTION_FEATURES_WORKFLOW = `## Features & Goals Document Workflow
|
|
1139
|
+
|
|
1140
|
+
After completing your work, check if the project has a .mr-features.md file. If changes are relevant, update it.
|
|
1141
|
+
Write the updated content to a temp file and run: mr features --file /tmp/mr-features-update.md
|
|
1142
|
+
Only update the sections relevant to your changes. Preserve existing content that is still accurate.
|
|
1143
|
+
If the project does not yet have .mr-features.md, create one capturing the current state of features grouped by area.`;
|
|
1144
|
+
var SYSTEM_SECTION_PRD_FORMAT = `### PRD Format Conventions
|
|
1145
|
+
|
|
1146
|
+
Follow these conventions so PRDs are scannable by humans and parseable by agents.
|
|
1147
|
+
All sections are optional \u2014 include only what's relevant for this task type (greenfield, feature, bug fix, refactor). Omit sections that would be boilerplate.
|
|
1148
|
+
|
|
1149
|
+
**TL;DR Block** \u2014 Start the PRD with a \`> \` blockquote summary (2-3 sentences) immediately after the \`# PRD: Title\` heading.
|
|
1150
|
+
|
|
1151
|
+
**Requirement IDs** \u2014 Every requirement gets a unique ID prefix and a single-sentence summary on the first line:
|
|
1152
|
+
\`**F1: Name** \u2014 One-sentence summary.\` followed by optional \`- Detail bullet\` lines.
|
|
1153
|
+
Use \`F\` prefix for functional requirements, \`NF\` for non-functional. Number sequentially within each category.
|
|
1154
|
+
|
|
1155
|
+
**Success Metrics as JSON** \u2014 Use a fenced \`json\` code block containing an array of objects with \`Metric\`, \`Target\`, and \`Type\` fields.
|
|
1156
|
+
|
|
1157
|
+
**Affected Components as JSON** \u2014 Use the same JSON table format for the technical approach section.
|
|
1158
|
+
|
|
1159
|
+
**Implementation Steps as Phased Checklists** \u2014 Use \`### Phase N: Name\` headers with markdown checklists (\`- [ ]\`).
|
|
1160
|
+
|
|
1161
|
+
**Edge Cases as Tagged List** \u2014 Each edge case leads with a bold tag: \`- **Tag** \u2014 Description.\`
|
|
1162
|
+
|
|
1163
|
+
**No Frontmatter** \u2014 Do not include YAML frontmatter. Metadata lives in the database.`;
|
|
1164
|
+
var SYSTEM_SECTION_PRD_OPEN_QUESTIONS = `### Open Questions Format
|
|
1165
|
+
|
|
1166
|
+
The "Open Questions" section of the PRD MUST end with a fenced JSON code block tagged \`open-questions\`.
|
|
1167
|
+
|
|
1168
|
+
Format:
|
|
1169
|
+
\`\`\`open-questions
|
|
1170
|
+
[
|
|
1171
|
+
{
|
|
1172
|
+
"id": "q1",
|
|
1173
|
+
"question": "Which approach should we use for X?",
|
|
1174
|
+
"context": "One sentence explaining why this decision matters.",
|
|
1175
|
+
"options": [
|
|
1176
|
+
"Option A \u2014 brief description",
|
|
1177
|
+
"Option B \u2014 brief description"
|
|
1178
|
+
]
|
|
1179
|
+
}
|
|
1180
|
+
]
|
|
1181
|
+
\`\`\`
|
|
1182
|
+
|
|
1183
|
+
Rules:
|
|
1184
|
+
- Each question MUST have 2-4 concrete options.
|
|
1185
|
+
- Options should be actionable choices.
|
|
1186
|
+
- The "context" field should be a single sentence.
|
|
1187
|
+
- Use sequential IDs: q1, q2, q3, etc.
|
|
1188
|
+
- Place the JSON block at the very end of the "Open Questions" section.`;
|
|
1189
|
+
var SYSTEM_SECTIONS = {
|
|
1190
|
+
"status-updates": SYSTEM_SECTION_STATUS_UPDATES,
|
|
1191
|
+
"screenshots": SYSTEM_SECTION_SCREENSHOTS,
|
|
1192
|
+
"test-plan": SYSTEM_SECTION_TEST_PLAN,
|
|
1193
|
+
"no-mr": SYSTEM_SECTION_NO_MR,
|
|
1194
|
+
"features-workflow": SYSTEM_SECTION_FEATURES_WORKFLOW,
|
|
1195
|
+
"prd-format": SYSTEM_SECTION_PRD_FORMAT,
|
|
1196
|
+
"prd-open-questions": SYSTEM_SECTION_PRD_OPEN_QUESTIONS
|
|
1197
|
+
};
|
|
1198
|
+
function composeSystemPrompt(sections) {
|
|
1199
|
+
return sections.map((s) => SYSTEM_SECTIONS[s]).join("\n\n");
|
|
950
1200
|
}
|
|
1201
|
+
var EXECUTION_SYSTEM_SECTIONS = ["status-updates", "screenshots", "test-plan", "no-mr", "features-workflow"];
|
|
1202
|
+
var PRD_SYSTEM_SECTIONS = ["prd-format", "prd-open-questions"];
|
|
951
1203
|
var c = {
|
|
952
1204
|
reset: "\x1B[0m",
|
|
953
1205
|
bold: "\x1B[1m",
|
|
@@ -1047,7 +1299,7 @@ function worktreePath(name) {
|
|
|
1047
1299
|
function worktreeNameFromPath(wtPath) {
|
|
1048
1300
|
return wtPath.replace(/^\.mr-worktrees\//, "");
|
|
1049
1301
|
}
|
|
1050
|
-
function
|
|
1302
|
+
function normalizeWhitespace2(value) {
|
|
1051
1303
|
return value.replace(/\s+/g, " ").trim();
|
|
1052
1304
|
}
|
|
1053
1305
|
function extractFirstMeaningfulParagraph(markdown) {
|
|
@@ -1072,7 +1324,7 @@ function extractFirstMeaningfulParagraph(markdown) {
|
|
|
1072
1324
|
if (quoteLines.length > 0) break;
|
|
1073
1325
|
}
|
|
1074
1326
|
if (quoteLines.length > 0) {
|
|
1075
|
-
const text2 =
|
|
1327
|
+
const text2 = normalizeWhitespace2(quoteLines.join(" "));
|
|
1076
1328
|
return text2 || null;
|
|
1077
1329
|
}
|
|
1078
1330
|
const paragraphLines = [];
|
|
@@ -1095,7 +1347,7 @@ function extractFirstMeaningfulParagraph(markdown) {
|
|
|
1095
1347
|
paragraphLines.push(line);
|
|
1096
1348
|
}
|
|
1097
1349
|
if (paragraphLines.length === 0) return null;
|
|
1098
|
-
const text =
|
|
1350
|
+
const text = normalizeWhitespace2(paragraphLines.join(" "));
|
|
1099
1351
|
return text || null;
|
|
1100
1352
|
}
|
|
1101
1353
|
function truncateText(value, maxLength) {
|
|
@@ -1133,25 +1385,6 @@ function buildPrBodyTemplate(task, subtasks, protoRefs = [], feedbackUpdates = [
|
|
|
1133
1385
|
"_Replace every placeholder above before creating the PR. Remove bullets or sections that do not apply._"
|
|
1134
1386
|
].join("\n");
|
|
1135
1387
|
}
|
|
1136
|
-
function pullLatestMain(repoDir, prefix) {
|
|
1137
|
-
return new Promise((resolve7) => {
|
|
1138
|
-
exec(
|
|
1139
|
-
"git pull origin main --ff-only",
|
|
1140
|
-
{ cwd: repoDir },
|
|
1141
|
-
(err, stdout, stderr) => {
|
|
1142
|
-
if (err) {
|
|
1143
|
-
logWarn(prefix, `git pull failed (proceeding anyway): ${stderr.trim() || err.message}`);
|
|
1144
|
-
} else {
|
|
1145
|
-
const msg = stdout.trim();
|
|
1146
|
-
if (msg && msg !== "Already up to date.") {
|
|
1147
|
-
logInfo(prefix, `pulled latest main: ${paint("gray", msg.split("\n")[0])}`);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
resolve7();
|
|
1151
|
-
}
|
|
1152
|
-
);
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
1388
|
function buildPlanningPrompt(task, repoDir) {
|
|
1156
1389
|
const notes = task.notes ? `
|
|
1157
1390
|
|
|
@@ -1177,17 +1410,109 @@ ${task.notes}` : "";
|
|
|
1177
1410
|
}
|
|
1178
1411
|
function findPrUrl(branchName, repoDir, vcs = "github") {
|
|
1179
1412
|
const cmd = vcs === "gitlab" ? `glab mr view "${branchName}" --output json 2>/dev/null | jq -r '.web_url // empty'` : `gh pr view "${branchName}" --json url -q .url`;
|
|
1180
|
-
return new Promise((
|
|
1413
|
+
return new Promise((resolve8) => {
|
|
1181
1414
|
exec(
|
|
1182
1415
|
cmd,
|
|
1183
1416
|
{ cwd: repoDir },
|
|
1184
1417
|
(err, stdout) => {
|
|
1185
|
-
if (err)
|
|
1186
|
-
else
|
|
1418
|
+
if (err) resolve8(null);
|
|
1419
|
+
else resolve8(stdout.trim() || null);
|
|
1187
1420
|
}
|
|
1188
1421
|
);
|
|
1189
1422
|
});
|
|
1190
1423
|
}
|
|
1424
|
+
function buildAutomaticPrBody(task, branchName, vcs, subtasks, protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = []) {
|
|
1425
|
+
const contextSource = task.prdContent ?? task.notes ?? "";
|
|
1426
|
+
const taskContext = extractFirstMeaningfulParagraph(contextSource);
|
|
1427
|
+
const contextBullets = [
|
|
1428
|
+
`- Resolves MR Manager task ${task.id}.`,
|
|
1429
|
+
`- Task: ${task.title}.`,
|
|
1430
|
+
...taskContext ? [`- Goal/context: ${truncateText(taskContext, 220)}.`] : [],
|
|
1431
|
+
...subtasks.map((subtask) => `- Completed subtask: ${subtask.title}.`),
|
|
1432
|
+
...protoRefs.map((ref) => `- Related prototype: ${ref.prototype.title}${ref.selectedVariants?.length ? ` (variants ${ref.selectedVariants.join(", ")})` : ""}.`),
|
|
1433
|
+
...feedbackUpdates.slice(0, 3).map((update) => `- Addresses feedback from ${new Date(update.createdAt).toLocaleDateString("en-US")}.`),
|
|
1434
|
+
...existingResources.slice(0, 3).map((resource) => `- Referenced resource: ${resource.name}.${resource.kind === "link" && resource.url ? ` (${resource.url})` : ""}`),
|
|
1435
|
+
...skillRefs.slice(0, 3).map((ref) => `- Applied skill guidance: ${ref.skill.name}.`)
|
|
1436
|
+
];
|
|
1437
|
+
return [
|
|
1438
|
+
"## Summary",
|
|
1439
|
+
`- Auto-created by \`mr watch\` because no existing ${detectPrLabelFromVcs(vcs)} was found for branch \`${branchName}\` after the agent finished.`,
|
|
1440
|
+
`- Captures the implementation for MR Manager task \`${task.id}\` while preserving the task title as the review title.`,
|
|
1441
|
+
"",
|
|
1442
|
+
"## Context",
|
|
1443
|
+
...contextBullets,
|
|
1444
|
+
"",
|
|
1445
|
+
"## Testing",
|
|
1446
|
+
"- [ ] Automated tests: Not run by `mr watch`; see task updates or follow-up commits for validation details.",
|
|
1447
|
+
"- [ ] Manual verification completed by reviewer."
|
|
1448
|
+
].join("\n");
|
|
1449
|
+
}
|
|
1450
|
+
function detectPrLabelFromVcs(vcs) {
|
|
1451
|
+
return vcs === "gitlab" ? "MR" : "PR";
|
|
1452
|
+
}
|
|
1453
|
+
function extractPrUrlFromText(value) {
|
|
1454
|
+
const match = value.match(/https?:\/\/(?:github\.com\/[^\s]+\/pull\/\d+|gitlab\.[^\s]+\/merge_requests\/\d+)/);
|
|
1455
|
+
return match ? match[0] : null;
|
|
1456
|
+
}
|
|
1457
|
+
function commandSucceeds(command, cwd) {
|
|
1458
|
+
return new Promise((resolve8) => {
|
|
1459
|
+
exec(command, { cwd }, (err) => resolve8(!err));
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
async function createPrInRepo(task, branchName, repoDir, vcs, subtasks, protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = []) {
|
|
1463
|
+
const hasLocalBranch = await commandSucceeds(`git show-ref --verify --quiet refs/heads/${JSON.stringify(branchName)}`, repoDir);
|
|
1464
|
+
const hasRemoteBranch = await commandSucceeds(`git ls-remote --exit-code --heads origin ${JSON.stringify(branchName)}`, repoDir);
|
|
1465
|
+
if (!hasLocalBranch && !hasRemoteBranch) {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
const bodyPath = resolve2("/tmp", `mr-auto-pr-${randomUUID()}.md`);
|
|
1469
|
+
const body = buildAutomaticPrBody(task, branchName, vcs, subtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
|
|
1470
|
+
writeFileSync4(bodyPath, `${body}
|
|
1471
|
+
`, "utf-8");
|
|
1472
|
+
const createCommand2 = vcs === "gitlab" ? `glab mr create --source-branch ${JSON.stringify(branchName)} --title ${JSON.stringify(task.title)} --description-file ${JSON.stringify(bodyPath)} --yes` : `gh pr create --head ${JSON.stringify(branchName)} --title ${JSON.stringify(task.title)} --body-file ${JSON.stringify(bodyPath)}`;
|
|
1473
|
+
try {
|
|
1474
|
+
const output = await new Promise((resolve8, reject) => {
|
|
1475
|
+
exec(createCommand2, { cwd: repoDir }, (err, stdout, stderr) => {
|
|
1476
|
+
if (err) {
|
|
1477
|
+
reject(new Error(stderr.trim() || stdout.trim() || err.message));
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
resolve8(`${stdout}
|
|
1481
|
+
${stderr}`.trim());
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
const createdUrl = extractPrUrlFromText(output);
|
|
1485
|
+
return createdUrl ?? await findPrUrl(branchName, repoDir, vcs);
|
|
1486
|
+
} catch {
|
|
1487
|
+
return null;
|
|
1488
|
+
} finally {
|
|
1489
|
+
if (existsSync7(bodyPath)) {
|
|
1490
|
+
unlinkSync(bodyPath);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
async function createPrAcrossRepos(task, branchName, repoDir, vcs, subtasks, protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = []) {
|
|
1495
|
+
if (isGitRepo(repoDir)) {
|
|
1496
|
+
return createPrInRepo(task, branchName, repoDir, vcs, subtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
|
|
1497
|
+
}
|
|
1498
|
+
const childRepos = findChildGitRepos(repoDir);
|
|
1499
|
+
for (const repo of childRepos) {
|
|
1500
|
+
const childVcs = detectVcs(repo)?.provider ?? vcs;
|
|
1501
|
+
const url = await createPrInRepo(
|
|
1502
|
+
task,
|
|
1503
|
+
branchName,
|
|
1504
|
+
repo,
|
|
1505
|
+
childVcs,
|
|
1506
|
+
subtasks,
|
|
1507
|
+
protoRefs,
|
|
1508
|
+
feedbackUpdates,
|
|
1509
|
+
existingResources,
|
|
1510
|
+
skillRefs
|
|
1511
|
+
);
|
|
1512
|
+
if (url) return url;
|
|
1513
|
+
}
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1191
1516
|
function isGitRepo(dir) {
|
|
1192
1517
|
try {
|
|
1193
1518
|
return existsSync7(resolve2(dir, ".git"));
|
|
@@ -1235,38 +1560,39 @@ async function extractPrUrlFromUpdates(taskId) {
|
|
|
1235
1560
|
}
|
|
1236
1561
|
function checkPrStatus(prUrl, repoDir, vcs = "github") {
|
|
1237
1562
|
const cmd = vcs === "gitlab" ? `glab mr view "${prUrl}" --output json 2>/dev/null` : `gh pr view "${prUrl}" --json merged,mergeable 2>/dev/null`;
|
|
1238
|
-
return new Promise((
|
|
1563
|
+
return new Promise((resolve8) => {
|
|
1239
1564
|
exec(cmd, { cwd: repoDir }, (err, stdout) => {
|
|
1240
1565
|
if (err || !stdout.trim()) {
|
|
1241
|
-
|
|
1566
|
+
resolve8(null);
|
|
1242
1567
|
return;
|
|
1243
1568
|
}
|
|
1244
1569
|
try {
|
|
1245
1570
|
const data = JSON.parse(stdout.trim());
|
|
1246
1571
|
if (vcs === "gitlab") {
|
|
1247
|
-
|
|
1572
|
+
resolve8({
|
|
1248
1573
|
merged: data.state === "merged",
|
|
1249
1574
|
hasConflicts: data.has_conflicts === true
|
|
1250
1575
|
});
|
|
1251
1576
|
} else {
|
|
1252
|
-
|
|
1577
|
+
resolve8({
|
|
1253
1578
|
merged: data.merged === true,
|
|
1254
1579
|
hasConflicts: data.mergeable === "CONFLICTING"
|
|
1255
1580
|
});
|
|
1256
1581
|
}
|
|
1257
1582
|
} catch {
|
|
1258
|
-
|
|
1583
|
+
resolve8(null);
|
|
1259
1584
|
}
|
|
1260
1585
|
});
|
|
1261
1586
|
});
|
|
1262
1587
|
}
|
|
1263
|
-
function buildPrototypeSection(protoRefs) {
|
|
1588
|
+
function buildPrototypeSection(protoRefs, workingDir) {
|
|
1264
1589
|
if (protoRefs.length === 0) return "";
|
|
1265
1590
|
const sections = [
|
|
1266
1591
|
``,
|
|
1267
1592
|
`## Referenced Prototypes`,
|
|
1268
1593
|
``,
|
|
1269
1594
|
`The following prototype designs have been linked to this task as reference. Use them to guide the implementation.`,
|
|
1595
|
+
`Read the referenced files when you need to see the HTML content.`,
|
|
1270
1596
|
``
|
|
1271
1597
|
];
|
|
1272
1598
|
for (const ref of protoRefs) {
|
|
@@ -1284,10 +1610,19 @@ function buildPrototypeSection(protoRefs) {
|
|
|
1284
1610
|
for (let i = 0; i < selectedFiles.length; i++) {
|
|
1285
1611
|
const file = selectedFiles[i];
|
|
1286
1612
|
const variantLabel = selected.length < files.length ? `Variant ${selected[i] + 1} (selected)` : `Variant ${i + 1}`;
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1613
|
+
const tmpDir = workingDir ?? "/tmp";
|
|
1614
|
+
const safeProtoId = proto.id.slice(0, 8);
|
|
1615
|
+
const tmpPath = resolve2(tmpDir, `.mr-proto-${safeProtoId}-v${selected[i] ?? i}.html`);
|
|
1616
|
+
try {
|
|
1617
|
+
writeFileSync4(tmpPath, file.content, "utf-8");
|
|
1618
|
+
sections.push(`#### ${variantLabel}: ${file.name}`);
|
|
1619
|
+
sections.push(`File: \`${tmpPath}\` \u2014 read this file to see the full HTML content.`);
|
|
1620
|
+
} catch {
|
|
1621
|
+
sections.push(`#### ${variantLabel}: ${file.name}`);
|
|
1622
|
+
sections.push(`\`\`\`html`);
|
|
1623
|
+
sections.push(file.content);
|
|
1624
|
+
sections.push(`\`\`\``);
|
|
1625
|
+
}
|
|
1291
1626
|
sections.push(``);
|
|
1292
1627
|
}
|
|
1293
1628
|
}
|
|
@@ -1353,8 +1688,9 @@ function buildFeedbackSection(updates) {
|
|
|
1353
1688
|
return lines.join("\n");
|
|
1354
1689
|
}
|
|
1355
1690
|
function buildFeaturesSection(repoDir) {
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1691
|
+
const featuresPath = resolve2(repoDir, FEATURES_FILE2);
|
|
1692
|
+
const exists = existsSync7(featuresPath);
|
|
1693
|
+
if (!exists) {
|
|
1358
1694
|
return [
|
|
1359
1695
|
``,
|
|
1360
1696
|
`## Features & Goals Document`,
|
|
@@ -1374,11 +1710,8 @@ function buildFeaturesSection(repoDir) {
|
|
|
1374
1710
|
``,
|
|
1375
1711
|
`## Features & Goals Document`,
|
|
1376
1712
|
``,
|
|
1377
|
-
`The project maintains a features & goals document at
|
|
1378
|
-
|
|
1379
|
-
"```markdown",
|
|
1380
|
-
content,
|
|
1381
|
-
"```",
|
|
1713
|
+
`The project maintains a features & goals document at \`${featuresPath}\`.`,
|
|
1714
|
+
`Read this file if you need to understand existing features or update it after your work.`,
|
|
1382
1715
|
``,
|
|
1383
1716
|
`After completing your work, update this document to reflect any changes. Write the updated content to a temp file and run:`,
|
|
1384
1717
|
`\`mr features --file /tmp/mr-features-update.md\``,
|
|
@@ -1387,8 +1720,7 @@ function buildFeaturesSection(repoDir) {
|
|
|
1387
1720
|
``
|
|
1388
1721
|
].join("\n");
|
|
1389
1722
|
}
|
|
1390
|
-
function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = [], executionDir) {
|
|
1391
|
-
const sid = shortId(task.id);
|
|
1723
|
+
function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = [], executionDir, startWithoutWorktree = false) {
|
|
1392
1724
|
const slug = slugify(task.title);
|
|
1393
1725
|
const owner = ownerPrefix(task);
|
|
1394
1726
|
const branchName = `${owner}/${slug}`;
|
|
@@ -1415,7 +1747,6 @@ ${task.notes}` : "";
|
|
|
1415
1747
|
``
|
|
1416
1748
|
].join("\n") : "";
|
|
1417
1749
|
const hasFeedback = feedbackUpdates.length > 0;
|
|
1418
|
-
const feedbackWtPath = hasFeedback ? worktreePath(`${owner}-${slug}-fb`) : wtPath;
|
|
1419
1750
|
const prBodyTemplate = buildPrBodyTemplate(task, pendingSubtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
|
|
1420
1751
|
const prCreateCmd = vcs === "gitlab" ? `glab mr create --title "${task.title}" --description-file ${prBodyPath} --yes` : `gh pr create --title "${task.title}" --body-file ${prBodyPath}`;
|
|
1421
1752
|
return [
|
|
@@ -1427,7 +1758,7 @@ ${task.notes}` : "";
|
|
|
1427
1758
|
`ID: ${task.id}${notes}`,
|
|
1428
1759
|
subtaskSection,
|
|
1429
1760
|
buildResourcesSection(existingResources),
|
|
1430
|
-
buildPrototypeSection(protoRefs),
|
|
1761
|
+
buildPrototypeSection(protoRefs, workingDir),
|
|
1431
1762
|
buildSkillsSection(skillRefs),
|
|
1432
1763
|
buildFeedbackSection(feedbackUpdates),
|
|
1433
1764
|
`## Instructions`,
|
|
@@ -1442,9 +1773,16 @@ ${task.notes}` : "";
|
|
|
1442
1773
|
] : hasFeedback ? [
|
|
1443
1774
|
`2. Set up your working environment:`,
|
|
1444
1775
|
` - Fetch the latest from origin: \`git fetch origin\``,
|
|
1445
|
-
` -
|
|
1446
|
-
` -
|
|
1447
|
-
` -
|
|
1776
|
+
` - Reuse the existing task branch/worktree if it is still present.`,
|
|
1777
|
+
` - Otherwise check out the existing branch in a worktree: \`git worktree add ${wtPath} origin/${branchName}\``,
|
|
1778
|
+
` - If the origin branch doesn't exist yet, fall back to: \`git worktree add -b ${branchName} ${wtPath}\``,
|
|
1779
|
+
` - Work in the worktree directory: ${wtPath}`
|
|
1780
|
+
] : startWithoutWorktree ? [
|
|
1781
|
+
`2. Start in the linked project directory first:`,
|
|
1782
|
+
` - Current working directory: ${repoDir}`,
|
|
1783
|
+
` - This task appears likely to be a no-code workflow (research, documentation, UI configuration, or resource updates), so no worktree has been pre-created.`,
|
|
1784
|
+
` - If you discover you need to edit tracked files or write code after all, create a worktree before making changes: \`git worktree add -b ${branchName} ${wtPath}\``,
|
|
1785
|
+
` - If the task spans multiple repos, create matching worktrees only in the repo(s) where edits are actually needed.`
|
|
1448
1786
|
] : [
|
|
1449
1787
|
`2. Set up an isolated working environment:`,
|
|
1450
1788
|
` - If ${repoDir} is a git repo, create a worktree: \`git worktree add -b ${branchName} ${wtPath}\``,
|
|
@@ -1480,21 +1818,6 @@ ${task.notes}` : "";
|
|
|
1480
1818
|
``,
|
|
1481
1819
|
`This tells the watch system to skip looking for a ${vcs === "gitlab" ? "MR" : "PR"} and records what action was taken. You should still clean up any worktrees and exit normally.`,
|
|
1482
1820
|
``,
|
|
1483
|
-
`## Status Updates`,
|
|
1484
|
-
``,
|
|
1485
|
-
`As you work, post brief status updates so progress is visible in the UI:`,
|
|
1486
|
-
`\`mr update ${task.id} "your status message here"\``,
|
|
1487
|
-
``,
|
|
1488
|
-
`The system automatically posts updates for: agent dispatch and task completion. Do NOT duplicate these \u2014 focus your updates on what you're actually doing:`,
|
|
1489
|
-
`- What you found while exploring (e.g. "Found the auth logic in src/auth.ts, needs refactoring")`,
|
|
1490
|
-
`- What specific changes you're making (e.g. "Adding validation to the signup form component")`,
|
|
1491
|
-
`- Problems you hit and how you solved them (e.g. "Fixed circular dependency between User and Order models")`,
|
|
1492
|
-
`- What files/components you modified (e.g. "Updated api/routes.ts and middleware/auth.ts")`,
|
|
1493
|
-
``,
|
|
1494
|
-
`Be specific about your changes \u2014 don't post vague messages like "working on the task" or "exploring the codebase". Each update should tell the user something concrete about what changed or what you learned.`,
|
|
1495
|
-
``,
|
|
1496
|
-
`Keep messages short (1 sentence). Post 3-5 updates total.`,
|
|
1497
|
-
``,
|
|
1498
1821
|
...hasFeedback ? [] : [
|
|
1499
1822
|
`## PR Description Template`,
|
|
1500
1823
|
``,
|
|
@@ -1506,41 +1829,6 @@ ${task.notes}` : "";
|
|
|
1506
1829
|
"```",
|
|
1507
1830
|
``
|
|
1508
1831
|
],
|
|
1509
|
-
`## Screenshots`,
|
|
1510
|
-
``,
|
|
1511
|
-
`Before you finish, take a screenshot of your work to attach to the task. This helps the reviewer see what changed visually.`,
|
|
1512
|
-
`\`mr screenshot ${task.id} <path-to-image> -m "Description of what the screenshot shows"\``,
|
|
1513
|
-
``,
|
|
1514
|
-
`You can take a screenshot on macOS (no file arg needed) or provide a path to an existing image file.`,
|
|
1515
|
-
`When no file is provided, the command uses the browse daemon (headless Chromium) to screenshot the app's project page automatically.`,
|
|
1516
|
-
`You can also specify a custom URL: \`mr screenshot ${task.id} --url "http://localhost:3000/some-page" -m "description"\``,
|
|
1517
|
-
`If your changes are visual (UI changes), try to capture the result. If they are purely backend/logic changes, you can skip this step.`,
|
|
1518
|
-
``,
|
|
1519
|
-
`## Test Plan`,
|
|
1520
|
-
``,
|
|
1521
|
-
`After pushing your MR/PR, create a structured test plan so the reviewer can run automated browser tests against your changes.`,
|
|
1522
|
-
`Save the test plan as a TaskResource by calling:`,
|
|
1523
|
-
`\`mr update ${task.id} --resource test-plan "Test plan for ${task.title}" '<json>'\``,
|
|
1524
|
-
``,
|
|
1525
|
-
`Or write it to a file and upload it. The test plan is a JSON array of browse commands:`,
|
|
1526
|
-
``,
|
|
1527
|
-
"```json",
|
|
1528
|
-
`[`,
|
|
1529
|
-
` { "command": "goto", "args": ["/login"], "description": "Navigate to login page" },`,
|
|
1530
|
-
` { "command": "fill", "args": ["input[type=email]", "test@example.com"], "description": "Enter email" },`,
|
|
1531
|
-
` { "command": "fill", "args": ["input[type=password]", "test-password"], "description": "Enter password" },`,
|
|
1532
|
-
` { "command": "click", "args": ["button[type=submit]"], "description": "Submit login" },`,
|
|
1533
|
-
` { "command": "wait", "args": ["2000"], "description": "Wait for redirect" },`,
|
|
1534
|
-
` { "command": "goto", "args": ["/dashboard"], "description": "Navigate to changed page" },`,
|
|
1535
|
-
` { "command": "screenshot", "description": "Capture dashboard" },`,
|
|
1536
|
-
` { "command": "assertVisible", "args": [".dashboard-widget"], "description": "Verify widget renders" },`,
|
|
1537
|
-
` { "command": "assertText", "args": ["h1", "Dashboard"], "description": "Verify page title" }`,
|
|
1538
|
-
`]`,
|
|
1539
|
-
"```",
|
|
1540
|
-
``,
|
|
1541
|
-
`Supported commands: goto, click, fill, wait, screenshot, assertVisible, assertText, assertUrl.`,
|
|
1542
|
-
`Include auth steps (login/signup URLs) if the feature requires authentication.`,
|
|
1543
|
-
`If your changes don't have a testable UI flow, you can skip this step.`,
|
|
1544
1832
|
buildFeaturesSection(repoDir),
|
|
1545
1833
|
`Complete all steps autonomously without asking for confirmation. Exit with code 0 when done.`
|
|
1546
1834
|
].join("\n");
|
|
@@ -1551,16 +1839,31 @@ function buildPrdPrompt(task, repoDir, existingPrd, feedbackUpdates = []) {
|
|
|
1551
1839
|
Task notes:
|
|
1552
1840
|
${task.notes}` : "";
|
|
1553
1841
|
const isRevision = !!(existingPrd && feedbackUpdates.length > 0);
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1842
|
+
let feedbackSection = "";
|
|
1843
|
+
if (isRevision) {
|
|
1844
|
+
const prdTmpPath = resolve2(repoDir, ".mr-existing-prd.md");
|
|
1845
|
+
try {
|
|
1846
|
+
writeFileSync4(prdTmpPath, existingPrd, "utf-8");
|
|
1847
|
+
} catch {
|
|
1848
|
+
}
|
|
1849
|
+
const prdSummaryLines = [];
|
|
1850
|
+
for (const line of (existingPrd ?? "").split("\n")) {
|
|
1851
|
+
const trimmed = line.trim();
|
|
1852
|
+
if (/^#{1,3}\s/.test(trimmed)) prdSummaryLines.push(trimmed);
|
|
1853
|
+
else if (/^\*\*[FN]+\d+:/.test(trimmed)) prdSummaryLines.push(` ${trimmed.split("\u2014")[0].trim()}`);
|
|
1854
|
+
}
|
|
1855
|
+
feedbackSection = [
|
|
1856
|
+
``,
|
|
1857
|
+
`## Existing PRD`,
|
|
1858
|
+
``,
|
|
1859
|
+
`A PRD was previously generated. You must revise it based on the user's feedback below \u2014 do NOT start from scratch.`,
|
|
1860
|
+
``,
|
|
1861
|
+
`**Full PRD file:** \`${prdTmpPath}\` \u2014 read this file for the complete existing PRD.`,
|
|
1862
|
+
``,
|
|
1863
|
+
...prdSummaryLines.length > 0 ? [`**PRD structure summary:**`, ...prdSummaryLines, ``] : [],
|
|
1864
|
+
buildFeedbackSection(feedbackUpdates)
|
|
1865
|
+
].join("\n");
|
|
1866
|
+
}
|
|
1564
1867
|
const revisionInstructions = isRevision ? [
|
|
1565
1868
|
`1. Read the existing PRD and user feedback above carefully.`,
|
|
1566
1869
|
`2. Revise the PRD to address every piece of feedback. Keep sections that are still valid \u2014 only modify what the feedback requires.`,
|
|
@@ -1591,77 +1894,16 @@ ${task.notes}` : "";
|
|
|
1591
1894
|
` - Technical approach and affected components`,
|
|
1592
1895
|
` - Edge cases`,
|
|
1593
1896
|
` - Implementation steps / breakdown`,
|
|
1594
|
-
` - **Open Questions** \u2014 this section MUST use the structured format
|
|
1595
|
-
``,
|
|
1596
|
-
`### PRD Format Conventions`,
|
|
1597
|
-
``,
|
|
1598
|
-
`Follow these conventions so PRDs are scannable by humans and parseable by agents.`,
|
|
1599
|
-
`All sections are optional \u2014 include only what's relevant for this task type (greenfield, feature, bug fix, refactor). Omit sections that would be boilerplate.`,
|
|
1897
|
+
` - **Open Questions** \u2014 this section MUST use the structured format described in the system prompt`,
|
|
1600
1898
|
``,
|
|
1601
|
-
`**TL;DR Block** \u2014 Start the PRD with a \`> \` blockquote summary (2-3 sentences) immediately after the \`# PRD: Title\` heading. This gives humans instant orientation and agents a compact context summary.`,
|
|
1602
|
-
``,
|
|
1603
|
-
`**Requirement IDs** \u2014 Every requirement gets a unique ID prefix and a single-sentence summary on the first line:`,
|
|
1604
|
-
`\`**F1: Name** \u2014 One-sentence summary.\` followed by optional \`- Detail bullet\` lines.`,
|
|
1605
|
-
`Use \`F\` prefix for functional requirements, \`NF\` for non-functional. Number sequentially within each category.`,
|
|
1606
|
-
`The summary line must be a complete description \u2014 an agent should be able to extract just summary lines to build a work plan.`,
|
|
1607
|
-
...isRevision ? [`When revising, preserve existing requirement IDs. Add new requirements by continuing the numbering sequence \u2014 never restart at F1.`] : [],
|
|
1608
|
-
``,
|
|
1609
|
-
`**Success Metrics as JSON** \u2014 Use a fenced \`json\` code block containing an array of objects. The UI auto-renders these as tables.`,
|
|
1610
|
-
`Each object should have \`Metric\`, \`Target\`, and \`Type\` fields. Example:`,
|
|
1611
|
-
"````json",
|
|
1612
|
-
`[`,
|
|
1613
|
-
` { "Metric": "API response time under load", "Target": "< 200ms p95", "Type": "Performance" }`,
|
|
1614
|
-
`]`,
|
|
1615
|
-
"````",
|
|
1616
|
-
``,
|
|
1617
|
-
`**Affected Components as JSON** \u2014 Use the same JSON table format for the technical approach section:`,
|
|
1618
|
-
"````json",
|
|
1619
|
-
`[`,
|
|
1620
|
-
` { "Component": "path/to/file.ts", "Change": "What changes", "Scope": "~N lines" }`,
|
|
1621
|
-
`]`,
|
|
1622
|
-
"````",
|
|
1623
|
-
``,
|
|
1624
|
-
`**Implementation Steps as Phased Checklists** \u2014 Use \`### Phase N: Name\` headers with markdown checklists (\`- [ ]\`).`,
|
|
1625
|
-
`Gives humans a timeline overview at heading level; gives agents discrete work items.`,
|
|
1626
|
-
``,
|
|
1627
|
-
`**Edge Cases as Tagged List** \u2014 Each edge case leads with a bold tag: \`- **Tag** \u2014 Description.\``,
|
|
1628
|
-
``,
|
|
1629
|
-
`**No Frontmatter** \u2014 Do not include YAML frontmatter. Metadata lives in the database.`,
|
|
1630
|
-
``,
|
|
1631
|
-
`### Open Questions Format`,
|
|
1632
|
-
``,
|
|
1633
|
-
`The "Open Questions" section of the PRD MUST end with a fenced JSON code block`,
|
|
1634
|
-
`tagged \`open-questions\`. This allows the UI to present them as answerable cards.`,
|
|
1635
|
-
``,
|
|
1636
|
-
`Format:`,
|
|
1637
|
-
"```",
|
|
1638
|
-
"```open-questions",
|
|
1639
|
-
`[`,
|
|
1640
|
-
` {`,
|
|
1641
|
-
` "id": "q1",`,
|
|
1642
|
-
` "question": "Which approach should we use for X?",`,
|
|
1643
|
-
` "context": "One sentence explaining why this decision matters.",`,
|
|
1644
|
-
` "options": [`,
|
|
1645
|
-
` "Option A \u2014 brief description",`,
|
|
1646
|
-
` "Option B \u2014 brief description",`,
|
|
1647
|
-
` "Option C \u2014 brief description"`,
|
|
1648
|
-
` ]`,
|
|
1649
|
-
` }`,
|
|
1650
|
-
`]`,
|
|
1651
|
-
"```",
|
|
1652
|
-
"```",
|
|
1653
|
-
``,
|
|
1654
|
-
`Rules for open questions:`,
|
|
1655
|
-
`- Each question MUST have 2-4 concrete options.`,
|
|
1656
|
-
`- Options should be actionable choices, not vague ("Yes"/"No" is fine when appropriate).`,
|
|
1657
|
-
`- The "context" field should be a single sentence explaining why the decision matters.`,
|
|
1658
|
-
`- Use sequential IDs: q1, q2, q3, etc.`,
|
|
1659
|
-
`- Place the JSON block at the very end of the "Open Questions" section.`,
|
|
1660
|
-
`- You may include a brief prose introduction before the JSON block, but the block itself must be last.`,
|
|
1661
1899
|
...isRevision ? [
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1900
|
+
`Follow the PRD Format Conventions and Open Questions Format from the system prompt.`,
|
|
1901
|
+
`When revising, preserve existing requirement IDs. Add new requirements by continuing the numbering sequence \u2014 never restart at F1.`,
|
|
1902
|
+
`If the user's feedback answered an open question, incorporate their answer and remove that question.`,
|
|
1903
|
+
`Only include questions that are still genuinely open after considering the feedback.`
|
|
1904
|
+
] : [
|
|
1905
|
+
`Follow the PRD Format Conventions and Open Questions Format from the system prompt.`
|
|
1906
|
+
],
|
|
1665
1907
|
`${nextStep(4)}. Save the PRD as a Markdown file in the working directory:`,
|
|
1666
1908
|
` - File name: \`prd.md\``,
|
|
1667
1909
|
` - Write the full PRD content to this file.`,
|
|
@@ -1818,13 +2060,23 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
|
|
|
1818
2060
|
);
|
|
1819
2061
|
}
|
|
1820
2062
|
const variantList = Array.from({ length: proto.variantCount }, (_, i) => `prototype-${i + 1}.html`);
|
|
1821
|
-
const
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2063
|
+
const existingVariantLines = [];
|
|
2064
|
+
for (let i = 0; i < parentFiles.length; i++) {
|
|
2065
|
+
const f = parentFiles[i];
|
|
2066
|
+
const tmpPath = resolve2(repoDir, `.mr-parent-variant-${i + 1}.html`);
|
|
2067
|
+
try {
|
|
2068
|
+
writeFileSync4(tmpPath, f.content, "utf-8");
|
|
2069
|
+
existingVariantLines.push(`### Previous Variant ${i + 1} (${f.name})`);
|
|
2070
|
+
existingVariantLines.push(`File: \`${tmpPath}\` \u2014 read this file to see the full HTML content.`);
|
|
2071
|
+
} catch {
|
|
2072
|
+
existingVariantLines.push(`### Previous Variant ${i + 1} (${f.name})`);
|
|
2073
|
+
existingVariantLines.push(`\`\`\`html`);
|
|
2074
|
+
existingVariantLines.push(f.content);
|
|
2075
|
+
existingVariantLines.push(`\`\`\``);
|
|
2076
|
+
}
|
|
2077
|
+
existingVariantLines.push(``);
|
|
2078
|
+
}
|
|
2079
|
+
const existingVariants = existingVariantLines.join("\n");
|
|
1828
2080
|
return [
|
|
1829
2081
|
`You are a UI designer and frontend engineer. Your job is to REFINE an existing prototype based on user feedback.`,
|
|
1830
2082
|
``,
|
|
@@ -1941,7 +2193,7 @@ function buildIdeaPrompt(idea, repoDir) {
|
|
|
1941
2193
|
`- Do NOT exit until both files have been written and verified`
|
|
1942
2194
|
].join("\n");
|
|
1943
2195
|
}
|
|
1944
|
-
function buildAgentArgs(agent, prompt2, mode, sessionId, name) {
|
|
2196
|
+
function buildAgentArgs(agent, prompt2, mode, sessionId, name, resumeSession = false, systemPrompt) {
|
|
1945
2197
|
if (agent === "codex") {
|
|
1946
2198
|
const args = [];
|
|
1947
2199
|
if (mode === "execute") {
|
|
@@ -1951,26 +2203,40 @@ function buildAgentArgs(agent, prompt2, mode, sessionId, name) {
|
|
|
1951
2203
|
if (mode === "execute") {
|
|
1952
2204
|
args.push("-s", "danger-full-access");
|
|
1953
2205
|
}
|
|
1954
|
-
|
|
2206
|
+
const fullPrompt = systemPrompt ? `${prompt2}
|
|
2207
|
+
|
|
2208
|
+
${systemPrompt}` : prompt2;
|
|
2209
|
+
args.push(fullPrompt);
|
|
1955
2210
|
return { bin: "codex", args };
|
|
1956
2211
|
}
|
|
1957
2212
|
if (agent === "gemini") {
|
|
1958
|
-
const
|
|
2213
|
+
const fullPrompt = systemPrompt ? `${prompt2}
|
|
2214
|
+
|
|
2215
|
+
${systemPrompt}` : prompt2;
|
|
2216
|
+
const args = ["-p", fullPrompt];
|
|
1959
2217
|
if (mode === "execute") {
|
|
1960
2218
|
args.push("--yolo");
|
|
1961
2219
|
}
|
|
1962
2220
|
return { bin: "gemini", args };
|
|
1963
2221
|
}
|
|
1964
|
-
const sessionArgs = sessionId ? ["--session-id", sessionId] : [];
|
|
2222
|
+
const sessionArgs = sessionId ? resumeSession ? ["--resume", sessionId] : ["--session-id", sessionId] : [];
|
|
1965
2223
|
const nameArgs = name ? ["--name", name] : [];
|
|
2224
|
+
const systemArgs = systemPrompt ? ["--append-system-prompt", systemPrompt] : [];
|
|
1966
2225
|
if (mode === "plan") {
|
|
1967
|
-
return { bin: "claude", args: [...sessionArgs, ...nameArgs, "--permission-mode", "plan", "-p", prompt2] };
|
|
2226
|
+
return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, "--permission-mode", "plan", "-p", prompt2] };
|
|
1968
2227
|
}
|
|
1969
|
-
return { bin: "claude", args: [...sessionArgs, ...nameArgs, "-p", "--dangerously-skip-permissions", prompt2] };
|
|
2228
|
+
return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, "-p", "--dangerously-skip-permissions", prompt2] };
|
|
2229
|
+
}
|
|
2230
|
+
function commandExists(cmd) {
|
|
2231
|
+
return new Promise((resolve8) => {
|
|
2232
|
+
exec(`command -v ${cmd}`, (err) => resolve8(!err));
|
|
2233
|
+
});
|
|
1970
2234
|
}
|
|
1971
2235
|
function runPlanningPhase(task, repoDir, agent) {
|
|
1972
2236
|
return new Promise((res, reject) => {
|
|
1973
|
-
const
|
|
2237
|
+
const planPrompt = buildPlanningPrompt(task, repoDir);
|
|
2238
|
+
console.log(`${timestamp()} ${taskTag(shortId(task.id))} ${paint("dim", tokenLogLine("plan", shortId(task.id), planPrompt))}`);
|
|
2239
|
+
const { bin, args } = buildAgentArgs(agent, planPrompt, "plan", void 0, task.title);
|
|
1974
2240
|
const child = spawn4(bin, args, { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] });
|
|
1975
2241
|
child.on("error", (err) => {
|
|
1976
2242
|
reject(new Error(`Failed to spawn ${agent}: ${err.message}`));
|
|
@@ -2003,25 +2269,28 @@ ${output.trim()}`));
|
|
|
2003
2269
|
});
|
|
2004
2270
|
}
|
|
2005
2271
|
function askYesNo(question) {
|
|
2006
|
-
return new Promise((
|
|
2272
|
+
return new Promise((resolve8) => {
|
|
2007
2273
|
const rl = readline.createInterface({
|
|
2008
2274
|
input: process.stdin,
|
|
2009
2275
|
output: process.stdout
|
|
2010
2276
|
});
|
|
2011
2277
|
rl.question(question, (answer) => {
|
|
2012
2278
|
rl.close();
|
|
2013
|
-
|
|
2279
|
+
resolve8(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
2014
2280
|
});
|
|
2015
2281
|
});
|
|
2016
2282
|
}
|
|
2017
|
-
function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name) {
|
|
2018
|
-
const
|
|
2283
|
+
function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name, resumeSession = false, onSpawnError, systemPrompt) {
|
|
2284
|
+
const jobLabel = name ?? "unknown";
|
|
2285
|
+
console.log(`${timestamp()} ${prefix} ${paint("dim", tokenLogLine("agent", jobLabel, prompt2, systemPrompt))}`);
|
|
2286
|
+
const { bin, args } = buildAgentArgs(agent, prompt2, "execute", sessionId, name, resumeSession, systemPrompt);
|
|
2019
2287
|
const child = spawn4(bin, args, { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] });
|
|
2020
2288
|
child.on("error", (err) => {
|
|
2021
2289
|
logError(prefix, `Failed to spawn ${agent}: ${err.message}`);
|
|
2022
2290
|
if (err.code === "ENOENT") {
|
|
2023
2291
|
logError(prefix, `Check that "${bin}" is on PATH and "${repoDir}" exists`);
|
|
2024
2292
|
}
|
|
2293
|
+
onSpawnError?.(err);
|
|
2025
2294
|
});
|
|
2026
2295
|
if (agent === "codex") {
|
|
2027
2296
|
child.stdout?.on("data", () => onActivity?.());
|
|
@@ -2061,6 +2330,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2061
2330
|
let pollRunning = false;
|
|
2062
2331
|
const approvalQueue = [];
|
|
2063
2332
|
let approvalRunning = false;
|
|
2333
|
+
const agentAvailability = /* @__PURE__ */ new Map();
|
|
2064
2334
|
const flags = [
|
|
2065
2335
|
`interval=${paint("cyan", opts.interval + "s")}`,
|
|
2066
2336
|
`root=${paint("cyan", rootDir)}`,
|
|
@@ -2083,6 +2353,22 @@ var watchCommand = new Command8("watch").description(
|
|
|
2083
2353
|
console.log(banner);
|
|
2084
2354
|
console.log(` ${flags}
|
|
2085
2355
|
`);
|
|
2356
|
+
async function isAgentAvailable(candidate) {
|
|
2357
|
+
const cached = agentAvailability.get(candidate);
|
|
2358
|
+
if (cached !== void 0) {
|
|
2359
|
+
return cached;
|
|
2360
|
+
}
|
|
2361
|
+
const available = await commandExists(candidate);
|
|
2362
|
+
agentAvailability.set(candidate, available);
|
|
2363
|
+
return available;
|
|
2364
|
+
}
|
|
2365
|
+
async function resolveAgentChain(preferred) {
|
|
2366
|
+
const availability = {};
|
|
2367
|
+
for (const candidate of ["claude", "codex", "gemini"]) {
|
|
2368
|
+
availability[candidate] = await isAgentAvailable(candidate);
|
|
2369
|
+
}
|
|
2370
|
+
return getAvailableAgentFallbackChain(preferred, availability);
|
|
2371
|
+
}
|
|
2086
2372
|
async function moveTaskToError(task, prefix, reason) {
|
|
2087
2373
|
try {
|
|
2088
2374
|
await api.patch(`/api/tasks/${task.id}`, { status: "error" });
|
|
@@ -2104,7 +2390,6 @@ var watchCommand = new Command8("watch").description(
|
|
|
2104
2390
|
const vcs = detectVcs(repoDir)?.provider ?? "github";
|
|
2105
2391
|
logDispatch(prefix, `"${paint("bold", task.title)}" ${paint("gray", repoDir)} ${paint("dim", `[${vcs}]`)}`);
|
|
2106
2392
|
await postTaskUpdate(task.id, `Agent dispatched \u2014 starting work on "${task.title}"`, "system");
|
|
2107
|
-
await pullLatestMain(repoDir, prefix);
|
|
2108
2393
|
let subtasks = [];
|
|
2109
2394
|
try {
|
|
2110
2395
|
subtasks = await api.get(`/api/tasks/${task.id}/subtasks`);
|
|
@@ -2149,20 +2434,26 @@ var watchCommand = new Command8("watch").description(
|
|
|
2149
2434
|
}
|
|
2150
2435
|
const hasFeedback = feedbackUpdates.length > 0;
|
|
2151
2436
|
const wtName = `${owner}-${slug}`;
|
|
2152
|
-
const desiredWorktreePath =
|
|
2437
|
+
const desiredWorktreePath = worktreePath(wtName);
|
|
2438
|
+
const startWithoutWorktree = !hasFeedback && taskLikelyDoesNotNeedCodeChanges(task);
|
|
2153
2439
|
let executionDir = repoDir;
|
|
2154
2440
|
let cleanupWorktreePath;
|
|
2155
|
-
if (
|
|
2156
|
-
|
|
2441
|
+
if (startWithoutWorktree) {
|
|
2442
|
+
logInfo(prefix, `Skipping pre-created worktree; task looks like a no-code workflow`);
|
|
2443
|
+
} else if (isGitRepo(repoDir)) {
|
|
2444
|
+
const worktree = createWorktree(
|
|
2157
2445
|
repoDir,
|
|
2158
2446
|
branchName,
|
|
2159
2447
|
worktreeNameFromPath(desiredWorktreePath)
|
|
2160
2448
|
);
|
|
2161
|
-
|
|
2162
|
-
|
|
2449
|
+
executionDir = worktree.path;
|
|
2450
|
+
cleanupWorktreePath = worktree.created ? executionDir : void 0;
|
|
2451
|
+
logInfo(
|
|
2452
|
+
prefix,
|
|
2453
|
+
worktree.reusedBranchWorktree ? `Reusing existing worktree ${paint("cyan", executionDir)} for branch ${paint("dim", branchName)}` : `Prepared worktree ${paint("cyan", executionDir)}`
|
|
2454
|
+
);
|
|
2163
2455
|
}
|
|
2164
|
-
const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs, executionDir);
|
|
2165
|
-
const sessionId = agent === "claude" ? randomUUID() : void 0;
|
|
2456
|
+
const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs, executionDir, startWithoutWorktree);
|
|
2166
2457
|
const activeEntry = {
|
|
2167
2458
|
process: void 0,
|
|
2168
2459
|
title: task.title,
|
|
@@ -2175,93 +2466,181 @@ var watchCommand = new Command8("watch").description(
|
|
|
2175
2466
|
const touchActivity = () => {
|
|
2176
2467
|
activeEntry.lastActivityAt = Date.now();
|
|
2177
2468
|
};
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2469
|
+
const attemptOrder = await resolveAgentChain(agent);
|
|
2470
|
+
if (attemptOrder.length === 0) {
|
|
2471
|
+
logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
|
|
2472
|
+
await moveTaskToError(task, prefix, `No available agent found for fallback chain starting at ${agent}`);
|
|
2473
|
+
if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
|
|
2474
|
+
removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
|
|
2475
|
+
}
|
|
2476
|
+
queued.delete(task.id);
|
|
2477
|
+
return;
|
|
2184
2478
|
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2479
|
+
let attemptIndex = 0;
|
|
2480
|
+
const launchAttempt = async (attemptAgent) => {
|
|
2481
|
+
let spawnFailureReason = null;
|
|
2482
|
+
const shouldResumeClaudeSession = attemptAgent === "claude" && hasFeedback && !!task.claudeSessionId;
|
|
2483
|
+
const sessionId = attemptAgent === "claude" ? task.claudeSessionId ?? randomUUID() : void 0;
|
|
2484
|
+
const executionSystemPrompt = composeSystemPrompt(EXECUTION_SYSTEM_SECTIONS);
|
|
2485
|
+
const child = spawnAgent(
|
|
2486
|
+
attemptAgent,
|
|
2487
|
+
executionDir,
|
|
2488
|
+
prompt2,
|
|
2489
|
+
prefix,
|
|
2490
|
+
touchActivity,
|
|
2491
|
+
sessionId,
|
|
2492
|
+
task.title,
|
|
2493
|
+
shouldResumeClaudeSession,
|
|
2494
|
+
(err) => {
|
|
2495
|
+
spawnFailureReason = err.message;
|
|
2496
|
+
},
|
|
2497
|
+
executionSystemPrompt
|
|
2498
|
+
);
|
|
2499
|
+
activeEntry.process = child;
|
|
2500
|
+
activeEntry.currentAgent = attemptAgent;
|
|
2501
|
+
active.set(task.id, activeEntry);
|
|
2502
|
+
if (attemptAgent === "claude" && sessionId) {
|
|
2503
|
+
api.patch(`/api/tasks/${task.id}`, { claudeSessionId: sessionId }).catch(() => {
|
|
2504
|
+
});
|
|
2505
|
+
logInfo(
|
|
2506
|
+
prefix,
|
|
2507
|
+
shouldResumeClaudeSession ? `Resuming Claude session: ${paint("dim", sessionId)}` : `Claude session: ${paint("dim", sessionId)}`
|
|
2508
|
+
);
|
|
2509
|
+
} else {
|
|
2510
|
+
api.patch(`/api/tasks/${task.id}`, { claudeSessionId: null }).catch(() => {
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
child.on("exit", async (code) => {
|
|
2514
|
+
if (active.get(task.id)?.process === child) {
|
|
2515
|
+
active.delete(task.id);
|
|
2516
|
+
}
|
|
2517
|
+
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
2518
|
+
if (failedAttempt && !activeEntry.terminatedForError) {
|
|
2519
|
+
const nextAgent = attemptOrder[attemptIndex + 1];
|
|
2520
|
+
if (nextAgent) {
|
|
2521
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2522
|
+
logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying with ${nextAgent}`);
|
|
2523
|
+
await postTaskUpdate(
|
|
2524
|
+
task.id,
|
|
2525
|
+
`${attemptAgent} failed (${failureDetail}) \u2014 retrying with ${nextAgent}`,
|
|
2526
|
+
"system"
|
|
2527
|
+
);
|
|
2528
|
+
attemptIndex += 1;
|
|
2529
|
+
await launchAttempt(nextAgent);
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
finishing.add(task.id);
|
|
2534
|
+
try {
|
|
2535
|
+
if (code === 0) {
|
|
2536
|
+
try {
|
|
2537
|
+
const researchPath = resolve2(executionDir, "research.md");
|
|
2538
|
+
if (existsSync7(researchPath)) {
|
|
2539
|
+
try {
|
|
2540
|
+
const researchContent = readFileSync5(researchPath, "utf-8");
|
|
2541
|
+
const existingResearch = existingResources.find((r) => r.type === "research");
|
|
2542
|
+
if (existingResearch) {
|
|
2543
|
+
await api.patch(`/api/tasks/${task.id}/resources/${existingResearch.id}`, {
|
|
2544
|
+
content: researchContent
|
|
2545
|
+
});
|
|
2546
|
+
logSuccess(prefix, `Updated existing research resource`);
|
|
2547
|
+
} else {
|
|
2548
|
+
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2549
|
+
type: "research",
|
|
2550
|
+
title: `Research \u2014 ${task.title}`,
|
|
2551
|
+
content: researchContent
|
|
2552
|
+
});
|
|
2553
|
+
logSuccess(prefix, `Uploaded research.md as task resource`);
|
|
2554
|
+
}
|
|
2555
|
+
unlinkSync(researchPath);
|
|
2556
|
+
} catch (err) {
|
|
2557
|
+
logWarn(prefix, `Failed to upload research resource: ${err.message}`);
|
|
2209
2558
|
}
|
|
2210
|
-
unlinkSync(researchPath);
|
|
2211
|
-
} catch (err) {
|
|
2212
|
-
logWarn(prefix, `Failed to upload research resource: ${err.message}`);
|
|
2213
2559
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
logSuccess(prefix, `No ${vcs === "gitlab" ? "MR" : "PR"} needed \u2014 ${noMrDescription}`);
|
|
2222
|
-
}
|
|
2223
|
-
const prLabel = vcs === "gitlab" ? "MR" : "PR";
|
|
2224
|
-
let prUrl = null;
|
|
2225
|
-
if (!noMrRequested) {
|
|
2226
|
-
prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
|
|
2227
|
-
if (!prUrl) {
|
|
2228
|
-
prUrl = await findPrUrlAcrossRepos(legacyBranchName, repoDir, vcs);
|
|
2560
|
+
const noMrPath = resolve2(executionDir, ".mr-no-mr");
|
|
2561
|
+
const noMrRequested = existsSync7(noMrPath);
|
|
2562
|
+
let noMrDescription;
|
|
2563
|
+
if (noMrRequested) {
|
|
2564
|
+
noMrDescription = readFileSync5(noMrPath, "utf-8").trim();
|
|
2565
|
+
unlinkSync(noMrPath);
|
|
2566
|
+
logSuccess(prefix, `No ${vcs === "gitlab" ? "MR" : "PR"} needed \u2014 ${noMrDescription}`);
|
|
2229
2567
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2568
|
+
const prLabel = vcs === "gitlab" ? "MR" : "PR";
|
|
2569
|
+
let prUrl = null;
|
|
2570
|
+
if (!noMrRequested) {
|
|
2571
|
+
prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
|
|
2572
|
+
if (!prUrl) {
|
|
2573
|
+
prUrl = await findPrUrlAcrossRepos(legacyBranchName, repoDir, vcs);
|
|
2574
|
+
}
|
|
2575
|
+
if (!prUrl) {
|
|
2576
|
+
prUrl = await extractPrUrlFromUpdates(task.id);
|
|
2577
|
+
if (prUrl) {
|
|
2578
|
+
logInfo(prefix, `Found ${prLabel} URL from agent updates: ${paint("cyan", prUrl)}`);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
if (!prUrl) {
|
|
2582
|
+
prUrl = await createPrAcrossRepos(
|
|
2583
|
+
task,
|
|
2584
|
+
branchName,
|
|
2585
|
+
repoDir,
|
|
2586
|
+
vcs,
|
|
2587
|
+
subtasks,
|
|
2588
|
+
protoRefs,
|
|
2589
|
+
feedbackUpdates,
|
|
2590
|
+
existingResources,
|
|
2591
|
+
skillRefs
|
|
2592
|
+
);
|
|
2593
|
+
if (!prUrl) {
|
|
2594
|
+
prUrl = await createPrAcrossRepos(
|
|
2595
|
+
task,
|
|
2596
|
+
legacyBranchName,
|
|
2597
|
+
repoDir,
|
|
2598
|
+
vcs,
|
|
2599
|
+
subtasks,
|
|
2600
|
+
protoRefs,
|
|
2601
|
+
feedbackUpdates,
|
|
2602
|
+
existingResources,
|
|
2603
|
+
skillRefs
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
if (prUrl) {
|
|
2607
|
+
logInfo(prefix, `Created missing ${prLabel} for branch ${paint("cyan", branchName)}`);
|
|
2608
|
+
await postTaskUpdate(task.id, `Watch created the missing ${prLabel} for branch ${branchName}`, "system");
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2232
2611
|
if (prUrl) {
|
|
2233
|
-
|
|
2612
|
+
logSuccess(prefix, `${prLabel} ready: ${paint("cyan", prUrl)}`);
|
|
2613
|
+
} else {
|
|
2614
|
+
logWarn(prefix, `No ${prLabel} found for branch ${paint("cyan", branchName)}`);
|
|
2615
|
+
await postTaskUpdate(task.id, `Agent finished \u2014 no ${prLabel} found for branch ${branchName}`, "system");
|
|
2234
2616
|
}
|
|
2235
2617
|
}
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
}
|
|
2618
|
+
await api.patch(`/api/tasks/${task.id}`, {
|
|
2619
|
+
status: "review",
|
|
2620
|
+
claudeSessionId: activeEntry.currentAgent === "claude" ? void 0 : null,
|
|
2621
|
+
...prUrl ? { link: prUrl } : {}
|
|
2622
|
+
});
|
|
2623
|
+
logSuccess(prefix, `"${paint("bold", task.title)}" marked ready for review`);
|
|
2624
|
+
await postTaskUpdate(task.id, noMrRequested ? `Task marked ready for review \u2014 no ${prLabel} needed: ${noMrDescription}` : "Task marked ready for review", "system");
|
|
2625
|
+
} catch (err) {
|
|
2626
|
+
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2242
2627
|
}
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
});
|
|
2247
|
-
|
|
2248
|
-
await postTaskUpdate(task.id, noMrRequested ? `Task marked ready for review \u2014 no ${prLabel} needed: ${noMrDescription}` : "Task marked ready for review", "system");
|
|
2249
|
-
} catch (err) {
|
|
2250
|
-
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2628
|
+
} else if (!activeEntry.terminatedForError) {
|
|
2629
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2630
|
+
const reason = `${attemptAgent} failed (${failureDetail}) \u2014 task moved to error`;
|
|
2631
|
+
logError(prefix, `"${paint("bold", task.title)}" failed (${failureDetail}), moving task to error`);
|
|
2632
|
+
await moveTaskToError(task, prefix, reason);
|
|
2251
2633
|
}
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
|
|
2259
|
-
removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
|
|
2634
|
+
} finally {
|
|
2635
|
+
if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
|
|
2636
|
+
removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
|
|
2637
|
+
}
|
|
2638
|
+
queued.delete(task.id);
|
|
2639
|
+
finishing.delete(task.id);
|
|
2260
2640
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
});
|
|
2641
|
+
});
|
|
2642
|
+
};
|
|
2643
|
+
await launchAttempt(attemptOrder[attemptIndex]);
|
|
2265
2644
|
}
|
|
2266
2645
|
async function dispatchPlanModeTask(task, repoDir) {
|
|
2267
2646
|
const sid = shortId(task.id);
|
|
@@ -2292,65 +2671,122 @@ var watchCommand = new Command8("watch").description(
|
|
|
2292
2671
|
"system"
|
|
2293
2672
|
);
|
|
2294
2673
|
const prompt2 = buildPrdPrompt(task, repoDir, existingPlanResource?.content, feedbackUpdates);
|
|
2295
|
-
const
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2674
|
+
const attemptOrder = await resolveAgentChain(agent);
|
|
2675
|
+
if (attemptOrder.length === 0) {
|
|
2676
|
+
logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
|
|
2677
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "error" }).catch((err) => {
|
|
2678
|
+
logError(prefix, `Failed to mark task as error: ${err.message}`);
|
|
2679
|
+
});
|
|
2680
|
+
await postTaskUpdate(task.id, `PRD generation could not start \u2014 no available agent found for fallback chain starting at ${agent}`, "system");
|
|
2681
|
+
queued.delete(task.id);
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
const activeEntry = {
|
|
2685
|
+
process: void 0,
|
|
2686
|
+
title: task.title,
|
|
2687
|
+
repoDir,
|
|
2688
|
+
startedAt: Date.now(),
|
|
2689
|
+
lastActivityAt: Date.now()
|
|
2690
|
+
};
|
|
2691
|
+
let attemptIndex = 0;
|
|
2692
|
+
const launchAttempt = async (attemptAgent) => {
|
|
2693
|
+
let spawnFailureReason = null;
|
|
2694
|
+
const prdSystemPrompt = composeSystemPrompt(PRD_SYSTEM_SECTIONS);
|
|
2695
|
+
const child = spawnAgent(
|
|
2696
|
+
attemptAgent,
|
|
2697
|
+
repoDir,
|
|
2698
|
+
prompt2,
|
|
2699
|
+
prefix,
|
|
2700
|
+
void 0,
|
|
2701
|
+
void 0,
|
|
2702
|
+
task.title,
|
|
2703
|
+
false,
|
|
2704
|
+
(err) => {
|
|
2705
|
+
spawnFailureReason = err.message;
|
|
2706
|
+
},
|
|
2707
|
+
prdSystemPrompt
|
|
2708
|
+
);
|
|
2709
|
+
activeEntry.process = child;
|
|
2710
|
+
activeEntry.currentAgent = attemptAgent;
|
|
2711
|
+
active.set(task.id, activeEntry);
|
|
2712
|
+
child.on("exit", async (code) => {
|
|
2713
|
+
if (active.get(task.id)?.process === child) {
|
|
2714
|
+
active.delete(task.id);
|
|
2715
|
+
}
|
|
2716
|
+
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
2717
|
+
if (failedAttempt) {
|
|
2718
|
+
const nextAgent = attemptOrder[attemptIndex + 1];
|
|
2719
|
+
if (nextAgent) {
|
|
2720
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2721
|
+
logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying PRD generation with ${nextAgent}`);
|
|
2722
|
+
await postTaskUpdate(
|
|
2723
|
+
task.id,
|
|
2724
|
+
`${attemptAgent} failed (${failureDetail}) \u2014 retrying PRD generation with ${nextAgent}`,
|
|
2725
|
+
"system"
|
|
2726
|
+
);
|
|
2727
|
+
attemptIndex += 1;
|
|
2728
|
+
await launchAttempt(nextAgent);
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
finishing.add(task.id);
|
|
2733
|
+
try {
|
|
2734
|
+
if (code === 0) {
|
|
2305
2735
|
try {
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
unlinkSync(prdPath);
|
|
2309
|
-
} catch {
|
|
2310
|
-
logWarn(prefix, `No prd.md file found in ${repoDir} \u2014 PRD may have been posted inline`);
|
|
2311
|
-
}
|
|
2312
|
-
if (prdContent) {
|
|
2736
|
+
const prdPath = resolve2(repoDir, "prd.md");
|
|
2737
|
+
let prdContent;
|
|
2313
2738
|
try {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2739
|
+
prdContent = readFileSync5(prdPath, "utf-8");
|
|
2740
|
+
logInfo(prefix, `Read PRD from ${paint("cyan", "prd.md")} (${prdContent.length} chars)`);
|
|
2741
|
+
unlinkSync(prdPath);
|
|
2742
|
+
} catch {
|
|
2743
|
+
logWarn(prefix, `No prd.md file found in ${repoDir} \u2014 PRD may have been posted inline`);
|
|
2744
|
+
}
|
|
2745
|
+
if (prdContent) {
|
|
2746
|
+
try {
|
|
2747
|
+
if (existingPlanResource) {
|
|
2748
|
+
await api.patch(`/api/tasks/${task.id}/resources/${existingPlanResource.id}`, {
|
|
2749
|
+
content: prdContent
|
|
2750
|
+
});
|
|
2751
|
+
logSuccess(prefix, `Updated existing PRD resource`);
|
|
2752
|
+
} else {
|
|
2753
|
+
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2754
|
+
type: "plan",
|
|
2755
|
+
title: `PRD \u2014 ${task.title}`,
|
|
2756
|
+
content: prdContent
|
|
2757
|
+
});
|
|
2758
|
+
logSuccess(prefix, `Uploaded PRD as task resource`);
|
|
2759
|
+
}
|
|
2760
|
+
} catch (err) {
|
|
2761
|
+
logWarn(prefix, `Failed to upload PRD resource: ${err.message}`);
|
|
2326
2762
|
}
|
|
2327
|
-
} catch (err) {
|
|
2328
|
-
logWarn(prefix, `Failed to upload PRD resource: ${err.message}`);
|
|
2329
2763
|
}
|
|
2764
|
+
await api.patch(`/api/tasks/${task.id}`, {
|
|
2765
|
+
status: "review",
|
|
2766
|
+
...prdContent ? { prdContent } : {}
|
|
2767
|
+
});
|
|
2768
|
+
logSuccess(prefix, `"${paint("bold", task.title)}" PRD generated and marked ready for review`);
|
|
2769
|
+
await postTaskUpdate(task.id, "PRD generated \u2014 ready for review", "system");
|
|
2770
|
+
} catch (err) {
|
|
2771
|
+
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2330
2772
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
} else {
|
|
2341
|
-
logError(prefix, `"${paint("bold", task.title)}" PRD generation failed (exit ${code}), marked as error`);
|
|
2342
|
-
try {
|
|
2343
|
-
await api.patch(`/api/tasks/${task.id}`, { status: "error" });
|
|
2344
|
-
} catch (err) {
|
|
2345
|
-
logError(prefix, `Failed to mark task as error: ${err.message}`);
|
|
2773
|
+
} else {
|
|
2774
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2775
|
+
logError(prefix, `"${paint("bold", task.title)}" PRD generation failed (${failureDetail}), marked as error`);
|
|
2776
|
+
try {
|
|
2777
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "error" });
|
|
2778
|
+
} catch (err) {
|
|
2779
|
+
logError(prefix, `Failed to mark task as error: ${err.message}`);
|
|
2780
|
+
}
|
|
2781
|
+
await postTaskUpdate(task.id, `PRD generation failed via ${attemptAgent} (${failureDetail}) \u2014 task moved to error`, "system");
|
|
2346
2782
|
}
|
|
2347
|
-
|
|
2783
|
+
} finally {
|
|
2784
|
+
queued.delete(task.id);
|
|
2785
|
+
finishing.delete(task.id);
|
|
2348
2786
|
}
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
}
|
|
2353
|
-
});
|
|
2787
|
+
});
|
|
2788
|
+
};
|
|
2789
|
+
await launchAttempt(attemptOrder[attemptIndex]);
|
|
2354
2790
|
}
|
|
2355
2791
|
async function dispatchPrototypeJob(proto, repoDir) {
|
|
2356
2792
|
const sid = shortId(proto.id);
|
|
@@ -2376,53 +2812,101 @@ var watchCommand = new Command8("watch").description(
|
|
|
2376
2812
|
} else {
|
|
2377
2813
|
prompt2 = buildPrototypePrompt(proto, repoDir);
|
|
2378
2814
|
}
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2815
|
+
const key = `proto-${proto.id}`;
|
|
2816
|
+
const attemptOrder = await resolveAgentChain(agent);
|
|
2817
|
+
if (attemptOrder.length === 0) {
|
|
2818
|
+
logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
|
|
2819
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" }).catch(() => {
|
|
2820
|
+
});
|
|
2821
|
+
queued.delete(key);
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
const activeEntry = {
|
|
2825
|
+
process: void 0,
|
|
2826
|
+
title: proto.title,
|
|
2827
|
+
repoDir,
|
|
2828
|
+
startedAt: Date.now(),
|
|
2829
|
+
lastActivityAt: Date.now()
|
|
2830
|
+
};
|
|
2831
|
+
let attemptIndex = 0;
|
|
2832
|
+
const launchAttempt = async (attemptAgent) => {
|
|
2833
|
+
let spawnFailureReason = null;
|
|
2834
|
+
const child = spawnAgent(
|
|
2835
|
+
attemptAgent,
|
|
2836
|
+
repoDir,
|
|
2837
|
+
prompt2,
|
|
2838
|
+
prefix,
|
|
2839
|
+
void 0,
|
|
2840
|
+
void 0,
|
|
2841
|
+
proto.title,
|
|
2842
|
+
false,
|
|
2843
|
+
(err) => {
|
|
2844
|
+
spawnFailureReason = err.message;
|
|
2845
|
+
}
|
|
2846
|
+
);
|
|
2847
|
+
activeEntry.process = child;
|
|
2848
|
+
activeEntry.currentAgent = attemptAgent;
|
|
2849
|
+
active.set(key, activeEntry);
|
|
2850
|
+
child.on("exit", async (code) => {
|
|
2851
|
+
if (active.get(key)?.process === child) {
|
|
2852
|
+
active.delete(key);
|
|
2853
|
+
}
|
|
2854
|
+
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
2855
|
+
if (failedAttempt) {
|
|
2856
|
+
const nextAgent = attemptOrder[attemptIndex + 1];
|
|
2857
|
+
if (nextAgent) {
|
|
2858
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2859
|
+
logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying prototype generation with ${nextAgent}`);
|
|
2860
|
+
attemptIndex += 1;
|
|
2861
|
+
await launchAttempt(nextAgent);
|
|
2862
|
+
return;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
finishing.add(key);
|
|
2866
|
+
try {
|
|
2867
|
+
if (code === 0) {
|
|
2868
|
+
try {
|
|
2869
|
+
const protoPattern = /^prototype-\d+\.html$/;
|
|
2870
|
+
const found = readdirSync(repoDir).filter((f) => protoPattern.test(f)).sort();
|
|
2871
|
+
const files = found.map((f) => ({
|
|
2872
|
+
name: f,
|
|
2873
|
+
content: readFileSync5(resolve2(repoDir, f), "utf-8")
|
|
2874
|
+
}));
|
|
2875
|
+
if (files.length === 0) {
|
|
2876
|
+
logError(prefix, `No prototype HTML files found in ${repoDir}`);
|
|
2877
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "completed", files });
|
|
2881
|
+
logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${files.length} file(s)`);
|
|
2882
|
+
for (const file of files) {
|
|
2883
|
+
try {
|
|
2884
|
+
unlinkSync(resolve2(repoDir, file.name));
|
|
2885
|
+
} catch {
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
} catch (err) {
|
|
2889
|
+
logError(prefix, `Failed to upload prototype: ${err.message}`);
|
|
2402
2890
|
try {
|
|
2403
|
-
|
|
2891
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2404
2892
|
} catch {
|
|
2405
2893
|
}
|
|
2406
2894
|
}
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2895
|
+
} else {
|
|
2896
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2897
|
+
logError(prefix, `"${paint("bold", proto.title)}" prototype failed via ${attemptAgent} (${failureDetail})`);
|
|
2409
2898
|
try {
|
|
2410
2899
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2411
2900
|
} catch {
|
|
2412
2901
|
}
|
|
2413
2902
|
}
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2418
|
-
} catch {
|
|
2419
|
-
}
|
|
2903
|
+
} finally {
|
|
2904
|
+
queued.delete(key);
|
|
2905
|
+
finishing.delete(key);
|
|
2420
2906
|
}
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
}
|
|
2425
|
-
});
|
|
2907
|
+
});
|
|
2908
|
+
};
|
|
2909
|
+
await launchAttempt(attemptOrder[attemptIndex]);
|
|
2426
2910
|
}
|
|
2427
2911
|
async function dispatchRepoCreation(project, workDir) {
|
|
2428
2912
|
const sid = shortId(project.id);
|
|
@@ -2433,27 +2917,81 @@ var watchCommand = new Command8("watch").description(
|
|
|
2433
2917
|
} catch {
|
|
2434
2918
|
}
|
|
2435
2919
|
const prompt2 = buildRepoCreationPrompt(project, workDir);
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
finishing.add(key);
|
|
2920
|
+
const key = `repo-${project.id}`;
|
|
2921
|
+
const attemptOrder = await resolveAgentChain(agent);
|
|
2922
|
+
if (attemptOrder.length === 0) {
|
|
2923
|
+
logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
|
|
2924
|
+
queued.delete(key);
|
|
2442
2925
|
try {
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2926
|
+
await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "failed" });
|
|
2927
|
+
} catch {
|
|
2928
|
+
}
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
const activeEntry = {
|
|
2932
|
+
process: void 0,
|
|
2933
|
+
title: project.name,
|
|
2934
|
+
repoDir: workDir,
|
|
2935
|
+
startedAt: Date.now(),
|
|
2936
|
+
lastActivityAt: Date.now()
|
|
2937
|
+
};
|
|
2938
|
+
let attemptIndex = 0;
|
|
2939
|
+
const launchAttempt = async (attemptAgent) => {
|
|
2940
|
+
let spawnFailureReason = null;
|
|
2941
|
+
const child = spawnAgent(
|
|
2942
|
+
attemptAgent,
|
|
2943
|
+
workDir,
|
|
2944
|
+
prompt2,
|
|
2945
|
+
prefix,
|
|
2946
|
+
void 0,
|
|
2947
|
+
void 0,
|
|
2948
|
+
project.name,
|
|
2949
|
+
false,
|
|
2950
|
+
(err) => {
|
|
2951
|
+
spawnFailureReason = err.message;
|
|
2952
|
+
}
|
|
2953
|
+
);
|
|
2954
|
+
activeEntry.process = child;
|
|
2955
|
+
activeEntry.currentAgent = attemptAgent;
|
|
2956
|
+
active.set(key, activeEntry);
|
|
2957
|
+
child.on("exit", async (code) => {
|
|
2958
|
+
if (active.get(key)?.process === child) {
|
|
2959
|
+
active.delete(key);
|
|
2960
|
+
}
|
|
2961
|
+
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
2962
|
+
if (failedAttempt) {
|
|
2963
|
+
const nextAgent = attemptOrder[attemptIndex + 1];
|
|
2964
|
+
if (nextAgent) {
|
|
2965
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2966
|
+
logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying repo creation with ${nextAgent}`);
|
|
2967
|
+
attemptIndex += 1;
|
|
2968
|
+
await launchAttempt(nextAgent);
|
|
2969
|
+
return;
|
|
2450
2970
|
}
|
|
2451
2971
|
}
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2972
|
+
finishing.add(key);
|
|
2973
|
+
try {
|
|
2974
|
+
if (code === 0) {
|
|
2975
|
+
logSuccess(prefix, `"${paint("bold", project.name)}" repo creation complete`);
|
|
2976
|
+
try {
|
|
2977
|
+
await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "created" });
|
|
2978
|
+
} catch {
|
|
2979
|
+
}
|
|
2980
|
+
} else {
|
|
2981
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2982
|
+
logError(prefix, `"${paint("bold", project.name)}" repo creation failed via ${attemptAgent} (${failureDetail})`);
|
|
2983
|
+
try {
|
|
2984
|
+
await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "failed" });
|
|
2985
|
+
} catch {
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
} finally {
|
|
2989
|
+
queued.delete(key);
|
|
2990
|
+
finishing.delete(key);
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
};
|
|
2994
|
+
await launchAttempt(attemptOrder[attemptIndex]);
|
|
2457
2995
|
}
|
|
2458
2996
|
async function dispatchIdeaJob(idea, repoDir) {
|
|
2459
2997
|
const sid = shortId(idea.id);
|
|
@@ -2466,96 +3004,146 @@ var watchCommand = new Command8("watch").description(
|
|
|
2466
3004
|
}
|
|
2467
3005
|
}
|
|
2468
3006
|
const prompt2 = buildIdeaPrompt(idea, repoDir);
|
|
2469
|
-
const
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
finishing.add(key);
|
|
3007
|
+
const key = `idea-${idea.id}`;
|
|
3008
|
+
const attemptOrder = await resolveAgentChain(agent);
|
|
3009
|
+
if (attemptOrder.length === 0) {
|
|
3010
|
+
logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
|
|
3011
|
+
queued.delete(key);
|
|
2475
3012
|
try {
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
3013
|
+
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
3014
|
+
} catch {
|
|
3015
|
+
}
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
const activeEntry = {
|
|
3019
|
+
process: void 0,
|
|
3020
|
+
title: idea.title,
|
|
3021
|
+
repoDir,
|
|
3022
|
+
startedAt: Date.now(),
|
|
3023
|
+
lastActivityAt: Date.now()
|
|
3024
|
+
};
|
|
3025
|
+
let attemptIndex = 0;
|
|
3026
|
+
const launchAttempt = async (attemptAgent) => {
|
|
3027
|
+
let spawnFailureReason = null;
|
|
3028
|
+
const child = spawnAgent(
|
|
3029
|
+
attemptAgent,
|
|
3030
|
+
repoDir,
|
|
3031
|
+
prompt2,
|
|
3032
|
+
prefix,
|
|
3033
|
+
void 0,
|
|
3034
|
+
void 0,
|
|
3035
|
+
idea.title,
|
|
3036
|
+
false,
|
|
3037
|
+
(err) => {
|
|
3038
|
+
spawnFailureReason = err.message;
|
|
3039
|
+
}
|
|
3040
|
+
);
|
|
3041
|
+
activeEntry.process = child;
|
|
3042
|
+
activeEntry.currentAgent = attemptAgent;
|
|
3043
|
+
active.set(key, activeEntry);
|
|
3044
|
+
child.on("exit", async (code) => {
|
|
3045
|
+
if (active.get(key)?.process === child) {
|
|
3046
|
+
active.delete(key);
|
|
3047
|
+
}
|
|
3048
|
+
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
3049
|
+
if (failedAttempt) {
|
|
3050
|
+
const nextAgent = attemptOrder[attemptIndex + 1];
|
|
3051
|
+
if (nextAgent) {
|
|
3052
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
3053
|
+
logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying idea generation with ${nextAgent}`);
|
|
3054
|
+
attemptIndex += 1;
|
|
3055
|
+
await launchAttempt(nextAgent);
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
finishing.add(key);
|
|
3060
|
+
try {
|
|
3061
|
+
if (code === 0) {
|
|
2492
3062
|
try {
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
3063
|
+
let plan;
|
|
3064
|
+
let protoHtml;
|
|
3065
|
+
let followUpTasks;
|
|
3066
|
+
const planPath = resolve2(repoDir, "idea-plan.md");
|
|
3067
|
+
const tasksPath = resolve2(repoDir, "idea-tasks.json");
|
|
3068
|
+
const protoPath = resolve2(repoDir, "idea-prototype.html");
|
|
3069
|
+
try {
|
|
3070
|
+
plan = readFileSync5(planPath, "utf-8");
|
|
3071
|
+
} catch {
|
|
2497
3072
|
}
|
|
2498
|
-
} catch {
|
|
2499
|
-
}
|
|
2500
|
-
if (!plan && !protoHtml) {
|
|
2501
|
-
logError(prefix, `No output files found in ${repoDir}`);
|
|
2502
|
-
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2503
|
-
return;
|
|
2504
|
-
}
|
|
2505
|
-
const updateData = { status: "generated" };
|
|
2506
|
-
if (plan) updateData.plan = plan;
|
|
2507
|
-
if (followUpTasks) updateData.followUpTasks = followUpTasks;
|
|
2508
|
-
if (protoHtml) {
|
|
2509
3073
|
try {
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
3074
|
+
protoHtml = readFileSync5(protoPath, "utf-8");
|
|
3075
|
+
} catch {
|
|
3076
|
+
}
|
|
3077
|
+
try {
|
|
3078
|
+
const tasksRaw = readFileSync5(tasksPath, "utf-8");
|
|
3079
|
+
const parsed = JSON.parse(tasksRaw);
|
|
3080
|
+
if (Array.isArray(parsed)) {
|
|
3081
|
+
followUpTasks = parsed.filter((t) => t && typeof t === "object" && "title" in t);
|
|
3082
|
+
}
|
|
3083
|
+
} catch {
|
|
3084
|
+
}
|
|
3085
|
+
if (!plan && !protoHtml) {
|
|
3086
|
+
logError(prefix, `No output files found in ${repoDir}`);
|
|
3087
|
+
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
const updateData = { status: "generated" };
|
|
3091
|
+
if (plan) updateData.plan = plan;
|
|
3092
|
+
if (followUpTasks) updateData.followUpTasks = followUpTasks;
|
|
3093
|
+
if (protoHtml) {
|
|
3094
|
+
try {
|
|
3095
|
+
const proto = await api.post("/api/prototypes", {
|
|
3096
|
+
title: `${idea.title} \u2014 Idea Prototype`,
|
|
3097
|
+
prompt: idea.description || idea.title,
|
|
3098
|
+
variantCount: 1,
|
|
3099
|
+
projectId: idea.projectId ?? null
|
|
3100
|
+
});
|
|
3101
|
+
await api.patch(`/api/prototypes/${proto.id}`, {
|
|
3102
|
+
status: "completed",
|
|
3103
|
+
files: [{ name: "idea-prototype.html", content: protoHtml }]
|
|
3104
|
+
});
|
|
3105
|
+
updateData.generatedPrototypeId = proto.id;
|
|
3106
|
+
logSuccess(prefix, `Prototype created: ${paint("gray", proto.id.slice(0, 8))}`);
|
|
3107
|
+
} catch (err) {
|
|
3108
|
+
logError(prefix, `Failed to create prototype: ${err.message}`);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
await api.patch(`/api/ideas/${idea.id}`, updateData);
|
|
3112
|
+
logSuccess(prefix, `"${paint("bold", idea.title)}" generation complete`);
|
|
3113
|
+
try {
|
|
3114
|
+
unlinkSync(planPath);
|
|
3115
|
+
} catch {
|
|
3116
|
+
}
|
|
3117
|
+
try {
|
|
3118
|
+
unlinkSync(tasksPath);
|
|
3119
|
+
} catch {
|
|
3120
|
+
}
|
|
3121
|
+
try {
|
|
3122
|
+
unlinkSync(protoPath);
|
|
3123
|
+
} catch {
|
|
3124
|
+
}
|
|
3125
|
+
} catch (err) {
|
|
3126
|
+
logError(prefix, `Failed to upload idea output: ${err.message}`);
|
|
3127
|
+
try {
|
|
3128
|
+
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
3129
|
+
} catch {
|
|
2524
3130
|
}
|
|
2525
3131
|
}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
unlinkSync(planPath);
|
|
2530
|
-
} catch {
|
|
2531
|
-
}
|
|
2532
|
-
try {
|
|
2533
|
-
unlinkSync(tasksPath);
|
|
2534
|
-
} catch {
|
|
2535
|
-
}
|
|
2536
|
-
try {
|
|
2537
|
-
unlinkSync(protoPath);
|
|
2538
|
-
} catch {
|
|
2539
|
-
}
|
|
2540
|
-
} catch (err) {
|
|
2541
|
-
logError(prefix, `Failed to upload idea output: ${err.message}`);
|
|
3132
|
+
} else {
|
|
3133
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
3134
|
+
logError(prefix, `"${paint("bold", idea.title)}" generation failed via ${attemptAgent} (${failureDetail})`);
|
|
2542
3135
|
try {
|
|
2543
3136
|
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2544
3137
|
} catch {
|
|
2545
3138
|
}
|
|
2546
3139
|
}
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2551
|
-
} catch {
|
|
2552
|
-
}
|
|
3140
|
+
} finally {
|
|
3141
|
+
queued.delete(key);
|
|
3142
|
+
finishing.delete(key);
|
|
2553
3143
|
}
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
}
|
|
2558
|
-
});
|
|
3144
|
+
});
|
|
3145
|
+
};
|
|
3146
|
+
await launchAttempt(attemptOrder[attemptIndex]);
|
|
2559
3147
|
}
|
|
2560
3148
|
function dispatchScan(scan, prefix, key) {
|
|
2561
3149
|
logDispatch(prefix, `Running scan for project ${paint("cyan", scan.projectId.slice(0, 8))}`);
|
|
@@ -2569,7 +3157,13 @@ var watchCommand = new Command8("watch").description(
|
|
|
2569
3157
|
queued.delete(key);
|
|
2570
3158
|
failed.set(key, err.message);
|
|
2571
3159
|
});
|
|
2572
|
-
active.set(key, {
|
|
3160
|
+
active.set(key, {
|
|
3161
|
+
process: scanProc,
|
|
3162
|
+
title: `scan-${scan.id.slice(0, 8)}`,
|
|
3163
|
+
repoDir: rootDir,
|
|
3164
|
+
startedAt: Date.now(),
|
|
3165
|
+
lastActivityAt: Date.now()
|
|
3166
|
+
});
|
|
2573
3167
|
scanProc.stdout?.on("data", (d) => {
|
|
2574
3168
|
const lines = d.toString().trim().split("\n");
|
|
2575
3169
|
for (const line of lines) {
|
|
@@ -2601,7 +3195,28 @@ var watchCommand = new Command8("watch").description(
|
|
|
2601
3195
|
const prefix = taskTag(sid);
|
|
2602
3196
|
try {
|
|
2603
3197
|
logSpinner(prefix, `Generating plan for "${paint("bold", task.title)}"\u2026`);
|
|
2604
|
-
const
|
|
3198
|
+
const attemptOrder = await resolveAgentChain(agent);
|
|
3199
|
+
if (attemptOrder.length === 0) {
|
|
3200
|
+
throw new Error(`No available agent found for fallback chain starting at ${agent}`);
|
|
3201
|
+
}
|
|
3202
|
+
let plan;
|
|
3203
|
+
let lastError;
|
|
3204
|
+
for (let idx = 0; idx < attemptOrder.length; idx += 1) {
|
|
3205
|
+
const attemptAgent = attemptOrder[idx];
|
|
3206
|
+
try {
|
|
3207
|
+
plan = await runPlanningPhase(task, repoDir, attemptAgent);
|
|
3208
|
+
break;
|
|
3209
|
+
} catch (err) {
|
|
3210
|
+
lastError = err;
|
|
3211
|
+
const nextAgent = attemptOrder[idx + 1];
|
|
3212
|
+
if (nextAgent) {
|
|
3213
|
+
logWarn(prefix, `${attemptAgent} planning failed (${lastError.message}) \u2014 retrying with ${nextAgent}`);
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
if (!plan) {
|
|
3218
|
+
throw lastError ?? new Error("Planning failed");
|
|
3219
|
+
}
|
|
2605
3220
|
const width = 64;
|
|
2606
3221
|
const divider = paint("gray", "\u2500".repeat(width));
|
|
2607
3222
|
const header = `${paint("bold", "Plan")} "${paint("bold", task.title)}" ${taskTag(sid)}`;
|
|
@@ -2794,14 +3409,16 @@ ${divider}`);
|
|
|
2794
3409
|
if (failed.has(key)) continue;
|
|
2795
3410
|
const sid = shortId(proto.id);
|
|
2796
3411
|
const prefix = protoTag(sid);
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3412
|
+
let repoDir;
|
|
3413
|
+
if (proto.projectId) {
|
|
3414
|
+
const dir = findDirectoryForProject(config, proto.projectId, rootDir);
|
|
3415
|
+
if (!dir) {
|
|
3416
|
+
logWarn(prefix, `"${proto.title}": no linked directory found \u2014 skipping`);
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
repoDir = dir;
|
|
3420
|
+
} else {
|
|
3421
|
+
repoDir = rootDir;
|
|
2805
3422
|
}
|
|
2806
3423
|
if (!existsSync7(repoDir)) {
|
|
2807
3424
|
logError(prefix, `"${proto.title}": linked directory "${repoDir}" does not exist \u2014 skipping`);
|
|
@@ -2884,9 +3501,9 @@ ${divider}`);
|
|
|
2884
3501
|
browseRunner: runBrowseCommand2,
|
|
2885
3502
|
uploadScreenshot: async (screenshotPath, message) => {
|
|
2886
3503
|
try {
|
|
2887
|
-
const { readFileSync:
|
|
3504
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
2888
3505
|
const cfg = loadConfig();
|
|
2889
|
-
const imageBuffer =
|
|
3506
|
+
const imageBuffer = readFileSync13(screenshotPath);
|
|
2890
3507
|
const fileName = screenshotPath.split("/").pop() || "test-screenshot.png";
|
|
2891
3508
|
const formData = new FormData();
|
|
2892
3509
|
const blob = new Blob([imageBuffer], { type: "image/png" });
|
|
@@ -3312,22 +3929,21 @@ var prototypeCommand = new Command13("prototype").description("Manage prototypes
|
|
|
3312
3929
|
}
|
|
3313
3930
|
})
|
|
3314
3931
|
).addCommand(
|
|
3315
|
-
new Command13("create").description("Create a new prototype").argument("<title>", "Title of the prototype").requiredOption("--prompt <prompt>", "Design description / prompt").option("--project <projectId>", "Project ID (defaults to linked project)").option("--variants <count>", "Number of variants to generate (1-50)", "5").action(async (title, opts) => {
|
|
3932
|
+
new Command13("create").description("Create a new prototype").argument("<title>", "Title of the prototype").requiredOption("--prompt <prompt>", "Design description / prompt").option("--project <projectId>", "Project ID (defaults to linked project, when available)").option("--variants <count>", "Number of variants to generate (1-50)", "5").action(async (title, opts) => {
|
|
3316
3933
|
const projectId = opts.project ?? getLinkedProjectId();
|
|
3317
|
-
if (!projectId) {
|
|
3318
|
-
console.error('No project linked. Run "mr link <project-id>" or use --project.');
|
|
3319
|
-
process.exit(1);
|
|
3320
|
-
}
|
|
3321
3934
|
const variantCount = Math.max(1, Math.min(50, parseInt(opts.variants, 10) || 5));
|
|
3322
3935
|
const prototype = await api.post("/api/prototypes", {
|
|
3323
3936
|
title,
|
|
3324
3937
|
prompt: opts.prompt,
|
|
3325
3938
|
variantCount,
|
|
3326
|
-
projectId
|
|
3939
|
+
projectId: projectId ?? null
|
|
3327
3940
|
});
|
|
3328
3941
|
console.log();
|
|
3329
3942
|
console.log(` ${paint4("green", "\u2713")} Created prototype: ${paint4("bold", prototype.title)}`);
|
|
3330
3943
|
console.log(` ${paint4("gray", "ID:")} ${prototype.id}`);
|
|
3944
|
+
if (!prototype.projectId) {
|
|
3945
|
+
console.log(` ${paint4("gray", "Project:")} none (will generate in the active watch directory)`);
|
|
3946
|
+
}
|
|
3331
3947
|
console.log(` ${paint4("cyan", "\u27F3")} Generation will begin automatically via the watch agent.`);
|
|
3332
3948
|
console.log();
|
|
3333
3949
|
})
|
|
@@ -3370,20 +3986,20 @@ var c5 = {
|
|
|
3370
3986
|
function paint5(color, text) {
|
|
3371
3987
|
return `${c5[color]}${text}${c5.reset}`;
|
|
3372
3988
|
}
|
|
3373
|
-
function
|
|
3374
|
-
return new Promise((
|
|
3375
|
-
exec2(`which ${cmd}`, (err) =>
|
|
3989
|
+
function commandExists2(cmd) {
|
|
3990
|
+
return new Promise((resolve8) => {
|
|
3991
|
+
exec2(`which ${cmd}`, (err) => resolve8(!err));
|
|
3376
3992
|
});
|
|
3377
3993
|
}
|
|
3378
3994
|
function execQuiet(cmd) {
|
|
3379
|
-
return new Promise((
|
|
3995
|
+
return new Promise((resolve8) => {
|
|
3380
3996
|
exec2(cmd, (err, stdout, stderr) => {
|
|
3381
|
-
|
|
3997
|
+
resolve8({ ok: !err, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
3382
3998
|
});
|
|
3383
3999
|
});
|
|
3384
4000
|
}
|
|
3385
4001
|
async function checkGitInstalled() {
|
|
3386
|
-
const exists = await
|
|
4002
|
+
const exists = await commandExists2("git");
|
|
3387
4003
|
if (!exists) {
|
|
3388
4004
|
return {
|
|
3389
4005
|
name: "Git",
|
|
@@ -3423,7 +4039,7 @@ async function checkNodeVersion() {
|
|
|
3423
4039
|
};
|
|
3424
4040
|
}
|
|
3425
4041
|
async function checkGhInstalled() {
|
|
3426
|
-
const exists = await
|
|
4042
|
+
const exists = await commandExists2("gh");
|
|
3427
4043
|
return {
|
|
3428
4044
|
name: "GitHub CLI (gh)",
|
|
3429
4045
|
ok: exists,
|
|
@@ -3432,7 +4048,7 @@ async function checkGhInstalled() {
|
|
|
3432
4048
|
};
|
|
3433
4049
|
}
|
|
3434
4050
|
async function checkGhAuth() {
|
|
3435
|
-
const exists = await
|
|
4051
|
+
const exists = await commandExists2("gh");
|
|
3436
4052
|
if (!exists) {
|
|
3437
4053
|
return {
|
|
3438
4054
|
name: "GitHub CLI auth",
|
|
@@ -3450,7 +4066,7 @@ async function checkGhAuth() {
|
|
|
3450
4066
|
};
|
|
3451
4067
|
}
|
|
3452
4068
|
async function checkClaudeInstalled() {
|
|
3453
|
-
const exists = await
|
|
4069
|
+
const exists = await commandExists2("claude");
|
|
3454
4070
|
return {
|
|
3455
4071
|
name: "Claude Code (claude)",
|
|
3456
4072
|
ok: exists,
|
|
@@ -3459,7 +4075,7 @@ async function checkClaudeInstalled() {
|
|
|
3459
4075
|
};
|
|
3460
4076
|
}
|
|
3461
4077
|
async function checkClaudeAuth() {
|
|
3462
|
-
const exists = await
|
|
4078
|
+
const exists = await commandExists2("claude");
|
|
3463
4079
|
if (!exists) {
|
|
3464
4080
|
return {
|
|
3465
4081
|
name: "Claude Code auth",
|
|
@@ -3477,7 +4093,7 @@ async function checkClaudeAuth() {
|
|
|
3477
4093
|
};
|
|
3478
4094
|
}
|
|
3479
4095
|
async function checkCodexInstalled() {
|
|
3480
|
-
const exists = await
|
|
4096
|
+
const exists = await commandExists2("codex");
|
|
3481
4097
|
return {
|
|
3482
4098
|
name: "Codex CLI (codex)",
|
|
3483
4099
|
ok: exists,
|
|
@@ -3486,7 +4102,7 @@ async function checkCodexInstalled() {
|
|
|
3486
4102
|
};
|
|
3487
4103
|
}
|
|
3488
4104
|
async function checkCodexAuth() {
|
|
3489
|
-
const exists = await
|
|
4105
|
+
const exists = await commandExists2("codex");
|
|
3490
4106
|
if (!exists) {
|
|
3491
4107
|
return {
|
|
3492
4108
|
name: "Codex CLI auth",
|
|
@@ -3503,7 +4119,7 @@ async function checkCodexAuth() {
|
|
|
3503
4119
|
};
|
|
3504
4120
|
}
|
|
3505
4121
|
async function checkGeminiInstalled() {
|
|
3506
|
-
const exists = await
|
|
4122
|
+
const exists = await commandExists2("gemini");
|
|
3507
4123
|
return {
|
|
3508
4124
|
name: "Gemini CLI (gemini)",
|
|
3509
4125
|
ok: exists,
|
|
@@ -3512,7 +4128,7 @@ async function checkGeminiInstalled() {
|
|
|
3512
4128
|
};
|
|
3513
4129
|
}
|
|
3514
4130
|
async function checkGeminiAuth() {
|
|
3515
|
-
const exists = await
|
|
4131
|
+
const exists = await commandExists2("gemini");
|
|
3516
4132
|
if (!exists) {
|
|
3517
4133
|
return {
|
|
3518
4134
|
name: "Gemini CLI auth",
|
|
@@ -3529,7 +4145,7 @@ async function checkGeminiAuth() {
|
|
|
3529
4145
|
};
|
|
3530
4146
|
}
|
|
3531
4147
|
async function checkGlabInstalled() {
|
|
3532
|
-
const exists = await
|
|
4148
|
+
const exists = await commandExists2("glab");
|
|
3533
4149
|
return {
|
|
3534
4150
|
name: "GitLab CLI (glab)",
|
|
3535
4151
|
ok: exists,
|
|
@@ -3539,7 +4155,7 @@ async function checkGlabInstalled() {
|
|
|
3539
4155
|
};
|
|
3540
4156
|
}
|
|
3541
4157
|
async function checkGlabAuth() {
|
|
3542
|
-
const exists = await
|
|
4158
|
+
const exists = await commandExists2("glab");
|
|
3543
4159
|
if (!exists) {
|
|
3544
4160
|
return {
|
|
3545
4161
|
name: "GitLab CLI auth",
|
|
@@ -3559,7 +4175,7 @@ async function checkGlabAuth() {
|
|
|
3559
4175
|
};
|
|
3560
4176
|
}
|
|
3561
4177
|
async function checkJqInstalled() {
|
|
3562
|
-
const exists = await
|
|
4178
|
+
const exists = await commandExists2("jq");
|
|
3563
4179
|
return {
|
|
3564
4180
|
name: "jq (optional)",
|
|
3565
4181
|
ok: exists,
|
|
@@ -3633,26 +4249,26 @@ async function autoFix(checks, agent) {
|
|
|
3633
4249
|
if (claudeCheck && !claudeCheck.ok && agent === "claude") {
|
|
3634
4250
|
console.log(paint5("cyan", " Installing Claude Code..."));
|
|
3635
4251
|
console.log(paint5("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
|
|
3636
|
-
await new Promise((
|
|
4252
|
+
await new Promise((resolve8) => {
|
|
3637
4253
|
const child = spawn8("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], { stdio: "inherit" });
|
|
3638
|
-
child.on("exit", () =>
|
|
4254
|
+
child.on("exit", () => resolve8());
|
|
3639
4255
|
});
|
|
3640
4256
|
console.log("");
|
|
3641
4257
|
}
|
|
3642
4258
|
if (ghInstalled && !ghAuthed) {
|
|
3643
4259
|
console.log(paint5("cyan", " Running gh auth login..."));
|
|
3644
|
-
await new Promise((
|
|
4260
|
+
await new Promise((resolve8) => {
|
|
3645
4261
|
const child = spawn8("gh", ["auth", "login"], { stdio: "inherit" });
|
|
3646
|
-
child.on("exit", () =>
|
|
4262
|
+
child.on("exit", () => resolve8());
|
|
3647
4263
|
});
|
|
3648
4264
|
console.log("");
|
|
3649
4265
|
}
|
|
3650
4266
|
if (!mrAuthed) {
|
|
3651
4267
|
console.log(paint5("cyan", " Running mr login..."));
|
|
3652
4268
|
const entry = process.argv[1];
|
|
3653
|
-
await new Promise((
|
|
4269
|
+
await new Promise((resolve8) => {
|
|
3654
4270
|
const child = spawn8(process.execPath, [entry, "login"], { stdio: "inherit" });
|
|
3655
|
-
child.on("exit", () =>
|
|
4271
|
+
child.on("exit", () => resolve8());
|
|
3656
4272
|
});
|
|
3657
4273
|
console.log("");
|
|
3658
4274
|
}
|
|
@@ -3900,7 +4516,7 @@ var resumeCommand = new Command17("resume").description("Resume an interactive C
|
|
|
3900
4516
|
// cli/commands/browse.ts
|
|
3901
4517
|
import { Command as Command18 } from "commander";
|
|
3902
4518
|
import { execSync as execSync5, spawn as spawn6 } from "child_process";
|
|
3903
|
-
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as
|
|
4519
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
3904
4520
|
import { join as join8 } from "path";
|
|
3905
4521
|
var BROWSE_DIR2 = join8(import.meta.dirname, "..", "..", "browse");
|
|
3906
4522
|
function isProcessAlive(pid) {
|
|
@@ -3947,7 +4563,7 @@ async function ensureDevServer() {
|
|
|
3947
4563
|
env: { ...process.env }
|
|
3948
4564
|
});
|
|
3949
4565
|
devProc.unref();
|
|
3950
|
-
|
|
4566
|
+
writeFileSync5(
|
|
3951
4567
|
devStateFile,
|
|
3952
4568
|
JSON.stringify({ pid: devProc.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
3953
4569
|
{ mode: 384 }
|
|
@@ -4189,7 +4805,7 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
4189
4805
|
|
|
4190
4806
|
// cli/commands/features.ts
|
|
4191
4807
|
import { Command as Command21 } from "commander";
|
|
4192
|
-
import { readFileSync as readFileSync9, writeFileSync as
|
|
4808
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync12 } from "fs";
|
|
4193
4809
|
import { resolve as resolve5, sep as sep2 } from "path";
|
|
4194
4810
|
var FEATURES_FILE3 = ".mr-features.md";
|
|
4195
4811
|
var c7 = {
|
|
@@ -4230,13 +4846,13 @@ var featuresCommand = new Command21("features").description("View or update the
|
|
|
4230
4846
|
if (opts.file) {
|
|
4231
4847
|
const content2 = readFileSync9(resolve5(opts.file), "utf-8");
|
|
4232
4848
|
const featuresPath = getFeaturesPath();
|
|
4233
|
-
|
|
4849
|
+
writeFileSync6(featuresPath, content2);
|
|
4234
4850
|
console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)} from ${paint7("cyan", opts.file)}`);
|
|
4235
4851
|
return;
|
|
4236
4852
|
}
|
|
4237
4853
|
if (opts.update) {
|
|
4238
4854
|
const featuresPath = getFeaturesPath();
|
|
4239
|
-
|
|
4855
|
+
writeFileSync6(featuresPath, opts.update);
|
|
4240
4856
|
console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)}`);
|
|
4241
4857
|
return;
|
|
4242
4858
|
}
|
|
@@ -4251,12 +4867,12 @@ var featuresCommand = new Command21("features").description("View or update the
|
|
|
4251
4867
|
|
|
4252
4868
|
// cli/commands/no-mr.ts
|
|
4253
4869
|
import { Command as Command22 } from "commander";
|
|
4254
|
-
import { writeFileSync as
|
|
4870
|
+
import { writeFileSync as writeFileSync7 } from "fs";
|
|
4255
4871
|
import { resolve as resolve6 } from "path";
|
|
4256
4872
|
var NO_MR_FILE = ".mr-no-mr";
|
|
4257
4873
|
var noMrCommand = new Command22("no-mr").description("Signal that a task does not require a merge/pull request and describe what was done instead").argument("<task-id>", "Task ID").argument("<description>", "Description of what was done instead of creating an MR/PR").action(async (taskId, description) => {
|
|
4258
4874
|
const filePath = resolve6(process.cwd(), NO_MR_FILE);
|
|
4259
|
-
|
|
4875
|
+
writeFileSync7(filePath, description, "utf-8");
|
|
4260
4876
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
4261
4877
|
message: `No MR/PR needed \u2014 ${description}`,
|
|
4262
4878
|
source: "agent"
|
|
@@ -4495,8 +5111,8 @@ function matchesIgnorePattern(route, patterns) {
|
|
|
4495
5111
|
}
|
|
4496
5112
|
async function uploadScreenshot(imagePath, apiUrl, apiKey) {
|
|
4497
5113
|
try {
|
|
4498
|
-
const { readFileSync:
|
|
4499
|
-
const imageBuffer =
|
|
5114
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
5115
|
+
const imageBuffer = readFileSync13(imagePath);
|
|
4500
5116
|
const blob = new Blob([imageBuffer], { type: "image/webp" });
|
|
4501
5117
|
const formData = new FormData();
|
|
4502
5118
|
formData.append("file", blob, `scan-${Date.now()}.webp`);
|
|
@@ -4703,22 +5319,19 @@ function buildSynthesisPrompt(config, context, codebaseAnalysis, crawlResults, p
|
|
|
4703
5319
|
const promotedFindings = priorFindings.filter((f) => f.status === "promoted");
|
|
4704
5320
|
const crawlSummary = crawlResults.map((r) => {
|
|
4705
5321
|
let summary = `Route: ${r.route}
|
|
4706
|
-
Title: ${r.pageTitle}
|
|
4707
|
-
Headings: ${r.headings.join(", ") || "none"}`;
|
|
5322
|
+
Title: ${r.pageTitle}`;
|
|
4708
5323
|
if (r.consoleErrors.length > 0) {
|
|
4709
5324
|
summary += `
|
|
4710
|
-
Console Errors: ${r.consoleErrors.slice(0,
|
|
5325
|
+
Console Errors: ${r.consoleErrors.slice(0, 3).join("; ")}`;
|
|
4711
5326
|
}
|
|
4712
5327
|
if (r.consoleWarnings.length > 0) {
|
|
4713
5328
|
summary += `
|
|
4714
|
-
Console Warnings: ${r.consoleWarnings.slice(0,
|
|
5329
|
+
Console Warnings: ${r.consoleWarnings.slice(0, 2).join("; ")}`;
|
|
4715
5330
|
}
|
|
4716
|
-
if (r.
|
|
5331
|
+
if (r.loadTimeMs > 1e3) {
|
|
4717
5332
|
summary += `
|
|
4718
|
-
|
|
5333
|
+
Load time: ${r.loadTimeMs}ms (slow)`;
|
|
4719
5334
|
}
|
|
4720
|
-
summary += `
|
|
4721
|
-
Load time: ${r.loadTimeMs}ms`;
|
|
4722
5335
|
return summary;
|
|
4723
5336
|
}).join("\n\n");
|
|
4724
5337
|
return `You are an autonomous product manager reviewing a web application. Your job is to produce actionable, specific findings that a developer can use to improve their product.
|
|
@@ -4735,16 +5348,16 @@ ${codebaseAnalysis.routes.map((r) => `- ${r}`).join("\n")}
|
|
|
4735
5348
|
${codebaseAnalysis.prismaModels.map((m) => `- ${m}`).join("\n")}
|
|
4736
5349
|
|
|
4737
5350
|
**Components:**
|
|
4738
|
-
${codebaseAnalysis.components.slice(0,
|
|
5351
|
+
${codebaseAnalysis.components.slice(0, 15).map((c10) => `- ${c10}`).join("\n")}
|
|
4739
5352
|
|
|
4740
5353
|
**Recent Git Commits:**
|
|
4741
|
-
${codebaseAnalysis.recentCommits.slice(0,
|
|
5354
|
+
${codebaseAnalysis.recentCommits.slice(0, 8).map((c10) => `- ${c10}`).join("\n")}
|
|
4742
5355
|
|
|
4743
5356
|
**Completed Tasks:**
|
|
4744
|
-
${context.completedTasks.slice(0,
|
|
5357
|
+
${context.completedTasks.slice(0, 10).map((t) => `- ${t.title}`).join("\n") || "None"}
|
|
4745
5358
|
|
|
4746
5359
|
**Open Tasks:**
|
|
4747
|
-
${context.openTasks.slice(0,
|
|
5360
|
+
${context.openTasks.slice(0, 5).map((t) => `- ${t.title}`).join("\n") || "None"}
|
|
4748
5361
|
|
|
4749
5362
|
${config.focusAreas.length > 0 ? `**Focus Areas (user-specified):**
|
|
4750
5363
|
${config.focusAreas.map((a) => `- ${a}`).join("\n")}` : ""}
|
|
@@ -4755,11 +5368,9 @@ ${crawlResults.length > 0 ? crawlSummary : "Live crawl was not performed (app ma
|
|
|
4755
5368
|
|
|
4756
5369
|
## Prior Findings Context
|
|
4757
5370
|
|
|
4758
|
-
${dismissedFindings.length > 0 ? `**Previously Dismissed (do NOT re-suggest
|
|
4759
|
-
${dismissedFindings.map((f) => `- [${f.type}] ${f.title}`).join("\n")}` : "No dismissed findings."}
|
|
5371
|
+
${dismissedFindings.length > 0 ? `**Previously Dismissed (do NOT re-suggest):** ${dismissedFindings.map((f) => f.title).join("; ")}` : "No dismissed findings."}
|
|
4760
5372
|
|
|
4761
|
-
${promotedFindings.length > 0 ? `**Already Being Worked On (
|
|
4762
|
-
${promotedFindings.map((f) => `- [${f.type}] ${f.title}`).join("\n")}` : "No promoted findings."}
|
|
5373
|
+
${promotedFindings.length > 0 ? `**Already Being Worked On:** ${promotedFindings.map((f) => f.title).join("; ")}` : "No promoted findings."}
|
|
4763
5374
|
|
|
4764
5375
|
## Instructions
|
|
4765
5376
|
|
|
@@ -4947,7 +5558,7 @@ async function fetchScanContext(opts) {
|
|
|
4947
5558
|
};
|
|
4948
5559
|
}
|
|
4949
5560
|
function runClaude(prompt2) {
|
|
4950
|
-
return new Promise((
|
|
5561
|
+
return new Promise((resolve8, reject) => {
|
|
4951
5562
|
const child = spawn7("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
4952
5563
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4953
5564
|
});
|
|
@@ -4960,7 +5571,7 @@ function runClaude(prompt2) {
|
|
|
4960
5571
|
errOutput += d.toString();
|
|
4961
5572
|
});
|
|
4962
5573
|
child.on("exit", (code) => {
|
|
4963
|
-
if (code === 0)
|
|
5574
|
+
if (code === 0) resolve8(output.trim());
|
|
4964
5575
|
else reject(new Error(`claude exited with code ${code}
|
|
4965
5576
|
${errOutput.trim()}`));
|
|
4966
5577
|
});
|
|
@@ -5398,9 +6009,229 @@ var doctorCommand = new Command25("doctor").description("Diagnose Mr. Manager CL
|
|
|
5398
6009
|
process.exit(1);
|
|
5399
6010
|
});
|
|
5400
6011
|
|
|
6012
|
+
// cli/commands/prompt-audit.ts
|
|
6013
|
+
import { Command as Command26 } from "commander";
|
|
6014
|
+
import { resolve as resolve7 } from "path";
|
|
6015
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
|
|
6016
|
+
function auditLine(label, tokens) {
|
|
6017
|
+
const bar = "\u2588".repeat(Math.min(60, Math.round(tokens / 200)));
|
|
6018
|
+
return ` ${label.padEnd(30)} ${formatTokenCount(tokens).padStart(8)} ${bar}`;
|
|
6019
|
+
}
|
|
6020
|
+
var promptAuditCommand = new Command26("prompt-audit").description("Dry-run prompt construction and report estimated token counts by job type").option("--task <id>", "Audit prompts for a specific task ID").option("--all", "Audit all supported job types with representative data", false).option("--json", "Output as JSON instead of plain text", false).action(async (opts) => {
|
|
6021
|
+
const results = [];
|
|
6022
|
+
if (opts.task) {
|
|
6023
|
+
try {
|
|
6024
|
+
const task = await api.get(`/api/tasks/${opts.task}`);
|
|
6025
|
+
const [_subtasks, protoRefs, updates, resources, skills] = await Promise.all([
|
|
6026
|
+
api.get(`/api/tasks/${task.id}/subtasks`).catch(() => []),
|
|
6027
|
+
api.get(`/api/tasks/${task.id}/prototypes`).catch(() => []),
|
|
6028
|
+
api.get(`/api/tasks/${task.id}/updates`).catch(() => []),
|
|
6029
|
+
api.get(`/api/tasks/${task.id}/resources`).catch(() => []),
|
|
6030
|
+
api.get(`/api/tasks/${task.id}/skills`).catch(() => [])
|
|
6031
|
+
]);
|
|
6032
|
+
const feedbackUpdates = updates.filter((u) => u.source === "user");
|
|
6033
|
+
const sections = [];
|
|
6034
|
+
const notesContent = task.prdContent ? `
|
|
6035
|
+
|
|
6036
|
+
## PRD (Product Requirements Document)
|
|
6037
|
+
|
|
6038
|
+
${task.prdContent}` : task.notes ? `
|
|
6039
|
+
|
|
6040
|
+
Task notes:
|
|
6041
|
+
${task.notes}` : "";
|
|
6042
|
+
sections.push({ name: "task-notes/prd", tokens: estimateTokens(notesContent) });
|
|
6043
|
+
let protoContent = "";
|
|
6044
|
+
for (const ref of protoRefs) {
|
|
6045
|
+
const files = ref.prototype?.files ?? [];
|
|
6046
|
+
for (const f of files) {
|
|
6047
|
+
protoContent += f.content;
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
6050
|
+
sections.push({ name: "prototypes", tokens: estimateTokens(protoContent) });
|
|
6051
|
+
let skillContent = "";
|
|
6052
|
+
for (const s of skills) {
|
|
6053
|
+
skillContent += s.skill.content;
|
|
6054
|
+
}
|
|
6055
|
+
sections.push({ name: "skills", tokens: estimateTokens(skillContent) });
|
|
6056
|
+
let resourceContent = "";
|
|
6057
|
+
for (const r of resources) {
|
|
6058
|
+
resourceContent += r.content.slice(0, 8e3);
|
|
6059
|
+
}
|
|
6060
|
+
sections.push({ name: "resources", tokens: estimateTokens(resourceContent) });
|
|
6061
|
+
let feedbackContent = "";
|
|
6062
|
+
for (const f of feedbackUpdates) {
|
|
6063
|
+
feedbackContent += f.message;
|
|
6064
|
+
}
|
|
6065
|
+
sections.push({ name: "feedback", tokens: estimateTokens(feedbackContent) });
|
|
6066
|
+
const config = loadConfig();
|
|
6067
|
+
const repoDir = Object.entries(config.directories).find(([, pid]) => pid === task.projectId)?.[0];
|
|
6068
|
+
if (repoDir) {
|
|
6069
|
+
const featuresPath = resolve7(repoDir, ".mr-features.md");
|
|
6070
|
+
if (existsSync16(featuresPath)) {
|
|
6071
|
+
const featuresContent = readFileSync12(featuresPath, "utf-8");
|
|
6072
|
+
sections.push({ name: "features-doc", tokens: estimateTokens(featuresContent) });
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
sections.push({ name: "static-instructions (system)", tokens: estimateTokens(
|
|
6076
|
+
"status-updates + screenshots + test-plan + no-mr + features-workflow"
|
|
6077
|
+
) });
|
|
6078
|
+
const totalTokens = sections.reduce((sum, s) => sum + s.tokens, 0);
|
|
6079
|
+
const promptTokens = sections.filter((s) => !s.name.includes("system")).reduce((sum, s) => sum + s.tokens, 0);
|
|
6080
|
+
const systemTokens = totalTokens - promptTokens;
|
|
6081
|
+
results.push({
|
|
6082
|
+
jobType: "execution",
|
|
6083
|
+
identifier: task.id.slice(0, 8),
|
|
6084
|
+
promptTokens,
|
|
6085
|
+
systemTokens,
|
|
6086
|
+
totalTokens,
|
|
6087
|
+
sections
|
|
6088
|
+
});
|
|
6089
|
+
if (task.prdContent && feedbackUpdates.length > 0) {
|
|
6090
|
+
const prdSections = [
|
|
6091
|
+
{ name: "task-notes", tokens: estimateTokens(task.notes ?? "") },
|
|
6092
|
+
{ name: "existing-prd", tokens: estimateTokens(task.prdContent) },
|
|
6093
|
+
{ name: "feedback", tokens: estimateTokens(feedbackContent) },
|
|
6094
|
+
{ name: "prd-format (system)", tokens: estimateTokens("prd-format + open-questions") }
|
|
6095
|
+
];
|
|
6096
|
+
const prdTotal = prdSections.reduce((sum, s) => sum + s.tokens, 0);
|
|
6097
|
+
results.push({
|
|
6098
|
+
jobType: "prd-revision",
|
|
6099
|
+
identifier: task.id.slice(0, 8),
|
|
6100
|
+
promptTokens: prdTotal,
|
|
6101
|
+
systemTokens: 0,
|
|
6102
|
+
totalTokens: prdTotal,
|
|
6103
|
+
sections: prdSections
|
|
6104
|
+
});
|
|
6105
|
+
}
|
|
6106
|
+
} catch (err) {
|
|
6107
|
+
console.error(`Failed to fetch task: ${err.message}`);
|
|
6108
|
+
process.exit(1);
|
|
6109
|
+
}
|
|
6110
|
+
} else if (opts.all) {
|
|
6111
|
+
const syntheticSections = [
|
|
6112
|
+
{
|
|
6113
|
+
jobType: "execution",
|
|
6114
|
+
description: "Task execution prompt (baseline without attachments)",
|
|
6115
|
+
sections: [
|
|
6116
|
+
{ name: "core-prompt", tokens: 800 },
|
|
6117
|
+
{ name: "instructions", tokens: 600 },
|
|
6118
|
+
{ name: "pr-template", tokens: 400 },
|
|
6119
|
+
{ name: "features-ref", tokens: 50 }
|
|
6120
|
+
]
|
|
6121
|
+
},
|
|
6122
|
+
{
|
|
6123
|
+
jobType: "execution+prd",
|
|
6124
|
+
description: "Task execution with full PRD",
|
|
6125
|
+
sections: [
|
|
6126
|
+
{ name: "core-prompt", tokens: 800 },
|
|
6127
|
+
{ name: "prd-content", tokens: 4e3 },
|
|
6128
|
+
{ name: "instructions", tokens: 600 }
|
|
6129
|
+
]
|
|
6130
|
+
},
|
|
6131
|
+
{
|
|
6132
|
+
jobType: "prd-generation",
|
|
6133
|
+
description: "PRD generation prompt",
|
|
6134
|
+
sections: [
|
|
6135
|
+
{ name: "core-prompt", tokens: 300 },
|
|
6136
|
+
{ name: "task-notes", tokens: 500 },
|
|
6137
|
+
{ name: "format-ref", tokens: 50 }
|
|
6138
|
+
]
|
|
6139
|
+
},
|
|
6140
|
+
{
|
|
6141
|
+
jobType: "prd-revision",
|
|
6142
|
+
description: "PRD revision (delta mode)",
|
|
6143
|
+
sections: [
|
|
6144
|
+
{ name: "core-prompt", tokens: 300 },
|
|
6145
|
+
{ name: "prd-summary", tokens: 200 },
|
|
6146
|
+
{ name: "feedback", tokens: 300 },
|
|
6147
|
+
{ name: "prd-file-ref", tokens: 30 }
|
|
6148
|
+
]
|
|
6149
|
+
},
|
|
6150
|
+
{
|
|
6151
|
+
jobType: "prototype",
|
|
6152
|
+
description: "Prototype generation prompt",
|
|
6153
|
+
sections: [
|
|
6154
|
+
{ name: "core-prompt", tokens: 600 },
|
|
6155
|
+
{ name: "variant-steps", tokens: 200 }
|
|
6156
|
+
]
|
|
6157
|
+
},
|
|
6158
|
+
{
|
|
6159
|
+
jobType: "refinement",
|
|
6160
|
+
description: "Prototype refinement (file-ref mode)",
|
|
6161
|
+
sections: [
|
|
6162
|
+
{ name: "core-prompt", tokens: 600 },
|
|
6163
|
+
{ name: "parent-file-refs", tokens: 50 },
|
|
6164
|
+
{ name: "feedback", tokens: 200 }
|
|
6165
|
+
]
|
|
6166
|
+
},
|
|
6167
|
+
{
|
|
6168
|
+
jobType: "scanner-synthesis",
|
|
6169
|
+
description: "Scanner synthesis prompt (tightened caps)",
|
|
6170
|
+
sections: [
|
|
6171
|
+
{ name: "context", tokens: 400 },
|
|
6172
|
+
{ name: "routes+models", tokens: 300 },
|
|
6173
|
+
{ name: "components", tokens: 150 },
|
|
6174
|
+
{ name: "commits", tokens: 160 },
|
|
6175
|
+
{ name: "crawl-results", tokens: 500 },
|
|
6176
|
+
{ name: "prior-findings", tokens: 100 },
|
|
6177
|
+
{ name: "instructions", tokens: 500 }
|
|
6178
|
+
]
|
|
6179
|
+
},
|
|
6180
|
+
{
|
|
6181
|
+
jobType: "idea",
|
|
6182
|
+
description: "Idea generation prompt",
|
|
6183
|
+
sections: [
|
|
6184
|
+
{ name: "core-prompt", tokens: 500 },
|
|
6185
|
+
{ name: "idea-desc", tokens: 200 }
|
|
6186
|
+
]
|
|
6187
|
+
},
|
|
6188
|
+
{
|
|
6189
|
+
jobType: "repo-creation",
|
|
6190
|
+
description: "Repository creation prompt",
|
|
6191
|
+
sections: [
|
|
6192
|
+
{ name: "core-prompt", tokens: 400 },
|
|
6193
|
+
{ name: "project-desc", tokens: 200 }
|
|
6194
|
+
]
|
|
6195
|
+
}
|
|
6196
|
+
];
|
|
6197
|
+
console.log("Estimated prompt sizes by job type (representative baselines)");
|
|
6198
|
+
console.log("\u2550".repeat(70));
|
|
6199
|
+
for (const entry of syntheticSections) {
|
|
6200
|
+
const total = entry.sections.reduce((sum, s) => sum + s.tokens, 0);
|
|
6201
|
+
console.log(`
|
|
6202
|
+
${entry.jobType} \u2014 ${entry.description}`);
|
|
6203
|
+
console.log(` Total: ${formatTokenCount(total)} tokens`);
|
|
6204
|
+
for (const s of entry.sections) {
|
|
6205
|
+
console.log(auditLine(s.name, s.tokens));
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
console.log("");
|
|
6209
|
+
return;
|
|
6210
|
+
} else {
|
|
6211
|
+
console.log("Usage: mr prompt-audit --task <id> or mr prompt-audit --all");
|
|
6212
|
+
console.log("\nDry-run prompt construction and report estimated token counts.");
|
|
6213
|
+
return;
|
|
6214
|
+
}
|
|
6215
|
+
if (opts.json) {
|
|
6216
|
+
console.log(JSON.stringify(results, null, 2));
|
|
6217
|
+
} else {
|
|
6218
|
+
console.log("Prompt Audit Report");
|
|
6219
|
+
console.log("\u2550".repeat(70));
|
|
6220
|
+
for (const r of results) {
|
|
6221
|
+
console.log(`
|
|
6222
|
+
${r.jobType} [${r.identifier}]`);
|
|
6223
|
+
console.log(` Total: ${formatTokenCount(r.totalTokens)} tokens (prompt=${formatTokenCount(r.promptTokens)} system=${formatTokenCount(r.systemTokens)})`);
|
|
6224
|
+
for (const s of r.sections) {
|
|
6225
|
+
console.log(auditLine(s.name, s.tokens));
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6228
|
+
console.log("");
|
|
6229
|
+
}
|
|
6230
|
+
});
|
|
6231
|
+
|
|
5401
6232
|
// cli/index.ts
|
|
5402
6233
|
var configPath = join12(homedir3(), ".mr-manager", "config.json");
|
|
5403
|
-
var isFirstRun = !
|
|
6234
|
+
var isFirstRun = !existsSync17(configPath);
|
|
5404
6235
|
var userArgs = process.argv.slice(2);
|
|
5405
6236
|
var bypassCommands = /* @__PURE__ */ new Set(["login", "init", "auth", "help", "--help", "-h", "--version", "-V", "doctor", "setup"]);
|
|
5406
6237
|
var shouldBypass = userArgs.length > 0 && bypassCommands.has(userArgs[0]);
|
|
@@ -5436,7 +6267,7 @@ if (isFirstRun && !shouldBypass) {
|
|
|
5436
6267
|
console.log("");
|
|
5437
6268
|
process.exit(0);
|
|
5438
6269
|
}
|
|
5439
|
-
var program = new
|
|
6270
|
+
var program = new Command27();
|
|
5440
6271
|
program.name("mr").description("Mr. Manager - Task and project management CLI").version(CLI_VERSION);
|
|
5441
6272
|
program.addCommand(initCommand);
|
|
5442
6273
|
program.addCommand(authCommand);
|
|
@@ -5466,4 +6297,5 @@ program.addCommand(noMrCommand);
|
|
|
5466
6297
|
program.addCommand(scanCommand);
|
|
5467
6298
|
program.addCommand(ideaCommand);
|
|
5468
6299
|
program.addCommand(doctorCommand);
|
|
6300
|
+
program.addCommand(promptAuditCommand);
|
|
5469
6301
|
program.parse();
|