@osfactory/har 0.49.1 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -28694,6 +28694,10 @@ var init_control_config = __esm({
28694
28694
  });
28695
28695
 
28696
28696
  // src/utils/shell.ts
28697
+ function quoteShellArg(arg) {
28698
+ if (/^[A-Za-z0-9_./:=+-]+$/.test(arg)) return arg;
28699
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
28700
+ }
28697
28701
  function run(command2, options = {}) {
28698
28702
  try {
28699
28703
  const stdout = (0, import_child_process.execSync)(command2, {
@@ -29055,10 +29059,6 @@ var init_slot_registry = __esm({
29055
29059
  });
29056
29060
 
29057
29061
  // src/core/local-executor.ts
29058
- function quoteShellArg(arg) {
29059
- if (/^[A-Za-z0-9_./:=+-]+$/.test(arg)) return arg;
29060
- return `'${arg.replace(/'/g, `'\\''`)}'`;
29061
- }
29062
29062
  function buildLaunchFlagArgs(flags) {
29063
29063
  const args = [];
29064
29064
  if (flags.worktree === false) args.push("--no-worktree");
@@ -29790,10 +29790,32 @@ var init_slot_launch_guard_occupied = __esm({
29790
29790
  }
29791
29791
  });
29792
29792
 
29793
+ // src/utils/package-runner.ts
29794
+ function commandExists(command2) {
29795
+ const result = (0, import_child_process6.spawnSync)("sh", ["-c", `command -v ${JSON.stringify(command2)}`], {
29796
+ encoding: "utf8"
29797
+ });
29798
+ return result.status === 0 && Boolean(result.stdout?.trim());
29799
+ }
29800
+ function packageRunner() {
29801
+ if (cached !== void 0) return cached;
29802
+ const available = RUNNERS.find((runner) => commandExists(runner.split(" ")[0]));
29803
+ cached = available ?? RUNNERS[0];
29804
+ return cached;
29805
+ }
29806
+ var import_child_process6, RUNNERS, cached;
29807
+ var init_package_runner = __esm({
29808
+ "src/utils/package-runner.ts"() {
29809
+ "use strict";
29810
+ import_child_process6 = require("child_process");
29811
+ RUNNERS = ["npx --yes", "bunx", "pnpm dlx", "yarn dlx"];
29812
+ }
29813
+ });
29814
+
29793
29815
  // src/core/slot-preflight.ts
29794
29816
  function listPm2Processes() {
29795
29817
  try {
29796
- const raw = (0, import_child_process6.execSync)("npx --yes pm2 jlist", {
29818
+ const raw = (0, import_child_process7.execSync)(`${packageRunner()} pm2 jlist`, {
29797
29819
  encoding: "utf8",
29798
29820
  stdio: ["pipe", "pipe", "ignore"],
29799
29821
  timeout: 3e3
@@ -29874,7 +29896,7 @@ function inspectSlotReadiness(repoPath, agentId, options = {}) {
29874
29896
  remediation: "Stop the other harness session (./.har/teardown.sh in that repo) or use a different agent slot.",
29875
29897
  details: { processes: foreign.processes }
29876
29898
  });
29877
- remediations.push("Inspect with: npx pm2 jlist | grep agent-" + agentId);
29899
+ remediations.push(`Inspect with: ${packageRunner()} pm2 jlist | grep agent-${agentId}`);
29878
29900
  }
29879
29901
  if (usesPm2 && pm2Procs) {
29880
29902
  const slotPrefix = `har-${projectName}-agent-${agentId}-`;
@@ -30008,7 +30030,7 @@ function formatPreflightReport(agentId, readiness) {
30008
30030
  }
30009
30031
  return lines2.join("\n");
30010
30032
  }
30011
- var path16, import_child_process6;
30033
+ var path16, import_child_process7;
30012
30034
  var init_slot_preflight = __esm({
30013
30035
  "src/core/slot-preflight.ts"() {
30014
30036
  "use strict";
@@ -30019,14 +30041,15 @@ var init_slot_preflight = __esm({
30019
30041
  init_slot_launch_guard_occupied();
30020
30042
  init_slot_ports();
30021
30043
  init_slot_registry();
30022
- import_child_process6 = require("child_process");
30044
+ import_child_process7 = require("child_process");
30045
+ init_package_runner();
30023
30046
  }
30024
30047
  });
30025
30048
 
30026
30049
  // src/core/slot-status.ts
30027
30050
  function readGitRemote(repoPath) {
30028
30051
  try {
30029
- const remote = (0, import_child_process7.execSync)("git remote get-url origin", {
30052
+ const remote = (0, import_child_process8.execSync)("git remote get-url origin", {
30030
30053
  cwd: repoPath,
30031
30054
  encoding: "utf8",
30032
30055
  stdio: ["pipe", "pipe", "ignore"]
@@ -30038,7 +30061,7 @@ function readGitRemote(repoPath) {
30038
30061
  }
30039
30062
  function runGit2(cwd, args) {
30040
30063
  try {
30041
- return (0, import_child_process7.execSync)(`git ${args}`, {
30064
+ return (0, import_child_process8.execSync)(`git ${args}`, {
30042
30065
  cwd,
30043
30066
  encoding: "utf8",
30044
30067
  stdio: ["pipe", "pipe", "ignore"]
@@ -30114,7 +30137,7 @@ function lastBuildPass(run2) {
30114
30137
  }
30115
30138
  function listPm2Processes2() {
30116
30139
  try {
30117
- const raw = (0, import_child_process7.execSync)("npx --yes pm2 jlist", {
30140
+ const raw = (0, import_child_process8.execSync)(`${packageRunner()} pm2 jlist`, {
30118
30141
  encoding: "utf8",
30119
30142
  stdio: ["pipe", "pipe", "ignore"],
30120
30143
  timeout: 3e3
@@ -30217,14 +30240,14 @@ function collectEnvironmentStatus(repoPath) {
30217
30240
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
30218
30241
  };
30219
30242
  }
30220
- var fs16, os6, path17, import_child_process7, BYPASS_WARNING_MS;
30243
+ var fs16, os6, path17, import_child_process8, BYPASS_WARNING_MS;
30221
30244
  var init_slot_status = __esm({
30222
30245
  "src/core/slot-status.ts"() {
30223
30246
  "use strict";
30224
30247
  fs16 = __toESM(require("fs"));
30225
30248
  os6 = __toESM(require("os"));
30226
30249
  path17 = __toESM(require("path"));
30227
- import_child_process7 = require("child_process");
30250
+ import_child_process8 = require("child_process");
30228
30251
  init_env();
30229
30252
  init_manifest();
30230
30253
  init_stages();
@@ -30232,6 +30255,7 @@ var init_slot_status = __esm({
30232
30255
  init_runs();
30233
30256
  init_slot_registry();
30234
30257
  init_slot_preflight();
30258
+ init_package_runner();
30235
30259
  BYPASS_WARNING_MS = 15 * 60 * 1e3;
30236
30260
  }
30237
30261
  });
@@ -31227,11 +31251,11 @@ function buildTelemetryEnvBlock(attrs) {
31227
31251
  const lines2 = [
31228
31252
  "",
31229
31253
  "# HAR session attribution (generated)",
31230
- `HAR_SESSION_KEY=${attrs.sessionKey}`,
31231
- ...attrs.workUnitId ? [`HAR_WORK_UNIT_ID=${attrs.workUnitId}`] : [],
31232
- ...attrs.attemptId ? [`HAR_ATTEMPT_ID=${attrs.attemptId}`] : [],
31233
- `HAR_CONTROL_API_URL=${apiUrl}`,
31234
- `OTEL_RESOURCE_ATTRIBUTES=${buildOtelResourceAttributes(attrs)}`,
31254
+ `HAR_SESSION_KEY=${quoteShellArg(attrs.sessionKey)}`,
31255
+ ...attrs.workUnitId ? [`HAR_WORK_UNIT_ID=${quoteShellArg(attrs.workUnitId)}`] : [],
31256
+ ...attrs.attemptId ? [`HAR_ATTEMPT_ID=${quoteShellArg(attrs.attemptId)}`] : [],
31257
+ `HAR_CONTROL_API_URL=${quoteShellArg(apiUrl)}`,
31258
+ `OTEL_RESOURCE_ATTRIBUTES=${quoteShellArg(buildOtelResourceAttributes(attrs))}`,
31235
31259
  "# Agent telemetry: @osfactory/otel-hook \u2192 Mission Control (har telemetry on|off)"
31236
31260
  ];
31237
31261
  return lines2.join("\n") + "\n";
@@ -31264,6 +31288,7 @@ var init_telemetry_env = __esm({
31264
31288
  fs20 = __toESM(require("fs"));
31265
31289
  path21 = __toESM(require("path"));
31266
31290
  init_control_config();
31291
+ init_shell();
31267
31292
  TELEMETRY_MARKER_START = "# HAR session attribution (generated)";
31268
31293
  TELEMETRY_MARKER_END = "# end HAR telemetry";
31269
31294
  }
@@ -53489,8 +53514,8 @@ exec "${otelHookCommand}" run \\
53489
53514
  }
53490
53515
  return wrapperPath;
53491
53516
  }
53492
- function commandExists2(command2) {
53493
- const result = (0, import_child_process10.spawnSync)("sh", ["-c", `command -v ${JSON.stringify(command2)}`], {
53517
+ function commandExists3(command2) {
53518
+ const result = (0, import_child_process11.spawnSync)("sh", ["-c", `command -v ${JSON.stringify(command2)}`], {
53494
53519
  encoding: "utf8"
53495
53520
  });
53496
53521
  return result.status === 0 && Boolean(result.stdout?.trim());
@@ -53500,25 +53525,37 @@ function resolveOtelHookCommand(hooksHome) {
53500
53525
  if (fs41.existsSync(local)) return local;
53501
53526
  return null;
53502
53527
  }
53528
+ function resolveHooksInstaller(hooksHome) {
53529
+ const candidates = [
53530
+ {
53531
+ command: "npm",
53532
+ args: ["install", "--prefix", hooksHome, "--no-fund", "--no-audit", OTEL_HOOKS_PACKAGE]
53533
+ },
53534
+ { command: "bun", args: ["add", "--cwd", hooksHome, OTEL_HOOKS_PACKAGE] },
53535
+ { command: "pnpm", args: ["add", "--dir", hooksHome, OTEL_HOOKS_PACKAGE] }
53536
+ ];
53537
+ return candidates.find((candidate) => commandExists3(candidate.command)) ?? null;
53538
+ }
53503
53539
  function tryInstallPackage(hooksHome) {
53504
53540
  fs41.mkdirSync(hooksHome, { recursive: true });
53505
- if (!commandExists2("npm")) {
53541
+ const installer = resolveHooksInstaller(hooksHome);
53542
+ if (!installer) {
53506
53543
  return {
53507
53544
  ok: false,
53508
- detail: "npm not found; install Node.js to use @osfactory/otel-hook"
53545
+ detail: "no package manager found; install Node.js or bun to use @osfactory/otel-hook"
53509
53546
  };
53510
53547
  }
53511
- const result = (0, import_child_process10.spawnSync)(
53512
- "npm",
53513
- ["install", "--prefix", hooksHome, "--no-fund", "--no-audit", OTEL_HOOKS_PACKAGE],
53514
- { encoding: "utf8", timeout: 18e4 }
53515
- );
53548
+ const described = `${installer.command} ${installer.args.join(" ")}`;
53549
+ const result = (0, import_child_process11.spawnSync)(installer.command, installer.args, {
53550
+ encoding: "utf8",
53551
+ timeout: 18e4
53552
+ });
53516
53553
  if (result.status === 0 && fs41.existsSync(localOtelHookBin(hooksHome))) {
53517
- return { ok: true, detail: `npm install --prefix ${hooksHome} ${OTEL_HOOKS_PACKAGE}` };
53554
+ return { ok: true, detail: described };
53518
53555
  }
53519
53556
  return {
53520
53557
  ok: false,
53521
- detail: `npm install ${OTEL_HOOKS_PACKAGE} failed: ${(result.stderr || result.stdout || "").trim().slice(0, 400)}`
53558
+ detail: `${described} failed: ${(result.stderr || result.stdout || "").trim().slice(0, 400)}`
53522
53559
  };
53523
53560
  }
53524
53561
  function uninstallLegacyPythonHooks(hooksHome) {
@@ -53529,9 +53566,9 @@ function uninstallLegacyPythonHooks(hooksHome) {
53529
53566
  ["pip3", ["uninstall", "-y", "opentelemetry-hooks"]]
53530
53567
  ];
53531
53568
  for (const [cmd, args] of attempts) {
53532
- if (!commandExists2(cmd)) continue;
53569
+ if (!commandExists3(cmd)) continue;
53533
53570
  try {
53534
- (0, import_child_process10.spawnSync)(cmd, args, { encoding: "utf8", timeout: 6e4 });
53571
+ (0, import_child_process11.spawnSync)(cmd, args, { encoding: "utf8", timeout: 6e4 });
53535
53572
  } catch {
53536
53573
  }
53537
53574
  }
@@ -53563,7 +53600,7 @@ function normalizeLegacyProviderFlagsInCommand(command2) {
53563
53600
  }
53564
53601
  function runSetupProvider(providerId, otelHookCommand, wrapperPath) {
53565
53602
  const hookCommand = `${wrapperPath} --provider ${providerId}`;
53566
- const result = (0, import_child_process10.spawnSync)(
53603
+ const result = (0, import_child_process11.spawnSync)(
53567
53604
  otelHookCommand,
53568
53605
  [
53569
53606
  "setup",
@@ -53804,11 +53841,11 @@ function ensureOtelHooks(options) {
53804
53841
  function disableOtelHooksExport() {
53805
53842
  return writeOtelHooksConfig(buildOtelHooksConfig({ enabled: false }));
53806
53843
  }
53807
- var import_child_process10, fs41, os15, path47, OTEL_HOOKS_PACKAGE, OTEL_HOOKS_PROVIDERS, OTEL_HOOKS_AGENTS, LEGACY_CURSOR_ONLY_HOOK_EVENTS;
53844
+ var import_child_process11, fs41, os15, path47, OTEL_HOOKS_PACKAGE, OTEL_HOOKS_PROVIDERS, OTEL_HOOKS_AGENTS, LEGACY_CURSOR_ONLY_HOOK_EVENTS;
53808
53845
  var init_otel_hooks = __esm({
53809
53846
  "src/core/otel-hooks.ts"() {
53810
53847
  "use strict";
53811
- import_child_process10 = require("child_process");
53848
+ import_child_process11 = require("child_process");
53812
53849
  fs41 = __toESM(require("fs"));
53813
53850
  os15 = __toESM(require("os"));
53814
53851
  path47 = __toESM(require("path"));
@@ -70177,13 +70214,13 @@ var path44 = __toESM(require("path"));
70177
70214
  var readline5 = __toESM(require("readline"));
70178
70215
 
70179
70216
  // src/utils/clipboard.ts
70180
- var import_child_process8 = require("child_process");
70181
- function commandExists(command2) {
70182
- const probe = process.platform === "win32" ? (0, import_child_process8.spawnSync)("where", [command2], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] }) : (0, import_child_process8.spawnSync)("which", [command2], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] });
70217
+ var import_child_process9 = require("child_process");
70218
+ function commandExists2(command2) {
70219
+ const probe = process.platform === "win32" ? (0, import_child_process9.spawnSync)("where", [command2], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] }) : (0, import_child_process9.spawnSync)("which", [command2], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] });
70183
70220
  return probe.status === 0;
70184
70221
  }
70185
70222
  function tryPipeCommand(command2, args, text) {
70186
- const result = (0, import_child_process8.spawnSync)(command2, args, {
70223
+ const result = (0, import_child_process9.spawnSync)(command2, args, {
70187
70224
  input: text,
70188
70225
  encoding: "utf8",
70189
70226
  stdio: ["pipe", "ignore", "ignore"]
@@ -70198,12 +70235,12 @@ function writeOsc52Clipboard(text, stream = process.stderr) {
70198
70235
  return true;
70199
70236
  }
70200
70237
  function copyViaPlatformTools(text) {
70201
- if (process.platform === "darwin" && commandExists("pbcopy")) {
70238
+ if (process.platform === "darwin" && commandExists2("pbcopy")) {
70202
70239
  if (tryPipeCommand("pbcopy", [], text)) return { ok: true, method: "pbcopy" };
70203
70240
  }
70204
70241
  if (process.platform === "win32") {
70205
- if (commandExists("powershell")) {
70206
- const result = (0, import_child_process8.spawnSync)(
70242
+ if (commandExists2("powershell")) {
70243
+ const result = (0, import_child_process9.spawnSync)(
70207
70244
  "powershell",
70208
70245
  [
70209
70246
  "-NoProfile",
@@ -70219,18 +70256,18 @@ function copyViaPlatformTools(text) {
70219
70256
  );
70220
70257
  if (result.status === 0 && !result.error) return { ok: true, method: "powershell" };
70221
70258
  }
70222
- if (commandExists("clip") && tryPipeCommand("clip", [], text)) {
70259
+ if (commandExists2("clip") && tryPipeCommand("clip", [], text)) {
70223
70260
  return { ok: true, method: "clip" };
70224
70261
  }
70225
70262
  }
70226
70263
  if (process.platform === "linux" || process.platform === "freebsd" || process.platform === "openbsd") {
70227
- if (commandExists("wl-copy") && tryPipeCommand("wl-copy", [], text)) {
70264
+ if (commandExists2("wl-copy") && tryPipeCommand("wl-copy", [], text)) {
70228
70265
  return { ok: true, method: "wl-copy" };
70229
70266
  }
70230
- if (commandExists("xclip") && tryPipeCommand("xclip", ["-selection", "clipboard"], text)) {
70267
+ if (commandExists2("xclip") && tryPipeCommand("xclip", ["-selection", "clipboard"], text)) {
70231
70268
  return { ok: true, method: "xclip" };
70232
70269
  }
70233
- if (commandExists("xsel") && tryPipeCommand("xsel", ["--clipboard", "--input"], text)) {
70270
+ if (commandExists2("xsel") && tryPipeCommand("xsel", ["--clipboard", "--input"], text)) {
70234
70271
  return { ok: true, method: "xsel" };
70235
70272
  }
70236
70273
  }
@@ -70449,7 +70486,7 @@ init_validation();
70449
70486
  init_control_config();
70450
70487
 
70451
70488
  // src/core/control-lifecycle.ts
70452
- var import_child_process9 = require("child_process");
70489
+ var import_child_process10 = require("child_process");
70453
70490
  var path46 = __toESM(require("path"));
70454
70491
 
70455
70492
  // src/core/control-image.ts
@@ -70502,7 +70539,7 @@ function buildDockerRunArgs(options) {
70502
70539
  return args;
70503
70540
  }
70504
70541
  function runDocker(args) {
70505
- const result = (0, import_child_process9.spawnSync)("docker", args, {
70542
+ const result = (0, import_child_process10.spawnSync)("docker", args, {
70506
70543
  stdio: "inherit",
70507
70544
  env: process.env
70508
70545
  });
@@ -70512,7 +70549,7 @@ function runDocker(args) {
70512
70549
  return result.status ?? 1;
70513
70550
  }
70514
70551
  function controlContainerExists(name = CONTROL_CONTAINER_NAME) {
70515
- const result = (0, import_child_process9.spawnSync)("docker", ["ps", "-aq", "-f", `name=^${name}$`], {
70552
+ const result = (0, import_child_process10.spawnSync)("docker", ["ps", "-aq", "-f", `name=^${name}$`], {
70516
70553
  encoding: "utf8",
70517
70554
  stdio: ["pipe", "pipe", "ignore"]
70518
70555
  });
@@ -72352,13 +72389,13 @@ init_portal_credentials();
72352
72389
  // src/core/portal-login.ts
72353
72390
  var http = __toESM(require("http"));
72354
72391
  var import_crypto = require("crypto");
72355
- var import_child_process11 = require("child_process");
72392
+ var import_child_process12 = require("child_process");
72356
72393
  var CALLBACK_PATH = "/callback";
72357
72394
  var LOGIN_TIMEOUT_MS = 3 * 6e4;
72358
72395
  function openBrowser(url) {
72359
72396
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
72360
72397
  try {
72361
- const child = (0, import_child_process11.spawn)(cmd, args, { stdio: "ignore", detached: true });
72398
+ const child = (0, import_child_process12.spawn)(cmd, args, { stdio: "ignore", detached: true });
72362
72399
  child.on("error", () => {
72363
72400
  });
72364
72401
  child.unref();
@@ -73518,7 +73555,7 @@ __export(util_exports, {
73518
73555
  assertNever: () => assertNever,
73519
73556
  assertNotEqual: () => assertNotEqual,
73520
73557
  assignProp: () => assignProp,
73521
- cached: () => cached,
73558
+ cached: () => cached2,
73522
73559
  captureStackTrace: () => captureStackTrace,
73523
73560
  cleanEnum: () => cleanEnum,
73524
73561
  cleanRegex: () => cleanRegex,
@@ -73583,7 +73620,7 @@ function jsonStringifyReplacer(_2, value) {
73583
73620
  return value.toString();
73584
73621
  return value;
73585
73622
  }
73586
- function cached(getter) {
73623
+ function cached2(getter) {
73587
73624
  const set = false;
73588
73625
  return {
73589
73626
  get value() {
@@ -73672,7 +73709,7 @@ var captureStackTrace = Error.captureStackTrace ? Error.captureStackTrace : (...
73672
73709
  function isObject(data) {
73673
73710
  return typeof data === "object" && data !== null && !Array.isArray(data);
73674
73711
  }
73675
- var allowsEval = cached(() => {
73712
+ var allowsEval = cached2(() => {
73676
73713
  if (typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare")) {
73677
73714
  return false;
73678
73715
  }
@@ -75167,7 +75204,7 @@ function handleOptionalObjectResult(result, final, key, input) {
75167
75204
  }
75168
75205
  var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
75169
75206
  $ZodType.init(inst, def);
75170
- const _normalized = cached(() => {
75207
+ const _normalized = cached2(() => {
75171
75208
  const keys = Object.keys(def.shape);
75172
75209
  for (const k of keys) {
75173
75210
  if (!(def.shape[k] instanceof $ZodType)) {
@@ -75402,7 +75439,7 @@ var $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnio
75402
75439
  }
75403
75440
  return propValues;
75404
75441
  });
75405
- const disc = cached(() => {
75442
+ const disc = cached2(() => {
75406
75443
  const opts = def.options;
75407
75444
  const map = /* @__PURE__ */ new Map();
75408
75445
  for (const o of opts) {
@@ -69,6 +69,11 @@ how this project is built and tested. Use toolchain variables from `.env.agent.<
69
69
  The stock verify section is keyed by `HARNESS_ECOSYSTEM`; it is a starting point,
70
70
  not the repo's final contract. Replace conventions that do not match this project.
71
71
 
72
+ `${NPM_BIN}` may resolve to bun, pnpm, or yarn as well as npm, so keep Node steps
73
+ package-manager agnostic: always `${NPM_BIN:-npm} run <script>` (never bare
74
+ `${NPM_BIN} test`, which runs bun's own test runner), and never npm-only flags
75
+ such as `--prefix` — use a subshell like `(cd docs && ${NPM_BIN:-npm} run build)`.
76
+
72
77
  **Tier contract:**
73
78
 
74
79
  | Mode | Command | Intent |
@@ -25,7 +25,7 @@ Prefer targeted edits over full rewrites. Key files to review:
25
25
  Keep this accurate — it is the harness index. Update whenever scripts, stages, or workflow change.
26
26
 
27
27
  ### `.har/harness.env`, `verify.sh`, `provision-toolchain.sh`, `ecosystem.agent.template.cjs`, `CLAUDE.agent.md`
28
- Align commands and instructions with the current stack. Verify steps must use toolchain paths from `.env.agent.<id>` (`PYTHON_BIN`, `NPM_BIN`, `XCODEBUILD_BIN`, …) — never hardcoded venv or interpreter paths. Replace stock ecosystem conventions that do not match the repository; do not leave npm/pytest/go/cargo/maven/gradle examples in place by accident.
28
+ Align commands and instructions with the current stack. Verify steps must use toolchain paths from `.env.agent.<id>` (`PYTHON_BIN`, `NPM_BIN`, `XCODEBUILD_BIN`, …) — never hardcoded venv or interpreter paths. `NPM_BIN` may be bun, pnpm, or yarn, so Node steps must use `${NPM_BIN:-npm} run <script>` and avoid npm-only flags such as `--prefix`. Replace stock ecosystem conventions that do not match the repository; do not leave npm/pytest/go/cargo/maven/gradle examples in place by accident.
29
29
 
30
30
  ### `.har/env.template`, `setup-infra.sh`, `docker-compose.agent.yml`
31
31
  Update only if infra changed.
@@ -28,7 +28,7 @@ case "$COMMAND" in
28
28
  ENV_FILE="$(resolve_agent_env_file "$AGENT_ID" "$REPO_ROOT" || true)"
29
29
  WORKTREE_DIR="$(existing_slot_worktree "$AGENT_ID")"
30
30
  REG_FILE="$(slot_registry_file "$AGENT_ID")"
31
- PM2_RAW=$(npx --yes pm2 jlist 2>/dev/null || true)
31
+ PM2_RAW=$($(har_pkg_exec) pm2 jlist 2>/dev/null || true)
32
32
  PM2_FOUND=false
33
33
  PM2_FOREIGN=false
34
34
  PM2_LEGACY=false
@@ -123,22 +123,23 @@ process.stdin.on('end', () => {
123
123
  logs)
124
124
  SERVICE="${3:-}"
125
125
  if [ -n "$SERVICE" ]; then
126
- npx pm2 logs "${PM2_SLOT_PREFIX}-${SERVICE}" --lines 100
126
+ $(har_pkg_exec) pm2 logs "${PM2_SLOT_PREFIX}-${SERVICE}" --lines 100
127
127
  else
128
- npx pm2 logs --name "${PM2_SLOT_PREFIX}" --lines 100
128
+ $(har_pkg_exec) pm2 logs --name "${PM2_SLOT_PREFIX}" --lines 100
129
129
  fi
130
130
  ;;
131
131
 
132
132
  restart)
133
133
  SERVICE="${3:-}"
134
+ PKG_EXEC="$(har_pkg_exec)"
134
135
  if [ -n "$SERVICE" ]; then
135
- npx pm2 restart "${PM2_SLOT_PREFIX}-${SERVICE}"
136
+ $(har_pkg_exec) pm2 restart "${PM2_SLOT_PREFIX}-${SERVICE}"
136
137
  else
137
- npx pm2 jlist 2>/dev/null | node -e "
138
+ $(har_pkg_exec) pm2 jlist 2>/dev/null | node -e "
138
139
  const prefix = '${PM2_SLOT_PREFIX}-';
139
140
  const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
140
141
  const names = d.filter(x => x.name && x.name.startsWith(prefix)).map(x => x.name);
141
- names.forEach(n => require('child_process').execSync('npx pm2 restart ' + n, {stdio:'inherit'}));
142
+ names.forEach(n => require('child_process').execSync('${PKG_EXEC} pm2 restart ' + n, {stdio:'inherit'}));
142
143
  if (names.length === 0) console.log('No processes found for ${PM2_SLOT_PREFIX}');
143
144
  " 2>/dev/null || true
144
145
  fi
@@ -184,7 +184,7 @@ har_check_foreign_pm2() {
184
184
  local agent_id="$1"
185
185
  local pm2_raw
186
186
  har_harness_uses_pm2 || return 0
187
- pm2_raw="$(npx --yes pm2 jlist 2>/dev/null || true)"
187
+ pm2_raw="$($(har_pkg_exec) pm2 jlist 2>/dev/null || true)"
188
188
  [ -n "$pm2_raw" ] || return 0
189
189
  set +e
190
190
  echo "$pm2_raw" | node -e "
@@ -51,6 +51,10 @@ export HARNESS_AGENT_SLOT_MAX=5
51
51
  # Ecosystem: auto (detect from manifests) | node | python | go | rust | java | ruby | none
52
52
  export HARNESS_ECOSYSTEM="auto"
53
53
  # export HARNESS_INSTALL_CMD="make deps"
54
+ # Node package manager: auto-detected from package.json "packageManager" and the
55
+ # lockfile (bun.lock → bun, pnpm-lock.yaml → pnpm, yarn.lock → yarn, else npm),
56
+ # falling back to whichever is installed. Set to pin one: npm | bun | pnpm | yarn
57
+ # export HARNESS_NODE_PACKAGE_MANAGER="bun"
54
58
 
55
59
  # Shared infrastructure — space-separated docker compose service names from
56
60
  # docker-compose.agent.yml. Started ONCE by setup-infra.sh and shared by all
@@ -66,6 +70,119 @@ har_infra_enabled() {
66
70
  esac
67
71
  }
68
72
 
73
+ # ── Node package manager helpers ──────────────────────────────────────────────
74
+ # Mirrored verbatim in harness.env and provision-toolchain.sh so a sourcing
75
+ # script and the provisioning subprocess resolve the same tool.
76
+
77
+ # Package managers HAR can drive, in fallback preference order.
78
+ HAR_NODE_PACKAGE_MANAGERS="npm bun pnpm yarn"
79
+
80
+ # Package manager the repo declares: an explicit HARNESS_NODE_PACKAGE_MANAGER
81
+ # wins, then package.json "packageManager", then the lockfile. Empty when the
82
+ # repo says nothing.
83
+ har_node_declared_package_manager() {
84
+ local dir="${1:-.}"
85
+
86
+ if [ -n "${HARNESS_NODE_PACKAGE_MANAGER:-}" ]; then
87
+ echo "$HARNESS_NODE_PACKAGE_MANAGER"
88
+ return
89
+ fi
90
+ if [ -f "$dir/package.json" ]; then
91
+ local field
92
+ field="$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"\([a-z]*\)@.*/\1/p' "$dir/package.json" | head -1)"
93
+ if [ -n "$field" ]; then
94
+ echo "$field"
95
+ return
96
+ fi
97
+ fi
98
+ if [ -f "$dir/bun.lock" ] || [ -f "$dir/bun.lockb" ]; then echo bun; return; fi
99
+ if [ -f "$dir/pnpm-lock.yaml" ]; then echo pnpm; return; fi
100
+ if [ -f "$dir/yarn.lock" ]; then echo yarn; return; fi
101
+ if [ -f "$dir/package-lock.json" ]; then echo npm; return; fi
102
+ echo ""
103
+ }
104
+
105
+ # Package manager to actually run. A manager the repo declares but this machine
106
+ # lacks falls back to one that is installed, so a repo pinned to npm still
107
+ # provisions on a bun-only machine (and the reverse).
108
+ har_node_package_manager() {
109
+ local dir="${1:-.}"
110
+ local declared
111
+ declared="$(har_node_declared_package_manager "$dir")"
112
+
113
+ if [ -n "$declared" ] && command -v "$declared" >/dev/null 2>&1; then
114
+ echo "$declared"
115
+ return
116
+ fi
117
+
118
+ local candidate
119
+ for candidate in $HAR_NODE_PACKAGE_MANAGERS; do
120
+ if command -v "$candidate" >/dev/null 2>&1; then
121
+ echo "$candidate"
122
+ return
123
+ fi
124
+ done
125
+
126
+ echo "${declared:-npm}"
127
+ }
128
+
129
+ # Lockfile a manager writes, so a substitute install can clean up after itself.
130
+ har_node_lockfile() {
131
+ case "$1" in
132
+ bun) echo "bun.lock" ;;
133
+ npm) echo "package-lock.json" ;;
134
+ pnpm) echo "pnpm-lock.yaml" ;;
135
+ yarn) echo "yarn.lock" ;;
136
+ *) echo "" ;;
137
+ esac
138
+ }
139
+
140
+ # Install dependencies in a directory. When a substitute manager stands in for
141
+ # the one the repo declares, any lockfile it creates is removed afterwards:
142
+ # provisioning must not migrate the repo to a different package manager.
143
+ # (bun writes bun.lock even under --no-save, so the flags alone are not enough.)
144
+ har_node_install() {
145
+ local dir="$1"
146
+ local manager="$2"
147
+ local declared="${3:-}"
148
+ local substituting=false
149
+ local lockfile=""
150
+ local had_lockfile=false
151
+
152
+ if [ -n "$declared" ] && [ "$declared" != "$manager" ]; then
153
+ substituting=true
154
+ lockfile="$(har_node_lockfile "$manager")"
155
+ if [ -n "$lockfile" ] && [ -e "$dir/$lockfile" ]; then
156
+ had_lockfile=true
157
+ fi
158
+ fi
159
+
160
+ local code=0
161
+ (cd "$dir" && "$manager" install --silent) || code=$?
162
+
163
+ if [ "$substituting" = true ] && [ "$had_lockfile" = false ] && [ -n "$lockfile" ]; then
164
+ rm -f "$dir/$lockfile"
165
+ [ "$manager" = "bun" ] && rm -f "$dir/bun.lockb"
166
+ fi
167
+ return $code
168
+ }
169
+
170
+ # Package runner (npx equivalent) for one-off CLIs such as pm2, including the
171
+ # flag that keeps it non-interactive. Takes a manager name, or none to resolve
172
+ # from the environment and PATH.
173
+ har_pkg_exec() {
174
+ case "${1:-${HARNESS_NODE_PACKAGE_MANAGER:-}}" in
175
+ bun) echo "bunx"; return ;;
176
+ pnpm) echo "pnpm dlx"; return ;;
177
+ yarn) echo "yarn dlx"; return ;;
178
+ npm) echo "npx --yes"; return ;;
179
+ esac
180
+ if command -v npx >/dev/null 2>&1; then echo "npx --yes"; return; fi
181
+ if command -v bunx >/dev/null 2>&1; then echo "bunx"; return; fi
182
+ if command -v pnpm >/dev/null 2>&1; then echo "pnpm dlx"; return; fi
183
+ echo "npx --yes"
184
+ }
185
+
69
186
  # Postgres client — uses host tools when installed, otherwise docker exec into
70
187
  # the shared db container. Usage: har_pg psql -d postgres -c "SELECT 1"
71
188
  # har_pg createdb -T template_x agent_1
@@ -249,13 +249,13 @@ DEBUG_PORT="$DEBUG_PORT" \
249
249
 
250
250
  PM2_REGEX="$(har_pm2_delete_regex "$AGENT_ID")"
251
251
  # Stop existing processes for this agent (project-scoped — never touch other harnesses).
252
- npx --yes pm2 delete "$PM2_REGEX" 2>/dev/null || true
252
+ $(har_pkg_exec) pm2 delete "$PM2_REGEX" 2>/dev/null || true
253
253
 
254
254
  # Start PM2 processes
255
255
  log "Starting PM2 processes..."
256
256
  cd "$WORK_DIR"
257
- npx pm2 start "$ECOSYSTEM_FILE"
258
- npx pm2 save --force >/dev/null 2>&1 || true
257
+ $(har_pkg_exec) pm2 start "$ECOSYSTEM_FILE"
258
+ $(har_pkg_exec) pm2 save --force >/dev/null 2>&1 || true
259
259
 
260
260
  # Health check
261
261
  if [ -n "${HARNESS_HEALTH_CHECK_PATH:-}" ]; then