@dunnewold-labs/mr-manager 0.4.14 → 0.4.18

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.
Files changed (2) hide show
  1. package/dist/index.mjs +540 -78
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // cli/index.ts
4
- import { Command as Command27 } from "commander";
4
+ import { Command as Command28 } from "commander";
5
5
  import { existsSync as existsSync17 } from "fs";
6
6
  import { homedir as homedir3 } from "os";
7
7
  import { join as join12 } from "path";
@@ -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.14",
188
+ version: "0.4.18",
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 writeFileSync4 } from "fs";
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
- await postUpdate(result.summary);
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 execSync4, spawn as spawn3 } from "child_process";
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
- execSync4("which bun", { stdio: "pipe" });
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
- async function postTaskUpdate(taskId, message, source = "system") {
1404
+ function isLikelyNetworkFailureDetail(detail) {
1405
+ return isLikelyNetworkError(new Error(detail));
1406
+ }
1407
+ async function postTaskUpdate(taskId, payloadOrMessage, source = "system") {
1251
1408
  try {
1252
- await api.post(`/api/tasks/${taskId}/updates`, { message, source });
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
- writeFileSync4(bodyPath, `${body}
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
- writeFileSync4(tmpPath, file.content, "utf-8");
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 branchName = `${owner}/${slug}`;
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
- ...executionDir && executionDir !== repoDir ? [
1770
- `2. Your isolated git worktree is already prepared and this session starts inside it.`,
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 in this worktree: ${branchName}`,
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
- writeFileSync4(prdTmpPath, existingPrd, "utf-8");
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
- writeFileSync4(tmpPath, f.content, "utf-8");
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 = `${owner}/${slug}`;
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 shouldResumeClaudeSession = attemptAgent === "claude" && hasFeedback && !!task.claudeSessionId;
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
- const repoDir = findDirectoryForProject(config, task.projectId, rootDir);
3633
+ let repoDir = findDirectoryForProject(config, task.projectId, rootDir);
3354
3634
  if (!repoDir) {
3355
- const reason = `no linked directory found under ${rootDir}`;
3356
- logError(prefix, `"${task.title}": ${reason} \u2014 will not retry`);
3357
- failed.set(task.id, reason);
3358
- continue;
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
- postUpdate: async (message, imageUrl) => {
3540
- await postTaskUpdate(task.id, message);
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);
3541
3859
  },
3542
- onProgress: (msg) => logInfo(prefix, msg)
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);
@@ -4238,7 +4557,7 @@ async function checkApiConnectivity() {
4238
4557
  }
4239
4558
  }
4240
4559
  function printResults(checks) {
4241
- const maxNameLen = Math.max(...checks.map((c10) => c10.name.length));
4560
+ const maxNameLen = Math.max(...checks.map((c11) => c11.name.length));
4242
4561
  let allOk = true;
4243
4562
  for (const check of checks) {
4244
4563
  const isOptional = check.optional ?? false;
@@ -4252,10 +4571,10 @@ function printResults(checks) {
4252
4571
  }
4253
4572
  async function autoFix(checks, agent) {
4254
4573
  const { spawn: spawn8 } = await import("child_process");
4255
- const ghInstalled = checks.find((c10) => c10.name === "GitHub CLI (gh)").ok;
4256
- const ghAuthed = checks.find((c10) => c10.name === "GitHub CLI auth").ok;
4257
- const mrAuthed = checks.find((c10) => c10.name === "Mr. Manager CLI auth").ok;
4258
- const claudeCheck = checks.find((c10) => c10.name === "Claude Code (claude)");
4574
+ const ghInstalled = checks.find((c11) => c11.name === "GitHub CLI (gh)").ok;
4575
+ const ghAuthed = checks.find((c11) => c11.name === "GitHub CLI auth").ok;
4576
+ const mrAuthed = checks.find((c11) => c11.name === "Mr. Manager CLI auth").ok;
4577
+ const claudeCheck = checks.find((c11) => c11.name === "Claude Code (claude)");
4259
4578
  if (claudeCheck && !claudeCheck.ok && agent === "claude") {
4260
4579
  console.log(paint5("cyan", " Installing Claude Code..."));
4261
4580
  console.log(paint5("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
@@ -4318,7 +4637,7 @@ var setupCommand = new Command14("setup").description("Check that all dependenci
4318
4637
  console.log("");
4319
4638
  return;
4320
4639
  }
4321
- const fixes = checks.filter((c10) => !c10.ok && c10.fix && !c10.optional);
4640
+ const fixes = checks.filter((c11) => !c11.ok && c11.fix && !c11.optional);
4322
4641
  if (fixes.length > 0) {
4323
4642
  console.log(paint5("yellow", " To fix:"));
4324
4643
  for (const fix of fixes) {
@@ -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 execSync5, spawn as spawn6 } from "child_process";
4529
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
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
- writeFileSync5(
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
- execSync5("./setup", { cwd: BROWSE_DIR2, stdio: "inherit" });
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}`, { testStatus: "running" });
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
- postUpdate: async (message, imageUrl) => {
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}`, { testStatus: result.status });
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 writeFileSync6, existsSync as existsSync12 } from "fs";
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
- writeFileSync6(featuresPath, content2);
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
- writeFileSync6(featuresPath, opts.update);
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 writeFileSync7 } from "fs";
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
- writeFileSync7(filePath, description, "utf-8");
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 execSync6 } from "child_process";
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 = execSync6(
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
  );
@@ -5358,10 +5719,10 @@ ${codebaseAnalysis.routes.map((r) => `- ${r}`).join("\n")}
5358
5719
  ${codebaseAnalysis.prismaModels.map((m) => `- ${m}`).join("\n")}
5359
5720
 
5360
5721
  **Components:**
5361
- ${codebaseAnalysis.components.slice(0, 15).map((c10) => `- ${c10}`).join("\n")}
5722
+ ${codebaseAnalysis.components.slice(0, 15).map((c11) => `- ${c11}`).join("\n")}
5362
5723
 
5363
5724
  **Recent Git Commits:**
5364
- ${codebaseAnalysis.recentCommits.slice(0, 8).map((c10) => `- ${c10}`).join("\n")}
5725
+ ${codebaseAnalysis.recentCommits.slice(0, 8).map((c11) => `- ${c11}`).join("\n")}
5365
5726
 
5366
5727
  **Completed Tasks:**
5367
5728
  ${context.completedTasks.slice(0, 10).map((t) => `- ${t.title}`).join("\n") || "None"}
@@ -6008,7 +6369,7 @@ var doctorCommand = new Command25("doctor").description("Diagnose Mr. Manager CL
6008
6369
  console.log("");
6009
6370
  return;
6010
6371
  }
6011
- const fixes = checks.filter((c10) => !c10.ok && c10.fix && !c10.optional);
6372
+ const fixes = checks.filter((c11) => !c11.ok && c11.fix && !c11.optional);
6012
6373
  if (fixes.length > 0) {
6013
6374
  console.log(paint5("yellow", " To fix:"));
6014
6375
  for (const fix of fixes) {
@@ -6239,6 +6600,106 @@ ${r.jobType} [${r.identifier}]`);
6239
6600
  }
6240
6601
  });
6241
6602
 
6603
+ // cli/commands/skill.ts
6604
+ import { Command as Command27 } from "commander";
6605
+ var c10 = {
6606
+ reset: "\x1B[0m",
6607
+ bold: "\x1B[1m",
6608
+ dim: "\x1B[2m",
6609
+ cyan: "\x1B[36m",
6610
+ green: "\x1B[32m",
6611
+ yellow: "\x1B[33m"
6612
+ };
6613
+ var skillCommand = new Command27("skill").description("Manage skills \u2014 reusable playbooks for AI agents");
6614
+ skillCommand.command("list").alias("ls").description("List all skills").option("--category <category>", "Filter by category").action(async (opts) => {
6615
+ const params = new URLSearchParams();
6616
+ if (opts.category) params.set("category", opts.category);
6617
+ const skills = await api.get(
6618
+ `/api/skills${params.toString() ? `?${params}` : ""}`
6619
+ );
6620
+ if (skills.length === 0) {
6621
+ console.log(`${c10.dim}No skills found.${c10.reset}`);
6622
+ return;
6623
+ }
6624
+ for (const skill of skills) {
6625
+ const cat = skill.category ? ` ${c10.dim}[${skill.category}]${c10.reset}` : "";
6626
+ const scope = skill.projectId ? ` ${c10.dim}(project)${c10.reset}` : ` ${c10.dim}(global)${c10.reset}`;
6627
+ console.log(` ${c10.cyan}${skill.name}${c10.reset}${cat}${scope}`);
6628
+ if (skill.description) {
6629
+ console.log(` ${c10.dim}${skill.description}${c10.reset}`);
6630
+ }
6631
+ console.log(` ${c10.dim}id: ${skill.id}${c10.reset}`);
6632
+ }
6633
+ });
6634
+ skillCommand.command("create").description("Create a new skill from a markdown file or inline content").argument("<name>", "Skill name").option("-d, --description <desc>", "Short description").option("-c, --category <cat>", "Category (e.g. Deployment, Testing)").option("-f, --file <path>", "Read content from a markdown file").option("--content <text>", "Inline markdown content").option("-p, --project", "Scope to the linked project").action(async (name, opts) => {
6635
+ let content = opts.content ?? "";
6636
+ if (opts.file) {
6637
+ const { readFileSync: readFileSync13 } = await import("fs");
6638
+ try {
6639
+ content = readFileSync13(opts.file, "utf-8");
6640
+ } catch (err) {
6641
+ console.error(`Failed to read file: ${err.message}`);
6642
+ process.exit(1);
6643
+ }
6644
+ }
6645
+ if (!content.trim()) {
6646
+ console.error("Content is required. Use --file <path> or --content <text>.");
6647
+ process.exit(1);
6648
+ }
6649
+ let projectId = null;
6650
+ if (opts.project) {
6651
+ projectId = getLinkedProjectId();
6652
+ if (!projectId) {
6653
+ console.error(
6654
+ 'No project linked to this directory. Run "mr link <project-id>" first.'
6655
+ );
6656
+ process.exit(1);
6657
+ }
6658
+ }
6659
+ const skill = await api.post("/api/skills", {
6660
+ name,
6661
+ description: opts.description ?? null,
6662
+ content: content.trim(),
6663
+ category: opts.category ?? null,
6664
+ projectId
6665
+ });
6666
+ console.log(
6667
+ `${c10.green}Created skill:${c10.reset} ${c10.bold}${skill.name}${c10.reset} ${c10.dim}(${skill.id})${c10.reset}`
6668
+ );
6669
+ });
6670
+ skillCommand.command("generate").alias("gen").description("Generate a new skill using AI from a text prompt").argument("<prompt>", "Describe the skill to generate").option("-p, --project", "Scope to the linked project").action(async (prompt2, opts) => {
6671
+ let projectId = null;
6672
+ if (opts.project) {
6673
+ projectId = getLinkedProjectId();
6674
+ if (!projectId) {
6675
+ console.error(
6676
+ 'No project linked to this directory. Run "mr link <project-id>" first.'
6677
+ );
6678
+ process.exit(1);
6679
+ }
6680
+ }
6681
+ console.log(`${c10.dim}Generating skill...${c10.reset}`);
6682
+ try {
6683
+ const skill = await api.post("/api/skills/generate", {
6684
+ prompt: prompt2,
6685
+ projectId
6686
+ });
6687
+ console.log(
6688
+ `${c10.green}Generated skill:${c10.reset} ${c10.bold}${skill.name}${c10.reset}`
6689
+ );
6690
+ if (skill.description) {
6691
+ console.log(` ${c10.dim}${skill.description}${c10.reset}`);
6692
+ }
6693
+ if (skill.category) {
6694
+ console.log(` ${c10.dim}Category: ${skill.category}${c10.reset}`);
6695
+ }
6696
+ console.log(` ${c10.dim}id: ${skill.id}${c10.reset}`);
6697
+ } catch (err) {
6698
+ console.error(`Failed to generate skill: ${err.message}`);
6699
+ process.exit(1);
6700
+ }
6701
+ });
6702
+
6242
6703
  // cli/index.ts
6243
6704
  var configPath = join12(homedir3(), ".mr-manager", "config.json");
6244
6705
  var isFirstRun = !existsSync17(configPath);
@@ -6246,7 +6707,7 @@ var userArgs = process.argv.slice(2);
6246
6707
  var bypassCommands = /* @__PURE__ */ new Set(["login", "init", "auth", "help", "--help", "-h", "--version", "-V", "doctor", "setup"]);
6247
6708
  var shouldBypass = userArgs.length > 0 && bypassCommands.has(userArgs[0]);
6248
6709
  if (isFirstRun && !shouldBypass) {
6249
- const c10 = {
6710
+ const c11 = {
6250
6711
  reset: "\x1B[0m",
6251
6712
  bold: "\x1B[1m",
6252
6713
  dim: "\x1B[2m",
@@ -6256,28 +6717,28 @@ if (isFirstRun && !shouldBypass) {
6256
6717
  magenta: "\x1B[35m"
6257
6718
  };
6258
6719
  console.log("");
6259
- console.log(`${c10.cyan} \u2554\u2566\u2557\u2566\u2550\u2557 \u2554\u2566\u2557\u2554\u2550\u2557\u2554\u2557\u2554\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2566\u2550\u2557${c10.reset}`);
6260
- console.log(`${c10.magenta} \u2551\u2551\u2551\u2560\u2566\u255D \u2551\u2551\u2551\u2560\u2550\u2563\u2551\u2551\u2551\u2560\u2550\u2563\u2551 \u2566\u2551\u2563 \u2560\u2566\u255D${c10.reset}`);
6261
- console.log(`${c10.cyan} \u2569 \u2569\u2569\u255A\u2550 \u2569 \u2569\u2569 \u2569\u255D\u255A\u255D\u2569 \u2569\u255A\u2550\u255D\u255A\u2550\u255D\u2569\u255A\u2550${c10.reset}`);
6262
- console.log(`${c10.dim} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c10.reset}`);
6720
+ console.log(`${c11.cyan} \u2554\u2566\u2557\u2566\u2550\u2557 \u2554\u2566\u2557\u2554\u2550\u2557\u2554\u2557\u2554\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2566\u2550\u2557${c11.reset}`);
6721
+ console.log(`${c11.magenta} \u2551\u2551\u2551\u2560\u2566\u255D \u2551\u2551\u2551\u2560\u2550\u2563\u2551\u2551\u2551\u2560\u2550\u2563\u2551 \u2566\u2551\u2563 \u2560\u2566\u255D${c11.reset}`);
6722
+ console.log(`${c11.cyan} \u2569 \u2569\u2569\u255A\u2550 \u2569 \u2569\u2569 \u2569\u255D\u255A\u255D\u2569 \u2569\u255A\u2550\u255D\u255A\u2550\u255D\u2569\u255A\u2550${c11.reset}`);
6723
+ console.log(`${c11.dim} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c11.reset}`);
6263
6724
  console.log("");
6264
- console.log(`${c10.bold} Welcome to Mr. Manager!${c10.reset}`);
6265
- console.log(`${c10.dim} Let's get you set up in a few quick steps.${c10.reset}`);
6725
+ console.log(`${c11.bold} Welcome to Mr. Manager!${c11.reset}`);
6726
+ console.log(`${c11.dim} Let's get you set up in a few quick steps.${c11.reset}`);
6266
6727
  console.log("");
6267
- console.log(` ${c10.yellow}Step 1:${c10.reset} Authenticate via Google OAuth`);
6268
- console.log(` ${c10.dim}Run:${c10.reset} ${c10.cyan}mr login${c10.reset}`);
6728
+ console.log(` ${c11.yellow}Step 1:${c11.reset} Authenticate via Google OAuth`);
6729
+ console.log(` ${c11.dim}Run:${c11.reset} ${c11.cyan}mr login${c11.reset}`);
6269
6730
  console.log("");
6270
- console.log(` ${c10.yellow}Step 2:${c10.reset} Verify your environment`);
6271
- console.log(` ${c10.dim}Run:${c10.reset} ${c10.cyan}mr setup${c10.reset}`);
6731
+ console.log(` ${c11.yellow}Step 2:${c11.reset} Verify your environment`);
6732
+ console.log(` ${c11.dim}Run:${c11.reset} ${c11.cyan}mr setup${c11.reset}`);
6272
6733
  console.log("");
6273
- console.log(` ${c10.yellow}Step 3:${c10.reset} Link a repo and start watching`);
6274
- console.log(` ${c10.dim}Run:${c10.reset} ${c10.cyan}mr link${c10.reset} ${c10.dim}&&${c10.reset} ${c10.cyan}mr watch${c10.reset}`);
6734
+ console.log(` ${c11.yellow}Step 3:${c11.reset} Link a repo and start watching`);
6735
+ console.log(` ${c11.dim}Run:${c11.reset} ${c11.cyan}mr link${c11.reset} ${c11.dim}&&${c11.reset} ${c11.cyan}mr watch${c11.reset}`);
6275
6736
  console.log("");
6276
- console.log(`${c10.dim} Or run ${c10.reset}${c10.cyan}mr login${c10.reset}${c10.dim} to get started now.${c10.reset}`);
6737
+ console.log(`${c11.dim} Or run ${c11.reset}${c11.cyan}mr login${c11.reset}${c11.dim} to get started now.${c11.reset}`);
6277
6738
  console.log("");
6278
6739
  process.exit(0);
6279
6740
  }
6280
- var program = new Command27();
6741
+ var program = new Command28();
6281
6742
  program.name("mr").description("Mr. Manager - Task and project management CLI").version(CLI_VERSION);
6282
6743
  program.addCommand(initCommand);
6283
6744
  program.addCommand(authCommand);
@@ -6308,4 +6769,5 @@ program.addCommand(scanCommand);
6308
6769
  program.addCommand(ideaCommand);
6309
6770
  program.addCommand(doctorCommand);
6310
6771
  program.addCommand(promptAuditCommand);
6772
+ program.addCommand(skillCommand);
6311
6773
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunnewold-labs/mr-manager",
3
- "version": "0.4.14",
3
+ "version": "0.4.18",
4
4
  "description": "Mr. Manager - Task and project management CLI",
5
5
  "bin": {
6
6
  "mr": "./dist/index.mjs"