harnessed 3.0.0 → 3.0.2

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/cli.mjs CHANGED
@@ -793,7 +793,7 @@ var init_resume = __esm({
793
793
 
794
794
  // package.json
795
795
  var package_default = {
796
- version: "3.0.0"};
796
+ version: "3.0.2"};
797
797
 
798
798
  // src/manifest/errors.ts
799
799
  function instancePathToKeyPath(instancePath) {
@@ -2940,7 +2940,12 @@ function validateNonInteractiveFlags(raw, cmdName) {
2940
2940
 
2941
2941
  // src/cli/execute-task.ts
2942
2942
  function registerExecuteTask(program2) {
2943
- program2.command("execute-task").description("Run execute-task workflow (4-phase chain \u2192 ralph-loop COMPLETE)").requiredOption("--task <text>", "task description (required)").option("--workflow <name>", "workflow name", "execute-task").option("--apply", "execute the spawn (default: dry-run preview)").option("--dry-run", "force dry-run (overrides --apply if both set)").option("--non-interactive", "CI / scripts \u2014 requires --apply or --dry-run").option("--model <model>", "subagent model: 'haiku' | 'sonnet' | 'opus'").option("--model-tier <tier>", "override: 'inherit' bypasses per-phase phase.model (B-10)").option("--max-iterations <n>", "ralph-loop max iter (default 20)", (v) => parseInt(v, 10)).action(async (raw) => {
2943
+ program2.command("execute-task").description(
2944
+ "Run execute-task workflow (4-phase chain \u2192 ralph-loop COMPLETE; immediate by default \u2014 use --dry-run for preview)"
2945
+ ).requiredOption("--task <text>", "task description (required)").option("--workflow <name>", "workflow name", "execute-task").option(
2946
+ "--apply",
2947
+ "(deprecated; kept for backward compat \u2014 execute-task spawns immediately by default)"
2948
+ ).option("--dry-run", "preview only \u2014 do not spawn subagent (opt-in for advanced users)").option("--non-interactive", "CI / scripts \u2014 requires --apply or --dry-run").option("--model <model>", "subagent model: 'haiku' | 'sonnet' | 'opus'").option("--model-tier <tier>", "override: 'inherit' bypasses per-phase phase.model (B-10)").option("--max-iterations <n>", "ralph-loop max iter (default 20)", (v) => parseInt(v, 10)).action(async (raw) => {
2944
2949
  validateNonInteractiveFlags(raw, "execute-task --task <text>");
2945
2950
  if (!raw.task) {
2946
2951
  console.error("error: --task <text> is required");
@@ -2963,7 +2968,7 @@ function registerExecuteTask(program2) {
2963
2968
  };
2964
2969
  }
2965
2970
  const taskCtx = { task: raw.task, task_type: "execute-task" };
2966
- const isDryRun = raw.dryRun === true || !raw.apply && !raw.nonInteractive;
2971
+ const isDryRun = raw.dryRun === true;
2967
2972
  if (isDryRun) {
2968
2973
  console.log(
2969
2974
  JSON.stringify({ workflow: phases.workflow, phases: phases.phases, taskCtx }, null, 2)
@@ -3041,8 +3046,10 @@ async function dirSizeKb(dir) {
3041
3046
  return Math.round(total / 1024);
3042
3047
  }
3043
3048
  function registerGc(program2) {
3044
- program2.command("gc").description("Garbage-collect old backup snapshots (dry-run by default)").option("--older-than <duration>", "delete snapshots older than (e.g. 30d / 24h / 4w)", "30d").option("--keep-last <N>", "always keep the most recent N snapshots", "0").option("--apply", "actually delete (default: dry-run preview only)").option("--dry-run", "force dry-run (overrides --apply)").action(async (opts) => {
3045
- const dryRun = opts.dryRun === true || opts.apply !== true;
3049
+ program2.command("gc").description(
3050
+ "Garbage-collect old backup snapshots (immediate by default \u2014 use --dry-run for preview)"
3051
+ ).option("--older-than <duration>", "delete snapshots older than (e.g. 30d / 24h / 4w)", "30d").option("--keep-last <N>", "always keep the most recent N snapshots", "0").option("--apply", "(deprecated; kept for backward compat \u2014 gc deletes immediately by default)").option("--dry-run", "preview only \u2014 do not delete (opt-in for advanced users)").action(async (opts) => {
3052
+ const dryRun = opts.dryRun === true;
3046
3053
  const olderMs = parseDuration(opts.olderThan ?? "30d");
3047
3054
  if (olderMs == null) {
3048
3055
  console.error(
@@ -3092,7 +3099,7 @@ function registerGc(program2) {
3092
3099
  console.log(` ${c.ts} ${c.manifest} (${c.sizeKb} KB)`);
3093
3100
  if (!dryRun) await rm(c.path, { recursive: true, force: true });
3094
3101
  }
3095
- if (dryRun) console.log("\n(dry-run \u2014 re-run with --apply to actually delete)");
3102
+ if (dryRun) console.log("\n(dry-run \u2014 re-run without --dry-run to actually delete)");
3096
3103
  });
3097
3104
  }
3098
3105
  async function confirmAt(level, ctx) {
@@ -3397,6 +3404,9 @@ function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
3397
3404
  });
3398
3405
  });
3399
3406
  }
3407
+ function getMcpSpawnCwd() {
3408
+ return homedir();
3409
+ }
3400
3410
 
3401
3411
  // src/installers/ccPluginMarketplace.ts
3402
3412
  function parseCmd(cmd) {
@@ -3448,8 +3458,8 @@ var installCcPluginMarketplace = async (ctx) => {
3448
3458
  }
3449
3459
  };
3450
3460
  }
3451
- const pluginName = parsed.pluginAtMkt.split("@")[0];
3452
- const installArgs = ["plugin", "install", parsed.pluginAtMkt, "--scope", "project"];
3461
+ const pluginName = parsed.pluginAtMkt.split("@")[0] ?? parsed.pluginAtMkt;
3462
+ const installArgs = ["plugin", "install", parsed.pluginAtMkt, "--scope", "user"];
3453
3463
  const allArgs = [];
3454
3464
  if (parsed.marketplaceRef !== null) {
3455
3465
  allArgs.push(["plugin", "marketplace", "add", parsed.marketplaceRef]);
@@ -3457,30 +3467,30 @@ var installCcPluginMarketplace = async (ctx) => {
3457
3467
  allArgs.push(installArgs);
3458
3468
  for (const argSet of allArgs) {
3459
3469
  for (const a of argSet) {
3460
- const violation2 = checkCmdString(a);
3461
- if (violation2) {
3470
+ const violation = checkCmdString(a);
3471
+ if (violation) {
3462
3472
  return {
3463
3473
  ok: false,
3464
3474
  phase: "preflight",
3465
3475
  error: err(
3466
3476
  ctx,
3467
3477
  "/spec/install/cmd",
3468
- `shell escape detected in constructed cc-plugin arg '${a.slice(0, 60)}': ${violation2.label} (${violation2.hint})`,
3478
+ `shell escape detected in constructed cc-plugin arg '${a.slice(0, 60)}': ${violation.label} (${violation.hint})`,
3469
3479
  "security-gate-bypass"
3470
3480
  )
3471
3481
  };
3472
3482
  }
3473
3483
  }
3474
3484
  }
3475
- const settingsFile = `${ctx.cwd}/.claude/settings.json`;
3485
+ const settingsFile = `${getMcpSpawnCwd()}/.claude.json`;
3476
3486
  const newEntry = JSON.stringify({ enabledPlugins: { [parsed.pluginAtMkt]: true } }, null, 2);
3477
3487
  const plan = {
3478
3488
  files: [
3479
3489
  {
3480
3490
  target: settingsFile,
3481
- scope: "PROJECT",
3491
+ scope: "HOME",
3482
3492
  oldText: "",
3483
- newText: `// will be merged into .claude/settings.json enabledPlugins map by \`claude plugin install\`:
3493
+ newText: `// will be merged into ~/.claude.json enabledPlugins map by \`claude plugin install --scope user\`:
3484
3494
  ${newEntry}
3485
3495
  `
3486
3496
  }
@@ -3495,15 +3505,13 @@ ${newEntry}
3495
3505
  if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
3496
3506
  const bk = await backup(plan, ctx);
3497
3507
  if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
3508
+ const spawnCwd = install.cwd ?? getMcpSpawnCwd();
3498
3509
  let stepOneStderr = "";
3499
3510
  if (parsed.marketplaceRef !== null) {
3500
- const r1 = await runArgs(
3501
- ["plugin", "marketplace", "add", parsed.marketplaceRef],
3502
- install.cwd ?? ctx.cwd
3503
- );
3511
+ const r1 = await runArgs(["plugin", "marketplace", "add", parsed.marketplaceRef], spawnCwd);
3504
3512
  stepOneStderr = r1.stderr;
3505
3513
  }
3506
- const r2 = await runArgs(installArgs, install.cwd ?? ctx.cwd);
3514
+ const r2 = await runArgs(installArgs, spawnCwd);
3507
3515
  if (r2.exitCode !== 0) {
3508
3516
  return {
3509
3517
  ok: false,
@@ -3517,43 +3525,34 @@ ${newEntry}
3517
3525
  )
3518
3526
  };
3519
3527
  }
3520
- const verifyShell = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
3521
- const verifyFlag = process.platform === "win32" ? "/c" : "-c";
3522
- const verifyLine = `claude plugin list --json | grep -q ${pluginName}`;
3523
- const violation = checkCmdString(verifyLine);
3524
- if (violation) {
3525
- return {
3526
- ok: false,
3527
- phase: "verify",
3528
- backupId: bk.backupId,
3529
- error: err(
3530
- ctx,
3531
- "/spec/verify/cmd",
3532
- `verify shell escape: ${violation.label}`,
3533
- "security-gate-bypass"
3534
- )
3535
- };
3536
- }
3537
3528
  const vr = await new Promise((resolve9) => {
3538
- const child = spawn(verifyShell, [verifyFlag, verifyLine], { cwd: ctx.cwd, windowsHide: true });
3529
+ const child = spawn("claude", ["plugin", "list", "--json"], {
3530
+ cwd: spawnCwd,
3531
+ shell: process.platform === "win32",
3532
+ windowsHide: true
3533
+ });
3534
+ let stdout2 = "";
3539
3535
  let stderr = "";
3536
+ child.stdout?.setEncoding("utf8").on("data", (c) => {
3537
+ stdout2 += c;
3538
+ });
3540
3539
  child.stderr?.setEncoding("utf8").on("data", (c) => {
3541
3540
  stderr += c;
3542
3541
  });
3543
3542
  const timer = setTimeout(() => {
3544
3543
  child.kill("SIGKILL");
3545
- resolve9({ exitCode: -1, stderr: `${stderr}[timeout]` });
3544
+ resolve9({ exitCode: -1, stderr: `${stderr}[timeout]`, stdout: stdout2 });
3546
3545
  }, 15e3);
3547
3546
  child.on("error", (e) => {
3548
3547
  clearTimeout(timer);
3549
- resolve9({ exitCode: -1, stderr: e.message });
3548
+ resolve9({ exitCode: -1, stderr: e.message, stdout: stdout2 });
3550
3549
  });
3551
3550
  child.on("close", (code) => {
3552
3551
  clearTimeout(timer);
3553
- resolve9({ exitCode: code ?? -1, stderr });
3552
+ resolve9({ exitCode: code ?? -1, stderr, stdout: stdout2 });
3554
3553
  });
3555
3554
  });
3556
- if (vr.exitCode !== 0) {
3555
+ if (vr.exitCode !== 0 || !vr.stdout.includes(pluginName)) {
3557
3556
  return {
3558
3557
  ok: false,
3559
3558
  phase: "verify",
@@ -3561,7 +3560,7 @@ ${newEntry}
3561
3560
  error: err(
3562
3561
  ctx,
3563
3562
  "/spec/verify/cmd",
3564
- `verify exit ${vr.exitCode}: ${vr.stderr.slice(0, 200)}`,
3563
+ `verify exit ${vr.exitCode} or '${pluginName}' not in plugin list stdout: ${vr.stderr.slice(0, 200)}`,
3565
3564
  "verify-failed"
3566
3565
  )
3567
3566
  };
@@ -3569,8 +3568,9 @@ ${newEntry}
3569
3568
  await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, install.git_ref, "");
3570
3569
  return { ok: true, backupId: bk.backupId, appliedFiles: [settingsFile] };
3571
3570
  };
3572
- var DEFAULT_TIMEOUT_MS = 15e3;
3573
- async function spawnCmd(ctx, cmd, args) {
3571
+ var DEFAULT_VERIFY_TIMEOUT_MS = 15e3;
3572
+ var DEFAULT_INSTALL_TIMEOUT_MS = 6e4;
3573
+ async function spawnCmd(ctx, cmd, args, timeoutMs) {
3574
3574
  const violation = checkCmdString(cmd);
3575
3575
  if (violation) {
3576
3576
  return {
@@ -3587,8 +3587,7 @@ async function spawnCmd(ctx, cmd, args) {
3587
3587
  };
3588
3588
  }
3589
3589
  const installCfg = ctx.manifest.spec.install;
3590
- const verifyCfg = ctx.manifest.spec.verify;
3591
- const timeoutMs = verifyCfg.timeout_ms ?? DEFAULT_TIMEOUT_MS;
3590
+ const effectiveTimeoutMs = timeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
3592
3591
  const env = { ...process.env, ...installCfg.env ?? {} };
3593
3592
  const cwd = installCfg.cwd ?? ctx.cwd;
3594
3593
  let child;
@@ -3615,13 +3614,13 @@ async function spawnCmd(ctx, cmd, args) {
3615
3614
  error: {
3616
3615
  file: ctx.manifest.metadata.name,
3617
3616
  path: "/spec/install/cmd",
3618
- message: `spawn timed out after ${timeoutMs}ms (cmd: ${cmd}); partial stderr: ${stderr.slice(0, 200)}`,
3617
+ message: `spawn timed out after ${effectiveTimeoutMs}ms (cmd: ${cmd}); partial stderr: ${stderr.slice(0, 200)}`,
3619
3618
  line: null,
3620
3619
  column: null,
3621
3620
  keyword: "spawn-timeout"
3622
3621
  }
3623
3622
  });
3624
- }, timeoutMs);
3623
+ }, effectiveTimeoutMs);
3625
3624
  child.on("error", (err2) => {
3626
3625
  clearTimeout(timer);
3627
3626
  resolve9({
@@ -3767,7 +3766,7 @@ var installGitCloneWithSetup = async (ctx) => {
3767
3766
  if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
3768
3767
  const bk = await backup(plan, ctx);
3769
3768
  if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
3770
- const sp = await spawnCmd(ctx, install.cmd, []);
3769
+ const sp = await spawnCmd(ctx, install.cmd, [], DEFAULT_INSTALL_TIMEOUT_MS);
3771
3770
  if (!("exitCode" in sp)) return { ...sp, backupId: bk.backupId };
3772
3771
  if (sp.exitCode !== 0) {
3773
3772
  return {
@@ -3809,7 +3808,8 @@ var installGitCloneWithSetup = async (ctx) => {
3809
3808
  )
3810
3809
  };
3811
3810
  }
3812
- const vr = await spawnCmd(ctx, ctx.manifest.spec.verify.cmd, []);
3811
+ const verifyTimeoutMs = ctx.manifest.spec.verify.timeout_ms ?? DEFAULT_VERIFY_TIMEOUT_MS;
3812
+ const vr = await spawnCmd(ctx, ctx.manifest.spec.verify.cmd, [], verifyTimeoutMs);
3813
3813
  if (!("exitCode" in vr)) return { ...vr, backupId: bk.backupId };
3814
3814
  const expected = ctx.manifest.spec.verify.expected_exit_code ?? 0;
3815
3815
  if (vr.exitCode !== expected) {
@@ -3912,33 +3912,23 @@ var installMcpHttpAdd = async (ctx) => {
3912
3912
  }
3913
3913
  };
3914
3914
  }
3915
- const addArgs = [
3916
- "mcp",
3917
- "add",
3918
- "--scope",
3919
- "project",
3920
- "--transport",
3921
- "http",
3922
- ...hdr.flat,
3923
- name,
3924
- url
3925
- ];
3915
+ const addArgs = ["mcp", "add", "--scope", "user", "--transport", "http", ...hdr.flat, name, url];
3926
3916
  for (const a of addArgs) {
3927
- const violation2 = checkCmdString(a);
3928
- if (violation2) {
3917
+ const violation = checkCmdString(a);
3918
+ if (violation) {
3929
3919
  return {
3930
3920
  ok: false,
3931
3921
  phase: "preflight",
3932
3922
  error: err(
3933
3923
  ctx,
3934
3924
  "/spec/install/cmd",
3935
- `shell escape detected in constructed mcp-http-add arg '${a.slice(0, 60)}': ${violation2.label} (${violation2.hint})`,
3925
+ `shell escape detected in constructed mcp-http-add arg '${a.slice(0, 60)}': ${violation.label} (${violation.hint})`,
3936
3926
  "security-gate-bypass"
3937
3927
  )
3938
3928
  };
3939
3929
  }
3940
3930
  }
3941
- const mcpFile = `${ctx.cwd}/.mcp.json`;
3931
+ const mcpFile = `${getMcpSpawnCwd()}/.claude.json`;
3942
3932
  const headersObj = {};
3943
3933
  for (let i = 0; i < hdr.flat.length; i += 2) {
3944
3934
  const kv = hdr.flat[i + 1];
@@ -3952,9 +3942,9 @@ var installMcpHttpAdd = async (ctx) => {
3952
3942
  files: [
3953
3943
  {
3954
3944
  target: mcpFile,
3955
- scope: "PROJECT",
3945
+ scope: "HOME",
3956
3946
  oldText: "",
3957
- newText: `// will be merged into .mcp.json mcpServers map by \`claude mcp add\`:
3947
+ newText: `// will be merged into ~/.claude.json mcpServers map by \`claude mcp add --scope user\`:
3958
3948
  ${newEntry}
3959
3949
  `
3960
3950
  }
@@ -3969,9 +3959,10 @@ ${newEntry}
3969
3959
  if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
3970
3960
  const bk = await backup(plan, ctx);
3971
3961
  if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
3972
- const r = await runArgs(addArgs, install.cwd ?? ctx.cwd);
3962
+ const spawnCwd = install.cwd ?? getMcpSpawnCwd();
3963
+ const r = await runArgs(addArgs, spawnCwd);
3973
3964
  if (r.exitCode !== 0) {
3974
- if (r.stderr.includes("already exists in .mcp.json")) {
3965
+ if (r.stderr.includes("already exists")) {
3975
3966
  return { ok: true, alreadyInstalled: true, backupId: bk.backupId };
3976
3967
  }
3977
3968
  return {
@@ -3986,43 +3977,34 @@ ${newEntry}
3986
3977
  )
3987
3978
  };
3988
3979
  }
3989
- const verifyShell = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
3990
- const verifyFlag = process.platform === "win32" ? "/c" : "-c";
3991
- const verifyLine = `claude mcp list | grep -q ${name}`;
3992
- const violation = checkCmdString(verifyLine);
3993
- if (violation) {
3994
- return {
3995
- ok: false,
3996
- phase: "verify",
3997
- backupId: bk.backupId,
3998
- error: err(
3999
- ctx,
4000
- "/spec/verify/cmd",
4001
- `verify shell escape: ${violation.label}`,
4002
- "security-gate-bypass"
4003
- )
4004
- };
4005
- }
4006
3980
  const vr = await new Promise((resolve9) => {
4007
- const child = spawn(verifyShell, [verifyFlag, verifyLine], { cwd: ctx.cwd, windowsHide: true });
3981
+ const child = spawn("claude", ["mcp", "list"], {
3982
+ cwd: spawnCwd,
3983
+ shell: process.platform === "win32",
3984
+ windowsHide: true
3985
+ });
3986
+ let stdout2 = "";
4008
3987
  let stderr = "";
3988
+ child.stdout?.setEncoding("utf8").on("data", (c) => {
3989
+ stdout2 += c;
3990
+ });
4009
3991
  child.stderr?.setEncoding("utf8").on("data", (c) => {
4010
3992
  stderr += c;
4011
3993
  });
4012
3994
  const timer = setTimeout(() => {
4013
3995
  child.kill("SIGKILL");
4014
- resolve9({ exitCode: -1, stderr: `${stderr}[timeout]` });
3996
+ resolve9({ exitCode: -1, stderr: `${stderr}[timeout]`, stdout: stdout2 });
4015
3997
  }, 15e3);
4016
3998
  child.on("error", (e) => {
4017
3999
  clearTimeout(timer);
4018
- resolve9({ exitCode: -1, stderr: e.message });
4000
+ resolve9({ exitCode: -1, stderr: e.message, stdout: stdout2 });
4019
4001
  });
4020
4002
  child.on("close", (code) => {
4021
4003
  clearTimeout(timer);
4022
- resolve9({ exitCode: code ?? -1, stderr });
4004
+ resolve9({ exitCode: code ?? -1, stderr, stdout: stdout2 });
4023
4005
  });
4024
4006
  });
4025
- if (vr.exitCode !== 0) {
4007
+ if (vr.exitCode !== 0 || !vr.stdout.includes(name)) {
4026
4008
  return {
4027
4009
  ok: false,
4028
4010
  phase: "verify",
@@ -4030,7 +4012,7 @@ ${newEntry}
4030
4012
  error: err(
4031
4013
  ctx,
4032
4014
  "/spec/verify/cmd",
4033
- `verify exit ${vr.exitCode}: ${vr.stderr.slice(0, 200)}`,
4015
+ `verify exit ${vr.exitCode} or '${name}' not in mcp list stdout: ${vr.stderr.slice(0, 200)}`,
4034
4016
  "verify-failed"
4035
4017
  )
4036
4018
  };
@@ -4066,7 +4048,7 @@ var installMcpStdioAdd = async (ctx) => {
4066
4048
  "mcp",
4067
4049
  "add",
4068
4050
  "--scope",
4069
- "project",
4051
+ "user",
4070
4052
  "--transport",
4071
4053
  "stdio",
4072
4054
  name,
@@ -4076,21 +4058,21 @@ var installMcpStdioAdd = async (ctx) => {
4076
4058
  `${pkg}@${ver}`
4077
4059
  ];
4078
4060
  for (const a of addArgs) {
4079
- const violation2 = checkCmdString(a);
4080
- if (violation2) {
4061
+ const violation = checkCmdString(a);
4062
+ if (violation) {
4081
4063
  return {
4082
4064
  ok: false,
4083
4065
  phase: "preflight",
4084
4066
  error: err(
4085
4067
  ctx,
4086
4068
  "/spec/install/cmd",
4087
- `shell escape detected in constructed mcp-add arg '${a.slice(0, 60)}': ${violation2.label} (${violation2.hint})`,
4069
+ `shell escape detected in constructed mcp-add arg '${a.slice(0, 60)}': ${violation.label} (${violation.hint})`,
4088
4070
  "security-gate-bypass"
4089
4071
  )
4090
4072
  };
4091
4073
  }
4092
4074
  }
4093
- const mcpFile = `${ctx.cwd}/.mcp.json`;
4075
+ const mcpFile = `${getMcpSpawnCwd()}/.claude.json`;
4094
4076
  const newEntry = JSON.stringify(
4095
4077
  { [name]: { type: "stdio", command: "npx", args: ["--yes", `${pkg}@${ver}`] } },
4096
4078
  null,
@@ -4100,9 +4082,9 @@ var installMcpStdioAdd = async (ctx) => {
4100
4082
  files: [
4101
4083
  {
4102
4084
  target: mcpFile,
4103
- scope: "PROJECT",
4085
+ scope: "HOME",
4104
4086
  oldText: "",
4105
- newText: `// will be merged into .mcp.json mcpServers map by \`claude mcp add\`:
4087
+ newText: `// will be merged into ~/.claude.json mcpServers map by \`claude mcp add --scope user\`:
4106
4088
  ${newEntry}
4107
4089
  `
4108
4090
  }
@@ -4117,9 +4099,10 @@ ${newEntry}
4117
4099
  if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
4118
4100
  const bk = await backup(plan, ctx);
4119
4101
  if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
4120
- const r = await runArgs(addArgs, install.cwd ?? ctx.cwd);
4102
+ const spawnCwd = install.cwd ?? getMcpSpawnCwd();
4103
+ const r = await runArgs(addArgs, spawnCwd);
4121
4104
  if (r.exitCode !== 0) {
4122
- if (r.stderr.includes("already exists in .mcp.json")) {
4105
+ if (r.stderr.includes("already exists")) {
4123
4106
  return { ok: true, alreadyInstalled: true, backupId: bk.backupId };
4124
4107
  }
4125
4108
  return {
@@ -4134,43 +4117,34 @@ ${newEntry}
4134
4117
  )
4135
4118
  };
4136
4119
  }
4137
- const verifyShell = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
4138
- const verifyFlag = process.platform === "win32" ? "/c" : "-c";
4139
- const verifyLine = `claude mcp list | grep -q ${name}`;
4140
- const violation = checkCmdString(verifyLine);
4141
- if (violation) {
4142
- return {
4143
- ok: false,
4144
- phase: "verify",
4145
- backupId: bk.backupId,
4146
- error: err(
4147
- ctx,
4148
- "/spec/verify/cmd",
4149
- `verify shell escape: ${violation.label}`,
4150
- "security-gate-bypass"
4151
- )
4152
- };
4153
- }
4154
4120
  const vr = await new Promise((resolve9) => {
4155
- const child = spawn(verifyShell, [verifyFlag, verifyLine], { cwd: ctx.cwd, windowsHide: true });
4121
+ const child = spawn("claude", ["mcp", "list"], {
4122
+ cwd: spawnCwd,
4123
+ shell: process.platform === "win32",
4124
+ windowsHide: true
4125
+ });
4126
+ let stdout2 = "";
4156
4127
  let stderr = "";
4128
+ child.stdout?.setEncoding("utf8").on("data", (c) => {
4129
+ stdout2 += c;
4130
+ });
4157
4131
  child.stderr?.setEncoding("utf8").on("data", (c) => {
4158
4132
  stderr += c;
4159
4133
  });
4160
4134
  const timer = setTimeout(() => {
4161
4135
  child.kill("SIGKILL");
4162
- resolve9({ exitCode: -1, stderr: `${stderr}[timeout]` });
4136
+ resolve9({ exitCode: -1, stderr: `${stderr}[timeout]`, stdout: stdout2 });
4163
4137
  }, 15e3);
4164
4138
  child.on("error", (e) => {
4165
4139
  clearTimeout(timer);
4166
- resolve9({ exitCode: -1, stderr: e.message });
4140
+ resolve9({ exitCode: -1, stderr: e.message, stdout: stdout2 });
4167
4141
  });
4168
4142
  child.on("close", (code) => {
4169
4143
  clearTimeout(timer);
4170
- resolve9({ exitCode: code ?? -1, stderr });
4144
+ resolve9({ exitCode: code ?? -1, stderr, stdout: stdout2 });
4171
4145
  });
4172
4146
  });
4173
- if (vr.exitCode !== 0) {
4147
+ if (vr.exitCode !== 0 || !vr.stdout.includes(name)) {
4174
4148
  return {
4175
4149
  ok: false,
4176
4150
  phase: "verify",
@@ -4178,7 +4152,7 @@ ${newEntry}
4178
4152
  error: err(
4179
4153
  ctx,
4180
4154
  "/spec/verify/cmd",
4181
- `verify exit ${vr.exitCode}: ${vr.stderr.slice(0, 200)}`,
4155
+ `verify exit ${vr.exitCode} or '${name}' not in mcp list stdout: ${vr.stderr.slice(0, 200)}`,
4182
4156
  "verify-failed"
4183
4157
  )
4184
4158
  };
@@ -4244,7 +4218,7 @@ var installNpmCli = async (ctx) => {
4244
4218
  if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
4245
4219
  const bk = await backup(plan, ctx);
4246
4220
  if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
4247
- const sp = await spawnCmd(ctx, cmd, []);
4221
+ const sp = await spawnCmd(ctx, cmd, [], DEFAULT_INSTALL_TIMEOUT_MS);
4248
4222
  if (!("exitCode" in sp)) return { ...sp, backupId: bk.backupId };
4249
4223
  if (sp.exitCode !== 0) {
4250
4224
  return {
@@ -4259,7 +4233,8 @@ var installNpmCli = async (ctx) => {
4259
4233
  )
4260
4234
  };
4261
4235
  }
4262
- const vr = await spawnCmd(ctx, ctx.manifest.spec.verify.cmd, []);
4236
+ const verifyTimeoutMs = ctx.manifest.spec.verify.timeout_ms ?? DEFAULT_VERIFY_TIMEOUT_MS;
4237
+ const vr = await spawnCmd(ctx, ctx.manifest.spec.verify.cmd, [], verifyTimeoutMs);
4263
4238
  if (!("exitCode" in vr)) return { ...vr, backupId: bk.backupId };
4264
4239
  const expected = ctx.manifest.spec.verify.expected_exit_code ?? 0;
4265
4240
  if (vr.exitCode !== expected) {
@@ -4360,7 +4335,7 @@ var installNpxSkillInstaller = async (ctx) => {
4360
4335
  if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
4361
4336
  const bk = await backup(plan, ctx);
4362
4337
  if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
4363
- const sp = await spawnCmd(ctx, install.cmd, []);
4338
+ const sp = await spawnCmd(ctx, install.cmd, [], DEFAULT_INSTALL_TIMEOUT_MS);
4364
4339
  if (!("exitCode" in sp)) return { ...sp, backupId: bk.backupId };
4365
4340
  if (sp.exitCode !== 0) {
4366
4341
  return {
@@ -4393,7 +4368,8 @@ var installNpxSkillInstaller = async (ctx) => {
4393
4368
  }
4394
4369
  };
4395
4370
  }
4396
- const vr = await spawnCmd(ctx, ctx.manifest.spec.verify.cmd, []);
4371
+ const verifyTimeoutMs = ctx.manifest.spec.verify.timeout_ms ?? DEFAULT_VERIFY_TIMEOUT_MS;
4372
+ const vr = await spawnCmd(ctx, ctx.manifest.spec.verify.cmd, [], verifyTimeoutMs);
4397
4373
  if (!("exitCode" in vr)) return { ...vr, backupId: bk.backupId };
4398
4374
  const expected = ctx.manifest.spec.verify.expected_exit_code ?? 0;
4399
4375
  if (vr.exitCode !== expected) {
@@ -4465,7 +4441,7 @@ function formatError(e) {
4465
4441
  return `${head}${where}${tip}`;
4466
4442
  }
4467
4443
  function registerInstall(program2) {
4468
- program2.command("install <name>").description("Install an upstream (dry-run by default \u2014 pass --apply to execute)").option("--apply", "execute the install (default: dry-run preview only)").option("--dry-run", "force dry-run (overrides --apply if both are set)").option("--system", "allow L4 system-wide install (e.g. global npm install)").option("--non-interactive", "skip all prompts (CI / scripts) \u2014 requires --apply or --dry-run").option("--full-diff", "expand diffs longer than 200 lines").option("--no-color", "disable ANSI colors (auto-detected when piped)").option(
4444
+ program2.command("install <name>").description("Install an upstream (immediate by default \u2014 use --dry-run for preview)").option("--apply", "(deprecated; kept for backward compat \u2014 install is immediate by default)").option("--dry-run", "preview only \u2014 do not write to disk (opt-in for advanced users)").option("--system", "allow L4 system-wide install (e.g. global npm install)").option("--non-interactive", "skip all prompts (CI / scripts) \u2014 requires --apply or --dry-run").option("--full-diff", "expand diffs longer than 200 lines").option("--no-color", "disable ANSI colors (auto-detected when piped)").option(
4469
4445
  "--known-good",
4470
4446
  "use known-good version lock from versions/<harnessed-ver>-known-good.yaml"
4471
4447
  ).action(async (name, raw) => {
@@ -4497,9 +4473,10 @@ function registerInstall(program2) {
4497
4473
  console.error(` fix: run 'harnessed audit' to inspect manifest issues`);
4498
4474
  process.exit(1);
4499
4475
  }
4476
+ const dryRun = raw.dryRun === true;
4500
4477
  const opts = {
4501
- apply: raw.apply === true,
4502
- dryRun: raw.dryRun === true,
4478
+ apply: !dryRun,
4479
+ dryRun,
4503
4480
  system: raw.system === true,
4504
4481
  nonInteractive: raw.nonInteractive === true,
4505
4482
  fullDiff: raw.fullDiff === true,
@@ -4545,11 +4522,17 @@ async function listBaseManifests(cwd) {
4545
4522
  return out;
4546
4523
  }
4547
4524
  function registerInstallBase(program2) {
4548
- program2.command("install-base").description("Install the phase 1.3 base profile (auto-glob manifests; dry-run by default)").option("--apply", "execute the install (default: dry-run preview only)").option("--dry-run", "force dry-run (overrides --apply if both are set)").option("--non-interactive", "skip all prompts (CI / scripts) \u2014 requires --apply or --dry-run").action(async (raw) => {
4525
+ program2.command("install-base").description(
4526
+ "Install the phase 1.3 base profile (immediate by default \u2014 use --dry-run for preview)"
4527
+ ).option(
4528
+ "--apply",
4529
+ "(deprecated; kept for backward compat \u2014 install-base is immediate by default)"
4530
+ ).option("--dry-run", "preview only \u2014 do not write to disk (opt-in for advanced users)").option("--non-interactive", "skip all prompts (CI / scripts) \u2014 requires --apply or --dry-run").action(async (raw) => {
4549
4531
  validateNonInteractiveFlags(raw, "install-base");
4532
+ const dryRun = raw.dryRun === true;
4550
4533
  const opts = {
4551
- apply: raw.apply === true,
4552
- dryRun: raw.dryRun === true,
4534
+ apply: !dryRun,
4535
+ dryRun,
4553
4536
  system: false,
4554
4537
  nonInteractive: raw.nonInteractive === true,
4555
4538
  fullDiff: false,
@@ -4609,7 +4592,12 @@ function basename(upstream) {
4609
4592
  return (upstream.split("/").pop() ?? upstream).replace(/\.git$/, "");
4610
4593
  }
4611
4594
  function registerManifestAdd(program2) {
4612
- program2.command("manifest-add <upstream>").description("Add a new upstream adapter (EE-5 5-question merge gate, D-03 BOTH dry-run/apply)").option("--category <cat>", "manifest category (skill-packs | tools)", "skill-packs").option("--name <name>", "short adapter name (defaults to <upstream> basename)").option("--apply", "persist EE-5 answers (default: dry-run preview)").option("--dry-run", "force dry-run (overrides --apply if both set)").option("--non-interactive", "CI/scripts \u2014 requires --apply or --dry-run; WARN-only dry-run").action(async (upstream, raw) => {
4595
+ program2.command("manifest-add <upstream>").description(
4596
+ "Add a new upstream adapter (EE-5 5-question merge gate; immediate by default \u2014 use --dry-run for preview)"
4597
+ ).option("--category <cat>", "manifest category (skill-packs | tools)", "skill-packs").option("--name <name>", "short adapter name (defaults to <upstream> basename)").option(
4598
+ "--apply",
4599
+ "(deprecated; kept for backward compat \u2014 manifest-add persists immediately by default)"
4600
+ ).option("--dry-run", "preview only \u2014 do not write JSON (opt-in for advanced users)").option("--non-interactive", "CI/scripts \u2014 requires --apply or --dry-run; WARN-only dry-run").action(async (upstream, raw) => {
4613
4601
  validateNonInteractiveFlags(raw, "manifest-add <upstream>");
4614
4602
  const name = raw.name ?? basename(upstream);
4615
4603
  const category = raw.category ?? "skill-packs";
@@ -4638,7 +4626,8 @@ function registerManifestAdd(program2) {
4638
4626
  payload[f] = a;
4639
4627
  }
4640
4628
  rl.close();
4641
- if (raw.apply) {
4629
+ const dryRun = raw.dryRun === true;
4630
+ if (!dryRun) {
4642
4631
  writeFileSync(outPath, `${JSON.stringify(payload, null, 2)}
4643
4632
  `, "utf8");
4644
4633
  console.log(`[manifest-add] EE-5 gate passed; wrote ${outPath}`);
@@ -4652,14 +4641,19 @@ function registerManifestAdd(program2) {
4652
4641
 
4653
4642
  // src/cli/research.ts
4654
4643
  function registerResearch(program2) {
4655
- program2.command("research").description("Run research workflow (search category sub-routing \u2192 spawn \u2192 verbatim COMPLETE)").requiredOption("--query <text>", "research prompt (required)").option("--apply", "execute the spawn (default: dry-run preview only)").option("--dry-run", "force dry-run (overrides --apply if both are set)").option("--non-interactive", "skip all prompts (CI / scripts) \u2014 requires --apply or --dry-run").option("--model <model>", "subagent model: 'haiku' | 'sonnet' | 'opus'").action(async (raw) => {
4644
+ program2.command("research").description(
4645
+ "Run research workflow (search category sub-routing \u2192 spawn \u2192 verbatim COMPLETE; immediate by default \u2014 use --dry-run for preview)"
4646
+ ).requiredOption("--query <text>", "research prompt (required)").option(
4647
+ "--apply",
4648
+ "(deprecated; kept for backward compat \u2014 research spawns immediately by default)"
4649
+ ).option("--dry-run", "preview only \u2014 do not spawn subagent (opt-in for advanced users)").option("--non-interactive", "skip all prompts (CI / scripts) \u2014 requires --apply or --dry-run").option("--model <model>", "subagent model: 'haiku' | 'sonnet' | 'opus'").action(async (raw) => {
4656
4650
  validateNonInteractiveFlags(raw, "research --query <text>");
4657
4651
  if (!raw.query) {
4658
4652
  console.error("error: --query <text> is required");
4659
4653
  process.exit(2);
4660
4654
  }
4661
4655
  const taskCtx = { task: raw.query, task_type: "search" };
4662
- if (raw.dryRun === true || !raw.apply && !raw.nonInteractive) {
4656
+ if (raw.dryRun === true) {
4663
4657
  const preview = await runRouting(taskCtx, {
4664
4658
  skillsRoot: void 0,
4665
4659
  // Stub spawn — dry-run never reaches it; explicit COMPLETE keeps shape happy.
@@ -4677,7 +4671,9 @@ function registerResearch(program2) {
4677
4671
  }
4678
4672
  console.log(`[dry-run] matched_rule: ${preview.matchedRule?.id ?? "(fallback supervisor)"}`);
4679
4673
  console.log(`[dry-run] query: ${raw.query}`);
4680
- console.log(" (use --apply to spawn the subagent and emit verbatim COMPLETE round-trip)");
4674
+ console.log(
4675
+ " (run without --dry-run to spawn the subagent and emit verbatim COMPLETE round-trip)"
4676
+ );
4681
4677
  process.exit(0);
4682
4678
  }
4683
4679
  const result = await runRouting(taskCtx, {
@@ -4869,7 +4865,7 @@ async function warnIfAgentTeamsMissing() {
4869
4865
  console.warn("\n\u26A0\uFE0F Agent Teams \u672A\u542F\u7528 \u2014 parallelism-gate \u5347\u7EA7\u8DEF\u5F84\u4E0D\u53EF\u7528");
4870
4866
  console.warn(" \u4FEE\u590D: claude config set env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS 1");
4871
4867
  console.warn(
4872
- " \u8BF4\u660E: harnessed v2.0 \u4E09\u5C42\u6808\u65B9\u6CD5\u8BBA parallelism-gate \u5347\u7EA7\u8DEF\u5F84\u9700 CC 2.1.133+ Agent Teams enable (sister ~/.claude/rules/agent-teams.md)"
4868
+ " \u8BF4\u660E: harnessed v3.0 \u4E09\u5C42\u6808\u65B9\u6CD5\u8BBA parallelism-gate \u5347\u7EA7\u8DEF\u5F84\u9700 CC 2.1.133+ Agent Teams enable"
4873
4869
  );
4874
4870
  console.warn(
4875
4871
  " \u4E0D\u963B\u585E setup,\u540E\u7EED parallelism-gate workflow phase \u89E6\u53D1\u65F6\u81EA\u52A8\u964D\u7EA7 subagent fan-out\n"
@@ -5025,7 +5021,7 @@ MCP servers configured. Run \`/mcp\` in Claude Code to verify each server's conn
5025
5021
  );
5026
5022
  }
5027
5023
  console.log(
5028
- "\n\u2713 harnessed v2.0 \u4E09\u5C42\u6808\u65B9\u6CD5\u8BBA bundled \u2014 4 workflows + 6 judgments + 37 capabilities ready"
5024
+ "\n\u2713 harnessed v3.0 \u4E09\u5C42\u6808\u65B9\u6CD5\u8BBA bundled \u2014 23 workflows (4 master + 18 sub + 1 standalone) + 6 disciplines + 10 judgments + ~83 capabilities ready"
5029
5025
  );
5030
5026
  console.log(
5031
5027
  " workflows in <packageRoot>/workflows/ (Pure bundled, NOT user-dir override per D-01)"
@@ -5320,12 +5316,12 @@ async function runUninstall(manifest, opts) {
5320
5316
 
5321
5317
  // src/cli/uninstall.ts
5322
5318
  function registerUninstall(program2) {
5323
- program2.command("uninstall <name>").description("Uninstall an upstream (dry-run by default \u2014 pass --apply to execute)").option("--apply", "execute the uninstall (default: dry-run preview only)").option("--dry-run", "force dry-run (overrides --apply if both are set)").option("--yes", "skip interactive confirm \u2014 requires --apply (CI / scripts)").option("--non-interactive", "alias for --yes (CI compat)").action(async (name, raw) => {
5319
+ program2.command("uninstall <name>").description("Uninstall an upstream (immediate by default \u2014 use --dry-run for preview)").option("--apply", "(deprecated; kept for backward compat \u2014 uninstall is immediate by default)").option("--dry-run", "preview only \u2014 do not delete files (opt-in for advanced users)").option("--yes", "skip interactive confirm (CI / scripts) \u2014 fatal with --dry-run").option("--non-interactive", "alias for --yes (CI compat)").action(async (name, raw) => {
5324
5320
  const yes = raw.yes === true || raw.nonInteractive === true;
5325
- if (yes && !raw.apply) {
5321
+ if (yes && raw.dryRun) {
5326
5322
  console.error(
5327
- `error: --yes requires --apply to execute
5328
- fix: harnessed uninstall ${name} --yes --apply`
5323
+ `error: --yes is incompatible with --dry-run (dry-run does not mutate)
5324
+ fix: harnessed uninstall ${name} --yes (immediate) OR harnessed uninstall ${name} --dry-run (preview)`
5329
5325
  );
5330
5326
  process.exit(2);
5331
5327
  }
@@ -5356,10 +5352,10 @@ function registerUninstall(program2) {
5356
5352
  process.exit(1);
5357
5353
  }
5358
5354
  const method = v.manifest.spec.install.method;
5359
- const dryRun = raw.dryRun === true || !raw.apply;
5355
+ const dryRun = raw.dryRun === true;
5360
5356
  if (dryRun) {
5361
5357
  console.log(`[dry-run] would uninstall '${resolvedName}' via method '${method}'`);
5362
- console.log(` run with --apply to execute`);
5358
+ console.log(` run without --dry-run to execute`);
5363
5359
  process.exit(2);
5364
5360
  }
5365
5361
  if (!yes) {