@dunnewold-labs/mr-manager 0.4.12 → 0.4.16
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 +419 -55
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -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.16",
|
|
189
189
|
description: "Mr. Manager - Task and project management CLI",
|
|
190
190
|
bin: {
|
|
191
191
|
mr: "./dist/index.mjs"
|
|
@@ -250,6 +250,45 @@ var ApiError = class extends Error {
|
|
|
250
250
|
this.name = "ApiError";
|
|
251
251
|
}
|
|
252
252
|
};
|
|
253
|
+
var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
254
|
+
"ECONNABORTED",
|
|
255
|
+
"ECONNREFUSED",
|
|
256
|
+
"ECONNRESET",
|
|
257
|
+
"EHOSTUNREACH",
|
|
258
|
+
"ENETDOWN",
|
|
259
|
+
"ENETUNREACH",
|
|
260
|
+
"ENOTFOUND",
|
|
261
|
+
"EAI_AGAIN",
|
|
262
|
+
"ETIMEDOUT"
|
|
263
|
+
]);
|
|
264
|
+
var NETWORK_ERROR_PATTERNS = [
|
|
265
|
+
"fetch failed",
|
|
266
|
+
"network",
|
|
267
|
+
"offline",
|
|
268
|
+
"socket hang up",
|
|
269
|
+
"timed out",
|
|
270
|
+
"timeout",
|
|
271
|
+
"connection refused",
|
|
272
|
+
"connection reset",
|
|
273
|
+
"host is down",
|
|
274
|
+
"host unreachable",
|
|
275
|
+
"internet disconnected"
|
|
276
|
+
];
|
|
277
|
+
function isLikelyNetworkError(err) {
|
|
278
|
+
if (!err || typeof err !== "object") return false;
|
|
279
|
+
if (err instanceof ApiError) return false;
|
|
280
|
+
const error = err;
|
|
281
|
+
const directCode = typeof error.code === "string" ? error.code.toUpperCase() : null;
|
|
282
|
+
const causeCode = typeof error.cause?.code === "string" ? error.cause.code.toUpperCase() : null;
|
|
283
|
+
if (directCode && NETWORK_ERROR_CODES.has(directCode) || causeCode && NETWORK_ERROR_CODES.has(causeCode)) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
const combinedMessage = [
|
|
287
|
+
typeof error.message === "string" ? error.message : "",
|
|
288
|
+
typeof error.cause?.message === "string" ? error.cause.message : ""
|
|
289
|
+
].join(" ").toLowerCase();
|
|
290
|
+
return NETWORK_ERROR_PATTERNS.some((pattern) => combinedMessage.includes(pattern));
|
|
291
|
+
}
|
|
253
292
|
async function request(method, path, body) {
|
|
254
293
|
const config = loadConfig();
|
|
255
294
|
if (!config.apiKey) {
|
|
@@ -492,7 +531,7 @@ import { Command as Command8 } from "commander";
|
|
|
492
531
|
import { spawn as spawn4, exec } from "child_process";
|
|
493
532
|
import { randomUUID } from "crypto";
|
|
494
533
|
import { resolve as resolve2 } from "path";
|
|
495
|
-
import { readFileSync as readFileSync5, readdirSync, unlinkSync, existsSync as existsSync7, statSync, writeFileSync as
|
|
534
|
+
import { readFileSync as readFileSync5, readdirSync, unlinkSync, existsSync as existsSync7, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
496
535
|
import * as readline from "readline";
|
|
497
536
|
|
|
498
537
|
// lib/test-runner.ts
|
|
@@ -557,10 +596,13 @@ function findExistingWorktreeForBranch(repoDir, branch) {
|
|
|
557
596
|
return null;
|
|
558
597
|
}
|
|
559
598
|
}
|
|
560
|
-
function createWorktree(repoDir, branch, worktreeName) {
|
|
599
|
+
function createWorktree(repoDir, branch, worktreeName, options = {}) {
|
|
561
600
|
const wtPath = join4(repoDir, ".mr-worktrees", worktreeName);
|
|
562
601
|
if (existsSync4(wtPath)) {
|
|
563
|
-
execSync2(`git checkout ${branch}`, { cwd: wtPath, stdio: "pipe" });
|
|
602
|
+
execSync2(`git checkout "${branch}"`, { cwd: wtPath, stdio: "pipe" });
|
|
603
|
+
if (options.syncRemoteBranch) {
|
|
604
|
+
syncBranchWithRemote(repoDir, wtPath, branch);
|
|
605
|
+
}
|
|
564
606
|
return {
|
|
565
607
|
path: wtPath,
|
|
566
608
|
created: false,
|
|
@@ -569,6 +611,9 @@ function createWorktree(repoDir, branch, worktreeName) {
|
|
|
569
611
|
}
|
|
570
612
|
const existingBranchWorktree = findExistingWorktreeForBranch(repoDir, branch);
|
|
571
613
|
if (existingBranchWorktree) {
|
|
614
|
+
if (options.syncRemoteBranch) {
|
|
615
|
+
syncBranchWithRemote(repoDir, existingBranchWorktree, branch);
|
|
616
|
+
}
|
|
572
617
|
return {
|
|
573
618
|
path: existingBranchWorktree,
|
|
574
619
|
created: false,
|
|
@@ -613,6 +658,18 @@ function createWorktree(repoDir, branch, worktreeName) {
|
|
|
613
658
|
reusedBranchWorktree: false
|
|
614
659
|
};
|
|
615
660
|
}
|
|
661
|
+
function syncBranchWithRemote(repoDir, wtPath, branch) {
|
|
662
|
+
tryExec(`git fetch origin "${branch}"`, repoDir);
|
|
663
|
+
const hasRemoteBranch = tryExec(`git rev-parse --verify "origin/${branch}"`, repoDir);
|
|
664
|
+
if (!hasRemoteBranch) return;
|
|
665
|
+
try {
|
|
666
|
+
execSync2(`git pull --ff-only origin "${branch}"`, {
|
|
667
|
+
cwd: wtPath,
|
|
668
|
+
stdio: "pipe"
|
|
669
|
+
});
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
}
|
|
616
673
|
function removeWorktree(repoDir, wtPath) {
|
|
617
674
|
try {
|
|
618
675
|
execSync2(`git worktree remove --force "${wtPath}"`, {
|
|
@@ -819,7 +876,9 @@ async function runTest(options) {
|
|
|
819
876
|
uploadScreenshot: uploadScreenshot2,
|
|
820
877
|
uploadVideo,
|
|
821
878
|
postUpdate,
|
|
822
|
-
onProgress
|
|
879
|
+
onProgress,
|
|
880
|
+
recordingEnabled = true,
|
|
881
|
+
recordingContext = "test-run"
|
|
823
882
|
} = options;
|
|
824
883
|
const log2 = onProgress || (() => {
|
|
825
884
|
});
|
|
@@ -828,7 +887,11 @@ async function runTest(options) {
|
|
|
828
887
|
assertions: [],
|
|
829
888
|
screenshots: [],
|
|
830
889
|
errors: [],
|
|
831
|
-
summary: ""
|
|
890
|
+
summary: "",
|
|
891
|
+
proof: {
|
|
892
|
+
state: recordingEnabled ? "proof_missing_capture_failed" : "proof_missing_disabled",
|
|
893
|
+
details: recordingEnabled ? "Proof recording did not complete." : "Proof recording disabled for this run."
|
|
894
|
+
}
|
|
832
895
|
};
|
|
833
896
|
let devProc = null;
|
|
834
897
|
let wtPath = null;
|
|
@@ -858,6 +921,29 @@ async function runTest(options) {
|
|
|
858
921
|
devProc = await startDevServer(wtPath, port);
|
|
859
922
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
860
923
|
log2(`Dev server running on ${baseUrl}`);
|
|
924
|
+
let recordingStarted = false;
|
|
925
|
+
if (recordingEnabled) {
|
|
926
|
+
try {
|
|
927
|
+
const recordingStart = await browseRunner(["recording-start"]);
|
|
928
|
+
if (recordingStart.exitCode !== 0) {
|
|
929
|
+
throw new Error(recordingStart.stdout || "recording-start failed");
|
|
930
|
+
}
|
|
931
|
+
recordingStarted = true;
|
|
932
|
+
log2("Proof recording started");
|
|
933
|
+
} catch (err) {
|
|
934
|
+
result.proof = {
|
|
935
|
+
state: "proof_missing_capture_failed",
|
|
936
|
+
details: `Proof recording could not start: ${err.message}`
|
|
937
|
+
};
|
|
938
|
+
log2(result.proof.details);
|
|
939
|
+
}
|
|
940
|
+
} else {
|
|
941
|
+
result.proof = {
|
|
942
|
+
state: "proof_missing_disabled",
|
|
943
|
+
details: "Proof recording disabled for this run."
|
|
944
|
+
};
|
|
945
|
+
log2(result.proof.details);
|
|
946
|
+
}
|
|
861
947
|
await browseRunner(["goto", baseUrl]);
|
|
862
948
|
const plan = customPlan || buildDefaultTestPlan(baseUrl);
|
|
863
949
|
log2(`Executing ${plan.length}-step test plan...`);
|
|
@@ -917,6 +1003,62 @@ async function runTest(options) {
|
|
|
917
1003
|
log2(`Step ${i + 1} error: ${err.message}`);
|
|
918
1004
|
}
|
|
919
1005
|
}
|
|
1006
|
+
if (recordingStarted) {
|
|
1007
|
+
const recordingPath = `/tmp/mr-proof-${taskId.slice(0, 8)}-${Date.now()}.webm`;
|
|
1008
|
+
try {
|
|
1009
|
+
const recordingStop = await browseRunner(["recording-stop", recordingPath]);
|
|
1010
|
+
const savedPath = recordingStop.stdout.match(/Recording saved: (.+)/)?.[1]?.trim();
|
|
1011
|
+
if (!savedPath) {
|
|
1012
|
+
throw new Error(recordingStop.stdout || "recording-stop did not return a file path");
|
|
1013
|
+
}
|
|
1014
|
+
result.proof.localPath = savedPath;
|
|
1015
|
+
log2(`Proof recording finalized at ${savedPath}`);
|
|
1016
|
+
if (uploadVideo) {
|
|
1017
|
+
const videoUrl = await uploadVideo(savedPath);
|
|
1018
|
+
if (videoUrl) {
|
|
1019
|
+
result.videoUrl = videoUrl;
|
|
1020
|
+
result.proof = {
|
|
1021
|
+
state: "proof_attached",
|
|
1022
|
+
details: result.status === "passed" ? "Proof recording attached." : "Proof recording attached for the failed run.",
|
|
1023
|
+
localPath: savedPath,
|
|
1024
|
+
videoUrl,
|
|
1025
|
+
media: [
|
|
1026
|
+
{
|
|
1027
|
+
kind: "recording",
|
|
1028
|
+
url: videoUrl,
|
|
1029
|
+
mimeType: "video/webm",
|
|
1030
|
+
label: "Proof recording",
|
|
1031
|
+
captureContext: recordingContext,
|
|
1032
|
+
uploadState: "uploaded",
|
|
1033
|
+
isCanonical: true
|
|
1034
|
+
}
|
|
1035
|
+
]
|
|
1036
|
+
};
|
|
1037
|
+
log2(`Proof recording uploaded to ${videoUrl}`);
|
|
1038
|
+
} else {
|
|
1039
|
+
result.proof = {
|
|
1040
|
+
state: "proof_missing_upload_failed",
|
|
1041
|
+
details: "Proof recording captured, but upload failed.",
|
|
1042
|
+
localPath: savedPath
|
|
1043
|
+
};
|
|
1044
|
+
log2(result.proof.details);
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
result.proof = {
|
|
1048
|
+
state: "proof_missing_upload_failed",
|
|
1049
|
+
details: "Proof recording captured, but no upload handler was configured.",
|
|
1050
|
+
localPath: savedPath
|
|
1051
|
+
};
|
|
1052
|
+
log2(result.proof.details);
|
|
1053
|
+
}
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
result.proof = {
|
|
1056
|
+
state: "proof_missing_capture_failed",
|
|
1057
|
+
details: `Proof recording could not be finalized: ${err.message}`
|
|
1058
|
+
};
|
|
1059
|
+
log2(result.proof.details);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
920
1062
|
const totalAssertions = result.assertions.length;
|
|
921
1063
|
const passedAssertions = result.assertions.filter((a) => a.passed).length;
|
|
922
1064
|
const failedSteps = result.assertions.filter((a) => !a.passed).map((a) => `step ${a.step + 1}`);
|
|
@@ -928,13 +1070,25 @@ async function runTest(options) {
|
|
|
928
1070
|
if (result.errors.length > 0) {
|
|
929
1071
|
result.summary += ` (${result.errors.length} errors)`;
|
|
930
1072
|
}
|
|
931
|
-
|
|
1073
|
+
if (result.proof.state === "proof_attached") {
|
|
1074
|
+
result.summary += result.status === "passed" ? " Proof recording attached." : " Proof recording attached for review.";
|
|
1075
|
+
} else {
|
|
1076
|
+
result.summary = result.status === "passed" ? result.summary.replace(/^Test passed/, "Test passed with warning") : result.summary.replace(/^Test failed/, "Test failed with warning");
|
|
1077
|
+
result.summary += ` ${result.proof.details}`;
|
|
1078
|
+
}
|
|
1079
|
+
await postUpdate({
|
|
1080
|
+
message: result.summary,
|
|
1081
|
+
imageUrl: result.videoUrl,
|
|
1082
|
+
media: result.proof.media,
|
|
1083
|
+
proofState: result.proof.state,
|
|
1084
|
+
proofDetails: result.proof.details
|
|
1085
|
+
});
|
|
932
1086
|
} catch (err) {
|
|
933
1087
|
result.status = "failed";
|
|
934
1088
|
result.summary = `Test failed: ${err.message}`;
|
|
935
1089
|
result.errors.push(err.message);
|
|
936
1090
|
try {
|
|
937
|
-
await postUpdate(`Test failed: ${err.message}`);
|
|
1091
|
+
await postUpdate({ message: `Test failed: ${err.message}` });
|
|
938
1092
|
} catch {
|
|
939
1093
|
}
|
|
940
1094
|
} finally {
|
|
@@ -1000,6 +1154,7 @@ var NON_CODE_TASK_PATTERNS = [
|
|
|
1000
1154
|
];
|
|
1001
1155
|
function taskLikelyDoesNotNeedCodeChanges(task) {
|
|
1002
1156
|
if (task.mode && task.mode !== "development") return true;
|
|
1157
|
+
if (task.mode === "development") return false;
|
|
1003
1158
|
const haystack = normalizeWhitespace(
|
|
1004
1159
|
[task.title, task.notes, task.prdContent].filter(Boolean).join(" ").toLowerCase()
|
|
1005
1160
|
);
|
|
@@ -1008,7 +1163,7 @@ function taskLikelyDoesNotNeedCodeChanges(task) {
|
|
|
1008
1163
|
}
|
|
1009
1164
|
|
|
1010
1165
|
// cli/browse-runner.ts
|
|
1011
|
-
import { execSync as
|
|
1166
|
+
import { execSync as execSync3, spawn as spawn3 } from "child_process";
|
|
1012
1167
|
import { existsSync as existsSync6 } from "fs";
|
|
1013
1168
|
import { join as join6 } from "path";
|
|
1014
1169
|
var BROWSE_DIR = join6(import.meta.dirname, "..", "..", "browse");
|
|
@@ -1019,7 +1174,7 @@ function getBrowseRunner() {
|
|
|
1019
1174
|
return { cmd: BROWSE_BINARY, args: [] };
|
|
1020
1175
|
}
|
|
1021
1176
|
try {
|
|
1022
|
-
|
|
1177
|
+
execSync3("which bun", { stdio: "pipe" });
|
|
1023
1178
|
if (existsSync6(BROWSE_DEV_CMD)) {
|
|
1024
1179
|
return { cmd: "bun", args: ["run", BROWSE_DEV_CMD] };
|
|
1025
1180
|
}
|
|
@@ -1246,9 +1401,19 @@ function logDispatch(prefix, msg) {
|
|
|
1246
1401
|
function logSpinner(prefix, msg) {
|
|
1247
1402
|
console.log(`${timestamp()} ${prefix} ${paint("blue", "\u27F3")} ${msg}`);
|
|
1248
1403
|
}
|
|
1249
|
-
|
|
1404
|
+
function isLikelyNetworkFailureDetail(detail) {
|
|
1405
|
+
return isLikelyNetworkError(new Error(detail));
|
|
1406
|
+
}
|
|
1407
|
+
async function postTaskUpdate(taskId, payloadOrMessage, source = "system") {
|
|
1250
1408
|
try {
|
|
1251
|
-
|
|
1409
|
+
if (typeof payloadOrMessage === "string") {
|
|
1410
|
+
await api.post(`/api/tasks/${taskId}/updates`, { message: payloadOrMessage, source });
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
1414
|
+
...payloadOrMessage,
|
|
1415
|
+
source: payloadOrMessage.source || source
|
|
1416
|
+
});
|
|
1252
1417
|
} catch (err) {
|
|
1253
1418
|
logError(taskTag(shortId(taskId)), `Failed to post update: ${err.message}`);
|
|
1254
1419
|
}
|
|
@@ -1284,6 +1449,12 @@ function ownerPrefix(task) {
|
|
|
1284
1449
|
const first = name.split(/\s+/)[0].toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1285
1450
|
return first || "mr";
|
|
1286
1451
|
}
|
|
1452
|
+
function taskBranchName(task) {
|
|
1453
|
+
if (task.attachedBranch?.trim()) {
|
|
1454
|
+
return task.attachedBranch.trim();
|
|
1455
|
+
}
|
|
1456
|
+
return `${ownerPrefix(task)}/${slugify(task.title)}`;
|
|
1457
|
+
}
|
|
1287
1458
|
function formatElapsed(ms) {
|
|
1288
1459
|
const totalMinutes = Math.max(1, Math.floor(ms / 6e4));
|
|
1289
1460
|
const hours = Math.floor(totalMinutes / 60);
|
|
@@ -1467,7 +1638,7 @@ async function createPrInRepo(task, branchName, repoDir, vcs, subtasks, protoRef
|
|
|
1467
1638
|
}
|
|
1468
1639
|
const bodyPath = resolve2("/tmp", `mr-auto-pr-${randomUUID()}.md`);
|
|
1469
1640
|
const body = buildAutomaticPrBody(task, branchName, vcs, subtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
|
|
1470
|
-
|
|
1641
|
+
writeFileSync3(bodyPath, `${body}
|
|
1471
1642
|
`, "utf-8");
|
|
1472
1643
|
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
1644
|
try {
|
|
@@ -1614,7 +1785,7 @@ function buildPrototypeSection(protoRefs, workingDir) {
|
|
|
1614
1785
|
const safeProtoId = proto.id.slice(0, 8);
|
|
1615
1786
|
const tmpPath = resolve2(tmpDir, `.mr-proto-${safeProtoId}-v${selected[i] ?? i}.html`);
|
|
1616
1787
|
try {
|
|
1617
|
-
|
|
1788
|
+
writeFileSync3(tmpPath, file.content, "utf-8");
|
|
1618
1789
|
sections.push(`#### ${variantLabel}: ${file.name}`);
|
|
1619
1790
|
sections.push(`File: \`${tmpPath}\` \u2014 read this file to see the full HTML content.`);
|
|
1620
1791
|
} catch {
|
|
@@ -1720,13 +1891,15 @@ function buildFeaturesSection(repoDir) {
|
|
|
1720
1891
|
``
|
|
1721
1892
|
].join("\n");
|
|
1722
1893
|
}
|
|
1723
|
-
function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = [], executionDir, startWithoutWorktree = false) {
|
|
1894
|
+
function buildExecutionPrompt(task, repoDir, subtasks, vcs = "github", protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = [], executionDir, startWithoutWorktree = false, preparedBranchName) {
|
|
1724
1895
|
const slug = slugify(task.title);
|
|
1725
1896
|
const owner = ownerPrefix(task);
|
|
1726
|
-
const
|
|
1897
|
+
const generatedBranchName = `${owner}/${slug}`;
|
|
1898
|
+
const branchName = taskBranchName(task);
|
|
1727
1899
|
const wtPath = worktreePath(`${owner}-${slug}`);
|
|
1728
1900
|
const workingDir = executionDir ?? repoDir;
|
|
1729
1901
|
const prBodyPath = "/tmp/mr-pr-body.md";
|
|
1902
|
+
const hasAttachedBranch = !!task.attachedBranch?.trim();
|
|
1730
1903
|
const notes = task.prdContent ? `
|
|
1731
1904
|
|
|
1732
1905
|
## PRD (Product Requirements Document)
|
|
@@ -1765,10 +1938,11 @@ ${task.notes}` : "";
|
|
|
1765
1938
|
``,
|
|
1766
1939
|
`1. **Gather project context first.** Run \`mr context\` to get the project description, current task list, and the last 10 completed tasks. Use this to understand what's already been built, recent decisions, and the overall project direction before you start coding.`,
|
|
1767
1940
|
``,
|
|
1768
|
-
...
|
|
1769
|
-
`2. Your
|
|
1941
|
+
...preparedBranchName ? [
|
|
1942
|
+
`2. Your git working tree is already prepared and this session starts inside it.`,
|
|
1770
1943
|
` - Current working directory: ${executionDir}`,
|
|
1771
|
-
` - Branch checked out
|
|
1944
|
+
` - Branch checked out for this task: ${preparedBranchName}`,
|
|
1945
|
+
...hasAttachedBranch ? [` - This task is attached to an existing branch, so agent work should stay on that branch instead of creating ${generatedBranchName}.`] : [],
|
|
1772
1946
|
` - If the task spans additional repos, create matching worktrees there as needed.`
|
|
1773
1947
|
] : hasFeedback ? [
|
|
1774
1948
|
`2. Set up your working environment:`,
|
|
@@ -1843,7 +2017,7 @@ ${task.notes}` : "";
|
|
|
1843
2017
|
if (isRevision) {
|
|
1844
2018
|
const prdTmpPath = resolve2(repoDir, ".mr-existing-prd.md");
|
|
1845
2019
|
try {
|
|
1846
|
-
|
|
2020
|
+
writeFileSync3(prdTmpPath, existingPrd, "utf-8");
|
|
1847
2021
|
} catch {
|
|
1848
2022
|
}
|
|
1849
2023
|
const prdSummaryLines = [];
|
|
@@ -2065,7 +2239,7 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
|
|
|
2065
2239
|
const f = parentFiles[i];
|
|
2066
2240
|
const tmpPath = resolve2(repoDir, `.mr-parent-variant-${i + 1}.html`);
|
|
2067
2241
|
try {
|
|
2068
|
-
|
|
2242
|
+
writeFileSync3(tmpPath, f.content, "utf-8");
|
|
2069
2243
|
existingVariantLines.push(`### Previous Variant ${i + 1} (${f.name})`);
|
|
2070
2244
|
existingVariantLines.push(`File: \`${tmpPath}\` \u2014 read this file to see the full HTML content.`);
|
|
2071
2245
|
} catch {
|
|
@@ -2328,6 +2502,8 @@ var watchCommand = new Command8("watch").description(
|
|
|
2328
2502
|
const failed = /* @__PURE__ */ new Map();
|
|
2329
2503
|
const queued = /* @__PURE__ */ new Set();
|
|
2330
2504
|
const finishing = /* @__PURE__ */ new Set();
|
|
2505
|
+
const networkPaused = /* @__PURE__ */ new Map();
|
|
2506
|
+
let networkOffline = false;
|
|
2331
2507
|
let pollRunning = false;
|
|
2332
2508
|
const approvalQueue = [];
|
|
2333
2509
|
let approvalRunning = false;
|
|
@@ -2381,11 +2557,85 @@ var watchCommand = new Command8("watch").description(
|
|
|
2381
2557
|
} catch {
|
|
2382
2558
|
}
|
|
2383
2559
|
}
|
|
2560
|
+
async function isApiReachable() {
|
|
2561
|
+
try {
|
|
2562
|
+
await api.get("/api/tasks?status=queued");
|
|
2563
|
+
return true;
|
|
2564
|
+
} catch (err) {
|
|
2565
|
+
return !isLikelyNetworkError(err);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
function pauseTaskForNetwork(taskId, reason) {
|
|
2569
|
+
const activeEntry = active.get(taskId);
|
|
2570
|
+
if (networkPaused.has(taskId)) return;
|
|
2571
|
+
networkPaused.set(taskId, {
|
|
2572
|
+
pausedAt: Date.now(),
|
|
2573
|
+
reason,
|
|
2574
|
+
resumeSession: activeEntry?.currentAgent === "claude"
|
|
2575
|
+
});
|
|
2576
|
+
failed.delete(taskId);
|
|
2577
|
+
queued.delete(taskId);
|
|
2578
|
+
finishing.delete(taskId);
|
|
2579
|
+
if (activeEntry) {
|
|
2580
|
+
activeEntry.terminatedForError = true;
|
|
2581
|
+
activeEntry.process.kill("SIGTERM");
|
|
2582
|
+
active.delete(taskId);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
function handleNetworkLoss(reason) {
|
|
2586
|
+
if (!networkOffline) {
|
|
2587
|
+
networkOffline = true;
|
|
2588
|
+
logWarn(watchTag(), `Network unavailable \u2014 pausing active tasks until connectivity returns (${reason})`);
|
|
2589
|
+
}
|
|
2590
|
+
for (const taskId of active.keys()) {
|
|
2591
|
+
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "idea-", "test-"];
|
|
2592
|
+
if (nonTaskPrefixes.some((prefix) => taskId.startsWith(prefix))) continue;
|
|
2593
|
+
pauseTaskForNetwork(taskId, reason);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
async function resumeNetworkPausedTasks() {
|
|
2597
|
+
if (networkPaused.size === 0) {
|
|
2598
|
+
if (networkOffline) {
|
|
2599
|
+
networkOffline = false;
|
|
2600
|
+
logSuccess(watchTag(), "Network connection restored");
|
|
2601
|
+
}
|
|
2602
|
+
return true;
|
|
2603
|
+
}
|
|
2604
|
+
const pausedIds = [...networkPaused.keys()];
|
|
2605
|
+
for (const taskId of pausedIds) {
|
|
2606
|
+
try {
|
|
2607
|
+
const task = await api.get(`/api/tasks/${taskId}`);
|
|
2608
|
+
if (task.status === "delegated") {
|
|
2609
|
+
await api.patch(`/api/tasks/${taskId}`, { status: "queued" });
|
|
2610
|
+
}
|
|
2611
|
+
await postTaskUpdate(taskId, "Network connection restored \u2014 resuming task", "system");
|
|
2612
|
+
networkPaused.delete(taskId);
|
|
2613
|
+
} catch (err) {
|
|
2614
|
+
if (isLikelyNetworkError(err)) {
|
|
2615
|
+
handleNetworkLoss(err.message);
|
|
2616
|
+
return false;
|
|
2617
|
+
}
|
|
2618
|
+
logError(taskTag(shortId(taskId)), `Failed to resume paused task: ${err.message}`);
|
|
2619
|
+
networkPaused.delete(taskId);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
if (networkOffline) {
|
|
2623
|
+
networkOffline = false;
|
|
2624
|
+
logSuccess(watchTag(), "Network connection restored \u2014 paused tasks re-queued");
|
|
2625
|
+
}
|
|
2626
|
+
return true;
|
|
2627
|
+
}
|
|
2628
|
+
async function shouldPauseForNetwork(failureDetail) {
|
|
2629
|
+
if (networkOffline || isLikelyNetworkFailureDetail(failureDetail)) {
|
|
2630
|
+
return true;
|
|
2631
|
+
}
|
|
2632
|
+
return !await isApiReachable();
|
|
2633
|
+
}
|
|
2384
2634
|
async function dispatchTask(task, repoDir) {
|
|
2385
2635
|
const sid = shortId(task.id);
|
|
2386
2636
|
const slug = slugify(task.title);
|
|
2387
2637
|
const owner = ownerPrefix(task);
|
|
2388
|
-
const branchName =
|
|
2638
|
+
const branchName = taskBranchName(task);
|
|
2389
2639
|
const legacyBranchName = `mr/${sid}/${slug}`;
|
|
2390
2640
|
const prefix = taskTag(sid);
|
|
2391
2641
|
const vcs = detectVcs(repoDir)?.provider ?? "github";
|
|
@@ -2445,7 +2695,8 @@ var watchCommand = new Command8("watch").description(
|
|
|
2445
2695
|
const worktree = createWorktree(
|
|
2446
2696
|
repoDir,
|
|
2447
2697
|
branchName,
|
|
2448
|
-
worktreeNameFromPath(desiredWorktreePath)
|
|
2698
|
+
worktreeNameFromPath(desiredWorktreePath),
|
|
2699
|
+
{ syncRemoteBranch: !!task.attachedBranch?.trim() }
|
|
2449
2700
|
);
|
|
2450
2701
|
executionDir = worktree.path;
|
|
2451
2702
|
cleanupWorktreePath = worktree.created ? executionDir : void 0;
|
|
@@ -2454,7 +2705,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2454
2705
|
worktree.reusedBranchWorktree ? `Reusing existing worktree ${paint("cyan", executionDir)} for branch ${paint("dim", branchName)}` : `Prepared worktree ${paint("cyan", executionDir)}`
|
|
2455
2706
|
);
|
|
2456
2707
|
}
|
|
2457
|
-
const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs, executionDir, startWithoutWorktree);
|
|
2708
|
+
const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs, executionDir, startWithoutWorktree, !startWithoutWorktree && isGitRepo(repoDir) ? branchName : void 0);
|
|
2458
2709
|
const activeEntry = {
|
|
2459
2710
|
process: void 0,
|
|
2460
2711
|
title: task.title,
|
|
@@ -2480,7 +2731,8 @@ var watchCommand = new Command8("watch").description(
|
|
|
2480
2731
|
let attemptIndex = 0;
|
|
2481
2732
|
const launchAttempt = async (attemptAgent) => {
|
|
2482
2733
|
let spawnFailureReason = null;
|
|
2483
|
-
const
|
|
2734
|
+
const pausedForNetwork = networkPaused.get(task.id);
|
|
2735
|
+
const shouldResumeClaudeSession = attemptAgent === "claude" && !!task.claudeSessionId && (hasFeedback || pausedForNetwork?.resumeSession === true);
|
|
2484
2736
|
const sessionId = attemptAgent === "claude" ? task.claudeSessionId ?? randomUUID() : void 0;
|
|
2485
2737
|
const executionSystemPrompt = composeSystemPrompt(EXECUTION_SYSTEM_SECTIONS);
|
|
2486
2738
|
const child = spawnAgent(
|
|
@@ -2500,6 +2752,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2500
2752
|
activeEntry.process = child;
|
|
2501
2753
|
activeEntry.currentAgent = attemptAgent;
|
|
2502
2754
|
active.set(task.id, activeEntry);
|
|
2755
|
+
networkPaused.delete(task.id);
|
|
2503
2756
|
if (attemptAgent === "claude" && sessionId) {
|
|
2504
2757
|
api.patch(`/api/tasks/${task.id}`, { claudeSessionId: sessionId }).catch(() => {
|
|
2505
2758
|
});
|
|
@@ -2517,9 +2770,15 @@ var watchCommand = new Command8("watch").description(
|
|
|
2517
2770
|
}
|
|
2518
2771
|
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
2519
2772
|
if (failedAttempt && !activeEntry.terminatedForError) {
|
|
2773
|
+
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2774
|
+
if (await shouldPauseForNetwork(failureDetail)) {
|
|
2775
|
+
pauseTaskForNetwork(task.id, failureDetail);
|
|
2776
|
+
handleNetworkLoss(failureDetail);
|
|
2777
|
+
logWarn(prefix, `${attemptAgent} paused after network loss (${failureDetail})`);
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2520
2780
|
const nextAgent = attemptOrder[attemptIndex + 1];
|
|
2521
2781
|
if (nextAgent) {
|
|
2522
|
-
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2523
2782
|
logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying with ${nextAgent}`);
|
|
2524
2783
|
await postTaskUpdate(
|
|
2525
2784
|
task.id,
|
|
@@ -2570,7 +2829,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2570
2829
|
let prUrl = null;
|
|
2571
2830
|
if (!noMrRequested) {
|
|
2572
2831
|
prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
|
|
2573
|
-
if (!prUrl) {
|
|
2832
|
+
if (!prUrl && !task.attachedBranch?.trim()) {
|
|
2574
2833
|
prUrl = await findPrUrlAcrossRepos(legacyBranchName, repoDir, vcs);
|
|
2575
2834
|
}
|
|
2576
2835
|
if (!prUrl) {
|
|
@@ -2591,7 +2850,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2591
2850
|
existingResources,
|
|
2592
2851
|
skillRefs
|
|
2593
2852
|
);
|
|
2594
|
-
if (!prUrl) {
|
|
2853
|
+
if (!prUrl && !task.attachedBranch?.trim()) {
|
|
2595
2854
|
prUrl = await createPrAcrossRepos(
|
|
2596
2855
|
task,
|
|
2597
2856
|
legacyBranchName,
|
|
@@ -2622,8 +2881,20 @@ var watchCommand = new Command8("watch").description(
|
|
|
2622
2881
|
...prUrl ? { link: prUrl } : {}
|
|
2623
2882
|
});
|
|
2624
2883
|
logSuccess(prefix, `"${paint("bold", task.title)}" marked ready for review`);
|
|
2625
|
-
|
|
2884
|
+
if (noMrRequested) {
|
|
2885
|
+
await postTaskUpdate(task.id, `No ${prLabel} required: ${noMrDescription}`, "system");
|
|
2886
|
+
}
|
|
2626
2887
|
} catch (err) {
|
|
2888
|
+
if (isLikelyNetworkError(err)) {
|
|
2889
|
+
networkPaused.set(task.id, {
|
|
2890
|
+
pausedAt: Date.now(),
|
|
2891
|
+
reason: err.message,
|
|
2892
|
+
resumeSession: false
|
|
2893
|
+
});
|
|
2894
|
+
handleNetworkLoss(err.message);
|
|
2895
|
+
logWarn(prefix, `Task finalization paused after network loss (${err.message})`);
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2627
2898
|
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2628
2899
|
}
|
|
2629
2900
|
} else if (!activeEntry.terminatedForError) {
|
|
@@ -2633,7 +2904,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2633
2904
|
await moveTaskToError(task, prefix, reason);
|
|
2634
2905
|
}
|
|
2635
2906
|
} finally {
|
|
2636
|
-
if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
|
|
2907
|
+
if (!networkPaused.has(task.id) && activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
|
|
2637
2908
|
removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
|
|
2638
2909
|
}
|
|
2639
2910
|
queued.delete(task.id);
|
|
@@ -2767,7 +3038,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2767
3038
|
...prdContent ? { prdContent } : {}
|
|
2768
3039
|
});
|
|
2769
3040
|
logSuccess(prefix, `"${paint("bold", task.title)}" PRD generated and marked ready for review`);
|
|
2770
|
-
await postTaskUpdate(task.id, "PRD generated
|
|
3041
|
+
await postTaskUpdate(task.id, "PRD generated", "system");
|
|
2771
3042
|
} catch (err) {
|
|
2772
3043
|
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2773
3044
|
}
|
|
@@ -3257,18 +3528,29 @@ ${divider}`);
|
|
|
3257
3528
|
if (pollRunning) return;
|
|
3258
3529
|
pollRunning = true;
|
|
3259
3530
|
try {
|
|
3531
|
+
if (networkOffline) {
|
|
3532
|
+
const reachable = await isApiReachable();
|
|
3533
|
+
if (!reachable) return;
|
|
3534
|
+
const resumed = await resumeNetworkPausedTasks();
|
|
3535
|
+
if (!resumed) return;
|
|
3536
|
+
}
|
|
3260
3537
|
let queuedTasks;
|
|
3261
3538
|
let delegatedTasks;
|
|
3262
3539
|
try {
|
|
3263
3540
|
queuedTasks = await api.get("/api/tasks?status=queued");
|
|
3264
3541
|
delegatedTasks = await api.get("/api/tasks?status=delegated");
|
|
3265
3542
|
} catch (err) {
|
|
3543
|
+
if (isLikelyNetworkError(err)) {
|
|
3544
|
+
handleNetworkLoss(err.message);
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3266
3547
|
logError(watchTag(), `Failed to fetch tasks: ${err.message}`);
|
|
3267
3548
|
return;
|
|
3268
3549
|
}
|
|
3269
3550
|
const nonTestQueued = queuedTasks.filter((t) => t.mode !== "testing");
|
|
3270
3551
|
const nonTestDelegated = delegatedTasks.filter((t) => t.mode !== "testing");
|
|
3271
3552
|
const staleDelegatedTasks = nonTestDelegated.filter((task) => {
|
|
3553
|
+
if (networkPaused.has(task.id)) return false;
|
|
3272
3554
|
if (!task.inProgressSince) return false;
|
|
3273
3555
|
return Date.now() - new Date(task.inProgressSince).getTime() >= hungTaskTimeoutMs;
|
|
3274
3556
|
});
|
|
@@ -3298,6 +3580,7 @@ ${divider}`);
|
|
|
3298
3580
|
const sid = shortId(task.id);
|
|
3299
3581
|
const prefix = taskTag(sid);
|
|
3300
3582
|
const activeEntry = active.get(task.id);
|
|
3583
|
+
if (networkPaused.has(task.id)) continue;
|
|
3301
3584
|
const idleMs = activeEntry ? Date.now() - activeEntry.lastActivityAt : null;
|
|
3302
3585
|
const delegatedAtMs = task.inProgressSince ? Date.now() - new Date(task.inProgressSince).getTime() : null;
|
|
3303
3586
|
const exceededIdleTimeout = idleMs !== null && idleMs >= taskStallTimeoutMs;
|
|
@@ -3347,12 +3630,17 @@ ${divider}`);
|
|
|
3347
3630
|
if (failed.has(task.id)) continue;
|
|
3348
3631
|
const sid = shortId(task.id);
|
|
3349
3632
|
const prefix = taskTag(sid);
|
|
3350
|
-
|
|
3633
|
+
let repoDir = findDirectoryForProject(config, task.projectId, rootDir);
|
|
3351
3634
|
if (!repoDir) {
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3635
|
+
if (taskLikelyDoesNotNeedCodeChanges(task)) {
|
|
3636
|
+
repoDir = rootDir;
|
|
3637
|
+
logInfo(prefix, `No linked directory \u2014 using root dir for no-code task`);
|
|
3638
|
+
} else {
|
|
3639
|
+
const reason = `no linked directory found under ${rootDir}`;
|
|
3640
|
+
logError(prefix, `"${task.title}": ${reason} \u2014 will not retry`);
|
|
3641
|
+
failed.set(task.id, reason);
|
|
3642
|
+
continue;
|
|
3643
|
+
}
|
|
3356
3644
|
}
|
|
3357
3645
|
if (!existsSync7(repoDir)) {
|
|
3358
3646
|
const reason = `linked directory "${repoDir}" does not exist`;
|
|
@@ -3526,6 +3814,16 @@ ${divider}`);
|
|
|
3526
3814
|
await api.post(`/api/tasks/${task.id}/updates`, {
|
|
3527
3815
|
message,
|
|
3528
3816
|
imageUrl: uploadData.url,
|
|
3817
|
+
media: [
|
|
3818
|
+
{
|
|
3819
|
+
kind: "screenshot",
|
|
3820
|
+
url: uploadData.url,
|
|
3821
|
+
mimeType: "image/png",
|
|
3822
|
+
label: "Test screenshot",
|
|
3823
|
+
captureContext: "watch-completion",
|
|
3824
|
+
uploadState: "uploaded"
|
|
3825
|
+
}
|
|
3826
|
+
],
|
|
3529
3827
|
source: "system"
|
|
3530
3828
|
});
|
|
3531
3829
|
return uploadData.url;
|
|
@@ -3533,10 +3831,34 @@ ${divider}`);
|
|
|
3533
3831
|
return null;
|
|
3534
3832
|
}
|
|
3535
3833
|
},
|
|
3536
|
-
|
|
3537
|
-
|
|
3834
|
+
uploadVideo: async (videoPath) => {
|
|
3835
|
+
try {
|
|
3836
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
3837
|
+
const cfg = loadConfig();
|
|
3838
|
+
const videoBuffer = readFileSync13(videoPath);
|
|
3839
|
+
const fileName = videoPath.split("/").pop() || "proof-recording.webm";
|
|
3840
|
+
const isMp4 = fileName.endsWith(".mp4");
|
|
3841
|
+
const formData = new FormData();
|
|
3842
|
+
const blob = new Blob([videoBuffer], { type: isMp4 ? "video/mp4" : "video/webm" });
|
|
3843
|
+
formData.append("file", blob, fileName);
|
|
3844
|
+
formData.append("prefix", "test-recordings");
|
|
3845
|
+
const uploadRes = await fetch(`${cfg.apiUrl}/api/upload`, {
|
|
3846
|
+
method: "POST",
|
|
3847
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
3848
|
+
body: formData
|
|
3849
|
+
});
|
|
3850
|
+
if (!uploadRes.ok) return null;
|
|
3851
|
+
const uploadData = await uploadRes.json();
|
|
3852
|
+
return uploadData.url;
|
|
3853
|
+
} catch {
|
|
3854
|
+
return null;
|
|
3855
|
+
}
|
|
3856
|
+
},
|
|
3857
|
+
postUpdate: async (payload) => {
|
|
3858
|
+
await postTaskUpdate(task.id, payload);
|
|
3538
3859
|
},
|
|
3539
|
-
onProgress: (msg) => logInfo(prefix, msg)
|
|
3860
|
+
onProgress: (msg) => logInfo(prefix, msg),
|
|
3861
|
+
recordingContext: "watch-completion"
|
|
3540
3862
|
});
|
|
3541
3863
|
await api.patch(`/api/tasks/${task.id}`, { status: "completed", testResult: result.status });
|
|
3542
3864
|
logSuccess(prefix, result.summary);
|
|
@@ -4522,8 +4844,8 @@ var resumeCommand = new Command17("resume").description("Resume an interactive C
|
|
|
4522
4844
|
|
|
4523
4845
|
// cli/commands/browse.ts
|
|
4524
4846
|
import { Command as Command18 } from "commander";
|
|
4525
|
-
import { execSync as
|
|
4526
|
-
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as
|
|
4847
|
+
import { execSync as execSync4, spawn as spawn6 } from "child_process";
|
|
4848
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
4527
4849
|
import { join as join8 } from "path";
|
|
4528
4850
|
var BROWSE_DIR2 = join8(import.meta.dirname, "..", "..", "browse");
|
|
4529
4851
|
function isProcessAlive(pid) {
|
|
@@ -4570,7 +4892,7 @@ async function ensureDevServer() {
|
|
|
4570
4892
|
env: { ...process.env }
|
|
4571
4893
|
});
|
|
4572
4894
|
devProc.unref();
|
|
4573
|
-
|
|
4895
|
+
writeFileSync4(
|
|
4574
4896
|
devStateFile,
|
|
4575
4897
|
JSON.stringify({ pid: devProc.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
4576
4898
|
{ mode: 384 }
|
|
@@ -4598,7 +4920,7 @@ var browseCommand = new Command18("browse").description("Control a headless brow
|
|
|
4598
4920
|
if (command === "setup") {
|
|
4599
4921
|
console.log("[browse] Running browse daemon setup...");
|
|
4600
4922
|
try {
|
|
4601
|
-
|
|
4923
|
+
execSync4("./setup", { cwd: BROWSE_DIR2, stdio: "inherit" });
|
|
4602
4924
|
} catch {
|
|
4603
4925
|
console.error("[browse] Setup failed");
|
|
4604
4926
|
process.exit(1);
|
|
@@ -4691,7 +5013,7 @@ var setPathCommand = new Command19("set-path").description("Set or update the lo
|
|
|
4691
5013
|
// cli/commands/test.ts
|
|
4692
5014
|
import { Command as Command20 } from "commander";
|
|
4693
5015
|
import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
|
|
4694
|
-
var testCommand = new Command20("test").description("Run automated browser test for a task's MR/PR").argument("<task-id>", "Task ID to test").option("--plan <file>", "Path to a custom test plan JSON file").action(async (taskId, opts) => {
|
|
5016
|
+
var testCommand = new Command20("test").description("Run automated browser test for a task's MR/PR").argument("<task-id>", "Task ID to test").option("--plan <file>", "Path to a custom test plan JSON file").option("--no-recording", "Disable proof recording for this run").action(async (taskId, opts) => {
|
|
4695
5017
|
const config = loadConfig();
|
|
4696
5018
|
console.log("[test] Fetching task...");
|
|
4697
5019
|
let task;
|
|
@@ -4739,7 +5061,7 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
4739
5061
|
}
|
|
4740
5062
|
}
|
|
4741
5063
|
try {
|
|
4742
|
-
await api.patch(`/api/tasks/${taskId}`, {
|
|
5064
|
+
await api.patch(`/api/tasks/${taskId}`, { status: "delegated", testResult: null });
|
|
4743
5065
|
} catch {
|
|
4744
5066
|
}
|
|
4745
5067
|
console.log(`[test] Starting test for "${task.title}"...`);
|
|
@@ -4769,6 +5091,16 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
4769
5091
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
4770
5092
|
message,
|
|
4771
5093
|
imageUrl: uploadData.url,
|
|
5094
|
+
media: [
|
|
5095
|
+
{
|
|
5096
|
+
kind: "screenshot",
|
|
5097
|
+
url: uploadData.url,
|
|
5098
|
+
mimeType: "image/png",
|
|
5099
|
+
label: "Test screenshot",
|
|
5100
|
+
captureContext: "test-run",
|
|
5101
|
+
uploadState: "uploaded"
|
|
5102
|
+
}
|
|
5103
|
+
],
|
|
4772
5104
|
source: "system"
|
|
4773
5105
|
});
|
|
4774
5106
|
return uploadData.url;
|
|
@@ -4776,20 +5108,52 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
4776
5108
|
return null;
|
|
4777
5109
|
}
|
|
4778
5110
|
},
|
|
4779
|
-
|
|
5111
|
+
uploadVideo: async (videoPath) => {
|
|
5112
|
+
try {
|
|
5113
|
+
const videoBuffer = readFileSync8(videoPath);
|
|
5114
|
+
const fileName = videoPath.split("/").pop() || "proof-recording.webm";
|
|
5115
|
+
const isMp4 = fileName.endsWith(".mp4");
|
|
5116
|
+
const formData = new FormData();
|
|
5117
|
+
const blob = new Blob([videoBuffer], { type: isMp4 ? "video/mp4" : "video/webm" });
|
|
5118
|
+
formData.append("file", blob, fileName);
|
|
5119
|
+
formData.append("prefix", "test-recordings");
|
|
5120
|
+
const uploadRes = await fetch(`${config.apiUrl}/api/upload`, {
|
|
5121
|
+
method: "POST",
|
|
5122
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
5123
|
+
body: formData
|
|
5124
|
+
});
|
|
5125
|
+
if (!uploadRes.ok) return null;
|
|
5126
|
+
const uploadData = await uploadRes.json();
|
|
5127
|
+
return uploadData.url;
|
|
5128
|
+
} catch {
|
|
5129
|
+
return null;
|
|
5130
|
+
}
|
|
5131
|
+
},
|
|
5132
|
+
postUpdate: async ({
|
|
5133
|
+
message,
|
|
5134
|
+
imageUrl,
|
|
5135
|
+
media,
|
|
5136
|
+
proofState,
|
|
5137
|
+
proofDetails
|
|
5138
|
+
}) => {
|
|
4780
5139
|
try {
|
|
4781
5140
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
4782
5141
|
message,
|
|
4783
5142
|
imageUrl,
|
|
5143
|
+
media,
|
|
5144
|
+
proofState,
|
|
5145
|
+
proofDetails,
|
|
4784
5146
|
source: "system"
|
|
4785
5147
|
});
|
|
4786
5148
|
} catch {
|
|
4787
5149
|
}
|
|
4788
5150
|
},
|
|
4789
|
-
onProgress: (msg) => console.log(`[test] ${msg}`)
|
|
5151
|
+
onProgress: (msg) => console.log(`[test] ${msg}`),
|
|
5152
|
+
recordingEnabled: opts.recording !== false,
|
|
5153
|
+
recordingContext: "test-run"
|
|
4790
5154
|
});
|
|
4791
5155
|
try {
|
|
4792
|
-
await api.patch(`/api/tasks/${taskId}`, {
|
|
5156
|
+
await api.patch(`/api/tasks/${taskId}`, { status: "completed", testResult: result.status });
|
|
4793
5157
|
} catch {
|
|
4794
5158
|
}
|
|
4795
5159
|
console.log(`
|
|
@@ -4812,7 +5176,7 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
4812
5176
|
|
|
4813
5177
|
// cli/commands/features.ts
|
|
4814
5178
|
import { Command as Command21 } from "commander";
|
|
4815
|
-
import { readFileSync as readFileSync9, writeFileSync as
|
|
5179
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync12 } from "fs";
|
|
4816
5180
|
import { resolve as resolve5, sep as sep2 } from "path";
|
|
4817
5181
|
var FEATURES_FILE3 = ".mr-features.md";
|
|
4818
5182
|
var c7 = {
|
|
@@ -4853,13 +5217,13 @@ var featuresCommand = new Command21("features").description("View or update the
|
|
|
4853
5217
|
if (opts.file) {
|
|
4854
5218
|
const content2 = readFileSync9(resolve5(opts.file), "utf-8");
|
|
4855
5219
|
const featuresPath = getFeaturesPath();
|
|
4856
|
-
|
|
5220
|
+
writeFileSync5(featuresPath, content2);
|
|
4857
5221
|
console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)} from ${paint7("cyan", opts.file)}`);
|
|
4858
5222
|
return;
|
|
4859
5223
|
}
|
|
4860
5224
|
if (opts.update) {
|
|
4861
5225
|
const featuresPath = getFeaturesPath();
|
|
4862
|
-
|
|
5226
|
+
writeFileSync5(featuresPath, opts.update);
|
|
4863
5227
|
console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)}`);
|
|
4864
5228
|
return;
|
|
4865
5229
|
}
|
|
@@ -4874,12 +5238,12 @@ var featuresCommand = new Command21("features").description("View or update the
|
|
|
4874
5238
|
|
|
4875
5239
|
// cli/commands/no-mr.ts
|
|
4876
5240
|
import { Command as Command22 } from "commander";
|
|
4877
|
-
import { writeFileSync as
|
|
5241
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
4878
5242
|
import { resolve as resolve6 } from "path";
|
|
4879
5243
|
var NO_MR_FILE = ".mr-no-mr";
|
|
4880
5244
|
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) => {
|
|
4881
5245
|
const filePath = resolve6(process.cwd(), NO_MR_FILE);
|
|
4882
|
-
|
|
5246
|
+
writeFileSync6(filePath, description, "utf-8");
|
|
4883
5247
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
4884
5248
|
message: `No MR/PR needed \u2014 ${description}`,
|
|
4885
5249
|
source: "agent"
|
|
@@ -4961,7 +5325,7 @@ async function authenticateBrowseSession(magicUrl, runBrowse) {
|
|
|
4961
5325
|
// lib/scanner/codebase-analysis.ts
|
|
4962
5326
|
import { readdirSync as readdirSync2, readFileSync as readFileSync11, existsSync as existsSync14 } from "fs";
|
|
4963
5327
|
import { join as join10, relative } from "path";
|
|
4964
|
-
import { execSync as
|
|
5328
|
+
import { execSync as execSync5 } from "child_process";
|
|
4965
5329
|
function resolveDir(projectPath, candidates) {
|
|
4966
5330
|
for (const candidate of candidates) {
|
|
4967
5331
|
const dir = join10(projectPath, candidate);
|
|
@@ -5087,7 +5451,7 @@ function extractInternalLinks(projectPath) {
|
|
|
5087
5451
|
}
|
|
5088
5452
|
function getRecentCommits(projectPath, count = 20) {
|
|
5089
5453
|
try {
|
|
5090
|
-
const output =
|
|
5454
|
+
const output = execSync5(
|
|
5091
5455
|
`git log --oneline -${count} --no-decorate`,
|
|
5092
5456
|
{ cwd: projectPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
5093
5457
|
);
|