@osfactory/har 0.36.2 → 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
@@ -7068,7 +7068,7 @@ var init_otel_hooks = __esm({
7068
7068
  path46 = __toESM(require("path"));
7069
7069
  init_control_config();
7070
7070
  init_telemetry_config();
7071
- OTEL_HOOKS_PACKAGE = "@osfactory/otel-hook@0.1.1";
7071
+ OTEL_HOOKS_PACKAGE = "@osfactory/otel-hook@0.1.2";
7072
7072
  OTEL_HOOKS_PROVIDERS = [
7073
7073
  { id: "cursor", label: "cursor" },
7074
7074
  { id: "claude-code", label: "claude" },
@@ -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);
@@ -73365,11 +73375,24 @@ function requireApiKey() {
73365
73375
  }
73366
73376
  return key;
73367
73377
  }
73378
+ function formatInvalidAgentIdError(id, range) {
73379
+ const idLabel = id === void 0 || id === null || id === "" ? "(missing)" : typeof id === "string" ? id : String(id);
73380
+ const lines2 = [
73381
+ `Invalid agent slot id: ${idLabel}`,
73382
+ `Valid slots: ${range.min}\u2013${range.max} (configured in .har/stages.json \u2192 agentSlots)`,
73383
+ "Run `har env status` to see which slots are in use."
73384
+ ];
73385
+ const num3 = Number(id);
73386
+ if (Number.isInteger(num3) && num3 > range.max) {
73387
+ lines2.push(`To allow slot ${num3}, raise agentSlots.max in .har/stages.json.`);
73388
+ }
73389
+ return lines2.join("\n");
73390
+ }
73368
73391
  function validateAgentId(id, repoPath) {
73369
73392
  const num3 = Number(id);
73370
73393
  const range = getAgentSlotRange(repoPath);
73371
73394
  if (!Number.isInteger(num3) || num3 < range.min || num3 > range.max) {
73372
- throw new Error(`agent-id must be a number between ${range.min} and ${range.max}`);
73395
+ throw new Error(formatInvalidAgentIdError(id, range));
73373
73396
  }
73374
73397
  return num3;
73375
73398
  }
@@ -73950,8 +73973,6 @@ function buildLaunchFlagArgs(flags) {
73950
73973
  const args = [];
73951
73974
  if (flags.worktree === false) args.push("--no-worktree");
73952
73975
  if (flags.claude) args.push("--claude");
73953
- if (flags.confirmReplace) args.push("--replace");
73954
- if (flags.force) args.push("--force");
73955
73976
  if (flags.resume) args.push("--resume");
73956
73977
  if (flags.workUnitId) args.push(`--work-id=${flags.workUnitId}`);
73957
73978
  if (flags.attemptId) args.push(`--attempt-id=${flags.attemptId}`);
@@ -74210,6 +74231,7 @@ function registryChanged(before, after) {
74210
74231
  function recordRepoForControlSync(repoPath) {
74211
74232
  if (process.env.HAR_CONTROL_DISABLED === "true") return;
74212
74233
  const resolved = canonicalizeControlRepoPath(repoPath);
74234
+ if (!resolveMainWorkingTree(resolved)) return;
74213
74235
  if (!readManifest(resolved)) return;
74214
74236
  const registry2 = readRegistry();
74215
74237
  if (registry2.repos.includes(resolved)) return;
@@ -74696,7 +74718,7 @@ function describeLaunchBase(repoPath) {
74696
74718
  return [
74697
74719
  `New session will be based on: ${branch}${sha ? ` @ ${sha}` : ""}`,
74698
74720
  ` (HEAD of ${harnessRoot})`,
74699
- " --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."
74700
74722
  ];
74701
74723
  }
74702
74724
  function formatOccupiedSlot(slot, repoPath, resume) {
@@ -74708,27 +74730,30 @@ function formatOccupiedSlot(slot, repoPath, resume) {
74708
74730
  slot.sessionStatus ? ` Status: ${slot.sessionStatus}` : void 0,
74709
74731
  slot.lastError ? ` Error: ${slot.lastError}` : void 0,
74710
74732
  slot.sessionCreatedAt ? ` Since: ${slot.sessionCreatedAt}` : void 0,
74711
- 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",
74712
74734
  "",
74713
74735
  ...describeLaunchBase(repoPath),
74714
74736
  ""
74715
74737
  ];
74716
74738
  if (resume || slot.sessionStatus === "failed" || slot.sessionStatus === "starting") {
74717
74739
  lines2.push(
74718
- "This session failed partway through launch. Resume without replacing:",
74740
+ "This session failed partway through launch. Resume without a new launch:",
74719
74741
  ` har env launch ${slot.agentId} --resume`,
74720
74742
  ` har env recover ${slot.agentId}`,
74721
74743
  ` ./.har/launch.sh ${slot.agentId} --resume`,
74722
74744
  ""
74723
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
+ );
74724
74753
  }
74725
74754
  lines2.push(
74726
- "Prefer complete/teardown when the previous task is finished, then launch.",
74727
- "Replacing removes the worktree. The session branch is kept only if you committed.",
74728
- "Gitignored paths (state/, runs/, local clones) are NOT preserved.",
74729
- "",
74730
- "To replace: pass confirmReplace=true (MCP), --replace (CLI), or answer y at the prompt.",
74731
- "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."
74732
74757
  );
74733
74758
  return lines2.filter(Boolean).join("\n");
74734
74759
  }
@@ -74748,42 +74773,18 @@ function checkLaunchGuard(repoPath, agentId, options = {}) {
74748
74773
  reason: [
74749
74774
  `Slot ${agentId} is not resumable (status=${session?.status ?? "none"}).`,
74750
74775
  "Only failed or starting sessions can be resumed.",
74751
- "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}.`
74752
74777
  ].join("\n")
74753
74778
  };
74754
74779
  }
74755
74780
  return { allowed: true, slot };
74756
74781
  }
74757
- if (isSlotResumable(session) && !options.confirmReplace) {
74758
- return {
74759
- allowed: false,
74760
- blocked: true,
74761
- slot,
74762
- reason: formatOccupiedSlot(slot, repoPath)
74763
- };
74764
- }
74765
- if (!options.confirmReplace) {
74766
- return {
74767
- allowed: false,
74768
- blocked: true,
74769
- slot,
74770
- reason: formatOccupiedSlot(slot, repoPath)
74771
- };
74772
- }
74773
- if (slot.dirty && !options.force) {
74774
- return {
74775
- allowed: false,
74776
- blocked: true,
74777
- slot,
74778
- reason: [
74779
- formatOccupiedSlot(slot, repoPath),
74780
- "",
74781
- "The occupied worktree has uncommitted changes.",
74782
- "Pass force=true / --force to discard them (only after explicit user approval)."
74783
- ].join("\n")
74784
- };
74785
- }
74786
- return { allowed: true, slot };
74782
+ return {
74783
+ allowed: false,
74784
+ blocked: true,
74785
+ slot,
74786
+ reason: formatOccupiedSlot(slot, repoPath)
74787
+ };
74787
74788
  }
74788
74789
 
74789
74790
  // src/core/slot-preflight.ts
@@ -74845,24 +74846,20 @@ function inspectSlotReadiness(repoPath, agentId, options = {}) {
74845
74846
  const warnings = [];
74846
74847
  const ports = {};
74847
74848
  const session = readSlotRegistry(harnessRoot, agentId);
74848
- 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 } : {
74849
74850
  allowed: false,
74850
74851
  blocked: true,
74851
74852
  reason: `Slot ${agentId} is already in use.`
74852
- } : options.occupied.dirty && !options.force ? {
74853
- allowed: false,
74854
- blocked: true,
74855
- reason: `Slot ${agentId} worktree has uncommitted changes.`
74856
- } : { allowed: true } : { allowed: true } : checkLaunchGuard(repoPath, agentId, options);
74853
+ } : { allowed: true } : checkLaunchGuard(repoPath, agentId, options);
74857
74854
  if (!guard.allowed) {
74858
74855
  const resumable = isSlotResumable(session);
74859
74856
  blockers.push({
74860
74857
  code: options.occupied?.dirty ? "slot_dirty" : resumable ? "slot_resumable" : "slot_occupied",
74861
74858
  message: guard.reason ?? `Slot ${agentId} is occupied.`,
74862
- 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}.`
74863
74860
  });
74864
74861
  remediations.push(
74865
- 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}`
74866
74863
  );
74867
74864
  }
74868
74865
  const pm2Procs = usesPm2 ? options.pm2Processes !== void 0 ? options.pm2Processes : listPm2Processes() : void 0;
@@ -74891,7 +74888,7 @@ function inspectSlotReadiness(repoPath, agentId, options = {}) {
74891
74888
  blockers.push({
74892
74889
  code: "project_mismatch",
74893
74890
  message: `Slot registry projectName=${session.projectName} does not match harness ${projectName}.`,
74894
- remediation: "Teardown the stale session and relaunch with --replace."
74891
+ remediation: "Teardown the stale session, then relaunch."
74895
74892
  });
74896
74893
  }
74897
74894
  }
@@ -76384,23 +76381,11 @@ async function waitForControlApi(apiUrl = getControlApiUrl(), timeoutMs = 6e4, i
76384
76381
  return false;
76385
76382
  }
76386
76383
  async function discoverHarRepos(options) {
76387
- const apiUrl = options?.apiUrl ?? getControlApiUrl();
76388
76384
  const repoPaths = new Set(listRegisteredRepos());
76389
76385
  if (options?.cwd) {
76390
76386
  const cwd = canonicalizeControlRepoPath(options.cwd);
76391
76387
  if (readManifest(cwd)) repoPaths.add(cwd);
76392
76388
  }
76393
- try {
76394
- const listResponse = await fetch(`${apiUrl}/api/repos`);
76395
- if (listResponse.ok) {
76396
- const repos = await listResponse.json();
76397
- for (const repo of repos) {
76398
- const resolved = canonicalizeControlRepoPath(repo.path);
76399
- if (readManifest(resolved)) repoPaths.add(resolved);
76400
- }
76401
- }
76402
- } catch {
76403
- }
76404
76389
  return [...repoPaths];
76405
76390
  }
76406
76391
  async function syncReposWithControl(options) {
@@ -76641,6 +76626,7 @@ async function syncRepoWithControlAsync(repoPath) {
76641
76626
  if (process.env.HAR_CONTROL_DISABLED === "true" || process.env.NODE_ENV === "test") return;
76642
76627
  const verbose = process.env.HAR_CONTROL_VERBOSE === "true";
76643
76628
  try {
76629
+ recordRepoForControlSync(repoPath);
76644
76630
  const apiUrl = getControlApiUrl();
76645
76631
  if (!await isControlApiReachable(apiUrl)) {
76646
76632
  if (verbose) {
@@ -77000,10 +76986,7 @@ var RunService = class {
77000
76986
  return this.executor.listArtifacts({ repoPath: options.repoPath }, { stageId: options.stageId });
77001
76987
  }
77002
76988
  async preflightEnvironment(options) {
77003
- const readiness = inspectSlotReadiness(options.repoPath, options.agentId, {
77004
- confirmReplace: options.confirmReplace,
77005
- force: options.force
77006
- });
76989
+ const readiness = inspectSlotReadiness(options.repoPath, options.agentId, {});
77007
76990
  const report = formatPreflightReport(options.agentId, readiness);
77008
76991
  return {
77009
76992
  code: readiness.canLaunch ? 0 : 1,
@@ -77015,8 +76998,6 @@ var RunService = class {
77015
76998
  }
77016
76999
  async launchEnvironment(options) {
77017
77000
  const guard = checkLaunchGuard2(options.repoPath, options.agentId, {
77018
- confirmReplace: options.confirmReplace,
77019
- force: options.force,
77020
77001
  resume: options.resume
77021
77002
  });
77022
77003
  if (!guard.allowed) {
@@ -77066,7 +77047,7 @@ Usage from Cursor / Claude / Codex (@osfactory/otel-hook) will appear under Work
77066
77047
  const attemptId = workUnitId ? resumableSession?.attemptId ?? crypto5.randomUUID() : void 0;
77067
77048
  if (workUnitId && attemptId) {
77068
77049
  const conflictingSession = listSlotRegistryEntries(harnessRoot).find(
77069
- (entry) => entry.workUnitId === workUnitId && !(entry.agentId === options.agentId && (options.resume || options.confirmReplace))
77050
+ (entry) => entry.workUnitId === workUnitId && !(entry.agentId === options.agentId && options.resume)
77070
77051
  );
77071
77052
  if (conflictingSession) {
77072
77053
  return {
@@ -77099,8 +77080,6 @@ Usage from Cursor / Claude / Codex (@osfactory/otel-hook) will appear under Work
77099
77080
  launchFlags: {
77100
77081
  worktree: options.worktree,
77101
77082
  claude: options.claude,
77102
- confirmReplace: options.confirmReplace,
77103
- force: options.force,
77104
77083
  resume: options.resume,
77105
77084
  workUnitId,
77106
77085
  attemptId
@@ -77739,10 +77718,11 @@ Slot lifecycle (do not confuse these):
77739
77718
  complete finish successfully: full verify, then teardown (frees the slot)
77740
77719
  teardown abandon / free the slot without a completion validation
77741
77720
 
77742
- --replace only confirms destroying a previous session on that slot id
77743
- it does NOT choose main \u2014 new session still uses current HEAD
77744
- --force additionally discard dirty uncommitted work (user approval only)
77745
- --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
77746
77726
 
77747
77727
  For a new unrelated task: checkout main (or your intended base) on the main
77748
77728
  repo checkout, free the slot (complete/teardown), then launch.`;
@@ -77755,23 +77735,23 @@ var HAR_ENV_EPILOG = `Environment lifecycle:
77755
77735
  teardown <id> free the slot without recording a completion validation
77756
77736
 
77757
77737
  Occupied slots:
77758
- Prefer complete/teardown when the previous task is finished, then launch.
77759
- Use --replace only to reuse the same slot id immediately (destroys old worktree).
77760
- --replace does not select the base branch \u2014 HEAD of --repo does.
77761
- 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>).`;
77762
77745
  var LAUNCH_COMMAND_DESCRIBE = "Start a fresh agent session (new worktree from the main checkout HEAD)";
77763
- var LAUNCH_REPLACE_DESCRIBE = "Destroy the previous session on this slot and start another (does not choose main; still uses --repo HEAD)";
77764
- var LAUNCH_FORCE_DESCRIBE = "With --replace: discard uncommitted changes in the old worktree (only after explicit user approval)";
77765
- 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";
77766
77747
  var LAUNCH_EPILOG = `Every launch creates a NEW session from the current HEAD of --repo (the main
77767
77748
  checkout). The worktree path encodes that base branch \u2014 switch --repo to main
77768
77749
  before launch for a new unrelated task unless you intentionally stack on a
77769
77750
  feature branch.
77770
77751
 
77771
77752
  Free slot: har env launch 1
77772
- Occupied: har env complete 1 # or teardown \u2014 then launch
77773
- har env launch 1 --replace # reuse slot id now
77774
- 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
77775
77755
  Failed launch: har env recover 1 # or: har env launch 1 --resume`;
77776
77756
 
77777
77757
  // src/cli/commands/env.ts
@@ -77923,15 +77903,7 @@ var envCommand = {
77923
77903
  type: "boolean",
77924
77904
  default: true,
77925
77905
  describe: "Use an isolated git worktree (default)"
77926
- }).option("claude", { type: "boolean", default: false }).option("replace", {
77927
- type: "boolean",
77928
- default: false,
77929
- describe: LAUNCH_REPLACE_DESCRIBE
77930
- }).option("force", {
77931
- type: "boolean",
77932
- default: false,
77933
- describe: LAUNCH_FORCE_DESCRIBE
77934
- }).option("resume", {
77906
+ }).option("claude", { type: "boolean", default: false }).option("resume", {
77935
77907
  type: "boolean",
77936
77908
  default: false,
77937
77909
  describe: LAUNCH_RESUME_DESCRIBE
@@ -77954,15 +77926,7 @@ var envCommand = {
77954
77926
  ).command(
77955
77927
  "preflight <id>",
77956
77928
  "Check whether a slot can launch now (ports, PM2, Docker, occupied slot)",
77957
- (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", {
77958
- type: "boolean",
77959
- default: false,
77960
- describe: "Treat an occupied slot as replaceable (same as launch --replace)"
77961
- }).option("force", {
77962
- type: "boolean",
77963
- default: false,
77964
- describe: "Allow replacing a dirty worktree (only after explicit approval)"
77965
- }),
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" }),
77966
77930
  handlePreflight
77967
77931
  ).command(
77968
77932
  "verify <id>",
@@ -78058,7 +78022,6 @@ async function handleInit(argv) {
78058
78022
  }
78059
78023
  divider();
78060
78024
  success("Harness initialized!");
78061
- recordRepoForControlSync(repoPath);
78062
78025
  await handleCommitGateOnboarding({
78063
78026
  repoPath,
78064
78027
  ...onboarding.commitGate,
@@ -78323,9 +78286,7 @@ async function handlePreflight(argv) {
78323
78286
  const agentId = validateAgentId(argv.id, repo);
78324
78287
  const result = await preflightEnvironment({
78325
78288
  repoPath: repo,
78326
- agentId,
78327
- confirmReplace: argv.replace,
78328
- force: argv.force
78289
+ agentId
78329
78290
  });
78330
78291
  if (argv.json) {
78331
78292
  const output = SlotReadinessSchema.parse(result.readiness);
@@ -78339,46 +78300,11 @@ async function handlePreflight(argv) {
78339
78300
  async function handleLaunch(argv) {
78340
78301
  const repo = path50.resolve(argv.repo);
78341
78302
  const agentId = validateAgentId(argv.id, repo);
78342
- let confirmReplace = argv.replace;
78343
- let force = argv.force;
78344
78303
  if (!argv.resume) {
78345
- const guard = checkLaunchGuard2(repo, agentId, { confirmReplace, force });
78304
+ const guard = checkLaunchGuard2(repo, agentId, {});
78346
78305
  if (!guard.allowed && guard.blocked) {
78347
- if (!confirmReplace && process.stdin.isTTY && process.stdout.isTTY) {
78348
- warn("Occupied slot \u2014 review before replacing:");
78349
- console.error(guard.reason ?? "");
78350
- const readline10 = await import("readline");
78351
- const rl = readline10.createInterface({ input: process.stdin, output: process.stderr });
78352
- const answer = await new Promise((resolve45) => {
78353
- rl.question("Replace this slot? [y/N] ", resolve45);
78354
- });
78355
- rl.close();
78356
- if (!/^[Yy]$/.test(answer.trim())) {
78357
- error("Aborted \u2014 slot left unchanged.");
78358
- process.exit(2);
78359
- }
78360
- confirmReplace = true;
78361
- }
78362
- }
78363
- const guardAfterConfirm = checkLaunchGuard2(repo, agentId, { confirmReplace, force });
78364
- if (!guardAfterConfirm.allowed && guardAfterConfirm.blocked && guardAfterConfirm.slot?.dirty) {
78365
- if (!force && process.stdin.isTTY && process.stdout.isTTY) {
78366
- warn("Worktree has uncommitted changes.");
78367
- const readline10 = await import("readline");
78368
- const rl = readline10.createInterface({ input: process.stdin, output: process.stderr });
78369
- const answer = await new Promise((resolve45) => {
78370
- rl.question("Discard uncommitted changes? [y/N] ", resolve45);
78371
- });
78372
- rl.close();
78373
- if (!/^[Yy]$/.test(answer.trim())) {
78374
- error("Aborted \u2014 uncommitted work preserved.");
78375
- process.exit(2);
78376
- }
78377
- force = true;
78378
- } else if (!force) {
78379
- error(guardAfterConfirm.reason ?? "Dirty worktree requires --force.");
78380
- process.exit(2);
78381
- }
78306
+ error(guard.reason ?? `Slot ${agentId} is occupied.`);
78307
+ process.exit(2);
78382
78308
  }
78383
78309
  }
78384
78310
  const result = await launchEnvironment({
@@ -78386,8 +78312,6 @@ async function handleLaunch(argv) {
78386
78312
  agentId,
78387
78313
  worktree: argv.worktree,
78388
78314
  claude: argv.claude,
78389
- confirmReplace,
78390
- force,
78391
78315
  resume: argv.resume,
78392
78316
  workUnitId: argv.workId,
78393
78317
  source: argv.workSource,
@@ -78420,8 +78344,6 @@ async function handleRecover(argv) {
78420
78344
  repo: argv.repo,
78421
78345
  worktree: true,
78422
78346
  claude: false,
78423
- replace: false,
78424
- force: false,
78425
78347
  resume: true,
78426
78348
  workId: void 0,
78427
78349
  workSource: void 0,
@@ -89237,13 +89159,9 @@ var LaunchEnvironmentInputSchema = external_exports.object({
89237
89159
  agentId: agentIdSchema,
89238
89160
  worktree: external_exports.boolean().default(true),
89239
89161
  claude: external_exports.boolean().default(false),
89240
- confirmReplace: external_exports.boolean().default(false).describe(
89241
- "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."
89242
- ),
89243
- force: external_exports.boolean().default(false).describe(
89244
- "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."
89245
89164
  ),
89246
- resume: external_exports.boolean().default(false).describe("Resume a failed or partial launch without creating a new worktree."),
89247
89165
  workUnitId: external_exports.string().min(1).max(128).optional(),
89248
89166
  source: external_exports.string().min(1).max(64).optional(),
89249
89167
  sourceUrl: external_exports.string().url().optional(),
@@ -89269,9 +89187,7 @@ var LaunchEnvironmentOutputSchema = ShellRunOutputSchema.extend({
89269
89187
  });
89270
89188
  var PreflightEnvironmentInputSchema = external_exports.object({
89271
89189
  repo: external_exports.string().default("."),
89272
- agentId: agentIdSchema,
89273
- confirmReplace: external_exports.boolean().default(false).describe("Treat an occupied slot as replaceable (same as launch confirmReplace)."),
89274
- force: external_exports.boolean().default(false).describe("Allow replacing a dirty worktree (requires explicit user approval).")
89190
+ agentId: agentIdSchema
89275
89191
  });
89276
89192
  var PreflightEnvironmentOutputSchema = ShellRunOutputSchema.extend({
89277
89193
  readiness: SlotReadinessSchema,
@@ -89371,24 +89287,16 @@ var HAR_MCP_TOOLS = [
89371
89287
  },
89372
89288
  {
89373
89289
  name: "har_launch_environment",
89374
- 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).",
89375
89291
  inputSchema: objectJsonSchema(
89376
89292
  {
89377
89293
  repo: repoJsonProperty,
89378
89294
  agentId: agentIdJsonProperty,
89379
89295
  worktree: { type: "boolean" },
89380
89296
  claude: { type: "boolean" },
89381
- confirmReplace: {
89382
- type: "boolean",
89383
- 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."
89384
- },
89385
- force: {
89386
- type: "boolean",
89387
- description: "Discard uncommitted changes when replacing a dirty worktree. Requires confirmReplace=true and explicit user approval."
89388
- },
89389
89297
  resume: {
89390
89298
  type: "boolean",
89391
- 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."
89392
89300
  },
89393
89301
  workUnitId: {
89394
89302
  type: "string",
@@ -89422,15 +89330,7 @@ var HAR_MCP_TOOLS = [
89422
89330
  inputSchema: objectJsonSchema(
89423
89331
  {
89424
89332
  repo: repoJsonProperty,
89425
- agentId: agentIdJsonProperty,
89426
- confirmReplace: {
89427
- type: "boolean",
89428
- description: "Treat an occupied slot as replaceable (same as launch confirmReplace)."
89429
- },
89430
- force: {
89431
- type: "boolean",
89432
- description: "Allow replacing a dirty worktree (only after explicit user approval)."
89433
- }
89333
+ agentId: agentIdJsonProperty
89434
89334
  },
89435
89335
  ["agentId"]
89436
89336
  )
@@ -89564,7 +89464,6 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
89564
89464
  smoke: input.smoke,
89565
89465
  profile: input.profile
89566
89466
  });
89567
- recordRepoForControlSync(repo);
89568
89467
  return jsonContent({
89569
89468
  harnessDir: result.harnessDir,
89570
89469
  validation: result.validation,
@@ -89580,8 +89479,6 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
89580
89479
  agentId,
89581
89480
  worktree: input.worktree,
89582
89481
  claude: input.claude,
89583
- confirmReplace: input.confirmReplace,
89584
- force: input.force,
89585
89482
  resume: input.resume,
89586
89483
  workUnitId: input.workUnitId,
89587
89484
  source: input.source,
@@ -89604,8 +89501,6 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
89604
89501
  agentId,
89605
89502
  worktree: input.worktree,
89606
89503
  claude: input.claude,
89607
- confirmReplace: false,
89608
- force: false,
89609
89504
  resume: true,
89610
89505
  workUnitId: input.workUnitId,
89611
89506
  source: input.source,
@@ -89625,9 +89520,7 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
89625
89520
  const agentId = validateAgentId(input.agentId, repo);
89626
89521
  const result = await preflightEnvironment({
89627
89522
  repoPath: repo,
89628
- agentId,
89629
- confirmReplace: input.confirmReplace,
89630
- force: input.force
89523
+ agentId
89631
89524
  });
89632
89525
  const parsed = PreflightEnvironmentOutputSchema.parse(result);
89633
89526
  return {
@@ -89964,9 +89857,20 @@ async function runOnboarding(options, deps = {}) {
89964
89857
  warn("Harness has validation errors \u2014 review .har/ after adaptation.");
89965
89858
  }
89966
89859
  harnessInitialized = true;
89967
- recordRepoForControlSync(repoPath);
89968
89860
  success("Harness scaffolded");
89969
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
+ }
89970
89874
  const pluginsApplied = [];
89971
89875
  const pluginWarnings = [];
89972
89876
  if (options.plugins.length > 0) {
@@ -90015,7 +89919,8 @@ async function runOnboarding(options, deps = {}) {
90015
89919
  pluginsApplied,
90016
89920
  pluginWarnings,
90017
89921
  adaptationPromptPath,
90018
- adaptationPromptCopied
89922
+ adaptationPromptCopied,
89923
+ agentSlots
90019
89924
  };
90020
89925
  }
90021
89926
  async function finalizeOnboardingAdaptation(options) {
@@ -90046,6 +89951,27 @@ function parsePluginsFlag(raw) {
90046
89951
  }
90047
89952
  return selected;
90048
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
+ }
90049
89975
  async function pauseForGuide(autoYes) {
90050
89976
  if (autoYes || !process.stdin.isTTY || !process.stdout.isTTY) return;
90051
89977
  await esm_default13.prompt([
@@ -90057,20 +89983,23 @@ async function pauseForGuide(autoYes) {
90057
89983
  ]);
90058
89984
  }
90059
89985
  async function promptChoices(args) {
89986
+ const flaggedSlots = parseAgentSlotsFlag(args.agentSlots);
89987
+ const shouldConfigureSlots = !args.skipInit;
90060
89988
  if (args.yes) {
90061
89989
  const telemetry2 = args.telemetry ?? "on";
90062
89990
  return {
90063
89991
  profile: args.profile ?? "default",
90064
89992
  telemetry: telemetry2,
90065
89993
  startControl: args.control ?? telemetry2 !== "off",
90066
- plugins: parsePluginsFlag(args.plugins) ?? []
89994
+ plugins: parsePluginsFlag(args.plugins) ?? [],
89995
+ agentSlotsMax: shouldConfigureSlots ? flaggedSlots : void 0
90067
89996
  };
90068
89997
  }
90069
89998
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
90070
89999
  if (!interactive) {
90071
- 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) {
90072
90001
  throw new Error(
90073
- "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)."
90074
90003
  );
90075
90004
  }
90076
90005
  const telemetry2 = args.telemetry ?? "on";
@@ -90078,12 +90007,20 @@ async function promptChoices(args) {
90078
90007
  profile: args.profile ?? "default",
90079
90008
  telemetry: telemetry2,
90080
90009
  startControl: args.control ?? telemetry2 !== "off",
90081
- plugins: parsePluginsFlag(args.plugins) ?? []
90010
+ plugins: parsePluginsFlag(args.plugins) ?? [],
90011
+ agentSlotsMax: shouldConfigureSlots ? flaggedSlots : void 0
90082
90012
  };
90083
90013
  }
90084
90014
  const pluginChoices = listPluginChoices();
90085
- const alreadyPresent = harnessExists(path59.resolve(args.repo));
90015
+ const repoPath = path59.resolve(args.repo);
90016
+ const alreadyPresent = harnessExists(repoPath);
90086
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
+ }
90087
90024
  const answers = await esm_default13.prompt([
90088
90025
  {
90089
90026
  type: "list",
@@ -90125,14 +90062,37 @@ async function promptChoices(args) {
90125
90062
  name: plugin.label,
90126
90063
  value: plugin.id
90127
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
+ }
90128
90082
  }
90129
90083
  ]);
90130
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
+ }
90131
90090
  return {
90132
- profile: args.profile ?? answers.profile ?? "default",
90091
+ profile,
90133
90092
  telemetry,
90134
90093
  startControl: args.control ?? answers.startControl ?? telemetry !== "off",
90135
- plugins: parsePluginsFlag(args.plugins) ?? answers.plugins ?? []
90094
+ plugins: parsePluginsFlag(args.plugins) ?? answers.plugins ?? [],
90095
+ agentSlotsMax
90136
90096
  };
90137
90097
  }
90138
90098
  function printSummary(result) {
@@ -90149,6 +90109,11 @@ function printSummary(result) {
90149
90109
  info(
90150
90110
  `Plugins: ${result.pluginsApplied.length > 0 ? result.pluginsApplied.join(", ") : "none"}`
90151
90111
  );
90112
+ if (result.agentSlots) {
90113
+ info(
90114
+ `Agent slots: ${result.agentSlots.min}\u2013${result.agentSlots.max} parallel (see .har/stages.json)`
90115
+ );
90116
+ }
90152
90117
  if (result.adaptationPromptPath) {
90153
90118
  info(`Adapt prompt: ${result.adaptationPromptPath}`);
90154
90119
  info(
@@ -90185,7 +90150,8 @@ async function handleOnboard(argv) {
90185
90150
  skipInit: argv.skipInit,
90186
90151
  deferAdaptationPrompt: true,
90187
90152
  autoYes: argv.yes,
90188
- forcePlugins: argv.force
90153
+ forcePlugins: argv.force,
90154
+ agentSlotsMax: choices.agentSlotsMax
90189
90155
  });
90190
90156
  if (result.harnessInitialized || harnessExists(repoPath)) {
90191
90157
  await handleCommitGateOnboarding({
@@ -90248,6 +90214,9 @@ var onboardCommand = {
90248
90214
  }).option("plugins", {
90249
90215
  type: "string",
90250
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})`
90251
90220
  }).option("skip-guide", {
90252
90221
  type: "boolean",
90253
90222
  default: false,