@dunnewold-labs/mr-manager 0.4.3 → 0.4.6
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 +1559 -725
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// cli/index.ts
|
|
4
4
|
import { Command as Command27 } from "commander";
|
|
5
|
-
import { existsSync as
|
|
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.6",
|
|
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",
|
|
@@ -1026,6 +1278,12 @@ function slugify(title) {
|
|
|
1026
1278
|
function shortId(id) {
|
|
1027
1279
|
return id.slice(0, 8);
|
|
1028
1280
|
}
|
|
1281
|
+
function ownerPrefix(task) {
|
|
1282
|
+
const name = task.user?.name;
|
|
1283
|
+
if (!name) return "mr";
|
|
1284
|
+
const first = name.split(/\s+/)[0].toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1285
|
+
return first || "mr";
|
|
1286
|
+
}
|
|
1029
1287
|
function formatElapsed(ms) {
|
|
1030
1288
|
const totalMinutes = Math.max(1, Math.floor(ms / 6e4));
|
|
1031
1289
|
const hours = Math.floor(totalMinutes / 60);
|
|
@@ -1035,13 +1293,13 @@ function formatElapsed(ms) {
|
|
|
1035
1293
|
}
|
|
1036
1294
|
return `${totalMinutes}m`;
|
|
1037
1295
|
}
|
|
1038
|
-
function worktreePath(
|
|
1039
|
-
return `.mr-worktrees
|
|
1296
|
+
function worktreePath(name) {
|
|
1297
|
+
return `.mr-worktrees/${name}`;
|
|
1040
1298
|
}
|
|
1041
1299
|
function worktreeNameFromPath(wtPath) {
|
|
1042
1300
|
return wtPath.replace(/^\.mr-worktrees\//, "");
|
|
1043
1301
|
}
|
|
1044
|
-
function
|
|
1302
|
+
function normalizeWhitespace2(value) {
|
|
1045
1303
|
return value.replace(/\s+/g, " ").trim();
|
|
1046
1304
|
}
|
|
1047
1305
|
function extractFirstMeaningfulParagraph(markdown) {
|
|
@@ -1066,7 +1324,7 @@ function extractFirstMeaningfulParagraph(markdown) {
|
|
|
1066
1324
|
if (quoteLines.length > 0) break;
|
|
1067
1325
|
}
|
|
1068
1326
|
if (quoteLines.length > 0) {
|
|
1069
|
-
const text2 =
|
|
1327
|
+
const text2 = normalizeWhitespace2(quoteLines.join(" "));
|
|
1070
1328
|
return text2 || null;
|
|
1071
1329
|
}
|
|
1072
1330
|
const paragraphLines = [];
|
|
@@ -1089,7 +1347,7 @@ function extractFirstMeaningfulParagraph(markdown) {
|
|
|
1089
1347
|
paragraphLines.push(line);
|
|
1090
1348
|
}
|
|
1091
1349
|
if (paragraphLines.length === 0) return null;
|
|
1092
|
-
const text =
|
|
1350
|
+
const text = normalizeWhitespace2(paragraphLines.join(" "));
|
|
1093
1351
|
return text || null;
|
|
1094
1352
|
}
|
|
1095
1353
|
function truncateText(value, maxLength) {
|
|
@@ -1127,25 +1385,6 @@ function buildPrBodyTemplate(task, subtasks, protoRefs = [], feedbackUpdates = [
|
|
|
1127
1385
|
"_Replace every placeholder above before creating the PR. Remove bullets or sections that do not apply._"
|
|
1128
1386
|
].join("\n");
|
|
1129
1387
|
}
|
|
1130
|
-
function pullLatestMain(repoDir, prefix) {
|
|
1131
|
-
return new Promise((resolve7) => {
|
|
1132
|
-
exec(
|
|
1133
|
-
"git pull origin main --ff-only",
|
|
1134
|
-
{ cwd: repoDir },
|
|
1135
|
-
(err, stdout, stderr) => {
|
|
1136
|
-
if (err) {
|
|
1137
|
-
logWarn(prefix, `git pull failed (proceeding anyway): ${stderr.trim() || err.message}`);
|
|
1138
|
-
} else {
|
|
1139
|
-
const msg = stdout.trim();
|
|
1140
|
-
if (msg && msg !== "Already up to date.") {
|
|
1141
|
-
logInfo(prefix, `pulled latest main: ${paint("gray", msg.split("\n")[0])}`);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
resolve7();
|
|
1145
|
-
}
|
|
1146
|
-
);
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
1388
|
function buildPlanningPrompt(task, repoDir) {
|
|
1150
1389
|
const notes = task.notes ? `
|
|
1151
1390
|
|
|
@@ -1171,17 +1410,109 @@ ${task.notes}` : "";
|
|
|
1171
1410
|
}
|
|
1172
1411
|
function findPrUrl(branchName, repoDir, vcs = "github") {
|
|
1173
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`;
|
|
1174
|
-
return new Promise((
|
|
1413
|
+
return new Promise((resolve8) => {
|
|
1175
1414
|
exec(
|
|
1176
1415
|
cmd,
|
|
1177
1416
|
{ cwd: repoDir },
|
|
1178
1417
|
(err, stdout) => {
|
|
1179
|
-
if (err)
|
|
1180
|
-
else
|
|
1418
|
+
if (err) resolve8(null);
|
|
1419
|
+
else resolve8(stdout.trim() || null);
|
|
1181
1420
|
}
|
|
1182
1421
|
);
|
|
1183
1422
|
});
|
|
1184
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
|
+
}
|
|
1185
1516
|
function isGitRepo(dir) {
|
|
1186
1517
|
try {
|
|
1187
1518
|
return existsSync7(resolve2(dir, ".git"));
|
|
@@ -1227,13 +1558,41 @@ async function extractPrUrlFromUpdates(taskId) {
|
|
|
1227
1558
|
}
|
|
1228
1559
|
return null;
|
|
1229
1560
|
}
|
|
1230
|
-
function
|
|
1561
|
+
function checkPrStatus(prUrl, repoDir, vcs = "github") {
|
|
1562
|
+
const cmd = vcs === "gitlab" ? `glab mr view "${prUrl}" --output json 2>/dev/null` : `gh pr view "${prUrl}" --json merged,mergeable 2>/dev/null`;
|
|
1563
|
+
return new Promise((resolve8) => {
|
|
1564
|
+
exec(cmd, { cwd: repoDir }, (err, stdout) => {
|
|
1565
|
+
if (err || !stdout.trim()) {
|
|
1566
|
+
resolve8(null);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
try {
|
|
1570
|
+
const data = JSON.parse(stdout.trim());
|
|
1571
|
+
if (vcs === "gitlab") {
|
|
1572
|
+
resolve8({
|
|
1573
|
+
merged: data.state === "merged",
|
|
1574
|
+
hasConflicts: data.has_conflicts === true
|
|
1575
|
+
});
|
|
1576
|
+
} else {
|
|
1577
|
+
resolve8({
|
|
1578
|
+
merged: data.merged === true,
|
|
1579
|
+
hasConflicts: data.mergeable === "CONFLICTING"
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
} catch {
|
|
1583
|
+
resolve8(null);
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
function buildPrototypeSection(protoRefs, workingDir) {
|
|
1231
1589
|
if (protoRefs.length === 0) return "";
|
|
1232
1590
|
const sections = [
|
|
1233
1591
|
``,
|
|
1234
1592
|
`## Referenced Prototypes`,
|
|
1235
1593
|
``,
|
|
1236
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.`,
|
|
1237
1596
|
``
|
|
1238
1597
|
];
|
|
1239
1598
|
for (const ref of protoRefs) {
|
|
@@ -1251,10 +1610,19 @@ function buildPrototypeSection(protoRefs) {
|
|
|
1251
1610
|
for (let i = 0; i < selectedFiles.length; i++) {
|
|
1252
1611
|
const file = selectedFiles[i];
|
|
1253
1612
|
const variantLabel = selected.length < files.length ? `Variant ${selected[i] + 1} (selected)` : `Variant ${i + 1}`;
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
+
}
|
|
1258
1626
|
sections.push(``);
|
|
1259
1627
|
}
|
|
1260
1628
|
}
|
|
@@ -1320,8 +1688,9 @@ function buildFeedbackSection(updates) {
|
|
|
1320
1688
|
return lines.join("\n");
|
|
1321
1689
|
}
|
|
1322
1690
|
function buildFeaturesSection(repoDir) {
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1691
|
+
const featuresPath = resolve2(repoDir, FEATURES_FILE2);
|
|
1692
|
+
const exists = existsSync7(featuresPath);
|
|
1693
|
+
if (!exists) {
|
|
1325
1694
|
return [
|
|
1326
1695
|
``,
|
|
1327
1696
|
`## Features & Goals Document`,
|
|
@@ -1341,11 +1710,8 @@ function buildFeaturesSection(repoDir) {
|
|
|
1341
1710
|
``,
|
|
1342
1711
|
`## Features & Goals Document`,
|
|
1343
1712
|
``,
|
|
1344
|
-
`The project maintains a features & goals document at
|
|
1345
|
-
|
|
1346
|
-
"```markdown",
|
|
1347
|
-
content,
|
|
1348
|
-
"```",
|
|
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.`,
|
|
1349
1715
|
``,
|
|
1350
1716
|
`After completing your work, update this document to reflect any changes. Write the updated content to a temp file and run:`,
|
|
1351
1717
|
`\`mr features --file /tmp/mr-features-update.md\``,
|
|
@@ -1354,11 +1720,11 @@ function buildFeaturesSection(repoDir) {
|
|
|
1354
1720
|
``
|
|
1355
1721
|
].join("\n");
|
|
1356
1722
|
}
|
|
1357
|
-
function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = [], executionDir) {
|
|
1358
|
-
const sid = shortId(task.id);
|
|
1723
|
+
function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = [], executionDir, startWithoutWorktree = false) {
|
|
1359
1724
|
const slug = slugify(task.title);
|
|
1360
|
-
const
|
|
1361
|
-
const
|
|
1725
|
+
const owner = ownerPrefix(task);
|
|
1726
|
+
const branchName = `${owner}/${slug}`;
|
|
1727
|
+
const wtPath = worktreePath(`${owner}-${slug}`);
|
|
1362
1728
|
const workingDir = executionDir ?? repoDir;
|
|
1363
1729
|
const prBodyPath = "/tmp/mr-pr-body.md";
|
|
1364
1730
|
const notes = task.prdContent ? `
|
|
@@ -1381,7 +1747,6 @@ ${task.notes}` : "";
|
|
|
1381
1747
|
``
|
|
1382
1748
|
].join("\n") : "";
|
|
1383
1749
|
const hasFeedback = feedbackUpdates.length > 0;
|
|
1384
|
-
const feedbackWtPath = hasFeedback ? worktreePath(`${sid}-fb`) : wtPath;
|
|
1385
1750
|
const prBodyTemplate = buildPrBodyTemplate(task, pendingSubtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
|
|
1386
1751
|
const prCreateCmd = vcs === "gitlab" ? `glab mr create --title "${task.title}" --description-file ${prBodyPath} --yes` : `gh pr create --title "${task.title}" --body-file ${prBodyPath}`;
|
|
1387
1752
|
return [
|
|
@@ -1393,7 +1758,7 @@ ${task.notes}` : "";
|
|
|
1393
1758
|
`ID: ${task.id}${notes}`,
|
|
1394
1759
|
subtaskSection,
|
|
1395
1760
|
buildResourcesSection(existingResources),
|
|
1396
|
-
buildPrototypeSection(protoRefs),
|
|
1761
|
+
buildPrototypeSection(protoRefs, workingDir),
|
|
1397
1762
|
buildSkillsSection(skillRefs),
|
|
1398
1763
|
buildFeedbackSection(feedbackUpdates),
|
|
1399
1764
|
`## Instructions`,
|
|
@@ -1408,9 +1773,16 @@ ${task.notes}` : "";
|
|
|
1408
1773
|
] : hasFeedback ? [
|
|
1409
1774
|
`2. Set up your working environment:`,
|
|
1410
1775
|
` - Fetch the latest from origin: \`git fetch origin\``,
|
|
1411
|
-
` -
|
|
1412
|
-
` -
|
|
1413
|
-
` -
|
|
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.`
|
|
1414
1786
|
] : [
|
|
1415
1787
|
`2. Set up an isolated working environment:`,
|
|
1416
1788
|
` - If ${repoDir} is a git repo, create a worktree: \`git worktree add -b ${branchName} ${wtPath}\``,
|
|
@@ -1446,21 +1818,6 @@ ${task.notes}` : "";
|
|
|
1446
1818
|
``,
|
|
1447
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.`,
|
|
1448
1820
|
``,
|
|
1449
|
-
`## Status Updates`,
|
|
1450
|
-
``,
|
|
1451
|
-
`As you work, post brief status updates so progress is visible in the UI:`,
|
|
1452
|
-
`\`mr update ${task.id} "your status message here"\``,
|
|
1453
|
-
``,
|
|
1454
|
-
`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:`,
|
|
1455
|
-
`- What you found while exploring (e.g. "Found the auth logic in src/auth.ts, needs refactoring")`,
|
|
1456
|
-
`- What specific changes you're making (e.g. "Adding validation to the signup form component")`,
|
|
1457
|
-
`- Problems you hit and how you solved them (e.g. "Fixed circular dependency between User and Order models")`,
|
|
1458
|
-
`- What files/components you modified (e.g. "Updated api/routes.ts and middleware/auth.ts")`,
|
|
1459
|
-
``,
|
|
1460
|
-
`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.`,
|
|
1461
|
-
``,
|
|
1462
|
-
`Keep messages short (1 sentence). Post 3-5 updates total.`,
|
|
1463
|
-
``,
|
|
1464
1821
|
...hasFeedback ? [] : [
|
|
1465
1822
|
`## PR Description Template`,
|
|
1466
1823
|
``,
|
|
@@ -1472,41 +1829,6 @@ ${task.notes}` : "";
|
|
|
1472
1829
|
"```",
|
|
1473
1830
|
``
|
|
1474
1831
|
],
|
|
1475
|
-
`## Screenshots`,
|
|
1476
|
-
``,
|
|
1477
|
-
`Before you finish, take a screenshot of your work to attach to the task. This helps the reviewer see what changed visually.`,
|
|
1478
|
-
`\`mr screenshot ${task.id} <path-to-image> -m "Description of what the screenshot shows"\``,
|
|
1479
|
-
``,
|
|
1480
|
-
`You can take a screenshot on macOS (no file arg needed) or provide a path to an existing image file.`,
|
|
1481
|
-
`When no file is provided, the command uses the browse daemon (headless Chromium) to screenshot the app's project page automatically.`,
|
|
1482
|
-
`You can also specify a custom URL: \`mr screenshot ${task.id} --url "http://localhost:3000/some-page" -m "description"\``,
|
|
1483
|
-
`If your changes are visual (UI changes), try to capture the result. If they are purely backend/logic changes, you can skip this step.`,
|
|
1484
|
-
``,
|
|
1485
|
-
`## Test Plan`,
|
|
1486
|
-
``,
|
|
1487
|
-
`After pushing your MR/PR, create a structured test plan so the reviewer can run automated browser tests against your changes.`,
|
|
1488
|
-
`Save the test plan as a TaskResource by calling:`,
|
|
1489
|
-
`\`mr update ${task.id} --resource test-plan "Test plan for ${task.title}" '<json>'\``,
|
|
1490
|
-
``,
|
|
1491
|
-
`Or write it to a file and upload it. The test plan is a JSON array of browse commands:`,
|
|
1492
|
-
``,
|
|
1493
|
-
"```json",
|
|
1494
|
-
`[`,
|
|
1495
|
-
` { "command": "goto", "args": ["/login"], "description": "Navigate to login page" },`,
|
|
1496
|
-
` { "command": "fill", "args": ["input[type=email]", "test@example.com"], "description": "Enter email" },`,
|
|
1497
|
-
` { "command": "fill", "args": ["input[type=password]", "test-password"], "description": "Enter password" },`,
|
|
1498
|
-
` { "command": "click", "args": ["button[type=submit]"], "description": "Submit login" },`,
|
|
1499
|
-
` { "command": "wait", "args": ["2000"], "description": "Wait for redirect" },`,
|
|
1500
|
-
` { "command": "goto", "args": ["/dashboard"], "description": "Navigate to changed page" },`,
|
|
1501
|
-
` { "command": "screenshot", "description": "Capture dashboard" },`,
|
|
1502
|
-
` { "command": "assertVisible", "args": [".dashboard-widget"], "description": "Verify widget renders" },`,
|
|
1503
|
-
` { "command": "assertText", "args": ["h1", "Dashboard"], "description": "Verify page title" }`,
|
|
1504
|
-
`]`,
|
|
1505
|
-
"```",
|
|
1506
|
-
``,
|
|
1507
|
-
`Supported commands: goto, click, fill, wait, screenshot, assertVisible, assertText, assertUrl.`,
|
|
1508
|
-
`Include auth steps (login/signup URLs) if the feature requires authentication.`,
|
|
1509
|
-
`If your changes don't have a testable UI flow, you can skip this step.`,
|
|
1510
1832
|
buildFeaturesSection(repoDir),
|
|
1511
1833
|
`Complete all steps autonomously without asking for confirmation. Exit with code 0 when done.`
|
|
1512
1834
|
].join("\n");
|
|
@@ -1517,16 +1839,31 @@ function buildPrdPrompt(task, repoDir, existingPrd, feedbackUpdates = []) {
|
|
|
1517
1839
|
Task notes:
|
|
1518
1840
|
${task.notes}` : "";
|
|
1519
1841
|
const isRevision = !!(existingPrd && feedbackUpdates.length > 0);
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
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
|
+
}
|
|
1530
1867
|
const revisionInstructions = isRevision ? [
|
|
1531
1868
|
`1. Read the existing PRD and user feedback above carefully.`,
|
|
1532
1869
|
`2. Revise the PRD to address every piece of feedback. Keep sections that are still valid \u2014 only modify what the feedback requires.`,
|
|
@@ -1557,77 +1894,16 @@ ${task.notes}` : "";
|
|
|
1557
1894
|
` - Technical approach and affected components`,
|
|
1558
1895
|
` - Edge cases`,
|
|
1559
1896
|
` - Implementation steps / breakdown`,
|
|
1560
|
-
` - **Open Questions** \u2014 this section MUST use the structured format
|
|
1561
|
-
``,
|
|
1562
|
-
`### PRD Format Conventions`,
|
|
1563
|
-
``,
|
|
1564
|
-
`Follow these conventions so PRDs are scannable by humans and parseable by agents.`,
|
|
1565
|
-
`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.`,
|
|
1566
|
-
``,
|
|
1567
|
-
`**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.`,
|
|
1568
|
-
``,
|
|
1569
|
-
`**Requirement IDs** \u2014 Every requirement gets a unique ID prefix and a single-sentence summary on the first line:`,
|
|
1570
|
-
`\`**F1: Name** \u2014 One-sentence summary.\` followed by optional \`- Detail bullet\` lines.`,
|
|
1571
|
-
`Use \`F\` prefix for functional requirements, \`NF\` for non-functional. Number sequentially within each category.`,
|
|
1572
|
-
`The summary line must be a complete description \u2014 an agent should be able to extract just summary lines to build a work plan.`,
|
|
1573
|
-
...isRevision ? [`When revising, preserve existing requirement IDs. Add new requirements by continuing the numbering sequence \u2014 never restart at F1.`] : [],
|
|
1574
|
-
``,
|
|
1575
|
-
`**Success Metrics as JSON** \u2014 Use a fenced \`json\` code block containing an array of objects. The UI auto-renders these as tables.`,
|
|
1576
|
-
`Each object should have \`Metric\`, \`Target\`, and \`Type\` fields. Example:`,
|
|
1577
|
-
"````json",
|
|
1578
|
-
`[`,
|
|
1579
|
-
` { "Metric": "API response time under load", "Target": "< 200ms p95", "Type": "Performance" }`,
|
|
1580
|
-
`]`,
|
|
1581
|
-
"````",
|
|
1582
|
-
``,
|
|
1583
|
-
`**Affected Components as JSON** \u2014 Use the same JSON table format for the technical approach section:`,
|
|
1584
|
-
"````json",
|
|
1585
|
-
`[`,
|
|
1586
|
-
` { "Component": "path/to/file.ts", "Change": "What changes", "Scope": "~N lines" }`,
|
|
1587
|
-
`]`,
|
|
1588
|
-
"````",
|
|
1589
|
-
``,
|
|
1590
|
-
`**Implementation Steps as Phased Checklists** \u2014 Use \`### Phase N: Name\` headers with markdown checklists (\`- [ ]\`).`,
|
|
1591
|
-
`Gives humans a timeline overview at heading level; gives agents discrete work items.`,
|
|
1897
|
+
` - **Open Questions** \u2014 this section MUST use the structured format described in the system prompt`,
|
|
1592
1898
|
``,
|
|
1593
|
-
`**Edge Cases as Tagged List** \u2014 Each edge case leads with a bold tag: \`- **Tag** \u2014 Description.\``,
|
|
1594
|
-
``,
|
|
1595
|
-
`**No Frontmatter** \u2014 Do not include YAML frontmatter. Metadata lives in the database.`,
|
|
1596
|
-
``,
|
|
1597
|
-
`### Open Questions Format`,
|
|
1598
|
-
``,
|
|
1599
|
-
`The "Open Questions" section of the PRD MUST end with a fenced JSON code block`,
|
|
1600
|
-
`tagged \`open-questions\`. This allows the UI to present them as answerable cards.`,
|
|
1601
|
-
``,
|
|
1602
|
-
`Format:`,
|
|
1603
|
-
"```",
|
|
1604
|
-
"```open-questions",
|
|
1605
|
-
`[`,
|
|
1606
|
-
` {`,
|
|
1607
|
-
` "id": "q1",`,
|
|
1608
|
-
` "question": "Which approach should we use for X?",`,
|
|
1609
|
-
` "context": "One sentence explaining why this decision matters.",`,
|
|
1610
|
-
` "options": [`,
|
|
1611
|
-
` "Option A \u2014 brief description",`,
|
|
1612
|
-
` "Option B \u2014 brief description",`,
|
|
1613
|
-
` "Option C \u2014 brief description"`,
|
|
1614
|
-
` ]`,
|
|
1615
|
-
` }`,
|
|
1616
|
-
`]`,
|
|
1617
|
-
"```",
|
|
1618
|
-
"```",
|
|
1619
|
-
``,
|
|
1620
|
-
`Rules for open questions:`,
|
|
1621
|
-
`- Each question MUST have 2-4 concrete options.`,
|
|
1622
|
-
`- Options should be actionable choices, not vague ("Yes"/"No" is fine when appropriate).`,
|
|
1623
|
-
`- The "context" field should be a single sentence explaining why the decision matters.`,
|
|
1624
|
-
`- Use sequential IDs: q1, q2, q3, etc.`,
|
|
1625
|
-
`- Place the JSON block at the very end of the "Open Questions" section.`,
|
|
1626
|
-
`- You may include a brief prose introduction before the JSON block, but the block itself must be last.`,
|
|
1627
1899
|
...isRevision ? [
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
+
],
|
|
1631
1907
|
`${nextStep(4)}. Save the PRD as a Markdown file in the working directory:`,
|
|
1632
1908
|
` - File name: \`prd.md\``,
|
|
1633
1909
|
` - Write the full PRD content to this file.`,
|
|
@@ -1784,13 +2060,23 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
|
|
|
1784
2060
|
);
|
|
1785
2061
|
}
|
|
1786
2062
|
const variantList = Array.from({ length: proto.variantCount }, (_, i) => `prototype-${i + 1}.html`);
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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");
|
|
1794
2080
|
return [
|
|
1795
2081
|
`You are a UI designer and frontend engineer. Your job is to REFINE an existing prototype based on user feedback.`,
|
|
1796
2082
|
``,
|
|
@@ -1907,7 +2193,7 @@ function buildIdeaPrompt(idea, repoDir) {
|
|
|
1907
2193
|
`- Do NOT exit until both files have been written and verified`
|
|
1908
2194
|
].join("\n");
|
|
1909
2195
|
}
|
|
1910
|
-
function buildAgentArgs(agent, prompt2, mode, sessionId, name) {
|
|
2196
|
+
function buildAgentArgs(agent, prompt2, mode, sessionId, name, resumeSession = false, systemPrompt) {
|
|
1911
2197
|
if (agent === "codex") {
|
|
1912
2198
|
const args = [];
|
|
1913
2199
|
if (mode === "execute") {
|
|
@@ -1917,26 +2203,40 @@ function buildAgentArgs(agent, prompt2, mode, sessionId, name) {
|
|
|
1917
2203
|
if (mode === "execute") {
|
|
1918
2204
|
args.push("-s", "danger-full-access");
|
|
1919
2205
|
}
|
|
1920
|
-
|
|
2206
|
+
const fullPrompt = systemPrompt ? `${prompt2}
|
|
2207
|
+
|
|
2208
|
+
${systemPrompt}` : prompt2;
|
|
2209
|
+
args.push(fullPrompt);
|
|
1921
2210
|
return { bin: "codex", args };
|
|
1922
2211
|
}
|
|
1923
2212
|
if (agent === "gemini") {
|
|
1924
|
-
const
|
|
2213
|
+
const fullPrompt = systemPrompt ? `${prompt2}
|
|
2214
|
+
|
|
2215
|
+
${systemPrompt}` : prompt2;
|
|
2216
|
+
const args = ["-p", fullPrompt];
|
|
1925
2217
|
if (mode === "execute") {
|
|
1926
2218
|
args.push("--yolo");
|
|
1927
2219
|
}
|
|
1928
2220
|
return { bin: "gemini", args };
|
|
1929
2221
|
}
|
|
1930
|
-
const sessionArgs = sessionId ? ["--session-id", sessionId] : [];
|
|
2222
|
+
const sessionArgs = sessionId ? resumeSession ? ["--resume", sessionId] : ["--session-id", sessionId] : [];
|
|
1931
2223
|
const nameArgs = name ? ["--name", name] : [];
|
|
2224
|
+
const systemArgs = systemPrompt ? ["--append-system-prompt", systemPrompt] : [];
|
|
1932
2225
|
if (mode === "plan") {
|
|
1933
|
-
return { bin: "claude", args: [...sessionArgs, ...nameArgs, "--permission-mode", "plan", "-p", prompt2] };
|
|
2226
|
+
return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, "--permission-mode", "plan", "-p", prompt2] };
|
|
1934
2227
|
}
|
|
1935
|
-
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
|
+
});
|
|
1936
2234
|
}
|
|
1937
2235
|
function runPlanningPhase(task, repoDir, agent) {
|
|
1938
2236
|
return new Promise((res, reject) => {
|
|
1939
|
-
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);
|
|
1940
2240
|
const child = spawn4(bin, args, { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] });
|
|
1941
2241
|
child.on("error", (err) => {
|
|
1942
2242
|
reject(new Error(`Failed to spawn ${agent}: ${err.message}`));
|
|
@@ -1969,25 +2269,28 @@ ${output.trim()}`));
|
|
|
1969
2269
|
});
|
|
1970
2270
|
}
|
|
1971
2271
|
function askYesNo(question) {
|
|
1972
|
-
return new Promise((
|
|
2272
|
+
return new Promise((resolve8) => {
|
|
1973
2273
|
const rl = readline.createInterface({
|
|
1974
2274
|
input: process.stdin,
|
|
1975
2275
|
output: process.stdout
|
|
1976
2276
|
});
|
|
1977
2277
|
rl.question(question, (answer) => {
|
|
1978
2278
|
rl.close();
|
|
1979
|
-
|
|
2279
|
+
resolve8(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
1980
2280
|
});
|
|
1981
2281
|
});
|
|
1982
2282
|
}
|
|
1983
|
-
function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name) {
|
|
1984
|
-
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);
|
|
1985
2287
|
const child = spawn4(bin, args, { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] });
|
|
1986
2288
|
child.on("error", (err) => {
|
|
1987
2289
|
logError(prefix, `Failed to spawn ${agent}: ${err.message}`);
|
|
1988
2290
|
if (err.code === "ENOENT") {
|
|
1989
2291
|
logError(prefix, `Check that "${bin}" is on PATH and "${repoDir}" exists`);
|
|
1990
2292
|
}
|
|
2293
|
+
onSpawnError?.(err);
|
|
1991
2294
|
});
|
|
1992
2295
|
if (agent === "codex") {
|
|
1993
2296
|
child.stdout?.on("data", () => onActivity?.());
|
|
@@ -2018,6 +2321,8 @@ var watchCommand = new Command8("watch").description(
|
|
|
2018
2321
|
const agent = opts.agent === "codex" ? "codex" : opts.agent === "gemini" ? "gemini" : "claude";
|
|
2019
2322
|
const scanAt = opts.scanAt;
|
|
2020
2323
|
const taskStallTimeoutMs = getTaskStallTimeoutMs();
|
|
2324
|
+
const hungTaskTimeoutMinutes = Math.max(5, parseInt(process.env.MR_WATCH_HUNG_TASK_TIMEOUT_MINUTES ?? "60", 10) || 60);
|
|
2325
|
+
const hungTaskTimeoutMs = hungTaskTimeoutMinutes * 6e4;
|
|
2021
2326
|
const active = /* @__PURE__ */ new Map();
|
|
2022
2327
|
const failed = /* @__PURE__ */ new Map();
|
|
2023
2328
|
const queued = /* @__PURE__ */ new Set();
|
|
@@ -2025,6 +2330,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2025
2330
|
let pollRunning = false;
|
|
2026
2331
|
const approvalQueue = [];
|
|
2027
2332
|
let approvalRunning = false;
|
|
2333
|
+
const agentAvailability = /* @__PURE__ */ new Map();
|
|
2028
2334
|
const flags = [
|
|
2029
2335
|
`interval=${paint("cyan", opts.interval + "s")}`,
|
|
2030
2336
|
`root=${paint("cyan", rootDir)}`,
|
|
@@ -2047,6 +2353,22 @@ var watchCommand = new Command8("watch").description(
|
|
|
2047
2353
|
console.log(banner);
|
|
2048
2354
|
console.log(` ${flags}
|
|
2049
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
|
+
}
|
|
2050
2372
|
async function moveTaskToError(task, prefix, reason) {
|
|
2051
2373
|
try {
|
|
2052
2374
|
await api.patch(`/api/tasks/${task.id}`, { status: "error" });
|
|
@@ -2061,12 +2383,13 @@ var watchCommand = new Command8("watch").description(
|
|
|
2061
2383
|
async function dispatchTask(task, repoDir) {
|
|
2062
2384
|
const sid = shortId(task.id);
|
|
2063
2385
|
const slug = slugify(task.title);
|
|
2064
|
-
const
|
|
2386
|
+
const owner = ownerPrefix(task);
|
|
2387
|
+
const branchName = `${owner}/${slug}`;
|
|
2388
|
+
const legacyBranchName = `mr/${sid}/${slug}`;
|
|
2065
2389
|
const prefix = taskTag(sid);
|
|
2066
2390
|
const vcs = detectVcs(repoDir)?.provider ?? "github";
|
|
2067
2391
|
logDispatch(prefix, `"${paint("bold", task.title)}" ${paint("gray", repoDir)} ${paint("dim", `[${vcs}]`)}`);
|
|
2068
2392
|
await postTaskUpdate(task.id, `Agent dispatched \u2014 starting work on "${task.title}"`, "system");
|
|
2069
|
-
await pullLatestMain(repoDir, prefix);
|
|
2070
2393
|
let subtasks = [];
|
|
2071
2394
|
try {
|
|
2072
2395
|
subtasks = await api.get(`/api/tasks/${task.id}/subtasks`);
|
|
@@ -2110,20 +2433,27 @@ var watchCommand = new Command8("watch").description(
|
|
|
2110
2433
|
} catch {
|
|
2111
2434
|
}
|
|
2112
2435
|
const hasFeedback = feedbackUpdates.length > 0;
|
|
2113
|
-
const
|
|
2436
|
+
const wtName = `${owner}-${slug}`;
|
|
2437
|
+
const desiredWorktreePath = worktreePath(wtName);
|
|
2438
|
+
const startWithoutWorktree = !hasFeedback && taskLikelyDoesNotNeedCodeChanges(task);
|
|
2114
2439
|
let executionDir = repoDir;
|
|
2115
2440
|
let cleanupWorktreePath;
|
|
2116
|
-
if (
|
|
2117
|
-
|
|
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(
|
|
2118
2445
|
repoDir,
|
|
2119
2446
|
branchName,
|
|
2120
2447
|
worktreeNameFromPath(desiredWorktreePath)
|
|
2121
2448
|
);
|
|
2122
|
-
|
|
2123
|
-
|
|
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
|
+
);
|
|
2124
2455
|
}
|
|
2125
|
-
const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs, executionDir);
|
|
2126
|
-
const sessionId = agent === "claude" ? randomUUID() : void 0;
|
|
2456
|
+
const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs, executionDir, startWithoutWorktree);
|
|
2127
2457
|
const activeEntry = {
|
|
2128
2458
|
process: void 0,
|
|
2129
2459
|
title: task.title,
|
|
@@ -2136,90 +2466,181 @@ var watchCommand = new Command8("watch").description(
|
|
|
2136
2466
|
const touchActivity = () => {
|
|
2137
2467
|
activeEntry.lastActivityAt = Date.now();
|
|
2138
2468
|
};
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
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;
|
|
2145
2478
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
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}`);
|
|
2170
2558
|
}
|
|
2171
|
-
unlinkSync(researchPath);
|
|
2172
|
-
} catch (err) {
|
|
2173
|
-
logWarn(prefix, `Failed to upload research resource: ${err.message}`);
|
|
2174
2559
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
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}`);
|
|
2567
|
+
}
|
|
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
|
+
}
|
|
2190
2611
|
if (prUrl) {
|
|
2191
|
-
|
|
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");
|
|
2192
2616
|
}
|
|
2193
2617
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
}
|
|
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}`);
|
|
2200
2627
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
});
|
|
2205
|
-
|
|
2206
|
-
await postTaskUpdate(task.id, noMrRequested ? `Task marked ready for review \u2014 no ${prLabel} needed: ${noMrDescription}` : "Task marked ready for review", "system");
|
|
2207
|
-
} catch (err) {
|
|
2208
|
-
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);
|
|
2209
2633
|
}
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
|
|
2217
|
-
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);
|
|
2218
2640
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
});
|
|
2641
|
+
});
|
|
2642
|
+
};
|
|
2643
|
+
await launchAttempt(attemptOrder[attemptIndex]);
|
|
2223
2644
|
}
|
|
2224
2645
|
async function dispatchPlanModeTask(task, repoDir) {
|
|
2225
2646
|
const sid = shortId(task.id);
|
|
@@ -2250,65 +2671,122 @@ var watchCommand = new Command8("watch").description(
|
|
|
2250
2671
|
"system"
|
|
2251
2672
|
);
|
|
2252
2673
|
const prompt2 = buildPrdPrompt(task, repoDir, existingPlanResource?.content, feedbackUpdates);
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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) {
|
|
2263
2735
|
try {
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
unlinkSync(prdPath);
|
|
2267
|
-
} catch {
|
|
2268
|
-
logWarn(prefix, `No prd.md file found in ${repoDir} \u2014 PRD may have been posted inline`);
|
|
2269
|
-
}
|
|
2270
|
-
if (prdContent) {
|
|
2736
|
+
const prdPath = resolve2(repoDir, "prd.md");
|
|
2737
|
+
let prdContent;
|
|
2271
2738
|
try {
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
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}`);
|
|
2284
2762
|
}
|
|
2285
|
-
} catch (err) {
|
|
2286
|
-
logWarn(prefix, `Failed to upload PRD resource: ${err.message}`);
|
|
2287
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}`);
|
|
2288
2772
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
} else {
|
|
2299
|
-
logError(prefix, `"${paint("bold", task.title)}" PRD generation failed (exit ${code}), marked as error`);
|
|
2300
|
-
try {
|
|
2301
|
-
await api.patch(`/api/tasks/${task.id}`, { status: "error" });
|
|
2302
|
-
} catch (err) {
|
|
2303
|
-
logError(prefix, `Failed to mark task as error: ${err.message}`);
|
|
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");
|
|
2304
2782
|
}
|
|
2305
|
-
|
|
2783
|
+
} finally {
|
|
2784
|
+
queued.delete(task.id);
|
|
2785
|
+
finishing.delete(task.id);
|
|
2306
2786
|
}
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
}
|
|
2311
|
-
});
|
|
2787
|
+
});
|
|
2788
|
+
};
|
|
2789
|
+
await launchAttempt(attemptOrder[attemptIndex]);
|
|
2312
2790
|
}
|
|
2313
2791
|
async function dispatchPrototypeJob(proto, repoDir) {
|
|
2314
2792
|
const sid = shortId(proto.id);
|
|
@@ -2334,84 +2812,186 @@ var watchCommand = new Command8("watch").description(
|
|
|
2334
2812
|
} else {
|
|
2335
2813
|
prompt2 = buildPrototypePrompt(proto, repoDir);
|
|
2336
2814
|
}
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
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}`);
|
|
2360
2890
|
try {
|
|
2361
|
-
|
|
2891
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2362
2892
|
} catch {
|
|
2363
2893
|
}
|
|
2364
2894
|
}
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2895
|
+
} else {
|
|
2896
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2897
|
+
logError(prefix, `"${paint("bold", proto.title)}" prototype failed via ${attemptAgent} (${failureDetail})`);
|
|
2898
|
+
try {
|
|
2899
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2900
|
+
} catch {
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
} finally {
|
|
2904
|
+
queued.delete(key);
|
|
2905
|
+
finishing.delete(key);
|
|
2906
|
+
}
|
|
2907
|
+
});
|
|
2908
|
+
};
|
|
2909
|
+
await launchAttempt(attemptOrder[attemptIndex]);
|
|
2910
|
+
}
|
|
2911
|
+
async function dispatchRepoCreation(project, workDir) {
|
|
2912
|
+
const sid = shortId(project.id);
|
|
2913
|
+
const prefix = repoTag(sid);
|
|
2914
|
+
logDispatch(prefix, `"${paint("bold", project.name)}" ${paint("gray", workDir)}`);
|
|
2915
|
+
try {
|
|
2916
|
+
await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "creating" });
|
|
2917
|
+
} catch {
|
|
2918
|
+
}
|
|
2919
|
+
const prompt2 = buildRepoCreationPrompt(project, workDir);
|
|
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);
|
|
2925
|
+
try {
|
|
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;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
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})`);
|
|
2367
2983
|
try {
|
|
2368
|
-
await api.patch(`/api/
|
|
2984
|
+
await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "failed" });
|
|
2369
2985
|
} catch {
|
|
2370
2986
|
}
|
|
2371
2987
|
}
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2376
|
-
} catch {
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
} finally {
|
|
2380
|
-
queued.delete(key);
|
|
2381
|
-
finishing.delete(key);
|
|
2382
|
-
}
|
|
2383
|
-
});
|
|
2384
|
-
}
|
|
2385
|
-
async function dispatchRepoCreation(project, workDir) {
|
|
2386
|
-
const sid = shortId(project.id);
|
|
2387
|
-
const prefix = repoTag(sid);
|
|
2388
|
-
logDispatch(prefix, `"${paint("bold", project.name)}" ${paint("gray", workDir)}`);
|
|
2389
|
-
try {
|
|
2390
|
-
await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "creating" });
|
|
2391
|
-
} catch {
|
|
2392
|
-
}
|
|
2393
|
-
const prompt2 = buildRepoCreationPrompt(project, workDir);
|
|
2394
|
-
const child = spawnAgent(agent, workDir, prompt2, prefix, void 0, project.name);
|
|
2395
|
-
active.set(`repo-${project.id}`, { process: child, title: project.name, repoDir: workDir, startedAt: Date.now() });
|
|
2396
|
-
child.on("exit", async (code) => {
|
|
2397
|
-
const key = `repo-${project.id}`;
|
|
2398
|
-
active.delete(key);
|
|
2399
|
-
finishing.add(key);
|
|
2400
|
-
try {
|
|
2401
|
-
if (code === 0) {
|
|
2402
|
-
logSuccess(prefix, `"${paint("bold", project.name)}" repo creation complete`);
|
|
2403
|
-
} else {
|
|
2404
|
-
logError(prefix, `"${paint("bold", project.name)}" repo creation failed (exit ${code})`);
|
|
2405
|
-
try {
|
|
2406
|
-
await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "failed" });
|
|
2407
|
-
} catch {
|
|
2408
|
-
}
|
|
2988
|
+
} finally {
|
|
2989
|
+
queued.delete(key);
|
|
2990
|
+
finishing.delete(key);
|
|
2409
2991
|
}
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
}
|
|
2414
|
-
});
|
|
2992
|
+
});
|
|
2993
|
+
};
|
|
2994
|
+
await launchAttempt(attemptOrder[attemptIndex]);
|
|
2415
2995
|
}
|
|
2416
2996
|
async function dispatchIdeaJob(idea, repoDir) {
|
|
2417
2997
|
const sid = shortId(idea.id);
|
|
@@ -2424,96 +3004,146 @@ var watchCommand = new Command8("watch").description(
|
|
|
2424
3004
|
}
|
|
2425
3005
|
}
|
|
2426
3006
|
const prompt2 = buildIdeaPrompt(idea, repoDir);
|
|
2427
|
-
const
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
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);
|
|
2433
3012
|
try {
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
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) {
|
|
2450
3062
|
try {
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
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 {
|
|
2455
3072
|
}
|
|
2456
|
-
} catch {
|
|
2457
|
-
}
|
|
2458
|
-
if (!plan && !protoHtml) {
|
|
2459
|
-
logError(prefix, `No output files found in ${repoDir}`);
|
|
2460
|
-
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2461
|
-
return;
|
|
2462
|
-
}
|
|
2463
|
-
const updateData = { status: "generated" };
|
|
2464
|
-
if (plan) updateData.plan = plan;
|
|
2465
|
-
if (followUpTasks) updateData.followUpTasks = followUpTasks;
|
|
2466
|
-
if (protoHtml) {
|
|
2467
3073
|
try {
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
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 {
|
|
2482
3130
|
}
|
|
2483
3131
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
unlinkSync(planPath);
|
|
2488
|
-
} catch {
|
|
2489
|
-
}
|
|
2490
|
-
try {
|
|
2491
|
-
unlinkSync(tasksPath);
|
|
2492
|
-
} catch {
|
|
2493
|
-
}
|
|
2494
|
-
try {
|
|
2495
|
-
unlinkSync(protoPath);
|
|
2496
|
-
} catch {
|
|
2497
|
-
}
|
|
2498
|
-
} catch (err) {
|
|
2499
|
-
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})`);
|
|
2500
3135
|
try {
|
|
2501
3136
|
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2502
3137
|
} catch {
|
|
2503
3138
|
}
|
|
2504
3139
|
}
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2509
|
-
} catch {
|
|
2510
|
-
}
|
|
3140
|
+
} finally {
|
|
3141
|
+
queued.delete(key);
|
|
3142
|
+
finishing.delete(key);
|
|
2511
3143
|
}
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
}
|
|
2516
|
-
});
|
|
3144
|
+
});
|
|
3145
|
+
};
|
|
3146
|
+
await launchAttempt(attemptOrder[attemptIndex]);
|
|
2517
3147
|
}
|
|
2518
3148
|
function dispatchScan(scan, prefix, key) {
|
|
2519
3149
|
logDispatch(prefix, `Running scan for project ${paint("cyan", scan.projectId.slice(0, 8))}`);
|
|
@@ -2527,7 +3157,13 @@ var watchCommand = new Command8("watch").description(
|
|
|
2527
3157
|
queued.delete(key);
|
|
2528
3158
|
failed.set(key, err.message);
|
|
2529
3159
|
});
|
|
2530
|
-
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
|
+
});
|
|
2531
3167
|
scanProc.stdout?.on("data", (d) => {
|
|
2532
3168
|
const lines = d.toString().trim().split("\n");
|
|
2533
3169
|
for (const line of lines) {
|
|
@@ -2559,7 +3195,28 @@ var watchCommand = new Command8("watch").description(
|
|
|
2559
3195
|
const prefix = taskTag(sid);
|
|
2560
3196
|
try {
|
|
2561
3197
|
logSpinner(prefix, `Generating plan for "${paint("bold", task.title)}"\u2026`);
|
|
2562
|
-
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
|
+
}
|
|
2563
3220
|
const width = 64;
|
|
2564
3221
|
const divider = paint("gray", "\u2500".repeat(width));
|
|
2565
3222
|
const header = `${paint("bold", "Plan")} "${paint("bold", task.title)}" ${taskTag(sid)}`;
|
|
@@ -2752,14 +3409,16 @@ ${divider}`);
|
|
|
2752
3409
|
if (failed.has(key)) continue;
|
|
2753
3410
|
const sid = shortId(proto.id);
|
|
2754
3411
|
const prefix = protoTag(sid);
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
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;
|
|
2763
3422
|
}
|
|
2764
3423
|
if (!existsSync7(repoDir)) {
|
|
2765
3424
|
logError(prefix, `"${proto.title}": linked directory "${repoDir}" does not exist \u2014 skipping`);
|
|
@@ -2777,16 +3436,25 @@ ${divider}`);
|
|
|
2777
3436
|
}
|
|
2778
3437
|
dispatchPrototypeJob(proto, repoDir);
|
|
2779
3438
|
}
|
|
3439
|
+
const MAX_CONCURRENT_TESTS = 2;
|
|
2780
3440
|
const testTasks = queuedTasks.filter((t) => t.mode === "testing");
|
|
2781
3441
|
for (const task of testTasks) {
|
|
2782
3442
|
const key = `test-${task.id}`;
|
|
2783
3443
|
if (queued.has(key)) continue;
|
|
2784
3444
|
if (finishing.has(key)) continue;
|
|
2785
3445
|
if (failed.has(key)) continue;
|
|
3446
|
+
const activeTests = [...queued].filter((k) => k.startsWith("test-")).length;
|
|
3447
|
+
if (activeTests >= MAX_CONCURRENT_TESTS) break;
|
|
2786
3448
|
const sid = shortId(task.id);
|
|
2787
3449
|
const prefix = testTag(sid);
|
|
2788
3450
|
if (!task.link) {
|
|
2789
3451
|
logWarn(prefix, `"${task.title}": no MR/PR link \u2014 skipping`);
|
|
3452
|
+
failed.set(key, "no MR/PR link");
|
|
3453
|
+
try {
|
|
3454
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "error" });
|
|
3455
|
+
await postTaskUpdate(task.id, "Test skipped: task has no MR/PR link", "system");
|
|
3456
|
+
} catch {
|
|
3457
|
+
}
|
|
2790
3458
|
continue;
|
|
2791
3459
|
}
|
|
2792
3460
|
queued.add(key);
|
|
@@ -2833,9 +3501,9 @@ ${divider}`);
|
|
|
2833
3501
|
browseRunner: runBrowseCommand2,
|
|
2834
3502
|
uploadScreenshot: async (screenshotPath, message) => {
|
|
2835
3503
|
try {
|
|
2836
|
-
const { readFileSync:
|
|
3504
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
2837
3505
|
const cfg = loadConfig();
|
|
2838
|
-
const imageBuffer =
|
|
3506
|
+
const imageBuffer = readFileSync13(screenshotPath);
|
|
2839
3507
|
const fileName = screenshotPath.split("/").pop() || "test-screenshot.png";
|
|
2840
3508
|
const formData = new FormData();
|
|
2841
3509
|
const blob = new Blob([imageBuffer], { type: "image/png" });
|
|
@@ -2975,6 +3643,55 @@ ${divider}`);
|
|
|
2975
3643
|
}
|
|
2976
3644
|
dispatchScan(scan, prefix, key);
|
|
2977
3645
|
}
|
|
3646
|
+
let reviewTasks = [];
|
|
3647
|
+
try {
|
|
3648
|
+
reviewTasks = await api.get("/api/tasks?status=review");
|
|
3649
|
+
} catch (err) {
|
|
3650
|
+
logError(watchTag(), `Failed to fetch review tasks: ${err.message}`);
|
|
3651
|
+
}
|
|
3652
|
+
for (const task of reviewTasks) {
|
|
3653
|
+
if (!task.link) continue;
|
|
3654
|
+
if (queued.has(task.id) || finishing.has(task.id) || active.has(task.id)) continue;
|
|
3655
|
+
const sid = shortId(task.id);
|
|
3656
|
+
const prefix = taskTag(sid);
|
|
3657
|
+
const prLabel = task.link.includes("gitlab") ? "MR" : "PR";
|
|
3658
|
+
const vcs = task.link.includes("gitlab") ? "gitlab" : "github";
|
|
3659
|
+
const repoDir = findDirectoryForProject(config, task.projectId, rootDir);
|
|
3660
|
+
if (!repoDir) continue;
|
|
3661
|
+
const status = await checkPrStatus(task.link, repoDir, vcs);
|
|
3662
|
+
if (!status) continue;
|
|
3663
|
+
if (status.merged) {
|
|
3664
|
+
logSuccess(prefix, `${prLabel} merged \u2014 auto-completing "${paint("bold", task.title)}"`);
|
|
3665
|
+
try {
|
|
3666
|
+
await api.patch(`/api/tasks/${task.id}`, {
|
|
3667
|
+
status: "completed",
|
|
3668
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3669
|
+
inProgressSince: null
|
|
3670
|
+
});
|
|
3671
|
+
await postTaskUpdate(task.id, `${prLabel} merged \u2014 task automatically completed`, "system");
|
|
3672
|
+
} catch (err) {
|
|
3673
|
+
logError(prefix, `Failed to auto-complete task: ${err.message}`);
|
|
3674
|
+
}
|
|
3675
|
+
continue;
|
|
3676
|
+
}
|
|
3677
|
+
if (status.hasConflicts) {
|
|
3678
|
+
logWarn(prefix, `${prLabel} has merge conflicts \u2014 re-dispatching agent for "${paint("bold", task.title)}"`);
|
|
3679
|
+
try {
|
|
3680
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "queued" });
|
|
3681
|
+
await api.post(`/api/tasks/${task.id}/updates`, {
|
|
3682
|
+
message: `Your ${prLabel} has merge conflicts with the base branch. Please resolve the conflicts by rebasing or merging main into your branch, then push the updated branch.`,
|
|
3683
|
+
source: "user"
|
|
3684
|
+
});
|
|
3685
|
+
await postTaskUpdate(
|
|
3686
|
+
task.id,
|
|
3687
|
+
`${prLabel} has merge conflicts \u2014 re-dispatching agent to resolve`,
|
|
3688
|
+
"system"
|
|
3689
|
+
);
|
|
3690
|
+
} catch (err) {
|
|
3691
|
+
logError(prefix, `Failed to re-queue task for conflict resolution: ${err.message}`);
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
2978
3695
|
} finally {
|
|
2979
3696
|
pollRunning = false;
|
|
2980
3697
|
}
|
|
@@ -3212,22 +3929,21 @@ var prototypeCommand = new Command13("prototype").description("Manage prototypes
|
|
|
3212
3929
|
}
|
|
3213
3930
|
})
|
|
3214
3931
|
).addCommand(
|
|
3215
|
-
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) => {
|
|
3216
3933
|
const projectId = opts.project ?? getLinkedProjectId();
|
|
3217
|
-
if (!projectId) {
|
|
3218
|
-
console.error('No project linked. Run "mr link <project-id>" or use --project.');
|
|
3219
|
-
process.exit(1);
|
|
3220
|
-
}
|
|
3221
3934
|
const variantCount = Math.max(1, Math.min(50, parseInt(opts.variants, 10) || 5));
|
|
3222
3935
|
const prototype = await api.post("/api/prototypes", {
|
|
3223
3936
|
title,
|
|
3224
3937
|
prompt: opts.prompt,
|
|
3225
3938
|
variantCount,
|
|
3226
|
-
projectId
|
|
3939
|
+
projectId: projectId ?? null
|
|
3227
3940
|
});
|
|
3228
3941
|
console.log();
|
|
3229
3942
|
console.log(` ${paint4("green", "\u2713")} Created prototype: ${paint4("bold", prototype.title)}`);
|
|
3230
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
|
+
}
|
|
3231
3947
|
console.log(` ${paint4("cyan", "\u27F3")} Generation will begin automatically via the watch agent.`);
|
|
3232
3948
|
console.log();
|
|
3233
3949
|
})
|
|
@@ -3270,20 +3986,20 @@ var c5 = {
|
|
|
3270
3986
|
function paint5(color, text) {
|
|
3271
3987
|
return `${c5[color]}${text}${c5.reset}`;
|
|
3272
3988
|
}
|
|
3273
|
-
function
|
|
3274
|
-
return new Promise((
|
|
3275
|
-
exec2(`which ${cmd}`, (err) =>
|
|
3989
|
+
function commandExists2(cmd) {
|
|
3990
|
+
return new Promise((resolve8) => {
|
|
3991
|
+
exec2(`which ${cmd}`, (err) => resolve8(!err));
|
|
3276
3992
|
});
|
|
3277
3993
|
}
|
|
3278
3994
|
function execQuiet(cmd) {
|
|
3279
|
-
return new Promise((
|
|
3995
|
+
return new Promise((resolve8) => {
|
|
3280
3996
|
exec2(cmd, (err, stdout, stderr) => {
|
|
3281
|
-
|
|
3997
|
+
resolve8({ ok: !err, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
3282
3998
|
});
|
|
3283
3999
|
});
|
|
3284
4000
|
}
|
|
3285
4001
|
async function checkGitInstalled() {
|
|
3286
|
-
const exists = await
|
|
4002
|
+
const exists = await commandExists2("git");
|
|
3287
4003
|
if (!exists) {
|
|
3288
4004
|
return {
|
|
3289
4005
|
name: "Git",
|
|
@@ -3323,7 +4039,7 @@ async function checkNodeVersion() {
|
|
|
3323
4039
|
};
|
|
3324
4040
|
}
|
|
3325
4041
|
async function checkGhInstalled() {
|
|
3326
|
-
const exists = await
|
|
4042
|
+
const exists = await commandExists2("gh");
|
|
3327
4043
|
return {
|
|
3328
4044
|
name: "GitHub CLI (gh)",
|
|
3329
4045
|
ok: exists,
|
|
@@ -3332,7 +4048,7 @@ async function checkGhInstalled() {
|
|
|
3332
4048
|
};
|
|
3333
4049
|
}
|
|
3334
4050
|
async function checkGhAuth() {
|
|
3335
|
-
const exists = await
|
|
4051
|
+
const exists = await commandExists2("gh");
|
|
3336
4052
|
if (!exists) {
|
|
3337
4053
|
return {
|
|
3338
4054
|
name: "GitHub CLI auth",
|
|
@@ -3350,7 +4066,7 @@ async function checkGhAuth() {
|
|
|
3350
4066
|
};
|
|
3351
4067
|
}
|
|
3352
4068
|
async function checkClaudeInstalled() {
|
|
3353
|
-
const exists = await
|
|
4069
|
+
const exists = await commandExists2("claude");
|
|
3354
4070
|
return {
|
|
3355
4071
|
name: "Claude Code (claude)",
|
|
3356
4072
|
ok: exists,
|
|
@@ -3359,7 +4075,7 @@ async function checkClaudeInstalled() {
|
|
|
3359
4075
|
};
|
|
3360
4076
|
}
|
|
3361
4077
|
async function checkClaudeAuth() {
|
|
3362
|
-
const exists = await
|
|
4078
|
+
const exists = await commandExists2("claude");
|
|
3363
4079
|
if (!exists) {
|
|
3364
4080
|
return {
|
|
3365
4081
|
name: "Claude Code auth",
|
|
@@ -3377,7 +4093,7 @@ async function checkClaudeAuth() {
|
|
|
3377
4093
|
};
|
|
3378
4094
|
}
|
|
3379
4095
|
async function checkCodexInstalled() {
|
|
3380
|
-
const exists = await
|
|
4096
|
+
const exists = await commandExists2("codex");
|
|
3381
4097
|
return {
|
|
3382
4098
|
name: "Codex CLI (codex)",
|
|
3383
4099
|
ok: exists,
|
|
@@ -3386,7 +4102,7 @@ async function checkCodexInstalled() {
|
|
|
3386
4102
|
};
|
|
3387
4103
|
}
|
|
3388
4104
|
async function checkCodexAuth() {
|
|
3389
|
-
const exists = await
|
|
4105
|
+
const exists = await commandExists2("codex");
|
|
3390
4106
|
if (!exists) {
|
|
3391
4107
|
return {
|
|
3392
4108
|
name: "Codex CLI auth",
|
|
@@ -3403,7 +4119,7 @@ async function checkCodexAuth() {
|
|
|
3403
4119
|
};
|
|
3404
4120
|
}
|
|
3405
4121
|
async function checkGeminiInstalled() {
|
|
3406
|
-
const exists = await
|
|
4122
|
+
const exists = await commandExists2("gemini");
|
|
3407
4123
|
return {
|
|
3408
4124
|
name: "Gemini CLI (gemini)",
|
|
3409
4125
|
ok: exists,
|
|
@@ -3412,7 +4128,7 @@ async function checkGeminiInstalled() {
|
|
|
3412
4128
|
};
|
|
3413
4129
|
}
|
|
3414
4130
|
async function checkGeminiAuth() {
|
|
3415
|
-
const exists = await
|
|
4131
|
+
const exists = await commandExists2("gemini");
|
|
3416
4132
|
if (!exists) {
|
|
3417
4133
|
return {
|
|
3418
4134
|
name: "Gemini CLI auth",
|
|
@@ -3429,7 +4145,7 @@ async function checkGeminiAuth() {
|
|
|
3429
4145
|
};
|
|
3430
4146
|
}
|
|
3431
4147
|
async function checkGlabInstalled() {
|
|
3432
|
-
const exists = await
|
|
4148
|
+
const exists = await commandExists2("glab");
|
|
3433
4149
|
return {
|
|
3434
4150
|
name: "GitLab CLI (glab)",
|
|
3435
4151
|
ok: exists,
|
|
@@ -3439,7 +4155,7 @@ async function checkGlabInstalled() {
|
|
|
3439
4155
|
};
|
|
3440
4156
|
}
|
|
3441
4157
|
async function checkGlabAuth() {
|
|
3442
|
-
const exists = await
|
|
4158
|
+
const exists = await commandExists2("glab");
|
|
3443
4159
|
if (!exists) {
|
|
3444
4160
|
return {
|
|
3445
4161
|
name: "GitLab CLI auth",
|
|
@@ -3459,7 +4175,7 @@ async function checkGlabAuth() {
|
|
|
3459
4175
|
};
|
|
3460
4176
|
}
|
|
3461
4177
|
async function checkJqInstalled() {
|
|
3462
|
-
const exists = await
|
|
4178
|
+
const exists = await commandExists2("jq");
|
|
3463
4179
|
return {
|
|
3464
4180
|
name: "jq (optional)",
|
|
3465
4181
|
ok: exists,
|
|
@@ -3533,26 +4249,26 @@ async function autoFix(checks, agent) {
|
|
|
3533
4249
|
if (claudeCheck && !claudeCheck.ok && agent === "claude") {
|
|
3534
4250
|
console.log(paint5("cyan", " Installing Claude Code..."));
|
|
3535
4251
|
console.log(paint5("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
|
|
3536
|
-
await new Promise((
|
|
4252
|
+
await new Promise((resolve8) => {
|
|
3537
4253
|
const child = spawn8("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], { stdio: "inherit" });
|
|
3538
|
-
child.on("exit", () =>
|
|
4254
|
+
child.on("exit", () => resolve8());
|
|
3539
4255
|
});
|
|
3540
4256
|
console.log("");
|
|
3541
4257
|
}
|
|
3542
4258
|
if (ghInstalled && !ghAuthed) {
|
|
3543
4259
|
console.log(paint5("cyan", " Running gh auth login..."));
|
|
3544
|
-
await new Promise((
|
|
4260
|
+
await new Promise((resolve8) => {
|
|
3545
4261
|
const child = spawn8("gh", ["auth", "login"], { stdio: "inherit" });
|
|
3546
|
-
child.on("exit", () =>
|
|
4262
|
+
child.on("exit", () => resolve8());
|
|
3547
4263
|
});
|
|
3548
4264
|
console.log("");
|
|
3549
4265
|
}
|
|
3550
4266
|
if (!mrAuthed) {
|
|
3551
4267
|
console.log(paint5("cyan", " Running mr login..."));
|
|
3552
4268
|
const entry = process.argv[1];
|
|
3553
|
-
await new Promise((
|
|
4269
|
+
await new Promise((resolve8) => {
|
|
3554
4270
|
const child = spawn8(process.execPath, [entry, "login"], { stdio: "inherit" });
|
|
3555
|
-
child.on("exit", () =>
|
|
4271
|
+
child.on("exit", () => resolve8());
|
|
3556
4272
|
});
|
|
3557
4273
|
console.log("");
|
|
3558
4274
|
}
|
|
@@ -3800,7 +4516,7 @@ var resumeCommand = new Command17("resume").description("Resume an interactive C
|
|
|
3800
4516
|
// cli/commands/browse.ts
|
|
3801
4517
|
import { Command as Command18 } from "commander";
|
|
3802
4518
|
import { execSync as execSync5, spawn as spawn6 } from "child_process";
|
|
3803
|
-
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as
|
|
4519
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
3804
4520
|
import { join as join8 } from "path";
|
|
3805
4521
|
var BROWSE_DIR2 = join8(import.meta.dirname, "..", "..", "browse");
|
|
3806
4522
|
function isProcessAlive(pid) {
|
|
@@ -3847,7 +4563,7 @@ async function ensureDevServer() {
|
|
|
3847
4563
|
env: { ...process.env }
|
|
3848
4564
|
});
|
|
3849
4565
|
devProc.unref();
|
|
3850
|
-
|
|
4566
|
+
writeFileSync5(
|
|
3851
4567
|
devStateFile,
|
|
3852
4568
|
JSON.stringify({ pid: devProc.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
3853
4569
|
{ mode: 384 }
|
|
@@ -4089,7 +4805,7 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
4089
4805
|
|
|
4090
4806
|
// cli/commands/features.ts
|
|
4091
4807
|
import { Command as Command21 } from "commander";
|
|
4092
|
-
import { readFileSync as readFileSync9, writeFileSync as
|
|
4808
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync12 } from "fs";
|
|
4093
4809
|
import { resolve as resolve5, sep as sep2 } from "path";
|
|
4094
4810
|
var FEATURES_FILE3 = ".mr-features.md";
|
|
4095
4811
|
var c7 = {
|
|
@@ -4130,13 +4846,13 @@ var featuresCommand = new Command21("features").description("View or update the
|
|
|
4130
4846
|
if (opts.file) {
|
|
4131
4847
|
const content2 = readFileSync9(resolve5(opts.file), "utf-8");
|
|
4132
4848
|
const featuresPath = getFeaturesPath();
|
|
4133
|
-
|
|
4849
|
+
writeFileSync6(featuresPath, content2);
|
|
4134
4850
|
console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)} from ${paint7("cyan", opts.file)}`);
|
|
4135
4851
|
return;
|
|
4136
4852
|
}
|
|
4137
4853
|
if (opts.update) {
|
|
4138
4854
|
const featuresPath = getFeaturesPath();
|
|
4139
|
-
|
|
4855
|
+
writeFileSync6(featuresPath, opts.update);
|
|
4140
4856
|
console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)}`);
|
|
4141
4857
|
return;
|
|
4142
4858
|
}
|
|
@@ -4151,12 +4867,12 @@ var featuresCommand = new Command21("features").description("View or update the
|
|
|
4151
4867
|
|
|
4152
4868
|
// cli/commands/no-mr.ts
|
|
4153
4869
|
import { Command as Command22 } from "commander";
|
|
4154
|
-
import { writeFileSync as
|
|
4870
|
+
import { writeFileSync as writeFileSync7 } from "fs";
|
|
4155
4871
|
import { resolve as resolve6 } from "path";
|
|
4156
4872
|
var NO_MR_FILE = ".mr-no-mr";
|
|
4157
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) => {
|
|
4158
4874
|
const filePath = resolve6(process.cwd(), NO_MR_FILE);
|
|
4159
|
-
|
|
4875
|
+
writeFileSync7(filePath, description, "utf-8");
|
|
4160
4876
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
4161
4877
|
message: `No MR/PR needed \u2014 ${description}`,
|
|
4162
4878
|
source: "agent"
|
|
@@ -4165,105 +4881,8 @@ var noMrCommand = new Command22("no-mr").description("Signal that a task does no
|
|
|
4165
4881
|
console.log(` Reason: ${description}`);
|
|
4166
4882
|
});
|
|
4167
4883
|
|
|
4168
|
-
// cli/commands/mobile.ts
|
|
4169
|
-
import { Command as Command23 } from "commander";
|
|
4170
|
-
function paint8(color, text) {
|
|
4171
|
-
const colors = {
|
|
4172
|
-
cyan: "\x1B[36m",
|
|
4173
|
-
green: "\x1B[32m",
|
|
4174
|
-
yellow: "\x1B[33m",
|
|
4175
|
-
red: "\x1B[31m",
|
|
4176
|
-
dim: "\x1B[2m",
|
|
4177
|
-
reset: "\x1B[0m"
|
|
4178
|
-
};
|
|
4179
|
-
return `${colors[color] ?? ""}${text}${colors.reset}`;
|
|
4180
|
-
}
|
|
4181
|
-
var mobileCommand = new Command23("mobile").description(
|
|
4182
|
-
"Start the Web-to-Mobile Conversion Wizard for the linked project"
|
|
4183
|
-
).argument("[project-id]", "Project ID (defaults to linked project)").option("--framework <framework>", "Framework: react-native or native").option("--url <url>", "Web app URL for analysis").action(
|
|
4184
|
-
async (projectIdArg, opts) => {
|
|
4185
|
-
const projectId = projectIdArg || getLinkedProjectId();
|
|
4186
|
-
if (!projectId) {
|
|
4187
|
-
console.error(
|
|
4188
|
-
'No project specified. Provide a project ID or run "mr link <project-id>" first.'
|
|
4189
|
-
);
|
|
4190
|
-
process.exit(1);
|
|
4191
|
-
}
|
|
4192
|
-
console.log(
|
|
4193
|
-
paint8("cyan", "mobile") + paint8("dim", " \u2014 starting conversion wizard")
|
|
4194
|
-
);
|
|
4195
|
-
const task = await api.post("/api/tasks", {
|
|
4196
|
-
title: "Convert to Mobile App",
|
|
4197
|
-
projectId,
|
|
4198
|
-
status: "in_progress"
|
|
4199
|
-
});
|
|
4200
|
-
console.log(` Created task: ${task.title} (${task.id})`);
|
|
4201
|
-
console.log(" Analyzing web app...");
|
|
4202
|
-
const analysis = await api.post("/api/wizard/mobile/analyze", {
|
|
4203
|
-
projectId,
|
|
4204
|
-
parentTaskId: task.id,
|
|
4205
|
-
webAppUrl: opts.url
|
|
4206
|
-
});
|
|
4207
|
-
console.log(
|
|
4208
|
-
` Found ${analysis.analysis.screens.length} screens (${analysis.analysis.framework})`
|
|
4209
|
-
);
|
|
4210
|
-
if (opts.framework) {
|
|
4211
|
-
console.log(
|
|
4212
|
-
` Generating architecture plan for ${opts.framework}...`
|
|
4213
|
-
);
|
|
4214
|
-
await api.post("/api/wizard/mobile/generate-plan", {
|
|
4215
|
-
projectId,
|
|
4216
|
-
parentTaskId: task.id,
|
|
4217
|
-
framework: opts.framework,
|
|
4218
|
-
analysisResourceId: analysis.resourceId
|
|
4219
|
-
});
|
|
4220
|
-
console.log(" Architecture plan created.");
|
|
4221
|
-
}
|
|
4222
|
-
console.log(
|
|
4223
|
-
`
|
|
4224
|
-
${paint8("green", "\u2713")} Wizard initialized. Open the web UI to continue:
|
|
4225
|
-
\u2192 Task ID: ${task.id}
|
|
4226
|
-
\u2192 Use the "Convert to Mobile" button on the project page`
|
|
4227
|
-
);
|
|
4228
|
-
}
|
|
4229
|
-
);
|
|
4230
|
-
var statusSubcommand = new Command23("status").description("Show mobile conversion status for a task").argument("<task-id>", "Parent conversion task ID").action(async (taskId) => {
|
|
4231
|
-
const resources = await api.get(
|
|
4232
|
-
`/api/tasks/${taskId}/resources`
|
|
4233
|
-
);
|
|
4234
|
-
const wizardState = resources.find(
|
|
4235
|
-
(r) => r.title === "Wizard State" && r.type === "plan"
|
|
4236
|
-
);
|
|
4237
|
-
if (!wizardState) {
|
|
4238
|
-
console.log("No wizard state found for this task.");
|
|
4239
|
-
return;
|
|
4240
|
-
}
|
|
4241
|
-
try {
|
|
4242
|
-
const state = JSON.parse(wizardState.content);
|
|
4243
|
-
console.log(paint8("cyan", "Mobile Conversion Status"));
|
|
4244
|
-
console.log(` Phase: ${state.phase}`);
|
|
4245
|
-
if (state.framework) {
|
|
4246
|
-
console.log(` Framework: ${state.framework}`);
|
|
4247
|
-
}
|
|
4248
|
-
if (state.screenDesigns?.length) {
|
|
4249
|
-
const completed = state.screenDesigns.filter(
|
|
4250
|
-
(d) => d.status === "complete"
|
|
4251
|
-
).length;
|
|
4252
|
-
console.log(
|
|
4253
|
-
` Screens: ${completed}/${state.screenDesigns.length} complete`
|
|
4254
|
-
);
|
|
4255
|
-
}
|
|
4256
|
-
if (state.mobileRepoUrl) {
|
|
4257
|
-
console.log(` Repo: ${state.mobileRepoUrl}`);
|
|
4258
|
-
}
|
|
4259
|
-
} catch {
|
|
4260
|
-
console.log("Could not parse wizard state.");
|
|
4261
|
-
}
|
|
4262
|
-
});
|
|
4263
|
-
mobileCommand.addCommand(statusSubcommand);
|
|
4264
|
-
|
|
4265
4884
|
// cli/commands/scan.ts
|
|
4266
|
-
import { Command as
|
|
4885
|
+
import { Command as Command23 } from "commander";
|
|
4267
4886
|
|
|
4268
4887
|
// lib/scanner/index.ts
|
|
4269
4888
|
import { spawn as spawn7 } from "child_process";
|
|
@@ -4492,8 +5111,8 @@ function matchesIgnorePattern(route, patterns) {
|
|
|
4492
5111
|
}
|
|
4493
5112
|
async function uploadScreenshot(imagePath, apiUrl, apiKey) {
|
|
4494
5113
|
try {
|
|
4495
|
-
const { readFileSync:
|
|
4496
|
-
const imageBuffer =
|
|
5114
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
5115
|
+
const imageBuffer = readFileSync13(imagePath);
|
|
4497
5116
|
const blob = new Blob([imageBuffer], { type: "image/webp" });
|
|
4498
5117
|
const formData = new FormData();
|
|
4499
5118
|
formData.append("file", blob, `scan-${Date.now()}.webp`);
|
|
@@ -4700,22 +5319,19 @@ function buildSynthesisPrompt(config, context, codebaseAnalysis, crawlResults, p
|
|
|
4700
5319
|
const promotedFindings = priorFindings.filter((f) => f.status === "promoted");
|
|
4701
5320
|
const crawlSummary = crawlResults.map((r) => {
|
|
4702
5321
|
let summary = `Route: ${r.route}
|
|
4703
|
-
Title: ${r.pageTitle}
|
|
4704
|
-
Headings: ${r.headings.join(", ") || "none"}`;
|
|
5322
|
+
Title: ${r.pageTitle}`;
|
|
4705
5323
|
if (r.consoleErrors.length > 0) {
|
|
4706
5324
|
summary += `
|
|
4707
|
-
Console Errors: ${r.consoleErrors.slice(0,
|
|
5325
|
+
Console Errors: ${r.consoleErrors.slice(0, 3).join("; ")}`;
|
|
4708
5326
|
}
|
|
4709
5327
|
if (r.consoleWarnings.length > 0) {
|
|
4710
5328
|
summary += `
|
|
4711
|
-
Console Warnings: ${r.consoleWarnings.slice(0,
|
|
5329
|
+
Console Warnings: ${r.consoleWarnings.slice(0, 2).join("; ")}`;
|
|
4712
5330
|
}
|
|
4713
|
-
if (r.
|
|
5331
|
+
if (r.loadTimeMs > 1e3) {
|
|
4714
5332
|
summary += `
|
|
4715
|
-
|
|
5333
|
+
Load time: ${r.loadTimeMs}ms (slow)`;
|
|
4716
5334
|
}
|
|
4717
|
-
summary += `
|
|
4718
|
-
Load time: ${r.loadTimeMs}ms`;
|
|
4719
5335
|
return summary;
|
|
4720
5336
|
}).join("\n\n");
|
|
4721
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.
|
|
@@ -4732,16 +5348,16 @@ ${codebaseAnalysis.routes.map((r) => `- ${r}`).join("\n")}
|
|
|
4732
5348
|
${codebaseAnalysis.prismaModels.map((m) => `- ${m}`).join("\n")}
|
|
4733
5349
|
|
|
4734
5350
|
**Components:**
|
|
4735
|
-
${codebaseAnalysis.components.slice(0,
|
|
5351
|
+
${codebaseAnalysis.components.slice(0, 15).map((c10) => `- ${c10}`).join("\n")}
|
|
4736
5352
|
|
|
4737
5353
|
**Recent Git Commits:**
|
|
4738
|
-
${codebaseAnalysis.recentCommits.slice(0,
|
|
5354
|
+
${codebaseAnalysis.recentCommits.slice(0, 8).map((c10) => `- ${c10}`).join("\n")}
|
|
4739
5355
|
|
|
4740
5356
|
**Completed Tasks:**
|
|
4741
|
-
${context.completedTasks.slice(0,
|
|
5357
|
+
${context.completedTasks.slice(0, 10).map((t) => `- ${t.title}`).join("\n") || "None"}
|
|
4742
5358
|
|
|
4743
5359
|
**Open Tasks:**
|
|
4744
|
-
${context.openTasks.slice(0,
|
|
5360
|
+
${context.openTasks.slice(0, 5).map((t) => `- ${t.title}`).join("\n") || "None"}
|
|
4745
5361
|
|
|
4746
5362
|
${config.focusAreas.length > 0 ? `**Focus Areas (user-specified):**
|
|
4747
5363
|
${config.focusAreas.map((a) => `- ${a}`).join("\n")}` : ""}
|
|
@@ -4752,11 +5368,9 @@ ${crawlResults.length > 0 ? crawlSummary : "Live crawl was not performed (app ma
|
|
|
4752
5368
|
|
|
4753
5369
|
## Prior Findings Context
|
|
4754
5370
|
|
|
4755
|
-
${dismissedFindings.length > 0 ? `**Previously Dismissed (do NOT re-suggest
|
|
4756
|
-
${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."}
|
|
4757
5372
|
|
|
4758
|
-
${promotedFindings.length > 0 ? `**Already Being Worked On (
|
|
4759
|
-
${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."}
|
|
4760
5374
|
|
|
4761
5375
|
## Instructions
|
|
4762
5376
|
|
|
@@ -4944,7 +5558,7 @@ async function fetchScanContext(opts) {
|
|
|
4944
5558
|
};
|
|
4945
5559
|
}
|
|
4946
5560
|
function runClaude(prompt2) {
|
|
4947
|
-
return new Promise((
|
|
5561
|
+
return new Promise((resolve8, reject) => {
|
|
4948
5562
|
const child = spawn7("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
4949
5563
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4950
5564
|
});
|
|
@@ -4957,7 +5571,7 @@ function runClaude(prompt2) {
|
|
|
4957
5571
|
errOutput += d.toString();
|
|
4958
5572
|
});
|
|
4959
5573
|
child.on("exit", (code) => {
|
|
4960
|
-
if (code === 0)
|
|
5574
|
+
if (code === 0) resolve8(output.trim());
|
|
4961
5575
|
else reject(new Error(`claude exited with code ${code}
|
|
4962
5576
|
${errOutput.trim()}`));
|
|
4963
5577
|
});
|
|
@@ -5012,25 +5626,25 @@ var c8 = {
|
|
|
5012
5626
|
magenta: "\x1B[35m",
|
|
5013
5627
|
gray: "\x1B[90m"
|
|
5014
5628
|
};
|
|
5015
|
-
function
|
|
5629
|
+
function paint8(color, text) {
|
|
5016
5630
|
return `${c8[color]}${text}${c8.reset}`;
|
|
5017
5631
|
}
|
|
5018
5632
|
function timestamp2() {
|
|
5019
|
-
return
|
|
5633
|
+
return paint8("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
|
|
5020
5634
|
}
|
|
5021
5635
|
function scanTag() {
|
|
5022
|
-
return
|
|
5636
|
+
return paint8("magenta", "[scan]");
|
|
5023
5637
|
}
|
|
5024
5638
|
function log(msg) {
|
|
5025
5639
|
console.log(`${timestamp2()} ${scanTag()} ${msg}`);
|
|
5026
5640
|
}
|
|
5027
5641
|
function logOk(msg) {
|
|
5028
|
-
console.log(`${timestamp2()} ${scanTag()} ${
|
|
5642
|
+
console.log(`${timestamp2()} ${scanTag()} ${paint8("green", "\u2713")} ${msg}`);
|
|
5029
5643
|
}
|
|
5030
5644
|
function logErr(msg) {
|
|
5031
|
-
console.error(`${timestamp2()} ${scanTag()} ${
|
|
5645
|
+
console.error(`${timestamp2()} ${scanTag()} ${paint8("red", "\u2717")} ${msg}`);
|
|
5032
5646
|
}
|
|
5033
|
-
var scanCommand = new
|
|
5647
|
+
var scanCommand = new Command23("scan").description("Run a product scan on the current project \u2014 analyzes codebase, crawls the app, and surfaces findings").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing scan report ID (created by UI trigger)").option("--no-crawl", "Skip live crawl (codebase analysis only)").action(async (opts) => {
|
|
5034
5648
|
const config = loadConfig();
|
|
5035
5649
|
if (!config.apiKey) {
|
|
5036
5650
|
logErr('Not authenticated. Run "mr login" first.');
|
|
@@ -5038,11 +5652,11 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
|
|
|
5038
5652
|
}
|
|
5039
5653
|
const banner = [
|
|
5040
5654
|
``,
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5655
|
+
paint8("magenta", ` \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2557\u2554`),
|
|
5656
|
+
paint8("magenta", ` \u255A\u2550\u2557\u2551 \u2560\u2550\u2563\u2551\u2551\u2551`),
|
|
5657
|
+
paint8("magenta", ` \u255A\u2550\u255D\u255A\u2550\u255D\u2569 \u2569\u255D\u255A\u255D`),
|
|
5658
|
+
paint8("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
5659
|
+
paint8("dim", ` autonomous product scanner`),
|
|
5046
5660
|
``
|
|
5047
5661
|
].join("\n");
|
|
5048
5662
|
console.log(banner);
|
|
@@ -5058,7 +5672,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
|
|
|
5058
5672
|
logErr(`Failed to fetch project ${projectId}`);
|
|
5059
5673
|
process.exit(1);
|
|
5060
5674
|
}
|
|
5061
|
-
log(`Scanning project: ${
|
|
5675
|
+
log(`Scanning project: ${paint8("cyan", project.name)}`);
|
|
5062
5676
|
let projectPath = project.localPath;
|
|
5063
5677
|
if (!projectPath) {
|
|
5064
5678
|
for (const [dir, pid] of Object.entries(config.directories)) {
|
|
@@ -5074,7 +5688,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
|
|
|
5074
5688
|
let reportId;
|
|
5075
5689
|
if (opts.report) {
|
|
5076
5690
|
reportId = opts.report;
|
|
5077
|
-
log(`Using existing scan report ${
|
|
5691
|
+
log(`Using existing scan report ${paint8("yellow", reportId.slice(0, 8))}`);
|
|
5078
5692
|
} else {
|
|
5079
5693
|
try {
|
|
5080
5694
|
const scans = await api.get(`/api/scans?projectId=${projectId}&status=processing`);
|
|
@@ -5090,7 +5704,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
|
|
|
5090
5704
|
status: "pending"
|
|
5091
5705
|
});
|
|
5092
5706
|
reportId = report.id;
|
|
5093
|
-
log(`Created scan report ${
|
|
5707
|
+
log(`Created scan report ${paint8("yellow", reportId.slice(0, 8))}`);
|
|
5094
5708
|
} catch (err) {
|
|
5095
5709
|
logErr(`Failed to create scan report: ${err.message}`);
|
|
5096
5710
|
process.exit(1);
|
|
@@ -5103,7 +5717,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
|
|
|
5103
5717
|
try {
|
|
5104
5718
|
const current = await api.get(`/api/scans/${reportId}`);
|
|
5105
5719
|
if (current.status === "cancelled") {
|
|
5106
|
-
log(
|
|
5720
|
+
log(paint8("yellow", "Scan was cancelled \u2014 aborting."));
|
|
5107
5721
|
process.exit(0);
|
|
5108
5722
|
}
|
|
5109
5723
|
} catch {
|
|
@@ -5119,7 +5733,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
|
|
|
5119
5733
|
runBrowse: runBrowseCommand2,
|
|
5120
5734
|
onLog: log,
|
|
5121
5735
|
onProgress: (phase, detail) => {
|
|
5122
|
-
log(`${
|
|
5736
|
+
log(`${paint8("dim", `[${phase}]`)} ${detail}`);
|
|
5123
5737
|
}
|
|
5124
5738
|
});
|
|
5125
5739
|
let wasCancelled = false;
|
|
@@ -5131,7 +5745,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
|
|
|
5131
5745
|
} catch {
|
|
5132
5746
|
}
|
|
5133
5747
|
if (wasCancelled) {
|
|
5134
|
-
log(
|
|
5748
|
+
log(paint8("yellow", "Scan was cancelled by user \u2014 discarding results."));
|
|
5135
5749
|
process.exit(0);
|
|
5136
5750
|
}
|
|
5137
5751
|
await api.patch(`/api/scans/${reportId}`, {
|
|
@@ -5142,32 +5756,32 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
|
|
|
5142
5756
|
scanDurationMs: result.scanDurationMs,
|
|
5143
5757
|
routesCrawled: result.routesCrawled
|
|
5144
5758
|
});
|
|
5145
|
-
logOk(`Scan complete \u2014 ${
|
|
5759
|
+
logOk(`Scan complete \u2014 ${paint8("cyan", String(result.findings.length))} findings`);
|
|
5146
5760
|
console.log("");
|
|
5147
|
-
console.log(` ${
|
|
5761
|
+
console.log(` ${paint8("bold", "Summary:")} ${result.summary}`);
|
|
5148
5762
|
console.log("");
|
|
5149
5763
|
const high = result.findings.filter((f) => f.priority === "high");
|
|
5150
5764
|
const medium = result.findings.filter((f) => f.priority === "medium");
|
|
5151
5765
|
const low = result.findings.filter((f) => f.priority === "low");
|
|
5152
5766
|
if (high.length > 0) {
|
|
5153
|
-
console.log(` ${
|
|
5767
|
+
console.log(` ${paint8("bold", paint8("red", `High Priority (${high.length})`))}`);
|
|
5154
5768
|
for (const f of high) {
|
|
5155
|
-
console.log(` ${
|
|
5156
|
-
console.log(` ${
|
|
5769
|
+
console.log(` ${paint8("red", "\u25CF")} [${f.type}] ${f.title}`);
|
|
5770
|
+
console.log(` ${paint8("dim", f.description.slice(0, 120))}`);
|
|
5157
5771
|
}
|
|
5158
5772
|
console.log("");
|
|
5159
5773
|
}
|
|
5160
5774
|
if (medium.length > 0) {
|
|
5161
|
-
console.log(` ${
|
|
5775
|
+
console.log(` ${paint8("bold", paint8("yellow", `Medium Priority (${medium.length})`))}`);
|
|
5162
5776
|
for (const f of medium) {
|
|
5163
|
-
console.log(` ${
|
|
5777
|
+
console.log(` ${paint8("yellow", "\u25CF")} [${f.type}] ${f.title}`);
|
|
5164
5778
|
}
|
|
5165
5779
|
console.log("");
|
|
5166
5780
|
}
|
|
5167
5781
|
if (low.length > 0) {
|
|
5168
|
-
console.log(` ${
|
|
5782
|
+
console.log(` ${paint8("dim", `Low Priority (${low.length})`)} `);
|
|
5169
5783
|
for (const f of low) {
|
|
5170
|
-
console.log(` ${
|
|
5784
|
+
console.log(` ${paint8("dim", `\u25CB [${f.type}] ${f.title}`)}`);
|
|
5171
5785
|
}
|
|
5172
5786
|
console.log("");
|
|
5173
5787
|
}
|
|
@@ -5185,7 +5799,7 @@ var scanCommand = new Command24("scan").description("Run a product scan on the c
|
|
|
5185
5799
|
});
|
|
5186
5800
|
|
|
5187
5801
|
// cli/commands/idea.ts
|
|
5188
|
-
import { Command as
|
|
5802
|
+
import { Command as Command24 } from "commander";
|
|
5189
5803
|
var c9 = {
|
|
5190
5804
|
reset: "\x1B[0m",
|
|
5191
5805
|
bold: "\x1B[1m",
|
|
@@ -5198,27 +5812,27 @@ var c9 = {
|
|
|
5198
5812
|
gray: "\x1B[90m",
|
|
5199
5813
|
magenta: "\x1B[35m"
|
|
5200
5814
|
};
|
|
5201
|
-
function
|
|
5815
|
+
function paint9(color, text) {
|
|
5202
5816
|
return `${c9[color]}${text}${c9.reset}`;
|
|
5203
5817
|
}
|
|
5204
5818
|
function statusBadge2(status) {
|
|
5205
5819
|
switch (status) {
|
|
5206
5820
|
case "draft":
|
|
5207
|
-
return
|
|
5821
|
+
return paint9("gray", "\u25CB draft");
|
|
5208
5822
|
case "generating":
|
|
5209
|
-
return
|
|
5823
|
+
return paint9("cyan", "\u27F3 generating");
|
|
5210
5824
|
case "generated":
|
|
5211
|
-
return
|
|
5825
|
+
return paint9("green", "\u2713 generated");
|
|
5212
5826
|
case "promoted":
|
|
5213
|
-
return
|
|
5827
|
+
return paint9("magenta", "\u2191 promoted");
|
|
5214
5828
|
case "archived":
|
|
5215
|
-
return
|
|
5829
|
+
return paint9("dim", "\u2298 archived");
|
|
5216
5830
|
default:
|
|
5217
|
-
return
|
|
5831
|
+
return paint9("gray", status);
|
|
5218
5832
|
}
|
|
5219
5833
|
}
|
|
5220
|
-
var ideaCommand = new
|
|
5221
|
-
new
|
|
5834
|
+
var ideaCommand = new Command24("idea").description("Manage ideas \u2014 brainstorm, generate prototypes & plans").addCommand(
|
|
5835
|
+
new Command24("list").description("List ideas for the linked project").option("--all", "Show ideas for all projects").option("--status <status>", "Filter by status").action(async (opts) => {
|
|
5222
5836
|
const params = new URLSearchParams();
|
|
5223
5837
|
if (!opts.all) {
|
|
5224
5838
|
const projectId = getLinkedProjectId();
|
|
@@ -5229,23 +5843,23 @@ var ideaCommand = new Command25("idea").description("Manage ideas \u2014 brainst
|
|
|
5229
5843
|
if (opts.status) params.set("status", opts.status);
|
|
5230
5844
|
const ideas = await api.get(`/api/ideas?${params.toString()}`);
|
|
5231
5845
|
if (ideas.length === 0) {
|
|
5232
|
-
console.log(
|
|
5846
|
+
console.log(paint9("gray", "No ideas found."));
|
|
5233
5847
|
return;
|
|
5234
5848
|
}
|
|
5235
5849
|
console.log();
|
|
5236
5850
|
for (const idea of ideas) {
|
|
5237
5851
|
const date = new Date(idea.createdAt).toLocaleDateString();
|
|
5238
5852
|
console.log(
|
|
5239
|
-
` ${
|
|
5853
|
+
` ${paint9("bold", idea.title)} ${statusBadge2(idea.status)} ${paint9("gray", idea.id.slice(0, 8))} ${paint9("dim", date)}`
|
|
5240
5854
|
);
|
|
5241
5855
|
if (idea.description) {
|
|
5242
|
-
console.log(` ${
|
|
5856
|
+
console.log(` ${paint9("dim", idea.description.slice(0, 80) + (idea.description.length > 80 ? "\u2026" : ""))}`);
|
|
5243
5857
|
}
|
|
5244
5858
|
console.log();
|
|
5245
5859
|
}
|
|
5246
5860
|
})
|
|
5247
5861
|
).addCommand(
|
|
5248
|
-
new
|
|
5862
|
+
new Command24("create").description("Create a new idea").argument("<title>", "Title of the idea").option("--description <desc>", "Description of the idea").option("--project <projectId>", "Project ID (defaults to linked project)").option("--generate", "Immediately start generating plan & prototype").action(async (title, opts) => {
|
|
5249
5863
|
const projectId = opts.project ?? getLinkedProjectId() ?? null;
|
|
5250
5864
|
const idea = await api.post("/api/ideas", {
|
|
5251
5865
|
title,
|
|
@@ -5253,59 +5867,59 @@ var ideaCommand = new Command25("idea").description("Manage ideas \u2014 brainst
|
|
|
5253
5867
|
projectId
|
|
5254
5868
|
});
|
|
5255
5869
|
console.log();
|
|
5256
|
-
console.log(` ${
|
|
5257
|
-
console.log(` ${
|
|
5870
|
+
console.log(` ${paint9("green", "\u2713")} Created idea: ${paint9("bold", idea.title)}`);
|
|
5871
|
+
console.log(` ${paint9("gray", "ID:")} ${idea.id}`);
|
|
5258
5872
|
if (opts.generate) {
|
|
5259
5873
|
await api.post(`/api/ideas/${idea.id}/generate`);
|
|
5260
|
-
console.log(` ${
|
|
5874
|
+
console.log(` ${paint9("cyan", "\u27F3")} Generation will begin automatically via the watch agent.`);
|
|
5261
5875
|
}
|
|
5262
5876
|
console.log();
|
|
5263
5877
|
})
|
|
5264
5878
|
).addCommand(
|
|
5265
|
-
new
|
|
5879
|
+
new Command24("generate").description("Start generating plan & prototype for an idea").argument("<id>", "Idea ID").action(async (id) => {
|
|
5266
5880
|
const idea = await api.post(`/api/ideas/${id}/generate`);
|
|
5267
5881
|
console.log();
|
|
5268
|
-
console.log(` ${
|
|
5269
|
-
console.log(` ${
|
|
5882
|
+
console.log(` ${paint9("cyan", "\u27F3")} Generating: ${paint9("bold", idea.title)}`);
|
|
5883
|
+
console.log(` ${paint9("gray", "The watch agent will pick this up shortly.")}`);
|
|
5270
5884
|
console.log();
|
|
5271
5885
|
})
|
|
5272
5886
|
).addCommand(
|
|
5273
|
-
new
|
|
5887
|
+
new Command24("feedback").description("Send feedback to iterate on an idea's generated content").argument("<id>", "Idea ID").argument("<feedback>", "Feedback text").action(async (id, feedback) => {
|
|
5274
5888
|
const idea = await api.post(`/api/ideas/${id}/feedback`, { feedback });
|
|
5275
5889
|
console.log();
|
|
5276
|
-
console.log(` ${
|
|
5277
|
-
console.log(` ${
|
|
5890
|
+
console.log(` ${paint9("cyan", "\u27F3")} Feedback sent for: ${paint9("bold", idea.title)}`);
|
|
5891
|
+
console.log(` ${paint9("gray", "The watch agent will re-generate with your feedback.")}`);
|
|
5278
5892
|
console.log();
|
|
5279
5893
|
})
|
|
5280
5894
|
).addCommand(
|
|
5281
|
-
new
|
|
5895
|
+
new Command24("promote").description("Promote an idea to a task").argument("<id>", "Idea ID").action(async (id) => {
|
|
5282
5896
|
const result = await api.post(`/api/ideas/${id}/promote`);
|
|
5283
5897
|
console.log();
|
|
5284
|
-
console.log(` ${
|
|
5285
|
-
console.log(` ${
|
|
5898
|
+
console.log(` ${paint9("green", "\u2713")} Promoted idea to task: ${paint9("bold", result.task.title)}`);
|
|
5899
|
+
console.log(` ${paint9("gray", "Task ID:")} ${result.task.id}`);
|
|
5286
5900
|
console.log();
|
|
5287
5901
|
})
|
|
5288
5902
|
).addCommand(
|
|
5289
|
-
new
|
|
5903
|
+
new Command24("spin-up").description("Spin up a new project with a GitHub repo from an idea").argument("<id>", "Idea ID").option("--name <name>", "Custom project name (defaults to idea title)").action(async (id, opts) => {
|
|
5290
5904
|
const body = {};
|
|
5291
5905
|
if (opts.name) body.name = opts.name;
|
|
5292
5906
|
const result = await api.post(`/api/ideas/${id}/spin-up`, body);
|
|
5293
5907
|
console.log();
|
|
5294
|
-
console.log(` ${
|
|
5295
|
-
console.log(` ${
|
|
5908
|
+
console.log(` ${paint9("green", "\u2713")} Spinning up project: ${paint9("bold", result.project.name)}`);
|
|
5909
|
+
console.log(` ${paint9("gray", "Project ID:")} ${result.project.id}`);
|
|
5296
5910
|
if (result.tasks && result.tasks.length > 0) {
|
|
5297
|
-
console.log(` ${
|
|
5911
|
+
console.log(` ${paint9("green", "\u2713")} Created ${result.tasks.length} follow-up task(s):`);
|
|
5298
5912
|
for (const task of result.tasks) {
|
|
5299
|
-
console.log(` ${
|
|
5913
|
+
console.log(` ${paint9("gray", "\u2022")} ${task.title}`);
|
|
5300
5914
|
}
|
|
5301
5915
|
}
|
|
5302
|
-
console.log(` ${
|
|
5916
|
+
console.log(` ${paint9("cyan", "\u27F3")} Repo creation is queued \u2014 the watch daemon will pick it up.`);
|
|
5303
5917
|
console.log();
|
|
5304
5918
|
})
|
|
5305
5919
|
);
|
|
5306
5920
|
|
|
5307
5921
|
// cli/commands/doctor.ts
|
|
5308
|
-
import { Command as
|
|
5922
|
+
import { Command as Command25 } from "commander";
|
|
5309
5923
|
import { existsSync as existsSync15 } from "fs";
|
|
5310
5924
|
import { homedir as homedir2 } from "os";
|
|
5311
5925
|
import { join as join11 } from "path";
|
|
@@ -5353,7 +5967,7 @@ async function checkProjectLink() {
|
|
|
5353
5967
|
optional: true
|
|
5354
5968
|
};
|
|
5355
5969
|
}
|
|
5356
|
-
var doctorCommand = new
|
|
5970
|
+
var doctorCommand = new Command25("doctor").description("Diagnose Mr. Manager CLI installation and environment").action(async () => {
|
|
5357
5971
|
const banner = [
|
|
5358
5972
|
``,
|
|
5359
5973
|
paint5("cyan", ` MR DOCTOR`),
|
|
@@ -5395,9 +6009,229 @@ var doctorCommand = new Command26("doctor").description("Diagnose Mr. Manager CL
|
|
|
5395
6009
|
process.exit(1);
|
|
5396
6010
|
});
|
|
5397
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
|
+
|
|
5398
6232
|
// cli/index.ts
|
|
5399
6233
|
var configPath = join12(homedir3(), ".mr-manager", "config.json");
|
|
5400
|
-
var isFirstRun = !
|
|
6234
|
+
var isFirstRun = !existsSync17(configPath);
|
|
5401
6235
|
var userArgs = process.argv.slice(2);
|
|
5402
6236
|
var bypassCommands = /* @__PURE__ */ new Set(["login", "init", "auth", "help", "--help", "-h", "--version", "-V", "doctor", "setup"]);
|
|
5403
6237
|
var shouldBypass = userArgs.length > 0 && bypassCommands.has(userArgs[0]);
|
|
@@ -5460,8 +6294,8 @@ program.addCommand(setPathCommand);
|
|
|
5460
6294
|
program.addCommand(testCommand);
|
|
5461
6295
|
program.addCommand(featuresCommand);
|
|
5462
6296
|
program.addCommand(noMrCommand);
|
|
5463
|
-
program.addCommand(mobileCommand);
|
|
5464
6297
|
program.addCommand(scanCommand);
|
|
5465
6298
|
program.addCommand(ideaCommand);
|
|
5466
6299
|
program.addCommand(doctorCommand);
|
|
6300
|
+
program.addCommand(promptAuditCommand);
|
|
5467
6301
|
program.parse();
|