@osfactory/har 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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;
@@ -26966,22 +27014,22 @@ var SHELL_SCRIPTS = [
26966
27014
  function validateHarness(repoPath) {
26967
27015
  const harnessDir = getHarnessDir(repoPath);
26968
27016
  const issues = [];
26969
- if (!fs12.existsSync(harnessDir)) {
27017
+ if (!fs13.existsSync(harnessDir)) {
26970
27018
  return {
26971
27019
  pass: false,
26972
27020
  issues: [{ file: ".har", message: "Harness directory not found", severity: "error" }]
26973
27021
  };
26974
27022
  }
26975
27023
  for (const file of getRequiredFiles(repoPath)) {
26976
- const filePath = path11.join(harnessDir, file);
26977
- if (!fs12.existsSync(filePath)) {
27024
+ const filePath = path12.join(harnessDir, file);
27025
+ if (!fs13.existsSync(filePath)) {
26978
27026
  issues.push({ file, message: "Required file missing", severity: "error" });
26979
27027
  }
26980
27028
  }
26981
27029
  for (const script of SHELL_SCRIPTS) {
26982
- const scriptPath = path11.join(harnessDir, script);
26983
- if (!fs12.existsSync(scriptPath)) continue;
26984
- const stat = fs12.statSync(scriptPath);
27030
+ const scriptPath = path12.join(harnessDir, script);
27031
+ if (!fs13.existsSync(scriptPath)) continue;
27032
+ const stat = fs13.statSync(scriptPath);
26985
27033
  if (!(stat.mode & 73)) {
26986
27034
  issues.push({ file: script, message: "Script is not executable", severity: "warning" });
26987
27035
  }
@@ -26996,9 +27044,9 @@ function validateHarness(repoPath) {
26996
27044
  }
26997
27045
  const manifest = readManifest(repoPath);
26998
27046
  const profile = manifest?.profile ?? "default";
26999
- const ecosystemPath = path11.join(harnessDir, "ecosystem.agent.template.cjs");
27000
- if (profile !== "cli" && fs12.existsSync(ecosystemPath)) {
27001
- 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");
27002
27050
  if (!content.includes("module.exports")) {
27003
27051
  issues.push({
27004
27052
  file: "ecosystem.agent.template.cjs",
@@ -27007,9 +27055,9 @@ function validateHarness(repoPath) {
27007
27055
  });
27008
27056
  }
27009
27057
  }
27010
- const harnessEnvPath = path11.join(harnessDir, "harness.env");
27011
- if (fs12.existsSync(harnessEnvPath)) {
27012
- 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");
27013
27061
  if (content.includes("TODO: set migrate command")) {
27014
27062
  issues.push({ file: "harness.env", message: "Migrate command still has TODO", severity: "warning" });
27015
27063
  }
@@ -27017,17 +27065,17 @@ function validateHarness(repoPath) {
27017
27065
  issues.push({ file: "harness.env", message: "Seed command still has TODO", severity: "warning" });
27018
27066
  }
27019
27067
  }
27020
- const verifyPath = path11.join(harnessDir, "verify.sh");
27021
- if (fs12.existsSync(verifyPath)) {
27022
- 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");
27023
27071
  if (content.includes("echo 'TODO:")) {
27024
27072
  issues.push({ file: "verify.sh", message: "Verification steps still have TODO placeholders", severity: "warning" });
27025
27073
  }
27026
27074
  }
27027
- const stagesPath = path11.join(harnessDir, "stages.json");
27028
- if (fs12.existsSync(stagesPath)) {
27075
+ const stagesPath = path12.join(harnessDir, "stages.json");
27076
+ if (fs13.existsSync(stagesPath)) {
27029
27077
  try {
27030
- JSON.parse(fs12.readFileSync(stagesPath, "utf8"));
27078
+ JSON.parse(fs13.readFileSync(stagesPath, "utf8"));
27031
27079
  const registry2 = readStageRegistry(repoPath);
27032
27080
  if (registry2.stages.length === 0) {
27033
27081
  issues.push({ file: "stages.json", message: "No harness stages declared", severity: "warning" });
@@ -27042,9 +27090,9 @@ function validateHarness(repoPath) {
27042
27090
  } else {
27043
27091
  issues.push({ file: "stages.json", message: "Stage registry missing", severity: "warning" });
27044
27092
  }
27045
- const readmePath = path11.join(harnessDir, "README.md");
27046
- if (fs12.existsSync(readmePath)) {
27047
- 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");
27048
27096
  if (!content.includes(".har")) {
27049
27097
  issues.push({ file: "README.md", message: "README should document .har/ paths", severity: "warning" });
27050
27098
  }
@@ -27058,8 +27106,8 @@ function validateHarness(repoPath) {
27058
27106
  async function smokeTestHarness(repoPath) {
27059
27107
  const harnessDir = getHarnessDir(repoPath);
27060
27108
  const issues = [];
27061
- const setupScript = path11.join(harnessDir, "setup-infra.sh");
27062
- if (fs12.existsSync(setupScript)) {
27109
+ const setupScript = path12.join(harnessDir, "setup-infra.sh");
27110
+ if (fs13.existsSync(setupScript)) {
27063
27111
  const result = run(`bash "${setupScript}"`, { cwd: repoPath });
27064
27112
  if (result.code !== 0) {
27065
27113
  issues.push({
@@ -27074,8 +27122,8 @@ async function smokeTestHarness(repoPath) {
27074
27122
  }
27075
27123
 
27076
27124
  // src/harness/drift.ts
27077
- var fs13 = __toESM(require("fs"));
27078
- var path12 = __toESM(require("path"));
27125
+ var fs14 = __toESM(require("fs"));
27126
+ var path13 = __toESM(require("path"));
27079
27127
  var PROFILE_DIRS2 = {
27080
27128
  default: "har-boilerplate",
27081
27129
  cli: "har-boilerplate-cli",
@@ -27086,37 +27134,90 @@ var CLI_EXPECTED_ABSENT = /* @__PURE__ */ new Set([
27086
27134
  "env.template",
27087
27135
  "attach.sh"
27088
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
+ }
27089
27190
  function substituteProjectName(content, projectName) {
27090
27191
  return content.replace(/__PROJECT_NAME__/g, projectName).replace(/template___PROJECT_NAME__/g, `template_${projectName}`);
27091
27192
  }
27092
27193
  function listBoilerplateFiles(boilerplateDir) {
27093
- if (!fs13.existsSync(boilerplateDir)) return [];
27094
- 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();
27095
27196
  }
27096
27197
  function compareHarnessToTemplate(repoPath) {
27097
- const resolved = path12.resolve(repoPath);
27198
+ const resolved = path13.resolve(repoPath);
27098
27199
  const manifest = readManifest(resolved);
27099
27200
  const profile = manifest?.profile ?? "default";
27100
27201
  const harnessDir = getHarnessDir(resolved);
27101
- const projectName = path12.basename(resolved).toLowerCase().replace(/[^a-z0-9]/g, "_");
27102
- 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]);
27103
27204
  const templateFiles = listBoilerplateFiles(boilerplateDir);
27104
27205
  const missing = [];
27105
27206
  const checksumMismatch = [];
27106
27207
  const extra = [];
27107
27208
  const unchanged = [];
27108
27209
  for (const file of templateFiles) {
27109
- const templatePath = path12.join(boilerplateDir, file);
27110
- const harnessPath = path12.join(harnessDir, file);
27111
- 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");
27112
27213
  if (file === "harness.env") {
27113
27214
  templateContent = substituteProjectName(templateContent, projectName);
27114
27215
  }
27115
- if (!fs13.existsSync(harnessPath)) {
27216
+ if (!fs14.existsSync(harnessPath)) {
27116
27217
  missing.push(file);
27117
27218
  continue;
27118
27219
  }
27119
- const harnessChecksum = computeFileChecksum(fs13.readFileSync(harnessPath, "utf8"));
27220
+ const harnessChecksum = computeFileChecksum(fs14.readFileSync(harnessPath, "utf8"));
27120
27221
  const templateChecksum = computeFileChecksum(templateContent);
27121
27222
  if (harnessChecksum === templateChecksum) {
27122
27223
  unchanged.push(file);
@@ -27124,12 +27225,11 @@ function compareHarnessToTemplate(repoPath) {
27124
27225
  checksumMismatch.push(file);
27125
27226
  }
27126
27227
  }
27127
- if (fs13.existsSync(harnessDir)) {
27128
- for (const file of fs13.readdirSync(harnessDir)) {
27129
- const full = path12.join(harnessDir, file);
27130
- 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;
27131
27232
  if (file === "manifest.json" || file.startsWith("ADAPT-PROMPT")) continue;
27132
- if (templateFiles.includes(file)) continue;
27133
27233
  if (profile === "cli" && CLI_EXPECTED_ABSENT.has(file)) {
27134
27234
  extra.push(file);
27135
27235
  } else if (!templateFiles.includes(file)) {
@@ -27138,6 +27238,8 @@ function compareHarnessToTemplate(repoPath) {
27138
27238
  }
27139
27239
  }
27140
27240
  const installed = manifest?.generatorVersion;
27241
+ const harnessEnv = readHarnessEnv(resolved);
27242
+ const missingPortVars = missingPortDocumentationVars(profile, harnessEnv);
27141
27243
  return {
27142
27244
  generatorVersion: {
27143
27245
  installed,
@@ -27147,71 +27249,355 @@ function compareHarnessToTemplate(repoPath) {
27147
27249
  missing,
27148
27250
  checksumMismatch,
27149
27251
  extra,
27150
- unchanged
27252
+ unchanged,
27253
+ missingPortVars
27151
27254
  };
27152
27255
  }
27153
27256
 
27154
- // src/harness/stage-templates.ts
27257
+ // src/harness/maintain-bundle.ts
27155
27258
  var fs15 = __toESM(require("fs"));
27156
- 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"));
27157
27543
 
27158
27544
  // src/harness/parser.ts
27159
- var fs14 = __toESM(require("fs"));
27545
+ var fs16 = __toESM(require("fs"));
27160
27546
  function harnessExists(repoPath) {
27161
- return fs14.existsSync(`${repoPath}/.har/setup-infra.sh`);
27547
+ return fs16.existsSync(`${repoPath}/.har/setup-infra.sh`);
27162
27548
  }
27163
27549
 
27164
27550
  // src/harness/stage-templates.ts
27165
27551
  function resolveTemplateDir(templateId) {
27166
- const dir = path13.join(resolveTemplatesDir(), "stage-templates", templateId);
27167
- if (!fs15.existsSync(dir)) {
27552
+ const dir = path15.join(resolveTemplatesDir(), "stage-templates", templateId);
27553
+ if (!fs17.existsSync(dir)) {
27168
27554
  throw new Error(`Stage template not found: ${templateId}. Run npm run build.`);
27169
27555
  }
27170
27556
  return dir;
27171
27557
  }
27172
27558
  function readTemplateManifest(templateId) {
27173
- const manifestPath = path13.join(resolveTemplateDir(templateId), "template.manifest.json");
27174
- 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"));
27175
27561
  if (raw.id !== templateId) {
27176
27562
  throw new Error(`Template manifest id mismatch: expected ${templateId}, got ${raw.id}`);
27177
27563
  }
27178
27564
  return raw;
27179
27565
  }
27180
27566
  function ensureParentDir(filePath) {
27181
- const parent = path13.dirname(filePath);
27182
- if (!fs15.existsSync(parent)) {
27183
- fs15.mkdirSync(parent, { recursive: true });
27567
+ const parent = path15.dirname(filePath);
27568
+ if (!fs17.existsSync(parent)) {
27569
+ fs17.mkdirSync(parent, { recursive: true });
27184
27570
  }
27185
27571
  }
27186
27572
  function copyTemplateFile(templateDir, file, repoPath, force) {
27187
- const srcPath = path13.join(templateDir, file.src);
27188
- const destPath = path13.join(repoPath, file.dest);
27189
- 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)) {
27190
27576
  throw new Error(`Template file missing: ${file.src}`);
27191
27577
  }
27192
- if (fs15.existsSync(destPath) && !force) {
27578
+ if (fs17.existsSync(destPath) && !force) {
27193
27579
  throw new Error(
27194
27580
  `File already exists: ${file.dest}. Use --force to overwrite or remove it first.`
27195
27581
  );
27196
27582
  }
27197
27583
  ensureParentDir(destPath);
27198
- fs15.copyFileSync(srcPath, destPath);
27584
+ fs17.copyFileSync(srcPath, destPath);
27199
27585
  if (file.executable) {
27200
- fs15.chmodSync(destPath, 493);
27586
+ fs17.chmodSync(destPath, 493);
27201
27587
  }
27202
27588
  return { written: true, path: file.dest };
27203
27589
  }
27204
27590
  function mergePackageJson(repoPath, templateDir, fragmentRelPath, warnings) {
27205
- const packagePath = path13.join(repoPath, "package.json");
27206
- if (!fs15.existsSync(packagePath)) {
27591
+ const packagePath = path15.join(repoPath, "package.json");
27592
+ if (!fs17.existsSync(packagePath)) {
27207
27593
  throw new Error("No package.json in repo root. Add one before applying the Playwright template.");
27208
27594
  }
27209
- const fragmentPath = path13.join(templateDir, fragmentRelPath);
27210
- if (!fs15.existsSync(fragmentPath)) {
27595
+ const fragmentPath = path15.join(templateDir, fragmentRelPath);
27596
+ if (!fs17.existsSync(fragmentPath)) {
27211
27597
  throw new Error(`Package fragment missing: ${fragmentRelPath}`);
27212
27598
  }
27213
- const pkg = JSON.parse(fs15.readFileSync(packagePath, "utf8"));
27214
- 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"));
27215
27601
  for (const section of ["scripts", "devDependencies"]) {
27216
27602
  const existing = pkg[section] ?? {};
27217
27603
  const incoming = fragment[section] ?? {};
@@ -27224,7 +27610,7 @@ function mergePackageJson(repoPath, templateDir, fragmentRelPath, warnings) {
27224
27610
  }
27225
27611
  pkg[section] = existing;
27226
27612
  }
27227
- fs15.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + "\n");
27613
+ fs17.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + "\n");
27228
27614
  }
27229
27615
  function patchStageRegistry(repoPath, manifest, force) {
27230
27616
  const registry2 = readStageRegistry(repoPath);
@@ -27265,8 +27651,8 @@ function assertHarnessPresent(repoPath) {
27265
27651
  }
27266
27652
  function assertStageNotPresent(repoPath, stageId, force) {
27267
27653
  if (force) return;
27268
- const scriptPath = path13.join(repoPath, ".har", "stages", `${stageId}.sh`);
27269
- if (fs15.existsSync(scriptPath)) {
27654
+ const scriptPath = path15.join(repoPath, ".har", "stages", `${stageId}.sh`);
27655
+ if (fs17.existsSync(scriptPath)) {
27270
27656
  throw new Error(
27271
27657
  `Stage script already exists: .har/stages/${stageId}.sh. Use --force to overwrite.`
27272
27658
  );
@@ -27279,7 +27665,7 @@ function assertStageNotPresent(repoPath, stageId, force) {
27279
27665
  }
27280
27666
  }
27281
27667
  function applyStageTemplate(repoPath, templateId, options = {}) {
27282
- const resolved = path13.resolve(repoPath);
27668
+ const resolved = path15.resolve(repoPath);
27283
27669
  const force = options.force ?? false;
27284
27670
  const warnings = [];
27285
27671
  const filesWritten = [];
@@ -27298,7 +27684,7 @@ function applyStageTemplate(repoPath, templateId, options = {}) {
27298
27684
  if (file.skipFlag === "skipCi" && options.skipCi) {
27299
27685
  continue;
27300
27686
  }
27301
- if (fs15.existsSync(path13.join(resolved, file.dest)) && !force) {
27687
+ if (fs17.existsSync(path15.join(resolved, file.dest)) && !force) {
27302
27688
  warnings.push(`Skipped optional file (exists): ${file.dest}`);
27303
27689
  continue;
27304
27690
  }
@@ -27361,11 +27747,11 @@ function validateAgentId(id, repoPath) {
27361
27747
  // src/core/harness.ts
27362
27748
  function listHarnessScripts(repoPath) {
27363
27749
  const harnessDir = getHarnessDir(repoPath);
27364
- if (!fs16.existsSync(harnessDir)) return [];
27365
- 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();
27366
27752
  }
27367
27753
  function describeProject(repoPath) {
27368
- const resolved = path14.resolve(repoPath);
27754
+ const resolved = path16.resolve(repoPath);
27369
27755
  const manifest = readManifest(resolved);
27370
27756
  const present = harnessExists(resolved);
27371
27757
  return {
@@ -27385,8 +27771,8 @@ function describeProject(repoPath) {
27385
27771
  };
27386
27772
  }
27387
27773
  async function initHarness(options) {
27388
- const repoPath = path14.resolve(options.repoPath);
27389
- if (!fs16.existsSync(repoPath)) {
27774
+ const repoPath = path16.resolve(options.repoPath);
27775
+ if (!fs18.existsSync(repoPath)) {
27390
27776
  throw new Error(`Path not found: ${repoPath}`);
27391
27777
  }
27392
27778
  const scaffold = scaffoldHarnessBoilerplate(repoPath, {
@@ -27417,9 +27803,9 @@ async function initHarness(options) {
27417
27803
  };
27418
27804
  }
27419
27805
  async function maintainHarness(options) {
27420
- const repoPath = path14.resolve(options.repoPath);
27806
+ const repoPath = path16.resolve(options.repoPath);
27421
27807
  const harnessDir = getHarnessDir(repoPath);
27422
- if (!fs16.existsSync(harnessDir)) {
27808
+ if (!fs18.existsSync(harnessDir)) {
27423
27809
  throw new Error('No .har/ found. Run "har env init" first.');
27424
27810
  }
27425
27811
  if (options.auto) {
@@ -27430,6 +27816,7 @@ async function maintainHarness(options) {
27430
27816
  mode: "maintain"
27431
27817
  });
27432
27818
  finalizeHarness(repoPath, authoringResult.summary, authoringResult.stack);
27819
+ removeMaintainBundle(repoPath);
27433
27820
  const validation3 = validateHarness(repoPath);
27434
27821
  return {
27435
27822
  validation: validation3,
@@ -27437,6 +27824,7 @@ async function maintainHarness(options) {
27437
27824
  drift: compareHarnessToTemplate(repoPath)
27438
27825
  };
27439
27826
  }
27827
+ const drift = compareHarnessToTemplate(repoPath);
27440
27828
  const validation2 = validateHarness(repoPath);
27441
27829
  if (options.finalize) {
27442
27830
  if (!validation2.pass) {
@@ -27448,16 +27836,19 @@ async function maintainHarness(options) {
27448
27836
  options.summary ?? "Manual adaptation finalized via har env maintain --finalize",
27449
27837
  existing?.stack
27450
27838
  );
27839
+ removeMaintainBundle(repoPath);
27451
27840
  return {
27452
27841
  validation: validation2,
27453
27842
  adaptationSummary: options.summary,
27454
27843
  drift: compareHarnessToTemplate(repoPath)
27455
27844
  };
27456
27845
  }
27846
+ const bundle = buildMaintainBundle(repoPath, validation2, drift);
27457
27847
  return {
27458
27848
  validation: validation2,
27459
27849
  adaptationSummary: "Manual maintenance \u2014 use coding agent prompt in .har/ADAPT-PROMPT.md",
27460
- drift: compareHarnessToTemplate(repoPath)
27850
+ drift,
27851
+ bundle
27461
27852
  };
27462
27853
  }
27463
27854
  function addStageTemplate(repoPath, templateId, options = {}) {
@@ -27465,8 +27856,8 @@ function addStageTemplate(repoPath, templateId, options = {}) {
27465
27856
  }
27466
27857
 
27467
27858
  // src/harness/adaptation-prompt.ts
27468
- var fs17 = __toESM(require("fs"));
27469
- var path15 = __toESM(require("path"));
27859
+ var fs19 = __toESM(require("fs"));
27860
+ var path17 = __toESM(require("path"));
27470
27861
  var ADAPTATION_PROMPT_FILE = "ADAPT-PROMPT.md";
27471
27862
  var PROFILE_HINTS = {
27472
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.",
@@ -27478,7 +27869,7 @@ function loadTemplate(name) {
27478
27869
  if (!filePath) {
27479
27870
  throw new Error(`Adaptation prompt template not found: ${name}. Run npm run build.`);
27480
27871
  }
27481
- return fs17.readFileSync(filePath, "utf8");
27872
+ return fs19.readFileSync(filePath, "utf8");
27482
27873
  }
27483
27874
  function applyProfilePlaceholders(content, profile) {
27484
27875
  return content.replace(/\{\{PROFILE\}\}/g, profile).replace(/\{\{PROFILE_HINT\}\}/g, PROFILE_HINTS[profile]);
@@ -27486,12 +27877,21 @@ function applyProfilePlaceholders(content, profile) {
27486
27877
  function buildInitAdaptationPrompt(_repoPath, profile) {
27487
27878
  return applyProfilePlaceholders(loadTemplate("adaptation-prompt-init.md"), profile);
27488
27879
  }
27489
- function buildMaintainAdaptationPrompt(_repoPath) {
27490
- 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);
27491
27891
  }
27492
27892
  function writeAdaptationPrompt(repoPath, content) {
27493
27893
  const harnessDir = getHarnessDir(repoPath);
27494
- const filePath = path15.join(harnessDir, ADAPTATION_PROMPT_FILE);
27894
+ const filePath = path17.join(harnessDir, ADAPTATION_PROMPT_FILE);
27495
27895
  writeFileSafe(filePath, content);
27496
27896
  return filePath;
27497
27897
  }
@@ -27510,25 +27910,25 @@ function printAdaptationPrompt(content) {
27510
27910
  }
27511
27911
 
27512
27912
  // src/harness/cursor-rule.ts
27513
- var fs18 = __toESM(require("fs"));
27514
- var path16 = __toESM(require("path"));
27913
+ var fs20 = __toESM(require("fs"));
27914
+ var path18 = __toESM(require("path"));
27515
27915
  var readline3 = __toESM(require("readline"));
27516
27916
  var CURSOR_RULE_RELATIVE_PATH = ".cursor/rules/har-workflow.mdc";
27517
27917
  function getCursorRulePath(repoPath) {
27518
- return path16.join(repoPath, CURSOR_RULE_RELATIVE_PATH);
27918
+ return path18.join(repoPath, CURSOR_RULE_RELATIVE_PATH);
27519
27919
  }
27520
27920
  function isCursorWorkspace(repoPath) {
27521
- return fs18.existsSync(path16.join(repoPath, ".cursor"));
27921
+ return fs20.existsSync(path18.join(repoPath, ".cursor"));
27522
27922
  }
27523
27923
  function cursorRuleExists(repoPath) {
27524
- return fs18.existsSync(getCursorRulePath(repoPath));
27924
+ return fs20.existsSync(getCursorRulePath(repoPath));
27525
27925
  }
27526
27926
  function scaffoldCursorRule(repoPath) {
27527
27927
  const templatePath = resolveTemplateFile("cursor-rule.mdc.template");
27528
27928
  if (!templatePath) {
27529
27929
  throw new Error("Cursor rule template not found (cursor-rule.mdc.template). Run npm run build.");
27530
27930
  }
27531
- const content = fs18.readFileSync(templatePath, "utf8");
27931
+ const content = fs20.readFileSync(templatePath, "utf8");
27532
27932
  writeFileSafe(getCursorRulePath(repoPath), content);
27533
27933
  }
27534
27934
  async function handleCursorRule(options) {
@@ -27566,16 +27966,16 @@ async function promptScaffoldCursorRule(exists) {
27566
27966
  }
27567
27967
  async function askYesNo2(question) {
27568
27968
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
27569
- return new Promise((resolve24) => {
27969
+ return new Promise((resolve26) => {
27570
27970
  process.stderr.write(`${question} `);
27571
27971
  rl.once("line", (answer) => {
27572
27972
  rl.close();
27573
27973
  const trimmed = answer.trim();
27574
27974
  if (trimmed === "") {
27575
- resolve24(true);
27975
+ resolve26(true);
27576
27976
  return;
27577
27977
  }
27578
- resolve24(/^y(es)?$/i.test(trimmed));
27978
+ resolve26(/^y(es)?$/i.test(trimmed));
27579
27979
  });
27580
27980
  });
27581
27981
  }
@@ -27654,17 +28054,65 @@ function buildStageResult(input) {
27654
28054
  }
27655
28055
 
27656
28056
  // src/core/local-executor.ts
27657
- var fs19 = __toESM(require("fs"));
27658
- 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
27659
28085
  function resolveRepoPath(repoPath) {
27660
- 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;
27661
28100
  }
27662
28101
  function computePreviewUrls(repoPath, agentId) {
27663
- 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);
27664
28112
  const feBase = Number(env3.HARNESS_FE_BASE_PORT ?? 3e3);
27665
28113
  const apiBase = Number(env3.HARNESS_API_BASE_PORT ?? 8e3);
27666
- const fePort = feBase + agentId * 10;
27667
- const apiPort = apiBase + agentId * 10;
28114
+ const fePort = feBase + agentId * step;
28115
+ const apiPort = apiBase + agentId * step;
27668
28116
  const urls = {
27669
28117
  frontend: `http://localhost:${fePort}`,
27670
28118
  api: `http://localhost:${apiPort}`
@@ -27673,10 +28121,10 @@ function computePreviewUrls(repoPath, agentId) {
27673
28121
  urls.health = `http://localhost:${apiPort}${env3.HARNESS_HEALTH_CHECK_PATH}`;
27674
28122
  }
27675
28123
  if (env3.HARNESS_INFRA_BROWSER === "true") {
27676
- urls.browser = "http://localhost:13001";
28124
+ urls.browser = `http://localhost:${env3.HARNESS_BROWSER_PORT_DEFAULT ?? 13001}`;
27677
28125
  }
27678
28126
  if (env3.HARNESS_INFRA_MAILPIT === "true") {
27679
- urls.mailpit = "http://localhost:18025";
28127
+ urls.mailpit = `http://localhost:${env3.HARNESS_MAILPIT_WEB_PORT_DEFAULT ?? 18025}`;
27680
28128
  }
27681
28129
  return urls;
27682
28130
  }
@@ -27687,14 +28135,14 @@ function substituteAgentId(value, agentId) {
27687
28135
  function resolveStageScriptPath(repoPath, stage) {
27688
28136
  const harnessDir = getHarnessDir(repoPath);
27689
28137
  if (stage.script) {
27690
- return path17.join(harnessDir, stage.script);
28138
+ return path20.join(harnessDir, stage.script);
27691
28139
  }
27692
28140
  if (stage.command) {
27693
28141
  const scriptName = stage.command.split(/\s+/)[0].replace(/^\.\/\.har\//, "").replace(/^\.\//, "");
27694
- return path17.join(harnessDir, scriptName);
28142
+ return path20.join(harnessDir, scriptName);
27695
28143
  }
27696
- const stageScript = path17.join(harnessDir, "stages", `${stage.id}.sh`);
27697
- if (fs19.existsSync(stageScript)) {
28144
+ const stageScript = path20.join(harnessDir, "stages", `${stage.id}.sh`);
28145
+ if (fs22.existsSync(stageScript)) {
27698
28146
  return stageScript;
27699
28147
  }
27700
28148
  throw new Error(
@@ -27709,7 +28157,7 @@ function buildExecutionPlan(repoPath, stage, options) {
27709
28157
  ...harnessEnv,
27710
28158
  ...stage.env ?? {}
27711
28159
  };
27712
- const cwd = stage.cwd ? path17.resolve(resolvedRepo, stage.cwd) : resolvedRepo;
28160
+ const cwd = stage.cwd ? path20.resolve(resolvedRepo, stage.cwd) : resolvedRepo;
27713
28161
  const extraArgs = options.args ?? [];
27714
28162
  const launchFlagArgs = [];
27715
28163
  if (stage.kind === "launch" && options.launchFlags) {
@@ -27717,6 +28165,7 @@ function buildExecutionPlan(repoPath, stage, options) {
27717
28165
  if (options.launchFlags.claude) launchFlagArgs.push("--claude");
27718
28166
  if (options.launchFlags.confirmReplace) launchFlagArgs.push("--replace");
27719
28167
  if (options.launchFlags.force) launchFlagArgs.push("--force");
28168
+ if (options.launchFlags.resume) launchFlagArgs.push("--resume");
27720
28169
  }
27721
28170
  if (stage.command) {
27722
28171
  let shellCommand = substituteAgentId(stage.command, options.agentId);
@@ -27785,19 +28234,19 @@ var LocalScriptExecutor = class {
27785
28234
  const repoPath = resolveRepoPath(ctx.repoPath);
27786
28235
  const harnessDir = getHarnessDir(repoPath);
27787
28236
  const artifactsDirName = getArtifactsDir(repoPath);
27788
- const artifactsDir = path17.join(harnessDir, artifactsDirName);
27789
- if (!fs19.existsSync(artifactsDir)) return [];
28237
+ const artifactsDir = path20.join(harnessDir, artifactsDirName);
28238
+ if (!fs22.existsSync(artifactsDir)) return [];
27790
28239
  const entries = [];
27791
28240
  const walk = (dir, prefix) => {
27792
- for (const entry of fs19.readdirSync(dir, { withFileTypes: true })) {
27793
- const full = path17.join(dir, entry.name);
27794
- 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);
27795
28244
  if (entry.isDirectory()) {
27796
28245
  walk(full, relative2);
27797
28246
  continue;
27798
28247
  }
27799
28248
  if (filter?.stageId && !relative2.includes(filter.stageId)) continue;
27800
- const stat = fs19.statSync(full);
28249
+ const stat = fs22.statSync(full);
27801
28250
  entries.push({
27802
28251
  path: full,
27803
28252
  relativePath: relative2,
@@ -27813,7 +28262,7 @@ var LocalScriptExecutor = class {
27813
28262
  var localScriptExecutor = new LocalScriptExecutor();
27814
28263
 
27815
28264
  // src/core/control-sync.ts
27816
- var path24 = __toESM(require("path"));
28265
+ var path30 = __toESM(require("path"));
27817
28266
 
27818
28267
  // src/core/control-config.ts
27819
28268
  var DEFAULT_CONTROL_API_URL = "http://localhost:3847";
@@ -27825,32 +28274,32 @@ function isControlEnabled() {
27825
28274
  }
27826
28275
 
27827
28276
  // src/core/control-registry.ts
27828
- var fs20 = __toESM(require("fs"));
27829
- var os2 = __toESM(require("os"));
27830
- var path18 = __toESM(require("path"));
28277
+ var fs23 = __toESM(require("fs"));
28278
+ var os3 = __toESM(require("os"));
28279
+ var path21 = __toESM(require("path"));
27831
28280
  function getRegistryPath() {
27832
28281
  if (process.env.HAR_CONTROL_REGISTRY_PATH) {
27833
- return path18.resolve(process.env.HAR_CONTROL_REGISTRY_PATH);
28282
+ return path21.resolve(process.env.HAR_CONTROL_REGISTRY_PATH);
27834
28283
  }
27835
- return path18.join(os2.homedir(), ".har", "repos.json");
28284
+ return path21.join(os3.homedir(), ".har", "repos.json");
27836
28285
  }
27837
28286
  function readRegistry() {
27838
28287
  const registryPath = getRegistryPath();
27839
28288
  try {
27840
- if (!fs20.existsSync(registryPath)) return { repos: [] };
27841
- return JSON.parse(fs20.readFileSync(registryPath, "utf8"));
28289
+ if (!fs23.existsSync(registryPath)) return { repos: [] };
28290
+ return JSON.parse(fs23.readFileSync(registryPath, "utf8"));
27842
28291
  } catch {
27843
28292
  return { repos: [] };
27844
28293
  }
27845
28294
  }
27846
28295
  function writeRegistry(registry2) {
27847
28296
  const registryPath = getRegistryPath();
27848
- fs20.mkdirSync(path18.dirname(registryPath), { recursive: true });
27849
- 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");
27850
28299
  }
27851
28300
  function recordRepoForControlSync(repoPath) {
27852
28301
  if (process.env.HAR_CONTROL_DISABLED === "true") return;
27853
- const resolved = path18.resolve(repoPath);
28302
+ const resolved = path21.resolve(repoPath);
27854
28303
  if (!readManifest(resolved)) return;
27855
28304
  const registry2 = readRegistry();
27856
28305
  if (registry2.repos.includes(resolved)) return;
@@ -27859,7 +28308,7 @@ function recordRepoForControlSync(repoPath) {
27859
28308
  }
27860
28309
  function listRegisteredRepos() {
27861
28310
  const registry2 = readRegistry();
27862
- const kept = registry2.repos.filter((repoPath) => fs20.existsSync(repoPath) && readManifest(repoPath));
28311
+ const kept = registry2.repos.filter((repoPath) => fs23.existsSync(repoPath) && readManifest(repoPath));
27863
28312
  if (kept.length !== registry2.repos.length) {
27864
28313
  writeRegistry({ repos: kept });
27865
28314
  }
@@ -27867,40 +28316,17 @@ function listRegisteredRepos() {
27867
28316
  }
27868
28317
 
27869
28318
  // src/core/slot-status.ts
27870
- var fs23 = __toESM(require("fs"));
27871
- var os4 = __toESM(require("os"));
27872
- var path21 = __toESM(require("path"));
27873
- 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");
27874
28323
 
27875
28324
  // src/core/runs.ts
27876
28325
  var crypto2 = __toESM(require("crypto"));
27877
28326
  var import_child_process2 = require("child_process");
27878
- var fs22 = __toESM(require("fs"));
27879
- var os3 = __toESM(require("os"));
27880
- var path20 = __toESM(require("path"));
27881
-
27882
- // src/core/slot-registry.ts
27883
- var fs21 = __toESM(require("fs"));
27884
- var path19 = __toESM(require("path"));
27885
- function getSlotRegistryDir(repoPath) {
27886
- return path19.join(getHarnessDir(repoPath), "slots");
27887
- }
27888
- function getSlotRegistryPath(repoPath, agentId) {
27889
- return path19.join(getSlotRegistryDir(repoPath), `agent-${agentId}.json`);
27890
- }
27891
- function readSlotRegistry(repoPath, agentId) {
27892
- const file = getSlotRegistryPath(repoPath, agentId);
27893
- if (!fs21.existsSync(file)) return void 0;
27894
- try {
27895
- const raw = JSON.parse(fs21.readFileSync(file, "utf8"));
27896
- const result = SlotRegistryEntrySchema.safeParse(raw);
27897
- return result.success ? result.data : void 0;
27898
- } catch {
27899
- return void 0;
27900
- }
27901
- }
27902
-
27903
- // src/core/runs.ts
28327
+ var fs24 = __toESM(require("fs"));
28328
+ var os4 = __toESM(require("os"));
28329
+ var path22 = __toESM(require("path"));
27904
28330
  var RUNS_DIR = "runs";
27905
28331
  function gitCommonDir(cwd) {
27906
28332
  try {
@@ -27909,7 +28335,7 @@ function gitCommonDir(cwd) {
27909
28335
  encoding: "utf8",
27910
28336
  stdio: ["pipe", "pipe", "ignore"]
27911
28337
  }).trim();
27912
- return out ? path20.resolve(cwd, out) : void 0;
28338
+ return out ? path22.resolve(cwd, out) : void 0;
27913
28339
  } catch {
27914
28340
  return void 0;
27915
28341
  }
@@ -27931,7 +28357,7 @@ function gitPrefix(cwd) {
27931
28357
  }
27932
28358
  }
27933
28359
  function getRunsDir(harnessRoot) {
27934
- return path20.join(getHarnessDir(harnessRoot), RUNS_DIR);
28360
+ return path22.join(getHarnessDir(harnessRoot), RUNS_DIR);
27935
28361
  }
27936
28362
  function formatLocalDate(d2) {
27937
28363
  const y2 = d2.getFullYear();
@@ -27951,24 +28377,24 @@ function buildRunRelativePath(stageId, agentId, startedAt, runId, runsDir) {
27951
28377
  const timePart = formatLocalTime(started);
27952
28378
  const agentPart = agentId !== void 0 ? `_agent-${agentId}` : "";
27953
28379
  let filename = `${timePart}_${stageId}${agentPart}.json`;
27954
- const fullPath = path20.join(runsDir, dateFolder, filename);
27955
- if (fs22.existsSync(fullPath)) {
28380
+ const fullPath = path22.join(runsDir, dateFolder, filename);
28381
+ if (fs24.existsSync(fullPath)) {
27956
28382
  filename = `${timePart}_${stageId}${agentPart}-${runId.slice(0, 8)}.json`;
27957
28383
  }
27958
- return path20.join(dateFolder, filename);
28384
+ return path22.join(dateFolder, filename);
27959
28385
  }
27960
28386
  function resolveRunFilePath(harnessRoot, run2) {
27961
28387
  if (run2.relativePath) {
27962
- return path20.join(getRunsDir(harnessRoot), run2.relativePath);
28388
+ return path22.join(getRunsDir(harnessRoot), run2.relativePath);
27963
28389
  }
27964
- return path20.join(getRunsDir(harnessRoot), `${run2.runId}.json`);
28390
+ return path22.join(getRunsDir(harnessRoot), `${run2.runId}.json`);
27965
28391
  }
27966
28392
  function collectRunFiles(runsDir) {
27967
- if (!fs22.existsSync(runsDir)) return [];
28393
+ if (!fs24.existsSync(runsDir)) return [];
27968
28394
  const files = [];
27969
28395
  const walk = (dir) => {
27970
- for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
27971
- 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);
27972
28398
  if (entry.isDirectory()) {
27973
28399
  walk(full);
27974
28400
  } else if (entry.name.endsWith(".json")) {
@@ -27982,30 +28408,30 @@ function collectRunFiles(runsDir) {
27982
28408
  function resolveAgentWorkDir(harnessRoot, agentId) {
27983
28409
  if (agentId === void 0) return void 0;
27984
28410
  const entry = readSlotRegistry(harnessRoot, agentId);
27985
- if (entry?.workDir && fs22.existsSync(entry.workDir)) return entry.workDir;
28411
+ if (entry?.workDir && fs24.existsSync(entry.workDir)) return entry.workDir;
27986
28412
  const env3 = readHarnessEnv(harnessRoot);
27987
- const projectName = env3.HARNESS_PROJECT_NAME ?? path20.basename(harnessRoot);
27988
- 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}`);
27989
28415
  const relPrefix = gitPrefix(harnessRoot);
27990
28416
  const sessionEnvFiles = [];
27991
- const worktreesRoot = path20.join(os3.homedir(), "worktrees");
27992
- if (fs22.existsSync(worktreesRoot)) {
28417
+ const worktreesRoot = path22.join(os4.homedir(), "worktrees");
28418
+ if (fs24.existsSync(worktreesRoot)) {
27993
28419
  const suffix = `-har-agent-${agentId}-`;
27994
- for (const entry2 of fs22.readdirSync(worktreesRoot, { withFileTypes: true })) {
28420
+ for (const entry2 of fs24.readdirSync(worktreesRoot, { withFileTypes: true })) {
27995
28421
  if (!entry2.isDirectory() || !entry2.name.includes(suffix)) continue;
27996
- const sessionDir = path20.join(worktreesRoot, entry2.name);
28422
+ const sessionDir = path22.join(worktreesRoot, entry2.name);
27997
28423
  if (!sameGitCheckout(harnessRoot, sessionDir)) continue;
27998
- sessionEnvFiles.push(path20.join(sessionDir, relPrefix, `.env.agent.${agentId}`));
28424
+ sessionEnvFiles.push(path22.join(sessionDir, relPrefix, `.env.agent.${agentId}`));
27999
28425
  }
28000
28426
  }
28001
28427
  const candidates = [
28002
- path20.join(worktreeDir, `.env.agent.${agentId}`),
28003
- path20.join(harnessRoot, `.env.agent.${agentId}`),
28428
+ path22.join(worktreeDir, `.env.agent.${agentId}`),
28429
+ path22.join(harnessRoot, `.env.agent.${agentId}`),
28004
28430
  ...sessionEnvFiles.sort()
28005
28431
  ];
28006
28432
  for (const envFile of candidates) {
28007
- if (!fs22.existsSync(envFile)) continue;
28008
- const content = fs22.readFileSync(envFile, "utf8");
28433
+ if (!fs24.existsSync(envFile)) continue;
28434
+ const content = fs24.readFileSync(envFile, "utf8");
28009
28435
  const match = content.match(/^REPO_ROOT=(.+)$/m);
28010
28436
  if (match) return match[1].trim();
28011
28437
  }
@@ -28017,11 +28443,11 @@ function createRun(ctx, meta) {
28017
28443
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
28018
28444
  const runId = crypto2.randomUUID();
28019
28445
  const relativePath = buildRunRelativePath(meta.stageId, meta.agentId, startedAt, runId, runsDir);
28020
- const runFilePath = path20.join(runsDir, relativePath);
28021
- fs22.mkdirSync(path20.dirname(runFilePath), { recursive: true });
28446
+ const runFilePath = path22.join(runsDir, relativePath);
28447
+ fs24.mkdirSync(path22.dirname(runFilePath), { recursive: true });
28022
28448
  const run2 = RunRecordSchema.parse({
28023
28449
  runId,
28024
- repoPath: path20.resolve(ctx.repoPath),
28450
+ repoPath: path22.resolve(ctx.repoPath),
28025
28451
  harnessRoot,
28026
28452
  stageId: meta.stageId,
28027
28453
  kind: meta.kind,
@@ -28032,7 +28458,7 @@ function createRun(ctx, meta) {
28032
28458
  relativePath,
28033
28459
  trigger: ctx.trigger ?? "cli"
28034
28460
  });
28035
- fs22.writeFileSync(runFilePath, JSON.stringify(run2, null, 2) + "\n");
28461
+ fs24.writeFileSync(runFilePath, JSON.stringify(run2, null, 2) + "\n");
28036
28462
  return run2;
28037
28463
  }
28038
28464
  function finishRun(repoPath, runId, update) {
@@ -28051,13 +28477,13 @@ function finishRun(repoPath, runId, update) {
28051
28477
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
28052
28478
  });
28053
28479
  const runPath = resolveRunFilePath(harnessRoot, finished);
28054
- fs22.writeFileSync(runPath, JSON.stringify(finished, null, 2) + "\n");
28480
+ fs24.writeFileSync(runPath, JSON.stringify(finished, null, 2) + "\n");
28055
28481
  return finished;
28056
28482
  }
28057
28483
  function findRunRecord(harnessRoot, runId) {
28058
28484
  const runsDir = getRunsDir(harnessRoot);
28059
28485
  for (const filePath of collectRunFiles(runsDir)) {
28060
- const parsed = RunRecordSchema.safeParse(JSON.parse(fs22.readFileSync(filePath, "utf8")));
28486
+ const parsed = RunRecordSchema.safeParse(JSON.parse(fs24.readFileSync(filePath, "utf8")));
28061
28487
  if (parsed.success && parsed.data.runId === runId) {
28062
28488
  return parsed.data;
28063
28489
  }
@@ -28069,10 +28495,10 @@ function getRun(repoPath, runId) {
28069
28495
  }
28070
28496
  function listRuns(repoPath, filter = {}) {
28071
28497
  const runsDir = getRunsDir(resolveHarnessRoot(repoPath));
28072
- if (!fs22.existsSync(runsDir)) return [];
28498
+ if (!fs24.existsSync(runsDir)) return [];
28073
28499
  const runs = [];
28074
28500
  for (const filePath of collectRunFiles(runsDir)) {
28075
- const parsed = RunRecordSchema.safeParse(JSON.parse(fs22.readFileSync(filePath, "utf8")));
28501
+ const parsed = RunRecordSchema.safeParse(JSON.parse(fs24.readFileSync(filePath, "utf8")));
28076
28502
  if (!parsed.success) continue;
28077
28503
  if (filter.stageId && parsed.data.stageId !== filter.stageId) continue;
28078
28504
  runs.push(parsed.data);
@@ -28084,11 +28510,552 @@ function listRuns(repoPath, filter = {}) {
28084
28510
  return runs;
28085
28511
  }
28086
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
+
28087
29054
  // src/core/slot-status.ts
28088
29055
  var BYPASS_WARNING_MS = 15 * 60 * 1e3;
28089
29056
  function readGitRemote(repoPath) {
28090
29057
  try {
28091
- 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", {
28092
29059
  cwd: repoPath,
28093
29060
  encoding: "utf8",
28094
29061
  stdio: ["pipe", "pipe", "ignore"]
@@ -28098,9 +29065,9 @@ function readGitRemote(repoPath) {
28098
29065
  return void 0;
28099
29066
  }
28100
29067
  }
28101
- function runGit(cwd, args) {
29068
+ function runGit2(cwd, args) {
28102
29069
  try {
28103
- return (0, import_child_process3.execSync)(`git ${args}`, {
29070
+ return (0, import_child_process7.execSync)(`git ${args}`, {
28104
29071
  cwd,
28105
29072
  encoding: "utf8",
28106
29073
  stdio: ["pipe", "pipe", "ignore"]
@@ -28110,40 +29077,40 @@ function runGit(cwd, args) {
28110
29077
  }
28111
29078
  }
28112
29079
  function readWorktreeBranch(worktreePath) {
28113
- return runGit(worktreePath, "rev-parse --abbrev-ref HEAD");
29080
+ return runGit2(worktreePath, "rev-parse --abbrev-ref HEAD");
28114
29081
  }
28115
- function gitCommonDir2(cwd) {
28116
- const out = runGit(cwd, "rev-parse --git-common-dir");
28117
- 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;
28118
29085
  }
28119
- function sameGitCheckout2(a2, b2) {
28120
- const left2 = gitCommonDir2(a2);
28121
- const right2 = gitCommonDir2(b2);
29086
+ function sameGitCheckout3(a2, b2) {
29087
+ const left2 = gitCommonDir3(a2);
29088
+ const right2 = gitCommonDir3(b2);
28122
29089
  return left2 !== void 0 && right2 !== void 0 && left2 === right2;
28123
29090
  }
28124
29091
  function gitPrefix2(cwd) {
28125
- const out = runGit(cwd, "rev-parse --show-prefix");
29092
+ const out = runGit2(cwd, "rev-parse --show-prefix");
28126
29093
  return out ?? "";
28127
29094
  }
28128
29095
  function discoverSessionWorktreePath(harnessRoot, agentId) {
28129
- const worktreesRoot = path21.join(os4.homedir(), "worktrees");
28130
- if (!fs23.existsSync(worktreesRoot)) return void 0;
29096
+ const worktreesRoot = path27.join(os6.homedir(), "worktrees");
29097
+ if (!fs28.existsSync(worktreesRoot)) return void 0;
28131
29098
  const suffix = `-har-agent-${agentId}-`;
28132
29099
  const relPrefix = gitPrefix2(harnessRoot);
28133
- const matches = fs23.readdirSync(worktreesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.includes(suffix)).map((entry) => path21.join(worktreesRoot, entry.name)).filter(
28134
- (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}`))
28135
29102
  );
28136
29103
  return matches.sort()[0];
28137
29104
  }
28138
29105
  function collectWorktreeDrift(harnessRoot, worktreePath, baseCommit) {
28139
29106
  const drift = {};
28140
- drift.detachedHead = runGit(worktreePath, "symbolic-ref -q HEAD") === void 0;
28141
- const porcelain = runGit(worktreePath, "status --porcelain");
29107
+ drift.detachedHead = runGit2(worktreePath, "symbolic-ref -q HEAD") === void 0;
29108
+ const porcelain = runGit2(worktreePath, "status --porcelain");
28142
29109
  if (porcelain !== void 0) drift.dirty = porcelain.length > 0;
28143
29110
  if (baseCommit) {
28144
- const ahead = runGit(worktreePath, `rev-list --count ${baseCommit}..HEAD`);
29111
+ const ahead = runGit2(worktreePath, `rev-list --count ${baseCommit}..HEAD`);
28145
29112
  if (ahead !== void 0) drift.ahead = Number(ahead);
28146
- const behind = runGit(harnessRoot, `rev-list --count ${baseCommit}..HEAD`);
29113
+ const behind = runGit2(harnessRoot, `rev-list --count ${baseCommit}..HEAD`);
28147
29114
  if (behind !== void 0) {
28148
29115
  drift.behind = Number(behind);
28149
29116
  drift.stale = drift.behind > 0;
@@ -28174,20 +29141,50 @@ function lastBuildPass(run2) {
28174
29141
  const build = data.verification?.stages?.find((s2) => s2.name === "build");
28175
29142
  return build?.pass;
28176
29143
  }
28177
- 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) {
28178
29175
  const env3 = readHarnessEnv(harnessRoot);
28179
- const projectName = env3.HARNESS_PROJECT_NAME ?? path21.basename(harnessRoot);
29176
+ const projectName = env3.HARNESS_PROJECT_NAME ?? path27.basename(harnessRoot);
28180
29177
  const session = readSlotRegistry(harnessRoot, agentId);
28181
- const legacyWorktreePath = path21.join(
28182
- os4.homedir(),
29178
+ const legacyWorktreePath = path27.join(
29179
+ os6.homedir(),
28183
29180
  "worktrees",
28184
29181
  `${projectName}-agent-${agentId}`
28185
29182
  );
28186
- 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);
28187
29184
  const workDir = resolveAgentWorkDir(harnessRoot, agentId);
28188
- const envInWorkDir = workDir ? fs23.existsSync(path21.join(workDir, `.env.agent.${agentId}`)) : false;
28189
- const envInRoot = fs23.existsSync(path21.join(harnessRoot, `.env.agent.${agentId}`));
28190
- 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;
28191
29188
  const active = session !== void 0 && session.status !== "completed" || envInWorkDir || envInRoot || envInWorktree;
28192
29189
  const latest = latestRunForAgent(runs, agentId);
28193
29190
  const verifyRun = runs.find((r2) => r2.agentId === agentId && r2.stageId === "verify");
@@ -28200,6 +29197,12 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
28200
29197
  }
28201
29198
  }
28202
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;
28203
29206
  return {
28204
29207
  agentId,
28205
29208
  active,
@@ -28207,6 +29210,9 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
28207
29210
  worktreePath,
28208
29211
  branch: session?.branch ?? (worktreePath ? readWorktreeBranch(worktreePath) : void 0),
28209
29212
  previewUrls,
29213
+ ports: session?.ports,
29214
+ pm2Issue,
29215
+ readiness,
28210
29216
  harnessUsage: deriveHarnessUsage(latest, active),
28211
29217
  lastRunId: latest?.runId,
28212
29218
  lastRunAt: latest?.startedAt,
@@ -28220,6 +29226,7 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
28220
29226
  purpose: session?.purpose,
28221
29227
  sessionStatus: session?.status,
28222
29228
  lastError: session?.lastError,
29229
+ resumeHint,
28223
29230
  ...drift
28224
29231
  };
28225
29232
  }
@@ -28228,25 +29235,26 @@ function collectEnvironmentStatus(repoPath) {
28228
29235
  const runs = listRuns(harnessRoot, { limit: 200 });
28229
29236
  const manifest = readManifest(harnessRoot);
28230
29237
  const slotIds = getAgentSlotIds(harnessRoot);
29238
+ const pm2Procs = listPm2Processes2();
28231
29239
  return {
28232
- repoPath: path21.resolve(repoPath),
29240
+ repoPath: path27.resolve(repoPath),
28233
29241
  harnessRoot,
28234
29242
  gitRemote: readGitRemote(harnessRoot),
28235
29243
  profile: manifest?.profile,
28236
- slots: slotIds.map((id) => collectSlotStatus(harnessRoot, id, runs)),
29244
+ slots: slotIds.map((id) => collectSlotStatus(harnessRoot, id, runs, pm2Procs)),
28237
29245
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
28238
29246
  };
28239
29247
  }
28240
29248
 
28241
29249
  // src/core/validations.ts
28242
29250
  var crypto3 = __toESM(require("crypto"));
28243
- var fs25 = __toESM(require("fs"));
28244
- var path23 = __toESM(require("path"));
29251
+ var fs30 = __toESM(require("fs"));
29252
+ var path29 = __toESM(require("path"));
28245
29253
 
28246
29254
  // src/core/change-batch.ts
28247
- var fs24 = __toESM(require("fs"));
28248
- var os5 = __toESM(require("os"));
28249
- var path22 = __toESM(require("path"));
29255
+ var fs29 = __toESM(require("fs"));
29256
+ var os7 = __toESM(require("os"));
29257
+ var path28 = __toESM(require("path"));
28250
29258
  function git(checkoutDir, args, env3) {
28251
29259
  const result = run(`git ${args}`, { cwd: checkoutDir, env: env3 });
28252
29260
  if (result.code !== 0) {
@@ -28262,7 +29270,7 @@ function isCheckoutRoot(checkoutDir) {
28262
29270
  const toplevel = tryGit(checkoutDir, "rev-parse --show-toplevel");
28263
29271
  if (!toplevel) return false;
28264
29272
  try {
28265
- return fs24.realpathSync(toplevel) === fs24.realpathSync(checkoutDir);
29273
+ return fs29.realpathSync(toplevel) === fs29.realpathSync(checkoutDir);
28266
29274
  } catch {
28267
29275
  return false;
28268
29276
  }
@@ -28284,7 +29292,7 @@ function isMergeOrRebaseInProgress(checkoutDir) {
28284
29292
  if (tryGit(checkoutDir, "rev-parse -q --verify MERGE_HEAD") !== void 0) return true;
28285
29293
  for (const dir of ["rebase-merge", "rebase-apply"]) {
28286
29294
  const gitPath = tryGit(checkoutDir, `rev-parse --git-path ${dir}`);
28287
- if (gitPath && fs24.existsSync(path22.resolve(checkoutDir, gitPath))) return true;
29295
+ if (gitPath && fs29.existsSync(path28.resolve(checkoutDir, gitPath))) return true;
28288
29296
  }
28289
29297
  return false;
28290
29298
  }
@@ -28307,8 +29315,8 @@ function parseNameStatus(output) {
28307
29315
  return files;
28308
29316
  }
28309
29317
  function computeWorktreeSnapshot(checkoutDir) {
28310
- const tmpDir = fs24.mkdtempSync(path22.join(os5.tmpdir(), "har-idx-"));
28311
- const tmpIndex = path22.join(tmpDir, "index");
29318
+ const tmpDir = fs29.mkdtempSync(path28.join(os7.tmpdir(), "har-idx-"));
29319
+ const tmpIndex = path28.join(tmpDir, "index");
28312
29320
  const env3 = { GIT_INDEX_FILE: tmpIndex };
28313
29321
  try {
28314
29322
  const headTree = getHeadTree(checkoutDir);
@@ -28331,35 +29339,35 @@ function computeWorktreeSnapshot(checkoutDir) {
28331
29339
  changedFiles: parseNameStatus(diffOutput)
28332
29340
  };
28333
29341
  } finally {
28334
- fs24.rmSync(tmpDir, { recursive: true, force: true });
29342
+ fs29.rmSync(tmpDir, { recursive: true, force: true });
28335
29343
  }
28336
29344
  }
28337
29345
 
28338
29346
  // src/core/validations.ts
28339
29347
  var VALIDATIONS_DIR = "validations";
28340
29348
  function getValidationsDir(checkoutDir) {
28341
- return path23.join(checkoutDir, ".har", VALIDATIONS_DIR);
29349
+ return path29.join(checkoutDir, ".har", VALIDATIONS_DIR);
28342
29350
  }
28343
29351
  function validationPath(checkoutDir, treeHash) {
28344
- return path23.join(getValidationsDir(checkoutDir), `${treeHash}.json`);
29352
+ return path29.join(getValidationsDir(checkoutDir), `${treeHash}.json`);
28345
29353
  }
28346
29354
  function writeRecord(checkoutDir, record2) {
28347
29355
  const dir = getValidationsDir(checkoutDir);
28348
- fs25.mkdirSync(dir, { recursive: true });
28349
- fs25.writeFileSync(
29356
+ fs30.mkdirSync(dir, { recursive: true });
29357
+ fs30.writeFileSync(
28350
29358
  validationPath(checkoutDir, record2.treeHash),
28351
29359
  `${JSON.stringify(record2, null, 2)}
28352
29360
  `
28353
29361
  );
28354
29362
  }
28355
29363
  function ensureValidationsIgnored(checkoutDir) {
28356
- const harDir = path23.join(checkoutDir, ".har");
28357
- if (!fs25.existsSync(harDir)) return;
28358
- const gitignorePath = path23.join(harDir, ".gitignore");
28359
- 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") : "";
28360
29368
  if (content.split("\n").some((line) => line.trim() === `${VALIDATIONS_DIR}/`)) return;
28361
29369
  const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
28362
- fs25.writeFileSync(gitignorePath, `${content}${suffix}${VALIDATIONS_DIR}/
29370
+ fs30.writeFileSync(gitignorePath, `${content}${suffix}${VALIDATIONS_DIR}/
28363
29371
  `);
28364
29372
  }
28365
29373
  function recordValidation(input) {
@@ -28376,8 +29384,8 @@ function recordValidation(input) {
28376
29384
  treeHash: snapshot.treeHash,
28377
29385
  headSha: snapshot.headSha,
28378
29386
  branch: snapshot.branch,
28379
- workDir: path23.resolve(input.checkoutDir),
28380
- harnessRoot: path23.resolve(input.harnessRoot),
29387
+ workDir: path29.resolve(input.checkoutDir),
29388
+ harnessRoot: path29.resolve(input.harnessRoot),
28381
29389
  agentId: input.agentId,
28382
29390
  status: input.status,
28383
29391
  full: input.full,
@@ -28389,16 +29397,16 @@ function recordValidation(input) {
28389
29397
  committedAt: existing?.committedAt
28390
29398
  });
28391
29399
  writeRecord(input.checkoutDir, record2);
28392
- if (path23.resolve(input.harnessRoot) !== path23.resolve(input.checkoutDir)) {
29400
+ if (path29.resolve(input.harnessRoot) !== path29.resolve(input.checkoutDir)) {
28393
29401
  writeRecord(input.harnessRoot, record2);
28394
29402
  }
28395
29403
  return record2;
28396
29404
  }
28397
29405
  function findValidation(checkoutDir, treeHash) {
28398
29406
  const file = validationPath(checkoutDir, treeHash);
28399
- if (!fs25.existsSync(file)) return void 0;
29407
+ if (!fs30.existsSync(file)) return void 0;
28400
29408
  try {
28401
- return ValidationRecordSchema.parse(JSON.parse(fs25.readFileSync(file, "utf8")));
29409
+ return ValidationRecordSchema.parse(JSON.parse(fs30.readFileSync(file, "utf8")));
28402
29410
  } catch {
28403
29411
  return void 0;
28404
29412
  }
@@ -28413,19 +29421,19 @@ function attachCommit(checkoutDir, treeHash, commitSha) {
28413
29421
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
28414
29422
  };
28415
29423
  writeRecord(checkoutDir, updated);
28416
- 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"))) {
28417
29425
  writeRecord(record2.harnessRoot, updated);
28418
29426
  }
28419
29427
  return updated;
28420
29428
  }
28421
29429
  function listValidations(harnessRoot) {
28422
29430
  const dir = getValidationsDir(harnessRoot);
28423
- if (!fs25.existsSync(dir)) return [];
29431
+ if (!fs30.existsSync(dir)) return [];
28424
29432
  const records = [];
28425
- for (const entry of fs25.readdirSync(dir)) {
29433
+ for (const entry of fs30.readdirSync(dir)) {
28426
29434
  if (!entry.endsWith(".json")) continue;
28427
29435
  try {
28428
- 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"))));
28429
29437
  } catch {
28430
29438
  }
28431
29439
  }
@@ -28483,7 +29491,7 @@ async function waitForControlApi(apiUrl = getControlApiUrl(), timeoutMs = 6e4, i
28483
29491
  const deadline = Date.now() + timeoutMs;
28484
29492
  while (Date.now() < deadline) {
28485
29493
  if (await isControlApiReachable(apiUrl)) return true;
28486
- await new Promise((resolve24) => setTimeout(resolve24, intervalMs));
29494
+ await new Promise((resolve26) => setTimeout(resolve26, intervalMs));
28487
29495
  }
28488
29496
  return false;
28489
29497
  }
@@ -28495,7 +29503,7 @@ async function syncAllKnownReposWithControl(options) {
28495
29503
  }
28496
29504
  const repoPaths = new Set(listRegisteredRepos());
28497
29505
  if (options?.cwd) {
28498
- const cwd = path24.resolve(options.cwd);
29506
+ const cwd = path30.resolve(options.cwd);
28499
29507
  if (readManifest(cwd)) repoPaths.add(cwd);
28500
29508
  }
28501
29509
  try {
@@ -28503,7 +29511,7 @@ async function syncAllKnownReposWithControl(options) {
28503
29511
  if (listResponse.ok) {
28504
29512
  const repos = await listResponse.json();
28505
29513
  for (const repo of repos) {
28506
- const resolved = path24.resolve(repo.path);
29514
+ const resolved = path30.resolve(repo.path);
28507
29515
  if (readManifest(resolved)) repoPaths.add(resolved);
28508
29516
  }
28509
29517
  }
@@ -28522,7 +29530,7 @@ async function syncAllKnownReposWithControl(options) {
28522
29530
  return { synced, failed };
28523
29531
  }
28524
29532
  async function registerRepoWithControl(options) {
28525
- const repoPath = path24.resolve(options.repoPath);
29533
+ const repoPath = path30.resolve(options.repoPath);
28526
29534
  const apiUrl = options.apiUrl ?? getControlApiUrl();
28527
29535
  const manifest = readManifest(repoPath);
28528
29536
  const stagesRegistry = readStageRegistry(repoPath);
@@ -28535,7 +29543,7 @@ async function registerRepoWithControl(options) {
28535
29543
  return postJson(`${apiUrl}/api/repos`, body, options.dryRun);
28536
29544
  }
28537
29545
  async function syncRepoWithControl(options) {
28538
- const repoPath = path24.resolve(options.repoPath);
29546
+ const repoPath = path30.resolve(options.repoPath);
28539
29547
  const apiUrl = options.apiUrl ?? getControlApiUrl();
28540
29548
  if (options.cloud) {
28541
29549
  const remote = createRemoteExecutor();
@@ -28563,7 +29571,7 @@ async function syncRepoWithControl(options) {
28563
29571
  const listResponse = await fetch(`${apiUrl}/api/repos`);
28564
29572
  if (!listResponse.ok) throw new Error("Failed to list repos from Control API");
28565
29573
  const repos = await listResponse.json();
28566
- const existing = repos.find((r2) => path24.resolve(r2.path) === repoPath);
29574
+ const existing = repos.find((r2) => path30.resolve(r2.path) === repoPath);
28567
29575
  if (!existing) throw new Error(`Repo not registered: ${repoPath}`);
28568
29576
  await syncRepoRunsAndSlots(apiUrl, existing.id, repoPath, options.dryRun);
28569
29577
  return;
@@ -28619,53 +29627,19 @@ async function syncRepoWithControlAsync(repoPath) {
28619
29627
  }
28620
29628
 
28621
29629
  // src/core/slot-launch-guard.ts
28622
- function formatOccupiedSlot(slot) {
28623
- const lines = [
28624
- `Slot ${slot.agentId} is already in use.`,
28625
- slot.worktreePath ? ` Worktree: ${slot.worktreePath}` : void 0,
28626
- slot.branch ? ` Branch: ${slot.branch}` : void 0,
28627
- slot.workDir ? ` Work dir: ${slot.workDir}` : void 0,
28628
- slot.sessionStatus ? ` Status: ${slot.sessionStatus}` : void 0,
28629
- slot.lastError ? ` Error: ${slot.lastError}` : void 0,
28630
- slot.sessionCreatedAt ? ` Since: ${slot.sessionCreatedAt}` : void 0,
28631
- slot.dirty ? " Git: dirty (uncommitted changes \u2014 commit or use force to discard)" : " Git: clean",
28632
- "",
28633
- "Replacing removes the worktree. The session branch is kept only if you committed.",
28634
- "Gitignored paths (state/, runs/, local clones) are NOT preserved.",
28635
- "",
28636
- "To replace: pass confirmReplace=true (MCP), --replace (CLI), or answer y at the prompt.",
28637
- "If the worktree is dirty, also pass force=true / --force after explicit user approval."
28638
- ];
28639
- return lines.filter(Boolean).join("\n");
28640
- }
28641
- function checkLaunchGuard(repoPath, agentId, options = {}) {
28642
- const status = collectEnvironmentStatus(repoPath);
28643
- const slot = status.slots.find((s2) => s2.agentId === agentId);
28644
- if (!slot?.active) {
28645
- return { allowed: true };
28646
- }
28647
- if (!options.confirmReplace) {
29630
+ function checkLaunchGuard2(repoPath, agentId, options = {}) {
29631
+ const readiness = inspectSlotReadiness(repoPath, agentId, options);
29632
+ if (!readiness.canLaunch) {
28648
29633
  return {
28649
29634
  allowed: false,
28650
29635
  blocked: true,
28651
- slot,
28652
- reason: formatOccupiedSlot(slot)
29636
+ reason: formatPreflightReport(agentId, readiness),
29637
+ slot: checkLaunchGuard(repoPath, agentId, options).slot,
29638
+ readiness
28653
29639
  };
28654
29640
  }
28655
- if (slot.dirty && !options.force) {
28656
- return {
28657
- allowed: false,
28658
- blocked: true,
28659
- slot,
28660
- reason: [
28661
- formatOccupiedSlot(slot),
28662
- "",
28663
- "The occupied worktree has uncommitted changes.",
28664
- "Pass force=true / --force to discard them (only after explicit user approval)."
28665
- ].join("\n")
28666
- };
28667
- }
28668
- return { allowed: true, slot };
29641
+ const occupied = checkLaunchGuard(repoPath, agentId, options);
29642
+ return { allowed: true, slot: occupied.slot, readiness };
28669
29643
  }
28670
29644
 
28671
29645
  // src/core/run-service.ts
@@ -28751,11 +29725,26 @@ var RunService = class {
28751
29725
  listArtifacts(options) {
28752
29726
  return this.executor.listArtifacts({ repoPath: options.repoPath }, { stageId: options.stageId });
28753
29727
  }
28754
- async launchEnvironment(options) {
28755
- const guard = checkLaunchGuard(options.repoPath, options.agentId, {
29728
+ async preflightEnvironment(options) {
29729
+ const readiness = inspectSlotReadiness(options.repoPath, options.agentId, {
28756
29730
  confirmReplace: options.confirmReplace,
28757
29731
  force: options.force
28758
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
+ });
28759
29748
  if (!guard.allowed) {
28760
29749
  const slot = guard.slot;
28761
29750
  return {
@@ -28782,7 +29771,8 @@ var RunService = class {
28782
29771
  worktree: options.worktree,
28783
29772
  claude: options.claude,
28784
29773
  confirmReplace: options.confirmReplace,
28785
- force: options.force
29774
+ force: options.force,
29775
+ resume: options.resume
28786
29776
  },
28787
29777
  trigger: "cli"
28788
29778
  });
@@ -28953,6 +29943,7 @@ var defaultRunService = new RunService();
28953
29943
  var runStage = defaultRunService.runStage.bind(defaultRunService);
28954
29944
  var listArtifacts = defaultRunService.listArtifacts.bind(defaultRunService);
28955
29945
  var launchEnvironment = defaultRunService.launchEnvironment.bind(defaultRunService);
29946
+ var preflightEnvironment = defaultRunService.preflightEnvironment.bind(defaultRunService);
28956
29947
  var runVerification = defaultRunService.runVerification.bind(defaultRunService);
28957
29948
  var teardownEnvironment = defaultRunService.teardownEnvironment.bind(defaultRunService);
28958
29949
  var completeEnvironment = defaultRunService.completeEnvironment.bind(defaultRunService);
@@ -29044,8 +30035,30 @@ var envCommand = {
29044
30035
  type: "boolean",
29045
30036
  default: false,
29046
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"
29047
30042
  }),
29048
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
29049
30062
  ).command(
29050
30063
  "verify <id>",
29051
30064
  "Run verification suite for an agent",
@@ -29094,12 +30107,12 @@ var envCommand = {
29094
30107
  ).demandCommand(1, "Use: runs list | runs get <runId>"),
29095
30108
  () => {
29096
30109
  }
29097
- ).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"),
29098
30111
  handler: () => {
29099
30112
  }
29100
30113
  };
29101
30114
  async function handleInit(argv) {
29102
- const repoPath = path25.resolve(argv.repo);
30115
+ const repoPath = path31.resolve(argv.repo);
29103
30116
  header("har env init");
29104
30117
  info(`Repository: ${repoPath}`);
29105
30118
  try {
@@ -29150,7 +30163,7 @@ async function handleInit(argv) {
29150
30163
  }
29151
30164
  }
29152
30165
  async function handleMaintain(argv) {
29153
- const repoPath = path25.resolve(argv.repo);
30166
+ const repoPath = path31.resolve(argv.repo);
29154
30167
  header("har env maintain");
29155
30168
  info(`Repository: ${repoPath}`);
29156
30169
  try {
@@ -29172,20 +30185,36 @@ async function handleMaintain(argv) {
29172
30185
  info("Validating harness...");
29173
30186
  printValidation(result.validation);
29174
30187
  printDrift(result.drift);
29175
- if (!result.validation.pass) {
29176
- warn("Harness has validation errors after maintenance.");
29177
- process.exit(1);
30188
+ if (result.bundle) {
30189
+ printMaintainBundleSummary(result.bundle.report);
29178
30190
  }
29179
- if (argv.auto) {
29180
- await handleAgentMdProposal(repoPath, argv.yes);
29181
- } 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
+ }
29182
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);
29183
30203
  } else {
29184
- 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);
29185
30208
  info("After your coding agent finishes adapting, record it with: har env maintain --finalize");
29186
30209
  }
29187
30210
  divider();
29188
- 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
+ }
29189
30218
  await handleCursorRule({
29190
30219
  repoPath,
29191
30220
  cursorRule: resolveCursorRuleFlag(argv.cursorRule, argv.noCursorRule),
@@ -29202,8 +30231,8 @@ function resolveCursorRuleFlag(cursorRule, noCursorRule) {
29202
30231
  if (cursorRule) return true;
29203
30232
  return void 0;
29204
30233
  }
29205
- function emitManualAdaptationPrompt(repoPath, mode, profile = "default") {
29206
- 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);
29207
30236
  writeAdaptationPrompt(repoPath, prompt);
29208
30237
  process.stderr.write("\n");
29209
30238
  if (mode === "init") {
@@ -29212,14 +30241,18 @@ function emitManualAdaptationPrompt(repoPath, mode, profile = "default") {
29212
30241
  info("Review harness drift with your coding agent:");
29213
30242
  }
29214
30243
  info(" Paste the prompt below (also saved to .har/ADAPT-PROMPT.md)");
29215
- 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
+ }
29216
30249
  printAdaptationPrompt(prompt);
29217
30250
  }
29218
30251
  async function handleAgentMdProposal(repoPath, autoYes) {
29219
30252
  if (autoYes) {
29220
30253
  const proposal = readAgentMdProposal(repoPath);
29221
30254
  if (proposal) {
29222
- writeFileSafe(path25.join(repoPath, "AGENT.md"), proposal.content);
30255
+ writeFileSafe(path31.join(repoPath, "AGENT.md"), proposal.content);
29223
30256
  clearAgentMdProposal(repoPath);
29224
30257
  info("Applied AGENT.md proposal (--yes)");
29225
30258
  }
@@ -29228,7 +30261,7 @@ async function handleAgentMdProposal(repoPath, autoYes) {
29228
30261
  await promptApplyAgentMdProposal(repoPath);
29229
30262
  }
29230
30263
  async function handleAddStage(argv) {
29231
- const repoPath = path25.resolve(argv.repo);
30264
+ const repoPath = path31.resolve(argv.repo);
29232
30265
  if (argv.template !== "playwright") {
29233
30266
  error(`Unknown stage template: ${argv.template ?? "(missing)"}. Available: playwright`);
29234
30267
  process.exit(1);
@@ -29256,47 +30289,67 @@ async function handleAddStage(argv) {
29256
30289
  process.exit(1);
29257
30290
  }
29258
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
+ }
29259
30310
  async function handleLaunch(argv) {
29260
- const repo = path25.resolve(argv.repo);
30311
+ const repo = path31.resolve(argv.repo);
29261
30312
  const agentId = validateAgentId(argv.id, repo);
29262
30313
  let confirmReplace = argv.replace;
29263
30314
  let force = argv.force;
29264
- const guard = checkLaunchGuard(repo, agentId, { confirmReplace, force });
29265
- if (!guard.allowed && guard.blocked) {
29266
- if (!confirmReplace && process.stdin.isTTY && process.stdout.isTTY) {
29267
- warn("Occupied slot \u2014 review before replacing:");
29268
- console.error(guard.reason ?? "");
29269
- const readline4 = await import("readline");
29270
- const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
29271
- const answer = await new Promise((resolve24) => {
29272
- rl.question("Replace this slot? [y/N] ", resolve24);
29273
- });
29274
- rl.close();
29275
- if (!/^[Yy]$/.test(answer.trim())) {
29276
- error("Aborted \u2014 slot left unchanged.");
29277
- process.exit(2);
29278
- }
29279
- confirmReplace = true;
29280
- }
29281
- }
29282
- const guardAfterConfirm = checkLaunchGuard(repo, agentId, { confirmReplace, force });
29283
- if (!guardAfterConfirm.allowed && guardAfterConfirm.blocked && guardAfterConfirm.slot?.dirty) {
29284
- if (!force && process.stdin.isTTY && process.stdout.isTTY) {
29285
- warn("Worktree has uncommitted changes.");
29286
- const readline4 = await import("readline");
29287
- const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
29288
- const answer = await new Promise((resolve24) => {
29289
- rl.question("Discard uncommitted changes? [y/N] ", resolve24);
29290
- });
29291
- rl.close();
29292
- if (!/^[Yy]$/.test(answer.trim())) {
29293
- 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.");
29294
30351
  process.exit(2);
29295
30352
  }
29296
- force = true;
29297
- } else if (!force) {
29298
- error(guardAfterConfirm.reason ?? "Dirty worktree requires --force.");
29299
- process.exit(2);
29300
30353
  }
29301
30354
  }
29302
30355
  const result = await launchEnvironment({
@@ -29306,6 +30359,7 @@ async function handleLaunch(argv) {
29306
30359
  claude: argv.claude,
29307
30360
  confirmReplace,
29308
30361
  force,
30362
+ resume: argv.resume,
29309
30363
  capture: false
29310
30364
  });
29311
30365
  if (result.blocked) {
@@ -29319,8 +30373,19 @@ async function handleLaunch(argv) {
29319
30373
  }
29320
30374
  process.exit(result.code);
29321
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
+ }
29322
30387
  async function handleVerify(argv) {
29323
- const repo = path25.resolve(argv.repo);
30388
+ const repo = path31.resolve(argv.repo);
29324
30389
  const agentId = validateAgentId(argv.id, repo);
29325
30390
  const result = await runVerification({
29326
30391
  repoPath: repo,
@@ -29332,7 +30397,7 @@ async function handleVerify(argv) {
29332
30397
  process.exit(result.code);
29333
30398
  }
29334
30399
  async function handleTeardown(argv) {
29335
- const repo = path25.resolve(argv.repo);
30400
+ const repo = path31.resolve(argv.repo);
29336
30401
  const agentId = validateAgentId(argv.id, repo);
29337
30402
  const result = await teardownEnvironment({
29338
30403
  repoPath: repo,
@@ -29343,7 +30408,7 @@ async function handleTeardown(argv) {
29343
30408
  process.exit(result.code);
29344
30409
  }
29345
30410
  async function handleComplete(argv) {
29346
- const repo = path25.resolve(argv.repo);
30411
+ const repo = path31.resolve(argv.repo);
29347
30412
  const agentId = validateAgentId(argv.id, repo);
29348
30413
  const result = await completeEnvironment({
29349
30414
  repoPath: repo,
@@ -29364,7 +30429,7 @@ async function handleComplete(argv) {
29364
30429
  process.exit(result.code);
29365
30430
  }
29366
30431
  async function handleStatus(argv) {
29367
- const repoPath = path25.resolve(argv.repo);
30432
+ const repoPath = path31.resolve(argv.repo);
29368
30433
  if (argv.json) {
29369
30434
  const status = collectEnvironmentStatus(repoPath);
29370
30435
  const output = EnvironmentStatusSchema.parse(status);
@@ -29377,7 +30442,7 @@ async function handleStatus(argv) {
29377
30442
  });
29378
30443
  }
29379
30444
  async function handleRunsList(argv) {
29380
- const repoPath = path25.resolve(argv.repo);
30445
+ const repoPath = path31.resolve(argv.repo);
29381
30446
  const runs = listRuns(repoPath, { stageId: argv.stage, limit: argv.limit });
29382
30447
  if (argv.json) {
29383
30448
  process.stdout.write(JSON.stringify({ runs }, null, 2) + "\n");
@@ -29394,7 +30459,7 @@ async function handleRunsList(argv) {
29394
30459
  }
29395
30460
  }
29396
30461
  async function handleRunsGet(argv) {
29397
- const run2 = getRun(path25.resolve(argv.repo), argv.runId);
30462
+ const run2 = getRun(path31.resolve(argv.repo), argv.runId);
29398
30463
  if (!run2) {
29399
30464
  error(`Run not found: ${argv.runId}`);
29400
30465
  process.exit(1);
@@ -29429,10 +30494,26 @@ function printDrift(drift) {
29429
30494
  if (drift.extra.length > 0) {
29430
30495
  warn(` Extra/stale files: ${drift.extra.join(", ")}`);
29431
30496
  }
29432
- 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) {
29433
30504
  success(" Harness matches bundled templates");
29434
30505
  }
29435
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
+ }
29436
30517
  function printNextSteps(auto2) {
29437
30518
  console.error("");
29438
30519
  console.error(" Read: .har/README.md");
@@ -29451,30 +30532,11 @@ function printNextSteps(auto2) {
29451
30532
  }
29452
30533
 
29453
30534
  // src/cli/commands/control.ts
29454
- var path28 = __toESM(require("path"));
30535
+ var path33 = __toESM(require("path"));
29455
30536
 
29456
30537
  // src/core/control-lifecycle.ts
29457
- var import_child_process4 = require("child_process");
29458
- var path27 = __toESM(require("path"));
29459
-
29460
- // src/core/package-version.ts
29461
- var fs26 = __toESM(require("fs"));
29462
- var path26 = __toESM(require("path"));
29463
- function getHarPackageVersion() {
29464
- if (process.env.HAR_PACKAGE_VERSION) {
29465
- return process.env.HAR_PACKAGE_VERSION;
29466
- }
29467
- const candidates = [
29468
- path26.resolve(__dirname, "..", "package.json"),
29469
- path26.resolve(__dirname, "..", "..", "package.json")
29470
- ];
29471
- for (const packageJsonPath of candidates) {
29472
- if (!fs26.existsSync(packageJsonPath)) continue;
29473
- const pkg = JSON.parse(fs26.readFileSync(packageJsonPath, "utf8"));
29474
- return pkg.version;
29475
- }
29476
- throw new Error("Could not resolve @osfactory/har package.json for version lookup");
29477
- }
30538
+ var import_child_process8 = require("child_process");
30539
+ var path32 = __toESM(require("path"));
29478
30540
 
29479
30541
  // src/core/control-image.ts
29480
30542
  var DEFAULT_CONTROL_IMAGE = "theosfactory/har-control";
@@ -29493,13 +30555,13 @@ function shouldBuildControlLocally() {
29493
30555
 
29494
30556
  // src/core/control-lifecycle.ts
29495
30557
  function resolveControlDir() {
29496
- return path27.resolve(__dirname, "..", "control");
30558
+ return path32.resolve(__dirname, "..", "control");
29497
30559
  }
29498
30560
  function resolveControlComposeFiles(options) {
29499
30561
  const controlDir = resolveControlDir();
29500
- const files = [path27.join(controlDir, "docker-compose.yml")];
30562
+ const files = [path32.join(controlDir, "docker-compose.yml")];
29501
30563
  if (options?.build ?? shouldBuildControlLocally()) {
29502
- files.push(path27.join(controlDir, "docker-compose.build.yml"));
30564
+ files.push(path32.join(controlDir, "docker-compose.build.yml"));
29503
30565
  }
29504
30566
  return files;
29505
30567
  }
@@ -29514,7 +30576,7 @@ function runDockerCompose(args, options) {
29514
30576
  const controlDir = resolveControlDir();
29515
30577
  const composeFiles = resolveControlComposeFiles(options);
29516
30578
  const composeArgs = composeFiles.flatMap((file) => ["-f", file]);
29517
- const result = (0, import_child_process4.spawnSync)("docker", ["compose", ...composeArgs, ...args], {
30579
+ const result = (0, import_child_process8.spawnSync)("docker", ["compose", ...composeArgs, ...args], {
29518
30580
  cwd: controlDir,
29519
30581
  stdio: "inherit",
29520
30582
  env: buildDockerComposeEnv()
@@ -29614,6 +30676,27 @@ var controlCommand = {
29614
30676
  };
29615
30677
  async function handleUp(argv) {
29616
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
+ }
29617
30700
  const { code, apiUrl, imageRef } = await startMissionControl({
29618
30701
  detach: argv.detach,
29619
30702
  build: argv.build
@@ -29647,7 +30730,7 @@ async function handleDown() {
29647
30730
  process.exit(code);
29648
30731
  }
29649
30732
  async function handleRegister(argv) {
29650
- const repoPath = path28.resolve(argv.repo);
30733
+ const repoPath = path33.resolve(argv.repo);
29651
30734
  header("har control register");
29652
30735
  info(`Repository: ${repoPath}`);
29653
30736
  try {
@@ -29667,7 +30750,7 @@ async function handleRegister(argv) {
29667
30750
  }
29668
30751
  }
29669
30752
  async function handleSync(argv) {
29670
- const repoPath = path28.resolve(argv.repo);
30753
+ const repoPath = path33.resolve(argv.repo);
29671
30754
  try {
29672
30755
  await syncRepoWithControl({
29673
30756
  repoPath,
@@ -29706,7 +30789,7 @@ async function handleWatch(argv) {
29706
30789
  };
29707
30790
  const tick = async () => {
29708
30791
  if (argv.repo) {
29709
- await syncOne(path28.resolve(argv.repo));
30792
+ await syncOne(path33.resolve(argv.repo));
29710
30793
  return;
29711
30794
  }
29712
30795
  try {
@@ -29737,12 +30820,12 @@ async function handleLogin(argv) {
29737
30820
  }
29738
30821
 
29739
30822
  // src/cli/commands/hooks.ts
29740
- var path30 = __toESM(require("path"));
30823
+ var path35 = __toESM(require("path"));
29741
30824
 
29742
30825
  // src/core/hooks.ts
29743
- var fs27 = __toESM(require("fs"));
29744
- var os6 = __toESM(require("os"));
29745
- var path29 = __toESM(require("path"));
30826
+ var fs31 = __toESM(require("fs"));
30827
+ var os8 = __toESM(require("os"));
30828
+ var path34 = __toESM(require("path"));
29746
30829
  var MARKER_START = "# >>> har commit gate (managed by `har hooks`) >>>";
29747
30830
  var MARKER_END = "# <<< har commit gate <<<";
29748
30831
  var PRE_COMMIT_BLOCK = [
@@ -29767,7 +30850,7 @@ function resolveCheckoutRoot(cwd) {
29767
30850
  function resolveHooksDir(checkoutDir) {
29768
30851
  const hooksPath = tryGit2(checkoutDir, "rev-parse --git-path hooks");
29769
30852
  if (!hooksPath) throw new Error(`Not a git checkout: ${checkoutDir}`);
29770
- return path29.resolve(checkoutDir, hooksPath);
30853
+ return path34.resolve(checkoutDir, hooksPath);
29771
30854
  }
29772
30855
  function getConfiguredHooksPath(checkoutDir) {
29773
30856
  return tryGit2(checkoutDir, "config core.hooksPath") || void 0;
@@ -29775,9 +30858,9 @@ function getConfiguredHooksPath(checkoutDir) {
29775
30858
  function defaultHarInvocation() {
29776
30859
  const entry = process.argv[1];
29777
30860
  if (entry && entry.endsWith(".js")) {
29778
- return `"${process.execPath}" "${path29.resolve(entry)}"`;
30861
+ return `"${process.execPath}" "${path34.resolve(entry)}"`;
29779
30862
  }
29780
- if (entry) return `"${path29.resolve(entry)}"`;
30863
+ if (entry) return `"${path34.resolve(entry)}"`;
29781
30864
  return "har";
29782
30865
  }
29783
30866
  function buildHookScript(harInvocation, subcommand, failOpenNotice) {
@@ -29798,40 +30881,40 @@ exit 0
29798
30881
  `;
29799
30882
  }
29800
30883
  function upsertMarkedBlock(filePath, block) {
29801
- if (!fs27.existsSync(filePath)) {
29802
- fs27.writeFileSync(filePath, `#!/bin/sh
30884
+ if (!fs31.existsSync(filePath)) {
30885
+ fs31.writeFileSync(filePath, `#!/bin/sh
29803
30886
  ${block}
29804
30887
  `, { mode: 493 });
29805
30888
  return "created";
29806
30889
  }
29807
- const content = fs27.readFileSync(filePath, "utf8");
30890
+ const content = fs31.readFileSync(filePath, "utf8");
29808
30891
  if (content.includes(MARKER_START)) {
29809
30892
  const pattern = new RegExp(
29810
30893
  `${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}`
29811
30894
  );
29812
- fs27.writeFileSync(filePath, content.replace(pattern, block));
29813
- fs27.chmodSync(filePath, 493);
30895
+ fs31.writeFileSync(filePath, content.replace(pattern, block));
30896
+ fs31.chmodSync(filePath, 493);
29814
30897
  return "updated";
29815
30898
  }
29816
30899
  const suffix = content.endsWith("\n") ? "" : "\n";
29817
- fs27.writeFileSync(filePath, `${content}${suffix}
30900
+ fs31.writeFileSync(filePath, `${content}${suffix}
29818
30901
  ${block}
29819
30902
  `);
29820
- fs27.chmodSync(filePath, 493);
30903
+ fs31.chmodSync(filePath, 493);
29821
30904
  return "appended";
29822
30905
  }
29823
30906
  function removeMarkedBlock(filePath) {
29824
- if (!fs27.existsSync(filePath)) return false;
29825
- const content = fs27.readFileSync(filePath, "utf8");
30907
+ if (!fs31.existsSync(filePath)) return false;
30908
+ const content = fs31.readFileSync(filePath, "utf8");
29826
30909
  if (!content.includes(MARKER_START)) return false;
29827
30910
  const pattern = new RegExp(
29828
30911
  `\\n?${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}\\n?`
29829
30912
  );
29830
30913
  const stripped = content.replace(pattern, "\n");
29831
30914
  if (stripped.replace(/^#!\/bin\/sh\n?/, "").trim() === "") {
29832
- fs27.rmSync(filePath);
30915
+ fs31.rmSync(filePath);
29833
30916
  } else {
29834
- fs27.writeFileSync(filePath, stripped);
30917
+ fs31.writeFileSync(filePath, stripped);
29835
30918
  }
29836
30919
  return true;
29837
30920
  }
@@ -29853,20 +30936,20 @@ Or re-run with --force to write into that directory anyway.`
29853
30936
  );
29854
30937
  }
29855
30938
  const hooksDir = resolveHooksDir(checkout);
29856
- fs27.mkdirSync(hooksDir, { recursive: true });
30939
+ fs31.mkdirSync(hooksDir, { recursive: true });
29857
30940
  const invocation = options.harInvocation ?? defaultHarInvocation();
29858
- fs27.writeFileSync(
29859
- path29.join(hooksDir, "har-pre-commit"),
30941
+ fs31.writeFileSync(
30942
+ path34.join(hooksDir, "har-pre-commit"),
29860
30943
  buildHookScript(invocation, "check", "har: binary not found; skipping commit gate (reinstall with har hooks install)"),
29861
30944
  { mode: 493 }
29862
30945
  );
29863
- fs27.writeFileSync(
29864
- path29.join(hooksDir, "har-post-commit"),
30946
+ fs31.writeFileSync(
30947
+ path34.join(hooksDir, "har-post-commit"),
29865
30948
  buildHookScript(invocation, "record-commit", "har: binary not found; skipping commit association"),
29866
30949
  { mode: 493 }
29867
30950
  );
29868
- const preCommit = upsertMarkedBlock(path29.join(hooksDir, "pre-commit"), PRE_COMMIT_BLOCK);
29869
- 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);
29870
30953
  ensureValidationsIgnored(checkout);
29871
30954
  return { hooksDir, preCommit, postCommit };
29872
30955
  }
@@ -29875,12 +30958,12 @@ function uninstallHooks(repoPath) {
29875
30958
  if (!checkout) throw new Error(`Not a git repository: ${repoPath}`);
29876
30959
  const hooksDir = resolveHooksDir(checkout);
29877
30960
  let removed = false;
29878
- removed = removeMarkedBlock(path29.join(hooksDir, "pre-commit")) || removed;
29879
- 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;
29880
30963
  for (const file of ["har-pre-commit", "har-post-commit"]) {
29881
- const full = path29.join(hooksDir, file);
29882
- if (fs27.existsSync(full)) {
29883
- fs27.rmSync(full);
30964
+ const full = path34.join(hooksDir, file);
30965
+ if (fs31.existsSync(full)) {
30966
+ fs31.rmSync(full);
29884
30967
  removed = true;
29885
30968
  }
29886
30969
  }
@@ -29897,9 +30980,9 @@ function getCommitGateConfig(checkoutDir) {
29897
30980
  function isAgentWorktree(checkoutDir) {
29898
30981
  const branch = getCurrentBranch(checkoutDir);
29899
30982
  if (branch && /^har-agent-\d+$/.test(branch)) return true;
29900
- const worktreesRoot = path29.join(os6.homedir(), "worktrees");
29901
- const resolved = path29.resolve(checkoutDir);
29902
- 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));
29903
30986
  }
29904
30987
  function resolveEffectiveMode(checkoutDir, gate) {
29905
30988
  if (!gate.enabled) return "off";
@@ -29912,15 +30995,15 @@ function getHooksStatus(repoPath) {
29912
30995
  const hooksDir = resolveHooksDir(checkout);
29913
30996
  const gate = getCommitGateConfig(checkout);
29914
30997
  const hasBlock = (name) => {
29915
- const file = path29.join(hooksDir, name);
29916
- 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);
29917
31000
  };
29918
31001
  return {
29919
31002
  checkout,
29920
31003
  hooksDir,
29921
31004
  configuredHooksPath: getConfiguredHooksPath(checkout),
29922
- preCommitInstalled: hasBlock("pre-commit") && fs27.existsSync(path29.join(hooksDir, "har-pre-commit")),
29923
- 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")),
29924
31007
  gate,
29925
31008
  effectiveMode: resolveEffectiveMode(checkout, gate)
29926
31009
  };
@@ -29931,7 +31014,7 @@ function checkCommitGate(cwd) {
29931
31014
  }
29932
31015
  const checkout = resolveCheckoutRoot(cwd);
29933
31016
  if (!checkout) return { exitCode: 0, messages: [] };
29934
- if (!fs27.existsSync(path29.join(checkout, ".har", "stages.json"))) {
31017
+ if (!fs31.existsSync(path34.join(checkout, ".har", "stages.json"))) {
29935
31018
  return { exitCode: 0, messages: [] };
29936
31019
  }
29937
31020
  const gate = getCommitGateConfig(checkout);
@@ -30007,7 +31090,7 @@ function repoOption(y2) {
30007
31090
  }
30008
31091
  function handleInstall(argv) {
30009
31092
  try {
30010
- const result = installHooks({ repoPath: path30.resolve(argv.repo), force: argv.force });
31093
+ const result = installHooks({ repoPath: path35.resolve(argv.repo), force: argv.force });
30011
31094
  success(`Commit gate installed in ${result.hooksDir}`);
30012
31095
  info(`pre-commit: ${result.preCommit}, post-commit: ${result.postCommit}`);
30013
31096
  info("Commits of unverified change batches will be blocked in agent worktrees.");
@@ -30019,7 +31102,7 @@ function handleInstall(argv) {
30019
31102
  }
30020
31103
  function handleUninstall(argv) {
30021
31104
  try {
30022
- const result = uninstallHooks(path30.resolve(argv.repo));
31105
+ const result = uninstallHooks(path35.resolve(argv.repo));
30023
31106
  if (result.removed) {
30024
31107
  success(`Commit gate removed from ${result.hooksDir}`);
30025
31108
  } else {
@@ -30032,7 +31115,7 @@ function handleUninstall(argv) {
30032
31115
  }
30033
31116
  function handleStatus2(argv) {
30034
31117
  try {
30035
- const status = getHooksStatus(path30.resolve(argv.repo));
31118
+ const status = getHooksStatus(path35.resolve(argv.repo));
30036
31119
  if (argv.json) {
30037
31120
  process.stdout.write(`${JSON.stringify(status, null, 2)}
30038
31121
  `);
@@ -30295,10 +31378,10 @@ function assignProp(target, prop, value) {
30295
31378
  configurable: true
30296
31379
  });
30297
31380
  }
30298
- function getElementAtPath(obj, path32) {
30299
- if (!path32)
31381
+ function getElementAtPath(obj, path37) {
31382
+ if (!path37)
30300
31383
  return obj;
30301
- return path32.reduce((acc, key) => acc?.[key], obj);
31384
+ return path37.reduce((acc, key) => acc?.[key], obj);
30302
31385
  }
30303
31386
  function promiseAllObject(promisesObj) {
30304
31387
  const keys = Object.keys(promisesObj);
@@ -30618,11 +31701,11 @@ function aborted(x2, startIndex = 0) {
30618
31701
  }
30619
31702
  return false;
30620
31703
  }
30621
- function prefixIssues(path32, issues) {
31704
+ function prefixIssues(path37, issues) {
30622
31705
  return issues.map((iss) => {
30623
31706
  var _a3;
30624
31707
  (_a3 = iss).path ?? (_a3.path = []);
30625
- iss.path.unshift(path32);
31708
+ iss.path.unshift(path37);
30626
31709
  return iss;
30627
31710
  });
30628
31711
  }
@@ -35945,7 +37028,7 @@ var Protocol = class {
35945
37028
  return;
35946
37029
  }
35947
37030
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
35948
- await new Promise((resolve24) => setTimeout(resolve24, pollInterval));
37031
+ await new Promise((resolve26) => setTimeout(resolve26, pollInterval));
35949
37032
  options?.signal?.throwIfAborted();
35950
37033
  }
35951
37034
  } catch (error3) {
@@ -35962,7 +37045,7 @@ var Protocol = class {
35962
37045
  */
35963
37046
  request(request, resultSchema, options) {
35964
37047
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
35965
- return new Promise((resolve24, reject) => {
37048
+ return new Promise((resolve26, reject) => {
35966
37049
  const earlyReject = (error3) => {
35967
37050
  reject(error3);
35968
37051
  };
@@ -36040,7 +37123,7 @@ var Protocol = class {
36040
37123
  if (!parseResult.success) {
36041
37124
  reject(parseResult.error);
36042
37125
  } else {
36043
- resolve24(parseResult.data);
37126
+ resolve26(parseResult.data);
36044
37127
  }
36045
37128
  } catch (error3) {
36046
37129
  reject(error3);
@@ -36301,12 +37384,12 @@ var Protocol = class {
36301
37384
  }
36302
37385
  } catch {
36303
37386
  }
36304
- return new Promise((resolve24, reject) => {
37387
+ return new Promise((resolve26, reject) => {
36305
37388
  if (signal.aborted) {
36306
37389
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
36307
37390
  return;
36308
37391
  }
36309
- const timeoutId = setTimeout(resolve24, interval);
37392
+ const timeoutId = setTimeout(resolve26, interval);
36310
37393
  signal.addEventListener("abort", () => {
36311
37394
  clearTimeout(timeoutId);
36312
37395
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -37096,7 +38179,7 @@ var Server = class extends Protocol {
37096
38179
  };
37097
38180
 
37098
38181
  // src/mcp/server.ts
37099
- var path31 = __toESM(require("path"));
38182
+ var path36 = __toESM(require("path"));
37100
38183
 
37101
38184
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
37102
38185
  var import_node_process2 = __toESM(require("node:process"), 1);
@@ -37179,12 +38262,12 @@ var StdioServerTransport = class {
37179
38262
  this.onclose?.();
37180
38263
  }
37181
38264
  send(message) {
37182
- return new Promise((resolve24) => {
38265
+ return new Promise((resolve26) => {
37183
38266
  const json = serializeMessage(message);
37184
38267
  if (this._stdout.write(json)) {
37185
- resolve24();
38268
+ resolve26();
37186
38269
  } else {
37187
- this._stdout.once("drain", resolve24);
38270
+ this._stdout.once("drain", resolve26);
37188
38271
  }
37189
38272
  });
37190
38273
  }
@@ -37266,7 +38349,8 @@ var LaunchEnvironmentInputSchema = external_exports.object({
37266
38349
  ),
37267
38350
  force: external_exports.boolean().default(false).describe(
37268
38351
  "Discard uncommitted changes in a dirty occupied worktree. Requires confirmReplace=true and explicit user approval \u2014 never set autonomously."
37269
- )
38352
+ ),
38353
+ resume: external_exports.boolean().default(false).describe("Resume a failed or partial launch without creating a new worktree.")
37270
38354
  });
37271
38355
  var LaunchEnvironmentOutputSchema = ShellRunOutputSchema.extend({
37272
38356
  previewUrls: external_exports.record(external_exports.string()).optional(),
@@ -37283,6 +38367,16 @@ var LaunchEnvironmentOutputSchema = ShellRunOutputSchema.extend({
37283
38367
  sessionCreatedAt: external_exports.string().optional()
37284
38368
  }).optional()
37285
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
+ });
37286
38380
  var CompleteEnvironmentInputSchema = external_exports.object({
37287
38381
  repo: external_exports.string().default("."),
37288
38382
  agentId: agentIdSchema,
@@ -37391,6 +38485,40 @@ var HAR_MCP_TOOLS = [
37391
38485
  force: {
37392
38486
  type: "boolean",
37393
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)."
37394
38522
  }
37395
38523
  },
37396
38524
  ["agentId"]
@@ -37543,6 +38671,26 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
37543
38671
  claude: input.claude,
37544
38672
  confirmReplace: input.confirmReplace,
37545
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,
37546
38694
  capture: true
37547
38695
  });
37548
38696
  const parsed = LaunchEnvironmentOutputSchema.parse(result);
@@ -37551,6 +38699,21 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
37551
38699
  ...result.blocked ? { isError: true } : {}
37552
38700
  };
37553
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
+ }
37554
38717
  case "har_run_stage": {
37555
38718
  const input = RunStageInputSchema.parse({ ...args, repo });
37556
38719
  const agentId = input.agentId !== void 0 ? validateAgentId(input.agentId, repo) : void 0;
@@ -37648,7 +38811,7 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
37648
38811
  const input = ControlUpInputSchema.parse({ ...args, repo });
37649
38812
  const result = await startControlAndSync({
37650
38813
  detach: input.detach,
37651
- cwd: path31.resolve(input.repo)
38814
+ cwd: path36.resolve(input.repo)
37652
38815
  });
37653
38816
  if (result.code !== 0) {
37654
38817
  return {
@@ -37712,7 +38875,7 @@ var mcpCommand = {
37712
38875
 
37713
38876
  // src/cli/index.ts
37714
38877
  async function runCli() {
37715
- 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();
37716
38879
  }
37717
38880
 
37718
38881
  // src/index.ts