@dunnewold-labs/mr-manager 0.4.14 → 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 +414 -53
- 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 {
|
|
@@ -1009,7 +1163,7 @@ function taskLikelyDoesNotNeedCodeChanges(task) {
|
|
|
1009
1163
|
}
|
|
1010
1164
|
|
|
1011
1165
|
// cli/browse-runner.ts
|
|
1012
|
-
import { execSync as
|
|
1166
|
+
import { execSync as execSync3, spawn as spawn3 } from "child_process";
|
|
1013
1167
|
import { existsSync as existsSync6 } from "fs";
|
|
1014
1168
|
import { join as join6 } from "path";
|
|
1015
1169
|
var BROWSE_DIR = join6(import.meta.dirname, "..", "..", "browse");
|
|
@@ -1020,7 +1174,7 @@ function getBrowseRunner() {
|
|
|
1020
1174
|
return { cmd: BROWSE_BINARY, args: [] };
|
|
1021
1175
|
}
|
|
1022
1176
|
try {
|
|
1023
|
-
|
|
1177
|
+
execSync3("which bun", { stdio: "pipe" });
|
|
1024
1178
|
if (existsSync6(BROWSE_DEV_CMD)) {
|
|
1025
1179
|
return { cmd: "bun", args: ["run", BROWSE_DEV_CMD] };
|
|
1026
1180
|
}
|
|
@@ -1247,9 +1401,19 @@ function logDispatch(prefix, msg) {
|
|
|
1247
1401
|
function logSpinner(prefix, msg) {
|
|
1248
1402
|
console.log(`${timestamp()} ${prefix} ${paint("blue", "\u27F3")} ${msg}`);
|
|
1249
1403
|
}
|
|
1250
|
-
|
|
1404
|
+
function isLikelyNetworkFailureDetail(detail) {
|
|
1405
|
+
return isLikelyNetworkError(new Error(detail));
|
|
1406
|
+
}
|
|
1407
|
+
async function postTaskUpdate(taskId, payloadOrMessage, source = "system") {
|
|
1251
1408
|
try {
|
|
1252
|
-
|
|
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
|
+
});
|
|
1253
1417
|
} catch (err) {
|
|
1254
1418
|
logError(taskTag(shortId(taskId)), `Failed to post update: ${err.message}`);
|
|
1255
1419
|
}
|
|
@@ -1285,6 +1449,12 @@ function ownerPrefix(task) {
|
|
|
1285
1449
|
const first = name.split(/\s+/)[0].toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1286
1450
|
return first || "mr";
|
|
1287
1451
|
}
|
|
1452
|
+
function taskBranchName(task) {
|
|
1453
|
+
if (task.attachedBranch?.trim()) {
|
|
1454
|
+
return task.attachedBranch.trim();
|
|
1455
|
+
}
|
|
1456
|
+
return `${ownerPrefix(task)}/${slugify(task.title)}`;
|
|
1457
|
+
}
|
|
1288
1458
|
function formatElapsed(ms) {
|
|
1289
1459
|
const totalMinutes = Math.max(1, Math.floor(ms / 6e4));
|
|
1290
1460
|
const hours = Math.floor(totalMinutes / 60);
|
|
@@ -1468,7 +1638,7 @@ async function createPrInRepo(task, branchName, repoDir, vcs, subtasks, protoRef
|
|
|
1468
1638
|
}
|
|
1469
1639
|
const bodyPath = resolve2("/tmp", `mr-auto-pr-${randomUUID()}.md`);
|
|
1470
1640
|
const body = buildAutomaticPrBody(task, branchName, vcs, subtasks, protoRefs, feedbackUpdates, existingResources, skillRefs);
|
|
1471
|
-
|
|
1641
|
+
writeFileSync3(bodyPath, `${body}
|
|
1472
1642
|
`, "utf-8");
|
|
1473
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)}`;
|
|
1474
1644
|
try {
|
|
@@ -1615,7 +1785,7 @@ function buildPrototypeSection(protoRefs, workingDir) {
|
|
|
1615
1785
|
const safeProtoId = proto.id.slice(0, 8);
|
|
1616
1786
|
const tmpPath = resolve2(tmpDir, `.mr-proto-${safeProtoId}-v${selected[i] ?? i}.html`);
|
|
1617
1787
|
try {
|
|
1618
|
-
|
|
1788
|
+
writeFileSync3(tmpPath, file.content, "utf-8");
|
|
1619
1789
|
sections.push(`#### ${variantLabel}: ${file.name}`);
|
|
1620
1790
|
sections.push(`File: \`${tmpPath}\` \u2014 read this file to see the full HTML content.`);
|
|
1621
1791
|
} catch {
|
|
@@ -1721,13 +1891,15 @@ function buildFeaturesSection(repoDir) {
|
|
|
1721
1891
|
``
|
|
1722
1892
|
].join("\n");
|
|
1723
1893
|
}
|
|
1724
|
-
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) {
|
|
1725
1895
|
const slug = slugify(task.title);
|
|
1726
1896
|
const owner = ownerPrefix(task);
|
|
1727
|
-
const
|
|
1897
|
+
const generatedBranchName = `${owner}/${slug}`;
|
|
1898
|
+
const branchName = taskBranchName(task);
|
|
1728
1899
|
const wtPath = worktreePath(`${owner}-${slug}`);
|
|
1729
1900
|
const workingDir = executionDir ?? repoDir;
|
|
1730
1901
|
const prBodyPath = "/tmp/mr-pr-body.md";
|
|
1902
|
+
const hasAttachedBranch = !!task.attachedBranch?.trim();
|
|
1731
1903
|
const notes = task.prdContent ? `
|
|
1732
1904
|
|
|
1733
1905
|
## PRD (Product Requirements Document)
|
|
@@ -1766,10 +1938,11 @@ ${task.notes}` : "";
|
|
|
1766
1938
|
``,
|
|
1767
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.`,
|
|
1768
1940
|
``,
|
|
1769
|
-
...
|
|
1770
|
-
`2. Your
|
|
1941
|
+
...preparedBranchName ? [
|
|
1942
|
+
`2. Your git working tree is already prepared and this session starts inside it.`,
|
|
1771
1943
|
` - Current working directory: ${executionDir}`,
|
|
1772
|
-
` - 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}.`] : [],
|
|
1773
1946
|
` - If the task spans additional repos, create matching worktrees there as needed.`
|
|
1774
1947
|
] : hasFeedback ? [
|
|
1775
1948
|
`2. Set up your working environment:`,
|
|
@@ -1844,7 +2017,7 @@ ${task.notes}` : "";
|
|
|
1844
2017
|
if (isRevision) {
|
|
1845
2018
|
const prdTmpPath = resolve2(repoDir, ".mr-existing-prd.md");
|
|
1846
2019
|
try {
|
|
1847
|
-
|
|
2020
|
+
writeFileSync3(prdTmpPath, existingPrd, "utf-8");
|
|
1848
2021
|
} catch {
|
|
1849
2022
|
}
|
|
1850
2023
|
const prdSummaryLines = [];
|
|
@@ -2066,7 +2239,7 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
|
|
|
2066
2239
|
const f = parentFiles[i];
|
|
2067
2240
|
const tmpPath = resolve2(repoDir, `.mr-parent-variant-${i + 1}.html`);
|
|
2068
2241
|
try {
|
|
2069
|
-
|
|
2242
|
+
writeFileSync3(tmpPath, f.content, "utf-8");
|
|
2070
2243
|
existingVariantLines.push(`### Previous Variant ${i + 1} (${f.name})`);
|
|
2071
2244
|
existingVariantLines.push(`File: \`${tmpPath}\` \u2014 read this file to see the full HTML content.`);
|
|
2072
2245
|
} catch {
|
|
@@ -2329,6 +2502,8 @@ var watchCommand = new Command8("watch").description(
|
|
|
2329
2502
|
const failed = /* @__PURE__ */ new Map();
|
|
2330
2503
|
const queued = /* @__PURE__ */ new Set();
|
|
2331
2504
|
const finishing = /* @__PURE__ */ new Set();
|
|
2505
|
+
const networkPaused = /* @__PURE__ */ new Map();
|
|
2506
|
+
let networkOffline = false;
|
|
2332
2507
|
let pollRunning = false;
|
|
2333
2508
|
const approvalQueue = [];
|
|
2334
2509
|
let approvalRunning = false;
|
|
@@ -2382,11 +2557,85 @@ var watchCommand = new Command8("watch").description(
|
|
|
2382
2557
|
} catch {
|
|
2383
2558
|
}
|
|
2384
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
|
+
}
|
|
2385
2634
|
async function dispatchTask(task, repoDir) {
|
|
2386
2635
|
const sid = shortId(task.id);
|
|
2387
2636
|
const slug = slugify(task.title);
|
|
2388
2637
|
const owner = ownerPrefix(task);
|
|
2389
|
-
const branchName =
|
|
2638
|
+
const branchName = taskBranchName(task);
|
|
2390
2639
|
const legacyBranchName = `mr/${sid}/${slug}`;
|
|
2391
2640
|
const prefix = taskTag(sid);
|
|
2392
2641
|
const vcs = detectVcs(repoDir)?.provider ?? "github";
|
|
@@ -2446,7 +2695,8 @@ var watchCommand = new Command8("watch").description(
|
|
|
2446
2695
|
const worktree = createWorktree(
|
|
2447
2696
|
repoDir,
|
|
2448
2697
|
branchName,
|
|
2449
|
-
worktreeNameFromPath(desiredWorktreePath)
|
|
2698
|
+
worktreeNameFromPath(desiredWorktreePath),
|
|
2699
|
+
{ syncRemoteBranch: !!task.attachedBranch?.trim() }
|
|
2450
2700
|
);
|
|
2451
2701
|
executionDir = worktree.path;
|
|
2452
2702
|
cleanupWorktreePath = worktree.created ? executionDir : void 0;
|
|
@@ -2455,7 +2705,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2455
2705
|
worktree.reusedBranchWorktree ? `Reusing existing worktree ${paint("cyan", executionDir)} for branch ${paint("dim", branchName)}` : `Prepared worktree ${paint("cyan", executionDir)}`
|
|
2456
2706
|
);
|
|
2457
2707
|
}
|
|
2458
|
-
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);
|
|
2459
2709
|
const activeEntry = {
|
|
2460
2710
|
process: void 0,
|
|
2461
2711
|
title: task.title,
|
|
@@ -2481,7 +2731,8 @@ var watchCommand = new Command8("watch").description(
|
|
|
2481
2731
|
let attemptIndex = 0;
|
|
2482
2732
|
const launchAttempt = async (attemptAgent) => {
|
|
2483
2733
|
let spawnFailureReason = null;
|
|
2484
|
-
const
|
|
2734
|
+
const pausedForNetwork = networkPaused.get(task.id);
|
|
2735
|
+
const shouldResumeClaudeSession = attemptAgent === "claude" && !!task.claudeSessionId && (hasFeedback || pausedForNetwork?.resumeSession === true);
|
|
2485
2736
|
const sessionId = attemptAgent === "claude" ? task.claudeSessionId ?? randomUUID() : void 0;
|
|
2486
2737
|
const executionSystemPrompt = composeSystemPrompt(EXECUTION_SYSTEM_SECTIONS);
|
|
2487
2738
|
const child = spawnAgent(
|
|
@@ -2501,6 +2752,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2501
2752
|
activeEntry.process = child;
|
|
2502
2753
|
activeEntry.currentAgent = attemptAgent;
|
|
2503
2754
|
active.set(task.id, activeEntry);
|
|
2755
|
+
networkPaused.delete(task.id);
|
|
2504
2756
|
if (attemptAgent === "claude" && sessionId) {
|
|
2505
2757
|
api.patch(`/api/tasks/${task.id}`, { claudeSessionId: sessionId }).catch(() => {
|
|
2506
2758
|
});
|
|
@@ -2518,9 +2770,15 @@ var watchCommand = new Command8("watch").description(
|
|
|
2518
2770
|
}
|
|
2519
2771
|
const failedAttempt = code !== 0 || spawnFailureReason !== null;
|
|
2520
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
|
+
}
|
|
2521
2780
|
const nextAgent = attemptOrder[attemptIndex + 1];
|
|
2522
2781
|
if (nextAgent) {
|
|
2523
|
-
const failureDetail = spawnFailureReason ?? `exit code ${code}`;
|
|
2524
2782
|
logWarn(prefix, `${attemptAgent} failed (${failureDetail}) \u2014 retrying with ${nextAgent}`);
|
|
2525
2783
|
await postTaskUpdate(
|
|
2526
2784
|
task.id,
|
|
@@ -2571,7 +2829,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2571
2829
|
let prUrl = null;
|
|
2572
2830
|
if (!noMrRequested) {
|
|
2573
2831
|
prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
|
|
2574
|
-
if (!prUrl) {
|
|
2832
|
+
if (!prUrl && !task.attachedBranch?.trim()) {
|
|
2575
2833
|
prUrl = await findPrUrlAcrossRepos(legacyBranchName, repoDir, vcs);
|
|
2576
2834
|
}
|
|
2577
2835
|
if (!prUrl) {
|
|
@@ -2592,7 +2850,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2592
2850
|
existingResources,
|
|
2593
2851
|
skillRefs
|
|
2594
2852
|
);
|
|
2595
|
-
if (!prUrl) {
|
|
2853
|
+
if (!prUrl && !task.attachedBranch?.trim()) {
|
|
2596
2854
|
prUrl = await createPrAcrossRepos(
|
|
2597
2855
|
task,
|
|
2598
2856
|
legacyBranchName,
|
|
@@ -2627,6 +2885,16 @@ var watchCommand = new Command8("watch").description(
|
|
|
2627
2885
|
await postTaskUpdate(task.id, `No ${prLabel} required: ${noMrDescription}`, "system");
|
|
2628
2886
|
}
|
|
2629
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
|
+
}
|
|
2630
2898
|
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2631
2899
|
}
|
|
2632
2900
|
} else if (!activeEntry.terminatedForError) {
|
|
@@ -2636,7 +2904,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2636
2904
|
await moveTaskToError(task, prefix, reason);
|
|
2637
2905
|
}
|
|
2638
2906
|
} finally {
|
|
2639
|
-
if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
|
|
2907
|
+
if (!networkPaused.has(task.id) && activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
|
|
2640
2908
|
removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
|
|
2641
2909
|
}
|
|
2642
2910
|
queued.delete(task.id);
|
|
@@ -3260,18 +3528,29 @@ ${divider}`);
|
|
|
3260
3528
|
if (pollRunning) return;
|
|
3261
3529
|
pollRunning = true;
|
|
3262
3530
|
try {
|
|
3531
|
+
if (networkOffline) {
|
|
3532
|
+
const reachable = await isApiReachable();
|
|
3533
|
+
if (!reachable) return;
|
|
3534
|
+
const resumed = await resumeNetworkPausedTasks();
|
|
3535
|
+
if (!resumed) return;
|
|
3536
|
+
}
|
|
3263
3537
|
let queuedTasks;
|
|
3264
3538
|
let delegatedTasks;
|
|
3265
3539
|
try {
|
|
3266
3540
|
queuedTasks = await api.get("/api/tasks?status=queued");
|
|
3267
3541
|
delegatedTasks = await api.get("/api/tasks?status=delegated");
|
|
3268
3542
|
} catch (err) {
|
|
3543
|
+
if (isLikelyNetworkError(err)) {
|
|
3544
|
+
handleNetworkLoss(err.message);
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3269
3547
|
logError(watchTag(), `Failed to fetch tasks: ${err.message}`);
|
|
3270
3548
|
return;
|
|
3271
3549
|
}
|
|
3272
3550
|
const nonTestQueued = queuedTasks.filter((t) => t.mode !== "testing");
|
|
3273
3551
|
const nonTestDelegated = delegatedTasks.filter((t) => t.mode !== "testing");
|
|
3274
3552
|
const staleDelegatedTasks = nonTestDelegated.filter((task) => {
|
|
3553
|
+
if (networkPaused.has(task.id)) return false;
|
|
3275
3554
|
if (!task.inProgressSince) return false;
|
|
3276
3555
|
return Date.now() - new Date(task.inProgressSince).getTime() >= hungTaskTimeoutMs;
|
|
3277
3556
|
});
|
|
@@ -3301,6 +3580,7 @@ ${divider}`);
|
|
|
3301
3580
|
const sid = shortId(task.id);
|
|
3302
3581
|
const prefix = taskTag(sid);
|
|
3303
3582
|
const activeEntry = active.get(task.id);
|
|
3583
|
+
if (networkPaused.has(task.id)) continue;
|
|
3304
3584
|
const idleMs = activeEntry ? Date.now() - activeEntry.lastActivityAt : null;
|
|
3305
3585
|
const delegatedAtMs = task.inProgressSince ? Date.now() - new Date(task.inProgressSince).getTime() : null;
|
|
3306
3586
|
const exceededIdleTimeout = idleMs !== null && idleMs >= taskStallTimeoutMs;
|
|
@@ -3350,12 +3630,17 @@ ${divider}`);
|
|
|
3350
3630
|
if (failed.has(task.id)) continue;
|
|
3351
3631
|
const sid = shortId(task.id);
|
|
3352
3632
|
const prefix = taskTag(sid);
|
|
3353
|
-
|
|
3633
|
+
let repoDir = findDirectoryForProject(config, task.projectId, rootDir);
|
|
3354
3634
|
if (!repoDir) {
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
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
|
+
}
|
|
3359
3644
|
}
|
|
3360
3645
|
if (!existsSync7(repoDir)) {
|
|
3361
3646
|
const reason = `linked directory "${repoDir}" does not exist`;
|
|
@@ -3529,6 +3814,16 @@ ${divider}`);
|
|
|
3529
3814
|
await api.post(`/api/tasks/${task.id}/updates`, {
|
|
3530
3815
|
message,
|
|
3531
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
|
+
],
|
|
3532
3827
|
source: "system"
|
|
3533
3828
|
});
|
|
3534
3829
|
return uploadData.url;
|
|
@@ -3536,10 +3831,34 @@ ${divider}`);
|
|
|
3536
3831
|
return null;
|
|
3537
3832
|
}
|
|
3538
3833
|
},
|
|
3539
|
-
|
|
3540
|
-
|
|
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
|
+
}
|
|
3541
3856
|
},
|
|
3542
|
-
|
|
3857
|
+
postUpdate: async (payload) => {
|
|
3858
|
+
await postTaskUpdate(task.id, payload);
|
|
3859
|
+
},
|
|
3860
|
+
onProgress: (msg) => logInfo(prefix, msg),
|
|
3861
|
+
recordingContext: "watch-completion"
|
|
3543
3862
|
});
|
|
3544
3863
|
await api.patch(`/api/tasks/${task.id}`, { status: "completed", testResult: result.status });
|
|
3545
3864
|
logSuccess(prefix, result.summary);
|
|
@@ -4525,8 +4844,8 @@ var resumeCommand = new Command17("resume").description("Resume an interactive C
|
|
|
4525
4844
|
|
|
4526
4845
|
// cli/commands/browse.ts
|
|
4527
4846
|
import { Command as Command18 } from "commander";
|
|
4528
|
-
import { execSync as
|
|
4529
|
-
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";
|
|
4530
4849
|
import { join as join8 } from "path";
|
|
4531
4850
|
var BROWSE_DIR2 = join8(import.meta.dirname, "..", "..", "browse");
|
|
4532
4851
|
function isProcessAlive(pid) {
|
|
@@ -4573,7 +4892,7 @@ async function ensureDevServer() {
|
|
|
4573
4892
|
env: { ...process.env }
|
|
4574
4893
|
});
|
|
4575
4894
|
devProc.unref();
|
|
4576
|
-
|
|
4895
|
+
writeFileSync4(
|
|
4577
4896
|
devStateFile,
|
|
4578
4897
|
JSON.stringify({ pid: devProc.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
4579
4898
|
{ mode: 384 }
|
|
@@ -4601,7 +4920,7 @@ var browseCommand = new Command18("browse").description("Control a headless brow
|
|
|
4601
4920
|
if (command === "setup") {
|
|
4602
4921
|
console.log("[browse] Running browse daemon setup...");
|
|
4603
4922
|
try {
|
|
4604
|
-
|
|
4923
|
+
execSync4("./setup", { cwd: BROWSE_DIR2, stdio: "inherit" });
|
|
4605
4924
|
} catch {
|
|
4606
4925
|
console.error("[browse] Setup failed");
|
|
4607
4926
|
process.exit(1);
|
|
@@ -4694,7 +5013,7 @@ var setPathCommand = new Command19("set-path").description("Set or update the lo
|
|
|
4694
5013
|
// cli/commands/test.ts
|
|
4695
5014
|
import { Command as Command20 } from "commander";
|
|
4696
5015
|
import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
|
|
4697
|
-
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) => {
|
|
4698
5017
|
const config = loadConfig();
|
|
4699
5018
|
console.log("[test] Fetching task...");
|
|
4700
5019
|
let task;
|
|
@@ -4742,7 +5061,7 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
4742
5061
|
}
|
|
4743
5062
|
}
|
|
4744
5063
|
try {
|
|
4745
|
-
await api.patch(`/api/tasks/${taskId}`, {
|
|
5064
|
+
await api.patch(`/api/tasks/${taskId}`, { status: "delegated", testResult: null });
|
|
4746
5065
|
} catch {
|
|
4747
5066
|
}
|
|
4748
5067
|
console.log(`[test] Starting test for "${task.title}"...`);
|
|
@@ -4772,6 +5091,16 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
4772
5091
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
4773
5092
|
message,
|
|
4774
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
|
+
],
|
|
4775
5104
|
source: "system"
|
|
4776
5105
|
});
|
|
4777
5106
|
return uploadData.url;
|
|
@@ -4779,20 +5108,52 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
4779
5108
|
return null;
|
|
4780
5109
|
}
|
|
4781
5110
|
},
|
|
4782
|
-
|
|
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
|
+
}) => {
|
|
4783
5139
|
try {
|
|
4784
5140
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
4785
5141
|
message,
|
|
4786
5142
|
imageUrl,
|
|
5143
|
+
media,
|
|
5144
|
+
proofState,
|
|
5145
|
+
proofDetails,
|
|
4787
5146
|
source: "system"
|
|
4788
5147
|
});
|
|
4789
5148
|
} catch {
|
|
4790
5149
|
}
|
|
4791
5150
|
},
|
|
4792
|
-
onProgress: (msg) => console.log(`[test] ${msg}`)
|
|
5151
|
+
onProgress: (msg) => console.log(`[test] ${msg}`),
|
|
5152
|
+
recordingEnabled: opts.recording !== false,
|
|
5153
|
+
recordingContext: "test-run"
|
|
4793
5154
|
});
|
|
4794
5155
|
try {
|
|
4795
|
-
await api.patch(`/api/tasks/${taskId}`, {
|
|
5156
|
+
await api.patch(`/api/tasks/${taskId}`, { status: "completed", testResult: result.status });
|
|
4796
5157
|
} catch {
|
|
4797
5158
|
}
|
|
4798
5159
|
console.log(`
|
|
@@ -4815,7 +5176,7 @@ var testCommand = new Command20("test").description("Run automated browser test
|
|
|
4815
5176
|
|
|
4816
5177
|
// cli/commands/features.ts
|
|
4817
5178
|
import { Command as Command21 } from "commander";
|
|
4818
|
-
import { readFileSync as readFileSync9, writeFileSync as
|
|
5179
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync12 } from "fs";
|
|
4819
5180
|
import { resolve as resolve5, sep as sep2 } from "path";
|
|
4820
5181
|
var FEATURES_FILE3 = ".mr-features.md";
|
|
4821
5182
|
var c7 = {
|
|
@@ -4856,13 +5217,13 @@ var featuresCommand = new Command21("features").description("View or update the
|
|
|
4856
5217
|
if (opts.file) {
|
|
4857
5218
|
const content2 = readFileSync9(resolve5(opts.file), "utf-8");
|
|
4858
5219
|
const featuresPath = getFeaturesPath();
|
|
4859
|
-
|
|
5220
|
+
writeFileSync5(featuresPath, content2);
|
|
4860
5221
|
console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)} from ${paint7("cyan", opts.file)}`);
|
|
4861
5222
|
return;
|
|
4862
5223
|
}
|
|
4863
5224
|
if (opts.update) {
|
|
4864
5225
|
const featuresPath = getFeaturesPath();
|
|
4865
|
-
|
|
5226
|
+
writeFileSync5(featuresPath, opts.update);
|
|
4866
5227
|
console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)}`);
|
|
4867
5228
|
return;
|
|
4868
5229
|
}
|
|
@@ -4877,12 +5238,12 @@ var featuresCommand = new Command21("features").description("View or update the
|
|
|
4877
5238
|
|
|
4878
5239
|
// cli/commands/no-mr.ts
|
|
4879
5240
|
import { Command as Command22 } from "commander";
|
|
4880
|
-
import { writeFileSync as
|
|
5241
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
4881
5242
|
import { resolve as resolve6 } from "path";
|
|
4882
5243
|
var NO_MR_FILE = ".mr-no-mr";
|
|
4883
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) => {
|
|
4884
5245
|
const filePath = resolve6(process.cwd(), NO_MR_FILE);
|
|
4885
|
-
|
|
5246
|
+
writeFileSync6(filePath, description, "utf-8");
|
|
4886
5247
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
4887
5248
|
message: `No MR/PR needed \u2014 ${description}`,
|
|
4888
5249
|
source: "agent"
|
|
@@ -4964,7 +5325,7 @@ async function authenticateBrowseSession(magicUrl, runBrowse) {
|
|
|
4964
5325
|
// lib/scanner/codebase-analysis.ts
|
|
4965
5326
|
import { readdirSync as readdirSync2, readFileSync as readFileSync11, existsSync as existsSync14 } from "fs";
|
|
4966
5327
|
import { join as join10, relative } from "path";
|
|
4967
|
-
import { execSync as
|
|
5328
|
+
import { execSync as execSync5 } from "child_process";
|
|
4968
5329
|
function resolveDir(projectPath, candidates) {
|
|
4969
5330
|
for (const candidate of candidates) {
|
|
4970
5331
|
const dir = join10(projectPath, candidate);
|
|
@@ -5090,7 +5451,7 @@ function extractInternalLinks(projectPath) {
|
|
|
5090
5451
|
}
|
|
5091
5452
|
function getRecentCommits(projectPath, count = 20) {
|
|
5092
5453
|
try {
|
|
5093
|
-
const output =
|
|
5454
|
+
const output = execSync5(
|
|
5094
5455
|
`git log --oneline -${count} --no-decorate`,
|
|
5095
5456
|
{ cwd: projectPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
5096
5457
|
);
|