@dunnewold-labs/mr-manager 0.4.52 → 0.4.55

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 +182 -26
  2. 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.52",
188
+ version: "0.4.55",
189
189
  description: "Mr. Manager - Task and project management CLI",
190
190
  bin: {
191
191
  mr: "./dist/index.mjs"
@@ -1151,6 +1151,118 @@ function getAvailableAgentFallbackChain(agent, availability) {
1151
1151
  return getAgentFallbackChain(agent).filter((candidate) => availability[candidate] !== false);
1152
1152
  }
1153
1153
 
1154
+ // lib/permissions.ts
1155
+ var DEFAULT_HEADLESS_MODE = "bypass";
1156
+ var ENV_VAR = "MR_HEADLESS_PERMISSION_MODE";
1157
+ var DESTRUCTIVE_BASH_DENY = [
1158
+ // Privilege escalation.
1159
+ "Bash(sudo:*)",
1160
+ "Bash(su:*)",
1161
+ // Recursive deletes of broad / out-of-repo roots.
1162
+ "Bash(rm -rf /:*)",
1163
+ "Bash(rm -rf /*:*)",
1164
+ "Bash(rm -fr /:*)",
1165
+ "Bash(rm -rf ~:*)",
1166
+ "Bash(rm -rf ~/:*)",
1167
+ "Bash(rm -rf $HOME:*)",
1168
+ "Bash(rm -rf ..:*)",
1169
+ // Filesystem / device destruction.
1170
+ "Bash(mkfs:*)",
1171
+ "Bash(dd:*)",
1172
+ // Mass permission/ownership changes from root.
1173
+ "Bash(chmod -R 777 /:*)",
1174
+ "Bash(chown -R:*)",
1175
+ // Force-push (history rewrite / remote clobber).
1176
+ "Bash(git push --force:*)",
1177
+ "Bash(git push -f:*)",
1178
+ "Bash(git push origin --force:*)",
1179
+ "Bash(git push origin -f:*)",
1180
+ // Remote pipe-to-shell (curl|sh / wget|bash).
1181
+ "Bash(curl:* | sh)",
1182
+ "Bash(curl:* | bash)",
1183
+ "Bash(wget:* | sh)",
1184
+ "Bash(wget:* | bash)"
1185
+ ];
1186
+ var OUT_OF_REPO_WRITE_DENY = [
1187
+ "Write(/etc/**)",
1188
+ "Edit(/etc/**)",
1189
+ "Write(//usr/**)",
1190
+ "Edit(//usr/**)",
1191
+ "Write(~/.ssh/**)",
1192
+ "Edit(~/.ssh/**)",
1193
+ "Write(~/.aws/**)",
1194
+ "Edit(~/.aws/**)"
1195
+ ];
1196
+ var READ_ONLY_EXTRA_DENY = ["Edit", "Write", "MultiEdit", "NotebookEdit"];
1197
+ function resolveHeadlessMode(config) {
1198
+ const fromEnv = process.env[ENV_VAR]?.trim().toLowerCase();
1199
+ if (fromEnv === "auto" || fromEnv === "bypass") return fromEnv;
1200
+ const fromConfig = config?.headlessPermissionMode?.trim().toLowerCase();
1201
+ if (fromConfig === "auto" || fromConfig === "bypass") return fromConfig;
1202
+ if (config?.claudePermissionMode != null && config.headlessPermissionMode == null) {
1203
+ return "bypass";
1204
+ }
1205
+ return DEFAULT_HEADLESS_MODE;
1206
+ }
1207
+ function claudeDenyList(runKind) {
1208
+ const base = [...DESTRUCTIVE_BASH_DENY, ...OUT_OF_REPO_WRITE_DENY];
1209
+ if (runKind === "review" || runKind === "scan") {
1210
+ return [...base, ...READ_ONLY_EXTRA_DENY];
1211
+ }
1212
+ return base;
1213
+ }
1214
+ function resolvePermissionArgs(agent, runKind, mode) {
1215
+ if (runKind === "plan") {
1216
+ if (agent === "claude") {
1217
+ return { args: ["--permission-mode", "plan"], effectiveMode: "plan", mappedToBypassForLackOfSandbox: false };
1218
+ }
1219
+ return resolvePermissionArgs(agent, "execute", mode);
1220
+ }
1221
+ if (mode === "bypass") {
1222
+ return { args: bypassArgs(agent, runKind), effectiveMode: "bypass", mappedToBypassForLackOfSandbox: false };
1223
+ }
1224
+ switch (agent) {
1225
+ case "claude": {
1226
+ const deny = claudeDenyList(runKind);
1227
+ return {
1228
+ args: ["--permission-mode", "acceptEdits", "--disallowedTools", deny.join(",")],
1229
+ effectiveMode: "auto",
1230
+ mappedToBypassForLackOfSandbox: false
1231
+ };
1232
+ }
1233
+ case "codex": {
1234
+ return { args: ["-s", "workspace-write"], effectiveMode: "auto", mappedToBypassForLackOfSandbox: false };
1235
+ }
1236
+ case "antigravity":
1237
+ return { args: ["--dangerously-skip-permissions"], effectiveMode: "bypass", mappedToBypassForLackOfSandbox: true };
1238
+ default:
1239
+ return { args: bypassArgs(agent, runKind), effectiveMode: "bypass", mappedToBypassForLackOfSandbox: true };
1240
+ }
1241
+ }
1242
+ function bypassArgs(agent, _runKind) {
1243
+ switch (agent) {
1244
+ case "codex":
1245
+ return ["-s", "danger-full-access"];
1246
+ case "antigravity":
1247
+ case "claude":
1248
+ default:
1249
+ return ["--dangerously-skip-permissions"];
1250
+ }
1251
+ }
1252
+ function describePermissionMode(res) {
1253
+ if (res.mappedToBypassForLackOfSandbox) {
1254
+ return "bypass (no scoped sandbox for this agent \u2014 full access)";
1255
+ }
1256
+ switch (res.effectiveMode) {
1257
+ case "auto":
1258
+ return "auto (scoped: acceptEdits + destructive-command deny-list)";
1259
+ case "plan":
1260
+ return "plan (read-only)";
1261
+ case "bypass":
1262
+ return "bypass (--dangerously-skip-permissions \u2014 full access)";
1263
+ }
1264
+ }
1265
+
1154
1266
  // lib/task-workflow.ts
1155
1267
  function normalizeWhitespace(value) {
1156
1268
  return value.replace(/\s+/g, " ").trim();
@@ -1874,14 +1986,23 @@ function mergePrViaCli(prUrl, repoDir, vcs = "github") {
1874
1986
  });
1875
1987
  });
1876
1988
  }
1877
- function buildPrototypeSection(protoRefs, workingDir) {
1989
+ function buildPrototypeSection(protoRefs, workingDir, mode = "build") {
1878
1990
  if (protoRefs.length === 0) return "";
1879
- const sections = [
1991
+ const sections = mode === "prd" ? [
1880
1992
  ``,
1881
1993
  `## Referenced Prototypes`,
1882
1994
  ``,
1883
- `The following prototype designs have been linked to this task as reference. Use them to guide the implementation.`,
1884
- `Read the referenced files when you need to see the HTML content.`,
1995
+ `The following prototype designs have been linked to this task. They represent the intended product UI. **Read every referenced HTML file in full** (paths below) and base the PRD's UI/functional requirements on what they show: page/screen structure, layout, every component and element, all the states depicted (empty/loading/error/populated, hover/active/disabled, etc.), navigation, and the interactions the design implies. Enumerate these as concrete requirements so the implementation phase builds the complete prototype, not just a restyle.`,
1996
+ ``
1997
+ ] : [
1998
+ ``,
1999
+ `## Referenced Prototypes`,
2000
+ ``,
2001
+ `The following prototype designs have been linked to this task. They are not loose visual inspiration \u2014 they are the intended product. Your job is to BUILD what each prototype shows: its full page/screen structure, layout, all the UI components and elements, every state shown (empty/loading/error/populated, hover/active/disabled, etc.), navigation, and the interactions and behaviors the design implies. Do not stop at restyling or "rethemeing" existing UI \u2014 implement the complete structure and functionality, wired to real data and logic where the rest of the task requires it.`,
2002
+ ``,
2003
+ `**Before you start implementing, read every referenced HTML file in full** (paths are given below). Do not skip this \u2014 the HTML is the source of truth for what to build. Inspect the markup to enumerate the components, sections, and states you need to create.`,
2004
+ ``,
2005
+ `Where the prototype and the task PRD/notes conflict, the PRD/notes win; otherwise treat the prototype as the spec for the UI.`,
1885
2006
  ``
1886
2007
  ];
1887
2008
  for (const ref of protoRefs) {
@@ -1889,8 +2010,13 @@ function buildPrototypeSection(protoRefs, workingDir) {
1889
2010
  const files = proto.files ?? [];
1890
2011
  const selected = ref.selectedVariants ?? Array.from({ length: proto.variantCount }, (_, i) => i);
1891
2012
  const selectedFiles = files.filter((_, i) => selected.includes(i));
2013
+ const fidelity = (proto.fidelity ?? "high").toLowerCase();
2014
+ const isLowFi = fidelity === "low" || fidelity === "low_fidelity";
2015
+ const fidelityNote = isLowFi ? `This is a LOW-FIDELITY wireframe. Build the full structure, layout, components, and behavior it shows, but do NOT copy its placeholder visual style (greys, boxes, sketchy borders) \u2014 apply the project's real design system / existing styling conventions.` : `This is a HIGH-FIDELITY design. Treat it as the definitive brief for both structure/behavior AND look-and-feel \u2014 match its layout, spacing, colors, typography, and component styling faithfully.`;
1892
2016
  sections.push(`### ${proto.title}`);
1893
2017
  sections.push(``);
2018
+ sections.push(fidelityNote);
2019
+ sections.push(``);
1894
2020
  if (selectedFiles.length === 0) {
1895
2021
  sections.push(`(No variant files available)`);
1896
2022
  sections.push(``);
@@ -1905,7 +2031,7 @@ function buildPrototypeSection(protoRefs, workingDir) {
1905
2031
  try {
1906
2032
  writeFileSync3(tmpPath, file.content, "utf-8");
1907
2033
  sections.push(`#### ${variantLabel}: ${file.name}`);
1908
- sections.push(`File: \`${tmpPath}\` \u2014 read this file to see the full HTML content.`);
2034
+ sections.push(`File: \`${tmpPath}\` \u2014 read this file in full before implementing; it is the source of truth for what to build.`);
1909
2035
  } catch {
1910
2036
  sections.push(`#### ${variantLabel}: ${file.name}`);
1911
2037
  sections.push(`\`\`\`html`);
@@ -2171,7 +2297,7 @@ ${task.notes}` : "";
2171
2297
  `Complete all steps autonomously without asking for confirmation. Exit with code 0 when done.`
2172
2298
  ].join("\n");
2173
2299
  }
2174
- function buildPrdPrompt(task, repoDir, existingPrd, feedbackUpdates = []) {
2300
+ function buildPrdPrompt(task, repoDir, existingPrd, feedbackUpdates = [], protoRefs = []) {
2175
2301
  const notes = task.notes ? `
2176
2302
 
2177
2303
  Task notes:
@@ -2223,6 +2349,7 @@ ${task.notes}` : "";
2223
2349
  `Title: ${task.title}`,
2224
2350
  `ID: ${task.id}${notes}`,
2225
2351
  feedbackSection,
2352
+ buildPrototypeSection(protoRefs, repoDir, "prd"),
2226
2353
  `## Instructions`,
2227
2354
  ``,
2228
2355
  ...revisionInstructions,
@@ -2836,41 +2963,46 @@ function buildRefinementPrompt(proto, parentFiles, repoDir, options = {}) {
2836
2963
  ].join("\n");
2837
2964
  }
2838
2965
  function buildAgentArgs(agent, prompt2, mode, sessionId, name, resumeSession = false, systemPrompt, maxTurns, claudeModel) {
2966
+ const headlessMode = resolveHeadlessMode(loadConfig());
2967
+ const runKind = mode === "plan" ? "plan" : "execute";
2839
2968
  if (agent === "codex") {
2969
+ const permission2 = resolvePermissionArgs("codex", runKind, headlessMode);
2840
2970
  const args = [];
2841
2971
  if (mode === "execute") {
2842
2972
  args.push("-a", "never");
2843
2973
  }
2844
2974
  args.push("exec");
2845
2975
  if (mode === "execute") {
2846
- args.push("-s", "danger-full-access");
2976
+ args.push(...permission2.args);
2847
2977
  }
2848
2978
  const fullPrompt = systemPrompt ? `${prompt2}
2849
2979
 
2850
2980
  ${systemPrompt}` : prompt2;
2851
2981
  args.push(fullPrompt);
2852
- return { bin: "codex", args };
2982
+ return { bin: "codex", args, permission: permission2 };
2853
2983
  }
2854
2984
  if (agent === "antigravity") {
2985
+ const permission2 = resolvePermissionArgs("antigravity", runKind, headlessMode);
2855
2986
  const fullPrompt = systemPrompt ? `${prompt2}
2856
2987
 
2857
2988
  ${systemPrompt}` : prompt2;
2858
2989
  const args = ["-p", fullPrompt];
2859
2990
  if (mode === "execute") {
2860
- args.push("--dangerously-skip-permissions");
2991
+ args.push(...permission2.args);
2861
2992
  }
2862
- return { bin: AGENT_BINARIES.antigravity, args };
2993
+ return { bin: AGENT_BINARIES.antigravity, args, permission: permission2 };
2863
2994
  }
2995
+ const permission = resolvePermissionArgs("claude", runKind, headlessMode);
2864
2996
  const sessionArgs = sessionId ? resumeSession ? ["--resume", sessionId] : ["--session-id", sessionId] : [];
2865
2997
  const nameArgs = name ? ["--name", name] : [];
2866
2998
  const systemArgs = systemPrompt ? ["--append-system-prompt", systemPrompt] : [];
2867
2999
  const turnsArgs = maxTurns ? ["--max-turns", String(maxTurns)] : [];
2868
3000
  const modelArgs = claudeModel ? ["--model", claudeModel] : [];
2869
- if (mode === "plan") {
2870
- return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, ...turnsArgs, ...modelArgs, "--permission-mode", "plan", "-p", prompt2] };
2871
- }
2872
- const permissionArgs = ["--dangerously-skip-permissions"];
2873
- return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, ...turnsArgs, ...modelArgs, ...permissionArgs, "-p", prompt2] };
3001
+ return {
3002
+ bin: "claude",
3003
+ args: [...sessionArgs, ...nameArgs, ...systemArgs, ...turnsArgs, ...modelArgs, ...permission.args, "-p", prompt2],
3004
+ permission
3005
+ };
2874
3006
  }
2875
3007
  var AGENT_BINARIES = {
2876
3008
  claude: "claude",
@@ -2953,7 +3085,13 @@ function askYesNo(question) {
2953
3085
  function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name, resumeSession = false, onSpawnError, systemPrompt, maxTurns, claudeModel, onOutputBytes) {
2954
3086
  const jobLabel = name ?? "unknown";
2955
3087
  console.log(`${timestamp()} ${prefix} ${paint("dim", tokenLogLine("agent", jobLabel, prompt2, systemPrompt))}`);
2956
- const { bin, args } = buildAgentArgs(agent, prompt2, "execute", sessionId, name, resumeSession, systemPrompt, maxTurns, claudeModel);
3088
+ const { bin, args, permission } = buildAgentArgs(agent, prompt2, "execute", sessionId, name, resumeSession, systemPrompt, maxTurns, claudeModel);
3089
+ const permLine = `${timestamp()} ${prefix} ${paint("dim", `permission: ${describePermissionMode(permission)}`)}`;
3090
+ if (permission.mappedToBypassForLackOfSandbox) {
3091
+ logWarn(prefix, `permission: ${describePermissionMode(permission)}`);
3092
+ } else {
3093
+ console.log(permLine);
3094
+ }
2957
3095
  const child = spawn4(bin, args, { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] });
2958
3096
  child.on("error", (err) => {
2959
3097
  logError(prefix, `Failed to spawn ${agent}: ${err.message}`);
@@ -3489,13 +3627,21 @@ var watchCommand = new Command9("watch").description(
3489
3627
  }
3490
3628
  } catch {
3491
3629
  }
3630
+ let protoRefs = [];
3631
+ try {
3632
+ protoRefs = await api.get(`/api/tasks/${task.id}/prototypes`);
3633
+ if (protoRefs.length > 0) {
3634
+ logInfo(prefix, `${paint("cyan", String(protoRefs.length))} linked prototype(s)`);
3635
+ }
3636
+ } catch {
3637
+ }
3492
3638
  const isRevision = !!(existingPlanResource && feedbackUpdates.length > 0);
3493
3639
  await postTaskUpdate(
3494
3640
  task.id,
3495
3641
  isRevision ? "Agent dispatched in plan mode \u2014 revising PRD based on feedback" : "Agent dispatched in plan mode \u2014 generating PRD",
3496
3642
  "system"
3497
3643
  );
3498
- const prompt2 = buildPrdPrompt(task, repoDir, existingPlanResource?.content, feedbackUpdates);
3644
+ const prompt2 = buildPrdPrompt(task, repoDir, existingPlanResource?.content, feedbackUpdates, protoRefs);
3499
3645
  const attemptOrder = await resolveAgentChain(agent);
3500
3646
  if (attemptOrder.length === 0) {
3501
3647
  logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
@@ -6096,18 +6242,26 @@ async function resolveReviewAgentChain(preferred2 = "claude") {
6096
6242
  }
6097
6243
  return getAvailableAgentFallbackChain(preferred2, availability);
6098
6244
  }
6099
- function buildArgs(agent, prompt2) {
6245
+ function buildArgs(agent, prompt2, runKind) {
6246
+ const mode = resolveHeadlessMode(loadConfig());
6247
+ const permission = resolvePermissionArgs(agent, runKind, mode);
6100
6248
  if (agent === "codex") {
6101
- return ["-a", "never", "exec", "-s", "danger-full-access", prompt2];
6249
+ return ["-a", "never", "exec", ...permission.args, prompt2];
6102
6250
  }
6103
6251
  if (agent === "antigravity") {
6104
- return ["-p", prompt2, "--dangerously-skip-permissions"];
6252
+ return ["-p", prompt2, ...permission.args];
6105
6253
  }
6106
- return ["-p", "--dangerously-skip-permissions", prompt2];
6254
+ return ["-p", ...permission.args, prompt2];
6255
+ }
6256
+ function logPermission(agent, runKind) {
6257
+ const permission = resolvePermissionArgs(agent, runKind, resolveHeadlessMode(loadConfig()));
6258
+ const label = permission.mappedToBypassForLackOfSandbox ? "warning" : "info";
6259
+ console.error(`[review:${label}] ${agent} ${runKind} permission: ${describePermissionMode(permission)}`);
6107
6260
  }
6108
6261
  function runOnce(agent, prompt2, opts) {
6109
6262
  return new Promise((resolve9) => {
6110
- const child = spawn7(AGENT_BINARIES2[agent], buildArgs(agent, prompt2), {
6263
+ logPermission(agent, opts.runKind);
6264
+ const child = spawn7(AGENT_BINARIES2[agent], buildArgs(agent, prompt2, opts.runKind), {
6111
6265
  cwd: opts.cwd,
6112
6266
  stdio: opts.capture ? ["ignore", "pipe", "pipe"] : ["ignore", "inherit", "inherit"]
6113
6267
  });
@@ -6133,7 +6287,7 @@ async function runAgentCaptured(prompt2, opts) {
6133
6287
  }
6134
6288
  let lastError = "";
6135
6289
  for (const agent of opts.chain) {
6136
- const result = await runOnce(agent, prompt2, { cwd: opts.cwd, capture: true });
6290
+ const result = await runOnce(agent, prompt2, { cwd: opts.cwd, capture: true, runKind: "review" });
6137
6291
  if (result.ok && result.output) {
6138
6292
  return { output: result.output, agent };
6139
6293
  }
@@ -6147,7 +6301,7 @@ async function runAgentInteractive(prompt2, opts) {
6147
6301
  }
6148
6302
  let lastError = "";
6149
6303
  for (const agent of opts.chain) {
6150
- const result = await runOnce(agent, prompt2, { cwd: opts.cwd, capture: false });
6304
+ const result = await runOnce(agent, prompt2, { cwd: opts.cwd, capture: false, runKind: "execute" });
6151
6305
  if (result.ok) return { agent };
6152
6306
  lastError = result.spawnError ? `${agent} could not be spawned` : `${agent} exited non-zero`;
6153
6307
  }
@@ -7846,7 +8000,9 @@ async function fetchScanContext(opts) {
7846
8000
  }
7847
8001
  function runClaude(prompt2) {
7848
8002
  return new Promise((resolve9, reject) => {
7849
- const child = spawn8("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
8003
+ const permission = resolvePermissionArgs("claude", "scan", resolveHeadlessMode(loadConfig()));
8004
+ console.error(`[scanner] permission: ${describePermissionMode(permission)}`);
8005
+ const child = spawn8("claude", ["-p", ...permission.args, prompt2], {
7850
8006
  stdio: ["ignore", "pipe", "pipe"]
7851
8007
  });
7852
8008
  let output = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunnewold-labs/mr-manager",
3
- "version": "0.4.52",
3
+ "version": "0.4.55",
4
4
  "description": "Mr. Manager - Task and project management CLI",
5
5
  "bin": {
6
6
  "mr": "./dist/index.mjs"