@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 +153 -197
- package/dist/templates/adaptation-prompt-init.md +1 -1
- package/dist/templates/agent-skills/har-wt.md +2 -2
- package/dist/templates/cursor-rule.mdc.template +2 -3
- package/dist/templates/har-boilerplate/CLAUDE.agent.md +1 -1
- package/dist/templates/har-boilerplate/README.md +4 -4
- package/dist/templates/har-boilerplate/agent-cli.sh +1 -1
- package/dist/templates/har-boilerplate/agent-slot.sh +24 -66
- package/dist/templates/har-boilerplate/launch.sh +6 -15
- package/dist/templates/har-boilerplate/preflight.sh +3 -12
- package/dist/templates/har-boilerplate-cli/CLAUDE.agent.md +1 -1
- package/dist/templates/har-boilerplate-cli/README.md +3 -3
- package/dist/templates/har-boilerplate-cli/agent-slot.sh +24 -64
- package/dist/templates/har-boilerplate-cli/launch.sh +6 -14
- package/dist/templates/har-boilerplate-cli/preflight.sh +3 -12
- package/dist/templates/har-boilerplate-ios/CLAUDE.agent.md +1 -1
- package/dist/templates/har-boilerplate-ios/README.md +3 -3
- package/dist/templates/har-boilerplate-ios/agent-slot.sh +15 -48
- package/dist/templates/har-boilerplate-ios/launch.sh +9 -11
- package/package.json +1 -1
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
"
|
|
74740
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
74771
|
-
|
|
74772
|
-
|
|
74773
|
-
|
|
74774
|
-
|
|
74775
|
-
|
|
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 } :
|
|
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
|
-
} :
|
|
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})` :
|
|
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` :
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
77756
|
-
|
|
77757
|
-
|
|
77758
|
-
|
|
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
|
-
|
|
77772
|
-
|
|
77773
|
-
|
|
77774
|
-
|
|
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
|
|
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
|
|
77786
|
-
har env launch 1
|
|
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("
|
|
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" })
|
|
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, {
|
|
78304
|
+
const guard = checkLaunchGuard2(repo, agentId, {});
|
|
78359
78305
|
if (!guard.allowed && guard.blocked) {
|
|
78360
|
-
|
|
78361
|
-
|
|
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
|
-
|
|
89254
|
-
"
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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,
|