@osfactory/har 0.37.0 → 0.38.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
@@ -44506,7 +44506,7 @@ var AgentSlotStatusSchema = external_exports.object({
44506
44506
  attemptId: WorkAttemptIdSchema.optional(),
44507
44507
  sessionStatus: external_exports.enum(["starting", "active", "failed", "completed"]).optional(),
44508
44508
  lastError: external_exports.string().optional(),
44509
- /** Set when a failed/starting session can be resumed without --replace. */
44509
+ /** Set when a failed/starting session can be resumed with --resume instead of a fresh launch. */
44510
44510
  resumeHint: external_exports.string().optional(),
44511
44511
  /** Worktree checked out to no branch (legacy failure mode; should not happen with sessions). */
44512
44512
  detachedHead: external_exports.boolean().optional(),
@@ -68486,6 +68486,7 @@ function parseHarnessEnvInt(env3, key) {
68486
68486
 
68487
68487
  // src/harness/stages.ts
68488
68488
  var STAGE_REGISTRY_FILE = "stages.json";
68489
+ var HAR_AGENT_SLOT_ONBOARD_MAX = 10;
68489
68490
  var AGENT_REQUIRED_KINDS = /* @__PURE__ */ new Set([
68490
68491
  "launch",
68491
68492
  "verify",
@@ -68672,6 +68673,15 @@ ${maxLine}
68672
68673
  fs10.writeFileSync(envPath, content);
68673
68674
  return true;
68674
68675
  }
68676
+ function applyAgentSlotMax(repoPath, max) {
68677
+ const slots = HarnessAgentSlotsSchema.parse({ min: HAR_AGENT_SLOT_MIN, max });
68678
+ const registry2 = readStageRegistry(repoPath);
68679
+ writeStageRegistry(repoPath, {
68680
+ ...registry2,
68681
+ agentSlots: slots
68682
+ });
68683
+ syncAgentSlotsToHarnessEnv(repoPath);
68684
+ }
68675
68685
  function stageRequiresAgentId(stage) {
68676
68686
  if (typeof stage.requiresAgentId === "boolean") return stage.requiresAgentId;
68677
68687
  return AGENT_REQUIRED_KINDS.has(stage.kind);
@@ -73963,8 +73973,6 @@ function buildLaunchFlagArgs(flags) {
73963
73973
  const args = [];
73964
73974
  if (flags.worktree === false) args.push("--no-worktree");
73965
73975
  if (flags.claude) args.push("--claude");
73966
- if (flags.confirmReplace) args.push("--replace");
73967
- if (flags.force) args.push("--force");
73968
73976
  if (flags.resume) args.push("--resume");
73969
73977
  if (flags.workUnitId) args.push(`--work-id=${flags.workUnitId}`);
73970
73978
  if (flags.attemptId) args.push(`--attempt-id=${flags.attemptId}`);
@@ -74223,6 +74231,7 @@ function registryChanged(before, after) {
74223
74231
  function recordRepoForControlSync(repoPath) {
74224
74232
  if (process.env.HAR_CONTROL_DISABLED === "true") return;
74225
74233
  const resolved = canonicalizeControlRepoPath(repoPath);
74234
+ if (!resolveMainWorkingTree(resolved)) return;
74226
74235
  if (!readManifest(resolved)) return;
74227
74236
  const registry2 = readRegistry();
74228
74237
  if (registry2.repos.includes(resolved)) return;
@@ -74709,7 +74718,7 @@ function describeLaunchBase(repoPath) {
74709
74718
  return [
74710
74719
  `New session will be based on: ${branch}${sha ? ` @ ${sha}` : ""}`,
74711
74720
  ` (HEAD of ${harnessRoot})`,
74712
- " --replace does not choose this base \u2014 switch the main checkout first for a new unrelated task."
74721
+ " Switch the main checkout to your intended base before launching a new session."
74713
74722
  ];
74714
74723
  }
74715
74724
  function formatOccupiedSlot(slot, repoPath, resume) {
@@ -74721,27 +74730,30 @@ function formatOccupiedSlot(slot, repoPath, resume) {
74721
74730
  slot.sessionStatus ? ` Status: ${slot.sessionStatus}` : void 0,
74722
74731
  slot.lastError ? ` Error: ${slot.lastError}` : void 0,
74723
74732
  slot.sessionCreatedAt ? ` Since: ${slot.sessionCreatedAt}` : void 0,
74724
- slot.dirty ? " Git: dirty (uncommitted changes \u2014 commit or use force to discard)" : " Git: clean",
74733
+ slot.dirty ? " Git: dirty (uncommitted changes \u2014 commit or discard them in the worktree)" : " Git: clean",
74725
74734
  "",
74726
74735
  ...describeLaunchBase(repoPath),
74727
74736
  ""
74728
74737
  ];
74729
74738
  if (resume || slot.sessionStatus === "failed" || slot.sessionStatus === "starting") {
74730
74739
  lines2.push(
74731
- "This session failed partway through launch. Resume without replacing:",
74740
+ "This session failed partway through launch. Resume without a new launch:",
74732
74741
  ` har env launch ${slot.agentId} --resume`,
74733
74742
  ` har env recover ${slot.agentId}`,
74734
74743
  ` ./.har/launch.sh ${slot.agentId} --resume`,
74735
74744
  ""
74736
74745
  );
74746
+ } else {
74747
+ lines2.push(
74748
+ "Free the slot before starting a new session:",
74749
+ slot.dirty ? ` Commit or discard changes in the worktree, then: har env teardown ${slot.agentId} (or complete ${slot.agentId})` : ` har env complete ${slot.agentId} # or: har env teardown ${slot.agentId}`,
74750
+ ` har env launch ${slot.agentId}`,
74751
+ ""
74752
+ );
74737
74753
  }
74738
74754
  lines2.push(
74739
- "Prefer complete/teardown when the previous task is finished, then launch.",
74740
- "Replacing removes the worktree. The session branch is kept only if you committed.",
74741
- "Gitignored paths (state/, runs/, local clones) are NOT preserved.",
74742
- "",
74743
- "To replace: pass confirmReplace=true (MCP), --replace (CLI), or answer y at the prompt.",
74744
- "If the worktree is dirty, also pass force=true / --force after explicit user approval."
74755
+ "complete/teardown remove the worktree. The session branch is kept only if you committed.",
74756
+ "Gitignored paths (state/, runs/, local clones) are NOT preserved."
74745
74757
  );
74746
74758
  return lines2.filter(Boolean).join("\n");
74747
74759
  }
@@ -74761,42 +74773,18 @@ function checkLaunchGuard(repoPath, agentId, options = {}) {
74761
74773
  reason: [
74762
74774
  `Slot ${agentId} is not resumable (status=${session?.status ?? "none"}).`,
74763
74775
  "Only failed or starting sessions can be resumed.",
74764
- "Use a normal launch with --replace to start fresh."
74776
+ `Free the slot first: har env teardown ${agentId} (or complete ${agentId}), then har env launch ${agentId}.`
74765
74777
  ].join("\n")
74766
74778
  };
74767
74779
  }
74768
74780
  return { allowed: true, slot };
74769
74781
  }
74770
- if (isSlotResumable(session) && !options.confirmReplace) {
74771
- return {
74772
- allowed: false,
74773
- blocked: true,
74774
- slot,
74775
- reason: formatOccupiedSlot(slot, repoPath)
74776
- };
74777
- }
74778
- if (!options.confirmReplace) {
74779
- return {
74780
- allowed: false,
74781
- blocked: true,
74782
- slot,
74783
- reason: formatOccupiedSlot(slot, repoPath)
74784
- };
74785
- }
74786
- if (slot.dirty && !options.force) {
74787
- return {
74788
- allowed: false,
74789
- blocked: true,
74790
- slot,
74791
- reason: [
74792
- formatOccupiedSlot(slot, repoPath),
74793
- "",
74794
- "The occupied worktree has uncommitted changes.",
74795
- "Pass force=true / --force to discard them (only after explicit user approval)."
74796
- ].join("\n")
74797
- };
74798
- }
74799
- return { allowed: true, slot };
74782
+ return {
74783
+ allowed: false,
74784
+ blocked: true,
74785
+ slot,
74786
+ reason: formatOccupiedSlot(slot, repoPath)
74787
+ };
74800
74788
  }
74801
74789
 
74802
74790
  // src/core/slot-preflight.ts
@@ -74858,24 +74846,20 @@ function inspectSlotReadiness(repoPath, agentId, options = {}) {
74858
74846
  const warnings = [];
74859
74847
  const ports = {};
74860
74848
  const session = readSlotRegistry(harnessRoot, agentId);
74861
- const guard = options.occupied !== void 0 ? options.occupied.active ? options.resume && isSlotResumable(session) ? { allowed: true } : !options.confirmReplace ? {
74849
+ const guard = options.occupied !== void 0 ? options.occupied.active ? options.resume && isSlotResumable(session) ? { allowed: true } : {
74862
74850
  allowed: false,
74863
74851
  blocked: true,
74864
74852
  reason: `Slot ${agentId} is already in use.`
74865
- } : options.occupied.dirty && !options.force ? {
74866
- allowed: false,
74867
- blocked: true,
74868
- reason: `Slot ${agentId} worktree has uncommitted changes.`
74869
- } : { allowed: true } : { allowed: true } : checkLaunchGuard(repoPath, agentId, options);
74853
+ } : { allowed: true } : checkLaunchGuard(repoPath, agentId, options);
74870
74854
  if (!guard.allowed) {
74871
74855
  const resumable = isSlotResumable(session);
74872
74856
  blockers.push({
74873
74857
  code: options.occupied?.dirty ? "slot_dirty" : resumable ? "slot_resumable" : "slot_occupied",
74874
74858
  message: guard.reason ?? `Slot ${agentId} is occupied.`,
74875
- remediation: resumable ? `Resume the partial launch: har env launch ${agentId} --resume (or har env recover ${agentId})` : guard.slot?.dirty ? "Commit changes in the worktree, or pass --force after explicit user approval." : "Pass --replace (CLI), confirmReplace=true (MCP), or answer y at the launch prompt."
74859
+ remediation: resumable ? `Resume the partial launch: har env launch ${agentId} --resume (or har env recover ${agentId})` : `Free the slot: har env teardown ${agentId} (or complete ${agentId}), then har env launch ${agentId}.`
74876
74860
  });
74877
74861
  remediations.push(
74878
- resumable ? `har env launch ${agentId} --resume` : guard.slot?.dirty ? "har env preflight <id> --replace --force" : "har env preflight <id> --replace"
74862
+ resumable ? `har env launch ${agentId} --resume` : `har env teardown ${agentId}`
74879
74863
  );
74880
74864
  }
74881
74865
  const pm2Procs = usesPm2 ? options.pm2Processes !== void 0 ? options.pm2Processes : listPm2Processes() : void 0;
@@ -74904,7 +74888,7 @@ function inspectSlotReadiness(repoPath, agentId, options = {}) {
74904
74888
  blockers.push({
74905
74889
  code: "project_mismatch",
74906
74890
  message: `Slot registry projectName=${session.projectName} does not match harness ${projectName}.`,
74907
- remediation: "Teardown the stale session and relaunch with --replace."
74891
+ remediation: "Teardown the stale session, then relaunch."
74908
74892
  });
74909
74893
  }
74910
74894
  }
@@ -76397,23 +76381,11 @@ async function waitForControlApi(apiUrl = getControlApiUrl(), timeoutMs = 6e4, i
76397
76381
  return false;
76398
76382
  }
76399
76383
  async function discoverHarRepos(options) {
76400
- const apiUrl = options?.apiUrl ?? getControlApiUrl();
76401
76384
  const repoPaths = new Set(listRegisteredRepos());
76402
76385
  if (options?.cwd) {
76403
76386
  const cwd = canonicalizeControlRepoPath(options.cwd);
76404
76387
  if (readManifest(cwd)) repoPaths.add(cwd);
76405
76388
  }
76406
- try {
76407
- const listResponse = await fetch(`${apiUrl}/api/repos`);
76408
- if (listResponse.ok) {
76409
- const repos = await listResponse.json();
76410
- for (const repo of repos) {
76411
- const resolved = canonicalizeControlRepoPath(repo.path);
76412
- if (readManifest(resolved)) repoPaths.add(resolved);
76413
- }
76414
- }
76415
- } catch {
76416
- }
76417
76389
  return [...repoPaths];
76418
76390
  }
76419
76391
  async function syncReposWithControl(options) {
@@ -76654,6 +76626,7 @@ async function syncRepoWithControlAsync(repoPath) {
76654
76626
  if (process.env.HAR_CONTROL_DISABLED === "true" || process.env.NODE_ENV === "test") return;
76655
76627
  const verbose = process.env.HAR_CONTROL_VERBOSE === "true";
76656
76628
  try {
76629
+ recordRepoForControlSync(repoPath);
76657
76630
  const apiUrl = getControlApiUrl();
76658
76631
  if (!await isControlApiReachable(apiUrl)) {
76659
76632
  if (verbose) {
@@ -77013,10 +76986,7 @@ var RunService = class {
77013
76986
  return this.executor.listArtifacts({ repoPath: options.repoPath }, { stageId: options.stageId });
77014
76987
  }
77015
76988
  async preflightEnvironment(options) {
77016
- const readiness = inspectSlotReadiness(options.repoPath, options.agentId, {
77017
- confirmReplace: options.confirmReplace,
77018
- force: options.force
77019
- });
76989
+ const readiness = inspectSlotReadiness(options.repoPath, options.agentId, {});
77020
76990
  const report = formatPreflightReport(options.agentId, readiness);
77021
76991
  return {
77022
76992
  code: readiness.canLaunch ? 0 : 1,
@@ -77028,8 +76998,6 @@ var RunService = class {
77028
76998
  }
77029
76999
  async launchEnvironment(options) {
77030
77000
  const guard = checkLaunchGuard2(options.repoPath, options.agentId, {
77031
- confirmReplace: options.confirmReplace,
77032
- force: options.force,
77033
77001
  resume: options.resume
77034
77002
  });
77035
77003
  if (!guard.allowed) {
@@ -77079,7 +77047,7 @@ Usage from Cursor / Claude / Codex (@osfactory/otel-hook) will appear under Work
77079
77047
  const attemptId = workUnitId ? resumableSession?.attemptId ?? crypto5.randomUUID() : void 0;
77080
77048
  if (workUnitId && attemptId) {
77081
77049
  const conflictingSession = listSlotRegistryEntries(harnessRoot).find(
77082
- (entry) => entry.workUnitId === workUnitId && !(entry.agentId === options.agentId && (options.resume || options.confirmReplace))
77050
+ (entry) => entry.workUnitId === workUnitId && !(entry.agentId === options.agentId && options.resume)
77083
77051
  );
77084
77052
  if (conflictingSession) {
77085
77053
  return {
@@ -77112,8 +77080,6 @@ Usage from Cursor / Claude / Codex (@osfactory/otel-hook) will appear under Work
77112
77080
  launchFlags: {
77113
77081
  worktree: options.worktree,
77114
77082
  claude: options.claude,
77115
- confirmReplace: options.confirmReplace,
77116
- force: options.force,
77117
77083
  resume: options.resume,
77118
77084
  workUnitId,
77119
77085
  attemptId
@@ -77752,10 +77718,11 @@ Slot lifecycle (do not confuse these):
77752
77718
  complete finish successfully: full verify, then teardown (frees the slot)
77753
77719
  teardown abandon / free the slot without a completion validation
77754
77720
 
77755
- --replace only confirms destroying a previous session on that slot id
77756
- it does NOT choose main \u2014 new session still uses current HEAD
77757
- --force additionally discard dirty uncommitted work (user approval only)
77758
- --resume recover failed/starting; never use --replace for that
77721
+ Occupied slots always block a new launch:
77722
+ har env complete <id> # or: har env teardown <id>
77723
+ har env launch <id> # then start the new session
77724
+
77725
+ --resume recover a failed/starting session; never needed for a fresh launch
77759
77726
 
77760
77727
  For a new unrelated task: checkout main (or your intended base) on the main
77761
77728
  repo checkout, free the slot (complete/teardown), then launch.`;
@@ -77768,23 +77735,23 @@ var HAR_ENV_EPILOG = `Environment lifecycle:
77768
77735
  teardown <id> free the slot without recording a completion validation
77769
77736
 
77770
77737
  Occupied slots:
77771
- Prefer complete/teardown when the previous task is finished, then launch.
77772
- Use --replace only to reuse the same slot id immediately (destroys old worktree).
77773
- --replace does not select the base branch \u2014 HEAD of --repo does.
77774
- Dirty previous worktree also needs --force after explicit user approval.`;
77738
+ Always blocked for a fresh launch \u2014 free the slot first, then launch again:
77739
+ har env complete <id> # or: har env teardown <id>
77740
+ har env launch <id>
77741
+ A new launch uses the main checkout's current HEAD \u2014 switch that checkout to
77742
+ your intended base first.
77743
+ Failed/starting sessions can be resumed instead: har env launch <id> --resume
77744
+ (or har env recover <id>).`;
77775
77745
  var LAUNCH_COMMAND_DESCRIBE = "Start a fresh agent session (new worktree from the main checkout HEAD)";
77776
- var LAUNCH_REPLACE_DESCRIBE = "Destroy the previous session on this slot and start another (does not choose main; still uses --repo HEAD)";
77777
- var LAUNCH_FORCE_DESCRIBE = "With --replace: discard uncommitted changes in the old worktree (only after explicit user approval)";
77778
- var LAUNCH_RESUME_DESCRIBE = "Continue a failed or starting launch without creating a new worktree (prefer over --replace)";
77746
+ var LAUNCH_RESUME_DESCRIBE = "Continue a failed or starting launch without creating a new worktree";
77779
77747
  var LAUNCH_EPILOG = `Every launch creates a NEW session from the current HEAD of --repo (the main
77780
77748
  checkout). The worktree path encodes that base branch \u2014 switch --repo to main
77781
77749
  before launch for a new unrelated task unless you intentionally stack on a
77782
77750
  feature branch.
77783
77751
 
77784
77752
  Free slot: har env launch 1
77785
- Occupied: har env complete 1 # or teardown \u2014 then launch
77786
- har env launch 1 --replace # reuse slot id now
77787
- har env launch 1 --replace --force # + discard dirty work
77753
+ Occupied: har env complete 1 # or teardown
77754
+ har env launch 1 # then start the new session
77788
77755
  Failed launch: har env recover 1 # or: har env launch 1 --resume`;
77789
77756
 
77790
77757
  // src/cli/commands/env.ts
@@ -77936,15 +77903,7 @@ var envCommand = {
77936
77903
  type: "boolean",
77937
77904
  default: true,
77938
77905
  describe: "Use an isolated git worktree (default)"
77939
- }).option("claude", { type: "boolean", default: false }).option("replace", {
77940
- type: "boolean",
77941
- default: false,
77942
- describe: LAUNCH_REPLACE_DESCRIBE
77943
- }).option("force", {
77944
- type: "boolean",
77945
- default: false,
77946
- describe: LAUNCH_FORCE_DESCRIBE
77947
- }).option("resume", {
77906
+ }).option("claude", { type: "boolean", default: false }).option("resume", {
77948
77907
  type: "boolean",
77949
77908
  default: false,
77950
77909
  describe: LAUNCH_RESUME_DESCRIBE
@@ -77967,15 +77926,7 @@ var envCommand = {
77967
77926
  ).command(
77968
77927
  "preflight <id>",
77969
77928
  "Check whether a slot can launch now (ports, PM2, Docker, occupied slot)",
77970
- (y2) => y2.positional("id", { type: "number", describe: "Agent slot id (see .har/stages.json agentSlots)" }).option("repo", { type: "string", default: "." }).option("json", { type: "boolean", default: false, describe: "Structured JSON output" }).option("replace", {
77971
- type: "boolean",
77972
- default: false,
77973
- describe: "Treat an occupied slot as replaceable (same as launch --replace)"
77974
- }).option("force", {
77975
- type: "boolean",
77976
- default: false,
77977
- describe: "Allow replacing a dirty worktree (only after explicit approval)"
77978
- }),
77929
+ (y2) => y2.positional("id", { type: "number", describe: "Agent slot id (see .har/stages.json agentSlots)" }).option("repo", { type: "string", default: "." }).option("json", { type: "boolean", default: false, describe: "Structured JSON output" }),
77979
77930
  handlePreflight
77980
77931
  ).command(
77981
77932
  "verify <id>",
@@ -78071,7 +78022,6 @@ async function handleInit(argv) {
78071
78022
  }
78072
78023
  divider();
78073
78024
  success("Harness initialized!");
78074
- recordRepoForControlSync(repoPath);
78075
78025
  await handleCommitGateOnboarding({
78076
78026
  repoPath,
78077
78027
  ...onboarding.commitGate,
@@ -78336,9 +78286,7 @@ async function handlePreflight(argv) {
78336
78286
  const agentId = validateAgentId(argv.id, repo);
78337
78287
  const result = await preflightEnvironment({
78338
78288
  repoPath: repo,
78339
- agentId,
78340
- confirmReplace: argv.replace,
78341
- force: argv.force
78289
+ agentId
78342
78290
  });
78343
78291
  if (argv.json) {
78344
78292
  const output = SlotReadinessSchema.parse(result.readiness);
@@ -78352,46 +78300,11 @@ async function handlePreflight(argv) {
78352
78300
  async function handleLaunch(argv) {
78353
78301
  const repo = path50.resolve(argv.repo);
78354
78302
  const agentId = validateAgentId(argv.id, repo);
78355
- let confirmReplace = argv.replace;
78356
- let force = argv.force;
78357
78303
  if (!argv.resume) {
78358
- const guard = checkLaunchGuard2(repo, agentId, { confirmReplace, force });
78304
+ const guard = checkLaunchGuard2(repo, agentId, {});
78359
78305
  if (!guard.allowed && guard.blocked) {
78360
- if (!confirmReplace && process.stdin.isTTY && process.stdout.isTTY) {
78361
- warn("Occupied slot \u2014 review before replacing:");
78362
- console.error(guard.reason ?? "");
78363
- const readline10 = await import("readline");
78364
- const rl = readline10.createInterface({ input: process.stdin, output: process.stderr });
78365
- const answer = await new Promise((resolve45) => {
78366
- rl.question("Replace this slot? [y/N] ", resolve45);
78367
- });
78368
- rl.close();
78369
- if (!/^[Yy]$/.test(answer.trim())) {
78370
- error("Aborted \u2014 slot left unchanged.");
78371
- process.exit(2);
78372
- }
78373
- confirmReplace = true;
78374
- }
78375
- }
78376
- const guardAfterConfirm = checkLaunchGuard2(repo, agentId, { confirmReplace, force });
78377
- if (!guardAfterConfirm.allowed && guardAfterConfirm.blocked && guardAfterConfirm.slot?.dirty) {
78378
- if (!force && process.stdin.isTTY && process.stdout.isTTY) {
78379
- warn("Worktree has uncommitted changes.");
78380
- const readline10 = await import("readline");
78381
- const rl = readline10.createInterface({ input: process.stdin, output: process.stderr });
78382
- const answer = await new Promise((resolve45) => {
78383
- rl.question("Discard uncommitted changes? [y/N] ", resolve45);
78384
- });
78385
- rl.close();
78386
- if (!/^[Yy]$/.test(answer.trim())) {
78387
- error("Aborted \u2014 uncommitted work preserved.");
78388
- process.exit(2);
78389
- }
78390
- force = true;
78391
- } else if (!force) {
78392
- error(guardAfterConfirm.reason ?? "Dirty worktree requires --force.");
78393
- process.exit(2);
78394
- }
78306
+ error(guard.reason ?? `Slot ${agentId} is occupied.`);
78307
+ process.exit(2);
78395
78308
  }
78396
78309
  }
78397
78310
  const result = await launchEnvironment({
@@ -78399,8 +78312,6 @@ async function handleLaunch(argv) {
78399
78312
  agentId,
78400
78313
  worktree: argv.worktree,
78401
78314
  claude: argv.claude,
78402
- confirmReplace,
78403
- force,
78404
78315
  resume: argv.resume,
78405
78316
  workUnitId: argv.workId,
78406
78317
  source: argv.workSource,
@@ -78433,8 +78344,6 @@ async function handleRecover(argv) {
78433
78344
  repo: argv.repo,
78434
78345
  worktree: true,
78435
78346
  claude: false,
78436
- replace: false,
78437
- force: false,
78438
78347
  resume: true,
78439
78348
  workId: void 0,
78440
78349
  workSource: void 0,
@@ -89250,13 +89159,9 @@ var LaunchEnvironmentInputSchema = external_exports.object({
89250
89159
  agentId: agentIdSchema,
89251
89160
  worktree: external_exports.boolean().default(true),
89252
89161
  claude: external_exports.boolean().default(false),
89253
- confirmReplace: external_exports.boolean().default(false).describe(
89254
- "Destroy the previous session on this slot and start another from main-checkout HEAD. Does not choose main. Prefer complete/teardown when the prior task is finished. Call har_get_status first; get explicit user approval."
89255
- ),
89256
- force: external_exports.boolean().default(false).describe(
89257
- "Discard uncommitted changes in a dirty occupied worktree. Requires confirmReplace=true and explicit user approval \u2014 never set autonomously."
89162
+ resume: external_exports.boolean().default(false).describe(
89163
+ "Resume a failed or partial launch without creating a new worktree. Only valid for failed/starting sessions \u2014 otherwise call har_get_status, then har_complete_environment or har_teardown_environment before launching again."
89258
89164
  ),
89259
- resume: external_exports.boolean().default(false).describe("Resume a failed or partial launch without creating a new worktree."),
89260
89165
  workUnitId: external_exports.string().min(1).max(128).optional(),
89261
89166
  source: external_exports.string().min(1).max(64).optional(),
89262
89167
  sourceUrl: external_exports.string().url().optional(),
@@ -89282,9 +89187,7 @@ var LaunchEnvironmentOutputSchema = ShellRunOutputSchema.extend({
89282
89187
  });
89283
89188
  var PreflightEnvironmentInputSchema = external_exports.object({
89284
89189
  repo: external_exports.string().default("."),
89285
- agentId: agentIdSchema,
89286
- confirmReplace: external_exports.boolean().default(false).describe("Treat an occupied slot as replaceable (same as launch confirmReplace)."),
89287
- force: external_exports.boolean().default(false).describe("Allow replacing a dirty worktree (requires explicit user approval).")
89190
+ agentId: agentIdSchema
89288
89191
  });
89289
89192
  var PreflightEnvironmentOutputSchema = ShellRunOutputSchema.extend({
89290
89193
  readiness: SlotReadinessSchema,
@@ -89384,24 +89287,16 @@ var HAR_MCP_TOOLS = [
89384
89287
  },
89385
89288
  {
89386
89289
  name: "har_launch_environment",
89387
- description: "Start a FRESH agent session from the main checkout HEAD (switch that checkout to main first for a new unrelated task). Run BEFORE editing any file: returns workDir \u2014 ALL edits go there, never the main checkout. Prefer har_complete_environment / har_teardown_environment when a prior task is done. Occupied slots block until confirmReplace=true (does NOT choose main). force=true discards dirty work \u2014 never set without user approval. Failed launches: resume=true / har_recover_environment.",
89290
+ description: "Start a FRESH agent session from the main checkout HEAD (switch that checkout to main first for a new unrelated task). Run BEFORE editing any file: returns workDir \u2014 ALL edits go there, never the main checkout. Occupied slots always block: call har_get_status, then har_complete_environment or har_teardown_environment, then relaunch \u2014 unless the session is failed/starting, in which case pass resume=true (or use har_recover_environment).",
89388
89291
  inputSchema: objectJsonSchema(
89389
89292
  {
89390
89293
  repo: repoJsonProperty,
89391
89294
  agentId: agentIdJsonProperty,
89392
89295
  worktree: { type: "boolean" },
89393
89296
  claude: { type: "boolean" },
89394
- confirmReplace: {
89395
- type: "boolean",
89396
- description: "Destroy the previous session on this slot and start another from main-checkout HEAD. Does not choose main. Prefer complete/teardown when the prior task is finished. Call har_get_status first; get explicit user approval."
89397
- },
89398
- force: {
89399
- type: "boolean",
89400
- description: "Discard uncommitted changes when replacing a dirty worktree. Requires confirmReplace=true and explicit user approval."
89401
- },
89402
89297
  resume: {
89403
89298
  type: "boolean",
89404
- description: "Resume a failed or partial launch (status failed/starting) without confirmReplace. Preserves worktree and env."
89299
+ description: "Resume a failed or partial launch (status failed/starting) without creating a new worktree. Preserves worktree and env. Not usable to replace an active/completed slot \u2014 free it with har_complete_environment or har_teardown_environment first."
89405
89300
  },
89406
89301
  workUnitId: {
89407
89302
  type: "string",
@@ -89435,15 +89330,7 @@ var HAR_MCP_TOOLS = [
89435
89330
  inputSchema: objectJsonSchema(
89436
89331
  {
89437
89332
  repo: repoJsonProperty,
89438
- agentId: agentIdJsonProperty,
89439
- confirmReplace: {
89440
- type: "boolean",
89441
- description: "Treat an occupied slot as replaceable (same as launch confirmReplace)."
89442
- },
89443
- force: {
89444
- type: "boolean",
89445
- description: "Allow replacing a dirty worktree (only after explicit user approval)."
89446
- }
89333
+ agentId: agentIdJsonProperty
89447
89334
  },
89448
89335
  ["agentId"]
89449
89336
  )
@@ -89577,7 +89464,6 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
89577
89464
  smoke: input.smoke,
89578
89465
  profile: input.profile
89579
89466
  });
89580
- recordRepoForControlSync(repo);
89581
89467
  return jsonContent({
89582
89468
  harnessDir: result.harnessDir,
89583
89469
  validation: result.validation,
@@ -89593,8 +89479,6 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
89593
89479
  agentId,
89594
89480
  worktree: input.worktree,
89595
89481
  claude: input.claude,
89596
- confirmReplace: input.confirmReplace,
89597
- force: input.force,
89598
89482
  resume: input.resume,
89599
89483
  workUnitId: input.workUnitId,
89600
89484
  source: input.source,
@@ -89617,8 +89501,6 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
89617
89501
  agentId,
89618
89502
  worktree: input.worktree,
89619
89503
  claude: input.claude,
89620
- confirmReplace: false,
89621
- force: false,
89622
89504
  resume: true,
89623
89505
  workUnitId: input.workUnitId,
89624
89506
  source: input.source,
@@ -89638,9 +89520,7 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
89638
89520
  const agentId = validateAgentId(input.agentId, repo);
89639
89521
  const result = await preflightEnvironment({
89640
89522
  repoPath: repo,
89641
- agentId,
89642
- confirmReplace: input.confirmReplace,
89643
- force: input.force
89523
+ agentId
89644
89524
  });
89645
89525
  const parsed = PreflightEnvironmentOutputSchema.parse(result);
89646
89526
  return {
@@ -89977,9 +89857,20 @@ async function runOnboarding(options, deps = {}) {
89977
89857
  warn("Harness has validation errors \u2014 review .har/ after adaptation.");
89978
89858
  }
89979
89859
  harnessInitialized = true;
89980
- recordRepoForControlSync(repoPath);
89981
89860
  success("Harness scaffolded");
89982
89861
  }
89862
+ let agentSlots = null;
89863
+ if (harnessExists(repoPath)) {
89864
+ if (options.agentSlotsMax !== void 0) {
89865
+ applyAgentSlotMax(repoPath, options.agentSlotsMax);
89866
+ info(`Agent slots: 1\u2013${options.agentSlotsMax} parallel (written to .har/stages.json)`);
89867
+ }
89868
+ try {
89869
+ agentSlots = getAgentSlotRange(repoPath);
89870
+ } catch {
89871
+ agentSlots = null;
89872
+ }
89873
+ }
89983
89874
  const pluginsApplied = [];
89984
89875
  const pluginWarnings = [];
89985
89876
  if (options.plugins.length > 0) {
@@ -90028,7 +89919,8 @@ async function runOnboarding(options, deps = {}) {
90028
89919
  pluginsApplied,
90029
89920
  pluginWarnings,
90030
89921
  adaptationPromptPath,
90031
- adaptationPromptCopied
89922
+ adaptationPromptCopied,
89923
+ agentSlots
90032
89924
  };
90033
89925
  }
90034
89926
  async function finalizeOnboardingAdaptation(options) {
@@ -90059,6 +89951,27 @@ function parsePluginsFlag(raw) {
90059
89951
  }
90060
89952
  return selected;
90061
89953
  }
89954
+ function defaultAgentSlotMaxForProfile(profile) {
89955
+ return profile === "default" ? 5 : 3;
89956
+ }
89957
+ function parseAgentSlotsFlag(raw) {
89958
+ if (raw === void 0) return void 0;
89959
+ if (!Number.isInteger(raw) || raw < HAR_AGENT_SLOT_MIN || raw > HAR_AGENT_SLOT_ONBOARD_MAX) {
89960
+ throw new Error(
89961
+ `--agent-slots must be an integer from ${HAR_AGENT_SLOT_MIN} to ${HAR_AGENT_SLOT_ONBOARD_MAX}`
89962
+ );
89963
+ }
89964
+ return raw;
89965
+ }
89966
+ function resolveDefaultAgentSlotsMax(repoPath, profile, alreadyPresent) {
89967
+ if (alreadyPresent) {
89968
+ try {
89969
+ return getAgentSlotRange(repoPath).max;
89970
+ } catch {
89971
+ }
89972
+ }
89973
+ return defaultAgentSlotMaxForProfile(profile);
89974
+ }
90062
89975
  async function pauseForGuide(autoYes) {
90063
89976
  if (autoYes || !process.stdin.isTTY || !process.stdout.isTTY) return;
90064
89977
  await esm_default13.prompt([
@@ -90070,20 +89983,23 @@ async function pauseForGuide(autoYes) {
90070
89983
  ]);
90071
89984
  }
90072
89985
  async function promptChoices(args) {
89986
+ const flaggedSlots = parseAgentSlotsFlag(args.agentSlots);
89987
+ const shouldConfigureSlots = !args.skipInit;
90073
89988
  if (args.yes) {
90074
89989
  const telemetry2 = args.telemetry ?? "on";
90075
89990
  return {
90076
89991
  profile: args.profile ?? "default",
90077
89992
  telemetry: telemetry2,
90078
89993
  startControl: args.control ?? telemetry2 !== "off",
90079
- plugins: parsePluginsFlag(args.plugins) ?? []
89994
+ plugins: parsePluginsFlag(args.plugins) ?? [],
89995
+ agentSlotsMax: shouldConfigureSlots ? flaggedSlots : void 0
90080
89996
  };
90081
89997
  }
90082
89998
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
90083
89999
  if (!interactive) {
90084
- if (args.telemetry === void 0 && args.control === void 0 && args.plugins === void 0 && args.profile === void 0) {
90000
+ if (args.telemetry === void 0 && args.control === void 0 && args.plugins === void 0 && args.profile === void 0 && args.agentSlots === void 0) {
90085
90001
  throw new Error(
90086
- "Interactive onboarding requires a TTY. Pass --yes or flags (--profile, --telemetry, --control/--no-control, --plugins)."
90002
+ "Interactive onboarding requires a TTY. Pass --yes or flags (--profile, --telemetry, --control/--no-control, --plugins, --agent-slots)."
90087
90003
  );
90088
90004
  }
90089
90005
  const telemetry2 = args.telemetry ?? "on";
@@ -90091,12 +90007,20 @@ async function promptChoices(args) {
90091
90007
  profile: args.profile ?? "default",
90092
90008
  telemetry: telemetry2,
90093
90009
  startControl: args.control ?? telemetry2 !== "off",
90094
- plugins: parsePluginsFlag(args.plugins) ?? []
90010
+ plugins: parsePluginsFlag(args.plugins) ?? [],
90011
+ agentSlotsMax: shouldConfigureSlots ? flaggedSlots : void 0
90095
90012
  };
90096
90013
  }
90097
90014
  const pluginChoices = listPluginChoices();
90098
- const alreadyPresent = harnessExists(path59.resolve(args.repo));
90015
+ const repoPath = path59.resolve(args.repo);
90016
+ const alreadyPresent = harnessExists(repoPath);
90099
90017
  const needProfile = !alreadyPresent && !args.skipInit && args.profile === void 0;
90018
+ const needAgentSlots = shouldConfigureSlots && flaggedSlots === void 0;
90019
+ if (needAgentSlots) {
90020
+ info(
90021
+ "Each parallel slot runs an isolated copy of the stack. The practical limit depends on your machine resources (RAM/CPU) and how expensive the stack is to run (Docker services, databases, frontends, etc.). You can change this later in .har/stages.json."
90022
+ );
90023
+ }
90100
90024
  const answers = await esm_default13.prompt([
90101
90025
  {
90102
90026
  type: "list",
@@ -90138,14 +90062,37 @@ async function promptChoices(args) {
90138
90062
  name: plugin.label,
90139
90063
  value: plugin.id
90140
90064
  }))
90065
+ },
90066
+ {
90067
+ type: "number",
90068
+ name: "agentSlotsMax",
90069
+ message: `How many agents do you want to run in parallel? (${HAR_AGENT_SLOT_MIN}\u2013${HAR_AGENT_SLOT_ONBOARD_MAX})`,
90070
+ when: () => needAgentSlots,
90071
+ default: (current) => resolveDefaultAgentSlotsMax(
90072
+ repoPath,
90073
+ args.profile ?? current.profile ?? "default",
90074
+ alreadyPresent
90075
+ ),
90076
+ validate: (value) => {
90077
+ if (value === void 0 || !Number.isInteger(value) || value < HAR_AGENT_SLOT_MIN || value > HAR_AGENT_SLOT_ONBOARD_MAX) {
90078
+ return `Enter an integer from ${HAR_AGENT_SLOT_MIN} to ${HAR_AGENT_SLOT_ONBOARD_MAX}`;
90079
+ }
90080
+ return true;
90081
+ }
90141
90082
  }
90142
90083
  ]);
90143
90084
  const telemetry = args.telemetry ?? answers.telemetry ?? "on";
90085
+ const profile = args.profile ?? answers.profile ?? "default";
90086
+ let agentSlotsMax;
90087
+ if (shouldConfigureSlots) {
90088
+ agentSlotsMax = flaggedSlots ?? answers.agentSlotsMax;
90089
+ }
90144
90090
  return {
90145
- profile: args.profile ?? answers.profile ?? "default",
90091
+ profile,
90146
90092
  telemetry,
90147
90093
  startControl: args.control ?? answers.startControl ?? telemetry !== "off",
90148
- plugins: parsePluginsFlag(args.plugins) ?? answers.plugins ?? []
90094
+ plugins: parsePluginsFlag(args.plugins) ?? answers.plugins ?? [],
90095
+ agentSlotsMax
90149
90096
  };
90150
90097
  }
90151
90098
  function printSummary(result) {
@@ -90162,6 +90109,11 @@ function printSummary(result) {
90162
90109
  info(
90163
90110
  `Plugins: ${result.pluginsApplied.length > 0 ? result.pluginsApplied.join(", ") : "none"}`
90164
90111
  );
90112
+ if (result.agentSlots) {
90113
+ info(
90114
+ `Agent slots: ${result.agentSlots.min}\u2013${result.agentSlots.max} parallel (see .har/stages.json)`
90115
+ );
90116
+ }
90165
90117
  if (result.adaptationPromptPath) {
90166
90118
  info(`Adapt prompt: ${result.adaptationPromptPath}`);
90167
90119
  info(
@@ -90198,7 +90150,8 @@ async function handleOnboard(argv) {
90198
90150
  skipInit: argv.skipInit,
90199
90151
  deferAdaptationPrompt: true,
90200
90152
  autoYes: argv.yes,
90201
- forcePlugins: argv.force
90153
+ forcePlugins: argv.force,
90154
+ agentSlotsMax: choices.agentSlotsMax
90202
90155
  });
90203
90156
  if (result.harnessInitialized || harnessExists(repoPath)) {
90204
90157
  await handleCommitGateOnboarding({
@@ -90261,6 +90214,9 @@ var onboardCommand = {
90261
90214
  }).option("plugins", {
90262
90215
  type: "string",
90263
90216
  describe: "Comma-separated plugins to install (e.g. playwright); --no-plugins to skip; interactive checkbox when omitted"
90217
+ }).option("agent-slots", {
90218
+ type: "number",
90219
+ describe: `Max parallel agent slots to configure in .har/stages.json (${HAR_AGENT_SLOT_MIN}\u2013${HAR_AGENT_SLOT_ONBOARD_MAX})`
90264
90220
  }).option("skip-guide", {
90265
90221
  type: "boolean",
90266
90222
  default: false,