@osfactory/har 0.5.0 → 0.7.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.
Files changed (36) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +1817 -651
  3. package/dist/prompts/system-authoring.md +27 -2
  4. package/dist/templates/adaptation-prompt-init.md +60 -5
  5. package/dist/templates/adaptation-prompt-maintain.md +7 -4
  6. package/dist/templates/har-boilerplate/CLAUDE.agent.md +5 -3
  7. package/dist/templates/har-boilerplate/README.md +51 -9
  8. package/dist/templates/har-boilerplate/agent-cli.sh +72 -26
  9. package/dist/templates/har-boilerplate/agent-slot.sh +351 -0
  10. package/dist/templates/har-boilerplate/attach.sh +7 -1
  11. package/dist/templates/har-boilerplate/ecosystem.agent.template.cjs +3 -1
  12. package/dist/templates/har-boilerplate/env.template +4 -1
  13. package/dist/templates/har-boilerplate/harness.env +32 -0
  14. package/dist/templates/har-boilerplate/launch.sh +167 -120
  15. package/dist/templates/har-boilerplate/preflight.sh +41 -0
  16. package/dist/templates/har-boilerplate/provision-toolchain.sh +285 -0
  17. package/dist/templates/har-boilerplate/setup-infra.sh +87 -11
  18. package/dist/templates/har-boilerplate/teardown.sh +2 -1
  19. package/dist/templates/har-boilerplate/verify.sh +91 -11
  20. package/dist/templates/har-boilerplate-cli/CLAUDE.agent.md +5 -2
  21. package/dist/templates/har-boilerplate-cli/README.md +40 -6
  22. package/dist/templates/har-boilerplate-cli/agent-cli.sh +1 -1
  23. package/dist/templates/har-boilerplate-cli/agent-slot.sh +331 -0
  24. package/dist/templates/har-boilerplate-cli/harness.env +13 -0
  25. package/dist/templates/har-boilerplate-cli/launch.sh +140 -95
  26. package/dist/templates/har-boilerplate-cli/preflight.sh +37 -0
  27. package/dist/templates/har-boilerplate-cli/provision-toolchain.sh +285 -0
  28. package/dist/templates/har-boilerplate-cli/setup-infra.sh +87 -11
  29. package/dist/templates/har-boilerplate-cli/verify.sh +93 -12
  30. package/dist/templates/har-boilerplate-ios/README.md +29 -2
  31. package/dist/templates/har-boilerplate-ios/harness.env +6 -1
  32. package/dist/templates/har-boilerplate-ios/launch.sh +8 -13
  33. package/dist/templates/har-boilerplate-ios/provision-toolchain.sh +285 -0
  34. package/dist/templates/har-boilerplate-ios/verify.sh +20 -20
  35. package/package.json +1 -1
  36. package/dist/templates/har-boilerplate-ios/ADAPT-PROMPT.md +0 -57
package/dist/index.js CHANGED
@@ -765,14 +765,14 @@ var require_url_state_machine = __commonJS({
765
765
  return url.replace(/\u0009|\u000A|\u000D/g, "");
766
766
  }
767
767
  function shortenPath(url) {
768
- const path32 = url.path;
769
- if (path32.length === 0) {
768
+ const path37 = url.path;
769
+ if (path37.length === 0) {
770
770
  return;
771
771
  }
772
- if (url.scheme === "file" && path32.length === 1 && isNormalizedWindowsDriveLetter(path32[0])) {
772
+ if (url.scheme === "file" && path37.length === 1 && isNormalizedWindowsDriveLetter(path37[0])) {
773
773
  return;
774
774
  }
775
- path32.pop();
775
+ path37.pop();
776
776
  }
777
777
  function includesCredentials(url) {
778
778
  return url.username !== "" || url.password !== "";
@@ -2092,7 +2092,7 @@ var require_lib2 = __commonJS({
2092
2092
  let accum = [];
2093
2093
  let accumBytes = 0;
2094
2094
  let abort = false;
2095
- return new Body.Promise(function(resolve24, reject) {
2095
+ return new Body.Promise(function(resolve26, reject) {
2096
2096
  let resTimeout;
2097
2097
  if (_this4.timeout) {
2098
2098
  resTimeout = setTimeout(function() {
@@ -2126,7 +2126,7 @@ var require_lib2 = __commonJS({
2126
2126
  }
2127
2127
  clearTimeout(resTimeout);
2128
2128
  try {
2129
- resolve24(Buffer.concat(accum, accumBytes));
2129
+ resolve26(Buffer.concat(accum, accumBytes));
2130
2130
  } catch (err) {
2131
2131
  reject(new FetchError(`Could not create Buffer from response body for ${_this4.url}: ${err.message}`, "system", err));
2132
2132
  }
@@ -2801,7 +2801,7 @@ var require_lib2 = __commonJS({
2801
2801
  throw new Error("native promise missing, set fetch.Promise to your favorite alternative");
2802
2802
  }
2803
2803
  Body.Promise = fetch3.Promise;
2804
- return new fetch3.Promise(function(resolve24, reject) {
2804
+ return new fetch3.Promise(function(resolve26, reject) {
2805
2805
  const request = new Request3(url, opts);
2806
2806
  const options = getNodeRequestOptions(request);
2807
2807
  const send = (options.protocol === "https:" ? https : http).request;
@@ -2934,7 +2934,7 @@ var require_lib2 = __commonJS({
2934
2934
  requestOpts.body = void 0;
2935
2935
  requestOpts.headers.delete("content-length");
2936
2936
  }
2937
- resolve24(fetch3(new Request3(locationURL, requestOpts)));
2937
+ resolve26(fetch3(new Request3(locationURL, requestOpts)));
2938
2938
  finalize();
2939
2939
  return;
2940
2940
  }
@@ -2955,7 +2955,7 @@ var require_lib2 = __commonJS({
2955
2955
  const codings = headers.get("Content-Encoding");
2956
2956
  if (!request.compress || request.method === "HEAD" || codings === null || res.statusCode === 204 || res.statusCode === 304) {
2957
2957
  response = new Response3(body, response_options);
2958
- resolve24(response);
2958
+ resolve26(response);
2959
2959
  return;
2960
2960
  }
2961
2961
  const zlibOptions = {
@@ -2965,7 +2965,7 @@ var require_lib2 = __commonJS({
2965
2965
  if (codings == "gzip" || codings == "x-gzip") {
2966
2966
  body = body.pipe(zlib.createGunzip(zlibOptions));
2967
2967
  response = new Response3(body, response_options);
2968
- resolve24(response);
2968
+ resolve26(response);
2969
2969
  return;
2970
2970
  }
2971
2971
  if (codings == "deflate" || codings == "x-deflate") {
@@ -2977,12 +2977,12 @@ var require_lib2 = __commonJS({
2977
2977
  body = body.pipe(zlib.createInflateRaw());
2978
2978
  }
2979
2979
  response = new Response3(body, response_options);
2980
- resolve24(response);
2980
+ resolve26(response);
2981
2981
  });
2982
2982
  raw.on("end", function() {
2983
2983
  if (!response) {
2984
2984
  response = new Response3(body, response_options);
2985
- resolve24(response);
2985
+ resolve26(response);
2986
2986
  }
2987
2987
  });
2988
2988
  return;
@@ -2990,11 +2990,11 @@ var require_lib2 = __commonJS({
2990
2990
  if (codings == "br" && typeof zlib.createBrotliDecompress === "function") {
2991
2991
  body = body.pipe(zlib.createBrotliDecompress());
2992
2992
  response = new Response3(body, response_options);
2993
- resolve24(response);
2993
+ resolve26(response);
2994
2994
  return;
2995
2995
  }
2996
2996
  response = new Response3(body, response_options);
2997
- resolve24(response);
2997
+ resolve26(response);
2998
2998
  });
2999
2999
  writeToStream(req, request);
3000
3000
  });
@@ -6351,14 +6351,14 @@ __export(fileFromPath_exports, {
6351
6351
  fileFromPathSync: () => fileFromPathSync,
6352
6352
  isFile: () => isFile
6353
6353
  });
6354
- function createFileFromPath(path32, { mtimeMs, size }, filenameOrOptions, options = {}) {
6354
+ function createFileFromPath(path37, { mtimeMs, size }, filenameOrOptions, options = {}) {
6355
6355
  let filename;
6356
6356
  if (isPlainObject_default2(filenameOrOptions)) {
6357
6357
  [options, filename] = [filenameOrOptions, void 0];
6358
6358
  } else {
6359
6359
  filename = filenameOrOptions;
6360
6360
  }
6361
- const file = new FileFromPath({ path: path32, size, lastModified: mtimeMs });
6361
+ const file = new FileFromPath({ path: path37, size, lastModified: mtimeMs });
6362
6362
  if (!filename) {
6363
6363
  filename = file.name;
6364
6364
  }
@@ -6367,13 +6367,13 @@ function createFileFromPath(path32, { mtimeMs, size }, filenameOrOptions, option
6367
6367
  lastModified: file.lastModified
6368
6368
  });
6369
6369
  }
6370
- function fileFromPathSync(path32, filenameOrOptions, options = {}) {
6371
- const stats = (0, import_fs5.statSync)(path32);
6372
- return createFileFromPath(path32, stats, filenameOrOptions, options);
6370
+ function fileFromPathSync(path37, filenameOrOptions, options = {}) {
6371
+ const stats = (0, import_fs5.statSync)(path37);
6372
+ return createFileFromPath(path37, stats, filenameOrOptions, options);
6373
6373
  }
6374
- async function fileFromPath2(path32, filenameOrOptions, options) {
6375
- const stats = await import_fs5.promises.stat(path32);
6376
- return createFileFromPath(path32, stats, filenameOrOptions, options);
6374
+ async function fileFromPath2(path37, filenameOrOptions, options) {
6375
+ const stats = await import_fs5.promises.stat(path37);
6376
+ return createFileFromPath(path37, stats, filenameOrOptions, options);
6377
6377
  }
6378
6378
  var import_fs5, import_path5, import_node_domexception, __classPrivateFieldSet5, __classPrivateFieldGet6, _FileFromPath_path, _FileFromPath_start, MESSAGE, FileFromPath;
6379
6379
  var init_fileFromPath = __esm({
@@ -9384,7 +9384,7 @@ var require_compile = __commonJS({
9384
9384
  const schOrFunc = root.refs[ref];
9385
9385
  if (schOrFunc)
9386
9386
  return schOrFunc;
9387
- let _sch = resolve24.call(this, root, ref);
9387
+ let _sch = resolve26.call(this, root, ref);
9388
9388
  if (_sch === void 0) {
9389
9389
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
9390
9390
  const { schemaId } = this.opts;
@@ -9411,7 +9411,7 @@ var require_compile = __commonJS({
9411
9411
  function sameSchemaEnv(s1, s2) {
9412
9412
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
9413
9413
  }
9414
- function resolve24(root, ref) {
9414
+ function resolve26(root, ref) {
9415
9415
  let sch;
9416
9416
  while (typeof (sch = this.refs[ref]) == "string")
9417
9417
  ref = sch;
@@ -9629,8 +9629,8 @@ var require_utils2 = __commonJS({
9629
9629
  }
9630
9630
  return ind;
9631
9631
  }
9632
- function removeDotSegments(path32) {
9633
- let input = path32;
9632
+ function removeDotSegments(path37) {
9633
+ let input = path37;
9634
9634
  const output = [];
9635
9635
  let nextSlash = -1;
9636
9636
  let len = 0;
@@ -9882,8 +9882,8 @@ var require_schemes = __commonJS({
9882
9882
  wsComponent.secure = void 0;
9883
9883
  }
9884
9884
  if (wsComponent.resourceName) {
9885
- const [path32, query] = wsComponent.resourceName.split("?");
9886
- wsComponent.path = path32 && path32 !== "/" ? path32 : void 0;
9885
+ const [path37, query] = wsComponent.resourceName.split("?");
9886
+ wsComponent.path = path37 && path37 !== "/" ? path37 : void 0;
9887
9887
  wsComponent.query = query;
9888
9888
  wsComponent.resourceName = void 0;
9889
9889
  }
@@ -10042,7 +10042,7 @@ var require_fast_uri = __commonJS({
10042
10042
  }
10043
10043
  return uri;
10044
10044
  }
10045
- function resolve24(baseURI, relativeURI, options) {
10045
+ function resolve26(baseURI, relativeURI, options) {
10046
10046
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
10047
10047
  const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
10048
10048
  schemelessOptions.skipEscape = true;
@@ -10300,7 +10300,7 @@ var require_fast_uri = __commonJS({
10300
10300
  var fastUri = {
10301
10301
  SCHEMES,
10302
10302
  normalize: normalize3,
10303
- resolve: resolve24,
10303
+ resolve: resolve26,
10304
10304
  resolveComponent,
10305
10305
  equal,
10306
10306
  serialize,
@@ -13276,12 +13276,12 @@ var require_dist = __commonJS({
13276
13276
  throw new Error(`Unknown format "${name}"`);
13277
13277
  return f2;
13278
13278
  };
13279
- function addFormats(ajv, list, fs28, exportName) {
13279
+ function addFormats(ajv, list, fs32, exportName) {
13280
13280
  var _a3;
13281
13281
  var _b2;
13282
13282
  (_a3 = (_b2 = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b2.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
13283
13283
  for (const f2 of list)
13284
- ajv.addFormat(f2, fs28[f2]);
13284
+ ajv.addFormat(f2, fs32[f2]);
13285
13285
  }
13286
13286
  module2.exports = exports2 = formatsPlugin;
13287
13287
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -14565,11 +14565,11 @@ var parser = new YargsParser({
14565
14565
  resolve: import_path2.resolve,
14566
14566
  // TODO: figure out a way to combine ESM and CJS coverage, such that
14567
14567
  // we can exercise all the lines below:
14568
- require: (path32) => {
14568
+ require: (path37) => {
14569
14569
  if (typeof require !== "undefined") {
14570
- return require(path32);
14571
- } else if (path32.match(/\.json$/)) {
14572
- return JSON.parse((0, import_fs2.readFileSync)(path32, "utf8"));
14570
+ return require(path37);
14571
+ } else if (path37.match(/\.json$/)) {
14572
+ return JSON.parse((0, import_fs2.readFileSync)(path37, "utf8"));
14573
14573
  } else {
14574
14574
  throw Error("only .json config files are supported in ESM");
14575
14575
  }
@@ -17095,12 +17095,12 @@ var YargsInstance = class {
17095
17095
  async getCompletion(args, done) {
17096
17096
  argsert("<array> [function]", [args, done], arguments.length);
17097
17097
  if (!done) {
17098
- return new Promise((resolve24, reject) => {
17098
+ return new Promise((resolve26, reject) => {
17099
17099
  __classPrivateFieldGet(this, _YargsInstance_completion, "f").getCompletion(args, (err, completions) => {
17100
17100
  if (err)
17101
17101
  reject(err);
17102
17102
  else
17103
- resolve24(completions);
17103
+ resolve26(completions);
17104
17104
  });
17105
17105
  });
17106
17106
  } else {
@@ -18139,26 +18139,45 @@ function isYargsInstance(y2) {
18139
18139
  var Yargs = YargsFactory(esm_default);
18140
18140
  var yargs_default = Yargs;
18141
18141
 
18142
+ // src/core/package-version.ts
18143
+ var fs = __toESM(require("fs"));
18144
+ var path = __toESM(require("path"));
18145
+ function getHarPackageVersion() {
18146
+ if (process.env.HAR_PACKAGE_VERSION) {
18147
+ return process.env.HAR_PACKAGE_VERSION;
18148
+ }
18149
+ const candidates = [
18150
+ path.resolve(__dirname, "..", "package.json"),
18151
+ path.resolve(__dirname, "..", "..", "package.json")
18152
+ ];
18153
+ for (const packageJsonPath of candidates) {
18154
+ if (!fs.existsSync(packageJsonPath)) continue;
18155
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
18156
+ return pkg.version;
18157
+ }
18158
+ throw new Error("Could not resolve @osfactory/har package.json for version lookup");
18159
+ }
18160
+
18142
18161
  // src/cli/commands/env.ts
18143
- var path25 = __toESM(require("path"));
18162
+ var path31 = __toESM(require("path"));
18144
18163
 
18145
18164
  // src/core/harness.ts
18146
- var fs16 = __toESM(require("fs"));
18147
- var path14 = __toESM(require("path"));
18165
+ var fs18 = __toESM(require("fs"));
18166
+ var path16 = __toESM(require("path"));
18148
18167
 
18149
18168
  // src/harness/generator.ts
18150
- var fs5 = __toESM(require("fs"));
18151
- var path5 = __toESM(require("path"));
18169
+ var fs6 = __toESM(require("fs"));
18170
+ var path6 = __toESM(require("path"));
18152
18171
 
18153
18172
  // src/utils/file-ops.ts
18154
- var fs = __toESM(require("fs"));
18155
- var path = __toESM(require("path"));
18173
+ var fs2 = __toESM(require("fs"));
18174
+ var path2 = __toESM(require("path"));
18156
18175
  function readFile(filePath, maxChars = 8e3) {
18157
- if (!fs.existsSync(filePath)) return `[file not found: ${filePath}]`;
18158
- if (fs.statSync(filePath).isDirectory()) {
18176
+ if (!fs2.existsSync(filePath)) return `[file not found: ${filePath}]`;
18177
+ if (fs2.statSync(filePath).isDirectory()) {
18159
18178
  return `[path is a directory, not a file: ${filePath}. Use listDir instead.]`;
18160
18179
  }
18161
- const content = fs.readFileSync(filePath, "utf8");
18180
+ const content = fs2.readFileSync(filePath, "utf8");
18162
18181
  if (content.length > maxChars) {
18163
18182
  return content.slice(0, maxChars) + `
18164
18183
  ... [truncated at ${maxChars} chars]`;
@@ -18166,8 +18185,8 @@ function readFile(filePath, maxChars = 8e3) {
18166
18185
  return content;
18167
18186
  }
18168
18187
  function listDir(dirPath, maxFiles = 60) {
18169
- if (!fs.existsSync(dirPath)) return [];
18170
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
18188
+ if (!fs2.existsSync(dirPath)) return [];
18189
+ const entries = fs2.readdirSync(dirPath, { withFileTypes: true });
18171
18190
  const result = [];
18172
18191
  for (const entry of entries.slice(0, maxFiles)) {
18173
18192
  const name = entry.isDirectory() ? entry.name + "/" : entry.name;
@@ -18176,30 +18195,30 @@ function listDir(dirPath, maxFiles = 60) {
18176
18195
  return result;
18177
18196
  }
18178
18197
  function writeFileSafe(filePath, content) {
18179
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
18180
- fs.writeFileSync(filePath, content);
18198
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
18199
+ fs2.writeFileSync(filePath, content);
18181
18200
  }
18182
18201
  function copyDirRecursive(srcDir, destDir) {
18183
- fs.mkdirSync(destDir, { recursive: true });
18184
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
18185
- const srcPath = path.join(srcDir, entry.name);
18186
- const destPath = path.join(destDir, entry.name);
18202
+ fs2.mkdirSync(destDir, { recursive: true });
18203
+ for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
18204
+ const srcPath = path2.join(srcDir, entry.name);
18205
+ const destPath = path2.join(destDir, entry.name);
18187
18206
  if (entry.isDirectory()) {
18188
18207
  copyDirRecursive(srcPath, destPath);
18189
18208
  } else {
18190
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
18191
- fs.copyFileSync(srcPath, destPath);
18209
+ fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
18210
+ fs2.copyFileSync(srcPath, destPath);
18192
18211
  if (entry.name.endsWith(".sh")) {
18193
- fs.chmodSync(destPath, 493);
18212
+ fs2.chmodSync(destPath, 493);
18194
18213
  }
18195
18214
  }
18196
18215
  }
18197
18216
  }
18198
18217
  function resolveSafePath(baseDir, relativePath) {
18199
- const normalized = path.normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, "");
18200
- const resolved = path.resolve(baseDir, normalized);
18201
- const base = path.resolve(baseDir);
18202
- if (!resolved.startsWith(base + path.sep) && resolved !== base) {
18218
+ const normalized = path2.normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, "");
18219
+ const resolved = path2.resolve(baseDir, normalized);
18220
+ const base = path2.resolve(baseDir);
18221
+ if (!resolved.startsWith(base + path2.sep) && resolved !== base) {
18203
18222
  throw new Error(`Path escapes harness directory: ${relativePath}`);
18204
18223
  }
18205
18224
  return resolved;
@@ -18721,45 +18740,45 @@ function divider() {
18721
18740
  }
18722
18741
 
18723
18742
  // src/utils/paths.ts
18724
- var fs2 = __toESM(require("fs"));
18725
- var path2 = __toESM(require("path"));
18743
+ var fs3 = __toESM(require("fs"));
18744
+ var path3 = __toESM(require("path"));
18726
18745
  function resolveTemplatesDir() {
18727
18746
  const candidates = [
18728
- path2.join(__dirname, "templates"),
18729
- path2.join(__dirname, "..", "templates"),
18730
- path2.join(__dirname, "..", "harness", "templates")
18747
+ path3.join(__dirname, "templates"),
18748
+ path3.join(__dirname, "..", "templates"),
18749
+ path3.join(__dirname, "..", "harness", "templates")
18731
18750
  ];
18732
18751
  for (const dir of candidates) {
18733
- if (fs2.existsSync(path2.join(dir, "har-boilerplate"))) return dir;
18752
+ if (fs3.existsSync(path3.join(dir, "har-boilerplate"))) return dir;
18734
18753
  }
18735
18754
  throw new Error("Templates directory not found. Run npm run build.");
18736
18755
  }
18737
18756
  function resolvePromptPath(name) {
18738
18757
  const candidates = [
18739
- path2.join(__dirname, "prompts", name),
18740
- path2.join(__dirname, "..", "llm", "prompts", name)
18758
+ path3.join(__dirname, "prompts", name),
18759
+ path3.join(__dirname, "..", "llm", "prompts", name)
18741
18760
  ];
18742
18761
  for (const p2 of candidates) {
18743
- if (fs2.existsSync(p2)) return p2;
18762
+ if (fs3.existsSync(p2)) return p2;
18744
18763
  }
18745
18764
  return candidates[0];
18746
18765
  }
18747
18766
  function resolveTemplateFile(name) {
18748
18767
  const dir = resolveTemplatesDir();
18749
- const filePath = path2.join(dir, name);
18750
- return fs2.existsSync(filePath) ? filePath : null;
18768
+ const filePath = path3.join(dir, name);
18769
+ return fs3.existsSync(filePath) ? filePath : null;
18751
18770
  }
18752
18771
 
18753
18772
  // src/core/gitignore.ts
18754
- var fs3 = __toESM(require("fs"));
18755
- var path3 = __toESM(require("path"));
18773
+ var fs4 = __toESM(require("fs"));
18774
+ var path4 = __toESM(require("path"));
18756
18775
  var HARNESS_ROOT_GITIGNORE_PATTERNS = [
18757
18776
  ".env.agent.*",
18758
18777
  "ecosystem.agent.*.config.cjs"
18759
18778
  ];
18760
18779
  function ensureRootGitignorePatterns(checkoutDir) {
18761
- const gitignorePath = path3.join(checkoutDir, ".gitignore");
18762
- const content = fs3.existsSync(gitignorePath) ? fs3.readFileSync(gitignorePath, "utf8") : "";
18780
+ const gitignorePath = path4.join(checkoutDir, ".gitignore");
18781
+ const content = fs4.existsSync(gitignorePath) ? fs4.readFileSync(gitignorePath, "utf8") : "";
18763
18782
  const lines = content.split("\n").map((line) => line.trim());
18764
18783
  const missing = HARNESS_ROOT_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
18765
18784
  if (missing.length === 0) return;
@@ -18768,13 +18787,13 @@ function ensureRootGitignorePatterns(checkoutDir) {
18768
18787
  const block = lines.some((line) => line.includes("HAR agent slot artifacts")) || header2.length === 0 ? `${missing.join("\n")}
18769
18788
  ` : `${header2}${missing.join("\n")}
18770
18789
  `;
18771
- fs3.writeFileSync(gitignorePath, `${content}${suffix}${block}`);
18790
+ fs4.writeFileSync(gitignorePath, `${content}${suffix}${block}`);
18772
18791
  }
18773
18792
 
18774
18793
  // src/harness/manifest.ts
18775
18794
  var crypto = __toESM(require("crypto"));
18776
- var fs4 = __toESM(require("fs"));
18777
- var path4 = __toESM(require("path"));
18795
+ var fs5 = __toESM(require("fs"));
18796
+ var path5 = __toESM(require("path"));
18778
18797
 
18779
18798
  // node_modules/zod/v3/external.js
18780
18799
  var external_exports = {};
@@ -19254,8 +19273,8 @@ function getErrorMap() {
19254
19273
 
19255
19274
  // node_modules/zod/v3/helpers/parseUtil.js
19256
19275
  var makeIssue = (params) => {
19257
- const { data, path: path32, errorMaps, issueData } = params;
19258
- const fullPath = [...path32, ...issueData.path || []];
19276
+ const { data, path: path37, errorMaps, issueData } = params;
19277
+ const fullPath = [...path37, ...issueData.path || []];
19259
19278
  const fullIssue = {
19260
19279
  ...issueData,
19261
19280
  path: fullPath
@@ -19371,11 +19390,11 @@ var errorUtil;
19371
19390
 
19372
19391
  // node_modules/zod/v3/types.js
19373
19392
  var ParseInputLazyPath = class {
19374
- constructor(parent, value, path32, key) {
19393
+ constructor(parent, value, path37, key) {
19375
19394
  this._cachedPath = [];
19376
19395
  this.parent = parent;
19377
19396
  this.data = value;
19378
- this._path = path32;
19397
+ this._path = path37;
19379
19398
  this._key = key;
19380
19399
  }
19381
19400
  get path() {
@@ -22999,6 +23018,22 @@ var AgentSlotHarnessUsageSchema = external_exports.enum([
22999
23018
  "none",
23000
23019
  "bypass_warning"
23001
23020
  ]);
23021
+ var PreflightBlockerSchema = external_exports.object({
23022
+ code: external_exports.string(),
23023
+ message: external_exports.string(),
23024
+ remediation: external_exports.string().optional(),
23025
+ details: external_exports.record(external_exports.unknown()).optional()
23026
+ });
23027
+ var SlotReadinessSchema = external_exports.object({
23028
+ canLaunch: external_exports.boolean(),
23029
+ verdict: external_exports.enum(["ready", "blocked"]),
23030
+ blockers: external_exports.array(PreflightBlockerSchema),
23031
+ remediations: external_exports.array(external_exports.string()),
23032
+ ports: external_exports.record(external_exports.number()).optional(),
23033
+ allocatedPorts: external_exports.boolean().optional(),
23034
+ /** Non-blocking notices (e.g. har control up holds the default port but an alternate was picked). */
23035
+ warnings: external_exports.array(external_exports.string()).optional()
23036
+ });
23002
23037
  var AgentSlotStatusSchema = external_exports.object({
23003
23038
  agentId: external_exports.number().int().min(HAR_AGENT_SLOT_MIN),
23004
23039
  active: external_exports.boolean(),
@@ -23019,6 +23054,8 @@ var AgentSlotStatusSchema = external_exports.object({
23019
23054
  purpose: external_exports.string().optional(),
23020
23055
  sessionStatus: external_exports.enum(["starting", "active", "failed", "completed"]).optional(),
23021
23056
  lastError: external_exports.string().optional(),
23057
+ /** Set when a failed/starting session can be resumed without --replace. */
23058
+ resumeHint: external_exports.string().optional(),
23022
23059
  /** Worktree checked out to no branch (legacy failure mode; should not happen with sessions). */
23023
23060
  detachedHead: external_exports.boolean().optional(),
23024
23061
  /** Worktree has uncommitted changes. */
@@ -23027,7 +23064,13 @@ var AgentSlotStatusSchema = external_exports.object({
23027
23064
  ahead: external_exports.number().int().optional(),
23028
23065
  /** Main checkout has commits the session base doesn't (worktree serves stale code). */
23029
23066
  behind: external_exports.number().int().optional(),
23030
- stale: external_exports.boolean().optional()
23067
+ stale: external_exports.boolean().optional(),
23068
+ /** Persisted host ports from the slot registry (when dynamically allocated). */
23069
+ ports: external_exports.record(external_exports.number()).optional(),
23070
+ /** PM2 namespace mismatch — foreign or legacy processes for this slot id. */
23071
+ pm2Issue: external_exports.enum(["foreign_pm2", "registry_missing", "project_mismatch"]).optional(),
23072
+ /** Launch readiness slice — same core as `har env preflight`. */
23073
+ readiness: SlotReadinessSchema.optional()
23031
23074
  });
23032
23075
  var EnvironmentStatusSchema = external_exports.object({
23033
23076
  repoPath: external_exports.string(),
@@ -23081,17 +23124,22 @@ var SyncValidationsInputSchema = external_exports.object({
23081
23124
  var GENERATOR_VERSION = "0.4.0";
23082
23125
  var MANIFEST_VERSION = "1";
23083
23126
  var DEFAULT_HAR_DIR = ".har";
23084
- var CHECKSUM_SKIP = /* @__PURE__ */ new Set(["manifest.json", "AGENT.md.proposed", "AGENT.md.proposed.meta.json"]);
23127
+ var CHECKSUM_SKIP = /* @__PURE__ */ new Set([
23128
+ "manifest.json",
23129
+ "AGENT.md.proposed",
23130
+ "AGENT.md.proposed.meta.json",
23131
+ "ADAPT-PROMPT.md"
23132
+ ]);
23085
23133
  function getHarnessDir(repoPath) {
23086
- return path4.join(repoPath, DEFAULT_HAR_DIR);
23134
+ return path5.join(repoPath, DEFAULT_HAR_DIR);
23087
23135
  }
23088
23136
  function getManifestPath(repoPath) {
23089
- return path4.join(getHarnessDir(repoPath), "manifest.json");
23137
+ return path5.join(getHarnessDir(repoPath), "manifest.json");
23090
23138
  }
23091
23139
  function readManifest(repoPath) {
23092
23140
  const manifestPath = getManifestPath(repoPath);
23093
- if (!fs4.existsSync(manifestPath)) return null;
23094
- const raw = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
23141
+ if (!fs5.existsSync(manifestPath)) return null;
23142
+ const raw = JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
23095
23143
  const result = HarnessManifestSchema.safeParse(raw);
23096
23144
  return result.success ? result.data : null;
23097
23145
  }
@@ -23104,11 +23152,11 @@ function computeFileChecksum(content) {
23104
23152
  }
23105
23153
  function computeHarnessChecksums(harnessDir) {
23106
23154
  const checksums = {};
23107
- if (!fs4.existsSync(harnessDir)) return checksums;
23108
- for (const entry of fs4.readdirSync(harnessDir, { withFileTypes: true })) {
23155
+ if (!fs5.existsSync(harnessDir)) return checksums;
23156
+ for (const entry of fs5.readdirSync(harnessDir, { withFileTypes: true })) {
23109
23157
  if (!entry.isFile() || CHECKSUM_SKIP.has(entry.name)) continue;
23110
- const full = path4.join(harnessDir, entry.name);
23111
- checksums[entry.name] = computeFileChecksum(fs4.readFileSync(full, "utf8"));
23158
+ const full = path5.join(harnessDir, entry.name);
23159
+ checksums[entry.name] = computeFileChecksum(fs5.readFileSync(full, "utf8"));
23112
23160
  }
23113
23161
  return checksums;
23114
23162
  }
@@ -23128,17 +23176,17 @@ function createManifest(repoPath, adaptationSummary, stack, profile) {
23128
23176
  };
23129
23177
  }
23130
23178
  function resolveHarnessRoot(inputPath) {
23131
- let current = path4.resolve(inputPath);
23132
- const { root } = path4.parse(current);
23179
+ let current = path5.resolve(inputPath);
23180
+ const { root } = path5.parse(current);
23133
23181
  for (; ; ) {
23134
- const manifestPath = path4.join(current, DEFAULT_HAR_DIR, "manifest.json");
23135
- if (fs4.existsSync(manifestPath)) {
23182
+ const manifestPath = path5.join(current, DEFAULT_HAR_DIR, "manifest.json");
23183
+ if (fs5.existsSync(manifestPath)) {
23136
23184
  return current;
23137
23185
  }
23138
23186
  if (current === root) break;
23139
- current = path4.dirname(current);
23187
+ current = path5.dirname(current);
23140
23188
  }
23141
- return path4.resolve(inputPath);
23189
+ return path5.resolve(inputPath);
23142
23190
  }
23143
23191
 
23144
23192
  // src/harness/generator.ts
@@ -23154,46 +23202,46 @@ var CLI_PRUNE_FILES = [
23154
23202
  ];
23155
23203
  function pruneCliProfile(harnessDir) {
23156
23204
  for (const file of CLI_PRUNE_FILES) {
23157
- const filePath = path5.join(harnessDir, file);
23158
- if (fs5.existsSync(filePath)) {
23159
- fs5.unlinkSync(filePath);
23205
+ const filePath = path6.join(harnessDir, file);
23206
+ if (fs6.existsSync(filePath)) {
23207
+ fs6.unlinkSync(filePath);
23160
23208
  }
23161
23209
  }
23162
23210
  }
23163
23211
  function scaffoldClaudeMd(repoPath, projectName, force) {
23164
23212
  const templatePath = resolveTemplateFile("CLAUDE.md.template");
23165
23213
  if (!templatePath) return;
23166
- const dest = path5.join(repoPath, "CLAUDE.md");
23167
- if (fs5.existsSync(dest) && !force) return;
23214
+ const dest = path6.join(repoPath, "CLAUDE.md");
23215
+ if (fs6.existsSync(dest) && !force) return;
23168
23216
  const displayName = projectName.replace(/_/g, " ");
23169
- const content = fs5.readFileSync(templatePath, "utf8").replace(/__PROJECT_DISPLAY_NAME__/g, displayName);
23170
- fs5.writeFileSync(dest, content);
23217
+ const content = fs6.readFileSync(templatePath, "utf8").replace(/__PROJECT_DISPLAY_NAME__/g, displayName);
23218
+ fs6.writeFileSync(dest, content);
23171
23219
  }
23172
23220
  function scaffoldHarnessBoilerplate(repoPath, options = {}) {
23173
- const harnessDir = path5.join(repoPath, DEFAULT_HAR_DIR);
23174
- const projectName = path5.basename(repoPath).toLowerCase().replace(/[^a-z0-9]/g, "_");
23221
+ const harnessDir = path6.join(repoPath, DEFAULT_HAR_DIR);
23222
+ const projectName = path6.basename(repoPath).toLowerCase().replace(/[^a-z0-9]/g, "_");
23175
23223
  const profile = options.profile ?? "default";
23176
- const boilerplateDir = path5.join(resolveTemplatesDir(), PROFILE_DIRS[profile]);
23177
- if (fs5.existsSync(harnessDir) && !options.force) {
23224
+ const boilerplateDir = path6.join(resolveTemplatesDir(), PROFILE_DIRS[profile]);
23225
+ if (fs6.existsSync(harnessDir) && !options.force) {
23178
23226
  throw new Error(
23179
23227
  '.har/ already exists. Use --force to overwrite or run "har env maintain" to update in place.'
23180
23228
  );
23181
23229
  }
23182
- if (!fs5.existsSync(boilerplateDir)) {
23230
+ if (!fs6.existsSync(boilerplateDir)) {
23183
23231
  throw new Error(`Boilerplate template not found at ${boilerplateDir}`);
23184
23232
  }
23185
- if (options.force && fs5.existsSync(harnessDir)) {
23186
- fs5.rmSync(harnessDir, { recursive: true, force: true });
23233
+ if (options.force && fs6.existsSync(harnessDir)) {
23234
+ fs6.rmSync(harnessDir, { recursive: true, force: true });
23187
23235
  }
23188
23236
  copyDirRecursive(boilerplateDir, harnessDir);
23189
23237
  if (profile === "cli") {
23190
23238
  pruneCliProfile(harnessDir);
23191
23239
  }
23192
- const harnessEnvPath = path5.join(harnessDir, "harness.env");
23193
- if (fs5.existsSync(harnessEnvPath)) {
23194
- let content = fs5.readFileSync(harnessEnvPath, "utf8");
23240
+ const harnessEnvPath = path6.join(harnessDir, "harness.env");
23241
+ if (fs6.existsSync(harnessEnvPath)) {
23242
+ let content = fs6.readFileSync(harnessEnvPath, "utf8");
23195
23243
  content = content.replace(/__PROJECT_NAME__/g, projectName).replace(/template___PROJECT_NAME__/g, `template_${projectName}`);
23196
- fs5.writeFileSync(harnessEnvPath, content);
23244
+ fs6.writeFileSync(harnessEnvPath, content);
23197
23245
  }
23198
23246
  const manifest = createManifest(
23199
23247
  repoPath,
@@ -23216,8 +23264,8 @@ function finalizeHarness(repoPath, adaptationSummary, stack) {
23216
23264
  }
23217
23265
 
23218
23266
  // src/llm/authoring-agent.ts
23219
- var fs9 = __toESM(require("fs"));
23220
- var path8 = __toESM(require("path"));
23267
+ var fs10 = __toESM(require("fs"));
23268
+ var path9 = __toESM(require("path"));
23221
23269
 
23222
23270
  // node_modules/@anthropic-ai/sdk/error.mjs
23223
23271
  var error_exports = {};
@@ -23616,13 +23664,13 @@ var MultipartBody = class {
23616
23664
  // node_modules/@anthropic-ai/sdk/_shims/node-runtime.mjs
23617
23665
  var import_web = require("node:stream/web");
23618
23666
  var fileFromPathWarned = false;
23619
- async function fileFromPath3(path32, ...args) {
23667
+ async function fileFromPath3(path37, ...args) {
23620
23668
  const { fileFromPath: _fileFromPath } = await Promise.resolve().then(() => (init_fileFromPath(), fileFromPath_exports));
23621
23669
  if (!fileFromPathWarned) {
23622
- console.warn(`fileFromPath is deprecated; use fs.createReadStream(${JSON.stringify(path32)}) instead`);
23670
+ console.warn(`fileFromPath is deprecated; use fs.createReadStream(${JSON.stringify(path37)}) instead`);
23623
23671
  fileFromPathWarned = true;
23624
23672
  }
23625
- return await _fileFromPath(path32, ...args);
23673
+ return await _fileFromPath(path37, ...args);
23626
23674
  }
23627
23675
  var defaultHttpAgent = new import_agentkeepalive.default({ keepAlive: true, timeout: 5 * 60 * 1e3 });
23628
23676
  var defaultHttpsAgent = new import_agentkeepalive.default.HttpsAgent({ keepAlive: true, timeout: 5 * 60 * 1e3 });
@@ -24118,8 +24166,8 @@ async function defaultParseResponse(props) {
24118
24166
  }
24119
24167
  var APIPromise = class _APIPromise extends Promise {
24120
24168
  constructor(responsePromise, parseResponse = defaultParseResponse) {
24121
- super((resolve24) => {
24122
- resolve24(null);
24169
+ super((resolve26) => {
24170
+ resolve26(null);
24123
24171
  });
24124
24172
  this.responsePromise = responsePromise;
24125
24173
  this.parseResponse = parseResponse;
@@ -24219,29 +24267,29 @@ var APIClient = class {
24219
24267
  defaultIdempotencyKey() {
24220
24268
  return `stainless-node-retry-${uuid4()}`;
24221
24269
  }
24222
- get(path32, opts) {
24223
- return this.methodRequest("get", path32, opts);
24270
+ get(path37, opts) {
24271
+ return this.methodRequest("get", path37, opts);
24224
24272
  }
24225
- post(path32, opts) {
24226
- return this.methodRequest("post", path32, opts);
24273
+ post(path37, opts) {
24274
+ return this.methodRequest("post", path37, opts);
24227
24275
  }
24228
- patch(path32, opts) {
24229
- return this.methodRequest("patch", path32, opts);
24276
+ patch(path37, opts) {
24277
+ return this.methodRequest("patch", path37, opts);
24230
24278
  }
24231
- put(path32, opts) {
24232
- return this.methodRequest("put", path32, opts);
24279
+ put(path37, opts) {
24280
+ return this.methodRequest("put", path37, opts);
24233
24281
  }
24234
- delete(path32, opts) {
24235
- return this.methodRequest("delete", path32, opts);
24282
+ delete(path37, opts) {
24283
+ return this.methodRequest("delete", path37, opts);
24236
24284
  }
24237
- methodRequest(method, path32, opts) {
24285
+ methodRequest(method, path37, opts) {
24238
24286
  return this.request(Promise.resolve(opts).then(async (opts2) => {
24239
24287
  const body = opts2 && isBlobLike(opts2?.body) ? new DataView(await opts2.body.arrayBuffer()) : opts2?.body instanceof DataView ? opts2.body : opts2?.body instanceof ArrayBuffer ? new DataView(opts2.body) : opts2 && ArrayBuffer.isView(opts2?.body) ? new DataView(opts2.body.buffer) : opts2?.body;
24240
- return { method, path: path32, ...opts2, body };
24288
+ return { method, path: path37, ...opts2, body };
24241
24289
  }));
24242
24290
  }
24243
- getAPIList(path32, Page, opts) {
24244
- return this.requestAPIList(Page, { method: "get", path: path32, ...opts });
24291
+ getAPIList(path37, Page, opts) {
24292
+ return this.requestAPIList(Page, { method: "get", path: path37, ...opts });
24245
24293
  }
24246
24294
  calculateContentLength(body) {
24247
24295
  if (typeof body === "string") {
@@ -24259,10 +24307,10 @@ var APIClient = class {
24259
24307
  return null;
24260
24308
  }
24261
24309
  buildRequest(options) {
24262
- const { method, path: path32, query, headers = {} } = options;
24310
+ const { method, path: path37, query, headers = {} } = options;
24263
24311
  const body = ArrayBuffer.isView(options.body) || options.__binaryRequest && typeof options.body === "string" ? options.body : isMultipartBody(options.body) ? options.body.body : options.body ? JSON.stringify(options.body, null, 2) : null;
24264
24312
  const contentLength = this.calculateContentLength(body);
24265
- const url = this.buildURL(path32, query);
24313
+ const url = this.buildURL(path37, query);
24266
24314
  if ("timeout" in options)
24267
24315
  validatePositiveInteger("timeout", options.timeout);
24268
24316
  const timeout = options.timeout ?? this.timeout;
@@ -24371,8 +24419,8 @@ var APIClient = class {
24371
24419
  const request = this.makeRequest(options, null);
24372
24420
  return new PagePromise(this, request, Page);
24373
24421
  }
24374
- buildURL(path32, query) {
24375
- const url = isAbsoluteURL(path32) ? new URL(path32) : new URL(this.baseURL + (this.baseURL.endsWith("/") && path32.startsWith("/") ? path32.slice(1) : path32));
24422
+ buildURL(path37, query) {
24423
+ const url = isAbsoluteURL(path37) ? new URL(path37) : new URL(this.baseURL + (this.baseURL.endsWith("/") && path37.startsWith("/") ? path37.slice(1) : path37));
24376
24424
  const defaultQuery = this.defaultQuery();
24377
24425
  if (!isEmptyObj(defaultQuery)) {
24378
24426
  query = { ...defaultQuery, ...query };
@@ -24657,7 +24705,7 @@ var startsWithSchemeRegexp = new RegExp("^(?:[a-z]+:)?//", "i");
24657
24705
  var isAbsoluteURL = (url) => {
24658
24706
  return startsWithSchemeRegexp.test(url);
24659
24707
  };
24660
- var sleep = (ms) => new Promise((resolve24) => setTimeout(resolve24, ms));
24708
+ var sleep = (ms) => new Promise((resolve26) => setTimeout(resolve26, ms));
24661
24709
  var validatePositiveInteger = (name, n2) => {
24662
24710
  if (typeof n2 !== "number" || !Number.isInteger(n2)) {
24663
24711
  throw new AnthropicError(`${name} must be an integer`);
@@ -25148,12 +25196,12 @@ var PromptCachingBetaMessageStream = class _PromptCachingBetaMessageStream {
25148
25196
  }
25149
25197
  return this._emit("error", new AnthropicError(String(error3)));
25150
25198
  });
25151
- __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_connectedPromise, new Promise((resolve24, reject) => {
25152
- __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveConnectedPromise, resolve24, "f");
25199
+ __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_connectedPromise, new Promise((resolve26, reject) => {
25200
+ __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveConnectedPromise, resolve26, "f");
25153
25201
  __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_rejectConnectedPromise, reject, "f");
25154
25202
  }), "f");
25155
- __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_endPromise, new Promise((resolve24, reject) => {
25156
- __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveEndPromise, resolve24, "f");
25203
+ __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_endPromise, new Promise((resolve26, reject) => {
25204
+ __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveEndPromise, resolve26, "f");
25157
25205
  __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_rejectEndPromise, reject, "f");
25158
25206
  }), "f");
25159
25207
  __classPrivateFieldGet8(this, _PromptCachingBetaMessageStream_connectedPromise, "f").catch(() => {
@@ -25282,11 +25330,11 @@ var PromptCachingBetaMessageStream = class _PromptCachingBetaMessageStream {
25282
25330
  * const message = await stream.emitted('message') // rejects if the stream errors
25283
25331
  */
25284
25332
  emitted(event) {
25285
- return new Promise((resolve24, reject) => {
25333
+ return new Promise((resolve26, reject) => {
25286
25334
  __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_catchingPromiseCreated, true, "f");
25287
25335
  if (event !== "error")
25288
25336
  this.once("error", reject);
25289
- this.once(event, resolve24);
25337
+ this.once(event, resolve26);
25290
25338
  });
25291
25339
  }
25292
25340
  async done() {
@@ -25513,7 +25561,7 @@ var PromptCachingBetaMessageStream = class _PromptCachingBetaMessageStream {
25513
25561
  if (done) {
25514
25562
  return { value: void 0, done: true };
25515
25563
  }
25516
- return new Promise((resolve24, reject) => readQueue.push({ resolve: resolve24, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: void 0, done: true });
25564
+ return new Promise((resolve26, reject) => readQueue.push({ resolve: resolve26, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: void 0, done: true });
25517
25565
  }
25518
25566
  const chunk = pushQueue.shift();
25519
25567
  return { value: chunk, done: false };
@@ -25661,12 +25709,12 @@ var MessageStream = class _MessageStream {
25661
25709
  }
25662
25710
  return this._emit("error", new AnthropicError(String(error3)));
25663
25711
  });
25664
- __classPrivateFieldSet8(this, _MessageStream_connectedPromise, new Promise((resolve24, reject) => {
25665
- __classPrivateFieldSet8(this, _MessageStream_resolveConnectedPromise, resolve24, "f");
25712
+ __classPrivateFieldSet8(this, _MessageStream_connectedPromise, new Promise((resolve26, reject) => {
25713
+ __classPrivateFieldSet8(this, _MessageStream_resolveConnectedPromise, resolve26, "f");
25666
25714
  __classPrivateFieldSet8(this, _MessageStream_rejectConnectedPromise, reject, "f");
25667
25715
  }), "f");
25668
- __classPrivateFieldSet8(this, _MessageStream_endPromise, new Promise((resolve24, reject) => {
25669
- __classPrivateFieldSet8(this, _MessageStream_resolveEndPromise, resolve24, "f");
25716
+ __classPrivateFieldSet8(this, _MessageStream_endPromise, new Promise((resolve26, reject) => {
25717
+ __classPrivateFieldSet8(this, _MessageStream_resolveEndPromise, resolve26, "f");
25670
25718
  __classPrivateFieldSet8(this, _MessageStream_rejectEndPromise, reject, "f");
25671
25719
  }), "f");
25672
25720
  __classPrivateFieldGet9(this, _MessageStream_connectedPromise, "f").catch(() => {
@@ -25795,11 +25843,11 @@ var MessageStream = class _MessageStream {
25795
25843
  * const message = await stream.emitted('message') // rejects if the stream errors
25796
25844
  */
25797
25845
  emitted(event) {
25798
- return new Promise((resolve24, reject) => {
25846
+ return new Promise((resolve26, reject) => {
25799
25847
  __classPrivateFieldSet8(this, _MessageStream_catchingPromiseCreated, true, "f");
25800
25848
  if (event !== "error")
25801
25849
  this.once("error", reject);
25802
- this.once(event, resolve24);
25850
+ this.once(event, resolve26);
25803
25851
  });
25804
25852
  }
25805
25853
  async done() {
@@ -26026,7 +26074,7 @@ var MessageStream = class _MessageStream {
26026
26074
  if (done) {
26027
26075
  return { value: void 0, done: true };
26028
26076
  }
26029
- return new Promise((resolve24, reject) => readQueue.push({ resolve: resolve24, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: void 0, done: true });
26077
+ return new Promise((resolve26, reject) => readQueue.push({ resolve: resolve26, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: void 0, done: true });
26030
26078
  }
26031
26079
  const chunk = pushQueue.shift();
26032
26080
  return { value: chunk, done: false };
@@ -26195,34 +26243,34 @@ var { AnthropicError: AnthropicError2, APIError: APIError2, APIConnectionError:
26195
26243
  var sdk_default = Anthropic;
26196
26244
 
26197
26245
  // src/llm/tools.ts
26198
- var fs8 = __toESM(require("fs"));
26199
- var path7 = __toESM(require("path"));
26246
+ var fs9 = __toESM(require("fs"));
26247
+ var path8 = __toESM(require("path"));
26200
26248
  var readline2 = __toESM(require("readline"));
26201
26249
 
26202
26250
  // src/harness/agent-md.ts
26203
- var fs7 = __toESM(require("fs"));
26204
- var path6 = __toESM(require("path"));
26251
+ var fs8 = __toESM(require("fs"));
26252
+ var path7 = __toESM(require("path"));
26205
26253
  var readline = __toESM(require("readline"));
26206
26254
  var AGENT_MD_PROPOSAL = "AGENT.md.proposed";
26207
26255
  var AGENT_MD_PROPOSAL_META = "AGENT.md.proposed.meta.json";
26208
26256
  function writeAgentMdProposal(repoPath, content, rationale) {
26209
26257
  const harnessDir = getHarnessDir(repoPath);
26210
- writeFileSafe(path6.join(harnessDir, AGENT_MD_PROPOSAL), content);
26258
+ writeFileSafe(path7.join(harnessDir, AGENT_MD_PROPOSAL), content);
26211
26259
  writeFileSafe(
26212
- path6.join(harnessDir, AGENT_MD_PROPOSAL_META),
26260
+ path7.join(harnessDir, AGENT_MD_PROPOSAL_META),
26213
26261
  JSON.stringify({ rationale, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
26214
26262
  );
26215
26263
  }
26216
26264
  function readAgentMdProposal(repoPath) {
26217
26265
  const harnessDir = getHarnessDir(repoPath);
26218
- const proposalPath = path6.join(harnessDir, AGENT_MD_PROPOSAL);
26219
- const metaPath = path6.join(harnessDir, AGENT_MD_PROPOSAL_META);
26220
- if (!fs7.existsSync(proposalPath)) return null;
26221
- const content = fs7.readFileSync(proposalPath, "utf8");
26266
+ const proposalPath = path7.join(harnessDir, AGENT_MD_PROPOSAL);
26267
+ const metaPath = path7.join(harnessDir, AGENT_MD_PROPOSAL_META);
26268
+ if (!fs8.existsSync(proposalPath)) return null;
26269
+ const content = fs8.readFileSync(proposalPath, "utf8");
26222
26270
  let rationale = "";
26223
- if (fs7.existsSync(metaPath)) {
26271
+ if (fs8.existsSync(metaPath)) {
26224
26272
  try {
26225
- rationale = JSON.parse(fs7.readFileSync(metaPath, "utf8")).rationale ?? "";
26273
+ rationale = JSON.parse(fs8.readFileSync(metaPath, "utf8")).rationale ?? "";
26226
26274
  } catch {
26227
26275
  rationale = "";
26228
26276
  }
@@ -26230,21 +26278,21 @@ function readAgentMdProposal(repoPath) {
26230
26278
  return {
26231
26279
  content,
26232
26280
  rationale,
26233
- createdAt: fs7.existsSync(metaPath) ? JSON.parse(fs7.readFileSync(metaPath, "utf8")).createdAt : (/* @__PURE__ */ new Date()).toISOString()
26281
+ createdAt: fs8.existsSync(metaPath) ? JSON.parse(fs8.readFileSync(metaPath, "utf8")).createdAt : (/* @__PURE__ */ new Date()).toISOString()
26234
26282
  };
26235
26283
  }
26236
26284
  function clearAgentMdProposal(repoPath) {
26237
26285
  const harnessDir = getHarnessDir(repoPath);
26238
26286
  for (const file of [AGENT_MD_PROPOSAL, AGENT_MD_PROPOSAL_META]) {
26239
- const p2 = path6.join(harnessDir, file);
26240
- if (fs7.existsSync(p2)) fs7.unlinkSync(p2);
26287
+ const p2 = path7.join(harnessDir, file);
26288
+ if (fs8.existsSync(p2)) fs8.unlinkSync(p2);
26241
26289
  }
26242
26290
  }
26243
26291
  async function promptApplyAgentMdProposal(repoPath) {
26244
26292
  const proposal = readAgentMdProposal(repoPath);
26245
26293
  if (!proposal) return false;
26246
- const agentMdPath = path6.join(repoPath, "AGENT.md");
26247
- const exists = fs7.existsSync(agentMdPath);
26294
+ const agentMdPath = path7.join(repoPath, "AGENT.md");
26295
+ const exists = fs8.existsSync(agentMdPath);
26248
26296
  process.stderr.write("\n");
26249
26297
  process.stderr.write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
26250
26298
  process.stderr.write("Proposed AGENT.md (repo root)\n");
@@ -26286,11 +26334,11 @@ async function promptApplyAgentMdProposal(repoPath) {
26286
26334
  }
26287
26335
  async function askYesNo(question) {
26288
26336
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
26289
- return new Promise((resolve24) => {
26337
+ return new Promise((resolve26) => {
26290
26338
  process.stderr.write(`${question} `);
26291
26339
  rl.once("line", (answer) => {
26292
26340
  rl.close();
26293
- resolve24(/^y(es)?$/i.test(answer.trim()));
26341
+ resolve26(/^y(es)?$/i.test(answer.trim()));
26294
26342
  });
26295
26343
  });
26296
26344
  }
@@ -26432,13 +26480,13 @@ async function handleAuthoringToolCall(toolName, input, repoPath, harnessDir, ca
26432
26480
  async function executeAuthoringToolCall(toolName, input, repoPath, harnessDir, callbacks) {
26433
26481
  switch (toolName) {
26434
26482
  case "readRepoFile": {
26435
- const filePath = path7.join(repoPath, input.path);
26483
+ const filePath = path8.join(repoPath, input.path);
26436
26484
  const content = readFile(filePath, input.maxChars ?? 8e3);
26437
26485
  info(`Read repo: ${input.path}`);
26438
26486
  return content;
26439
26487
  }
26440
26488
  case "listRepoDir": {
26441
- const dirPath = path7.join(repoPath, input.path ?? ".");
26489
+ const dirPath = path8.join(repoPath, input.path ?? ".");
26442
26490
  const entries = listDir(dirPath, input.maxFiles ?? 60);
26443
26491
  info(`List repo: ${input.path} \u2192 ${entries.length} entries`);
26444
26492
  return entries.length > 0 ? entries.join("\n") : "(empty directory)";
@@ -26463,17 +26511,17 @@ async function executeAuthoringToolCall(toolName, input, repoPath, harnessDir, c
26463
26511
  const filePath = resolveSafePath(harnessDir, relPath);
26464
26512
  writeFileSafe(filePath, input.content);
26465
26513
  if (relPath.endsWith(".sh")) {
26466
- fs8.chmodSync(filePath, 493);
26514
+ fs9.chmodSync(filePath, 493);
26467
26515
  }
26468
26516
  info(`Wrote harness: ${relPath}`);
26469
26517
  return `Written ${relPath} (${input.content.length} bytes)`;
26470
26518
  }
26471
26519
  case "editHarnessFile": {
26472
26520
  const filePath = resolveSafePath(harnessDir, input.path);
26473
- if (!fs8.existsSync(filePath)) {
26521
+ if (!fs9.existsSync(filePath)) {
26474
26522
  return `Error: file not found: ${input.path}`;
26475
26523
  }
26476
- const content = fs8.readFileSync(filePath, "utf8");
26524
+ const content = fs9.readFileSync(filePath, "utf8");
26477
26525
  const oldStr = input.old_string;
26478
26526
  const newStr = input.new_string;
26479
26527
  const count = content.split(oldStr).length - 1;
@@ -26490,10 +26538,10 @@ async function executeAuthoringToolCall(toolName, input, repoPath, harnessDir, c
26490
26538
  }
26491
26539
  case "deleteHarnessFile": {
26492
26540
  const filePath = resolveSafePath(harnessDir, input.path);
26493
- if (!fs8.existsSync(filePath)) {
26541
+ if (!fs9.existsSync(filePath)) {
26494
26542
  return `File not found (already deleted): ${input.path}`;
26495
26543
  }
26496
- fs8.unlinkSync(filePath);
26544
+ fs9.unlinkSync(filePath);
26497
26545
  info(`Deleted harness: ${input.path}`);
26498
26546
  return `Deleted ${input.path}`;
26499
26547
  }
@@ -26520,7 +26568,7 @@ async function executeAuthoringToolCall(toolName, input, repoPath, harnessDir, c
26520
26568
  }
26521
26569
  async function askUser(question, options) {
26522
26570
  const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
26523
- return new Promise((resolve24) => {
26571
+ return new Promise((resolve26) => {
26524
26572
  process.stderr.write("\n");
26525
26573
  process.stderr.write(`\u2753 ${question}
26526
26574
  `);
@@ -26531,7 +26579,7 @@ async function askUser(question, options) {
26531
26579
  process.stderr.write("> ");
26532
26580
  rl.once("line", (answer) => {
26533
26581
  rl.close();
26534
- resolve24(answer.trim());
26582
+ resolve26(answer.trim());
26535
26583
  });
26536
26584
  });
26537
26585
  }
@@ -26544,29 +26592,29 @@ async function authorHarness(repoPath, apiKey, options = {}) {
26544
26592
  const mode = options.mode ?? "init";
26545
26593
  const systemPromptPath = resolvePromptPath("system-authoring.md");
26546
26594
  const agentMdTemplatePath = resolveTemplateFile("AGENT.md.template");
26547
- let systemPrompt = fs9.existsSync(systemPromptPath) ? fs9.readFileSync(systemPromptPath, "utf8") : "Adapt the .har/ boilerplate to match the repository. Edit files directly. Call finishAuthoring when done.";
26595
+ let systemPrompt = fs10.existsSync(systemPromptPath) ? fs10.readFileSync(systemPromptPath, "utf8") : "Adapt the .har/ boilerplate to match the repository. Edit files directly. Call finishAuthoring when done.";
26548
26596
  if (agentMdTemplatePath) {
26549
26597
  systemPrompt += `
26550
26598
 
26551
26599
  ## AGENT.md template (starting point for proposeAgentMd)
26552
26600
 
26553
- ${fs9.readFileSync(agentMdTemplatePath, "utf8")}`;
26601
+ ${fs10.readFileSync(agentMdTemplatePath, "utf8")}`;
26554
26602
  }
26555
26603
  const tools = buildAuthoringTools();
26556
26604
  const messages = [];
26557
26605
  let rootListing;
26558
26606
  try {
26559
- rootListing = fs9.readdirSync(repoPath).slice(0, 50).join("\n");
26607
+ rootListing = fs10.readdirSync(repoPath).slice(0, 50).join("\n");
26560
26608
  } catch {
26561
26609
  throw new Error(`Cannot read repo at path: ${repoPath}`);
26562
26610
  }
26563
26611
  let harnessListing;
26564
26612
  try {
26565
- harnessListing = fs9.readdirSync(harnessDir).join("\n");
26613
+ harnessListing = fs10.readdirSync(harnessDir).join("\n");
26566
26614
  } catch {
26567
26615
  throw new Error(`Cannot read harness dir at: ${harnessDir}`);
26568
26616
  }
26569
- const existingAgentMd = fs9.existsSync(path8.join(repoPath, "AGENT.md")) ? "AGENT.md exists at repo root \u2014 read it before proposing changes via proposeAgentMd." : "No AGENT.md at repo root yet \u2014 propose one via proposeAgentMd.";
26617
+ const existingAgentMd = fs10.existsSync(path9.join(repoPath, "AGENT.md")) ? "AGENT.md exists at repo root \u2014 read it before proposing changes via proposeAgentMd." : "No AGENT.md at repo root yet \u2014 propose one via proposeAgentMd.";
26570
26618
  const modeInstructions = mode === "maintain" ? `This is a MAINTENANCE run. .har/ already exists and may have been edited by humans.
26571
26619
  Update it to reflect current repo changes. Prefer editHarnessFile over writeHarnessFile.
26572
26620
  Keep README.md accurate \u2014 it is the index of the harness. ${existingAgentMd}` : `This is an INITIAL setup. A fresh boilerplate has been copied to .har/.
@@ -26646,8 +26694,8 @@ Try running with --verbose to see what the agent is doing.`
26646
26694
  }
26647
26695
 
26648
26696
  // src/harness/validator.ts
26649
- var fs12 = __toESM(require("fs"));
26650
- var path11 = __toESM(require("path"));
26697
+ var fs13 = __toESM(require("fs"));
26698
+ var path12 = __toESM(require("path"));
26651
26699
 
26652
26700
  // src/utils/shell.ts
26653
26701
  var import_child_process = require("child_process");
@@ -26675,7 +26723,7 @@ function runScript(scriptPath, args = [], options = {}) {
26675
26723
  function runScriptCapture(scriptPath, args = [], options = {}) {
26676
26724
  const stream = options.stream ?? false;
26677
26725
  const { stream: _stream, ...spawnOptions } = options;
26678
- return new Promise((resolve24) => {
26726
+ return new Promise((resolve26) => {
26679
26727
  const proc = (0, import_child_process.spawn)("bash", [scriptPath, ...args], {
26680
26728
  ...spawnOptions,
26681
26729
  stdio: ["inherit", "pipe", "pipe"]
@@ -26690,13 +26738,13 @@ function runScriptCapture(scriptPath, args = [], options = {}) {
26690
26738
  stderr += d2;
26691
26739
  if (stream) process.stderr.write(d2);
26692
26740
  });
26693
- proc.on("close", (code) => resolve24({ stdout, stderr, code: code ?? 0 }));
26741
+ proc.on("close", (code) => resolve26({ stdout, stderr, code: code ?? 0 }));
26694
26742
  });
26695
26743
  }
26696
26744
  function runShellCommand(command2, options = {}) {
26697
26745
  const stream = options.stream ?? false;
26698
26746
  const { stream: _stream, ...spawnOptions } = options;
26699
- return new Promise((resolve24) => {
26747
+ return new Promise((resolve26) => {
26700
26748
  const proc = (0, import_child_process.spawn)("bash", ["-lc", command2], {
26701
26749
  ...spawnOptions,
26702
26750
  stdio: ["inherit", "pipe", "pipe"]
@@ -26711,22 +26759,22 @@ function runShellCommand(command2, options = {}) {
26711
26759
  stderr += d2;
26712
26760
  if (stream) process.stderr.write(d2);
26713
26761
  });
26714
- proc.on("close", (code) => resolve24({ stdout, stderr, code: code ?? 0 }));
26762
+ proc.on("close", (code) => resolve26({ stdout, stderr, code: code ?? 0 }));
26715
26763
  });
26716
26764
  }
26717
26765
 
26718
26766
  // src/harness/stages.ts
26719
- var fs11 = __toESM(require("fs"));
26720
- var path10 = __toESM(require("path"));
26767
+ var fs12 = __toESM(require("fs"));
26768
+ var path11 = __toESM(require("path"));
26721
26769
 
26722
26770
  // src/harness/env.ts
26723
- var fs10 = __toESM(require("fs"));
26724
- var path9 = __toESM(require("path"));
26771
+ var fs11 = __toESM(require("fs"));
26772
+ var path10 = __toESM(require("path"));
26725
26773
  function readHarnessEnv(repoPath) {
26726
- const envPath = path9.join(getHarnessDir(repoPath), "harness.env");
26727
- if (!fs10.existsSync(envPath)) return {};
26774
+ const envPath = path10.join(getHarnessDir(repoPath), "harness.env");
26775
+ if (!fs11.existsSync(envPath)) return {};
26728
26776
  const values = {};
26729
- for (const line of fs10.readFileSync(envPath, "utf8").split("\n")) {
26777
+ for (const line of fs11.readFileSync(envPath, "utf8").split("\n")) {
26730
26778
  const match = line.match(/^export\s+([A-Z0-9_]+)=(.*)$/);
26731
26779
  if (!match) continue;
26732
26780
  values[match[1]] = match[2].replace(/^"|"$/g, "");
@@ -26753,7 +26801,7 @@ var AGENT_REQUIRED_KINDS = /* @__PURE__ */ new Set([
26753
26801
  "custom"
26754
26802
  ]);
26755
26803
  function getStageRegistryPath(repoPath) {
26756
- return path10.join(getHarnessDir(repoPath), STAGE_REGISTRY_FILE);
26804
+ return path11.join(getHarnessDir(repoPath), STAGE_REGISTRY_FILE);
26757
26805
  }
26758
26806
  function synthesizeStageRegistry(repoPath) {
26759
26807
  const harnessDir = getHarnessDir(repoPath);
@@ -26828,13 +26876,13 @@ function readAgentSlotsFromHarnessEnv(harnessEnv) {
26828
26876
  }
26829
26877
  function stageScriptExists(harnessDir, stage) {
26830
26878
  if (stage.script) {
26831
- return fs11.existsSync(path10.join(harnessDir, stage.script));
26879
+ return fs12.existsSync(path11.join(harnessDir, stage.script));
26832
26880
  }
26833
26881
  if (stage.command) {
26834
26882
  const scriptName = stage.command.split(/\s+/)[0].replace(/^\.\/\.har\//, "").replace(/^\.\//, "");
26835
- return fs11.existsSync(path10.join(harnessDir, scriptName));
26883
+ return fs12.existsSync(path11.join(harnessDir, scriptName));
26836
26884
  }
26837
- return fs11.existsSync(path10.join(harnessDir, "stages", `${stage.id}.sh`));
26885
+ return fs12.existsSync(path11.join(harnessDir, "stages", `${stage.id}.sh`));
26838
26886
  }
26839
26887
  function addStageIfRunnable(stages, harnessDir, stage) {
26840
26888
  const parsed = HarnessStageSchema.parse(stage);
@@ -26844,10 +26892,10 @@ function addStageIfRunnable(stages, harnessDir, stage) {
26844
26892
  }
26845
26893
  function readStageRegistry(repoPath) {
26846
26894
  const registryPath = getStageRegistryPath(repoPath);
26847
- if (!fs11.existsSync(registryPath)) {
26895
+ if (!fs12.existsSync(registryPath)) {
26848
26896
  return synthesizeStageRegistry(repoPath);
26849
26897
  }
26850
- const raw = JSON.parse(fs11.readFileSync(registryPath, "utf8"));
26898
+ const raw = JSON.parse(fs12.readFileSync(registryPath, "utf8"));
26851
26899
  const parsed = HarnessStageRegistrySchema.safeParse(raw);
26852
26900
  if (!parsed.success) {
26853
26901
  throw new Error(`Invalid .har/${STAGE_REGISTRY_FILE}: ${parsed.error.message}`);
@@ -26860,7 +26908,7 @@ function writeStageRegistry(repoPath, registry2) {
26860
26908
  if (!parsed.success) {
26861
26909
  throw new Error(`Invalid stage registry: ${parsed.error.message}`);
26862
26910
  }
26863
- fs11.writeFileSync(registryPath, JSON.stringify(parsed.data, null, 2) + "\n");
26911
+ fs12.writeFileSync(registryPath, JSON.stringify(parsed.data, null, 2) + "\n");
26864
26912
  }
26865
26913
  function listStages(repoPath) {
26866
26914
  return readStageRegistry(repoPath).stages;
@@ -26921,6 +26969,7 @@ var REQUIRED_FILES_DEFAULT = [
26921
26969
  "harness.env",
26922
26970
  "setup-infra.sh",
26923
26971
  "launch.sh",
26972
+ "provision-toolchain.sh",
26924
26973
  "verify.sh",
26925
26974
  "teardown.sh",
26926
26975
  "agent-cli.sh",
@@ -26939,6 +26988,7 @@ var REQUIRED_FILES_IOS = [
26939
26988
  "harness.env",
26940
26989
  "setup-infra.sh",
26941
26990
  "launch.sh",
26991
+ "provision-toolchain.sh",
26942
26992
  "verify.sh",
26943
26993
  "teardown.sh",
26944
26994
  "agent-cli.sh",
@@ -26955,6 +27005,7 @@ function getRequiredFiles(repoPath) {
26955
27005
  var SHELL_SCRIPTS = [
26956
27006
  "setup-infra.sh",
26957
27007
  "launch.sh",
27008
+ "provision-toolchain.sh",
26958
27009
  "verify.sh",
26959
27010
  "teardown.sh",
26960
27011
  "agent-cli.sh",
@@ -26963,22 +27014,22 @@ var SHELL_SCRIPTS = [
26963
27014
  function validateHarness(repoPath) {
26964
27015
  const harnessDir = getHarnessDir(repoPath);
26965
27016
  const issues = [];
26966
- if (!fs12.existsSync(harnessDir)) {
27017
+ if (!fs13.existsSync(harnessDir)) {
26967
27018
  return {
26968
27019
  pass: false,
26969
27020
  issues: [{ file: ".har", message: "Harness directory not found", severity: "error" }]
26970
27021
  };
26971
27022
  }
26972
27023
  for (const file of getRequiredFiles(repoPath)) {
26973
- const filePath = path11.join(harnessDir, file);
26974
- if (!fs12.existsSync(filePath)) {
27024
+ const filePath = path12.join(harnessDir, file);
27025
+ if (!fs13.existsSync(filePath)) {
26975
27026
  issues.push({ file, message: "Required file missing", severity: "error" });
26976
27027
  }
26977
27028
  }
26978
27029
  for (const script of SHELL_SCRIPTS) {
26979
- const scriptPath = path11.join(harnessDir, script);
26980
- if (!fs12.existsSync(scriptPath)) continue;
26981
- const stat = fs12.statSync(scriptPath);
27030
+ const scriptPath = path12.join(harnessDir, script);
27031
+ if (!fs13.existsSync(scriptPath)) continue;
27032
+ const stat = fs13.statSync(scriptPath);
26982
27033
  if (!(stat.mode & 73)) {
26983
27034
  issues.push({ file: script, message: "Script is not executable", severity: "warning" });
26984
27035
  }
@@ -26993,9 +27044,9 @@ function validateHarness(repoPath) {
26993
27044
  }
26994
27045
  const manifest = readManifest(repoPath);
26995
27046
  const profile = manifest?.profile ?? "default";
26996
- const ecosystemPath = path11.join(harnessDir, "ecosystem.agent.template.cjs");
26997
- if (profile !== "cli" && fs12.existsSync(ecosystemPath)) {
26998
- const content = fs12.readFileSync(ecosystemPath, "utf8");
27047
+ const ecosystemPath = path12.join(harnessDir, "ecosystem.agent.template.cjs");
27048
+ if (profile !== "cli" && fs13.existsSync(ecosystemPath)) {
27049
+ const content = fs13.readFileSync(ecosystemPath, "utf8");
26999
27050
  if (!content.includes("module.exports")) {
27000
27051
  issues.push({
27001
27052
  file: "ecosystem.agent.template.cjs",
@@ -27004,9 +27055,9 @@ function validateHarness(repoPath) {
27004
27055
  });
27005
27056
  }
27006
27057
  }
27007
- const harnessEnvPath = path11.join(harnessDir, "harness.env");
27008
- if (fs12.existsSync(harnessEnvPath)) {
27009
- const content = fs12.readFileSync(harnessEnvPath, "utf8");
27058
+ const harnessEnvPath = path12.join(harnessDir, "harness.env");
27059
+ if (fs13.existsSync(harnessEnvPath)) {
27060
+ const content = fs13.readFileSync(harnessEnvPath, "utf8");
27010
27061
  if (content.includes("TODO: set migrate command")) {
27011
27062
  issues.push({ file: "harness.env", message: "Migrate command still has TODO", severity: "warning" });
27012
27063
  }
@@ -27014,17 +27065,17 @@ function validateHarness(repoPath) {
27014
27065
  issues.push({ file: "harness.env", message: "Seed command still has TODO", severity: "warning" });
27015
27066
  }
27016
27067
  }
27017
- const verifyPath = path11.join(harnessDir, "verify.sh");
27018
- if (fs12.existsSync(verifyPath)) {
27019
- const content = fs12.readFileSync(verifyPath, "utf8");
27068
+ const verifyPath = path12.join(harnessDir, "verify.sh");
27069
+ if (fs13.existsSync(verifyPath)) {
27070
+ const content = fs13.readFileSync(verifyPath, "utf8");
27020
27071
  if (content.includes("echo 'TODO:")) {
27021
27072
  issues.push({ file: "verify.sh", message: "Verification steps still have TODO placeholders", severity: "warning" });
27022
27073
  }
27023
27074
  }
27024
- const stagesPath = path11.join(harnessDir, "stages.json");
27025
- if (fs12.existsSync(stagesPath)) {
27075
+ const stagesPath = path12.join(harnessDir, "stages.json");
27076
+ if (fs13.existsSync(stagesPath)) {
27026
27077
  try {
27027
- JSON.parse(fs12.readFileSync(stagesPath, "utf8"));
27078
+ JSON.parse(fs13.readFileSync(stagesPath, "utf8"));
27028
27079
  const registry2 = readStageRegistry(repoPath);
27029
27080
  if (registry2.stages.length === 0) {
27030
27081
  issues.push({ file: "stages.json", message: "No harness stages declared", severity: "warning" });
@@ -27039,9 +27090,9 @@ function validateHarness(repoPath) {
27039
27090
  } else {
27040
27091
  issues.push({ file: "stages.json", message: "Stage registry missing", severity: "warning" });
27041
27092
  }
27042
- const readmePath = path11.join(harnessDir, "README.md");
27043
- if (fs12.existsSync(readmePath)) {
27044
- const content = fs12.readFileSync(readmePath, "utf8");
27093
+ const readmePath = path12.join(harnessDir, "README.md");
27094
+ if (fs13.existsSync(readmePath)) {
27095
+ const content = fs13.readFileSync(readmePath, "utf8");
27045
27096
  if (!content.includes(".har")) {
27046
27097
  issues.push({ file: "README.md", message: "README should document .har/ paths", severity: "warning" });
27047
27098
  }
@@ -27055,8 +27106,8 @@ function validateHarness(repoPath) {
27055
27106
  async function smokeTestHarness(repoPath) {
27056
27107
  const harnessDir = getHarnessDir(repoPath);
27057
27108
  const issues = [];
27058
- const setupScript = path11.join(harnessDir, "setup-infra.sh");
27059
- if (fs12.existsSync(setupScript)) {
27109
+ const setupScript = path12.join(harnessDir, "setup-infra.sh");
27110
+ if (fs13.existsSync(setupScript)) {
27060
27111
  const result = run(`bash "${setupScript}"`, { cwd: repoPath });
27061
27112
  if (result.code !== 0) {
27062
27113
  issues.push({
@@ -27071,8 +27122,8 @@ async function smokeTestHarness(repoPath) {
27071
27122
  }
27072
27123
 
27073
27124
  // src/harness/drift.ts
27074
- var fs13 = __toESM(require("fs"));
27075
- var path12 = __toESM(require("path"));
27125
+ var fs14 = __toESM(require("fs"));
27126
+ var path13 = __toESM(require("path"));
27076
27127
  var PROFILE_DIRS2 = {
27077
27128
  default: "har-boilerplate",
27078
27129
  cli: "har-boilerplate-cli",
@@ -27083,37 +27134,90 @@ var CLI_EXPECTED_ABSENT = /* @__PURE__ */ new Set([
27083
27134
  "env.template",
27084
27135
  "attach.sh"
27085
27136
  ]);
27137
+ var APP_PORT_VARS = [
27138
+ "HARNESS_FE_BASE_PORT",
27139
+ "HARNESS_API_BASE_PORT",
27140
+ "HARNESS_PORT_STEP"
27141
+ ];
27142
+ var INFRA_PORT_VARS_BY_SERVICE = {
27143
+ db: [
27144
+ "HARNESS_DB_PORT_DEFAULT",
27145
+ "HARNESS_DB_PORT_SCAN_START",
27146
+ "HARNESS_DB_PORT_SCAN_END"
27147
+ ],
27148
+ minio: [
27149
+ "HARNESS_MINIO_PORT_DEFAULT",
27150
+ "HARNESS_MINIO_PORT_SCAN_START",
27151
+ "HARNESS_MINIO_PORT_SCAN_END",
27152
+ "HARNESS_MINIO_CONSOLE_PORT_DEFAULT",
27153
+ "HARNESS_MINIO_CONSOLE_PORT_SCAN_START",
27154
+ "HARNESS_MINIO_CONSOLE_PORT_SCAN_END"
27155
+ ],
27156
+ mailpit: [
27157
+ "HARNESS_MAILPIT_WEB_PORT_DEFAULT",
27158
+ "HARNESS_MAILPIT_WEB_PORT_SCAN_START",
27159
+ "HARNESS_MAILPIT_WEB_PORT_SCAN_END",
27160
+ "HARNESS_MAILPIT_SMTP_PORT_DEFAULT",
27161
+ "HARNESS_MAILPIT_SMTP_PORT_SCAN_START",
27162
+ "HARNESS_MAILPIT_SMTP_PORT_SCAN_END"
27163
+ ],
27164
+ "headless-browser": [
27165
+ "HARNESS_BROWSER_PORT_DEFAULT",
27166
+ "HARNESS_BROWSER_PORT_SCAN_START",
27167
+ "HARNESS_BROWSER_PORT_SCAN_END"
27168
+ ]
27169
+ };
27170
+ function missingPortDocumentationVars(profile, env3) {
27171
+ const missing = [];
27172
+ if (profile === "default") {
27173
+ for (const key of APP_PORT_VARS) {
27174
+ if (!(key in env3)) missing.push(key);
27175
+ }
27176
+ }
27177
+ if (profile === "cli" && !("HARNESS_PORT_STEP" in env3)) {
27178
+ missing.push("HARNESS_PORT_STEP");
27179
+ }
27180
+ const services = (env3.HARNESS_INFRA_SERVICES ?? "").trim().split(/\s+/).filter(Boolean);
27181
+ for (const service of services) {
27182
+ const vars = INFRA_PORT_VARS_BY_SERVICE[service];
27183
+ if (!vars) continue;
27184
+ for (const key of vars) {
27185
+ if (!(key in env3)) missing.push(key);
27186
+ }
27187
+ }
27188
+ return [...new Set(missing)];
27189
+ }
27086
27190
  function substituteProjectName(content, projectName) {
27087
27191
  return content.replace(/__PROJECT_NAME__/g, projectName).replace(/template___PROJECT_NAME__/g, `template_${projectName}`);
27088
27192
  }
27089
27193
  function listBoilerplateFiles(boilerplateDir) {
27090
- if (!fs13.existsSync(boilerplateDir)) return [];
27091
- return fs13.readdirSync(boilerplateDir, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
27194
+ if (!fs14.existsSync(boilerplateDir)) return [];
27195
+ return fs14.readdirSync(boilerplateDir, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
27092
27196
  }
27093
27197
  function compareHarnessToTemplate(repoPath) {
27094
- const resolved = path12.resolve(repoPath);
27198
+ const resolved = path13.resolve(repoPath);
27095
27199
  const manifest = readManifest(resolved);
27096
27200
  const profile = manifest?.profile ?? "default";
27097
27201
  const harnessDir = getHarnessDir(resolved);
27098
- const projectName = path12.basename(resolved).toLowerCase().replace(/[^a-z0-9]/g, "_");
27099
- const boilerplateDir = path12.join(resolveTemplatesDir(), PROFILE_DIRS2[profile]);
27202
+ const projectName = path13.basename(resolved).toLowerCase().replace(/[^a-z0-9]/g, "_");
27203
+ const boilerplateDir = path13.join(resolveTemplatesDir(), PROFILE_DIRS2[profile]);
27100
27204
  const templateFiles = listBoilerplateFiles(boilerplateDir);
27101
27205
  const missing = [];
27102
27206
  const checksumMismatch = [];
27103
27207
  const extra = [];
27104
27208
  const unchanged = [];
27105
27209
  for (const file of templateFiles) {
27106
- const templatePath = path12.join(boilerplateDir, file);
27107
- const harnessPath = path12.join(harnessDir, file);
27108
- let templateContent = fs13.readFileSync(templatePath, "utf8");
27210
+ const templatePath = path13.join(boilerplateDir, file);
27211
+ const harnessPath = path13.join(harnessDir, file);
27212
+ let templateContent = fs14.readFileSync(templatePath, "utf8");
27109
27213
  if (file === "harness.env") {
27110
27214
  templateContent = substituteProjectName(templateContent, projectName);
27111
27215
  }
27112
- if (!fs13.existsSync(harnessPath)) {
27216
+ if (!fs14.existsSync(harnessPath)) {
27113
27217
  missing.push(file);
27114
27218
  continue;
27115
27219
  }
27116
- const harnessChecksum = computeFileChecksum(fs13.readFileSync(harnessPath, "utf8"));
27220
+ const harnessChecksum = computeFileChecksum(fs14.readFileSync(harnessPath, "utf8"));
27117
27221
  const templateChecksum = computeFileChecksum(templateContent);
27118
27222
  if (harnessChecksum === templateChecksum) {
27119
27223
  unchanged.push(file);
@@ -27121,12 +27225,11 @@ function compareHarnessToTemplate(repoPath) {
27121
27225
  checksumMismatch.push(file);
27122
27226
  }
27123
27227
  }
27124
- if (fs13.existsSync(harnessDir)) {
27125
- for (const file of fs13.readdirSync(harnessDir)) {
27126
- const full = path12.join(harnessDir, file);
27127
- if (!fs13.statSync(full).isFile()) continue;
27228
+ if (fs14.existsSync(harnessDir)) {
27229
+ for (const file of fs14.readdirSync(harnessDir)) {
27230
+ const full = path13.join(harnessDir, file);
27231
+ if (!fs14.statSync(full).isFile()) continue;
27128
27232
  if (file === "manifest.json" || file.startsWith("ADAPT-PROMPT")) continue;
27129
- if (templateFiles.includes(file)) continue;
27130
27233
  if (profile === "cli" && CLI_EXPECTED_ABSENT.has(file)) {
27131
27234
  extra.push(file);
27132
27235
  } else if (!templateFiles.includes(file)) {
@@ -27135,6 +27238,8 @@ function compareHarnessToTemplate(repoPath) {
27135
27238
  }
27136
27239
  }
27137
27240
  const installed = manifest?.generatorVersion;
27241
+ const harnessEnv = readHarnessEnv(resolved);
27242
+ const missingPortVars = missingPortDocumentationVars(profile, harnessEnv);
27138
27243
  return {
27139
27244
  generatorVersion: {
27140
27245
  installed,
@@ -27144,71 +27249,355 @@ function compareHarnessToTemplate(repoPath) {
27144
27249
  missing,
27145
27250
  checksumMismatch,
27146
27251
  extra,
27147
- unchanged
27252
+ unchanged,
27253
+ missingPortVars
27148
27254
  };
27149
27255
  }
27150
27256
 
27151
- // src/harness/stage-templates.ts
27257
+ // src/harness/maintain-bundle.ts
27152
27258
  var fs15 = __toESM(require("fs"));
27153
- var path13 = __toESM(require("path"));
27259
+ var os2 = __toESM(require("os"));
27260
+ var path14 = __toESM(require("path"));
27261
+ var MAINTAIN_DIR = "maintain";
27262
+ var PROFILE_DIRS3 = {
27263
+ default: "har-boilerplate",
27264
+ cli: "har-boilerplate-cli",
27265
+ ios: "har-boilerplate-ios"
27266
+ };
27267
+ function substituteProjectName2(content, projectName) {
27268
+ return content.replace(/__PROJECT_NAME__/g, projectName).replace(/template___PROJECT_NAME__/g, `template_${projectName}`);
27269
+ }
27270
+ function projectNameFromRepo(repoPath) {
27271
+ return path14.basename(repoPath).toLowerCase().replace(/[^a-z0-9]/g, "_");
27272
+ }
27273
+ function boilerplateDirForProfile(profile) {
27274
+ return path14.join(resolveTemplatesDir(), PROFILE_DIRS3[profile]);
27275
+ }
27276
+ function readBundledTemplateContent(repoPath, profile, file) {
27277
+ const templatePath = path14.join(boilerplateDirForProfile(profile), file);
27278
+ let content = fs15.readFileSync(templatePath, "utf8");
27279
+ if (file === "harness.env") {
27280
+ content = substituteProjectName2(content, projectNameFromRepo(repoPath));
27281
+ }
27282
+ return content;
27283
+ }
27284
+ function staleFileHint(file) {
27285
+ if (file.endsWith(".sh")) {
27286
+ return "Legacy harness script \u2014 merge behavior into current templates, then delete.";
27287
+ }
27288
+ if (file.endsWith(".local") || file.includes(".env")) {
27289
+ return "Local override \u2014 move needed values into harness.env or delete if obsolete.";
27290
+ }
27291
+ return "Not in the current bundled template \u2014 review, merge, or delete.";
27292
+ }
27293
+ function actionHint(file, kind2) {
27294
+ if (kind2 === "missing") {
27295
+ if (file === "provision-toolchain.sh") {
27296
+ return "Add this file and ensure launch.sh calls it to provision the toolchain.";
27297
+ }
27298
+ return `Copy/adapt from maintain/templates/${file} into .har/${file}.`;
27299
+ }
27300
+ if (file === "verify.sh") {
27301
+ return "Merge template upgrades into .har/verify.sh \u2014 do not blind-overwrite repo-specific checks.";
27302
+ }
27303
+ if (file === "harness.env") {
27304
+ return "Merge template changes; keep repo-specific commands and add any missing port vars.";
27305
+ }
27306
+ return `Read maintain/diffs/${file}.diff and merge into .har/${file}.`;
27307
+ }
27308
+ function createUnifiedDiff(installedContent, templateContent, installedLabel, templateLabel) {
27309
+ const tmpDir = fs15.mkdtempSync(path14.join(os2.tmpdir(), "har-maintain-diff-"));
27310
+ try {
27311
+ const installedPath = path14.join(tmpDir, "installed");
27312
+ const templatePath = path14.join(tmpDir, "template");
27313
+ fs15.writeFileSync(installedPath, installedContent);
27314
+ fs15.writeFileSync(templatePath, templateContent);
27315
+ const result = run(`diff -u "${installedPath}" "${templatePath}"`);
27316
+ const body = result.stdout.trim();
27317
+ if (!body) return "";
27318
+ return `--- ${installedLabel}
27319
+ +++ ${templateLabel}
27320
+ ${body.split("\n").slice(2).join("\n")}
27321
+ `;
27322
+ } finally {
27323
+ fs15.rmSync(tmpDir, { recursive: true, force: true });
27324
+ }
27325
+ }
27326
+ function buildActions(repoPath, profile, drift) {
27327
+ const harnessDir = getHarnessDir(repoPath);
27328
+ const actions = [];
27329
+ for (const file of drift.missing) {
27330
+ actions.push({
27331
+ file,
27332
+ kind: "missing",
27333
+ template: `maintain/templates/${file}`,
27334
+ hint: actionHint(file, "missing")
27335
+ });
27336
+ }
27337
+ for (const file of drift.checksumMismatch) {
27338
+ const installedPath = path14.join(harnessDir, file);
27339
+ const installedRel = `maintain/installed/${file}`;
27340
+ const templateRel = `maintain/templates/${file}`;
27341
+ const diffRel = `maintain/diffs/${file}.diff`;
27342
+ actions.push({
27343
+ file,
27344
+ kind: "drift",
27345
+ template: templateRel,
27346
+ installed: fs15.existsSync(installedPath) ? installedRel : void 0,
27347
+ diff: diffRel,
27348
+ hint: actionHint(file, "drift")
27349
+ });
27350
+ }
27351
+ return actions.sort((a2, b2) => a2.file.localeCompare(b2.file));
27352
+ }
27353
+ function buildStale(drift) {
27354
+ return drift.extra.map((file) => ({ file, hint: staleFileHint(file) })).sort((a2, b2) => a2.file.localeCompare(b2.file));
27355
+ }
27356
+ function buildReadme(report) {
27357
+ const lines = [
27358
+ "# Harness maintenance bundle",
27359
+ "",
27360
+ `Generated by \`har env maintain\` on ${report.generatedAt}.`,
27361
+ `Bundled generator: ${report.generatorVersion.bundled} | Installed: ${report.generatorVersion.installed ?? "(unknown)"} | Profile: ${report.profile}`,
27362
+ "",
27363
+ "Reference templates and diffs live in this directory. Edit live files in `.har/` \u2014 not `maintain/templates/` directly.",
27364
+ "",
27365
+ "## Validation (fix before `--finalize`)",
27366
+ ""
27367
+ ];
27368
+ if (report.validation.errors.length === 0 && report.validation.warnings.length === 0) {
27369
+ lines.push("- All structural checks passed.");
27370
+ } else {
27371
+ for (const issue2 of report.validation.errors) {
27372
+ lines.push(`- \u2717 **${issue2.file}**: ${issue2.message}`);
27373
+ }
27374
+ for (const issue2 of report.validation.warnings) {
27375
+ lines.push(`- \u26A0 **${issue2.file}**: ${issue2.message}`);
27376
+ }
27377
+ }
27378
+ lines.push("", "## Drift actions", "");
27379
+ if (report.actions.length === 0) {
27380
+ lines.push("No missing or drifted template files.");
27381
+ } else {
27382
+ lines.push("| File | Status | Reference |");
27383
+ lines.push("|------|--------|-----------|");
27384
+ for (const action of report.actions) {
27385
+ const ref = action.kind === "missing" ? `templates/${action.file}` : `diffs/${action.file}.diff`;
27386
+ lines.push(`| ${action.file} | ${action.kind} | ${ref} |`);
27387
+ }
27388
+ }
27389
+ if (report.missingPortVars.length > 0) {
27390
+ lines.push("", "## Missing port vars in harness.env", "");
27391
+ for (const v2 of report.missingPortVars) {
27392
+ lines.push(`- \`${v2}\` \u2014 copy from \`maintain/templates/harness.env\``);
27393
+ }
27394
+ }
27395
+ lines.push("", "## Stale files (review)", "");
27396
+ if (report.stale.length === 0) {
27397
+ lines.push("None detected.");
27398
+ } else {
27399
+ for (const stale of report.stale) {
27400
+ lines.push(`- **${stale.file}** \u2014 ${stale.hint}`);
27401
+ }
27402
+ }
27403
+ lines.push(
27404
+ "",
27405
+ "## Next steps",
27406
+ "",
27407
+ "1. Resolve each drift action (merge diffs; keep repo-specific customizations).",
27408
+ "2. Review stale files \u2014 delete or merge superseded scripts.",
27409
+ "3. Re-run `har env maintain` until validation passes and drift is clear.",
27410
+ '4. `har env maintain --finalize --summary "<what changed>"`',
27411
+ ""
27412
+ );
27413
+ return lines.join("\n");
27414
+ }
27415
+ function writeBundleArtifacts(repoPath, profile, drift, report) {
27416
+ const harnessDir = getHarnessDir(repoPath);
27417
+ const bundleDir = path14.join(harnessDir, MAINTAIN_DIR);
27418
+ if (fs15.existsSync(bundleDir)) {
27419
+ fs15.rmSync(bundleDir, { recursive: true, force: true });
27420
+ }
27421
+ const dirs = [
27422
+ bundleDir,
27423
+ path14.join(bundleDir, "templates"),
27424
+ path14.join(bundleDir, "installed"),
27425
+ path14.join(bundleDir, "diffs"),
27426
+ path14.join(bundleDir, "stale")
27427
+ ];
27428
+ for (const dir of dirs) {
27429
+ fs15.mkdirSync(dir, { recursive: true });
27430
+ }
27431
+ const affectedFiles = [...drift.missing, ...drift.checksumMismatch];
27432
+ for (const file of affectedFiles) {
27433
+ const templateContent = readBundledTemplateContent(repoPath, profile, file);
27434
+ writeFileSafe(path14.join(bundleDir, "templates", file), templateContent);
27435
+ const harnessPath = path14.join(harnessDir, file);
27436
+ if (fs15.existsSync(harnessPath)) {
27437
+ const installedContent = fs15.readFileSync(harnessPath, "utf8");
27438
+ writeFileSafe(path14.join(bundleDir, "installed", file), installedContent);
27439
+ const diff = createUnifiedDiff(
27440
+ installedContent,
27441
+ templateContent,
27442
+ `installed/${file}`,
27443
+ `templates/${file}`
27444
+ );
27445
+ if (diff) {
27446
+ writeFileSafe(path14.join(bundleDir, "diffs", `${file}.diff`), diff);
27447
+ }
27448
+ }
27449
+ }
27450
+ writeFileSafe(path14.join(bundleDir, "README.md"), buildReadme(report));
27451
+ writeFileSafe(path14.join(bundleDir, "drift-report.json"), JSON.stringify(report, null, 2) + "\n");
27452
+ writeFileSafe(
27453
+ path14.join(bundleDir, "validation.json"),
27454
+ JSON.stringify(report.validation, null, 2) + "\n"
27455
+ );
27456
+ if (report.stale.length > 0) {
27457
+ const staleLines = [
27458
+ "# Stale harness files",
27459
+ "",
27460
+ "These files exist in `.har/` but are not part of the current bundled template.",
27461
+ "",
27462
+ ...report.stale.map((s2) => `- **${s2.file}** \u2014 ${s2.hint}`),
27463
+ ""
27464
+ ];
27465
+ writeFileSafe(path14.join(bundleDir, "stale", "MANIFEST.md"), staleLines.join("\n"));
27466
+ }
27467
+ return bundleDir;
27468
+ }
27469
+ function buildMaintainBundle(repoPath, validation2, drift) {
27470
+ const manifest = readManifest(repoPath);
27471
+ const profile = manifest?.profile ?? "default";
27472
+ const errors = validation2.issues.filter((i2) => i2.severity === "error");
27473
+ const warnings = validation2.issues.filter((i2) => i2.severity === "warning");
27474
+ const report = {
27475
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
27476
+ generatorVersion: drift.generatorVersion,
27477
+ profile,
27478
+ actions: buildActions(repoPath, profile, drift),
27479
+ stale: buildStale(drift),
27480
+ missingPortVars: drift.missingPortVars,
27481
+ validation: {
27482
+ pass: validation2.pass,
27483
+ errors,
27484
+ warnings
27485
+ }
27486
+ };
27487
+ const bundleDir = writeBundleArtifacts(repoPath, profile, drift, report);
27488
+ return { bundleDir, report };
27489
+ }
27490
+ function removeMaintainBundle(repoPath) {
27491
+ const bundleDir = path14.join(getHarnessDir(repoPath), MAINTAIN_DIR);
27492
+ if (fs15.existsSync(bundleDir)) {
27493
+ fs15.rmSync(bundleDir, { recursive: true, force: true });
27494
+ }
27495
+ }
27496
+ function formatMaintainBundlePromptSection(report) {
27497
+ const lines = [
27498
+ "## Step 0 \u2014 Read the maintenance bundle",
27499
+ "",
27500
+ "Open `.har/maintain/README.md` and `.har/maintain/drift-report.json`.",
27501
+ "All reference templates are under `.har/maintain/templates/`.",
27502
+ "Do **not** read files from the globally installed har package.",
27503
+ ""
27504
+ ];
27505
+ if (report.validation.errors.length > 0) {
27506
+ lines.push("### Validation blockers (fix first)", "");
27507
+ for (const issue2 of report.validation.errors) {
27508
+ lines.push(`- \u2717 **${issue2.file}**: ${issue2.message}`);
27509
+ }
27510
+ lines.push("");
27511
+ }
27512
+ if (report.actions.length > 0) {
27513
+ lines.push("### Drift actions", "", "| File | Status | Reference |", "|------|--------|-----------|");
27514
+ for (const action of report.actions) {
27515
+ const ref = action.kind === "missing" ? `maintain/templates/${action.file}` : `maintain/diffs/${action.file}.diff`;
27516
+ lines.push(`| ${action.file} | ${action.kind} | ${ref} |`);
27517
+ }
27518
+ lines.push("");
27519
+ }
27520
+ if (report.stale.length > 0) {
27521
+ lines.push("### Stale files", "");
27522
+ for (const stale of report.stale) {
27523
+ lines.push(`- **${stale.file}** \u2014 ${stale.hint}`);
27524
+ }
27525
+ lines.push("");
27526
+ }
27527
+ if (report.missingPortVars.length > 0) {
27528
+ lines.push("### Missing port vars in harness.env", "");
27529
+ for (const v2 of report.missingPortVars) {
27530
+ lines.push(`- \`${v2}\` \u2014 see \`maintain/templates/harness.env\``);
27531
+ }
27532
+ lines.push("");
27533
+ }
27534
+ if (report.actions.length === 0 && report.stale.length === 0 && report.missingPortVars.length === 0 && report.validation.errors.length === 0) {
27535
+ lines.push("No template drift detected. Review repo stack changes manually if needed.", "");
27536
+ }
27537
+ return lines.join("\n");
27538
+ }
27539
+
27540
+ // src/harness/stage-templates.ts
27541
+ var fs17 = __toESM(require("fs"));
27542
+ var path15 = __toESM(require("path"));
27154
27543
 
27155
27544
  // src/harness/parser.ts
27156
- var fs14 = __toESM(require("fs"));
27545
+ var fs16 = __toESM(require("fs"));
27157
27546
  function harnessExists(repoPath) {
27158
- return fs14.existsSync(`${repoPath}/.har/setup-infra.sh`);
27547
+ return fs16.existsSync(`${repoPath}/.har/setup-infra.sh`);
27159
27548
  }
27160
27549
 
27161
27550
  // src/harness/stage-templates.ts
27162
27551
  function resolveTemplateDir(templateId) {
27163
- const dir = path13.join(resolveTemplatesDir(), "stage-templates", templateId);
27164
- if (!fs15.existsSync(dir)) {
27552
+ const dir = path15.join(resolveTemplatesDir(), "stage-templates", templateId);
27553
+ if (!fs17.existsSync(dir)) {
27165
27554
  throw new Error(`Stage template not found: ${templateId}. Run npm run build.`);
27166
27555
  }
27167
27556
  return dir;
27168
27557
  }
27169
27558
  function readTemplateManifest(templateId) {
27170
- const manifestPath = path13.join(resolveTemplateDir(templateId), "template.manifest.json");
27171
- const raw = JSON.parse(fs15.readFileSync(manifestPath, "utf8"));
27559
+ const manifestPath = path15.join(resolveTemplateDir(templateId), "template.manifest.json");
27560
+ const raw = JSON.parse(fs17.readFileSync(manifestPath, "utf8"));
27172
27561
  if (raw.id !== templateId) {
27173
27562
  throw new Error(`Template manifest id mismatch: expected ${templateId}, got ${raw.id}`);
27174
27563
  }
27175
27564
  return raw;
27176
27565
  }
27177
27566
  function ensureParentDir(filePath) {
27178
- const parent = path13.dirname(filePath);
27179
- if (!fs15.existsSync(parent)) {
27180
- fs15.mkdirSync(parent, { recursive: true });
27567
+ const parent = path15.dirname(filePath);
27568
+ if (!fs17.existsSync(parent)) {
27569
+ fs17.mkdirSync(parent, { recursive: true });
27181
27570
  }
27182
27571
  }
27183
27572
  function copyTemplateFile(templateDir, file, repoPath, force) {
27184
- const srcPath = path13.join(templateDir, file.src);
27185
- const destPath = path13.join(repoPath, file.dest);
27186
- if (!fs15.existsSync(srcPath)) {
27573
+ const srcPath = path15.join(templateDir, file.src);
27574
+ const destPath = path15.join(repoPath, file.dest);
27575
+ if (!fs17.existsSync(srcPath)) {
27187
27576
  throw new Error(`Template file missing: ${file.src}`);
27188
27577
  }
27189
- if (fs15.existsSync(destPath) && !force) {
27578
+ if (fs17.existsSync(destPath) && !force) {
27190
27579
  throw new Error(
27191
27580
  `File already exists: ${file.dest}. Use --force to overwrite or remove it first.`
27192
27581
  );
27193
27582
  }
27194
27583
  ensureParentDir(destPath);
27195
- fs15.copyFileSync(srcPath, destPath);
27584
+ fs17.copyFileSync(srcPath, destPath);
27196
27585
  if (file.executable) {
27197
- fs15.chmodSync(destPath, 493);
27586
+ fs17.chmodSync(destPath, 493);
27198
27587
  }
27199
27588
  return { written: true, path: file.dest };
27200
27589
  }
27201
27590
  function mergePackageJson(repoPath, templateDir, fragmentRelPath, warnings) {
27202
- const packagePath = path13.join(repoPath, "package.json");
27203
- if (!fs15.existsSync(packagePath)) {
27591
+ const packagePath = path15.join(repoPath, "package.json");
27592
+ if (!fs17.existsSync(packagePath)) {
27204
27593
  throw new Error("No package.json in repo root. Add one before applying the Playwright template.");
27205
27594
  }
27206
- const fragmentPath = path13.join(templateDir, fragmentRelPath);
27207
- if (!fs15.existsSync(fragmentPath)) {
27595
+ const fragmentPath = path15.join(templateDir, fragmentRelPath);
27596
+ if (!fs17.existsSync(fragmentPath)) {
27208
27597
  throw new Error(`Package fragment missing: ${fragmentRelPath}`);
27209
27598
  }
27210
- const pkg = JSON.parse(fs15.readFileSync(packagePath, "utf8"));
27211
- const fragment = JSON.parse(fs15.readFileSync(fragmentPath, "utf8"));
27599
+ const pkg = JSON.parse(fs17.readFileSync(packagePath, "utf8"));
27600
+ const fragment = JSON.parse(fs17.readFileSync(fragmentPath, "utf8"));
27212
27601
  for (const section of ["scripts", "devDependencies"]) {
27213
27602
  const existing = pkg[section] ?? {};
27214
27603
  const incoming = fragment[section] ?? {};
@@ -27221,7 +27610,7 @@ function mergePackageJson(repoPath, templateDir, fragmentRelPath, warnings) {
27221
27610
  }
27222
27611
  pkg[section] = existing;
27223
27612
  }
27224
- fs15.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + "\n");
27613
+ fs17.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + "\n");
27225
27614
  }
27226
27615
  function patchStageRegistry(repoPath, manifest, force) {
27227
27616
  const registry2 = readStageRegistry(repoPath);
@@ -27262,8 +27651,8 @@ function assertHarnessPresent(repoPath) {
27262
27651
  }
27263
27652
  function assertStageNotPresent(repoPath, stageId, force) {
27264
27653
  if (force) return;
27265
- const scriptPath = path13.join(repoPath, ".har", "stages", `${stageId}.sh`);
27266
- if (fs15.existsSync(scriptPath)) {
27654
+ const scriptPath = path15.join(repoPath, ".har", "stages", `${stageId}.sh`);
27655
+ if (fs17.existsSync(scriptPath)) {
27267
27656
  throw new Error(
27268
27657
  `Stage script already exists: .har/stages/${stageId}.sh. Use --force to overwrite.`
27269
27658
  );
@@ -27276,7 +27665,7 @@ function assertStageNotPresent(repoPath, stageId, force) {
27276
27665
  }
27277
27666
  }
27278
27667
  function applyStageTemplate(repoPath, templateId, options = {}) {
27279
- const resolved = path13.resolve(repoPath);
27668
+ const resolved = path15.resolve(repoPath);
27280
27669
  const force = options.force ?? false;
27281
27670
  const warnings = [];
27282
27671
  const filesWritten = [];
@@ -27295,7 +27684,7 @@ function applyStageTemplate(repoPath, templateId, options = {}) {
27295
27684
  if (file.skipFlag === "skipCi" && options.skipCi) {
27296
27685
  continue;
27297
27686
  }
27298
- if (fs15.existsSync(path13.join(resolved, file.dest)) && !force) {
27687
+ if (fs17.existsSync(path15.join(resolved, file.dest)) && !force) {
27299
27688
  warnings.push(`Skipped optional file (exists): ${file.dest}`);
27300
27689
  continue;
27301
27690
  }
@@ -27358,11 +27747,11 @@ function validateAgentId(id, repoPath) {
27358
27747
  // src/core/harness.ts
27359
27748
  function listHarnessScripts(repoPath) {
27360
27749
  const harnessDir = getHarnessDir(repoPath);
27361
- if (!fs16.existsSync(harnessDir)) return [];
27362
- return fs16.readdirSync(harnessDir).filter((name) => name.endsWith(".sh")).sort();
27750
+ if (!fs18.existsSync(harnessDir)) return [];
27751
+ return fs18.readdirSync(harnessDir).filter((name) => name.endsWith(".sh")).sort();
27363
27752
  }
27364
27753
  function describeProject(repoPath) {
27365
- const resolved = path14.resolve(repoPath);
27754
+ const resolved = path16.resolve(repoPath);
27366
27755
  const manifest = readManifest(resolved);
27367
27756
  const present = harnessExists(resolved);
27368
27757
  return {
@@ -27382,8 +27771,8 @@ function describeProject(repoPath) {
27382
27771
  };
27383
27772
  }
27384
27773
  async function initHarness(options) {
27385
- const repoPath = path14.resolve(options.repoPath);
27386
- if (!fs16.existsSync(repoPath)) {
27774
+ const repoPath = path16.resolve(options.repoPath);
27775
+ if (!fs18.existsSync(repoPath)) {
27387
27776
  throw new Error(`Path not found: ${repoPath}`);
27388
27777
  }
27389
27778
  const scaffold = scaffoldHarnessBoilerplate(repoPath, {
@@ -27414,9 +27803,9 @@ async function initHarness(options) {
27414
27803
  };
27415
27804
  }
27416
27805
  async function maintainHarness(options) {
27417
- const repoPath = path14.resolve(options.repoPath);
27806
+ const repoPath = path16.resolve(options.repoPath);
27418
27807
  const harnessDir = getHarnessDir(repoPath);
27419
- if (!fs16.existsSync(harnessDir)) {
27808
+ if (!fs18.existsSync(harnessDir)) {
27420
27809
  throw new Error('No .har/ found. Run "har env init" first.');
27421
27810
  }
27422
27811
  if (options.auto) {
@@ -27427,6 +27816,7 @@ async function maintainHarness(options) {
27427
27816
  mode: "maintain"
27428
27817
  });
27429
27818
  finalizeHarness(repoPath, authoringResult.summary, authoringResult.stack);
27819
+ removeMaintainBundle(repoPath);
27430
27820
  const validation3 = validateHarness(repoPath);
27431
27821
  return {
27432
27822
  validation: validation3,
@@ -27434,6 +27824,7 @@ async function maintainHarness(options) {
27434
27824
  drift: compareHarnessToTemplate(repoPath)
27435
27825
  };
27436
27826
  }
27827
+ const drift = compareHarnessToTemplate(repoPath);
27437
27828
  const validation2 = validateHarness(repoPath);
27438
27829
  if (options.finalize) {
27439
27830
  if (!validation2.pass) {
@@ -27445,16 +27836,19 @@ async function maintainHarness(options) {
27445
27836
  options.summary ?? "Manual adaptation finalized via har env maintain --finalize",
27446
27837
  existing?.stack
27447
27838
  );
27839
+ removeMaintainBundle(repoPath);
27448
27840
  return {
27449
27841
  validation: validation2,
27450
27842
  adaptationSummary: options.summary,
27451
27843
  drift: compareHarnessToTemplate(repoPath)
27452
27844
  };
27453
27845
  }
27846
+ const bundle = buildMaintainBundle(repoPath, validation2, drift);
27454
27847
  return {
27455
27848
  validation: validation2,
27456
27849
  adaptationSummary: "Manual maintenance \u2014 use coding agent prompt in .har/ADAPT-PROMPT.md",
27457
- drift: compareHarnessToTemplate(repoPath)
27850
+ drift,
27851
+ bundle
27458
27852
  };
27459
27853
  }
27460
27854
  function addStageTemplate(repoPath, templateId, options = {}) {
@@ -27462,20 +27856,20 @@ function addStageTemplate(repoPath, templateId, options = {}) {
27462
27856
  }
27463
27857
 
27464
27858
  // src/harness/adaptation-prompt.ts
27465
- var fs17 = __toESM(require("fs"));
27466
- var path15 = __toESM(require("path"));
27859
+ var fs19 = __toESM(require("fs"));
27860
+ var path17 = __toESM(require("path"));
27467
27861
  var ADAPTATION_PROMPT_FILE = "ADAPT-PROMPT.md";
27468
27862
  var PROFILE_HINTS = {
27469
- default: "Web app profile \u2014 Docker Compose for shared infra (HARNESS_INFRA_SERVICES), PM2 for the primary application only, git worktree per agent slot by default. Identify the primary app agents modify; run supporting services shared. Adapt docker-compose, setup-infra, and ecosystem templates for this stack.",
27470
- cli: "CLI/library profile \u2014 no PM2. Optional Docker Compose via the `HARNESS_INFRA_SERVICES` list in harness.env. Agents work in an isolated git worktree by default (`--no-worktree` to use the repo root). Remove any leftover PM2/ecosystem files; keep docker-compose when shared services are enabled.",
27471
- ios: "iOS mobile app profile \u2014 no PM2, no web ports. Agents build and test via xcodebuild in an isolated git worktree. Set HARNESS_XCODE_SCHEME, HARNESS_XCODE_WORKSPACE/PROJECT, HARNESS_SIMULATOR_NAME, and HARNESS_BUNDLE_ID in harness.env. Adapt setup-infra.sh to boot the target simulator. Install the RocketSim stage template (har env add-stage rocketsim) for user-flow validation."
27863
+ default: "Web app profile (SaaS/full-stack) \u2014 Docker Compose for shared infra (HARNESS_INFRA_SERVICES), PM2 for the primary application only, git worktree per agent slot by default. Launch provisions toolchain via harness.env (HARNESS_ECOSYSTEM, HARNESS_INSTALL_CMD) and writes paths to .env.agent.<id>. Identify the primary app agents modify; run supporting services shared.",
27864
+ cli: "CLI/library profile (typical SWE-bench) \u2014 no PM2. Optional Docker Compose via HARNESS_INFRA_SERVICES. Git worktree by default. Launch provisions toolchain declaratively (HARNESS_ECOSYSTEM auto-detects common ecosystems); verify must use resolved tool paths from .env.agent.<id>, never hardcoded interpreter or package-manager paths.",
27865
+ ios: "iOS mobile app profile \u2014 xcodebuild + iOS Simulator in an isolated git worktree. Set HARNESS_XCODE_SCHEME, workspace/project, HARNESS_SIMULATOR_NAME, HARNESS_BUNDLE_ID. Launch writes XCODEBUILD_BIN to .env.agent.<id>; verify uses it. Install RocketSim stage (har env add-stage rocketsim) for user-flow validation."
27472
27866
  };
27473
27867
  function loadTemplate(name) {
27474
27868
  const filePath = resolveTemplateFile(name);
27475
27869
  if (!filePath) {
27476
27870
  throw new Error(`Adaptation prompt template not found: ${name}. Run npm run build.`);
27477
27871
  }
27478
- return fs17.readFileSync(filePath, "utf8");
27872
+ return fs19.readFileSync(filePath, "utf8");
27479
27873
  }
27480
27874
  function applyProfilePlaceholders(content, profile) {
27481
27875
  return content.replace(/\{\{PROFILE\}\}/g, profile).replace(/\{\{PROFILE_HINT\}\}/g, PROFILE_HINTS[profile]);
@@ -27483,12 +27877,21 @@ function applyProfilePlaceholders(content, profile) {
27483
27877
  function buildInitAdaptationPrompt(_repoPath, profile) {
27484
27878
  return applyProfilePlaceholders(loadTemplate("adaptation-prompt-init.md"), profile);
27485
27879
  }
27486
- function buildMaintainAdaptationPrompt(_repoPath) {
27487
- return loadTemplate("adaptation-prompt-maintain.md");
27880
+ function buildMaintainAdaptationPrompt(_repoPath, bundleReport) {
27881
+ const template = loadTemplate("adaptation-prompt-maintain.md");
27882
+ const section = bundleReport ? formatMaintainBundlePromptSection(bundleReport) : [
27883
+ "## Step 0 \u2014 Read the maintenance bundle",
27884
+ "",
27885
+ "Open `.har/maintain/README.md` and `.har/maintain/drift-report.json`.",
27886
+ "All reference templates are under `.har/maintain/templates/`.",
27887
+ "Do **not** read files from the globally installed har package.",
27888
+ ""
27889
+ ].join("\n");
27890
+ return template.replace("{{MAINTAIN_BUNDLE_SECTION}}", section);
27488
27891
  }
27489
27892
  function writeAdaptationPrompt(repoPath, content) {
27490
27893
  const harnessDir = getHarnessDir(repoPath);
27491
- const filePath = path15.join(harnessDir, ADAPTATION_PROMPT_FILE);
27894
+ const filePath = path17.join(harnessDir, ADAPTATION_PROMPT_FILE);
27492
27895
  writeFileSafe(filePath, content);
27493
27896
  return filePath;
27494
27897
  }
@@ -27507,25 +27910,25 @@ function printAdaptationPrompt(content) {
27507
27910
  }
27508
27911
 
27509
27912
  // src/harness/cursor-rule.ts
27510
- var fs18 = __toESM(require("fs"));
27511
- var path16 = __toESM(require("path"));
27913
+ var fs20 = __toESM(require("fs"));
27914
+ var path18 = __toESM(require("path"));
27512
27915
  var readline3 = __toESM(require("readline"));
27513
27916
  var CURSOR_RULE_RELATIVE_PATH = ".cursor/rules/har-workflow.mdc";
27514
27917
  function getCursorRulePath(repoPath) {
27515
- return path16.join(repoPath, CURSOR_RULE_RELATIVE_PATH);
27918
+ return path18.join(repoPath, CURSOR_RULE_RELATIVE_PATH);
27516
27919
  }
27517
27920
  function isCursorWorkspace(repoPath) {
27518
- return fs18.existsSync(path16.join(repoPath, ".cursor"));
27921
+ return fs20.existsSync(path18.join(repoPath, ".cursor"));
27519
27922
  }
27520
27923
  function cursorRuleExists(repoPath) {
27521
- return fs18.existsSync(getCursorRulePath(repoPath));
27924
+ return fs20.existsSync(getCursorRulePath(repoPath));
27522
27925
  }
27523
27926
  function scaffoldCursorRule(repoPath) {
27524
27927
  const templatePath = resolveTemplateFile("cursor-rule.mdc.template");
27525
27928
  if (!templatePath) {
27526
27929
  throw new Error("Cursor rule template not found (cursor-rule.mdc.template). Run npm run build.");
27527
27930
  }
27528
- const content = fs18.readFileSync(templatePath, "utf8");
27931
+ const content = fs20.readFileSync(templatePath, "utf8");
27529
27932
  writeFileSafe(getCursorRulePath(repoPath), content);
27530
27933
  }
27531
27934
  async function handleCursorRule(options) {
@@ -27563,16 +27966,16 @@ async function promptScaffoldCursorRule(exists) {
27563
27966
  }
27564
27967
  async function askYesNo2(question) {
27565
27968
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
27566
- return new Promise((resolve24) => {
27969
+ return new Promise((resolve26) => {
27567
27970
  process.stderr.write(`${question} `);
27568
27971
  rl.once("line", (answer) => {
27569
27972
  rl.close();
27570
27973
  const trimmed = answer.trim();
27571
27974
  if (trimmed === "") {
27572
- resolve24(true);
27975
+ resolve26(true);
27573
27976
  return;
27574
27977
  }
27575
- resolve24(/^y(es)?$/i.test(trimmed));
27978
+ resolve26(/^y(es)?$/i.test(trimmed));
27576
27979
  });
27577
27980
  });
27578
27981
  }
@@ -27651,17 +28054,65 @@ function buildStageResult(input) {
27651
28054
  }
27652
28055
 
27653
28056
  // src/core/local-executor.ts
27654
- var fs19 = __toESM(require("fs"));
27655
- var path17 = __toESM(require("path"));
28057
+ var fs22 = __toESM(require("fs"));
28058
+ var path20 = __toESM(require("path"));
28059
+
28060
+ // src/core/slot-registry.ts
28061
+ var fs21 = __toESM(require("fs"));
28062
+ var path19 = __toESM(require("path"));
28063
+ function getSlotRegistryDir(repoPath) {
28064
+ return path19.join(getHarnessDir(repoPath), "slots");
28065
+ }
28066
+ function getSlotRegistryPath(repoPath, agentId) {
28067
+ return path19.join(getSlotRegistryDir(repoPath), `agent-${agentId}.json`);
28068
+ }
28069
+ function isSlotResumable(session) {
28070
+ return session?.status === "failed" || session?.status === "starting";
28071
+ }
28072
+ function readSlotRegistry(repoPath, agentId) {
28073
+ const file = getSlotRegistryPath(repoPath, agentId);
28074
+ if (!fs21.existsSync(file)) return void 0;
28075
+ try {
28076
+ const raw = JSON.parse(fs21.readFileSync(file, "utf8"));
28077
+ const result = SlotRegistryEntrySchema.safeParse(raw);
28078
+ return result.success ? result.data : void 0;
28079
+ } catch {
28080
+ return void 0;
28081
+ }
28082
+ }
28083
+
28084
+ // src/core/local-executor.ts
27656
28085
  function resolveRepoPath(repoPath) {
27657
- return path17.resolve(repoPath);
28086
+ return path20.resolve(repoPath);
28087
+ }
28088
+ function buildPreviewUrlsFromPorts(ports, env3) {
28089
+ const urls = {};
28090
+ if (ports.frontend) urls.frontend = `http://localhost:${ports.frontend}`;
28091
+ if (ports.api) {
28092
+ urls.api = `http://localhost:${ports.api}`;
28093
+ if (env3.HARNESS_HEALTH_CHECK_PATH) {
28094
+ urls.health = `http://localhost:${ports.api}${env3.HARNESS_HEALTH_CHECK_PATH}`;
28095
+ }
28096
+ }
28097
+ if (ports.browser) urls.browser = `http://localhost:${ports.browser}`;
28098
+ if (ports.mailpit) urls.mailpit = `http://localhost:${ports.mailpit}`;
28099
+ return urls;
27658
28100
  }
27659
28101
  function computePreviewUrls(repoPath, agentId) {
27660
- const env3 = readHarnessEnv(repoPath);
28102
+ const harnessRoot = resolveHarnessRoot(repoPath);
28103
+ const session = readSlotRegistry(harnessRoot, agentId);
28104
+ if (session?.previewUrls && Object.keys(session.previewUrls).length > 0) {
28105
+ return session.previewUrls;
28106
+ }
28107
+ const env3 = readHarnessEnv(harnessRoot);
28108
+ if (session?.ports && Object.keys(session.ports).length > 0) {
28109
+ return buildPreviewUrlsFromPorts(session.ports, env3);
28110
+ }
28111
+ const step = Number(env3.HARNESS_PORT_STEP ?? 10);
27661
28112
  const feBase = Number(env3.HARNESS_FE_BASE_PORT ?? 3e3);
27662
28113
  const apiBase = Number(env3.HARNESS_API_BASE_PORT ?? 8e3);
27663
- const fePort = feBase + agentId * 10;
27664
- const apiPort = apiBase + agentId * 10;
28114
+ const fePort = feBase + agentId * step;
28115
+ const apiPort = apiBase + agentId * step;
27665
28116
  const urls = {
27666
28117
  frontend: `http://localhost:${fePort}`,
27667
28118
  api: `http://localhost:${apiPort}`
@@ -27670,10 +28121,10 @@ function computePreviewUrls(repoPath, agentId) {
27670
28121
  urls.health = `http://localhost:${apiPort}${env3.HARNESS_HEALTH_CHECK_PATH}`;
27671
28122
  }
27672
28123
  if (env3.HARNESS_INFRA_BROWSER === "true") {
27673
- urls.browser = "http://localhost:13001";
28124
+ urls.browser = `http://localhost:${env3.HARNESS_BROWSER_PORT_DEFAULT ?? 13001}`;
27674
28125
  }
27675
28126
  if (env3.HARNESS_INFRA_MAILPIT === "true") {
27676
- urls.mailpit = "http://localhost:18025";
28127
+ urls.mailpit = `http://localhost:${env3.HARNESS_MAILPIT_WEB_PORT_DEFAULT ?? 18025}`;
27677
28128
  }
27678
28129
  return urls;
27679
28130
  }
@@ -27684,14 +28135,14 @@ function substituteAgentId(value, agentId) {
27684
28135
  function resolveStageScriptPath(repoPath, stage) {
27685
28136
  const harnessDir = getHarnessDir(repoPath);
27686
28137
  if (stage.script) {
27687
- return path17.join(harnessDir, stage.script);
28138
+ return path20.join(harnessDir, stage.script);
27688
28139
  }
27689
28140
  if (stage.command) {
27690
28141
  const scriptName = stage.command.split(/\s+/)[0].replace(/^\.\/\.har\//, "").replace(/^\.\//, "");
27691
- return path17.join(harnessDir, scriptName);
28142
+ return path20.join(harnessDir, scriptName);
27692
28143
  }
27693
- const stageScript = path17.join(harnessDir, "stages", `${stage.id}.sh`);
27694
- if (fs19.existsSync(stageScript)) {
28144
+ const stageScript = path20.join(harnessDir, "stages", `${stage.id}.sh`);
28145
+ if (fs22.existsSync(stageScript)) {
27695
28146
  return stageScript;
27696
28147
  }
27697
28148
  throw new Error(
@@ -27706,7 +28157,7 @@ function buildExecutionPlan(repoPath, stage, options) {
27706
28157
  ...harnessEnv,
27707
28158
  ...stage.env ?? {}
27708
28159
  };
27709
- const cwd = stage.cwd ? path17.resolve(resolvedRepo, stage.cwd) : resolvedRepo;
28160
+ const cwd = stage.cwd ? path20.resolve(resolvedRepo, stage.cwd) : resolvedRepo;
27710
28161
  const extraArgs = options.args ?? [];
27711
28162
  const launchFlagArgs = [];
27712
28163
  if (stage.kind === "launch" && options.launchFlags) {
@@ -27714,6 +28165,7 @@ function buildExecutionPlan(repoPath, stage, options) {
27714
28165
  if (options.launchFlags.claude) launchFlagArgs.push("--claude");
27715
28166
  if (options.launchFlags.confirmReplace) launchFlagArgs.push("--replace");
27716
28167
  if (options.launchFlags.force) launchFlagArgs.push("--force");
28168
+ if (options.launchFlags.resume) launchFlagArgs.push("--resume");
27717
28169
  }
27718
28170
  if (stage.command) {
27719
28171
  let shellCommand = substituteAgentId(stage.command, options.agentId);
@@ -27782,19 +28234,19 @@ var LocalScriptExecutor = class {
27782
28234
  const repoPath = resolveRepoPath(ctx.repoPath);
27783
28235
  const harnessDir = getHarnessDir(repoPath);
27784
28236
  const artifactsDirName = getArtifactsDir(repoPath);
27785
- const artifactsDir = path17.join(harnessDir, artifactsDirName);
27786
- if (!fs19.existsSync(artifactsDir)) return [];
28237
+ const artifactsDir = path20.join(harnessDir, artifactsDirName);
28238
+ if (!fs22.existsSync(artifactsDir)) return [];
27787
28239
  const entries = [];
27788
28240
  const walk = (dir, prefix) => {
27789
- for (const entry of fs19.readdirSync(dir, { withFileTypes: true })) {
27790
- const full = path17.join(dir, entry.name);
27791
- const relative2 = path17.join(prefix, entry.name);
28241
+ for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
28242
+ const full = path20.join(dir, entry.name);
28243
+ const relative2 = path20.join(prefix, entry.name);
27792
28244
  if (entry.isDirectory()) {
27793
28245
  walk(full, relative2);
27794
28246
  continue;
27795
28247
  }
27796
28248
  if (filter?.stageId && !relative2.includes(filter.stageId)) continue;
27797
- const stat = fs19.statSync(full);
28249
+ const stat = fs22.statSync(full);
27798
28250
  entries.push({
27799
28251
  path: full,
27800
28252
  relativePath: relative2,
@@ -27810,7 +28262,7 @@ var LocalScriptExecutor = class {
27810
28262
  var localScriptExecutor = new LocalScriptExecutor();
27811
28263
 
27812
28264
  // src/core/control-sync.ts
27813
- var path24 = __toESM(require("path"));
28265
+ var path30 = __toESM(require("path"));
27814
28266
 
27815
28267
  // src/core/control-config.ts
27816
28268
  var DEFAULT_CONTROL_API_URL = "http://localhost:3847";
@@ -27822,32 +28274,32 @@ function isControlEnabled() {
27822
28274
  }
27823
28275
 
27824
28276
  // src/core/control-registry.ts
27825
- var fs20 = __toESM(require("fs"));
27826
- var os2 = __toESM(require("os"));
27827
- var path18 = __toESM(require("path"));
28277
+ var fs23 = __toESM(require("fs"));
28278
+ var os3 = __toESM(require("os"));
28279
+ var path21 = __toESM(require("path"));
27828
28280
  function getRegistryPath() {
27829
28281
  if (process.env.HAR_CONTROL_REGISTRY_PATH) {
27830
- return path18.resolve(process.env.HAR_CONTROL_REGISTRY_PATH);
28282
+ return path21.resolve(process.env.HAR_CONTROL_REGISTRY_PATH);
27831
28283
  }
27832
- return path18.join(os2.homedir(), ".har", "repos.json");
28284
+ return path21.join(os3.homedir(), ".har", "repos.json");
27833
28285
  }
27834
28286
  function readRegistry() {
27835
28287
  const registryPath = getRegistryPath();
27836
28288
  try {
27837
- if (!fs20.existsSync(registryPath)) return { repos: [] };
27838
- return JSON.parse(fs20.readFileSync(registryPath, "utf8"));
28289
+ if (!fs23.existsSync(registryPath)) return { repos: [] };
28290
+ return JSON.parse(fs23.readFileSync(registryPath, "utf8"));
27839
28291
  } catch {
27840
28292
  return { repos: [] };
27841
28293
  }
27842
28294
  }
27843
28295
  function writeRegistry(registry2) {
27844
28296
  const registryPath = getRegistryPath();
27845
- fs20.mkdirSync(path18.dirname(registryPath), { recursive: true });
27846
- fs20.writeFileSync(registryPath, JSON.stringify(registry2, null, 2) + "\n");
28297
+ fs23.mkdirSync(path21.dirname(registryPath), { recursive: true });
28298
+ fs23.writeFileSync(registryPath, JSON.stringify(registry2, null, 2) + "\n");
27847
28299
  }
27848
28300
  function recordRepoForControlSync(repoPath) {
27849
28301
  if (process.env.HAR_CONTROL_DISABLED === "true") return;
27850
- const resolved = path18.resolve(repoPath);
28302
+ const resolved = path21.resolve(repoPath);
27851
28303
  if (!readManifest(resolved)) return;
27852
28304
  const registry2 = readRegistry();
27853
28305
  if (registry2.repos.includes(resolved)) return;
@@ -27856,7 +28308,7 @@ function recordRepoForControlSync(repoPath) {
27856
28308
  }
27857
28309
  function listRegisteredRepos() {
27858
28310
  const registry2 = readRegistry();
27859
- const kept = registry2.repos.filter((repoPath) => fs20.existsSync(repoPath) && readManifest(repoPath));
28311
+ const kept = registry2.repos.filter((repoPath) => fs23.existsSync(repoPath) && readManifest(repoPath));
27860
28312
  if (kept.length !== registry2.repos.length) {
27861
28313
  writeRegistry({ repos: kept });
27862
28314
  }
@@ -27864,40 +28316,17 @@ function listRegisteredRepos() {
27864
28316
  }
27865
28317
 
27866
28318
  // src/core/slot-status.ts
27867
- var fs23 = __toESM(require("fs"));
27868
- var os4 = __toESM(require("os"));
27869
- var path21 = __toESM(require("path"));
27870
- var import_child_process3 = require("child_process");
28319
+ var fs28 = __toESM(require("fs"));
28320
+ var os6 = __toESM(require("os"));
28321
+ var path27 = __toESM(require("path"));
28322
+ var import_child_process7 = require("child_process");
27871
28323
 
27872
28324
  // src/core/runs.ts
27873
28325
  var crypto2 = __toESM(require("crypto"));
27874
28326
  var import_child_process2 = require("child_process");
27875
- var fs22 = __toESM(require("fs"));
27876
- var os3 = __toESM(require("os"));
27877
- var path20 = __toESM(require("path"));
27878
-
27879
- // src/core/slot-registry.ts
27880
- var fs21 = __toESM(require("fs"));
27881
- var path19 = __toESM(require("path"));
27882
- function getSlotRegistryDir(repoPath) {
27883
- return path19.join(getHarnessDir(repoPath), "slots");
27884
- }
27885
- function getSlotRegistryPath(repoPath, agentId) {
27886
- return path19.join(getSlotRegistryDir(repoPath), `agent-${agentId}.json`);
27887
- }
27888
- function readSlotRegistry(repoPath, agentId) {
27889
- const file = getSlotRegistryPath(repoPath, agentId);
27890
- if (!fs21.existsSync(file)) return void 0;
27891
- try {
27892
- const raw = JSON.parse(fs21.readFileSync(file, "utf8"));
27893
- const result = SlotRegistryEntrySchema.safeParse(raw);
27894
- return result.success ? result.data : void 0;
27895
- } catch {
27896
- return void 0;
27897
- }
27898
- }
27899
-
27900
- // src/core/runs.ts
28327
+ var fs24 = __toESM(require("fs"));
28328
+ var os4 = __toESM(require("os"));
28329
+ var path22 = __toESM(require("path"));
27901
28330
  var RUNS_DIR = "runs";
27902
28331
  function gitCommonDir(cwd) {
27903
28332
  try {
@@ -27906,7 +28335,7 @@ function gitCommonDir(cwd) {
27906
28335
  encoding: "utf8",
27907
28336
  stdio: ["pipe", "pipe", "ignore"]
27908
28337
  }).trim();
27909
- return out ? path20.resolve(cwd, out) : void 0;
28338
+ return out ? path22.resolve(cwd, out) : void 0;
27910
28339
  } catch {
27911
28340
  return void 0;
27912
28341
  }
@@ -27928,7 +28357,7 @@ function gitPrefix(cwd) {
27928
28357
  }
27929
28358
  }
27930
28359
  function getRunsDir(harnessRoot) {
27931
- return path20.join(getHarnessDir(harnessRoot), RUNS_DIR);
28360
+ return path22.join(getHarnessDir(harnessRoot), RUNS_DIR);
27932
28361
  }
27933
28362
  function formatLocalDate(d2) {
27934
28363
  const y2 = d2.getFullYear();
@@ -27948,24 +28377,24 @@ function buildRunRelativePath(stageId, agentId, startedAt, runId, runsDir) {
27948
28377
  const timePart = formatLocalTime(started);
27949
28378
  const agentPart = agentId !== void 0 ? `_agent-${agentId}` : "";
27950
28379
  let filename = `${timePart}_${stageId}${agentPart}.json`;
27951
- const fullPath = path20.join(runsDir, dateFolder, filename);
27952
- if (fs22.existsSync(fullPath)) {
28380
+ const fullPath = path22.join(runsDir, dateFolder, filename);
28381
+ if (fs24.existsSync(fullPath)) {
27953
28382
  filename = `${timePart}_${stageId}${agentPart}-${runId.slice(0, 8)}.json`;
27954
28383
  }
27955
- return path20.join(dateFolder, filename);
28384
+ return path22.join(dateFolder, filename);
27956
28385
  }
27957
28386
  function resolveRunFilePath(harnessRoot, run2) {
27958
28387
  if (run2.relativePath) {
27959
- return path20.join(getRunsDir(harnessRoot), run2.relativePath);
28388
+ return path22.join(getRunsDir(harnessRoot), run2.relativePath);
27960
28389
  }
27961
- return path20.join(getRunsDir(harnessRoot), `${run2.runId}.json`);
28390
+ return path22.join(getRunsDir(harnessRoot), `${run2.runId}.json`);
27962
28391
  }
27963
28392
  function collectRunFiles(runsDir) {
27964
- if (!fs22.existsSync(runsDir)) return [];
28393
+ if (!fs24.existsSync(runsDir)) return [];
27965
28394
  const files = [];
27966
28395
  const walk = (dir) => {
27967
- for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
27968
- const full = path20.join(dir, entry.name);
28396
+ for (const entry of fs24.readdirSync(dir, { withFileTypes: true })) {
28397
+ const full = path22.join(dir, entry.name);
27969
28398
  if (entry.isDirectory()) {
27970
28399
  walk(full);
27971
28400
  } else if (entry.name.endsWith(".json")) {
@@ -27979,30 +28408,30 @@ function collectRunFiles(runsDir) {
27979
28408
  function resolveAgentWorkDir(harnessRoot, agentId) {
27980
28409
  if (agentId === void 0) return void 0;
27981
28410
  const entry = readSlotRegistry(harnessRoot, agentId);
27982
- if (entry?.workDir && fs22.existsSync(entry.workDir)) return entry.workDir;
28411
+ if (entry?.workDir && fs24.existsSync(entry.workDir)) return entry.workDir;
27983
28412
  const env3 = readHarnessEnv(harnessRoot);
27984
- const projectName = env3.HARNESS_PROJECT_NAME ?? path20.basename(harnessRoot);
27985
- const worktreeDir = path20.join(os3.homedir(), "worktrees", `${projectName}-agent-${agentId}`);
28413
+ const projectName = env3.HARNESS_PROJECT_NAME ?? path22.basename(harnessRoot);
28414
+ const worktreeDir = path22.join(os4.homedir(), "worktrees", `${projectName}-agent-${agentId}`);
27986
28415
  const relPrefix = gitPrefix(harnessRoot);
27987
28416
  const sessionEnvFiles = [];
27988
- const worktreesRoot = path20.join(os3.homedir(), "worktrees");
27989
- if (fs22.existsSync(worktreesRoot)) {
28417
+ const worktreesRoot = path22.join(os4.homedir(), "worktrees");
28418
+ if (fs24.existsSync(worktreesRoot)) {
27990
28419
  const suffix = `-har-agent-${agentId}-`;
27991
- for (const entry2 of fs22.readdirSync(worktreesRoot, { withFileTypes: true })) {
28420
+ for (const entry2 of fs24.readdirSync(worktreesRoot, { withFileTypes: true })) {
27992
28421
  if (!entry2.isDirectory() || !entry2.name.includes(suffix)) continue;
27993
- const sessionDir = path20.join(worktreesRoot, entry2.name);
28422
+ const sessionDir = path22.join(worktreesRoot, entry2.name);
27994
28423
  if (!sameGitCheckout(harnessRoot, sessionDir)) continue;
27995
- sessionEnvFiles.push(path20.join(sessionDir, relPrefix, `.env.agent.${agentId}`));
28424
+ sessionEnvFiles.push(path22.join(sessionDir, relPrefix, `.env.agent.${agentId}`));
27996
28425
  }
27997
28426
  }
27998
28427
  const candidates = [
27999
- path20.join(worktreeDir, `.env.agent.${agentId}`),
28000
- path20.join(harnessRoot, `.env.agent.${agentId}`),
28428
+ path22.join(worktreeDir, `.env.agent.${agentId}`),
28429
+ path22.join(harnessRoot, `.env.agent.${agentId}`),
28001
28430
  ...sessionEnvFiles.sort()
28002
28431
  ];
28003
28432
  for (const envFile of candidates) {
28004
- if (!fs22.existsSync(envFile)) continue;
28005
- const content = fs22.readFileSync(envFile, "utf8");
28433
+ if (!fs24.existsSync(envFile)) continue;
28434
+ const content = fs24.readFileSync(envFile, "utf8");
28006
28435
  const match = content.match(/^REPO_ROOT=(.+)$/m);
28007
28436
  if (match) return match[1].trim();
28008
28437
  }
@@ -28014,11 +28443,11 @@ function createRun(ctx, meta) {
28014
28443
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
28015
28444
  const runId = crypto2.randomUUID();
28016
28445
  const relativePath = buildRunRelativePath(meta.stageId, meta.agentId, startedAt, runId, runsDir);
28017
- const runFilePath = path20.join(runsDir, relativePath);
28018
- fs22.mkdirSync(path20.dirname(runFilePath), { recursive: true });
28446
+ const runFilePath = path22.join(runsDir, relativePath);
28447
+ fs24.mkdirSync(path22.dirname(runFilePath), { recursive: true });
28019
28448
  const run2 = RunRecordSchema.parse({
28020
28449
  runId,
28021
- repoPath: path20.resolve(ctx.repoPath),
28450
+ repoPath: path22.resolve(ctx.repoPath),
28022
28451
  harnessRoot,
28023
28452
  stageId: meta.stageId,
28024
28453
  kind: meta.kind,
@@ -28029,7 +28458,7 @@ function createRun(ctx, meta) {
28029
28458
  relativePath,
28030
28459
  trigger: ctx.trigger ?? "cli"
28031
28460
  });
28032
- fs22.writeFileSync(runFilePath, JSON.stringify(run2, null, 2) + "\n");
28461
+ fs24.writeFileSync(runFilePath, JSON.stringify(run2, null, 2) + "\n");
28033
28462
  return run2;
28034
28463
  }
28035
28464
  function finishRun(repoPath, runId, update) {
@@ -28048,13 +28477,13 @@ function finishRun(repoPath, runId, update) {
28048
28477
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
28049
28478
  });
28050
28479
  const runPath = resolveRunFilePath(harnessRoot, finished);
28051
- fs22.writeFileSync(runPath, JSON.stringify(finished, null, 2) + "\n");
28480
+ fs24.writeFileSync(runPath, JSON.stringify(finished, null, 2) + "\n");
28052
28481
  return finished;
28053
28482
  }
28054
28483
  function findRunRecord(harnessRoot, runId) {
28055
28484
  const runsDir = getRunsDir(harnessRoot);
28056
28485
  for (const filePath of collectRunFiles(runsDir)) {
28057
- const parsed = RunRecordSchema.safeParse(JSON.parse(fs22.readFileSync(filePath, "utf8")));
28486
+ const parsed = RunRecordSchema.safeParse(JSON.parse(fs24.readFileSync(filePath, "utf8")));
28058
28487
  if (parsed.success && parsed.data.runId === runId) {
28059
28488
  return parsed.data;
28060
28489
  }
@@ -28066,10 +28495,10 @@ function getRun(repoPath, runId) {
28066
28495
  }
28067
28496
  function listRuns(repoPath, filter = {}) {
28068
28497
  const runsDir = getRunsDir(resolveHarnessRoot(repoPath));
28069
- if (!fs22.existsSync(runsDir)) return [];
28498
+ if (!fs24.existsSync(runsDir)) return [];
28070
28499
  const runs = [];
28071
28500
  for (const filePath of collectRunFiles(runsDir)) {
28072
- const parsed = RunRecordSchema.safeParse(JSON.parse(fs22.readFileSync(filePath, "utf8")));
28501
+ const parsed = RunRecordSchema.safeParse(JSON.parse(fs24.readFileSync(filePath, "utf8")));
28073
28502
  if (!parsed.success) continue;
28074
28503
  if (filter.stageId && parsed.data.stageId !== filter.stageId) continue;
28075
28504
  runs.push(parsed.data);
@@ -28081,11 +28510,552 @@ function listRuns(repoPath, filter = {}) {
28081
28510
  return runs;
28082
28511
  }
28083
28512
 
28513
+ // src/core/slot-preflight.ts
28514
+ var path26 = __toESM(require("path"));
28515
+
28516
+ // src/core/control-port.ts
28517
+ var import_child_process4 = require("child_process");
28518
+ var fs26 = __toESM(require("fs"));
28519
+ var path24 = __toESM(require("path"));
28520
+
28521
+ // src/core/slot-ports.ts
28522
+ var import_child_process3 = require("child_process");
28523
+ var fs25 = __toESM(require("fs"));
28524
+ var path23 = __toESM(require("path"));
28525
+ function portStep(env3) {
28526
+ return Number(env3.HARNESS_PORT_STEP ?? 10);
28527
+ }
28528
+ function defaultAppPort(base, agentId, step) {
28529
+ return base + agentId * step;
28530
+ }
28531
+ function slotPortLaneEnd(defaultPort, step) {
28532
+ return defaultPort + step - 1;
28533
+ }
28534
+ function isPortInUse(port, host = "127.0.0.1") {
28535
+ try {
28536
+ (0, import_child_process3.execSync)(`bash -c 'exec 3<>/dev/tcp/${host}/${port}'`, { stdio: "ignore" });
28537
+ return true;
28538
+ } catch {
28539
+ return false;
28540
+ }
28541
+ }
28542
+ function pickFreePort(start, end) {
28543
+ for (let port = start; port <= end; port++) {
28544
+ if (!isPortInUse(port)) return port;
28545
+ }
28546
+ return void 0;
28547
+ }
28548
+ function allocatePort(defaultPort, scanStart, scanEnd) {
28549
+ if (!isPortInUse(defaultPort)) return defaultPort;
28550
+ return pickFreePort(scanStart, scanEnd);
28551
+ }
28552
+ function allocateAppPorts(repoPath, agentId) {
28553
+ const harnessRoot = resolveHarnessRoot(repoPath);
28554
+ const env3 = readHarnessEnv(harnessRoot);
28555
+ const step = portStep(env3);
28556
+ const feDefault = defaultAppPort(Number(env3.HARNESS_FE_BASE_PORT ?? 3e3), agentId, step);
28557
+ const apiDefault = defaultAppPort(Number(env3.HARNESS_API_BASE_PORT ?? 8e3), agentId, step);
28558
+ const debugDefault = 9200 + agentId * step;
28559
+ const frontend = allocatePort(feDefault, feDefault, slotPortLaneEnd(feDefault, step));
28560
+ const api = allocatePort(apiDefault, apiDefault, slotPortLaneEnd(apiDefault, step));
28561
+ const debug2 = allocatePort(debugDefault, debugDefault, debugDefault + step - 1);
28562
+ if (frontend === void 0) {
28563
+ return { error: `No free frontend port in range ${feDefault}-${slotPortLaneEnd(feDefault, step)}` };
28564
+ }
28565
+ if (api === void 0) {
28566
+ return { error: `No free API port in range ${apiDefault}-${slotPortLaneEnd(apiDefault, step)}` };
28567
+ }
28568
+ if (debug2 === void 0) {
28569
+ return { error: `No free debug port in range ${debugDefault}-${debugDefault + step - 1}` };
28570
+ }
28571
+ return {
28572
+ frontend,
28573
+ api,
28574
+ debug: debug2,
28575
+ allocated: frontend !== feDefault || api !== apiDefault || debug2 !== debugDefault
28576
+ };
28577
+ }
28578
+ function harnessUsesPm2(repoPath) {
28579
+ const harnessRoot = resolveHarnessRoot(repoPath);
28580
+ return fs25.existsSync(path23.join(harnessRoot, ".har", "ecosystem.agent.template.cjs"));
28581
+ }
28582
+
28583
+ // src/core/control-port.ts
28584
+ function parseControlHostPort(apiUrl = getControlApiUrl()) {
28585
+ try {
28586
+ const url = new URL(apiUrl);
28587
+ if (url.port) return Number(url.port);
28588
+ return url.protocol === "https:" ? 443 : 80;
28589
+ } catch {
28590
+ return 3847;
28591
+ }
28592
+ }
28593
+ function portPublishedInDocker(ports, port) {
28594
+ const re2 = new RegExp(`:${port}->|:${port}/`);
28595
+ return re2.test(ports) || ports.includes(`0.0.0.0:${port}`);
28596
+ }
28597
+ function dockerOnPort(containers, port) {
28598
+ return containers.find((c2) => portPublishedInDocker(c2.ports, port));
28599
+ }
28600
+ function controlContainerOnPort(containers, port) {
28601
+ return containers.find(
28602
+ (c2) => /control/i.test(c2.name) && portPublishedInDocker(c2.ports, port)
28603
+ );
28604
+ }
28605
+ function listDockerContainers() {
28606
+ try {
28607
+ const raw = (0, import_child_process4.execSync)("docker ps --format {{.Names}}\\t{{.Ports}}", {
28608
+ encoding: "utf8",
28609
+ stdio: ["pipe", "pipe", "ignore"],
28610
+ timeout: 3e3
28611
+ });
28612
+ return raw.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
28613
+ const tab = line.indexOf(" ");
28614
+ if (tab === -1) return { name: line, ports: "" };
28615
+ return { name: line.slice(0, tab), ports: line.slice(tab + 1) };
28616
+ });
28617
+ } catch {
28618
+ return [];
28619
+ }
28620
+ }
28621
+ function formatControlPortBlocker(containerName, port, label) {
28622
+ const suffix = label ? ` (${label})` : "";
28623
+ return `har control up (container "${containerName}") occupies port ${port}${suffix}.`;
28624
+ }
28625
+ function formatControlDefaultPortWarning(containerName, defaultPort, allocatedPort) {
28626
+ return `har control up holds port ${defaultPort} (container "${containerName}"). Harness will use port ${allocatedPort} instead. Run har control down to reclaim the default port.`;
28627
+ }
28628
+ function controlDefaultPortWarnings(containers, defaultFrontendPort, allocatedFrontend) {
28629
+ if (allocatedFrontend === defaultFrontendPort) return [];
28630
+ const control = controlContainerOnPort(containers, defaultFrontendPort);
28631
+ if (!control) return [];
28632
+ return [formatControlDefaultPortWarning(control.name, defaultFrontendPort, allocatedFrontend)];
28633
+ }
28634
+ function inspectControlUpReadiness(repoPath) {
28635
+ const resolved = path24.resolve(repoPath);
28636
+ const port = parseControlHostPort();
28637
+ const containers = listDockerContainers();
28638
+ const control = controlContainerOnPort(containers, port);
28639
+ const warnings = [];
28640
+ const controlHarnessDir = path24.join(resolved, "control", ".har");
28641
+ const harnessSlot1Active = fs26.existsSync(controlHarnessDir) && readSlotRegistry(path24.join(resolved, "control"), 1)?.status === "active";
28642
+ if (control) {
28643
+ return {
28644
+ warnings,
28645
+ portInUse: true,
28646
+ controlAlreadyRunning: true,
28647
+ harnessSlot1Active
28648
+ };
28649
+ }
28650
+ if (harnessSlot1Active) {
28651
+ warnings.push(
28652
+ `Mission Control harness slot 1 is active under control/.har (port ${port}). Stop it with: cd control && har env teardown 1 \u2014 or use har control up only when the harness is down.`
28653
+ );
28654
+ }
28655
+ const portBusy = isPortInUse(port);
28656
+ if (portBusy) {
28657
+ const occupant = dockerOnPort(containers, port);
28658
+ const who = occupant ? `container "${occupant.name}"` : "another process";
28659
+ warnings.push(
28660
+ `Port ${port} is already in use by ${who}. har control up needs this port \u2014 free it first.`
28661
+ );
28662
+ }
28663
+ return {
28664
+ warnings,
28665
+ portInUse: portBusy,
28666
+ controlAlreadyRunning: false,
28667
+ harnessSlot1Active
28668
+ };
28669
+ }
28670
+
28671
+ // src/core/slot-launch-guard-occupied.ts
28672
+ var fs27 = __toESM(require("fs"));
28673
+ var os5 = __toESM(require("os"));
28674
+ var path25 = __toESM(require("path"));
28675
+ var import_child_process5 = require("child_process");
28676
+ function runGit(cwd, args) {
28677
+ try {
28678
+ return (0, import_child_process5.execSync)(`git ${args}`, {
28679
+ cwd,
28680
+ encoding: "utf8",
28681
+ stdio: ["pipe", "pipe", "ignore"]
28682
+ }).trim();
28683
+ } catch {
28684
+ return void 0;
28685
+ }
28686
+ }
28687
+ function worktreeDirty(worktreePath) {
28688
+ const porcelain = runGit(worktreePath, "status --porcelain");
28689
+ return porcelain !== void 0 && porcelain.length > 0;
28690
+ }
28691
+ function gitCommonDir2(cwd) {
28692
+ const out = runGit(cwd, "rev-parse --git-common-dir");
28693
+ return out ? path25.resolve(cwd, out) : void 0;
28694
+ }
28695
+ function sameGitCheckout2(a2, b2) {
28696
+ const left2 = gitCommonDir2(a2);
28697
+ const right2 = gitCommonDir2(b2);
28698
+ return left2 !== void 0 && right2 !== void 0 && left2 === right2;
28699
+ }
28700
+ function discoverWorktree(harnessRoot, agentId, projectName) {
28701
+ const session = readSlotRegistry(harnessRoot, agentId);
28702
+ if (session?.worktreePath && fs27.existsSync(session.worktreePath)) {
28703
+ return session.worktreePath;
28704
+ }
28705
+ const legacy = path25.join(os5.homedir(), "worktrees", `${projectName}-agent-${agentId}`);
28706
+ if (fs27.existsSync(legacy) && sameGitCheckout2(harnessRoot, legacy)) return legacy;
28707
+ const worktreesRoot = path25.join(os5.homedir(), "worktrees");
28708
+ if (!fs27.existsSync(worktreesRoot)) return void 0;
28709
+ const suffix = `-har-agent-${agentId}-`;
28710
+ const relPrefix = runGit(harnessRoot, "rev-parse --show-prefix") ?? "";
28711
+ for (const name of fs27.readdirSync(worktreesRoot)) {
28712
+ if (!name.includes(suffix)) continue;
28713
+ const candidate = path25.join(worktreesRoot, name);
28714
+ if (!sameGitCheckout2(harnessRoot, candidate)) continue;
28715
+ if (!fs27.existsSync(path25.join(candidate, relPrefix, `.env.agent.${agentId}`))) continue;
28716
+ return candidate;
28717
+ }
28718
+ return void 0;
28719
+ }
28720
+ function collectOccupiedSlot(repoPath, agentId) {
28721
+ const harnessRoot = resolveHarnessRoot(repoPath);
28722
+ const env3 = readHarnessEnv(harnessRoot);
28723
+ const projectName = env3.HARNESS_PROJECT_NAME ?? path25.basename(harnessRoot);
28724
+ const session = readSlotRegistry(harnessRoot, agentId);
28725
+ const worktreePath = discoverWorktree(harnessRoot, agentId, projectName);
28726
+ const workDir = session?.workDir;
28727
+ const envInWorkDir = workDir ? fs27.existsSync(path25.join(workDir, `.env.agent.${agentId}`)) : false;
28728
+ const envInRoot = fs27.existsSync(path25.join(harnessRoot, `.env.agent.${agentId}`));
28729
+ const envInWorktree = worktreePath ? fs27.existsSync(path25.join(worktreePath, `.env.agent.${agentId}`)) : false;
28730
+ const active = session !== void 0 && session.status !== "completed" || envInWorkDir || envInRoot || envInWorktree;
28731
+ if (!active) return void 0;
28732
+ const dirty = worktreePath ? worktreeDirty(worktreePath) : void 0;
28733
+ return {
28734
+ agentId,
28735
+ active: true,
28736
+ workDir: workDir ?? worktreePath,
28737
+ worktreePath,
28738
+ branch: session?.branch,
28739
+ sessionStatus: session?.status,
28740
+ lastError: session?.lastError,
28741
+ sessionCreatedAt: session?.createdAt,
28742
+ dirty,
28743
+ harnessUsage: "none"
28744
+ };
28745
+ }
28746
+ function formatOccupiedSlot(slot, resume) {
28747
+ const lines = [
28748
+ `Slot ${slot.agentId} is already in use.`,
28749
+ slot.worktreePath ? ` Worktree: ${slot.worktreePath}` : void 0,
28750
+ slot.branch ? ` Branch: ${slot.branch}` : void 0,
28751
+ slot.workDir ? ` Work dir: ${slot.workDir}` : void 0,
28752
+ slot.sessionStatus ? ` Status: ${slot.sessionStatus}` : void 0,
28753
+ slot.lastError ? ` Error: ${slot.lastError}` : void 0,
28754
+ slot.sessionCreatedAt ? ` Since: ${slot.sessionCreatedAt}` : void 0,
28755
+ slot.dirty ? " Git: dirty (uncommitted changes \u2014 commit or use force to discard)" : " Git: clean",
28756
+ ""
28757
+ ];
28758
+ if (resume || slot.sessionStatus === "failed" || slot.sessionStatus === "starting") {
28759
+ lines.push(
28760
+ "This session failed partway through launch. Resume without replacing:",
28761
+ ` har env launch ${slot.agentId} --resume`,
28762
+ ` har env recover ${slot.agentId}`,
28763
+ ` ./.har/launch.sh ${slot.agentId} --resume`,
28764
+ ""
28765
+ );
28766
+ }
28767
+ lines.push(
28768
+ "Replacing removes the worktree. The session branch is kept only if you committed.",
28769
+ "Gitignored paths (state/, runs/, local clones) are NOT preserved.",
28770
+ "",
28771
+ "To replace: pass confirmReplace=true (MCP), --replace (CLI), or answer y at the prompt.",
28772
+ "If the worktree is dirty, also pass force=true / --force after explicit user approval."
28773
+ );
28774
+ return lines.filter(Boolean).join("\n");
28775
+ }
28776
+ function checkLaunchGuard(repoPath, agentId, options = {}) {
28777
+ const slot = collectOccupiedSlot(repoPath, agentId);
28778
+ if (!slot?.active) {
28779
+ return { allowed: true };
28780
+ }
28781
+ const harnessRoot = resolveHarnessRoot(repoPath);
28782
+ const session = readSlotRegistry(harnessRoot, agentId);
28783
+ if (options.resume) {
28784
+ if (!isSlotResumable(session)) {
28785
+ return {
28786
+ allowed: false,
28787
+ blocked: true,
28788
+ slot,
28789
+ reason: [
28790
+ `Slot ${agentId} is not resumable (status=${session?.status ?? "none"}).`,
28791
+ "Only failed or starting sessions can be resumed.",
28792
+ "Use a normal launch with --replace to start fresh."
28793
+ ].join("\n")
28794
+ };
28795
+ }
28796
+ return { allowed: true, slot };
28797
+ }
28798
+ if (isSlotResumable(session) && !options.confirmReplace) {
28799
+ return {
28800
+ allowed: false,
28801
+ blocked: true,
28802
+ slot,
28803
+ reason: formatOccupiedSlot(slot)
28804
+ };
28805
+ }
28806
+ if (!options.confirmReplace) {
28807
+ return {
28808
+ allowed: false,
28809
+ blocked: true,
28810
+ slot,
28811
+ reason: formatOccupiedSlot(slot)
28812
+ };
28813
+ }
28814
+ if (slot.dirty && !options.force) {
28815
+ return {
28816
+ allowed: false,
28817
+ blocked: true,
28818
+ slot,
28819
+ reason: [
28820
+ formatOccupiedSlot(slot),
28821
+ "",
28822
+ "The occupied worktree has uncommitted changes.",
28823
+ "Pass force=true / --force to discard them (only after explicit user approval)."
28824
+ ].join("\n")
28825
+ };
28826
+ }
28827
+ return { allowed: true, slot };
28828
+ }
28829
+
28830
+ // src/core/slot-preflight.ts
28831
+ var import_child_process6 = require("child_process");
28832
+ function listPm2Processes() {
28833
+ try {
28834
+ const raw = (0, import_child_process6.execSync)("npx --yes pm2 jlist", {
28835
+ encoding: "utf8",
28836
+ stdio: ["pipe", "pipe", "ignore"],
28837
+ timeout: 3e3
28838
+ });
28839
+ const procs = JSON.parse(raw);
28840
+ return Array.isArray(procs) ? procs : void 0;
28841
+ } catch {
28842
+ return void 0;
28843
+ }
28844
+ }
28845
+ function detectForeignPm2(projectName, agentId, procs) {
28846
+ if (!procs) return void 0;
28847
+ const slotPrefix = `har-${projectName}-agent-${agentId}-`;
28848
+ const legacyPrefix = `agent-${agentId}-`;
28849
+ const foreign = procs.filter(
28850
+ (p2) => p2.name && (p2.name.startsWith("har-") && p2.name.includes(`-agent-${agentId}-`) && !p2.name.startsWith(slotPrefix) || p2.name.startsWith(legacyPrefix) && !p2.name.startsWith("har-"))
28851
+ );
28852
+ if (foreign.length === 0) return void 0;
28853
+ return {
28854
+ processes: foreign.map((p2) => ({
28855
+ name: p2.name,
28856
+ cwd: p2.pm2_env?.pm_cwd ?? p2.pm2_env?.cwd
28857
+ }))
28858
+ };
28859
+ }
28860
+ function infraEnabled(env3, service) {
28861
+ return ` ${env3.HARNESS_INFRA_SERVICES ?? ""} `.includes(` ${service} `);
28862
+ }
28863
+ function checkInfraPort(env3, varName, defaultPort, scanStart, scanEnd) {
28864
+ const current = env3[varName] ? Number(env3[varName]) : void 0;
28865
+ if (current !== void 0 && !isPortInUse(current)) {
28866
+ return { port: current };
28867
+ }
28868
+ if (current !== void 0 && isPortInUse(current)) {
28869
+ const alt = pickFreePort(scanStart, scanEnd);
28870
+ if (alt !== void 0) return { port: alt };
28871
+ return { error: `No free ${varName} port in range ${scanStart}-${scanEnd}` };
28872
+ }
28873
+ const port = pickFreePort(defaultPort, scanEnd) ?? pickFreePort(scanStart, scanEnd);
28874
+ if (port === void 0) {
28875
+ return { error: `No free ${varName} port in range ${scanStart}-${scanEnd}` };
28876
+ }
28877
+ return { port };
28878
+ }
28879
+ function inspectSlotReadiness(repoPath, agentId, options = {}) {
28880
+ const harnessRoot = resolveHarnessRoot(repoPath);
28881
+ const env3 = readHarnessEnv(harnessRoot);
28882
+ const projectName = env3.HARNESS_PROJECT_NAME ?? path26.basename(harnessRoot);
28883
+ const usesPm2 = harnessUsesPm2(repoPath);
28884
+ const blockers = [];
28885
+ const remediations = [];
28886
+ const warnings = [];
28887
+ const ports = {};
28888
+ const session = readSlotRegistry(harnessRoot, agentId);
28889
+ const guard = options.occupied !== void 0 ? options.occupied.active ? options.resume && isSlotResumable(session) ? { allowed: true } : !options.confirmReplace ? {
28890
+ allowed: false,
28891
+ blocked: true,
28892
+ reason: `Slot ${agentId} is already in use.`
28893
+ } : options.occupied.dirty && !options.force ? {
28894
+ allowed: false,
28895
+ blocked: true,
28896
+ reason: `Slot ${agentId} worktree has uncommitted changes.`
28897
+ } : { allowed: true } : { allowed: true } : checkLaunchGuard(repoPath, agentId, options);
28898
+ if (!guard.allowed) {
28899
+ const resumable = isSlotResumable(session);
28900
+ blockers.push({
28901
+ code: options.occupied?.dirty ? "slot_dirty" : resumable ? "slot_resumable" : "slot_occupied",
28902
+ message: guard.reason ?? `Slot ${agentId} is occupied.`,
28903
+ 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."
28904
+ });
28905
+ remediations.push(
28906
+ resumable ? `har env launch ${agentId} --resume` : guard.slot?.dirty ? "har env preflight <id> --replace --force" : "har env preflight <id> --replace"
28907
+ );
28908
+ }
28909
+ const pm2Procs = usesPm2 ? options.pm2Processes !== void 0 ? options.pm2Processes : listPm2Processes() : void 0;
28910
+ const foreign = usesPm2 ? detectForeignPm2(projectName, agentId, pm2Procs) : void 0;
28911
+ if (foreign) {
28912
+ const names = foreign.processes.map((p2) => p2.name).join(", ");
28913
+ blockers.push({
28914
+ code: "foreign_pm2",
28915
+ message: `Foreign PM2 processes match agent ${agentId}: ${names}`,
28916
+ remediation: "Stop the other harness session (./.har/teardown.sh in that repo) or use a different agent slot.",
28917
+ details: { processes: foreign.processes }
28918
+ });
28919
+ remediations.push("Inspect with: npx pm2 jlist | grep agent-" + agentId);
28920
+ }
28921
+ if (usesPm2 && pm2Procs) {
28922
+ const slotPrefix = `har-${projectName}-agent-${agentId}-`;
28923
+ const owned = pm2Procs.filter((p2) => p2.name?.startsWith(slotPrefix));
28924
+ if (owned.length > 0 && !session) {
28925
+ blockers.push({
28926
+ code: "registry_missing",
28927
+ message: `PM2 processes exist for ${projectName} agent ${agentId} but the slot registry is missing.`,
28928
+ remediation: "Run teardown in this harness or delete PM2 processes manually, then relaunch."
28929
+ });
28930
+ }
28931
+ if (owned.length > 0 && session?.projectName && session.projectName !== projectName) {
28932
+ blockers.push({
28933
+ code: "project_mismatch",
28934
+ message: `Slot registry projectName=${session.projectName} does not match harness ${projectName}.`,
28935
+ remediation: "Teardown the stale session and relaunch with --replace."
28936
+ });
28937
+ }
28938
+ }
28939
+ let allocatedPorts = false;
28940
+ if (usesPm2 && (options.allocatePorts ?? true)) {
28941
+ const alloc = allocateAppPorts(repoPath, agentId);
28942
+ if ("error" in alloc) {
28943
+ blockers.push({
28944
+ code: "ports_exhausted",
28945
+ message: alloc.error,
28946
+ remediation: "Free a port in the slot lane or stop the process/container using it."
28947
+ });
28948
+ } else {
28949
+ ports.frontend = alloc.frontend;
28950
+ ports.api = alloc.api;
28951
+ ports.debug = alloc.debug;
28952
+ allocatedPorts = alloc.allocated;
28953
+ const containers = options.dockerContainers ?? listDockerContainers();
28954
+ const feDefault = defaultAppPort(
28955
+ Number(env3.HARNESS_FE_BASE_PORT ?? 3e3),
28956
+ agentId,
28957
+ portStep(env3)
28958
+ );
28959
+ warnings.push(...controlDefaultPortWarnings(containers, feDefault, alloc.frontend));
28960
+ for (const [label, port] of Object.entries({ frontend: alloc.frontend, api: alloc.api })) {
28961
+ const control = controlContainerOnPort(containers, port);
28962
+ if (control) {
28963
+ blockers.push({
28964
+ code: "control_port_conflict",
28965
+ message: formatControlPortBlocker(control.name, port, label),
28966
+ remediation: "Run: har control down \u2014 or launch a different slot id.",
28967
+ details: { container: control.name, port, label }
28968
+ });
28969
+ remediations.push("har control down");
28970
+ } else {
28971
+ const occupant = dockerOnPort(containers, port);
28972
+ if (occupant && !occupant.name.startsWith(`har-${projectName}-`)) {
28973
+ blockers.push({
28974
+ code: "docker_port_conflict",
28975
+ message: `Docker container "${occupant.name}" binds port ${port} (${label}).`,
28976
+ remediation: `Stop the container: docker stop ${occupant.name}`,
28977
+ details: { container: occupant.name, port, label }
28978
+ });
28979
+ }
28980
+ }
28981
+ }
28982
+ }
28983
+ }
28984
+ if (infraEnabled(env3, "db")) {
28985
+ const dbDefault = Number(env3.HARNESS_DB_PORT_DEFAULT ?? 15432);
28986
+ const dbStart = Number(env3.HARNESS_DB_PORT_SCAN_START ?? dbDefault);
28987
+ const dbEnd = Number(env3.HARNESS_DB_PORT_SCAN_END ?? dbDefault + 67);
28988
+ const dbCheck = checkInfraPort(env3, "AGENT_DB_PORT", dbDefault, dbStart, dbEnd);
28989
+ if (dbCheck.error) {
28990
+ blockers.push({
28991
+ code: "db_port_exhausted",
28992
+ message: dbCheck.error,
28993
+ remediation: `Free a port in ${dbStart}-${dbEnd} or stop har-*-db-1 containers from other projects.`
28994
+ });
28995
+ } else if (dbCheck.port !== void 0) {
28996
+ ports.db = dbCheck.port;
28997
+ const occupant = dockerOnPort(
28998
+ options.dockerContainers ?? listDockerContainers(),
28999
+ dbCheck.port
29000
+ );
29001
+ if (occupant && !occupant.name.startsWith(`har-${projectName}-`) && !occupant.name.includes("-db-")) {
29002
+ blockers.push({
29003
+ code: "db_port_conflict",
29004
+ message: `Port ${dbCheck.port} (database) is held by "${occupant.name}".`,
29005
+ remediation: `Stop the container or pick another slot.`,
29006
+ details: { container: occupant.name, port: dbCheck.port }
29007
+ });
29008
+ }
29009
+ }
29010
+ }
29011
+ const canLaunch = blockers.length === 0;
29012
+ return {
29013
+ canLaunch,
29014
+ verdict: canLaunch ? "ready" : "blocked",
29015
+ blockers,
29016
+ remediations: [...new Set(remediations)],
29017
+ ports: Object.keys(ports).length > 0 ? ports : void 0,
29018
+ allocatedPorts: allocatedPorts || void 0,
29019
+ warnings: warnings.length > 0 ? warnings : void 0
29020
+ };
29021
+ }
29022
+ function formatPreflightReport(agentId, readiness) {
29023
+ const lines = [];
29024
+ if (readiness.canLaunch) {
29025
+ lines.push(`Slot ${agentId}: ready to launch.`);
29026
+ if (readiness.ports) {
29027
+ const p2 = readiness.ports;
29028
+ const parts = [
29029
+ p2.frontend !== void 0 ? `frontend=${p2.frontend}` : void 0,
29030
+ p2.api !== void 0 ? `api=${p2.api}` : void 0,
29031
+ p2.debug !== void 0 ? `debug=${p2.debug}` : void 0,
29032
+ p2.db !== void 0 ? `db=${p2.db}` : void 0
29033
+ ].filter(Boolean);
29034
+ if (parts.length) lines.push(` Ports: ${parts.join(" ")}`);
29035
+ if (readiness.allocatedPorts && !readiness.warnings?.length) {
29036
+ lines.push(" (alternate ports selected \u2014 defaults were busy)");
29037
+ }
29038
+ }
29039
+ if (readiness.warnings?.length) {
29040
+ for (const w2 of readiness.warnings) {
29041
+ lines.push(` WARN: ${w2}`);
29042
+ }
29043
+ }
29044
+ return lines.join("\n");
29045
+ }
29046
+ lines.push(`Slot ${agentId}: launch blocked.`);
29047
+ for (const b2 of readiness.blockers) {
29048
+ lines.push(` [${b2.code}] ${b2.message}`);
29049
+ if (b2.remediation) lines.push(` \u2192 ${b2.remediation}`);
29050
+ }
29051
+ return lines.join("\n");
29052
+ }
29053
+
28084
29054
  // src/core/slot-status.ts
28085
29055
  var BYPASS_WARNING_MS = 15 * 60 * 1e3;
28086
29056
  function readGitRemote(repoPath) {
28087
29057
  try {
28088
- const remote = (0, import_child_process3.execSync)("git remote get-url origin", {
29058
+ const remote = (0, import_child_process7.execSync)("git remote get-url origin", {
28089
29059
  cwd: repoPath,
28090
29060
  encoding: "utf8",
28091
29061
  stdio: ["pipe", "pipe", "ignore"]
@@ -28095,9 +29065,9 @@ function readGitRemote(repoPath) {
28095
29065
  return void 0;
28096
29066
  }
28097
29067
  }
28098
- function runGit(cwd, args) {
29068
+ function runGit2(cwd, args) {
28099
29069
  try {
28100
- return (0, import_child_process3.execSync)(`git ${args}`, {
29070
+ return (0, import_child_process7.execSync)(`git ${args}`, {
28101
29071
  cwd,
28102
29072
  encoding: "utf8",
28103
29073
  stdio: ["pipe", "pipe", "ignore"]
@@ -28107,40 +29077,40 @@ function runGit(cwd, args) {
28107
29077
  }
28108
29078
  }
28109
29079
  function readWorktreeBranch(worktreePath) {
28110
- return runGit(worktreePath, "rev-parse --abbrev-ref HEAD");
29080
+ return runGit2(worktreePath, "rev-parse --abbrev-ref HEAD");
28111
29081
  }
28112
- function gitCommonDir2(cwd) {
28113
- const out = runGit(cwd, "rev-parse --git-common-dir");
28114
- return out ? path21.resolve(cwd, out) : void 0;
29082
+ function gitCommonDir3(cwd) {
29083
+ const out = runGit2(cwd, "rev-parse --git-common-dir");
29084
+ return out ? path27.resolve(cwd, out) : void 0;
28115
29085
  }
28116
- function sameGitCheckout2(a2, b2) {
28117
- const left2 = gitCommonDir2(a2);
28118
- const right2 = gitCommonDir2(b2);
29086
+ function sameGitCheckout3(a2, b2) {
29087
+ const left2 = gitCommonDir3(a2);
29088
+ const right2 = gitCommonDir3(b2);
28119
29089
  return left2 !== void 0 && right2 !== void 0 && left2 === right2;
28120
29090
  }
28121
29091
  function gitPrefix2(cwd) {
28122
- const out = runGit(cwd, "rev-parse --show-prefix");
29092
+ const out = runGit2(cwd, "rev-parse --show-prefix");
28123
29093
  return out ?? "";
28124
29094
  }
28125
29095
  function discoverSessionWorktreePath(harnessRoot, agentId) {
28126
- const worktreesRoot = path21.join(os4.homedir(), "worktrees");
28127
- if (!fs23.existsSync(worktreesRoot)) return void 0;
29096
+ const worktreesRoot = path27.join(os6.homedir(), "worktrees");
29097
+ if (!fs28.existsSync(worktreesRoot)) return void 0;
28128
29098
  const suffix = `-har-agent-${agentId}-`;
28129
29099
  const relPrefix = gitPrefix2(harnessRoot);
28130
- const matches = fs23.readdirSync(worktreesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.includes(suffix)).map((entry) => path21.join(worktreesRoot, entry.name)).filter(
28131
- (candidate) => sameGitCheckout2(harnessRoot, candidate) && fs23.existsSync(path21.join(candidate, relPrefix, `.env.agent.${agentId}`))
29100
+ const matches = fs28.readdirSync(worktreesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.includes(suffix)).map((entry) => path27.join(worktreesRoot, entry.name)).filter(
29101
+ (candidate) => sameGitCheckout3(harnessRoot, candidate) && fs28.existsSync(path27.join(candidate, relPrefix, `.env.agent.${agentId}`))
28132
29102
  );
28133
29103
  return matches.sort()[0];
28134
29104
  }
28135
29105
  function collectWorktreeDrift(harnessRoot, worktreePath, baseCommit) {
28136
29106
  const drift = {};
28137
- drift.detachedHead = runGit(worktreePath, "symbolic-ref -q HEAD") === void 0;
28138
- const porcelain = runGit(worktreePath, "status --porcelain");
29107
+ drift.detachedHead = runGit2(worktreePath, "symbolic-ref -q HEAD") === void 0;
29108
+ const porcelain = runGit2(worktreePath, "status --porcelain");
28139
29109
  if (porcelain !== void 0) drift.dirty = porcelain.length > 0;
28140
29110
  if (baseCommit) {
28141
- const ahead = runGit(worktreePath, `rev-list --count ${baseCommit}..HEAD`);
29111
+ const ahead = runGit2(worktreePath, `rev-list --count ${baseCommit}..HEAD`);
28142
29112
  if (ahead !== void 0) drift.ahead = Number(ahead);
28143
- const behind = runGit(harnessRoot, `rev-list --count ${baseCommit}..HEAD`);
29113
+ const behind = runGit2(harnessRoot, `rev-list --count ${baseCommit}..HEAD`);
28144
29114
  if (behind !== void 0) {
28145
29115
  drift.behind = Number(behind);
28146
29116
  drift.stale = drift.behind > 0;
@@ -28171,20 +29141,50 @@ function lastBuildPass(run2) {
28171
29141
  const build = data.verification?.stages?.find((s2) => s2.name === "build");
28172
29142
  return build?.pass;
28173
29143
  }
28174
- function collectSlotStatus(harnessRoot, agentId, runs) {
29144
+ function listPm2Processes2() {
29145
+ try {
29146
+ const raw = (0, import_child_process7.execSync)("npx --yes pm2 jlist", {
29147
+ encoding: "utf8",
29148
+ stdio: ["pipe", "pipe", "ignore"],
29149
+ timeout: 3e3
29150
+ });
29151
+ const procs = JSON.parse(raw);
29152
+ return Array.isArray(procs) ? procs : void 0;
29153
+ } catch {
29154
+ return void 0;
29155
+ }
29156
+ }
29157
+ function detectPm2Issue(projectName, agentId, session, procs) {
29158
+ if (!procs) return void 0;
29159
+ const slotPrefix = `har-${projectName}-agent-${agentId}-`;
29160
+ const legacyPrefix = `agent-${agentId}-`;
29161
+ const owned = procs.filter((p2) => p2.name?.startsWith(slotPrefix));
29162
+ const foreign = procs.filter(
29163
+ (p2) => p2.name && (p2.name.startsWith("har-") && p2.name.includes(`-agent-${agentId}-`) && !p2.name.startsWith(slotPrefix) || p2.name.startsWith(legacyPrefix) && !p2.name.startsWith("har-"))
29164
+ );
29165
+ if (foreign.length > 0) return "foreign_pm2";
29166
+ if (owned.length > 0) {
29167
+ if (!session) return "registry_missing";
29168
+ if (session.projectName && session.projectName !== projectName) {
29169
+ return "project_mismatch";
29170
+ }
29171
+ }
29172
+ return void 0;
29173
+ }
29174
+ function collectSlotStatus(harnessRoot, agentId, runs, pm2Procs) {
28175
29175
  const env3 = readHarnessEnv(harnessRoot);
28176
- const projectName = env3.HARNESS_PROJECT_NAME ?? path21.basename(harnessRoot);
29176
+ const projectName = env3.HARNESS_PROJECT_NAME ?? path27.basename(harnessRoot);
28177
29177
  const session = readSlotRegistry(harnessRoot, agentId);
28178
- const legacyWorktreePath = path21.join(
28179
- os4.homedir(),
29178
+ const legacyWorktreePath = path27.join(
29179
+ os6.homedir(),
28180
29180
  "worktrees",
28181
29181
  `${projectName}-agent-${agentId}`
28182
29182
  );
28183
- const worktreePath = session?.worktreePath && fs23.existsSync(session.worktreePath) ? session.worktreePath : fs23.existsSync(legacyWorktreePath) ? legacyWorktreePath : discoverSessionWorktreePath(harnessRoot, agentId);
29183
+ const worktreePath = session?.worktreePath && fs28.existsSync(session.worktreePath) ? session.worktreePath : fs28.existsSync(legacyWorktreePath) ? legacyWorktreePath : discoverSessionWorktreePath(harnessRoot, agentId);
28184
29184
  const workDir = resolveAgentWorkDir(harnessRoot, agentId);
28185
- const envInWorkDir = workDir ? fs23.existsSync(path21.join(workDir, `.env.agent.${agentId}`)) : false;
28186
- const envInRoot = fs23.existsSync(path21.join(harnessRoot, `.env.agent.${agentId}`));
28187
- const envInWorktree = worktreePath ? fs23.existsSync(path21.join(worktreePath, `.env.agent.${agentId}`)) : false;
29185
+ const envInWorkDir = workDir ? fs28.existsSync(path27.join(workDir, `.env.agent.${agentId}`)) : false;
29186
+ const envInRoot = fs28.existsSync(path27.join(harnessRoot, `.env.agent.${agentId}`));
29187
+ const envInWorktree = worktreePath ? fs28.existsSync(path27.join(worktreePath, `.env.agent.${agentId}`)) : false;
28188
29188
  const active = session !== void 0 && session.status !== "completed" || envInWorkDir || envInRoot || envInWorktree;
28189
29189
  const latest = latestRunForAgent(runs, agentId);
28190
29190
  const verifyRun = runs.find((r2) => r2.agentId === agentId && r2.stageId === "verify");
@@ -28197,6 +29197,12 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
28197
29197
  }
28198
29198
  }
28199
29199
  const drift = worktreePath ? collectWorktreeDrift(harnessRoot, worktreePath, session?.baseCommit) : {};
29200
+ const pm2Issue = detectPm2Issue(projectName, agentId, session, pm2Procs);
29201
+ const readiness = inspectSlotReadiness(harnessRoot, agentId, {
29202
+ allocatePorts: true,
29203
+ occupied: { active, dirty: drift.dirty }
29204
+ });
29205
+ const resumeHint = session?.status === "failed" || session?.status === "starting" ? `har env launch ${agentId} --resume (alias: har env recover ${agentId})` : void 0;
28200
29206
  return {
28201
29207
  agentId,
28202
29208
  active,
@@ -28204,6 +29210,9 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
28204
29210
  worktreePath,
28205
29211
  branch: session?.branch ?? (worktreePath ? readWorktreeBranch(worktreePath) : void 0),
28206
29212
  previewUrls,
29213
+ ports: session?.ports,
29214
+ pm2Issue,
29215
+ readiness,
28207
29216
  harnessUsage: deriveHarnessUsage(latest, active),
28208
29217
  lastRunId: latest?.runId,
28209
29218
  lastRunAt: latest?.startedAt,
@@ -28217,6 +29226,7 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
28217
29226
  purpose: session?.purpose,
28218
29227
  sessionStatus: session?.status,
28219
29228
  lastError: session?.lastError,
29229
+ resumeHint,
28220
29230
  ...drift
28221
29231
  };
28222
29232
  }
@@ -28225,25 +29235,26 @@ function collectEnvironmentStatus(repoPath) {
28225
29235
  const runs = listRuns(harnessRoot, { limit: 200 });
28226
29236
  const manifest = readManifest(harnessRoot);
28227
29237
  const slotIds = getAgentSlotIds(harnessRoot);
29238
+ const pm2Procs = listPm2Processes2();
28228
29239
  return {
28229
- repoPath: path21.resolve(repoPath),
29240
+ repoPath: path27.resolve(repoPath),
28230
29241
  harnessRoot,
28231
29242
  gitRemote: readGitRemote(harnessRoot),
28232
29243
  profile: manifest?.profile,
28233
- slots: slotIds.map((id) => collectSlotStatus(harnessRoot, id, runs)),
29244
+ slots: slotIds.map((id) => collectSlotStatus(harnessRoot, id, runs, pm2Procs)),
28234
29245
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
28235
29246
  };
28236
29247
  }
28237
29248
 
28238
29249
  // src/core/validations.ts
28239
29250
  var crypto3 = __toESM(require("crypto"));
28240
- var fs25 = __toESM(require("fs"));
28241
- var path23 = __toESM(require("path"));
29251
+ var fs30 = __toESM(require("fs"));
29252
+ var path29 = __toESM(require("path"));
28242
29253
 
28243
29254
  // src/core/change-batch.ts
28244
- var fs24 = __toESM(require("fs"));
28245
- var os5 = __toESM(require("os"));
28246
- var path22 = __toESM(require("path"));
29255
+ var fs29 = __toESM(require("fs"));
29256
+ var os7 = __toESM(require("os"));
29257
+ var path28 = __toESM(require("path"));
28247
29258
  function git(checkoutDir, args, env3) {
28248
29259
  const result = run(`git ${args}`, { cwd: checkoutDir, env: env3 });
28249
29260
  if (result.code !== 0) {
@@ -28259,7 +29270,7 @@ function isCheckoutRoot(checkoutDir) {
28259
29270
  const toplevel = tryGit(checkoutDir, "rev-parse --show-toplevel");
28260
29271
  if (!toplevel) return false;
28261
29272
  try {
28262
- return fs24.realpathSync(toplevel) === fs24.realpathSync(checkoutDir);
29273
+ return fs29.realpathSync(toplevel) === fs29.realpathSync(checkoutDir);
28263
29274
  } catch {
28264
29275
  return false;
28265
29276
  }
@@ -28281,7 +29292,7 @@ function isMergeOrRebaseInProgress(checkoutDir) {
28281
29292
  if (tryGit(checkoutDir, "rev-parse -q --verify MERGE_HEAD") !== void 0) return true;
28282
29293
  for (const dir of ["rebase-merge", "rebase-apply"]) {
28283
29294
  const gitPath = tryGit(checkoutDir, `rev-parse --git-path ${dir}`);
28284
- if (gitPath && fs24.existsSync(path22.resolve(checkoutDir, gitPath))) return true;
29295
+ if (gitPath && fs29.existsSync(path28.resolve(checkoutDir, gitPath))) return true;
28285
29296
  }
28286
29297
  return false;
28287
29298
  }
@@ -28304,8 +29315,8 @@ function parseNameStatus(output) {
28304
29315
  return files;
28305
29316
  }
28306
29317
  function computeWorktreeSnapshot(checkoutDir) {
28307
- const tmpDir = fs24.mkdtempSync(path22.join(os5.tmpdir(), "har-idx-"));
28308
- const tmpIndex = path22.join(tmpDir, "index");
29318
+ const tmpDir = fs29.mkdtempSync(path28.join(os7.tmpdir(), "har-idx-"));
29319
+ const tmpIndex = path28.join(tmpDir, "index");
28309
29320
  const env3 = { GIT_INDEX_FILE: tmpIndex };
28310
29321
  try {
28311
29322
  const headTree = getHeadTree(checkoutDir);
@@ -28328,35 +29339,35 @@ function computeWorktreeSnapshot(checkoutDir) {
28328
29339
  changedFiles: parseNameStatus(diffOutput)
28329
29340
  };
28330
29341
  } finally {
28331
- fs24.rmSync(tmpDir, { recursive: true, force: true });
29342
+ fs29.rmSync(tmpDir, { recursive: true, force: true });
28332
29343
  }
28333
29344
  }
28334
29345
 
28335
29346
  // src/core/validations.ts
28336
29347
  var VALIDATIONS_DIR = "validations";
28337
29348
  function getValidationsDir(checkoutDir) {
28338
- return path23.join(checkoutDir, ".har", VALIDATIONS_DIR);
29349
+ return path29.join(checkoutDir, ".har", VALIDATIONS_DIR);
28339
29350
  }
28340
29351
  function validationPath(checkoutDir, treeHash) {
28341
- return path23.join(getValidationsDir(checkoutDir), `${treeHash}.json`);
29352
+ return path29.join(getValidationsDir(checkoutDir), `${treeHash}.json`);
28342
29353
  }
28343
29354
  function writeRecord(checkoutDir, record2) {
28344
29355
  const dir = getValidationsDir(checkoutDir);
28345
- fs25.mkdirSync(dir, { recursive: true });
28346
- fs25.writeFileSync(
29356
+ fs30.mkdirSync(dir, { recursive: true });
29357
+ fs30.writeFileSync(
28347
29358
  validationPath(checkoutDir, record2.treeHash),
28348
29359
  `${JSON.stringify(record2, null, 2)}
28349
29360
  `
28350
29361
  );
28351
29362
  }
28352
29363
  function ensureValidationsIgnored(checkoutDir) {
28353
- const harDir = path23.join(checkoutDir, ".har");
28354
- if (!fs25.existsSync(harDir)) return;
28355
- const gitignorePath = path23.join(harDir, ".gitignore");
28356
- const content = fs25.existsSync(gitignorePath) ? fs25.readFileSync(gitignorePath, "utf8") : "";
29364
+ const harDir = path29.join(checkoutDir, ".har");
29365
+ if (!fs30.existsSync(harDir)) return;
29366
+ const gitignorePath = path29.join(harDir, ".gitignore");
29367
+ const content = fs30.existsSync(gitignorePath) ? fs30.readFileSync(gitignorePath, "utf8") : "";
28357
29368
  if (content.split("\n").some((line) => line.trim() === `${VALIDATIONS_DIR}/`)) return;
28358
29369
  const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
28359
- fs25.writeFileSync(gitignorePath, `${content}${suffix}${VALIDATIONS_DIR}/
29370
+ fs30.writeFileSync(gitignorePath, `${content}${suffix}${VALIDATIONS_DIR}/
28360
29371
  `);
28361
29372
  }
28362
29373
  function recordValidation(input) {
@@ -28373,8 +29384,8 @@ function recordValidation(input) {
28373
29384
  treeHash: snapshot.treeHash,
28374
29385
  headSha: snapshot.headSha,
28375
29386
  branch: snapshot.branch,
28376
- workDir: path23.resolve(input.checkoutDir),
28377
- harnessRoot: path23.resolve(input.harnessRoot),
29387
+ workDir: path29.resolve(input.checkoutDir),
29388
+ harnessRoot: path29.resolve(input.harnessRoot),
28378
29389
  agentId: input.agentId,
28379
29390
  status: input.status,
28380
29391
  full: input.full,
@@ -28386,16 +29397,16 @@ function recordValidation(input) {
28386
29397
  committedAt: existing?.committedAt
28387
29398
  });
28388
29399
  writeRecord(input.checkoutDir, record2);
28389
- if (path23.resolve(input.harnessRoot) !== path23.resolve(input.checkoutDir)) {
29400
+ if (path29.resolve(input.harnessRoot) !== path29.resolve(input.checkoutDir)) {
28390
29401
  writeRecord(input.harnessRoot, record2);
28391
29402
  }
28392
29403
  return record2;
28393
29404
  }
28394
29405
  function findValidation(checkoutDir, treeHash) {
28395
29406
  const file = validationPath(checkoutDir, treeHash);
28396
- if (!fs25.existsSync(file)) return void 0;
29407
+ if (!fs30.existsSync(file)) return void 0;
28397
29408
  try {
28398
- return ValidationRecordSchema.parse(JSON.parse(fs25.readFileSync(file, "utf8")));
29409
+ return ValidationRecordSchema.parse(JSON.parse(fs30.readFileSync(file, "utf8")));
28399
29410
  } catch {
28400
29411
  return void 0;
28401
29412
  }
@@ -28410,19 +29421,19 @@ function attachCommit(checkoutDir, treeHash, commitSha) {
28410
29421
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
28411
29422
  };
28412
29423
  writeRecord(checkoutDir, updated);
28413
- if (record2.harnessRoot && path23.resolve(record2.harnessRoot) !== path23.resolve(checkoutDir) && fs25.existsSync(path23.join(record2.harnessRoot, ".har"))) {
29424
+ if (record2.harnessRoot && path29.resolve(record2.harnessRoot) !== path29.resolve(checkoutDir) && fs30.existsSync(path29.join(record2.harnessRoot, ".har"))) {
28414
29425
  writeRecord(record2.harnessRoot, updated);
28415
29426
  }
28416
29427
  return updated;
28417
29428
  }
28418
29429
  function listValidations(harnessRoot) {
28419
29430
  const dir = getValidationsDir(harnessRoot);
28420
- if (!fs25.existsSync(dir)) return [];
29431
+ if (!fs30.existsSync(dir)) return [];
28421
29432
  const records = [];
28422
- for (const entry of fs25.readdirSync(dir)) {
29433
+ for (const entry of fs30.readdirSync(dir)) {
28423
29434
  if (!entry.endsWith(".json")) continue;
28424
29435
  try {
28425
- records.push(ValidationRecordSchema.parse(JSON.parse(fs25.readFileSync(path23.join(dir, entry), "utf8"))));
29436
+ records.push(ValidationRecordSchema.parse(JSON.parse(fs30.readFileSync(path29.join(dir, entry), "utf8"))));
28426
29437
  } catch {
28427
29438
  }
28428
29439
  }
@@ -28480,7 +29491,7 @@ async function waitForControlApi(apiUrl = getControlApiUrl(), timeoutMs = 6e4, i
28480
29491
  const deadline = Date.now() + timeoutMs;
28481
29492
  while (Date.now() < deadline) {
28482
29493
  if (await isControlApiReachable(apiUrl)) return true;
28483
- await new Promise((resolve24) => setTimeout(resolve24, intervalMs));
29494
+ await new Promise((resolve26) => setTimeout(resolve26, intervalMs));
28484
29495
  }
28485
29496
  return false;
28486
29497
  }
@@ -28492,7 +29503,7 @@ async function syncAllKnownReposWithControl(options) {
28492
29503
  }
28493
29504
  const repoPaths = new Set(listRegisteredRepos());
28494
29505
  if (options?.cwd) {
28495
- const cwd = path24.resolve(options.cwd);
29506
+ const cwd = path30.resolve(options.cwd);
28496
29507
  if (readManifest(cwd)) repoPaths.add(cwd);
28497
29508
  }
28498
29509
  try {
@@ -28500,7 +29511,7 @@ async function syncAllKnownReposWithControl(options) {
28500
29511
  if (listResponse.ok) {
28501
29512
  const repos = await listResponse.json();
28502
29513
  for (const repo of repos) {
28503
- const resolved = path24.resolve(repo.path);
29514
+ const resolved = path30.resolve(repo.path);
28504
29515
  if (readManifest(resolved)) repoPaths.add(resolved);
28505
29516
  }
28506
29517
  }
@@ -28519,7 +29530,7 @@ async function syncAllKnownReposWithControl(options) {
28519
29530
  return { synced, failed };
28520
29531
  }
28521
29532
  async function registerRepoWithControl(options) {
28522
- const repoPath = path24.resolve(options.repoPath);
29533
+ const repoPath = path30.resolve(options.repoPath);
28523
29534
  const apiUrl = options.apiUrl ?? getControlApiUrl();
28524
29535
  const manifest = readManifest(repoPath);
28525
29536
  const stagesRegistry = readStageRegistry(repoPath);
@@ -28532,7 +29543,7 @@ async function registerRepoWithControl(options) {
28532
29543
  return postJson(`${apiUrl}/api/repos`, body, options.dryRun);
28533
29544
  }
28534
29545
  async function syncRepoWithControl(options) {
28535
- const repoPath = path24.resolve(options.repoPath);
29546
+ const repoPath = path30.resolve(options.repoPath);
28536
29547
  const apiUrl = options.apiUrl ?? getControlApiUrl();
28537
29548
  if (options.cloud) {
28538
29549
  const remote = createRemoteExecutor();
@@ -28560,7 +29571,7 @@ async function syncRepoWithControl(options) {
28560
29571
  const listResponse = await fetch(`${apiUrl}/api/repos`);
28561
29572
  if (!listResponse.ok) throw new Error("Failed to list repos from Control API");
28562
29573
  const repos = await listResponse.json();
28563
- const existing = repos.find((r2) => path24.resolve(r2.path) === repoPath);
29574
+ const existing = repos.find((r2) => path30.resolve(r2.path) === repoPath);
28564
29575
  if (!existing) throw new Error(`Repo not registered: ${repoPath}`);
28565
29576
  await syncRepoRunsAndSlots(apiUrl, existing.id, repoPath, options.dryRun);
28566
29577
  return;
@@ -28616,53 +29627,19 @@ async function syncRepoWithControlAsync(repoPath) {
28616
29627
  }
28617
29628
 
28618
29629
  // src/core/slot-launch-guard.ts
28619
- function formatOccupiedSlot(slot) {
28620
- const lines = [
28621
- `Slot ${slot.agentId} is already in use.`,
28622
- slot.worktreePath ? ` Worktree: ${slot.worktreePath}` : void 0,
28623
- slot.branch ? ` Branch: ${slot.branch}` : void 0,
28624
- slot.workDir ? ` Work dir: ${slot.workDir}` : void 0,
28625
- slot.sessionStatus ? ` Status: ${slot.sessionStatus}` : void 0,
28626
- slot.lastError ? ` Error: ${slot.lastError}` : void 0,
28627
- slot.sessionCreatedAt ? ` Since: ${slot.sessionCreatedAt}` : void 0,
28628
- slot.dirty ? " Git: dirty (uncommitted changes \u2014 commit or use force to discard)" : " Git: clean",
28629
- "",
28630
- "Replacing removes the worktree. The session branch is kept only if you committed.",
28631
- "Gitignored paths (state/, runs/, local clones) are NOT preserved.",
28632
- "",
28633
- "To replace: pass confirmReplace=true (MCP), --replace (CLI), or answer y at the prompt.",
28634
- "If the worktree is dirty, also pass force=true / --force after explicit user approval."
28635
- ];
28636
- return lines.filter(Boolean).join("\n");
28637
- }
28638
- function checkLaunchGuard(repoPath, agentId, options = {}) {
28639
- const status = collectEnvironmentStatus(repoPath);
28640
- const slot = status.slots.find((s2) => s2.agentId === agentId);
28641
- if (!slot?.active) {
28642
- return { allowed: true };
28643
- }
28644
- if (!options.confirmReplace) {
29630
+ function checkLaunchGuard2(repoPath, agentId, options = {}) {
29631
+ const readiness = inspectSlotReadiness(repoPath, agentId, options);
29632
+ if (!readiness.canLaunch) {
28645
29633
  return {
28646
29634
  allowed: false,
28647
29635
  blocked: true,
28648
- slot,
28649
- reason: formatOccupiedSlot(slot)
29636
+ reason: formatPreflightReport(agentId, readiness),
29637
+ slot: checkLaunchGuard(repoPath, agentId, options).slot,
29638
+ readiness
28650
29639
  };
28651
29640
  }
28652
- if (slot.dirty && !options.force) {
28653
- return {
28654
- allowed: false,
28655
- blocked: true,
28656
- slot,
28657
- reason: [
28658
- formatOccupiedSlot(slot),
28659
- "",
28660
- "The occupied worktree has uncommitted changes.",
28661
- "Pass force=true / --force to discard them (only after explicit user approval)."
28662
- ].join("\n")
28663
- };
28664
- }
28665
- return { allowed: true, slot };
29641
+ const occupied = checkLaunchGuard(repoPath, agentId, options);
29642
+ return { allowed: true, slot: occupied.slot, readiness };
28666
29643
  }
28667
29644
 
28668
29645
  // src/core/run-service.ts
@@ -28748,11 +29725,26 @@ var RunService = class {
28748
29725
  listArtifacts(options) {
28749
29726
  return this.executor.listArtifacts({ repoPath: options.repoPath }, { stageId: options.stageId });
28750
29727
  }
28751
- async launchEnvironment(options) {
28752
- const guard = checkLaunchGuard(options.repoPath, options.agentId, {
29728
+ async preflightEnvironment(options) {
29729
+ const readiness = inspectSlotReadiness(options.repoPath, options.agentId, {
28753
29730
  confirmReplace: options.confirmReplace,
28754
29731
  force: options.force
28755
29732
  });
29733
+ const report = formatPreflightReport(options.agentId, readiness);
29734
+ return {
29735
+ code: readiness.canLaunch ? 0 : 1,
29736
+ stdout: report + "\n",
29737
+ stderr: readiness.canLaunch ? "" : report + "\n",
29738
+ readiness,
29739
+ blocked: !readiness.canLaunch
29740
+ };
29741
+ }
29742
+ async launchEnvironment(options) {
29743
+ const guard = checkLaunchGuard2(options.repoPath, options.agentId, {
29744
+ confirmReplace: options.confirmReplace,
29745
+ force: options.force,
29746
+ resume: options.resume
29747
+ });
28756
29748
  if (!guard.allowed) {
28757
29749
  const slot = guard.slot;
28758
29750
  return {
@@ -28779,7 +29771,8 @@ var RunService = class {
28779
29771
  worktree: options.worktree,
28780
29772
  claude: options.claude,
28781
29773
  confirmReplace: options.confirmReplace,
28782
- force: options.force
29774
+ force: options.force,
29775
+ resume: options.resume
28783
29776
  },
28784
29777
  trigger: "cli"
28785
29778
  });
@@ -28950,6 +29943,7 @@ var defaultRunService = new RunService();
28950
29943
  var runStage = defaultRunService.runStage.bind(defaultRunService);
28951
29944
  var listArtifacts = defaultRunService.listArtifacts.bind(defaultRunService);
28952
29945
  var launchEnvironment = defaultRunService.launchEnvironment.bind(defaultRunService);
29946
+ var preflightEnvironment = defaultRunService.preflightEnvironment.bind(defaultRunService);
28953
29947
  var runVerification = defaultRunService.runVerification.bind(defaultRunService);
28954
29948
  var teardownEnvironment = defaultRunService.teardownEnvironment.bind(defaultRunService);
28955
29949
  var completeEnvironment = defaultRunService.completeEnvironment.bind(defaultRunService);
@@ -29041,8 +30035,30 @@ var envCommand = {
29041
30035
  type: "boolean",
29042
30036
  default: false,
29043
30037
  describe: "Discard uncommitted changes when replacing a dirty worktree (only after explicit approval)"
30038
+ }).option("resume", {
30039
+ type: "boolean",
30040
+ default: false,
30041
+ describe: "Resume a failed or partial launch without creating a new worktree"
29044
30042
  }),
29045
30043
  handleLaunch
30044
+ ).command(
30045
+ "recover <id>",
30046
+ "Resume a failed or partial launch (alias for launch --resume)",
30047
+ (y2) => y2.positional("id", { type: "number", describe: "Agent slot id (see .har/stages.json agentSlots)" }).option("repo", { type: "string", default: "." }),
30048
+ handleRecover
30049
+ ).command(
30050
+ "preflight <id>",
30051
+ "Check whether a slot can launch now (ports, PM2, Docker, occupied slot)",
30052
+ (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", {
30053
+ type: "boolean",
30054
+ default: false,
30055
+ describe: "Treat an occupied slot as replaceable (same as launch --replace)"
30056
+ }).option("force", {
30057
+ type: "boolean",
30058
+ default: false,
30059
+ describe: "Allow replacing a dirty worktree (only after explicit approval)"
30060
+ }),
30061
+ handlePreflight
29046
30062
  ).command(
29047
30063
  "verify <id>",
29048
30064
  "Run verification suite for an agent",
@@ -29091,12 +30107,12 @@ var envCommand = {
29091
30107
  ).demandCommand(1, "Use: runs list | runs get <runId>"),
29092
30108
  () => {
29093
30109
  }
29094
- ).demandCommand(1, "Please specify a subcommand: init, maintain, add-stage, launch, verify, complete, teardown, status, runs"),
30110
+ ).demandCommand(1, "Please specify a subcommand: init, maintain, add-stage, launch, recover, verify, complete, teardown, status, runs"),
29095
30111
  handler: () => {
29096
30112
  }
29097
30113
  };
29098
30114
  async function handleInit(argv) {
29099
- const repoPath = path25.resolve(argv.repo);
30115
+ const repoPath = path31.resolve(argv.repo);
29100
30116
  header("har env init");
29101
30117
  info(`Repository: ${repoPath}`);
29102
30118
  try {
@@ -29147,7 +30163,7 @@ async function handleInit(argv) {
29147
30163
  }
29148
30164
  }
29149
30165
  async function handleMaintain(argv) {
29150
- const repoPath = path25.resolve(argv.repo);
30166
+ const repoPath = path31.resolve(argv.repo);
29151
30167
  header("har env maintain");
29152
30168
  info(`Repository: ${repoPath}`);
29153
30169
  try {
@@ -29169,20 +30185,36 @@ async function handleMaintain(argv) {
29169
30185
  info("Validating harness...");
29170
30186
  printValidation(result.validation);
29171
30187
  printDrift(result.drift);
29172
- if (!result.validation.pass) {
29173
- warn("Harness has validation errors after maintenance.");
29174
- process.exit(1);
30188
+ if (result.bundle) {
30189
+ printMaintainBundleSummary(result.bundle.report);
29175
30190
  }
29176
- if (argv.auto) {
29177
- await handleAgentMdProposal(repoPath, argv.yes);
29178
- } else if (argv.finalize) {
30191
+ if (argv.finalize) {
30192
+ if (!result.validation.pass) {
30193
+ warn("Harness has validation errors \u2014 fix them before finalizing.");
30194
+ process.exit(1);
30195
+ }
29179
30196
  info("Manifest updated \u2014 generator version and file checksums recorded.");
30197
+ } else if (argv.auto) {
30198
+ if (!result.validation.pass) {
30199
+ warn("Harness has validation errors after maintenance.");
30200
+ process.exit(1);
30201
+ }
30202
+ await handleAgentMdProposal(repoPath, argv.yes);
29180
30203
  } else {
29181
- emitManualAdaptationPrompt(repoPath, "maintain");
30204
+ if (!result.validation.pass) {
30205
+ warn("Harness has validation errors \u2014 fix them before running --finalize.");
30206
+ }
30207
+ emitManualAdaptationPrompt(repoPath, "maintain", "default", result.bundle?.report);
29182
30208
  info("After your coding agent finishes adapting, record it with: har env maintain --finalize");
29183
30209
  }
29184
30210
  divider();
29185
- success("Harness updated!");
30211
+ if (argv.finalize) {
30212
+ success("Harness finalized!");
30213
+ } else if (result.bundle) {
30214
+ success("Maintenance bundle ready!");
30215
+ } else {
30216
+ success("Harness updated!");
30217
+ }
29186
30218
  await handleCursorRule({
29187
30219
  repoPath,
29188
30220
  cursorRule: resolveCursorRuleFlag(argv.cursorRule, argv.noCursorRule),
@@ -29199,8 +30231,8 @@ function resolveCursorRuleFlag(cursorRule, noCursorRule) {
29199
30231
  if (cursorRule) return true;
29200
30232
  return void 0;
29201
30233
  }
29202
- function emitManualAdaptationPrompt(repoPath, mode, profile = "default") {
29203
- const prompt = mode === "init" ? buildInitAdaptationPrompt(repoPath, profile) : buildMaintainAdaptationPrompt(repoPath);
30234
+ function emitManualAdaptationPrompt(repoPath, mode, profile = "default", bundleReport) {
30235
+ const prompt = mode === "init" ? buildInitAdaptationPrompt(repoPath, profile) : buildMaintainAdaptationPrompt(repoPath, bundleReport);
29204
30236
  writeAdaptationPrompt(repoPath, prompt);
29205
30237
  process.stderr.write("\n");
29206
30238
  if (mode === "init") {
@@ -29209,14 +30241,18 @@ function emitManualAdaptationPrompt(repoPath, mode, profile = "default") {
29209
30241
  info("Review harness drift with your coding agent:");
29210
30242
  }
29211
30243
  info(" Paste the prompt below (also saved to .har/ADAPT-PROMPT.md)");
29212
- info(" TODO validation warnings are expected until adaptation is complete.");
30244
+ if (mode === "init") {
30245
+ info(" TODO validation warnings are expected until adaptation is complete.");
30246
+ } else if (bundleReport) {
30247
+ info(" Reference templates are in .har/maintain/templates/");
30248
+ }
29213
30249
  printAdaptationPrompt(prompt);
29214
30250
  }
29215
30251
  async function handleAgentMdProposal(repoPath, autoYes) {
29216
30252
  if (autoYes) {
29217
30253
  const proposal = readAgentMdProposal(repoPath);
29218
30254
  if (proposal) {
29219
- writeFileSafe(path25.join(repoPath, "AGENT.md"), proposal.content);
30255
+ writeFileSafe(path31.join(repoPath, "AGENT.md"), proposal.content);
29220
30256
  clearAgentMdProposal(repoPath);
29221
30257
  info("Applied AGENT.md proposal (--yes)");
29222
30258
  }
@@ -29225,7 +30261,7 @@ async function handleAgentMdProposal(repoPath, autoYes) {
29225
30261
  await promptApplyAgentMdProposal(repoPath);
29226
30262
  }
29227
30263
  async function handleAddStage(argv) {
29228
- const repoPath = path25.resolve(argv.repo);
30264
+ const repoPath = path31.resolve(argv.repo);
29229
30265
  if (argv.template !== "playwright") {
29230
30266
  error(`Unknown stage template: ${argv.template ?? "(missing)"}. Available: playwright`);
29231
30267
  process.exit(1);
@@ -29253,47 +30289,67 @@ async function handleAddStage(argv) {
29253
30289
  process.exit(1);
29254
30290
  }
29255
30291
  }
30292
+ async function handlePreflight(argv) {
30293
+ const repo = path31.resolve(argv.repo);
30294
+ const agentId = validateAgentId(argv.id, repo);
30295
+ const result = await preflightEnvironment({
30296
+ repoPath: repo,
30297
+ agentId,
30298
+ confirmReplace: argv.replace,
30299
+ force: argv.force
30300
+ });
30301
+ if (argv.json) {
30302
+ const output = SlotReadinessSchema.parse(result.readiness);
30303
+ process.stdout.write(JSON.stringify(output, null, 2) + "\n");
30304
+ process.exit(result.code);
30305
+ return;
30306
+ }
30307
+ if (result.stdout) process.stdout.write(result.stdout);
30308
+ process.exit(result.code);
30309
+ }
29256
30310
  async function handleLaunch(argv) {
29257
- const repo = path25.resolve(argv.repo);
30311
+ const repo = path31.resolve(argv.repo);
29258
30312
  const agentId = validateAgentId(argv.id, repo);
29259
30313
  let confirmReplace = argv.replace;
29260
30314
  let force = argv.force;
29261
- const guard = checkLaunchGuard(repo, agentId, { confirmReplace, force });
29262
- if (!guard.allowed && guard.blocked) {
29263
- if (!confirmReplace && process.stdin.isTTY && process.stdout.isTTY) {
29264
- warn("Occupied slot \u2014 review before replacing:");
29265
- console.error(guard.reason ?? "");
29266
- const readline4 = await import("readline");
29267
- const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
29268
- const answer = await new Promise((resolve24) => {
29269
- rl.question("Replace this slot? [y/N] ", resolve24);
29270
- });
29271
- rl.close();
29272
- if (!/^[Yy]$/.test(answer.trim())) {
29273
- error("Aborted \u2014 slot left unchanged.");
29274
- process.exit(2);
29275
- }
29276
- confirmReplace = true;
29277
- }
29278
- }
29279
- const guardAfterConfirm = checkLaunchGuard(repo, agentId, { confirmReplace, force });
29280
- if (!guardAfterConfirm.allowed && guardAfterConfirm.blocked && guardAfterConfirm.slot?.dirty) {
29281
- if (!force && process.stdin.isTTY && process.stdout.isTTY) {
29282
- warn("Worktree has uncommitted changes.");
29283
- const readline4 = await import("readline");
29284
- const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
29285
- const answer = await new Promise((resolve24) => {
29286
- rl.question("Discard uncommitted changes? [y/N] ", resolve24);
29287
- });
29288
- rl.close();
29289
- if (!/^[Yy]$/.test(answer.trim())) {
29290
- error("Aborted \u2014 uncommitted work preserved.");
30315
+ if (!argv.resume) {
30316
+ const guard = checkLaunchGuard2(repo, agentId, { confirmReplace, force });
30317
+ if (!guard.allowed && guard.blocked) {
30318
+ if (!confirmReplace && process.stdin.isTTY && process.stdout.isTTY) {
30319
+ warn("Occupied slot \u2014 review before replacing:");
30320
+ console.error(guard.reason ?? "");
30321
+ const readline4 = await import("readline");
30322
+ const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
30323
+ const answer = await new Promise((resolve26) => {
30324
+ rl.question("Replace this slot? [y/N] ", resolve26);
30325
+ });
30326
+ rl.close();
30327
+ if (!/^[Yy]$/.test(answer.trim())) {
30328
+ error("Aborted \u2014 slot left unchanged.");
30329
+ process.exit(2);
30330
+ }
30331
+ confirmReplace = true;
30332
+ }
30333
+ }
30334
+ const guardAfterConfirm = checkLaunchGuard2(repo, agentId, { confirmReplace, force });
30335
+ if (!guardAfterConfirm.allowed && guardAfterConfirm.blocked && guardAfterConfirm.slot?.dirty) {
30336
+ if (!force && process.stdin.isTTY && process.stdout.isTTY) {
30337
+ warn("Worktree has uncommitted changes.");
30338
+ const readline4 = await import("readline");
30339
+ const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
30340
+ const answer = await new Promise((resolve26) => {
30341
+ rl.question("Discard uncommitted changes? [y/N] ", resolve26);
30342
+ });
30343
+ rl.close();
30344
+ if (!/^[Yy]$/.test(answer.trim())) {
30345
+ error("Aborted \u2014 uncommitted work preserved.");
30346
+ process.exit(2);
30347
+ }
30348
+ force = true;
30349
+ } else if (!force) {
30350
+ error(guardAfterConfirm.reason ?? "Dirty worktree requires --force.");
29291
30351
  process.exit(2);
29292
30352
  }
29293
- force = true;
29294
- } else if (!force) {
29295
- error(guardAfterConfirm.reason ?? "Dirty worktree requires --force.");
29296
- process.exit(2);
29297
30353
  }
29298
30354
  }
29299
30355
  const result = await launchEnvironment({
@@ -29303,6 +30359,7 @@ async function handleLaunch(argv) {
29303
30359
  claude: argv.claude,
29304
30360
  confirmReplace,
29305
30361
  force,
30362
+ resume: argv.resume,
29306
30363
  capture: false
29307
30364
  });
29308
30365
  if (result.blocked) {
@@ -29316,8 +30373,19 @@ async function handleLaunch(argv) {
29316
30373
  }
29317
30374
  process.exit(result.code);
29318
30375
  }
30376
+ async function handleRecover(argv) {
30377
+ return handleLaunch({
30378
+ id: argv.id,
30379
+ repo: argv.repo,
30380
+ worktree: true,
30381
+ claude: false,
30382
+ replace: false,
30383
+ force: false,
30384
+ resume: true
30385
+ });
30386
+ }
29319
30387
  async function handleVerify(argv) {
29320
- const repo = path25.resolve(argv.repo);
30388
+ const repo = path31.resolve(argv.repo);
29321
30389
  const agentId = validateAgentId(argv.id, repo);
29322
30390
  const result = await runVerification({
29323
30391
  repoPath: repo,
@@ -29329,7 +30397,7 @@ async function handleVerify(argv) {
29329
30397
  process.exit(result.code);
29330
30398
  }
29331
30399
  async function handleTeardown(argv) {
29332
- const repo = path25.resolve(argv.repo);
30400
+ const repo = path31.resolve(argv.repo);
29333
30401
  const agentId = validateAgentId(argv.id, repo);
29334
30402
  const result = await teardownEnvironment({
29335
30403
  repoPath: repo,
@@ -29340,7 +30408,7 @@ async function handleTeardown(argv) {
29340
30408
  process.exit(result.code);
29341
30409
  }
29342
30410
  async function handleComplete(argv) {
29343
- const repo = path25.resolve(argv.repo);
30411
+ const repo = path31.resolve(argv.repo);
29344
30412
  const agentId = validateAgentId(argv.id, repo);
29345
30413
  const result = await completeEnvironment({
29346
30414
  repoPath: repo,
@@ -29361,7 +30429,7 @@ async function handleComplete(argv) {
29361
30429
  process.exit(result.code);
29362
30430
  }
29363
30431
  async function handleStatus(argv) {
29364
- const repoPath = path25.resolve(argv.repo);
30432
+ const repoPath = path31.resolve(argv.repo);
29365
30433
  if (argv.json) {
29366
30434
  const status = collectEnvironmentStatus(repoPath);
29367
30435
  const output = EnvironmentStatusSchema.parse(status);
@@ -29374,7 +30442,7 @@ async function handleStatus(argv) {
29374
30442
  });
29375
30443
  }
29376
30444
  async function handleRunsList(argv) {
29377
- const repoPath = path25.resolve(argv.repo);
30445
+ const repoPath = path31.resolve(argv.repo);
29378
30446
  const runs = listRuns(repoPath, { stageId: argv.stage, limit: argv.limit });
29379
30447
  if (argv.json) {
29380
30448
  process.stdout.write(JSON.stringify({ runs }, null, 2) + "\n");
@@ -29391,7 +30459,7 @@ async function handleRunsList(argv) {
29391
30459
  }
29392
30460
  }
29393
30461
  async function handleRunsGet(argv) {
29394
- const run2 = getRun(path25.resolve(argv.repo), argv.runId);
30462
+ const run2 = getRun(path31.resolve(argv.repo), argv.runId);
29395
30463
  if (!run2) {
29396
30464
  error(`Run not found: ${argv.runId}`);
29397
30465
  process.exit(1);
@@ -29426,10 +30494,26 @@ function printDrift(drift) {
29426
30494
  if (drift.extra.length > 0) {
29427
30495
  warn(` Extra/stale files: ${drift.extra.join(", ")}`);
29428
30496
  }
29429
- if (!drift.generatorVersion.outdated && drift.checksumMismatch.length === 0 && drift.missing.length === 0 && drift.extra.length === 0) {
30497
+ if (drift.missingPortVars.length > 0) {
30498
+ warn(
30499
+ ` Missing port documentation vars in harness.env: ${drift.missingPortVars.join(", ")}`
30500
+ );
30501
+ warn(" See maintain/templates/harness.env in the maintenance bundle.");
30502
+ }
30503
+ if (!drift.generatorVersion.outdated && drift.checksumMismatch.length === 0 && drift.missing.length === 0 && drift.extra.length === 0 && drift.missingPortVars.length === 0) {
29430
30504
  success(" Harness matches bundled templates");
29431
30505
  }
29432
30506
  }
30507
+ function printMaintainBundleSummary(report) {
30508
+ const missing = report.actions.filter((a2) => a2.kind === "missing").length;
30509
+ const drifted = report.actions.filter((a2) => a2.kind === "drift").length;
30510
+ const stale = report.stale.length;
30511
+ info(`Maintenance bundle: .har/maintain/`);
30512
+ info(` ${missing} missing, ${drifted} drifted, ${stale} stale`);
30513
+ if (!report.validation.pass) {
30514
+ warn(` Validation: ${report.validation.errors.length} error(s) \u2014 blocks --finalize`);
30515
+ }
30516
+ }
29433
30517
  function printNextSteps(auto2) {
29434
30518
  console.error("");
29435
30519
  console.error(" Read: .har/README.md");
@@ -29448,30 +30532,11 @@ function printNextSteps(auto2) {
29448
30532
  }
29449
30533
 
29450
30534
  // src/cli/commands/control.ts
29451
- var path28 = __toESM(require("path"));
30535
+ var path33 = __toESM(require("path"));
29452
30536
 
29453
30537
  // src/core/control-lifecycle.ts
29454
- var import_child_process4 = require("child_process");
29455
- var path27 = __toESM(require("path"));
29456
-
29457
- // src/core/package-version.ts
29458
- var fs26 = __toESM(require("fs"));
29459
- var path26 = __toESM(require("path"));
29460
- function getHarPackageVersion() {
29461
- if (process.env.HAR_PACKAGE_VERSION) {
29462
- return process.env.HAR_PACKAGE_VERSION;
29463
- }
29464
- const candidates = [
29465
- path26.resolve(__dirname, "..", "package.json"),
29466
- path26.resolve(__dirname, "..", "..", "package.json")
29467
- ];
29468
- for (const packageJsonPath of candidates) {
29469
- if (!fs26.existsSync(packageJsonPath)) continue;
29470
- const pkg = JSON.parse(fs26.readFileSync(packageJsonPath, "utf8"));
29471
- return pkg.version;
29472
- }
29473
- throw new Error("Could not resolve @osfactory/har package.json for version lookup");
29474
- }
30538
+ var import_child_process8 = require("child_process");
30539
+ var path32 = __toESM(require("path"));
29475
30540
 
29476
30541
  // src/core/control-image.ts
29477
30542
  var DEFAULT_CONTROL_IMAGE = "theosfactory/har-control";
@@ -29490,13 +30555,13 @@ function shouldBuildControlLocally() {
29490
30555
 
29491
30556
  // src/core/control-lifecycle.ts
29492
30557
  function resolveControlDir() {
29493
- return path27.resolve(__dirname, "..", "control");
30558
+ return path32.resolve(__dirname, "..", "control");
29494
30559
  }
29495
30560
  function resolveControlComposeFiles(options) {
29496
30561
  const controlDir = resolveControlDir();
29497
- const files = [path27.join(controlDir, "docker-compose.yml")];
30562
+ const files = [path32.join(controlDir, "docker-compose.yml")];
29498
30563
  if (options?.build ?? shouldBuildControlLocally()) {
29499
- files.push(path27.join(controlDir, "docker-compose.build.yml"));
30564
+ files.push(path32.join(controlDir, "docker-compose.build.yml"));
29500
30565
  }
29501
30566
  return files;
29502
30567
  }
@@ -29511,7 +30576,7 @@ function runDockerCompose(args, options) {
29511
30576
  const controlDir = resolveControlDir();
29512
30577
  const composeFiles = resolveControlComposeFiles(options);
29513
30578
  const composeArgs = composeFiles.flatMap((file) => ["-f", file]);
29514
- const result = (0, import_child_process4.spawnSync)("docker", ["compose", ...composeArgs, ...args], {
30579
+ const result = (0, import_child_process8.spawnSync)("docker", ["compose", ...composeArgs, ...args], {
29515
30580
  cwd: controlDir,
29516
30581
  stdio: "inherit",
29517
30582
  env: buildDockerComposeEnv()
@@ -29611,6 +30676,27 @@ var controlCommand = {
29611
30676
  };
29612
30677
  async function handleUp(argv) {
29613
30678
  header("har control up");
30679
+ const readiness = inspectControlUpReadiness(process.cwd());
30680
+ for (const message of readiness.warnings) {
30681
+ warn(message);
30682
+ }
30683
+ if (readiness.controlAlreadyRunning) {
30684
+ success(`Mission Control is already running at ${getControlApiUrl()}`);
30685
+ if (!isControlEnabled()) return;
30686
+ info("Syncing repositories with Mission Control...");
30687
+ const { synced: synced2, failed: failed2, apiReady: apiReady2 } = await syncReposAfterControlStart(process.cwd());
30688
+ if (!apiReady2) {
30689
+ warn("Mission Control API did not become ready \u2014 run har control sync later");
30690
+ return;
30691
+ }
30692
+ if (synced2 > 0) {
30693
+ success(`Synced ${synced2} ${synced2 === 1 ? "repository" : "repositories"} with Mission Control`);
30694
+ }
30695
+ if (failed2 > 0) {
30696
+ warn(`${failed2} ${failed2 === 1 ? "repository" : "repositories"} could not be synced`);
30697
+ }
30698
+ return;
30699
+ }
29614
30700
  const { code, apiUrl, imageRef } = await startMissionControl({
29615
30701
  detach: argv.detach,
29616
30702
  build: argv.build
@@ -29644,7 +30730,7 @@ async function handleDown() {
29644
30730
  process.exit(code);
29645
30731
  }
29646
30732
  async function handleRegister(argv) {
29647
- const repoPath = path28.resolve(argv.repo);
30733
+ const repoPath = path33.resolve(argv.repo);
29648
30734
  header("har control register");
29649
30735
  info(`Repository: ${repoPath}`);
29650
30736
  try {
@@ -29664,7 +30750,7 @@ async function handleRegister(argv) {
29664
30750
  }
29665
30751
  }
29666
30752
  async function handleSync(argv) {
29667
- const repoPath = path28.resolve(argv.repo);
30753
+ const repoPath = path33.resolve(argv.repo);
29668
30754
  try {
29669
30755
  await syncRepoWithControl({
29670
30756
  repoPath,
@@ -29703,7 +30789,7 @@ async function handleWatch(argv) {
29703
30789
  };
29704
30790
  const tick = async () => {
29705
30791
  if (argv.repo) {
29706
- await syncOne(path28.resolve(argv.repo));
30792
+ await syncOne(path33.resolve(argv.repo));
29707
30793
  return;
29708
30794
  }
29709
30795
  try {
@@ -29734,12 +30820,12 @@ async function handleLogin(argv) {
29734
30820
  }
29735
30821
 
29736
30822
  // src/cli/commands/hooks.ts
29737
- var path30 = __toESM(require("path"));
30823
+ var path35 = __toESM(require("path"));
29738
30824
 
29739
30825
  // src/core/hooks.ts
29740
- var fs27 = __toESM(require("fs"));
29741
- var os6 = __toESM(require("os"));
29742
- var path29 = __toESM(require("path"));
30826
+ var fs31 = __toESM(require("fs"));
30827
+ var os8 = __toESM(require("os"));
30828
+ var path34 = __toESM(require("path"));
29743
30829
  var MARKER_START = "# >>> har commit gate (managed by `har hooks`) >>>";
29744
30830
  var MARKER_END = "# <<< har commit gate <<<";
29745
30831
  var PRE_COMMIT_BLOCK = [
@@ -29764,7 +30850,7 @@ function resolveCheckoutRoot(cwd) {
29764
30850
  function resolveHooksDir(checkoutDir) {
29765
30851
  const hooksPath = tryGit2(checkoutDir, "rev-parse --git-path hooks");
29766
30852
  if (!hooksPath) throw new Error(`Not a git checkout: ${checkoutDir}`);
29767
- return path29.resolve(checkoutDir, hooksPath);
30853
+ return path34.resolve(checkoutDir, hooksPath);
29768
30854
  }
29769
30855
  function getConfiguredHooksPath(checkoutDir) {
29770
30856
  return tryGit2(checkoutDir, "config core.hooksPath") || void 0;
@@ -29772,9 +30858,9 @@ function getConfiguredHooksPath(checkoutDir) {
29772
30858
  function defaultHarInvocation() {
29773
30859
  const entry = process.argv[1];
29774
30860
  if (entry && entry.endsWith(".js")) {
29775
- return `"${process.execPath}" "${path29.resolve(entry)}"`;
30861
+ return `"${process.execPath}" "${path34.resolve(entry)}"`;
29776
30862
  }
29777
- if (entry) return `"${path29.resolve(entry)}"`;
30863
+ if (entry) return `"${path34.resolve(entry)}"`;
29778
30864
  return "har";
29779
30865
  }
29780
30866
  function buildHookScript(harInvocation, subcommand, failOpenNotice) {
@@ -29795,40 +30881,40 @@ exit 0
29795
30881
  `;
29796
30882
  }
29797
30883
  function upsertMarkedBlock(filePath, block) {
29798
- if (!fs27.existsSync(filePath)) {
29799
- fs27.writeFileSync(filePath, `#!/bin/sh
30884
+ if (!fs31.existsSync(filePath)) {
30885
+ fs31.writeFileSync(filePath, `#!/bin/sh
29800
30886
  ${block}
29801
30887
  `, { mode: 493 });
29802
30888
  return "created";
29803
30889
  }
29804
- const content = fs27.readFileSync(filePath, "utf8");
30890
+ const content = fs31.readFileSync(filePath, "utf8");
29805
30891
  if (content.includes(MARKER_START)) {
29806
30892
  const pattern = new RegExp(
29807
30893
  `${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}`
29808
30894
  );
29809
- fs27.writeFileSync(filePath, content.replace(pattern, block));
29810
- fs27.chmodSync(filePath, 493);
30895
+ fs31.writeFileSync(filePath, content.replace(pattern, block));
30896
+ fs31.chmodSync(filePath, 493);
29811
30897
  return "updated";
29812
30898
  }
29813
30899
  const suffix = content.endsWith("\n") ? "" : "\n";
29814
- fs27.writeFileSync(filePath, `${content}${suffix}
30900
+ fs31.writeFileSync(filePath, `${content}${suffix}
29815
30901
  ${block}
29816
30902
  `);
29817
- fs27.chmodSync(filePath, 493);
30903
+ fs31.chmodSync(filePath, 493);
29818
30904
  return "appended";
29819
30905
  }
29820
30906
  function removeMarkedBlock(filePath) {
29821
- if (!fs27.existsSync(filePath)) return false;
29822
- const content = fs27.readFileSync(filePath, "utf8");
30907
+ if (!fs31.existsSync(filePath)) return false;
30908
+ const content = fs31.readFileSync(filePath, "utf8");
29823
30909
  if (!content.includes(MARKER_START)) return false;
29824
30910
  const pattern = new RegExp(
29825
30911
  `\\n?${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}\\n?`
29826
30912
  );
29827
30913
  const stripped = content.replace(pattern, "\n");
29828
30914
  if (stripped.replace(/^#!\/bin\/sh\n?/, "").trim() === "") {
29829
- fs27.rmSync(filePath);
30915
+ fs31.rmSync(filePath);
29830
30916
  } else {
29831
- fs27.writeFileSync(filePath, stripped);
30917
+ fs31.writeFileSync(filePath, stripped);
29832
30918
  }
29833
30919
  return true;
29834
30920
  }
@@ -29850,20 +30936,20 @@ Or re-run with --force to write into that directory anyway.`
29850
30936
  );
29851
30937
  }
29852
30938
  const hooksDir = resolveHooksDir(checkout);
29853
- fs27.mkdirSync(hooksDir, { recursive: true });
30939
+ fs31.mkdirSync(hooksDir, { recursive: true });
29854
30940
  const invocation = options.harInvocation ?? defaultHarInvocation();
29855
- fs27.writeFileSync(
29856
- path29.join(hooksDir, "har-pre-commit"),
30941
+ fs31.writeFileSync(
30942
+ path34.join(hooksDir, "har-pre-commit"),
29857
30943
  buildHookScript(invocation, "check", "har: binary not found; skipping commit gate (reinstall with har hooks install)"),
29858
30944
  { mode: 493 }
29859
30945
  );
29860
- fs27.writeFileSync(
29861
- path29.join(hooksDir, "har-post-commit"),
30946
+ fs31.writeFileSync(
30947
+ path34.join(hooksDir, "har-post-commit"),
29862
30948
  buildHookScript(invocation, "record-commit", "har: binary not found; skipping commit association"),
29863
30949
  { mode: 493 }
29864
30950
  );
29865
- const preCommit = upsertMarkedBlock(path29.join(hooksDir, "pre-commit"), PRE_COMMIT_BLOCK);
29866
- const postCommit = upsertMarkedBlock(path29.join(hooksDir, "post-commit"), POST_COMMIT_BLOCK);
30951
+ const preCommit = upsertMarkedBlock(path34.join(hooksDir, "pre-commit"), PRE_COMMIT_BLOCK);
30952
+ const postCommit = upsertMarkedBlock(path34.join(hooksDir, "post-commit"), POST_COMMIT_BLOCK);
29867
30953
  ensureValidationsIgnored(checkout);
29868
30954
  return { hooksDir, preCommit, postCommit };
29869
30955
  }
@@ -29872,12 +30958,12 @@ function uninstallHooks(repoPath) {
29872
30958
  if (!checkout) throw new Error(`Not a git repository: ${repoPath}`);
29873
30959
  const hooksDir = resolveHooksDir(checkout);
29874
30960
  let removed = false;
29875
- removed = removeMarkedBlock(path29.join(hooksDir, "pre-commit")) || removed;
29876
- removed = removeMarkedBlock(path29.join(hooksDir, "post-commit")) || removed;
30961
+ removed = removeMarkedBlock(path34.join(hooksDir, "pre-commit")) || removed;
30962
+ removed = removeMarkedBlock(path34.join(hooksDir, "post-commit")) || removed;
29877
30963
  for (const file of ["har-pre-commit", "har-post-commit"]) {
29878
- const full = path29.join(hooksDir, file);
29879
- if (fs27.existsSync(full)) {
29880
- fs27.rmSync(full);
30964
+ const full = path34.join(hooksDir, file);
30965
+ if (fs31.existsSync(full)) {
30966
+ fs31.rmSync(full);
29881
30967
  removed = true;
29882
30968
  }
29883
30969
  }
@@ -29894,9 +30980,9 @@ function getCommitGateConfig(checkoutDir) {
29894
30980
  function isAgentWorktree(checkoutDir) {
29895
30981
  const branch = getCurrentBranch(checkoutDir);
29896
30982
  if (branch && /^har-agent-\d+$/.test(branch)) return true;
29897
- const worktreesRoot = path29.join(os6.homedir(), "worktrees");
29898
- const resolved = path29.resolve(checkoutDir);
29899
- return resolved.startsWith(`${worktreesRoot}${path29.sep}`) && /-agent-\d+$/.test(path29.basename(resolved));
30983
+ const worktreesRoot = path34.join(os8.homedir(), "worktrees");
30984
+ const resolved = path34.resolve(checkoutDir);
30985
+ return resolved.startsWith(`${worktreesRoot}${path34.sep}`) && /-agent-\d+$/.test(path34.basename(resolved));
29900
30986
  }
29901
30987
  function resolveEffectiveMode(checkoutDir, gate) {
29902
30988
  if (!gate.enabled) return "off";
@@ -29909,15 +30995,15 @@ function getHooksStatus(repoPath) {
29909
30995
  const hooksDir = resolveHooksDir(checkout);
29910
30996
  const gate = getCommitGateConfig(checkout);
29911
30997
  const hasBlock = (name) => {
29912
- const file = path29.join(hooksDir, name);
29913
- return fs27.existsSync(file) && fs27.readFileSync(file, "utf8").includes(MARKER_START);
30998
+ const file = path34.join(hooksDir, name);
30999
+ return fs31.existsSync(file) && fs31.readFileSync(file, "utf8").includes(MARKER_START);
29914
31000
  };
29915
31001
  return {
29916
31002
  checkout,
29917
31003
  hooksDir,
29918
31004
  configuredHooksPath: getConfiguredHooksPath(checkout),
29919
- preCommitInstalled: hasBlock("pre-commit") && fs27.existsSync(path29.join(hooksDir, "har-pre-commit")),
29920
- postCommitInstalled: hasBlock("post-commit") && fs27.existsSync(path29.join(hooksDir, "har-post-commit")),
31005
+ preCommitInstalled: hasBlock("pre-commit") && fs31.existsSync(path34.join(hooksDir, "har-pre-commit")),
31006
+ postCommitInstalled: hasBlock("post-commit") && fs31.existsSync(path34.join(hooksDir, "har-post-commit")),
29921
31007
  gate,
29922
31008
  effectiveMode: resolveEffectiveMode(checkout, gate)
29923
31009
  };
@@ -29928,7 +31014,7 @@ function checkCommitGate(cwd) {
29928
31014
  }
29929
31015
  const checkout = resolveCheckoutRoot(cwd);
29930
31016
  if (!checkout) return { exitCode: 0, messages: [] };
29931
- if (!fs27.existsSync(path29.join(checkout, ".har", "stages.json"))) {
31017
+ if (!fs31.existsSync(path34.join(checkout, ".har", "stages.json"))) {
29932
31018
  return { exitCode: 0, messages: [] };
29933
31019
  }
29934
31020
  const gate = getCommitGateConfig(checkout);
@@ -30004,7 +31090,7 @@ function repoOption(y2) {
30004
31090
  }
30005
31091
  function handleInstall(argv) {
30006
31092
  try {
30007
- const result = installHooks({ repoPath: path30.resolve(argv.repo), force: argv.force });
31093
+ const result = installHooks({ repoPath: path35.resolve(argv.repo), force: argv.force });
30008
31094
  success(`Commit gate installed in ${result.hooksDir}`);
30009
31095
  info(`pre-commit: ${result.preCommit}, post-commit: ${result.postCommit}`);
30010
31096
  info("Commits of unverified change batches will be blocked in agent worktrees.");
@@ -30016,7 +31102,7 @@ function handleInstall(argv) {
30016
31102
  }
30017
31103
  function handleUninstall(argv) {
30018
31104
  try {
30019
- const result = uninstallHooks(path30.resolve(argv.repo));
31105
+ const result = uninstallHooks(path35.resolve(argv.repo));
30020
31106
  if (result.removed) {
30021
31107
  success(`Commit gate removed from ${result.hooksDir}`);
30022
31108
  } else {
@@ -30029,7 +31115,7 @@ function handleUninstall(argv) {
30029
31115
  }
30030
31116
  function handleStatus2(argv) {
30031
31117
  try {
30032
- const status = getHooksStatus(path30.resolve(argv.repo));
31118
+ const status = getHooksStatus(path35.resolve(argv.repo));
30033
31119
  if (argv.json) {
30034
31120
  process.stdout.write(`${JSON.stringify(status, null, 2)}
30035
31121
  `);
@@ -30292,10 +31378,10 @@ function assignProp(target, prop, value) {
30292
31378
  configurable: true
30293
31379
  });
30294
31380
  }
30295
- function getElementAtPath(obj, path32) {
30296
- if (!path32)
31381
+ function getElementAtPath(obj, path37) {
31382
+ if (!path37)
30297
31383
  return obj;
30298
- return path32.reduce((acc, key) => acc?.[key], obj);
31384
+ return path37.reduce((acc, key) => acc?.[key], obj);
30299
31385
  }
30300
31386
  function promiseAllObject(promisesObj) {
30301
31387
  const keys = Object.keys(promisesObj);
@@ -30615,11 +31701,11 @@ function aborted(x2, startIndex = 0) {
30615
31701
  }
30616
31702
  return false;
30617
31703
  }
30618
- function prefixIssues(path32, issues) {
31704
+ function prefixIssues(path37, issues) {
30619
31705
  return issues.map((iss) => {
30620
31706
  var _a3;
30621
31707
  (_a3 = iss).path ?? (_a3.path = []);
30622
- iss.path.unshift(path32);
31708
+ iss.path.unshift(path37);
30623
31709
  return iss;
30624
31710
  });
30625
31711
  }
@@ -35942,7 +37028,7 @@ var Protocol = class {
35942
37028
  return;
35943
37029
  }
35944
37030
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
35945
- await new Promise((resolve24) => setTimeout(resolve24, pollInterval));
37031
+ await new Promise((resolve26) => setTimeout(resolve26, pollInterval));
35946
37032
  options?.signal?.throwIfAborted();
35947
37033
  }
35948
37034
  } catch (error3) {
@@ -35959,7 +37045,7 @@ var Protocol = class {
35959
37045
  */
35960
37046
  request(request, resultSchema, options) {
35961
37047
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
35962
- return new Promise((resolve24, reject) => {
37048
+ return new Promise((resolve26, reject) => {
35963
37049
  const earlyReject = (error3) => {
35964
37050
  reject(error3);
35965
37051
  };
@@ -36037,7 +37123,7 @@ var Protocol = class {
36037
37123
  if (!parseResult.success) {
36038
37124
  reject(parseResult.error);
36039
37125
  } else {
36040
- resolve24(parseResult.data);
37126
+ resolve26(parseResult.data);
36041
37127
  }
36042
37128
  } catch (error3) {
36043
37129
  reject(error3);
@@ -36298,12 +37384,12 @@ var Protocol = class {
36298
37384
  }
36299
37385
  } catch {
36300
37386
  }
36301
- return new Promise((resolve24, reject) => {
37387
+ return new Promise((resolve26, reject) => {
36302
37388
  if (signal.aborted) {
36303
37389
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
36304
37390
  return;
36305
37391
  }
36306
- const timeoutId = setTimeout(resolve24, interval);
37392
+ const timeoutId = setTimeout(resolve26, interval);
36307
37393
  signal.addEventListener("abort", () => {
36308
37394
  clearTimeout(timeoutId);
36309
37395
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -37093,7 +38179,7 @@ var Server = class extends Protocol {
37093
38179
  };
37094
38180
 
37095
38181
  // src/mcp/server.ts
37096
- var path31 = __toESM(require("path"));
38182
+ var path36 = __toESM(require("path"));
37097
38183
 
37098
38184
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
37099
38185
  var import_node_process2 = __toESM(require("node:process"), 1);
@@ -37176,12 +38262,12 @@ var StdioServerTransport = class {
37176
38262
  this.onclose?.();
37177
38263
  }
37178
38264
  send(message) {
37179
- return new Promise((resolve24) => {
38265
+ return new Promise((resolve26) => {
37180
38266
  const json = serializeMessage(message);
37181
38267
  if (this._stdout.write(json)) {
37182
- resolve24();
38268
+ resolve26();
37183
38269
  } else {
37184
- this._stdout.once("drain", resolve24);
38270
+ this._stdout.once("drain", resolve26);
37185
38271
  }
37186
38272
  });
37187
38273
  }
@@ -37263,7 +38349,8 @@ var LaunchEnvironmentInputSchema = external_exports.object({
37263
38349
  ),
37264
38350
  force: external_exports.boolean().default(false).describe(
37265
38351
  "Discard uncommitted changes in a dirty occupied worktree. Requires confirmReplace=true and explicit user approval \u2014 never set autonomously."
37266
- )
38352
+ ),
38353
+ resume: external_exports.boolean().default(false).describe("Resume a failed or partial launch without creating a new worktree.")
37267
38354
  });
37268
38355
  var LaunchEnvironmentOutputSchema = ShellRunOutputSchema.extend({
37269
38356
  previewUrls: external_exports.record(external_exports.string()).optional(),
@@ -37280,6 +38367,16 @@ var LaunchEnvironmentOutputSchema = ShellRunOutputSchema.extend({
37280
38367
  sessionCreatedAt: external_exports.string().optional()
37281
38368
  }).optional()
37282
38369
  });
38370
+ var PreflightEnvironmentInputSchema = external_exports.object({
38371
+ repo: external_exports.string().default("."),
38372
+ agentId: agentIdSchema,
38373
+ confirmReplace: external_exports.boolean().default(false).describe("Treat an occupied slot as replaceable (same as launch confirmReplace)."),
38374
+ force: external_exports.boolean().default(false).describe("Allow replacing a dirty worktree (requires explicit user approval).")
38375
+ });
38376
+ var PreflightEnvironmentOutputSchema = ShellRunOutputSchema.extend({
38377
+ readiness: SlotReadinessSchema,
38378
+ blocked: external_exports.boolean().optional()
38379
+ });
37283
38380
  var CompleteEnvironmentInputSchema = external_exports.object({
37284
38381
  repo: external_exports.string().default("."),
37285
38382
  agentId: agentIdSchema,
@@ -37388,6 +38485,40 @@ var HAR_MCP_TOOLS = [
37388
38485
  force: {
37389
38486
  type: "boolean",
37390
38487
  description: "Discard uncommitted changes when replacing a dirty worktree. Requires confirmReplace=true and explicit user approval."
38488
+ },
38489
+ resume: {
38490
+ type: "boolean",
38491
+ description: "Resume a failed or partial launch (status failed/starting) without --replace. Preserves worktree and env."
38492
+ }
38493
+ },
38494
+ ["agentId"]
38495
+ )
38496
+ },
38497
+ {
38498
+ name: "har_recover_environment",
38499
+ description: "Resume a failed or partial agent launch without replacing the worktree. Alias for har_launch_environment with resume=true.",
38500
+ inputSchema: objectJsonSchema(
38501
+ {
38502
+ repo: repoJsonProperty,
38503
+ agentId: agentIdJsonProperty
38504
+ },
38505
+ ["agentId"]
38506
+ )
38507
+ },
38508
+ {
38509
+ name: "har_preflight_environment",
38510
+ description: "Readiness gate before launch: checks ports, foreign PM2, Docker conflicts, and occupied slot. Returns canLaunch with actionable blockers. Call before har_launch_environment.",
38511
+ inputSchema: objectJsonSchema(
38512
+ {
38513
+ repo: repoJsonProperty,
38514
+ agentId: agentIdJsonProperty,
38515
+ confirmReplace: {
38516
+ type: "boolean",
38517
+ description: "Treat an occupied slot as replaceable (same as launch confirmReplace)."
38518
+ },
38519
+ force: {
38520
+ type: "boolean",
38521
+ description: "Allow replacing a dirty worktree (only after explicit user approval)."
37391
38522
  }
37392
38523
  },
37393
38524
  ["agentId"]
@@ -37540,6 +38671,26 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
37540
38671
  claude: input.claude,
37541
38672
  confirmReplace: input.confirmReplace,
37542
38673
  force: input.force,
38674
+ resume: input.resume,
38675
+ capture: true
38676
+ });
38677
+ const parsed = LaunchEnvironmentOutputSchema.parse(result);
38678
+ return {
38679
+ ...jsonContent(parsed),
38680
+ ...result.blocked ? { isError: true } : {}
38681
+ };
38682
+ }
38683
+ case "har_recover_environment": {
38684
+ const input = LaunchEnvironmentInputSchema.parse({ ...args, repo, resume: true });
38685
+ const agentId = validateAgentId(input.agentId, repo);
38686
+ const result = await launchEnvironment({
38687
+ repoPath: repo,
38688
+ agentId,
38689
+ worktree: input.worktree,
38690
+ claude: input.claude,
38691
+ confirmReplace: false,
38692
+ force: false,
38693
+ resume: true,
37543
38694
  capture: true
37544
38695
  });
37545
38696
  const parsed = LaunchEnvironmentOutputSchema.parse(result);
@@ -37548,6 +38699,21 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
37548
38699
  ...result.blocked ? { isError: true } : {}
37549
38700
  };
37550
38701
  }
38702
+ case "har_preflight_environment": {
38703
+ const input = PreflightEnvironmentInputSchema.parse({ ...args, repo });
38704
+ const agentId = validateAgentId(input.agentId, repo);
38705
+ const result = await preflightEnvironment({
38706
+ repoPath: repo,
38707
+ agentId,
38708
+ confirmReplace: input.confirmReplace,
38709
+ force: input.force
38710
+ });
38711
+ const parsed = PreflightEnvironmentOutputSchema.parse(result);
38712
+ return {
38713
+ ...jsonContent(parsed),
38714
+ ...result.blocked ? { isError: true } : {}
38715
+ };
38716
+ }
37551
38717
  case "har_run_stage": {
37552
38718
  const input = RunStageInputSchema.parse({ ...args, repo });
37553
38719
  const agentId = input.agentId !== void 0 ? validateAgentId(input.agentId, repo) : void 0;
@@ -37645,7 +38811,7 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
37645
38811
  const input = ControlUpInputSchema.parse({ ...args, repo });
37646
38812
  const result = await startControlAndSync({
37647
38813
  detach: input.detach,
37648
- cwd: path31.resolve(input.repo)
38814
+ cwd: path36.resolve(input.repo)
37649
38815
  });
37650
38816
  if (result.code !== 0) {
37651
38817
  return {
@@ -37709,7 +38875,7 @@ var mcpCommand = {
37709
38875
 
37710
38876
  // src/cli/index.ts
37711
38877
  async function runCli() {
37712
- await yargs_default(hideBin(process.argv)).scriptName("har").usage("$0 <command> [options]").command(envCommand).command(controlCommand).command(hooksCommand).command(mcpCommand).demandCommand(1, "Please specify a command. Try: har env init").strict().help().version().parse();
38878
+ await yargs_default(hideBin(process.argv)).scriptName("har").usage("$0 <command> [options]").command(envCommand).command(controlCommand).command(hooksCommand).command(mcpCommand).demandCommand(1, "Please specify a command. Try: har env init").strict().help().version(getHarPackageVersion()).parse();
37713
38879
  }
37714
38880
 
37715
38881
  // src/index.ts