@osfactory/har 0.10.0 → 0.12.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 path40 = url.path;
769
- if (path40.length === 0) {
768
+ const path47 = url.path;
769
+ if (path47.length === 0) {
770
770
  return;
771
771
  }
772
- if (url.scheme === "file" && path40.length === 1 && isNormalizedWindowsDriveLetter(path40[0])) {
772
+ if (url.scheme === "file" && path47.length === 1 && isNormalizedWindowsDriveLetter(path47[0])) {
773
773
  return;
774
774
  }
775
- path40.pop();
775
+ path47.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(resolve27, reject) {
2095
+ return new Body.Promise(function(resolve33, 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
- resolve27(Buffer.concat(accum, accumBytes));
2129
+ resolve33(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(resolve27, reject) {
2804
+ return new fetch3.Promise(function(resolve33, 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
- resolve27(fetch3(new Request3(locationURL, requestOpts)));
2937
+ resolve33(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
- resolve27(response);
2958
+ resolve33(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
- resolve27(response);
2968
+ resolve33(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
- resolve27(response);
2980
+ resolve33(response);
2981
2981
  });
2982
2982
  raw.on("end", function() {
2983
2983
  if (!response) {
2984
2984
  response = new Response3(body, response_options);
2985
- resolve27(response);
2985
+ resolve33(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
- resolve27(response);
2993
+ resolve33(response);
2994
2994
  return;
2995
2995
  }
2996
2996
  response = new Response3(body, response_options);
2997
- resolve27(response);
2997
+ resolve33(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(path40, { mtimeMs, size }, filenameOrOptions, options = {}) {
6354
+ function createFileFromPath(path47, { 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: path40, size, lastModified: mtimeMs });
6361
+ const file = new FileFromPath({ path: path47, size, lastModified: mtimeMs });
6362
6362
  if (!filename) {
6363
6363
  filename = file.name;
6364
6364
  }
@@ -6367,13 +6367,13 @@ function createFileFromPath(path40, { mtimeMs, size }, filenameOrOptions, option
6367
6367
  lastModified: file.lastModified
6368
6368
  });
6369
6369
  }
6370
- function fileFromPathSync(path40, filenameOrOptions, options = {}) {
6371
- const stats = (0, import_fs5.statSync)(path40);
6372
- return createFileFromPath(path40, stats, filenameOrOptions, options);
6370
+ function fileFromPathSync(path47, filenameOrOptions, options = {}) {
6371
+ const stats = (0, import_fs5.statSync)(path47);
6372
+ return createFileFromPath(path47, stats, filenameOrOptions, options);
6373
6373
  }
6374
- async function fileFromPath2(path40, filenameOrOptions, options) {
6375
- const stats = await import_fs5.promises.stat(path40);
6376
- return createFileFromPath(path40, stats, filenameOrOptions, options);
6374
+ async function fileFromPath2(path47, filenameOrOptions, options) {
6375
+ const stats = await import_fs5.promises.stat(path47);
6376
+ return createFileFromPath(path47, 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 = resolve27.call(this, root, ref);
9387
+ let _sch = resolve33.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 resolve27(root, ref) {
9414
+ function resolve33(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(path40) {
9633
- let input = path40;
9632
+ function removeDotSegments(path47) {
9633
+ let input = path47;
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 [path40, query] = wsComponent.resourceName.split("?");
9886
- wsComponent.path = path40 && path40 !== "/" ? path40 : void 0;
9885
+ const [path47, query] = wsComponent.resourceName.split("?");
9886
+ wsComponent.path = path47 && path47 !== "/" ? path47 : 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 resolve27(baseURI, relativeURI, options) {
10045
+ function resolve33(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: resolve27,
10303
+ resolve: resolve33,
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, fs34, exportName) {
13279
+ function addFormats(ajv, list, fs40, 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, fs34[f2]);
13284
+ ajv.addFormat(f2, fs40[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: (path40) => {
14568
+ require: (path47) => {
14569
14569
  if (typeof require !== "undefined") {
14570
- return require(path40);
14571
- } else if (path40.match(/\.json$/)) {
14572
- return JSON.parse((0, import_fs2.readFileSync)(path40, "utf8"));
14570
+ return require(path47);
14571
+ } else if (path47.match(/\.json$/)) {
14572
+ return JSON.parse((0, import_fs2.readFileSync)(path47, "utf8"));
14573
14573
  } else {
14574
14574
  throw Error("only .json config files are supported in ESM");
14575
14575
  }
@@ -15912,13 +15912,13 @@ function usage(yargs, shim3) {
15912
15912
  };
15913
15913
  self.stringifiedValues = function stringifiedValues(values, separator) {
15914
15914
  let string3 = "";
15915
- const sep3 = separator || ", ";
15915
+ const sep5 = separator || ", ";
15916
15916
  const array2 = [].concat(values);
15917
15917
  if (!values || !array2.length)
15918
15918
  return string3;
15919
15919
  array2.forEach((value) => {
15920
15920
  if (string3.length)
15921
- string3 += sep3;
15921
+ string3 += sep5;
15922
15922
  string3 += JSON.stringify(value);
15923
15923
  });
15924
15924
  return string3;
@@ -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((resolve27, reject) => {
17098
+ return new Promise((resolve33, reject) => {
17099
17099
  __classPrivateFieldGet(this, _YargsInstance_completion, "f").getCompletion(args, (err, completions) => {
17100
17100
  if (err)
17101
17101
  reject(err);
17102
17102
  else
17103
- resolve27(completions);
17103
+ resolve33(completions);
17104
17104
  });
17105
17105
  });
17106
17106
  } else {
@@ -19250,8 +19250,8 @@ function getErrorMap() {
19250
19250
 
19251
19251
  // node_modules/zod/v3/helpers/parseUtil.js
19252
19252
  var makeIssue = (params) => {
19253
- const { data, path: path40, errorMaps, issueData } = params;
19254
- const fullPath = [...path40, ...issueData.path || []];
19253
+ const { data, path: path47, errorMaps, issueData } = params;
19254
+ const fullPath = [...path47, ...issueData.path || []];
19255
19255
  const fullIssue = {
19256
19256
  ...issueData,
19257
19257
  path: fullPath
@@ -19367,11 +19367,11 @@ var errorUtil;
19367
19367
 
19368
19368
  // node_modules/zod/v3/types.js
19369
19369
  var ParseInputLazyPath = class {
19370
- constructor(parent, value, path40, key) {
19370
+ constructor(parent, value, path47, key) {
19371
19371
  this._cachedPath = [];
19372
19372
  this.parent = parent;
19373
19373
  this.data = value;
19374
- this._path = path40;
19374
+ this._path = path47;
19375
19375
  this._key = key;
19376
19376
  }
19377
19377
  get path() {
@@ -23106,6 +23106,29 @@ var ValidationRecordSchema = external_exports.object({
23106
23106
  var SyncValidationsInputSchema = external_exports.object({
23107
23107
  validations: external_exports.array(ValidationRecordSchema)
23108
23108
  });
23109
+ var AgentToolSchema = external_exports.enum(["claude_code", "codex"]);
23110
+ var UsageSourceSchema = external_exports.enum(["otel", "harvest"]);
23111
+ var AgentSessionUsageSchema = external_exports.object({
23112
+ sessionKey: external_exports.string().min(1),
23113
+ agentId: external_exports.number().int().min(HAR_AGENT_SLOT_MIN),
23114
+ agentTool: AgentToolSchema,
23115
+ workDir: external_exports.string().optional(),
23116
+ branch: external_exports.string().optional(),
23117
+ suffix: external_exports.string().optional(),
23118
+ tokensInput: external_exports.number().nonnegative().default(0),
23119
+ tokensOutput: external_exports.number().nonnegative().default(0),
23120
+ tokensCacheRead: external_exports.number().nonnegative().default(0),
23121
+ tokensCacheCreation: external_exports.number().nonnegative().default(0),
23122
+ tokensTotal: external_exports.number().nonnegative().default(0),
23123
+ costUsd: external_exports.number().nonnegative().nullable().optional(),
23124
+ modelBreakdown: external_exports.record(external_exports.unknown()).optional(),
23125
+ sources: external_exports.array(UsageSourceSchema).default([]),
23126
+ firstSeenAt: external_exports.string(),
23127
+ lastSeenAt: external_exports.string()
23128
+ });
23129
+ var SyncUsageInputSchema = external_exports.object({
23130
+ usage: external_exports.array(AgentSessionUsageSchema)
23131
+ });
23109
23132
 
23110
23133
  // src/harness/manifest.ts
23111
23134
  var GENERATOR_VERSION = "0.4.0";
@@ -23371,16 +23394,16 @@ async function handleAgentSkills(options) {
23371
23394
  }
23372
23395
  async function askYesNo(question) {
23373
23396
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
23374
- return new Promise((resolve27) => {
23397
+ return new Promise((resolve33) => {
23375
23398
  process.stderr.write(`${question} `);
23376
23399
  rl.once("line", (answer) => {
23377
23400
  rl.close();
23378
23401
  const trimmed = answer.trim();
23379
23402
  if (trimmed === "") {
23380
- resolve27(true);
23403
+ resolve33(true);
23381
23404
  return;
23382
23405
  }
23383
- resolve27(/^y(es)?$/i.test(trimmed));
23406
+ resolve33(/^y(es)?$/i.test(trimmed));
23384
23407
  });
23385
23408
  });
23386
23409
  }
@@ -23459,7 +23482,7 @@ function handleRemove(argv) {
23459
23482
  }
23460
23483
 
23461
23484
  // src/cli/commands/env.ts
23462
- var path33 = __toESM(require("path"));
23485
+ var path40 = __toESM(require("path"));
23463
23486
 
23464
23487
  // src/core/harness.ts
23465
23488
  var fs19 = __toESM(require("fs"));
@@ -23965,13 +23988,13 @@ var MultipartBody = class {
23965
23988
  // node_modules/@anthropic-ai/sdk/_shims/node-runtime.mjs
23966
23989
  var import_web = require("node:stream/web");
23967
23990
  var fileFromPathWarned = false;
23968
- async function fileFromPath3(path40, ...args) {
23991
+ async function fileFromPath3(path47, ...args) {
23969
23992
  const { fileFromPath: _fileFromPath } = await Promise.resolve().then(() => (init_fileFromPath(), fileFromPath_exports));
23970
23993
  if (!fileFromPathWarned) {
23971
- console.warn(`fileFromPath is deprecated; use fs.createReadStream(${JSON.stringify(path40)}) instead`);
23994
+ console.warn(`fileFromPath is deprecated; use fs.createReadStream(${JSON.stringify(path47)}) instead`);
23972
23995
  fileFromPathWarned = true;
23973
23996
  }
23974
- return await _fileFromPath(path40, ...args);
23997
+ return await _fileFromPath(path47, ...args);
23975
23998
  }
23976
23999
  var defaultHttpAgent = new import_agentkeepalive.default({ keepAlive: true, timeout: 5 * 60 * 1e3 });
23977
24000
  var defaultHttpsAgent = new import_agentkeepalive.default.HttpsAgent({ keepAlive: true, timeout: 5 * 60 * 1e3 });
@@ -24467,8 +24490,8 @@ async function defaultParseResponse(props) {
24467
24490
  }
24468
24491
  var APIPromise = class _APIPromise extends Promise {
24469
24492
  constructor(responsePromise, parseResponse = defaultParseResponse) {
24470
- super((resolve27) => {
24471
- resolve27(null);
24493
+ super((resolve33) => {
24494
+ resolve33(null);
24472
24495
  });
24473
24496
  this.responsePromise = responsePromise;
24474
24497
  this.parseResponse = parseResponse;
@@ -24568,29 +24591,29 @@ var APIClient = class {
24568
24591
  defaultIdempotencyKey() {
24569
24592
  return `stainless-node-retry-${uuid4()}`;
24570
24593
  }
24571
- get(path40, opts) {
24572
- return this.methodRequest("get", path40, opts);
24594
+ get(path47, opts) {
24595
+ return this.methodRequest("get", path47, opts);
24573
24596
  }
24574
- post(path40, opts) {
24575
- return this.methodRequest("post", path40, opts);
24597
+ post(path47, opts) {
24598
+ return this.methodRequest("post", path47, opts);
24576
24599
  }
24577
- patch(path40, opts) {
24578
- return this.methodRequest("patch", path40, opts);
24600
+ patch(path47, opts) {
24601
+ return this.methodRequest("patch", path47, opts);
24579
24602
  }
24580
- put(path40, opts) {
24581
- return this.methodRequest("put", path40, opts);
24603
+ put(path47, opts) {
24604
+ return this.methodRequest("put", path47, opts);
24582
24605
  }
24583
- delete(path40, opts) {
24584
- return this.methodRequest("delete", path40, opts);
24606
+ delete(path47, opts) {
24607
+ return this.methodRequest("delete", path47, opts);
24585
24608
  }
24586
- methodRequest(method, path40, opts) {
24609
+ methodRequest(method, path47, opts) {
24587
24610
  return this.request(Promise.resolve(opts).then(async (opts2) => {
24588
24611
  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;
24589
- return { method, path: path40, ...opts2, body };
24612
+ return { method, path: path47, ...opts2, body };
24590
24613
  }));
24591
24614
  }
24592
- getAPIList(path40, Page, opts) {
24593
- return this.requestAPIList(Page, { method: "get", path: path40, ...opts });
24615
+ getAPIList(path47, Page, opts) {
24616
+ return this.requestAPIList(Page, { method: "get", path: path47, ...opts });
24594
24617
  }
24595
24618
  calculateContentLength(body) {
24596
24619
  if (typeof body === "string") {
@@ -24608,10 +24631,10 @@ var APIClient = class {
24608
24631
  return null;
24609
24632
  }
24610
24633
  buildRequest(options) {
24611
- const { method, path: path40, query, headers = {} } = options;
24634
+ const { method, path: path47, query, headers = {} } = options;
24612
24635
  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;
24613
24636
  const contentLength = this.calculateContentLength(body);
24614
- const url = this.buildURL(path40, query);
24637
+ const url = this.buildURL(path47, query);
24615
24638
  if ("timeout" in options)
24616
24639
  validatePositiveInteger("timeout", options.timeout);
24617
24640
  const timeout = options.timeout ?? this.timeout;
@@ -24720,8 +24743,8 @@ var APIClient = class {
24720
24743
  const request = this.makeRequest(options, null);
24721
24744
  return new PagePromise(this, request, Page);
24722
24745
  }
24723
- buildURL(path40, query) {
24724
- const url = isAbsoluteURL(path40) ? new URL(path40) : new URL(this.baseURL + (this.baseURL.endsWith("/") && path40.startsWith("/") ? path40.slice(1) : path40));
24746
+ buildURL(path47, query) {
24747
+ const url = isAbsoluteURL(path47) ? new URL(path47) : new URL(this.baseURL + (this.baseURL.endsWith("/") && path47.startsWith("/") ? path47.slice(1) : path47));
24725
24748
  const defaultQuery = this.defaultQuery();
24726
24749
  if (!isEmptyObj(defaultQuery)) {
24727
24750
  query = { ...defaultQuery, ...query };
@@ -25006,7 +25029,7 @@ var startsWithSchemeRegexp = new RegExp("^(?:[a-z]+:)?//", "i");
25006
25029
  var isAbsoluteURL = (url) => {
25007
25030
  return startsWithSchemeRegexp.test(url);
25008
25031
  };
25009
- var sleep = (ms) => new Promise((resolve27) => setTimeout(resolve27, ms));
25032
+ var sleep = (ms) => new Promise((resolve33) => setTimeout(resolve33, ms));
25010
25033
  var validatePositiveInteger = (name, n2) => {
25011
25034
  if (typeof n2 !== "number" || !Number.isInteger(n2)) {
25012
25035
  throw new AnthropicError(`${name} must be an integer`);
@@ -25497,12 +25520,12 @@ var PromptCachingBetaMessageStream = class _PromptCachingBetaMessageStream {
25497
25520
  }
25498
25521
  return this._emit("error", new AnthropicError(String(error3)));
25499
25522
  });
25500
- __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_connectedPromise, new Promise((resolve27, reject) => {
25501
- __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveConnectedPromise, resolve27, "f");
25523
+ __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_connectedPromise, new Promise((resolve33, reject) => {
25524
+ __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveConnectedPromise, resolve33, "f");
25502
25525
  __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_rejectConnectedPromise, reject, "f");
25503
25526
  }), "f");
25504
- __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_endPromise, new Promise((resolve27, reject) => {
25505
- __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveEndPromise, resolve27, "f");
25527
+ __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_endPromise, new Promise((resolve33, reject) => {
25528
+ __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveEndPromise, resolve33, "f");
25506
25529
  __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_rejectEndPromise, reject, "f");
25507
25530
  }), "f");
25508
25531
  __classPrivateFieldGet8(this, _PromptCachingBetaMessageStream_connectedPromise, "f").catch(() => {
@@ -25631,11 +25654,11 @@ var PromptCachingBetaMessageStream = class _PromptCachingBetaMessageStream {
25631
25654
  * const message = await stream.emitted('message') // rejects if the stream errors
25632
25655
  */
25633
25656
  emitted(event) {
25634
- return new Promise((resolve27, reject) => {
25657
+ return new Promise((resolve33, reject) => {
25635
25658
  __classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_catchingPromiseCreated, true, "f");
25636
25659
  if (event !== "error")
25637
25660
  this.once("error", reject);
25638
- this.once(event, resolve27);
25661
+ this.once(event, resolve33);
25639
25662
  });
25640
25663
  }
25641
25664
  async done() {
@@ -25862,7 +25885,7 @@ var PromptCachingBetaMessageStream = class _PromptCachingBetaMessageStream {
25862
25885
  if (done) {
25863
25886
  return { value: void 0, done: true };
25864
25887
  }
25865
- return new Promise((resolve27, reject) => readQueue.push({ resolve: resolve27, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: void 0, done: true });
25888
+ return new Promise((resolve33, reject) => readQueue.push({ resolve: resolve33, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: void 0, done: true });
25866
25889
  }
25867
25890
  const chunk = pushQueue.shift();
25868
25891
  return { value: chunk, done: false };
@@ -26010,12 +26033,12 @@ var MessageStream = class _MessageStream {
26010
26033
  }
26011
26034
  return this._emit("error", new AnthropicError(String(error3)));
26012
26035
  });
26013
- __classPrivateFieldSet8(this, _MessageStream_connectedPromise, new Promise((resolve27, reject) => {
26014
- __classPrivateFieldSet8(this, _MessageStream_resolveConnectedPromise, resolve27, "f");
26036
+ __classPrivateFieldSet8(this, _MessageStream_connectedPromise, new Promise((resolve33, reject) => {
26037
+ __classPrivateFieldSet8(this, _MessageStream_resolveConnectedPromise, resolve33, "f");
26015
26038
  __classPrivateFieldSet8(this, _MessageStream_rejectConnectedPromise, reject, "f");
26016
26039
  }), "f");
26017
- __classPrivateFieldSet8(this, _MessageStream_endPromise, new Promise((resolve27, reject) => {
26018
- __classPrivateFieldSet8(this, _MessageStream_resolveEndPromise, resolve27, "f");
26040
+ __classPrivateFieldSet8(this, _MessageStream_endPromise, new Promise((resolve33, reject) => {
26041
+ __classPrivateFieldSet8(this, _MessageStream_resolveEndPromise, resolve33, "f");
26019
26042
  __classPrivateFieldSet8(this, _MessageStream_rejectEndPromise, reject, "f");
26020
26043
  }), "f");
26021
26044
  __classPrivateFieldGet9(this, _MessageStream_connectedPromise, "f").catch(() => {
@@ -26144,11 +26167,11 @@ var MessageStream = class _MessageStream {
26144
26167
  * const message = await stream.emitted('message') // rejects if the stream errors
26145
26168
  */
26146
26169
  emitted(event) {
26147
- return new Promise((resolve27, reject) => {
26170
+ return new Promise((resolve33, reject) => {
26148
26171
  __classPrivateFieldSet8(this, _MessageStream_catchingPromiseCreated, true, "f");
26149
26172
  if (event !== "error")
26150
26173
  this.once("error", reject);
26151
- this.once(event, resolve27);
26174
+ this.once(event, resolve33);
26152
26175
  });
26153
26176
  }
26154
26177
  async done() {
@@ -26375,7 +26398,7 @@ var MessageStream = class _MessageStream {
26375
26398
  if (done) {
26376
26399
  return { value: void 0, done: true };
26377
26400
  }
26378
- return new Promise((resolve27, reject) => readQueue.push({ resolve: resolve27, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: void 0, done: true });
26401
+ return new Promise((resolve33, reject) => readQueue.push({ resolve: resolve33, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: void 0, done: true });
26379
26402
  }
26380
26403
  const chunk = pushQueue.shift();
26381
26404
  return { value: chunk, done: false };
@@ -26635,11 +26658,11 @@ async function promptApplyAgentMdProposal(repoPath) {
26635
26658
  }
26636
26659
  async function askYesNo2(question) {
26637
26660
  const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
26638
- return new Promise((resolve27) => {
26661
+ return new Promise((resolve33) => {
26639
26662
  process.stderr.write(`${question} `);
26640
26663
  rl.once("line", (answer) => {
26641
26664
  rl.close();
26642
- resolve27(/^y(es)?$/i.test(answer.trim()));
26665
+ resolve33(/^y(es)?$/i.test(answer.trim()));
26643
26666
  });
26644
26667
  });
26645
26668
  }
@@ -26869,7 +26892,7 @@ async function executeAuthoringToolCall(toolName, input, repoPath, harnessDir, c
26869
26892
  }
26870
26893
  async function askUser(question, options) {
26871
26894
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
26872
- return new Promise((resolve27) => {
26895
+ return new Promise((resolve33) => {
26873
26896
  process.stderr.write("\n");
26874
26897
  process.stderr.write(`\u2753 ${question}
26875
26898
  `);
@@ -26880,7 +26903,7 @@ async function askUser(question, options) {
26880
26903
  process.stderr.write("> ");
26881
26904
  rl.once("line", (answer) => {
26882
26905
  rl.close();
26883
- resolve27(answer.trim());
26906
+ resolve33(answer.trim());
26884
26907
  });
26885
26908
  });
26886
26909
  }
@@ -27024,7 +27047,7 @@ function runScript(scriptPath, args = [], options = {}) {
27024
27047
  function runScriptCapture(scriptPath, args = [], options = {}) {
27025
27048
  const stream = options.stream ?? false;
27026
27049
  const { stream: _stream, ...spawnOptions } = options;
27027
- return new Promise((resolve27) => {
27050
+ return new Promise((resolve33) => {
27028
27051
  const proc = (0, import_child_process.spawn)("bash", [scriptPath, ...args], {
27029
27052
  ...spawnOptions,
27030
27053
  stdio: ["inherit", "pipe", "pipe"]
@@ -27039,13 +27062,13 @@ function runScriptCapture(scriptPath, args = [], options = {}) {
27039
27062
  stderr += d2;
27040
27063
  if (stream) process.stderr.write(d2);
27041
27064
  });
27042
- proc.on("close", (code) => resolve27({ stdout, stderr, code: code ?? 0 }));
27065
+ proc.on("close", (code) => resolve33({ stdout, stderr, code: code ?? 0 }));
27043
27066
  });
27044
27067
  }
27045
27068
  function runShellCommand(command2, options = {}) {
27046
27069
  const stream = options.stream ?? false;
27047
27070
  const { stream: _stream, ...spawnOptions } = options;
27048
- return new Promise((resolve27) => {
27071
+ return new Promise((resolve33) => {
27049
27072
  const proc = (0, import_child_process.spawn)("bash", ["-lc", command2], {
27050
27073
  ...spawnOptions,
27051
27074
  stdio: ["inherit", "pipe", "pipe"]
@@ -27060,7 +27083,7 @@ function runShellCommand(command2, options = {}) {
27060
27083
  stderr += d2;
27061
27084
  if (stream) process.stderr.write(d2);
27062
27085
  });
27063
- proc.on("close", (code) => resolve27({ stdout, stderr, code: code ?? 0 }));
27086
+ proc.on("close", (code) => resolve33({ stdout, stderr, code: code ?? 0 }));
27064
27087
  });
27065
27088
  }
27066
27089
 
@@ -27849,6 +27872,24 @@ function harnessExists(repoPath) {
27849
27872
  }
27850
27873
 
27851
27874
  // src/harness/stage-templates.ts
27875
+ var STAGE_TEMPLATE_IDS = ["playwright", "rocketsim"];
27876
+ var TemplateManifestFileSchema = external_exports.object({
27877
+ src: external_exports.string().min(1),
27878
+ dest: external_exports.string().min(1),
27879
+ executable: external_exports.boolean().optional(),
27880
+ skipFlag: external_exports.string().optional()
27881
+ });
27882
+ var StageTemplateManifestSchema = external_exports.object({
27883
+ id: external_exports.enum(STAGE_TEMPLATE_IDS),
27884
+ stageId: external_exports.string().min(1),
27885
+ verificationStages: external_exports.array(external_exports.string().min(1)).min(1),
27886
+ stage: external_exports.record(external_exports.unknown()),
27887
+ files: external_exports.array(TemplateManifestFileSchema).min(1),
27888
+ optionalFiles: external_exports.array(TemplateManifestFileSchema).optional(),
27889
+ merge: external_exports.record(external_exports.string()).optional(),
27890
+ nextSteps: external_exports.array(external_exports.string().min(1)).min(1),
27891
+ docsPath: external_exports.string().min(1)
27892
+ });
27852
27893
  function resolveTemplateDir(templateId) {
27853
27894
  const dir = path17.join(resolveTemplatesDir(), "stage-templates", templateId);
27854
27895
  if (!fs18.existsSync(dir)) {
@@ -27858,11 +27899,16 @@ function resolveTemplateDir(templateId) {
27858
27899
  }
27859
27900
  function readTemplateManifest(templateId) {
27860
27901
  const manifestPath = path17.join(resolveTemplateDir(templateId), "template.manifest.json");
27861
- const raw = JSON.parse(fs18.readFileSync(manifestPath, "utf8"));
27862
- if (raw.id !== templateId) {
27863
- throw new Error(`Template manifest id mismatch: expected ${templateId}, got ${raw.id}`);
27902
+ const parsed = StageTemplateManifestSchema.safeParse(
27903
+ JSON.parse(fs18.readFileSync(manifestPath, "utf8"))
27904
+ );
27905
+ if (!parsed.success) {
27906
+ throw new Error(`Invalid template manifest for ${templateId}: ${parsed.error.message}`);
27907
+ }
27908
+ if (parsed.data.id !== templateId) {
27909
+ throw new Error(`Template manifest id mismatch: expected ${templateId}, got ${parsed.data.id}`);
27864
27910
  }
27865
- return raw;
27911
+ return parsed.data;
27866
27912
  }
27867
27913
  function ensureParentDir(filePath) {
27868
27914
  const parent = path17.dirname(filePath);
@@ -27891,7 +27937,7 @@ function copyTemplateFile(templateDir, file, repoPath, force) {
27891
27937
  function mergePackageJson(repoPath, templateDir, fragmentRelPath, warnings) {
27892
27938
  const packagePath = path17.join(repoPath, "package.json");
27893
27939
  if (!fs18.existsSync(packagePath)) {
27894
- throw new Error("No package.json in repo root. Add one before applying the Playwright template.");
27940
+ throw new Error("No package.json in repo root. Add one before applying this stage template.");
27895
27941
  }
27896
27942
  const fragmentPath = path17.join(templateDir, fragmentRelPath);
27897
27943
  if (!fs18.existsSync(fragmentPath)) {
@@ -27924,8 +27970,7 @@ function patchStageRegistry(repoPath, manifest, force) {
27924
27970
  }
27925
27971
  const stages = existing ? registry2.stages.map((s2) => s2.id === stage.id ? stage : s2) : [...registry2.stages, stage];
27926
27972
  const verificationStages = [...registry2.verificationStages ?? []];
27927
- const toAdd = manifest.verificationStages ?? (manifest.verificationStageId ? [manifest.verificationStageId] : [manifest.stageId]);
27928
- for (const id of toAdd) {
27973
+ for (const id of manifest.verificationStages) {
27929
27974
  if (!verificationStages.includes(id)) {
27930
27975
  verificationStages.push(id);
27931
27976
  }
@@ -27934,7 +27979,7 @@ function patchStageRegistry(repoPath, manifest, force) {
27934
27979
  if (verifyIdx >= 0) {
27935
27980
  stages[verifyIdx] = {
27936
27981
  ...stages[verifyIdx],
27937
- description: "Verification pipeline (quick smoke by default; --full adds tests, lint, and browser-e2e when installed)",
27982
+ description: `Verification pipeline (quick smoke by default; --full runs the registry's verificationStages: ${verificationStages.join(", ")})`,
27938
27983
  acceptsArgs: ["--full"]
27939
27984
  };
27940
27985
  }
@@ -28010,21 +28055,22 @@ function applyStageTemplate(repoPath, templateId, options = {}) {
28010
28055
  for (const warning of warnings) {
28011
28056
  warn(` \u26A0 ${warning}`);
28012
28057
  }
28013
- const nextSteps = manifest.nextSteps ?? [
28014
- "npm install",
28015
- "npx playwright install",
28016
- "./.har/launch.sh 1",
28017
- `./.har/stages/${manifest.stageId}.sh 1`,
28018
- "npx playwright show-report .har/artifacts/browser-e2e/playwright-report"
28019
- ];
28020
28058
  return {
28021
28059
  templateId,
28022
28060
  stageId: manifest.stageId,
28023
28061
  filesWritten,
28024
28062
  warnings,
28025
- nextSteps
28063
+ nextSteps: manifest.nextSteps,
28064
+ docsPath: manifest.docsPath
28026
28065
  };
28027
28066
  }
28067
+ function listStageTemplateIds() {
28068
+ const root = path17.join(resolveTemplatesDir(), "stage-templates");
28069
+ if (!fs18.existsSync(root)) return [...STAGE_TEMPLATE_IDS];
28070
+ return fs18.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter(
28071
+ (name) => STAGE_TEMPLATE_IDS.includes(name)
28072
+ );
28073
+ }
28028
28074
 
28029
28075
  // src/utils/validation.ts
28030
28076
  function requireApiKey() {
@@ -28156,9 +28202,111 @@ function addStageTemplate(repoPath, templateId, options = {}) {
28156
28202
  return applyStageTemplate(repoPath, templateId, options);
28157
28203
  }
28158
28204
 
28159
- // src/harness/adaptation-prompt.ts
28205
+ // src/harness/custom-stage.ts
28160
28206
  var fs20 = __toESM(require("fs"));
28161
28207
  var path19 = __toESM(require("path"));
28208
+ var STAGE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
28209
+ function addCustomStage(repoPath, options) {
28210
+ const resolved = path19.resolve(repoPath);
28211
+ if (!harnessExists(resolved)) {
28212
+ throw new Error('No .har/ harness found. Run "har env init" first.');
28213
+ }
28214
+ const id = options.id.trim();
28215
+ if (!STAGE_ID_PATTERN.test(id)) {
28216
+ throw new Error(
28217
+ `Invalid stage id "${id}". Use lowercase letters, digits, dots, dashes (e.g. "unit-tests").`
28218
+ );
28219
+ }
28220
+ const kind2 = options.kind ?? "custom";
28221
+ if (!HarnessStageKindSchema.options.includes(kind2)) {
28222
+ throw new Error(
28223
+ `Invalid stage kind "${kind2}". Available: ${HarnessStageKindSchema.options.join(", ")}`
28224
+ );
28225
+ }
28226
+ if (Boolean(options.command) === Boolean(options.script)) {
28227
+ throw new Error(
28228
+ 'Choose exactly one execution mode: --command "<shell command>" for a one-liner, or --script to scaffold .har/stages/<id>.sh.'
28229
+ );
28230
+ }
28231
+ const registry2 = readStageRegistry(resolved);
28232
+ const existing = registry2.stages.find((s2) => s2.id === id);
28233
+ if (existing && !options.force) {
28234
+ throw new Error(`Stage "${id}" already registered in .har/stages.json. Use --force to replace.`);
28235
+ }
28236
+ const filesWritten = [];
28237
+ const stage = {
28238
+ id,
28239
+ kind: kind2,
28240
+ description: options.description ?? `Custom ${kind2} stage`,
28241
+ requiresAgentId: true,
28242
+ artifacts: []
28243
+ };
28244
+ if (options.command) {
28245
+ stage.command = options.command;
28246
+ } else {
28247
+ const scriptRel = path19.join("stages", `${id}.sh`);
28248
+ const scriptAbs = path19.join(resolved, ".har", scriptRel);
28249
+ if (fs20.existsSync(scriptAbs) && !options.force) {
28250
+ throw new Error(`Stage script already exists: .har/${scriptRel}. Use --force to overwrite.`);
28251
+ }
28252
+ const skeletonPath = path19.join(
28253
+ resolveTemplatesDir(),
28254
+ "stage-templates",
28255
+ "custom-stage-skeleton.sh"
28256
+ );
28257
+ const skeleton = fs20.readFileSync(skeletonPath, "utf8").replace(/__STAGE_ID__/g, id).replace(/__STAGE_KIND__/g, kind2).replace(/__STAGE_DESCRIPTION__/g, stage.description);
28258
+ fs20.mkdirSync(path19.dirname(scriptAbs), { recursive: true });
28259
+ fs20.writeFileSync(scriptAbs, skeleton);
28260
+ fs20.chmodSync(scriptAbs, 493);
28261
+ filesWritten.push(`.har/${scriptRel}`);
28262
+ stage.script = scriptRel;
28263
+ stage.artifacts = [
28264
+ {
28265
+ path: `.har/artifacts/${id}`,
28266
+ kind: "directory",
28267
+ description: `Artifacts for the ${id} stage`
28268
+ }
28269
+ ];
28270
+ }
28271
+ const parsedStage = HarnessStageSchema.parse(stage);
28272
+ const stages = existing ? registry2.stages.map((s2) => s2.id === id ? parsedStage : s2) : [...registry2.stages, parsedStage];
28273
+ const verification = options.verification ?? false;
28274
+ const verificationStages = [...registry2.verificationStages ?? []];
28275
+ if (verification && !verificationStages.includes(id)) {
28276
+ verificationStages.push(id);
28277
+ const verifyIdx = stages.findIndex((s2) => s2.id === "verify");
28278
+ if (verifyIdx >= 0) {
28279
+ stages[verifyIdx] = {
28280
+ ...stages[verifyIdx],
28281
+ description: `Verification pipeline (quick smoke by default; --full runs the registry's verificationStages: ${verificationStages.join(", ")})`
28282
+ };
28283
+ }
28284
+ }
28285
+ writeStageRegistry(resolved, { ...registry2, stages, verificationStages });
28286
+ filesWritten.push(".har/stages.json");
28287
+ const nextSteps = options.command ? [
28288
+ `Try it: ${options.command.replace(/\{agentId\}/g, "1")}`,
28289
+ `Agents run it via the MCP tool har_run_stage (stageId: "${id}")`,
28290
+ ...verification ? ["./.har/verify.sh 1 --full # runs it as part of full verification"] : []
28291
+ ] : [
28292
+ `Edit .har/stages/${id}.sh \u2014 replace the TODO block with the real check`,
28293
+ "./.har/launch.sh 1",
28294
+ `./.har/stages/${id}.sh 1`,
28295
+ ...verification ? ["./.har/verify.sh 1 --full # runs it as part of full verification"] : []
28296
+ ];
28297
+ return {
28298
+ stageId: id,
28299
+ kind: kind2,
28300
+ mode: options.command ? "command" : "script",
28301
+ filesWritten,
28302
+ verification,
28303
+ nextSteps
28304
+ };
28305
+ }
28306
+
28307
+ // src/harness/adaptation-prompt.ts
28308
+ var fs21 = __toESM(require("fs"));
28309
+ var path20 = __toESM(require("path"));
28162
28310
  var ADAPTATION_PROMPT_FILE = "ADAPT-PROMPT.md";
28163
28311
  var PROFILE_HINTS = {
28164
28312
  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.",
@@ -28170,7 +28318,7 @@ function loadTemplate(name) {
28170
28318
  if (!filePath) {
28171
28319
  throw new Error(`Adaptation prompt template not found: ${name}. Run npm run build.`);
28172
28320
  }
28173
- return fs20.readFileSync(filePath, "utf8");
28321
+ return fs21.readFileSync(filePath, "utf8");
28174
28322
  }
28175
28323
  function applyProfilePlaceholders(content, profile) {
28176
28324
  return content.replace(/\{\{PROFILE\}\}/g, profile).replace(/\{\{PROFILE_HINT\}\}/g, PROFILE_HINTS[profile]);
@@ -28192,7 +28340,7 @@ function buildMaintainAdaptationPrompt(_repoPath, bundleReport) {
28192
28340
  }
28193
28341
  function writeAdaptationPrompt(repoPath, content) {
28194
28342
  const harnessDir = getHarnessDir(repoPath);
28195
- const filePath = path19.join(harnessDir, ADAPTATION_PROMPT_FILE);
28343
+ const filePath = path20.join(harnessDir, ADAPTATION_PROMPT_FILE);
28196
28344
  writeFileSafe(filePath, content);
28197
28345
  return filePath;
28198
28346
  }
@@ -28211,25 +28359,25 @@ function printAdaptationPrompt(content) {
28211
28359
  }
28212
28360
 
28213
28361
  // src/harness/cursor-rule.ts
28214
- var fs21 = __toESM(require("fs"));
28215
- var path20 = __toESM(require("path"));
28362
+ var fs22 = __toESM(require("fs"));
28363
+ var path21 = __toESM(require("path"));
28216
28364
  var readline4 = __toESM(require("readline"));
28217
28365
  var CURSOR_RULE_RELATIVE_PATH = ".cursor/rules/har-workflow.mdc";
28218
28366
  function getCursorRulePath(repoPath) {
28219
- return path20.join(repoPath, CURSOR_RULE_RELATIVE_PATH);
28367
+ return path21.join(repoPath, CURSOR_RULE_RELATIVE_PATH);
28220
28368
  }
28221
28369
  function isCursorWorkspace(repoPath) {
28222
- return fs21.existsSync(path20.join(repoPath, ".cursor"));
28370
+ return fs22.existsSync(path21.join(repoPath, ".cursor"));
28223
28371
  }
28224
28372
  function cursorRuleExists(repoPath) {
28225
- return fs21.existsSync(getCursorRulePath(repoPath));
28373
+ return fs22.existsSync(getCursorRulePath(repoPath));
28226
28374
  }
28227
28375
  function scaffoldCursorRule(repoPath) {
28228
28376
  const templatePath = resolveTemplateFile("cursor-rule.mdc.template");
28229
28377
  if (!templatePath) {
28230
28378
  throw new Error("Cursor rule template not found (cursor-rule.mdc.template). Run npm run build.");
28231
28379
  }
28232
- const content = fs21.readFileSync(templatePath, "utf8");
28380
+ const content = fs22.readFileSync(templatePath, "utf8");
28233
28381
  writeFileSafe(getCursorRulePath(repoPath), content);
28234
28382
  }
28235
28383
  async function handleCursorRule(options) {
@@ -28267,16 +28415,16 @@ async function promptScaffoldCursorRule(exists) {
28267
28415
  }
28268
28416
  async function askYesNo3(question) {
28269
28417
  const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
28270
- return new Promise((resolve27) => {
28418
+ return new Promise((resolve33) => {
28271
28419
  process.stderr.write(`${question} `);
28272
28420
  rl.once("line", (answer) => {
28273
28421
  rl.close();
28274
28422
  const trimmed = answer.trim();
28275
28423
  if (trimmed === "") {
28276
- resolve27(true);
28424
+ resolve33(true);
28277
28425
  return;
28278
28426
  }
28279
- resolve27(/^y(es)?$/i.test(trimmed));
28427
+ resolve33(/^y(es)?$/i.test(trimmed));
28280
28428
  });
28281
28429
  });
28282
28430
  }
@@ -28355,26 +28503,26 @@ function buildStageResult(input) {
28355
28503
  }
28356
28504
 
28357
28505
  // src/core/local-executor.ts
28358
- var fs23 = __toESM(require("fs"));
28359
- var path22 = __toESM(require("path"));
28506
+ var fs24 = __toESM(require("fs"));
28507
+ var path23 = __toESM(require("path"));
28360
28508
 
28361
28509
  // src/core/slot-registry.ts
28362
- var fs22 = __toESM(require("fs"));
28363
- var path21 = __toESM(require("path"));
28510
+ var fs23 = __toESM(require("fs"));
28511
+ var path22 = __toESM(require("path"));
28364
28512
  function getSlotRegistryDir(repoPath) {
28365
- return path21.join(getHarnessDir(repoPath), "slots");
28513
+ return path22.join(getHarnessDir(repoPath), "slots");
28366
28514
  }
28367
28515
  function getSlotRegistryPath(repoPath, agentId) {
28368
- return path21.join(getSlotRegistryDir(repoPath), `agent-${agentId}.json`);
28516
+ return path22.join(getSlotRegistryDir(repoPath), `agent-${agentId}.json`);
28369
28517
  }
28370
28518
  function isSlotResumable(session) {
28371
28519
  return session?.status === "failed" || session?.status === "starting";
28372
28520
  }
28373
28521
  function readSlotRegistry(repoPath, agentId) {
28374
28522
  const file = getSlotRegistryPath(repoPath, agentId);
28375
- if (!fs22.existsSync(file)) return void 0;
28523
+ if (!fs23.existsSync(file)) return void 0;
28376
28524
  try {
28377
- const raw = JSON.parse(fs22.readFileSync(file, "utf8"));
28525
+ const raw = JSON.parse(fs23.readFileSync(file, "utf8"));
28378
28526
  const result = SlotRegistryEntrySchema.safeParse(raw);
28379
28527
  return result.success ? result.data : void 0;
28380
28528
  } catch {
@@ -28384,7 +28532,7 @@ function readSlotRegistry(repoPath, agentId) {
28384
28532
 
28385
28533
  // src/core/local-executor.ts
28386
28534
  function resolveRepoPath(repoPath) {
28387
- return path22.resolve(repoPath);
28535
+ return path23.resolve(repoPath);
28388
28536
  }
28389
28537
  function buildPreviewUrlsFromPorts(ports, env3) {
28390
28538
  const urls = {};
@@ -28436,14 +28584,14 @@ function substituteAgentId(value, agentId) {
28436
28584
  function resolveStageScriptPath(repoPath, stage) {
28437
28585
  const harnessDir = getHarnessDir(repoPath);
28438
28586
  if (stage.script) {
28439
- return path22.join(harnessDir, stage.script);
28587
+ return path23.join(harnessDir, stage.script);
28440
28588
  }
28441
28589
  if (stage.command) {
28442
28590
  const scriptName = stage.command.split(/\s+/)[0].replace(/^\.\/\.har\//, "").replace(/^\.\//, "");
28443
- return path22.join(harnessDir, scriptName);
28591
+ return path23.join(harnessDir, scriptName);
28444
28592
  }
28445
- const stageScript = path22.join(harnessDir, "stages", `${stage.id}.sh`);
28446
- if (fs23.existsSync(stageScript)) {
28593
+ const stageScript = path23.join(harnessDir, "stages", `${stage.id}.sh`);
28594
+ if (fs24.existsSync(stageScript)) {
28447
28595
  return stageScript;
28448
28596
  }
28449
28597
  throw new Error(
@@ -28458,7 +28606,7 @@ function buildExecutionPlan(repoPath, stage, options) {
28458
28606
  ...harnessEnv,
28459
28607
  ...stage.env ?? {}
28460
28608
  };
28461
- const cwd = stage.cwd ? path22.resolve(resolvedRepo, stage.cwd) : resolvedRepo;
28609
+ const cwd = stage.cwd ? path23.resolve(resolvedRepo, stage.cwd) : resolvedRepo;
28462
28610
  const extraArgs = options.args ?? [];
28463
28611
  const launchFlagArgs = [];
28464
28612
  if (stage.kind === "launch" && options.launchFlags) {
@@ -28535,19 +28683,19 @@ var LocalScriptExecutor = class {
28535
28683
  const repoPath = resolveRepoPath(ctx.repoPath);
28536
28684
  const harnessDir = getHarnessDir(repoPath);
28537
28685
  const artifactsDirName = getArtifactsDir(repoPath);
28538
- const artifactsDir = path22.join(harnessDir, artifactsDirName);
28539
- if (!fs23.existsSync(artifactsDir)) return [];
28686
+ const artifactsDir = path23.join(harnessDir, artifactsDirName);
28687
+ if (!fs24.existsSync(artifactsDir)) return [];
28540
28688
  const entries = [];
28541
28689
  const walk = (dir, prefix) => {
28542
- for (const entry of fs23.readdirSync(dir, { withFileTypes: true })) {
28543
- const full = path22.join(dir, entry.name);
28544
- const relative2 = path22.join(prefix, entry.name);
28690
+ for (const entry of fs24.readdirSync(dir, { withFileTypes: true })) {
28691
+ const full = path23.join(dir, entry.name);
28692
+ const relative2 = path23.join(prefix, entry.name);
28545
28693
  if (entry.isDirectory()) {
28546
28694
  walk(full, relative2);
28547
28695
  continue;
28548
28696
  }
28549
28697
  if (filter?.stageId && !relative2.includes(filter.stageId)) continue;
28550
- const stat = fs23.statSync(full);
28698
+ const stat = fs24.statSync(full);
28551
28699
  entries.push({
28552
28700
  path: full,
28553
28701
  relativePath: relative2,
@@ -28563,7 +28711,7 @@ var LocalScriptExecutor = class {
28563
28711
  var localScriptExecutor = new LocalScriptExecutor();
28564
28712
 
28565
28713
  // src/core/control-sync.ts
28566
- var path32 = __toESM(require("path"));
28714
+ var path37 = __toESM(require("path"));
28567
28715
 
28568
28716
  // src/core/control-config.ts
28569
28717
  var DEFAULT_CONTROL_API_URL = "http://localhost:3847";
@@ -28575,32 +28723,32 @@ function isControlEnabled() {
28575
28723
  }
28576
28724
 
28577
28725
  // src/core/control-registry.ts
28578
- var fs24 = __toESM(require("fs"));
28726
+ var fs25 = __toESM(require("fs"));
28579
28727
  var os4 = __toESM(require("os"));
28580
- var path23 = __toESM(require("path"));
28728
+ var path24 = __toESM(require("path"));
28581
28729
  function getRegistryPath() {
28582
28730
  if (process.env.HAR_CONTROL_REGISTRY_PATH) {
28583
- return path23.resolve(process.env.HAR_CONTROL_REGISTRY_PATH);
28731
+ return path24.resolve(process.env.HAR_CONTROL_REGISTRY_PATH);
28584
28732
  }
28585
- return path23.join(os4.homedir(), ".har", "repos.json");
28733
+ return path24.join(os4.homedir(), ".har", "repos.json");
28586
28734
  }
28587
28735
  function readRegistry() {
28588
28736
  const registryPath = getRegistryPath();
28589
28737
  try {
28590
- if (!fs24.existsSync(registryPath)) return { repos: [] };
28591
- return JSON.parse(fs24.readFileSync(registryPath, "utf8"));
28738
+ if (!fs25.existsSync(registryPath)) return { repos: [] };
28739
+ return JSON.parse(fs25.readFileSync(registryPath, "utf8"));
28592
28740
  } catch {
28593
28741
  return { repos: [] };
28594
28742
  }
28595
28743
  }
28596
28744
  function writeRegistry(registry2) {
28597
28745
  const registryPath = getRegistryPath();
28598
- fs24.mkdirSync(path23.dirname(registryPath), { recursive: true });
28599
- fs24.writeFileSync(registryPath, JSON.stringify(registry2, null, 2) + "\n");
28746
+ fs25.mkdirSync(path24.dirname(registryPath), { recursive: true });
28747
+ fs25.writeFileSync(registryPath, JSON.stringify(registry2, null, 2) + "\n");
28600
28748
  }
28601
28749
  function recordRepoForControlSync(repoPath) {
28602
28750
  if (process.env.HAR_CONTROL_DISABLED === "true") return;
28603
- const resolved = path23.resolve(repoPath);
28751
+ const resolved = path24.resolve(repoPath);
28604
28752
  if (!readManifest(resolved)) return;
28605
28753
  const registry2 = readRegistry();
28606
28754
  if (registry2.repos.includes(resolved)) return;
@@ -28609,7 +28757,7 @@ function recordRepoForControlSync(repoPath) {
28609
28757
  }
28610
28758
  function listRegisteredRepos() {
28611
28759
  const registry2 = readRegistry();
28612
- const kept = registry2.repos.filter((repoPath) => fs24.existsSync(repoPath) && readManifest(repoPath));
28760
+ const kept = registry2.repos.filter((repoPath) => fs25.existsSync(repoPath) && readManifest(repoPath));
28613
28761
  if (kept.length !== registry2.repos.length) {
28614
28762
  writeRegistry({ repos: kept });
28615
28763
  }
@@ -28617,17 +28765,17 @@ function listRegisteredRepos() {
28617
28765
  }
28618
28766
 
28619
28767
  // src/core/slot-status.ts
28620
- var fs29 = __toESM(require("fs"));
28768
+ var fs30 = __toESM(require("fs"));
28621
28769
  var os7 = __toESM(require("os"));
28622
- var path29 = __toESM(require("path"));
28770
+ var path30 = __toESM(require("path"));
28623
28771
  var import_child_process7 = require("child_process");
28624
28772
 
28625
28773
  // src/core/runs.ts
28626
28774
  var crypto2 = __toESM(require("crypto"));
28627
28775
  var import_child_process2 = require("child_process");
28628
- var fs25 = __toESM(require("fs"));
28776
+ var fs26 = __toESM(require("fs"));
28629
28777
  var os5 = __toESM(require("os"));
28630
- var path24 = __toESM(require("path"));
28778
+ var path25 = __toESM(require("path"));
28631
28779
  var RUNS_DIR = "runs";
28632
28780
  function gitCommonDir(cwd) {
28633
28781
  try {
@@ -28636,7 +28784,7 @@ function gitCommonDir(cwd) {
28636
28784
  encoding: "utf8",
28637
28785
  stdio: ["pipe", "pipe", "ignore"]
28638
28786
  }).trim();
28639
- return out ? path24.resolve(cwd, out) : void 0;
28787
+ return out ? path25.resolve(cwd, out) : void 0;
28640
28788
  } catch {
28641
28789
  return void 0;
28642
28790
  }
@@ -28658,7 +28806,7 @@ function gitPrefix(cwd) {
28658
28806
  }
28659
28807
  }
28660
28808
  function getRunsDir(harnessRoot) {
28661
- return path24.join(getHarnessDir(harnessRoot), RUNS_DIR);
28809
+ return path25.join(getHarnessDir(harnessRoot), RUNS_DIR);
28662
28810
  }
28663
28811
  function formatLocalDate(d2) {
28664
28812
  const y2 = d2.getFullYear();
@@ -28678,24 +28826,24 @@ function buildRunRelativePath(stageId, agentId, startedAt, runId, runsDir) {
28678
28826
  const timePart = formatLocalTime(started);
28679
28827
  const agentPart = agentId !== void 0 ? `_agent-${agentId}` : "";
28680
28828
  let filename = `${timePart}_${stageId}${agentPart}.json`;
28681
- const fullPath = path24.join(runsDir, dateFolder, filename);
28682
- if (fs25.existsSync(fullPath)) {
28829
+ const fullPath = path25.join(runsDir, dateFolder, filename);
28830
+ if (fs26.existsSync(fullPath)) {
28683
28831
  filename = `${timePart}_${stageId}${agentPart}-${runId.slice(0, 8)}.json`;
28684
28832
  }
28685
- return path24.join(dateFolder, filename);
28833
+ return path25.join(dateFolder, filename);
28686
28834
  }
28687
28835
  function resolveRunFilePath(harnessRoot, run2) {
28688
28836
  if (run2.relativePath) {
28689
- return path24.join(getRunsDir(harnessRoot), run2.relativePath);
28837
+ return path25.join(getRunsDir(harnessRoot), run2.relativePath);
28690
28838
  }
28691
- return path24.join(getRunsDir(harnessRoot), `${run2.runId}.json`);
28839
+ return path25.join(getRunsDir(harnessRoot), `${run2.runId}.json`);
28692
28840
  }
28693
28841
  function collectRunFiles(runsDir) {
28694
- if (!fs25.existsSync(runsDir)) return [];
28842
+ if (!fs26.existsSync(runsDir)) return [];
28695
28843
  const files = [];
28696
28844
  const walk = (dir) => {
28697
- for (const entry of fs25.readdirSync(dir, { withFileTypes: true })) {
28698
- const full = path24.join(dir, entry.name);
28845
+ for (const entry of fs26.readdirSync(dir, { withFileTypes: true })) {
28846
+ const full = path25.join(dir, entry.name);
28699
28847
  if (entry.isDirectory()) {
28700
28848
  walk(full);
28701
28849
  } else if (entry.name.endsWith(".json")) {
@@ -28709,30 +28857,30 @@ function collectRunFiles(runsDir) {
28709
28857
  function resolveAgentWorkDir(harnessRoot, agentId) {
28710
28858
  if (agentId === void 0) return void 0;
28711
28859
  const entry = readSlotRegistry(harnessRoot, agentId);
28712
- if (entry?.workDir && fs25.existsSync(entry.workDir)) return entry.workDir;
28860
+ if (entry?.workDir && fs26.existsSync(entry.workDir)) return entry.workDir;
28713
28861
  const env3 = readHarnessEnv(harnessRoot);
28714
- const projectName = env3.HARNESS_PROJECT_NAME ?? path24.basename(harnessRoot);
28715
- const worktreeDir = path24.join(os5.homedir(), "worktrees", `${projectName}-agent-${agentId}`);
28862
+ const projectName = env3.HARNESS_PROJECT_NAME ?? path25.basename(harnessRoot);
28863
+ const worktreeDir = path25.join(os5.homedir(), "worktrees", `${projectName}-agent-${agentId}`);
28716
28864
  const relPrefix = gitPrefix(harnessRoot);
28717
28865
  const sessionEnvFiles = [];
28718
- const worktreesRoot = path24.join(os5.homedir(), "worktrees");
28719
- if (fs25.existsSync(worktreesRoot)) {
28866
+ const worktreesRoot = path25.join(os5.homedir(), "worktrees");
28867
+ if (fs26.existsSync(worktreesRoot)) {
28720
28868
  const suffix = `-har-agent-${agentId}-`;
28721
- for (const entry2 of fs25.readdirSync(worktreesRoot, { withFileTypes: true })) {
28869
+ for (const entry2 of fs26.readdirSync(worktreesRoot, { withFileTypes: true })) {
28722
28870
  if (!entry2.isDirectory() || !entry2.name.includes(suffix)) continue;
28723
- const sessionDir = path24.join(worktreesRoot, entry2.name);
28871
+ const sessionDir = path25.join(worktreesRoot, entry2.name);
28724
28872
  if (!sameGitCheckout(harnessRoot, sessionDir)) continue;
28725
- sessionEnvFiles.push(path24.join(sessionDir, relPrefix, `.env.agent.${agentId}`));
28873
+ sessionEnvFiles.push(path25.join(sessionDir, relPrefix, `.env.agent.${agentId}`));
28726
28874
  }
28727
28875
  }
28728
28876
  const candidates = [
28729
- path24.join(worktreeDir, `.env.agent.${agentId}`),
28730
- path24.join(harnessRoot, `.env.agent.${agentId}`),
28877
+ path25.join(worktreeDir, `.env.agent.${agentId}`),
28878
+ path25.join(harnessRoot, `.env.agent.${agentId}`),
28731
28879
  ...sessionEnvFiles.sort()
28732
28880
  ];
28733
28881
  for (const envFile of candidates) {
28734
- if (!fs25.existsSync(envFile)) continue;
28735
- const content = fs25.readFileSync(envFile, "utf8");
28882
+ if (!fs26.existsSync(envFile)) continue;
28883
+ const content = fs26.readFileSync(envFile, "utf8");
28736
28884
  const match = content.match(/^REPO_ROOT=(.+)$/m);
28737
28885
  if (match) return match[1].trim();
28738
28886
  }
@@ -28744,11 +28892,11 @@ function createRun(ctx, meta) {
28744
28892
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
28745
28893
  const runId = crypto2.randomUUID();
28746
28894
  const relativePath = buildRunRelativePath(meta.stageId, meta.agentId, startedAt, runId, runsDir);
28747
- const runFilePath = path24.join(runsDir, relativePath);
28748
- fs25.mkdirSync(path24.dirname(runFilePath), { recursive: true });
28895
+ const runFilePath = path25.join(runsDir, relativePath);
28896
+ fs26.mkdirSync(path25.dirname(runFilePath), { recursive: true });
28749
28897
  const run2 = RunRecordSchema.parse({
28750
28898
  runId,
28751
- repoPath: path24.resolve(ctx.repoPath),
28899
+ repoPath: path25.resolve(ctx.repoPath),
28752
28900
  harnessRoot,
28753
28901
  stageId: meta.stageId,
28754
28902
  kind: meta.kind,
@@ -28759,7 +28907,7 @@ function createRun(ctx, meta) {
28759
28907
  relativePath,
28760
28908
  trigger: ctx.trigger ?? "cli"
28761
28909
  });
28762
- fs25.writeFileSync(runFilePath, JSON.stringify(run2, null, 2) + "\n");
28910
+ fs26.writeFileSync(runFilePath, JSON.stringify(run2, null, 2) + "\n");
28763
28911
  return run2;
28764
28912
  }
28765
28913
  function finishRun(repoPath, runId, update) {
@@ -28778,13 +28926,13 @@ function finishRun(repoPath, runId, update) {
28778
28926
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
28779
28927
  });
28780
28928
  const runPath = resolveRunFilePath(harnessRoot, finished);
28781
- fs25.writeFileSync(runPath, JSON.stringify(finished, null, 2) + "\n");
28929
+ fs26.writeFileSync(runPath, JSON.stringify(finished, null, 2) + "\n");
28782
28930
  return finished;
28783
28931
  }
28784
28932
  function findRunRecord(harnessRoot, runId) {
28785
28933
  const runsDir = getRunsDir(harnessRoot);
28786
28934
  for (const filePath of collectRunFiles(runsDir)) {
28787
- const parsed = RunRecordSchema.safeParse(JSON.parse(fs25.readFileSync(filePath, "utf8")));
28935
+ const parsed = RunRecordSchema.safeParse(JSON.parse(fs26.readFileSync(filePath, "utf8")));
28788
28936
  if (parsed.success && parsed.data.runId === runId) {
28789
28937
  return parsed.data;
28790
28938
  }
@@ -28796,10 +28944,10 @@ function getRun(repoPath, runId) {
28796
28944
  }
28797
28945
  function listRuns(repoPath, filter = {}) {
28798
28946
  const runsDir = getRunsDir(resolveHarnessRoot(repoPath));
28799
- if (!fs25.existsSync(runsDir)) return [];
28947
+ if (!fs26.existsSync(runsDir)) return [];
28800
28948
  const runs = [];
28801
28949
  for (const filePath of collectRunFiles(runsDir)) {
28802
- const parsed = RunRecordSchema.safeParse(JSON.parse(fs25.readFileSync(filePath, "utf8")));
28950
+ const parsed = RunRecordSchema.safeParse(JSON.parse(fs26.readFileSync(filePath, "utf8")));
28803
28951
  if (!parsed.success) continue;
28804
28952
  if (filter.stageId && parsed.data.stageId !== filter.stageId) continue;
28805
28953
  runs.push(parsed.data);
@@ -28812,17 +28960,17 @@ function listRuns(repoPath, filter = {}) {
28812
28960
  }
28813
28961
 
28814
28962
  // src/core/slot-preflight.ts
28815
- var path28 = __toESM(require("path"));
28963
+ var path29 = __toESM(require("path"));
28816
28964
 
28817
28965
  // src/core/control-port.ts
28818
28966
  var import_child_process4 = require("child_process");
28819
- var fs27 = __toESM(require("fs"));
28820
- var path26 = __toESM(require("path"));
28967
+ var fs28 = __toESM(require("fs"));
28968
+ var path27 = __toESM(require("path"));
28821
28969
 
28822
28970
  // src/core/slot-ports.ts
28823
28971
  var import_child_process3 = require("child_process");
28824
- var fs26 = __toESM(require("fs"));
28825
- var path25 = __toESM(require("path"));
28972
+ var fs27 = __toESM(require("fs"));
28973
+ var path26 = __toESM(require("path"));
28826
28974
  function portStep(env3) {
28827
28975
  return Number(env3.HARNESS_PORT_STEP ?? 10);
28828
28976
  }
@@ -28878,7 +29026,7 @@ function allocateAppPorts(repoPath, agentId) {
28878
29026
  }
28879
29027
  function harnessUsesPm2(repoPath) {
28880
29028
  const harnessRoot = resolveHarnessRoot(repoPath);
28881
- return fs26.existsSync(path25.join(harnessRoot, ".har", "ecosystem.agent.template.cjs"));
29029
+ return fs27.existsSync(path26.join(harnessRoot, ".har", "ecosystem.agent.template.cjs"));
28882
29030
  }
28883
29031
 
28884
29032
  // src/core/control-port.ts
@@ -28933,13 +29081,13 @@ function controlDefaultPortWarnings(containers, defaultFrontendPort, allocatedFr
28933
29081
  return [formatControlDefaultPortWarning(control.name, defaultFrontendPort, allocatedFrontend)];
28934
29082
  }
28935
29083
  function inspectControlUpReadiness(repoPath) {
28936
- const resolved = path26.resolve(repoPath);
29084
+ const resolved = path27.resolve(repoPath);
28937
29085
  const port = parseControlHostPort();
28938
29086
  const containers = listDockerContainers();
28939
29087
  const control = controlContainerOnPort(containers, port);
28940
29088
  const warnings = [];
28941
- const controlHarnessDir = path26.join(resolved, "control", ".har");
28942
- const harnessSlot1Active = fs27.existsSync(controlHarnessDir) && readSlotRegistry(path26.join(resolved, "control"), 1)?.status === "active";
29089
+ const controlHarnessDir = path27.join(resolved, "control", ".har");
29090
+ const harnessSlot1Active = fs28.existsSync(controlHarnessDir) && readSlotRegistry(path27.join(resolved, "control"), 1)?.status === "active";
28943
29091
  if (control) {
28944
29092
  return {
28945
29093
  warnings,
@@ -28970,9 +29118,9 @@ function inspectControlUpReadiness(repoPath) {
28970
29118
  }
28971
29119
 
28972
29120
  // src/core/slot-launch-guard-occupied.ts
28973
- var fs28 = __toESM(require("fs"));
29121
+ var fs29 = __toESM(require("fs"));
28974
29122
  var os6 = __toESM(require("os"));
28975
- var path27 = __toESM(require("path"));
29123
+ var path28 = __toESM(require("path"));
28976
29124
  var import_child_process5 = require("child_process");
28977
29125
  function runGit(cwd, args) {
28978
29126
  try {
@@ -28991,7 +29139,7 @@ function worktreeDirty(worktreePath) {
28991
29139
  }
28992
29140
  function gitCommonDir2(cwd) {
28993
29141
  const out = runGit(cwd, "rev-parse --git-common-dir");
28994
- return out ? path27.resolve(cwd, out) : void 0;
29142
+ return out ? path28.resolve(cwd, out) : void 0;
28995
29143
  }
28996
29144
  function sameGitCheckout2(a2, b2) {
28997
29145
  const left2 = gitCommonDir2(a2);
@@ -29000,20 +29148,20 @@ function sameGitCheckout2(a2, b2) {
29000
29148
  }
29001
29149
  function discoverWorktree(harnessRoot, agentId, projectName) {
29002
29150
  const session = readSlotRegistry(harnessRoot, agentId);
29003
- if (session?.worktreePath && fs28.existsSync(session.worktreePath)) {
29151
+ if (session?.worktreePath && fs29.existsSync(session.worktreePath)) {
29004
29152
  return session.worktreePath;
29005
29153
  }
29006
- const legacy = path27.join(os6.homedir(), "worktrees", `${projectName}-agent-${agentId}`);
29007
- if (fs28.existsSync(legacy) && sameGitCheckout2(harnessRoot, legacy)) return legacy;
29008
- const worktreesRoot = path27.join(os6.homedir(), "worktrees");
29009
- if (!fs28.existsSync(worktreesRoot)) return void 0;
29154
+ const legacy = path28.join(os6.homedir(), "worktrees", `${projectName}-agent-${agentId}`);
29155
+ if (fs29.existsSync(legacy) && sameGitCheckout2(harnessRoot, legacy)) return legacy;
29156
+ const worktreesRoot = path28.join(os6.homedir(), "worktrees");
29157
+ if (!fs29.existsSync(worktreesRoot)) return void 0;
29010
29158
  const suffix = `-har-agent-${agentId}-`;
29011
29159
  const relPrefix = runGit(harnessRoot, "rev-parse --show-prefix") ?? "";
29012
- for (const name of fs28.readdirSync(worktreesRoot)) {
29160
+ for (const name of fs29.readdirSync(worktreesRoot)) {
29013
29161
  if (!name.includes(suffix)) continue;
29014
- const candidate = path27.join(worktreesRoot, name);
29162
+ const candidate = path28.join(worktreesRoot, name);
29015
29163
  if (!sameGitCheckout2(harnessRoot, candidate)) continue;
29016
- if (!fs28.existsSync(path27.join(candidate, relPrefix, `.env.agent.${agentId}`))) continue;
29164
+ if (!fs29.existsSync(path28.join(candidate, relPrefix, `.env.agent.${agentId}`))) continue;
29017
29165
  return candidate;
29018
29166
  }
29019
29167
  return void 0;
@@ -29021,13 +29169,13 @@ function discoverWorktree(harnessRoot, agentId, projectName) {
29021
29169
  function collectOccupiedSlot(repoPath, agentId) {
29022
29170
  const harnessRoot = resolveHarnessRoot(repoPath);
29023
29171
  const env3 = readHarnessEnv(harnessRoot);
29024
- const projectName = env3.HARNESS_PROJECT_NAME ?? path27.basename(harnessRoot);
29172
+ const projectName = env3.HARNESS_PROJECT_NAME ?? path28.basename(harnessRoot);
29025
29173
  const session = readSlotRegistry(harnessRoot, agentId);
29026
29174
  const worktreePath = discoverWorktree(harnessRoot, agentId, projectName);
29027
29175
  const workDir = session?.workDir;
29028
- const envInWorkDir = workDir ? fs28.existsSync(path27.join(workDir, `.env.agent.${agentId}`)) : false;
29029
- const envInRoot = fs28.existsSync(path27.join(harnessRoot, `.env.agent.${agentId}`));
29030
- const envInWorktree = worktreePath ? fs28.existsSync(path27.join(worktreePath, `.env.agent.${agentId}`)) : false;
29176
+ const envInWorkDir = workDir ? fs29.existsSync(path28.join(workDir, `.env.agent.${agentId}`)) : false;
29177
+ const envInRoot = fs29.existsSync(path28.join(harnessRoot, `.env.agent.${agentId}`));
29178
+ const envInWorktree = worktreePath ? fs29.existsSync(path28.join(worktreePath, `.env.agent.${agentId}`)) : false;
29031
29179
  const active = session !== void 0 && session.status !== "completed" || envInWorkDir || envInRoot || envInWorktree;
29032
29180
  if (!active) return void 0;
29033
29181
  const dirty = worktreePath ? worktreeDirty(worktreePath) : void 0;
@@ -29180,7 +29328,7 @@ function checkInfraPort(env3, varName, defaultPort, scanStart, scanEnd) {
29180
29328
  function inspectSlotReadiness(repoPath, agentId, options = {}) {
29181
29329
  const harnessRoot = resolveHarnessRoot(repoPath);
29182
29330
  const env3 = readHarnessEnv(harnessRoot);
29183
- const projectName = env3.HARNESS_PROJECT_NAME ?? path28.basename(harnessRoot);
29331
+ const projectName = env3.HARNESS_PROJECT_NAME ?? path29.basename(harnessRoot);
29184
29332
  const usesPm2 = harnessUsesPm2(repoPath);
29185
29333
  const blockers = [];
29186
29334
  const remediations = [];
@@ -29382,7 +29530,7 @@ function readWorktreeBranch(worktreePath) {
29382
29530
  }
29383
29531
  function gitCommonDir3(cwd) {
29384
29532
  const out = runGit2(cwd, "rev-parse --git-common-dir");
29385
- return out ? path29.resolve(cwd, out) : void 0;
29533
+ return out ? path30.resolve(cwd, out) : void 0;
29386
29534
  }
29387
29535
  function sameGitCheckout3(a2, b2) {
29388
29536
  const left2 = gitCommonDir3(a2);
@@ -29394,12 +29542,12 @@ function gitPrefix2(cwd) {
29394
29542
  return out ?? "";
29395
29543
  }
29396
29544
  function discoverSessionWorktreePath(harnessRoot, agentId) {
29397
- const worktreesRoot = path29.join(os7.homedir(), "worktrees");
29398
- if (!fs29.existsSync(worktreesRoot)) return void 0;
29545
+ const worktreesRoot = path30.join(os7.homedir(), "worktrees");
29546
+ if (!fs30.existsSync(worktreesRoot)) return void 0;
29399
29547
  const suffix = `-har-agent-${agentId}-`;
29400
29548
  const relPrefix = gitPrefix2(harnessRoot);
29401
- const matches = fs29.readdirSync(worktreesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.includes(suffix)).map((entry) => path29.join(worktreesRoot, entry.name)).filter(
29402
- (candidate) => sameGitCheckout3(harnessRoot, candidate) && fs29.existsSync(path29.join(candidate, relPrefix, `.env.agent.${agentId}`))
29549
+ const matches = fs30.readdirSync(worktreesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.includes(suffix)).map((entry) => path30.join(worktreesRoot, entry.name)).filter(
29550
+ (candidate) => sameGitCheckout3(harnessRoot, candidate) && fs30.existsSync(path30.join(candidate, relPrefix, `.env.agent.${agentId}`))
29403
29551
  );
29404
29552
  return matches.sort()[0];
29405
29553
  }
@@ -29474,18 +29622,18 @@ function detectPm2Issue(projectName, agentId, session, procs) {
29474
29622
  }
29475
29623
  function collectSlotStatus(harnessRoot, agentId, runs, pm2Procs) {
29476
29624
  const env3 = readHarnessEnv(harnessRoot);
29477
- const projectName = env3.HARNESS_PROJECT_NAME ?? path29.basename(harnessRoot);
29625
+ const projectName = env3.HARNESS_PROJECT_NAME ?? path30.basename(harnessRoot);
29478
29626
  const session = readSlotRegistry(harnessRoot, agentId);
29479
- const legacyWorktreePath = path29.join(
29627
+ const legacyWorktreePath = path30.join(
29480
29628
  os7.homedir(),
29481
29629
  "worktrees",
29482
29630
  `${projectName}-agent-${agentId}`
29483
29631
  );
29484
- const worktreePath = session?.worktreePath && fs29.existsSync(session.worktreePath) ? session.worktreePath : fs29.existsSync(legacyWorktreePath) ? legacyWorktreePath : discoverSessionWorktreePath(harnessRoot, agentId);
29632
+ const worktreePath = session?.worktreePath && fs30.existsSync(session.worktreePath) ? session.worktreePath : fs30.existsSync(legacyWorktreePath) ? legacyWorktreePath : discoverSessionWorktreePath(harnessRoot, agentId);
29485
29633
  const workDir = resolveAgentWorkDir(harnessRoot, agentId);
29486
- const envInWorkDir = workDir ? fs29.existsSync(path29.join(workDir, `.env.agent.${agentId}`)) : false;
29487
- const envInRoot = fs29.existsSync(path29.join(harnessRoot, `.env.agent.${agentId}`));
29488
- const envInWorktree = worktreePath ? fs29.existsSync(path29.join(worktreePath, `.env.agent.${agentId}`)) : false;
29634
+ const envInWorkDir = workDir ? fs30.existsSync(path30.join(workDir, `.env.agent.${agentId}`)) : false;
29635
+ const envInRoot = fs30.existsSync(path30.join(harnessRoot, `.env.agent.${agentId}`));
29636
+ const envInWorktree = worktreePath ? fs30.existsSync(path30.join(worktreePath, `.env.agent.${agentId}`)) : false;
29489
29637
  const active = session !== void 0 && session.status !== "completed" || envInWorkDir || envInRoot || envInWorktree;
29490
29638
  const latest = latestRunForAgent(runs, agentId);
29491
29639
  const verifyRun = runs.find((r2) => r2.agentId === agentId && r2.stageId === "verify");
@@ -29538,7 +29686,7 @@ function collectEnvironmentStatus(repoPath) {
29538
29686
  const slotIds = getAgentSlotIds(harnessRoot);
29539
29687
  const pm2Procs = listPm2Processes2();
29540
29688
  return {
29541
- repoPath: path29.resolve(repoPath),
29689
+ repoPath: path30.resolve(repoPath),
29542
29690
  harnessRoot,
29543
29691
  gitRemote: readGitRemote(harnessRoot),
29544
29692
  profile: manifest?.profile,
@@ -29549,13 +29697,13 @@ function collectEnvironmentStatus(repoPath) {
29549
29697
 
29550
29698
  // src/core/validations.ts
29551
29699
  var crypto3 = __toESM(require("crypto"));
29552
- var fs31 = __toESM(require("fs"));
29553
- var path31 = __toESM(require("path"));
29700
+ var fs32 = __toESM(require("fs"));
29701
+ var path32 = __toESM(require("path"));
29554
29702
 
29555
29703
  // src/core/change-batch.ts
29556
- var fs30 = __toESM(require("fs"));
29704
+ var fs31 = __toESM(require("fs"));
29557
29705
  var os8 = __toESM(require("os"));
29558
- var path30 = __toESM(require("path"));
29706
+ var path31 = __toESM(require("path"));
29559
29707
  function git(checkoutDir, args, env3) {
29560
29708
  const result = run(`git ${args}`, { cwd: checkoutDir, env: env3 });
29561
29709
  if (result.code !== 0) {
@@ -29571,7 +29719,7 @@ function isCheckoutRoot(checkoutDir) {
29571
29719
  const toplevel = tryGit(checkoutDir, "rev-parse --show-toplevel");
29572
29720
  if (!toplevel) return false;
29573
29721
  try {
29574
- return fs30.realpathSync(toplevel) === fs30.realpathSync(checkoutDir);
29722
+ return fs31.realpathSync(toplevel) === fs31.realpathSync(checkoutDir);
29575
29723
  } catch {
29576
29724
  return false;
29577
29725
  }
@@ -29593,7 +29741,7 @@ function isMergeOrRebaseInProgress(checkoutDir) {
29593
29741
  if (tryGit(checkoutDir, "rev-parse -q --verify MERGE_HEAD") !== void 0) return true;
29594
29742
  for (const dir of ["rebase-merge", "rebase-apply"]) {
29595
29743
  const gitPath = tryGit(checkoutDir, `rev-parse --git-path ${dir}`);
29596
- if (gitPath && fs30.existsSync(path30.resolve(checkoutDir, gitPath))) return true;
29744
+ if (gitPath && fs31.existsSync(path31.resolve(checkoutDir, gitPath))) return true;
29597
29745
  }
29598
29746
  return false;
29599
29747
  }
@@ -29616,8 +29764,8 @@ function parseNameStatus(output) {
29616
29764
  return files;
29617
29765
  }
29618
29766
  function computeWorktreeSnapshot(checkoutDir) {
29619
- const tmpDir = fs30.mkdtempSync(path30.join(os8.tmpdir(), "har-idx-"));
29620
- const tmpIndex = path30.join(tmpDir, "index");
29767
+ const tmpDir = fs31.mkdtempSync(path31.join(os8.tmpdir(), "har-idx-"));
29768
+ const tmpIndex = path31.join(tmpDir, "index");
29621
29769
  const env3 = { GIT_INDEX_FILE: tmpIndex };
29622
29770
  try {
29623
29771
  const headTree = getHeadTree(checkoutDir);
@@ -29640,35 +29788,35 @@ function computeWorktreeSnapshot(checkoutDir) {
29640
29788
  changedFiles: parseNameStatus(diffOutput)
29641
29789
  };
29642
29790
  } finally {
29643
- fs30.rmSync(tmpDir, { recursive: true, force: true });
29791
+ fs31.rmSync(tmpDir, { recursive: true, force: true });
29644
29792
  }
29645
29793
  }
29646
29794
 
29647
29795
  // src/core/validations.ts
29648
29796
  var VALIDATIONS_DIR = "validations";
29649
29797
  function getValidationsDir(checkoutDir) {
29650
- return path31.join(checkoutDir, ".har", VALIDATIONS_DIR);
29798
+ return path32.join(checkoutDir, ".har", VALIDATIONS_DIR);
29651
29799
  }
29652
29800
  function validationPath(checkoutDir, treeHash) {
29653
- return path31.join(getValidationsDir(checkoutDir), `${treeHash}.json`);
29801
+ return path32.join(getValidationsDir(checkoutDir), `${treeHash}.json`);
29654
29802
  }
29655
29803
  function writeRecord(checkoutDir, record2) {
29656
29804
  const dir = getValidationsDir(checkoutDir);
29657
- fs31.mkdirSync(dir, { recursive: true });
29658
- fs31.writeFileSync(
29805
+ fs32.mkdirSync(dir, { recursive: true });
29806
+ fs32.writeFileSync(
29659
29807
  validationPath(checkoutDir, record2.treeHash),
29660
29808
  `${JSON.stringify(record2, null, 2)}
29661
29809
  `
29662
29810
  );
29663
29811
  }
29664
29812
  function ensureValidationsIgnored(checkoutDir) {
29665
- const harDir = path31.join(checkoutDir, ".har");
29666
- if (!fs31.existsSync(harDir)) return;
29667
- const gitignorePath = path31.join(harDir, ".gitignore");
29668
- const content = fs31.existsSync(gitignorePath) ? fs31.readFileSync(gitignorePath, "utf8") : "";
29813
+ const harDir = path32.join(checkoutDir, ".har");
29814
+ if (!fs32.existsSync(harDir)) return;
29815
+ const gitignorePath = path32.join(harDir, ".gitignore");
29816
+ const content = fs32.existsSync(gitignorePath) ? fs32.readFileSync(gitignorePath, "utf8") : "";
29669
29817
  if (content.split("\n").some((line) => line.trim() === `${VALIDATIONS_DIR}/`)) return;
29670
29818
  const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
29671
- fs31.writeFileSync(gitignorePath, `${content}${suffix}${VALIDATIONS_DIR}/
29819
+ fs32.writeFileSync(gitignorePath, `${content}${suffix}${VALIDATIONS_DIR}/
29672
29820
  `);
29673
29821
  }
29674
29822
  function recordValidation(input) {
@@ -29685,8 +29833,8 @@ function recordValidation(input) {
29685
29833
  treeHash: snapshot.treeHash,
29686
29834
  headSha: snapshot.headSha,
29687
29835
  branch: snapshot.branch,
29688
- workDir: path31.resolve(input.checkoutDir),
29689
- harnessRoot: path31.resolve(input.harnessRoot),
29836
+ workDir: path32.resolve(input.checkoutDir),
29837
+ harnessRoot: path32.resolve(input.harnessRoot),
29690
29838
  agentId: input.agentId,
29691
29839
  status: input.status,
29692
29840
  full: input.full,
@@ -29698,16 +29846,16 @@ function recordValidation(input) {
29698
29846
  committedAt: existing?.committedAt
29699
29847
  });
29700
29848
  writeRecord(input.checkoutDir, record2);
29701
- if (path31.resolve(input.harnessRoot) !== path31.resolve(input.checkoutDir)) {
29849
+ if (path32.resolve(input.harnessRoot) !== path32.resolve(input.checkoutDir)) {
29702
29850
  writeRecord(input.harnessRoot, record2);
29703
29851
  }
29704
29852
  return record2;
29705
29853
  }
29706
29854
  function findValidation(checkoutDir, treeHash) {
29707
29855
  const file = validationPath(checkoutDir, treeHash);
29708
- if (!fs31.existsSync(file)) return void 0;
29856
+ if (!fs32.existsSync(file)) return void 0;
29709
29857
  try {
29710
- return ValidationRecordSchema.parse(JSON.parse(fs31.readFileSync(file, "utf8")));
29858
+ return ValidationRecordSchema.parse(JSON.parse(fs32.readFileSync(file, "utf8")));
29711
29859
  } catch {
29712
29860
  return void 0;
29713
29861
  }
@@ -29722,19 +29870,19 @@ function attachCommit(checkoutDir, treeHash, commitSha) {
29722
29870
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
29723
29871
  };
29724
29872
  writeRecord(checkoutDir, updated);
29725
- if (record2.harnessRoot && path31.resolve(record2.harnessRoot) !== path31.resolve(checkoutDir) && fs31.existsSync(path31.join(record2.harnessRoot, ".har"))) {
29873
+ if (record2.harnessRoot && path32.resolve(record2.harnessRoot) !== path32.resolve(checkoutDir) && fs32.existsSync(path32.join(record2.harnessRoot, ".har"))) {
29726
29874
  writeRecord(record2.harnessRoot, updated);
29727
29875
  }
29728
29876
  return updated;
29729
29877
  }
29730
29878
  function listValidations(harnessRoot) {
29731
29879
  const dir = getValidationsDir(harnessRoot);
29732
- if (!fs31.existsSync(dir)) return [];
29880
+ if (!fs32.existsSync(dir)) return [];
29733
29881
  const records = [];
29734
- for (const entry of fs31.readdirSync(dir)) {
29882
+ for (const entry of fs32.readdirSync(dir)) {
29735
29883
  if (!entry.endsWith(".json")) continue;
29736
29884
  try {
29737
- records.push(ValidationRecordSchema.parse(JSON.parse(fs31.readFileSync(path31.join(dir, entry), "utf8"))));
29885
+ records.push(ValidationRecordSchema.parse(JSON.parse(fs32.readFileSync(path32.join(dir, entry), "utf8"))));
29738
29886
  } catch {
29739
29887
  }
29740
29888
  }
@@ -29763,6 +29911,396 @@ function createRemoteExecutor(apiUrl, apiKey) {
29763
29911
  return new RemoteExecutor(url, key);
29764
29912
  }
29765
29913
 
29914
+ // src/core/telemetry-config.ts
29915
+ var fs33 = __toESM(require("fs"));
29916
+ var os9 = __toESM(require("os"));
29917
+ var path33 = __toESM(require("path"));
29918
+ function getPreferencePath() {
29919
+ if (process.env.HAR_TELEMETRY_CONFIG_PATH) {
29920
+ return path33.resolve(process.env.HAR_TELEMETRY_CONFIG_PATH);
29921
+ }
29922
+ return path33.join(os9.homedir(), ".har", "telemetry.json");
29923
+ }
29924
+ function parseEnvOverride(raw) {
29925
+ if (raw === void 0 || raw === "") return void 0;
29926
+ const normalized = raw.trim().toLowerCase();
29927
+ if (["0", "false", "off", "no", "disabled"].includes(normalized)) return false;
29928
+ if (["1", "true", "on", "yes", "enabled"].includes(normalized)) return true;
29929
+ return void 0;
29930
+ }
29931
+ function readTelemetryPreference() {
29932
+ const preferencePath = getPreferencePath();
29933
+ try {
29934
+ if (!fs33.existsSync(preferencePath)) {
29935
+ return { enabled: true };
29936
+ }
29937
+ const parsed = JSON.parse(fs33.readFileSync(preferencePath, "utf8"));
29938
+ return {
29939
+ enabled: parsed.enabled !== false,
29940
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : void 0
29941
+ };
29942
+ } catch {
29943
+ return { enabled: true };
29944
+ }
29945
+ }
29946
+ function writeTelemetryPreference(enabled) {
29947
+ const preference = {
29948
+ enabled,
29949
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
29950
+ };
29951
+ const preferencePath = getPreferencePath();
29952
+ fs33.mkdirSync(path33.dirname(preferencePath), { recursive: true });
29953
+ fs33.writeFileSync(preferencePath, JSON.stringify(preference, null, 2) + "\n");
29954
+ return preference;
29955
+ }
29956
+ function isTelemetryEnabled() {
29957
+ const envOverride = parseEnvOverride(process.env.HAR_TELEMETRY);
29958
+ if (envOverride !== void 0) return envOverride;
29959
+ return readTelemetryPreference().enabled;
29960
+ }
29961
+ function getTelemetryPreferencePath() {
29962
+ return getPreferencePath();
29963
+ }
29964
+ var TELEMETRY_SIGNALS = [
29965
+ "Claude Code: tokens (input/output/cache) and estimated USD cost via OTEL metrics",
29966
+ "Codex CLI: token usage via OTEL metrics (no native USD; harvest fills gaps)",
29967
+ "Fallback: har control sync harvests local Claude/Codex session files when OTEL is missing"
29968
+ ];
29969
+
29970
+ // src/core/usage-harvest/claude.ts
29971
+ var fs35 = __toESM(require("fs"));
29972
+ var os10 = __toESM(require("os"));
29973
+ var path35 = __toESM(require("path"));
29974
+
29975
+ // src/core/telemetry-env.ts
29976
+ var fs34 = __toESM(require("fs"));
29977
+ var path34 = __toESM(require("path"));
29978
+ function escapeAttrValue(value) {
29979
+ return value.replace(/[,=]/g, "_");
29980
+ }
29981
+ function buildOtelResourceAttributes(attrs) {
29982
+ const parts = [
29983
+ `har.session_key=${escapeAttrValue(attrs.sessionKey)}`,
29984
+ `har.agent_id=${attrs.agentId}`,
29985
+ `har.repo_path=${escapeAttrValue(attrs.repoPath)}`,
29986
+ `har.work_dir=${escapeAttrValue(attrs.workDir)}`
29987
+ ];
29988
+ if (attrs.branch) parts.push(`har.branch=${escapeAttrValue(attrs.branch)}`);
29989
+ if (attrs.suffix) parts.push(`har.suffix=${escapeAttrValue(attrs.suffix)}`);
29990
+ if (attrs.purpose) parts.push(`har.purpose=${escapeAttrValue(attrs.purpose)}`);
29991
+ return parts.join(",");
29992
+ }
29993
+ function buildSessionKey(input) {
29994
+ if (input.branch) return input.branch;
29995
+ const stamp = (input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()).replace(/[:.]/g, "-");
29996
+ return `agent-${input.agentId}-${input.suffix ?? stamp}`;
29997
+ }
29998
+ function buildTelemetryEnvBlock(attrs, options) {
29999
+ const apiUrl = getControlApiUrl().replace(/\/$/, "");
30000
+ const lines = [
30001
+ "",
30002
+ "# HAR session attribution (generated)",
30003
+ `HAR_SESSION_KEY=${attrs.sessionKey}`,
30004
+ `HAR_CONTROL_API_URL=${apiUrl}`,
30005
+ `OTEL_RESOURCE_ATTRIBUTES=${buildOtelResourceAttributes(attrs)}`
30006
+ ];
30007
+ const injectOtel = isTelemetryEnabled() && options?.otelReady !== false;
30008
+ if (injectOtel) {
30009
+ lines.push(
30010
+ "# HAR telemetry \u2192 Mission Control OTLP (disable: har telemetry off)",
30011
+ "CLAUDE_CODE_ENABLE_TELEMETRY=1",
30012
+ "OTEL_METRICS_EXPORTER=otlp",
30013
+ "OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
30014
+ `OTEL_EXPORTER_OTLP_ENDPOINT=${apiUrl}/api/otel`
30015
+ );
30016
+ }
30017
+ return lines.join("\n") + "\n";
30018
+ }
30019
+ var TELEMETRY_MARKER_START = "# HAR session attribution (generated)";
30020
+ var TELEMETRY_MARKER_END = "# end HAR telemetry";
30021
+ function appendTelemetryEnvToFile(envFilePath, attrs, options) {
30022
+ const block = buildTelemetryEnvBlock(attrs, options).trimEnd() + `
30023
+ ${TELEMETRY_MARKER_END}
30024
+ `;
30025
+ let existing = "";
30026
+ if (fs34.existsSync(envFilePath)) {
30027
+ existing = fs34.readFileSync(envFilePath, "utf8");
30028
+ const start = existing.indexOf(TELEMETRY_MARKER_START);
30029
+ if (start >= 0) {
30030
+ const end = existing.indexOf(TELEMETRY_MARKER_END, start);
30031
+ if (end >= 0) {
30032
+ existing = existing.slice(0, start).replace(/\n+$/, "\n") + existing.slice(end + TELEMETRY_MARKER_END.length).replace(/^\n+/, "\n");
30033
+ } else {
30034
+ existing = existing.slice(0, start).replace(/\n+$/, "\n");
30035
+ }
30036
+ }
30037
+ }
30038
+ fs34.mkdirSync(path34.dirname(envFilePath), { recursive: true });
30039
+ const combined = (existing.replace(/\n+$/, "\n") + block).replace(/^\n+/, "");
30040
+ fs34.writeFileSync(envFilePath, combined.endsWith("\n") ? combined : combined + "\n");
30041
+ }
30042
+ function buildCodexOtelSnippet(attrs) {
30043
+ const apiUrl = getControlApiUrl().replace(/\/$/, "");
30044
+ const resource = buildOtelResourceAttributes(attrs);
30045
+ return `# Merge into ~/.codex/config.toml (HAR does not overwrite your config)
30046
+ # Generated for session ${attrs.sessionKey}
30047
+
30048
+ [otel]
30049
+ exporter = "otlp-http"
30050
+ endpoint = "${apiUrl}/api/otel"
30051
+ metrics_exporter = "otlp-http"
30052
+
30053
+ # Optional: set resource attributes via environment when starting Codex:
30054
+ # export OTEL_RESOURCE_ATTRIBUTES="${resource}"
30055
+ # export HAR_SESSION_KEY="${attrs.sessionKey}"
30056
+ `;
30057
+ }
30058
+
30059
+ // src/core/usage-harvest/claude.ts
30060
+ function pathsMatch(candidate, targets) {
30061
+ const norm = path35.resolve(candidate);
30062
+ return targets.some((t2) => {
30063
+ const target = path35.resolve(t2);
30064
+ return norm === target || norm.startsWith(target + path35.sep) || target.startsWith(norm + path35.sep);
30065
+ });
30066
+ }
30067
+ function claudeProjectsRoot() {
30068
+ return process.env.HAR_CLAUDE_PROJECTS_DIR ? path35.resolve(process.env.HAR_CLAUDE_PROJECTS_DIR) : path35.join(os10.homedir(), ".claude", "projects");
30069
+ }
30070
+ function encodeClaudeProjectDir(cwd) {
30071
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
30072
+ }
30073
+ function readJsonlRecords(filePath) {
30074
+ if (!fs35.existsSync(filePath)) return [];
30075
+ const lines = fs35.readFileSync(filePath, "utf8").split(/\r?\n/);
30076
+ const out = [];
30077
+ for (const line of lines) {
30078
+ const trimmed = line.trim();
30079
+ if (!trimmed.startsWith("{")) continue;
30080
+ try {
30081
+ out.push(JSON.parse(trimmed));
30082
+ } catch {
30083
+ }
30084
+ }
30085
+ return out;
30086
+ }
30087
+ function extractClaudeUsageFromRecords(records) {
30088
+ let tokensInput = 0;
30089
+ let tokensOutput = 0;
30090
+ let tokensCacheRead = 0;
30091
+ let tokensCacheCreation = 0;
30092
+ let costUsd = null;
30093
+ for (const record2 of records) {
30094
+ if (!record2 || typeof record2 !== "object") continue;
30095
+ const payload = record2;
30096
+ if (payload.type === "result") {
30097
+ const usage2 = payload.usage ?? {};
30098
+ tokensInput = Number(usage2.input_tokens ?? 0) + Number(usage2.cache_read_input_tokens ?? 0);
30099
+ tokensOutput = Number(usage2.output_tokens ?? 0);
30100
+ tokensCacheRead = Number(usage2.cache_read_input_tokens ?? 0);
30101
+ tokensCacheCreation = Number(usage2.cache_creation_input_tokens ?? 0);
30102
+ if (payload.total_cost_usd != null) costUsd = Number(payload.total_cost_usd);
30103
+ }
30104
+ const nestedUsage = payload.usage ?? payload.message?.usage;
30105
+ if (nestedUsage && payload.type !== "result") {
30106
+ tokensInput += Number(nestedUsage.input_tokens ?? 0);
30107
+ tokensOutput += Number(nestedUsage.output_tokens ?? 0);
30108
+ tokensCacheRead += Number(nestedUsage.cache_read_input_tokens ?? 0);
30109
+ tokensCacheCreation += Number(nestedUsage.cache_creation_input_tokens ?? 0);
30110
+ }
30111
+ }
30112
+ return { tokensInput, tokensOutput, tokensCacheRead, tokensCacheCreation, costUsd };
30113
+ }
30114
+ function harvestClaudeUsage(slot) {
30115
+ const targets = [slot.workDir, slot.worktreePath].filter(Boolean);
30116
+ if (targets.length === 0) return null;
30117
+ const root = claudeProjectsRoot();
30118
+ if (!fs35.existsSync(root)) return null;
30119
+ const sessionKey = buildSessionKey({
30120
+ branch: slot.branch,
30121
+ agentId: slot.agentId,
30122
+ suffix: slot.suffix,
30123
+ createdAt: slot.sessionCreatedAt
30124
+ });
30125
+ let best = null;
30126
+ let bestMtime = 0;
30127
+ for (const entry of fs35.readdirSync(root, { withFileTypes: true })) {
30128
+ if (!entry.isDirectory()) continue;
30129
+ const projectDir = path35.join(root, entry.name);
30130
+ const encodedHit = targets.some((t2) => entry.name.includes(encodeClaudeProjectDir(t2).slice(0, 40)));
30131
+ for (const file of fs35.readdirSync(projectDir)) {
30132
+ if (!file.endsWith(".jsonl")) continue;
30133
+ const filePath = path35.join(projectDir, file);
30134
+ const stat = fs35.statSync(filePath);
30135
+ const records = readJsonlRecords(filePath);
30136
+ let cwdHit = encodedHit;
30137
+ for (const record2 of records) {
30138
+ if (!record2 || typeof record2 !== "object") continue;
30139
+ const cwd = String(record2.cwd ?? "");
30140
+ if (cwd && pathsMatch(cwd, targets)) {
30141
+ cwdHit = true;
30142
+ break;
30143
+ }
30144
+ }
30145
+ if (!cwdHit) continue;
30146
+ const usage2 = extractClaudeUsageFromRecords(records);
30147
+ if (usage2.tokensInput + usage2.tokensOutput + usage2.tokensCacheRead === 0 && (usage2.costUsd == null || usage2.costUsd === 0)) {
30148
+ continue;
30149
+ }
30150
+ if (stat.mtimeMs >= bestMtime) {
30151
+ bestMtime = stat.mtimeMs;
30152
+ best = usage2;
30153
+ }
30154
+ }
30155
+ }
30156
+ if (!best) return null;
30157
+ const now = (/* @__PURE__ */ new Date()).toISOString();
30158
+ const tokensTotal = best.tokensInput + best.tokensOutput + best.tokensCacheRead + best.tokensCacheCreation;
30159
+ return {
30160
+ sessionKey,
30161
+ agentId: slot.agentId,
30162
+ agentTool: "claude_code",
30163
+ workDir: slot.workDir,
30164
+ branch: slot.branch,
30165
+ suffix: slot.suffix,
30166
+ tokensInput: best.tokensInput,
30167
+ tokensOutput: best.tokensOutput,
30168
+ tokensCacheRead: best.tokensCacheRead,
30169
+ tokensCacheCreation: best.tokensCacheCreation,
30170
+ tokensTotal,
30171
+ costUsd: best.costUsd,
30172
+ sources: ["harvest"],
30173
+ firstSeenAt: slot.sessionCreatedAt ?? now,
30174
+ lastSeenAt: now
30175
+ };
30176
+ }
30177
+
30178
+ // src/core/usage-harvest/codex.ts
30179
+ var fs36 = __toESM(require("fs"));
30180
+ var os11 = __toESM(require("os"));
30181
+ var path36 = __toESM(require("path"));
30182
+ function pathsMatch2(candidate, targets) {
30183
+ const norm = path36.resolve(candidate);
30184
+ return targets.some((t2) => {
30185
+ const target = path36.resolve(t2);
30186
+ return norm === target || norm.startsWith(target + path36.sep) || target.startsWith(norm + path36.sep);
30187
+ });
30188
+ }
30189
+ function codexSessionsRoot() {
30190
+ return process.env.HAR_CODEX_SESSIONS_DIR ? path36.resolve(process.env.HAR_CODEX_SESSIONS_DIR) : path36.join(os11.homedir(), ".codex", "sessions");
30191
+ }
30192
+ function walkJsonlFiles(dir, out = []) {
30193
+ if (!fs36.existsSync(dir)) return out;
30194
+ for (const entry of fs36.readdirSync(dir, { withFileTypes: true })) {
30195
+ const full = path36.join(dir, entry.name);
30196
+ if (entry.isDirectory()) walkJsonlFiles(full, out);
30197
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(full);
30198
+ }
30199
+ return out;
30200
+ }
30201
+ function readJsonl(filePath) {
30202
+ const lines = fs36.readFileSync(filePath, "utf8").split(/\r?\n/);
30203
+ const out = [];
30204
+ for (const line of lines) {
30205
+ const trimmed = line.trim();
30206
+ if (!trimmed.startsWith("{")) continue;
30207
+ try {
30208
+ out.push(JSON.parse(trimmed));
30209
+ } catch {
30210
+ }
30211
+ }
30212
+ return out;
30213
+ }
30214
+ function extractCodexTokens(records) {
30215
+ let tokensInput = 0;
30216
+ let tokensOutput = 0;
30217
+ let tokensCacheRead = 0;
30218
+ let cwd;
30219
+ for (const record2 of records) {
30220
+ if (!record2 || typeof record2 !== "object") continue;
30221
+ const payload = record2;
30222
+ if (typeof payload.cwd === "string") cwd = payload.cwd;
30223
+ if (typeof payload.working_directory === "string") {
30224
+ cwd = payload.working_directory;
30225
+ }
30226
+ const usage2 = payload.usage ?? payload.token_usage ?? payload.event?.usage;
30227
+ if (usage2) {
30228
+ tokensInput += Number(
30229
+ usage2.input_tokens ?? usage2.input_token_count ?? usage2.input ?? 0
30230
+ );
30231
+ tokensOutput += Number(
30232
+ usage2.output_tokens ?? usage2.output_token_count ?? usage2.output ?? 0
30233
+ );
30234
+ tokensCacheRead += Number(
30235
+ usage2.cached_input_tokens ?? usage2.cached_token_count ?? usage2.cache_read_input_tokens ?? 0
30236
+ );
30237
+ tokensOutput += Number(usage2.reasoning_tokens ?? usage2.reasoning_token_count ?? 0);
30238
+ }
30239
+ if (payload.type === "response.completed" || payload["event.kind"] === "response.completed") {
30240
+ tokensInput = Math.max(tokensInput, Number(payload.input_token_count ?? 0));
30241
+ tokensOutput = Math.max(tokensOutput, Number(payload.output_token_count ?? 0));
30242
+ tokensCacheRead = Math.max(tokensCacheRead, Number(payload.cached_token_count ?? 0));
30243
+ }
30244
+ }
30245
+ return { tokensInput, tokensOutput, tokensCacheRead, cwd };
30246
+ }
30247
+ function harvestCodexUsage(slot) {
30248
+ const targets = [slot.workDir, slot.worktreePath].filter(Boolean);
30249
+ if (targets.length === 0) return null;
30250
+ const root = codexSessionsRoot();
30251
+ const files = walkJsonlFiles(root);
30252
+ if (files.length === 0) return null;
30253
+ const sessionKey = buildSessionKey({
30254
+ branch: slot.branch,
30255
+ agentId: slot.agentId,
30256
+ suffix: slot.suffix,
30257
+ createdAt: slot.sessionCreatedAt
30258
+ });
30259
+ let best = null;
30260
+ let bestMtime = 0;
30261
+ for (const file of files) {
30262
+ const records = readJsonl(file);
30263
+ const extracted = extractCodexTokens(records);
30264
+ if (!extracted.cwd || !pathsMatch2(extracted.cwd, targets)) continue;
30265
+ if (extracted.tokensInput + extracted.tokensOutput + extracted.tokensCacheRead === 0) continue;
30266
+ const mtime = fs36.statSync(file).mtimeMs;
30267
+ if (mtime >= bestMtime) {
30268
+ bestMtime = mtime;
30269
+ best = extracted;
30270
+ }
30271
+ }
30272
+ if (!best) return null;
30273
+ const now = (/* @__PURE__ */ new Date()).toISOString();
30274
+ const tokensTotal = best.tokensInput + best.tokensOutput + best.tokensCacheRead;
30275
+ return {
30276
+ sessionKey,
30277
+ agentId: slot.agentId,
30278
+ agentTool: "codex",
30279
+ workDir: slot.workDir,
30280
+ branch: slot.branch,
30281
+ suffix: slot.suffix,
30282
+ tokensInput: best.tokensInput,
30283
+ tokensOutput: best.tokensOutput,
30284
+ tokensCacheRead: best.tokensCacheRead,
30285
+ tokensCacheCreation: 0,
30286
+ tokensTotal,
30287
+ costUsd: null,
30288
+ sources: ["harvest"],
30289
+ firstSeenAt: slot.sessionCreatedAt ?? now,
30290
+ lastSeenAt: now
30291
+ };
30292
+ }
30293
+
30294
+ // src/core/usage-harvest/index.ts
30295
+ function harvestUsageForSlot(slot) {
30296
+ const out = [];
30297
+ const claude = harvestClaudeUsage(slot);
30298
+ if (claude) out.push(claude);
30299
+ const codex = harvestCodexUsage(slot);
30300
+ if (codex) out.push(codex);
30301
+ return out;
30302
+ }
30303
+
29766
30304
  // src/core/control-sync.ts
29767
30305
  async function postJson(url, body, dryRun) {
29768
30306
  if (dryRun) return null;
@@ -29792,7 +30330,7 @@ async function waitForControlApi(apiUrl = getControlApiUrl(), timeoutMs = 6e4, i
29792
30330
  const deadline = Date.now() + timeoutMs;
29793
30331
  while (Date.now() < deadline) {
29794
30332
  if (await isControlApiReachable(apiUrl)) return true;
29795
- await new Promise((resolve27) => setTimeout(resolve27, intervalMs));
30333
+ await new Promise((resolve33) => setTimeout(resolve33, intervalMs));
29796
30334
  }
29797
30335
  return false;
29798
30336
  }
@@ -29804,7 +30342,7 @@ async function syncAllKnownReposWithControl(options) {
29804
30342
  }
29805
30343
  const repoPaths = new Set(listRegisteredRepos());
29806
30344
  if (options?.cwd) {
29807
- const cwd = path32.resolve(options.cwd);
30345
+ const cwd = path37.resolve(options.cwd);
29808
30346
  if (readManifest(cwd)) repoPaths.add(cwd);
29809
30347
  }
29810
30348
  try {
@@ -29812,7 +30350,7 @@ async function syncAllKnownReposWithControl(options) {
29812
30350
  if (listResponse.ok) {
29813
30351
  const repos = await listResponse.json();
29814
30352
  for (const repo of repos) {
29815
- const resolved = path32.resolve(repo.path);
30353
+ const resolved = path37.resolve(repo.path);
29816
30354
  if (readManifest(resolved)) repoPaths.add(resolved);
29817
30355
  }
29818
30356
  }
@@ -29831,7 +30369,7 @@ async function syncAllKnownReposWithControl(options) {
29831
30369
  return { synced, failed };
29832
30370
  }
29833
30371
  async function registerRepoWithControl(options) {
29834
- const repoPath = path32.resolve(options.repoPath);
30372
+ const repoPath = path37.resolve(options.repoPath);
29835
30373
  const apiUrl = options.apiUrl ?? getControlApiUrl();
29836
30374
  const manifest = readManifest(repoPath);
29837
30375
  const stagesRegistry = readStageRegistry(repoPath);
@@ -29844,7 +30382,7 @@ async function registerRepoWithControl(options) {
29844
30382
  return postJson(`${apiUrl}/api/repos`, body, options.dryRun);
29845
30383
  }
29846
30384
  async function syncRepoWithControl(options) {
29847
- const repoPath = path32.resolve(options.repoPath);
30385
+ const repoPath = path37.resolve(options.repoPath);
29848
30386
  const apiUrl = options.apiUrl ?? getControlApiUrl();
29849
30387
  if (options.cloud) {
29850
30388
  const remote = createRemoteExecutor();
@@ -29872,7 +30410,7 @@ async function syncRepoWithControl(options) {
29872
30410
  const listResponse = await fetch(`${apiUrl}/api/repos`);
29873
30411
  if (!listResponse.ok) throw new Error("Failed to list repos from Control API");
29874
30412
  const repos = await listResponse.json();
29875
- const existing = repos.find((r2) => path32.resolve(r2.path) === repoPath);
30413
+ const existing = repos.find((r2) => path37.resolve(r2.path) === repoPath);
29876
30414
  if (!existing) throw new Error(`Repo not registered: ${repoPath}`);
29877
30415
  await syncRepoRunsAndSlots(apiUrl, existing.id, repoPath, options.dryRun);
29878
30416
  return;
@@ -29891,6 +30429,37 @@ async function syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun) {
29891
30429
  generatedAt: status.generatedAt
29892
30430
  });
29893
30431
  await postJson(`${apiUrl}/api/repos/${repoId}/slots`, slotsBody, dryRun);
30432
+ if (isTelemetryEnabled() && process.env.NODE_ENV !== "test") {
30433
+ try {
30434
+ const usage2 = status.slots.flatMap(
30435
+ (slot) => harvestUsageForSlot({
30436
+ agentId: slot.agentId,
30437
+ workDir: slot.workDir,
30438
+ worktreePath: slot.worktreePath,
30439
+ branch: slot.branch,
30440
+ suffix: slot.suffix,
30441
+ sessionCreatedAt: slot.sessionCreatedAt,
30442
+ repoPath
30443
+ }).map((row) => ({
30444
+ ...row,
30445
+ sessionKey: row.sessionKey || buildSessionKey({
30446
+ branch: slot.branch,
30447
+ agentId: slot.agentId,
30448
+ suffix: slot.suffix,
30449
+ createdAt: slot.sessionCreatedAt
30450
+ })
30451
+ }))
30452
+ );
30453
+ if (usage2.length > 0) {
30454
+ const usageBody = SyncUsageInputSchema.parse({ usage: usage2 });
30455
+ await postJson(`${apiUrl}/api/repos/${repoId}/usage`, usageBody, dryRun);
30456
+ }
30457
+ } catch (err) {
30458
+ const message = err instanceof Error ? err.message : String(err);
30459
+ process.stderr.write(`[har control] usage harvest skipped: ${message}
30460
+ `);
30461
+ }
30462
+ }
29894
30463
  const validations = listValidations(resolveHarnessRoot(repoPath));
29895
30464
  if (validations.length > 0) {
29896
30465
  const validationsBody = SyncValidationsInputSchema.parse({ validations });
@@ -29943,7 +30512,199 @@ function checkLaunchGuard2(repoPath, agentId, options = {}) {
29943
30512
  return { allowed: true, slot: occupied.slot, readiness };
29944
30513
  }
29945
30514
 
30515
+ // src/core/control-lifecycle.ts
30516
+ var import_child_process8 = require("child_process");
30517
+ var path38 = __toESM(require("path"));
30518
+
30519
+ // src/core/control-image.ts
30520
+ var DEFAULT_CONTROL_IMAGE = "theosfactory/har-control";
30521
+ function getControlImageName() {
30522
+ return process.env.HAR_CONTROL_IMAGE ?? DEFAULT_CONTROL_IMAGE;
30523
+ }
30524
+ function getControlImageTag() {
30525
+ return process.env.HAR_CONTROL_IMAGE_TAG ?? getHarPackageVersion();
30526
+ }
30527
+ function getControlImageRef() {
30528
+ return `${getControlImageName()}:${getControlImageTag()}`;
30529
+ }
30530
+ function shouldBuildControlLocally() {
30531
+ return process.env.HAR_CONTROL_BUILD === "true";
30532
+ }
30533
+
30534
+ // src/core/control-lifecycle.ts
30535
+ function resolveControlDir() {
30536
+ return path38.resolve(__dirname, "..", "control");
30537
+ }
30538
+ function resolveControlComposeFiles(options) {
30539
+ const controlDir = resolveControlDir();
30540
+ const files = [path38.join(controlDir, "docker-compose.yml")];
30541
+ if (options?.build ?? shouldBuildControlLocally()) {
30542
+ files.push(path38.join(controlDir, "docker-compose.build.yml"));
30543
+ }
30544
+ return files;
30545
+ }
30546
+ function buildDockerComposeEnv() {
30547
+ return {
30548
+ ...process.env,
30549
+ HAR_CONTROL_IMAGE: getControlImageName(),
30550
+ HAR_CONTROL_IMAGE_TAG: getControlImageTag()
30551
+ };
30552
+ }
30553
+ function runDockerCompose(args, options) {
30554
+ const controlDir = resolveControlDir();
30555
+ const composeFiles = resolveControlComposeFiles(options);
30556
+ const composeArgs = composeFiles.flatMap((file) => ["-f", file]);
30557
+ const result = (0, import_child_process8.spawnSync)("docker", ["compose", ...composeArgs, ...args], {
30558
+ cwd: controlDir,
30559
+ stdio: "inherit",
30560
+ env: buildDockerComposeEnv()
30561
+ });
30562
+ if (result.error) {
30563
+ throw result.error;
30564
+ }
30565
+ return result.status ?? 1;
30566
+ }
30567
+ async function startMissionControl(options) {
30568
+ const build = options.build ?? shouldBuildControlLocally();
30569
+ const imageRef = getControlImageRef();
30570
+ if (!build) {
30571
+ const pullCode = runDockerCompose(["pull"], { build: false });
30572
+ if (pullCode !== 0) {
30573
+ return { code: pullCode, apiUrl: getControlApiUrl(), imageRef };
30574
+ }
30575
+ }
30576
+ const detach = options.detach !== false;
30577
+ const upArgs = detach ? ["up", "-d"] : ["up"];
30578
+ if (build) {
30579
+ upArgs.push("--build");
30580
+ }
30581
+ const code = runDockerCompose(upArgs, { build });
30582
+ return { code, apiUrl: getControlApiUrl(), imageRef };
30583
+ }
30584
+ async function syncReposAfterControlStart(cwd) {
30585
+ if (!isControlEnabled()) {
30586
+ return { synced: 0, failed: 0, apiReady: false };
30587
+ }
30588
+ const apiUrl = getControlApiUrl();
30589
+ const apiReady = await waitForControlApi(apiUrl);
30590
+ if (!apiReady) {
30591
+ return { synced: 0, failed: 0, apiReady: false };
30592
+ }
30593
+ const { synced, failed } = await syncAllKnownReposWithControl({ apiUrl, cwd });
30594
+ return { synced, failed, apiReady: true };
30595
+ }
30596
+ async function startControlAndSync(options) {
30597
+ const { code, apiUrl, imageRef } = await startMissionControl({
30598
+ detach: options?.detach,
30599
+ build: options?.build
30600
+ });
30601
+ if (code !== 0) {
30602
+ return { code, apiUrl, imageRef, synced: 0, failed: 0, apiReady: false };
30603
+ }
30604
+ const { synced, failed, apiReady } = await syncReposAfterControlStart(options?.cwd ?? process.cwd());
30605
+ return { code, apiUrl, imageRef, synced, failed, apiReady };
30606
+ }
30607
+
30608
+ // src/core/telemetry-ensure.ts
30609
+ async function ensureTelemetryInfrastructure(options) {
30610
+ const apiUrl = getControlApiUrl();
30611
+ const telemetryEnabled = isTelemetryEnabled();
30612
+ const controlEnabled = isControlEnabled();
30613
+ const startIfNeeded = options?.startIfNeeded !== false;
30614
+ if (!telemetryEnabled) {
30615
+ return {
30616
+ telemetryEnabled: false,
30617
+ controlEnabled,
30618
+ apiUrl,
30619
+ reachable: false,
30620
+ started: false,
30621
+ otelReady: false,
30622
+ message: "Telemetry is off \u2014 Mission Control will not be auto-started. Enable with: har telemetry on"
30623
+ };
30624
+ }
30625
+ if (!controlEnabled) {
30626
+ return {
30627
+ telemetryEnabled: true,
30628
+ controlEnabled: false,
30629
+ apiUrl,
30630
+ reachable: false,
30631
+ started: false,
30632
+ otelReady: false,
30633
+ warning: "Telemetry is on but HAR_CONTROL_DISABLED=true \u2014 cannot start Mission Control. Unset it or run har control up manually."
30634
+ };
30635
+ }
30636
+ if (await isControlApiReachable(apiUrl)) {
30637
+ return {
30638
+ telemetryEnabled: true,
30639
+ controlEnabled: true,
30640
+ apiUrl,
30641
+ reachable: true,
30642
+ started: false,
30643
+ otelReady: true,
30644
+ message: `Telemetry on \u2014 Mission Control reachable at ${apiUrl}`
30645
+ };
30646
+ }
30647
+ if (!startIfNeeded || process.env.NODE_ENV === "test") {
30648
+ return {
30649
+ telemetryEnabled: true,
30650
+ controlEnabled: true,
30651
+ apiUrl,
30652
+ reachable: false,
30653
+ started: false,
30654
+ otelReady: false,
30655
+ warning: `Telemetry on but Mission Control is not reachable at ${apiUrl}. Run: har control up`
30656
+ };
30657
+ }
30658
+ try {
30659
+ const { code } = await startMissionControl({ detach: true });
30660
+ if (code !== 0) {
30661
+ return {
30662
+ telemetryEnabled: true,
30663
+ controlEnabled: true,
30664
+ apiUrl,
30665
+ reachable: false,
30666
+ started: false,
30667
+ otelReady: false,
30668
+ warning: `Telemetry on \u2014 failed to start Mission Control (exit ${code}). OTEL export skipped; harvest may fill gaps later. Fix Docker / run: har control up`
30669
+ };
30670
+ }
30671
+ } catch (err) {
30672
+ const detail = err instanceof Error ? err.message : String(err);
30673
+ return {
30674
+ telemetryEnabled: true,
30675
+ controlEnabled: true,
30676
+ apiUrl,
30677
+ reachable: false,
30678
+ started: false,
30679
+ otelReady: false,
30680
+ warning: `Telemetry on \u2014 could not start Mission Control (${detail}). OTEL export skipped. Run: har control up`
30681
+ };
30682
+ }
30683
+ const ready = await waitForControlApi(apiUrl, 9e4);
30684
+ if (!ready) {
30685
+ return {
30686
+ telemetryEnabled: true,
30687
+ controlEnabled: true,
30688
+ apiUrl,
30689
+ reachable: false,
30690
+ started: true,
30691
+ otelReady: false,
30692
+ warning: `Telemetry on \u2014 started Mission Control but API did not become ready at ${apiUrl}. OTEL export skipped for now.`
30693
+ };
30694
+ }
30695
+ return {
30696
+ telemetryEnabled: true,
30697
+ controlEnabled: true,
30698
+ apiUrl,
30699
+ reachable: true,
30700
+ started: true,
30701
+ otelReady: true,
30702
+ message: `Telemetry on \u2014 started Mission Control at ${apiUrl}`
30703
+ };
30704
+ }
30705
+
29946
30706
  // src/core/run-service.ts
30707
+ var path39 = __toESM(require("path"));
29947
30708
  function extractShellOutput(result) {
29948
30709
  const stdout = result.logs?.find((log) => log.stream === "stdout")?.content ?? result.logs?.find((log) => !log.stream)?.content ?? "";
29949
30710
  const stderr = result.logs?.find((log) => log.stream === "stderr")?.content ?? "";
@@ -30063,6 +30824,21 @@ var RunService = class {
30063
30824
  } : void 0
30064
30825
  };
30065
30826
  }
30827
+ let telemetryBanner = "";
30828
+ let otelReady = false;
30829
+ if (isTelemetryEnabled() && process.env.NODE_ENV !== "test") {
30830
+ const ensured = await ensureTelemetryInfrastructure({ startIfNeeded: true });
30831
+ otelReady = ensured.otelReady;
30832
+ if (ensured.message) {
30833
+ telemetryBanner += `${ensured.message}
30834
+ Usage from Claude Code / Codex will appear under Worktrees. Disable: har telemetry off
30835
+ `;
30836
+ }
30837
+ if (ensured.warning) {
30838
+ telemetryBanner += `${ensured.warning}
30839
+ `;
30840
+ }
30841
+ }
30066
30842
  const result = await this.runStage({
30067
30843
  repoPath: options.repoPath,
30068
30844
  kind: "launch",
@@ -30086,6 +30862,32 @@ var RunService = class {
30086
30862
  envResult.worktreePath = session.worktreePath;
30087
30863
  envResult.branch = session.branch;
30088
30864
  envResult.previewUrls = envResult.previewUrls ?? session.previewUrls;
30865
+ const sessionKey = buildSessionKey({
30866
+ branch: session.branch,
30867
+ agentId: options.agentId,
30868
+ suffix: session.suffix,
30869
+ createdAt: session.createdAt
30870
+ });
30871
+ const envFile = path39.join(session.workDir, `.env.agent.${options.agentId}`);
30872
+ try {
30873
+ appendTelemetryEnvToFile(
30874
+ envFile,
30875
+ {
30876
+ sessionKey,
30877
+ agentId: options.agentId,
30878
+ repoPath: path39.resolve(options.repoPath),
30879
+ workDir: session.workDir,
30880
+ branch: session.branch,
30881
+ suffix: session.suffix,
30882
+ purpose: session.purpose
30883
+ },
30884
+ { otelReady: otelReady && isTelemetryEnabled() }
30885
+ );
30886
+ } catch {
30887
+ }
30888
+ }
30889
+ if (telemetryBanner) {
30890
+ envResult.stderr = `${telemetryBanner}${envResult.stderr ?? ""}`;
30089
30891
  }
30090
30892
  }
30091
30893
  return envResult;
@@ -30320,11 +31122,36 @@ var envCommand = {
30320
31122
  }),
30321
31123
  handleMaintain
30322
31124
  ).command(
30323
- "add-stage <template>",
30324
- "Add an optional stage template (e.g. playwright)",
31125
+ "add-stage [template]",
31126
+ `Add a stage template (${STAGE_TEMPLATE_IDS.join(", ")}) or a custom stage (--custom)`,
30325
31127
  (y2) => y2.positional("template", {
30326
31128
  type: "string",
30327
- describe: "Stage template id (playwright)"
31129
+ describe: `Stage template id (${STAGE_TEMPLATE_IDS.join(", ")}), or the new stage id with --custom`
31130
+ }).option("list", {
31131
+ type: "boolean",
31132
+ default: false,
31133
+ describe: "List available stage templates and exit"
31134
+ }).option("custom", {
31135
+ type: "boolean",
31136
+ default: false,
31137
+ describe: "Register a custom stage instead of a shipped template"
31138
+ }).option("kind", {
31139
+ type: "string",
31140
+ describe: "Custom stage kind (setup, launch, verify, test, inspect, reset, teardown, custom)"
31141
+ }).option("command", {
31142
+ type: "string",
31143
+ describe: 'Custom stage shell command ({agentId} is substituted), e.g. "npm test"'
31144
+ }).option("script", {
31145
+ type: "boolean",
31146
+ default: false,
31147
+ describe: "Scaffold .har/stages/<id>.sh from the contract skeleton (see .har/STAGES.md)"
31148
+ }).option("description", {
31149
+ type: "string",
31150
+ describe: "Custom stage description shown in the registry and Mission Control"
31151
+ }).option("verification", {
31152
+ type: "boolean",
31153
+ default: false,
31154
+ describe: "Include the custom stage in verify --full (stages.json verificationStages)"
30328
31155
  }).option("repo", { type: "string", default: ".", describe: "Path to the repository" }).option("force", {
30329
31156
  type: "boolean",
30330
31157
  default: false,
@@ -30332,7 +31159,7 @@ var envCommand = {
30332
31159
  }).option("skip-ci", {
30333
31160
  type: "boolean",
30334
31161
  default: false,
30335
- describe: "Do not copy .github/workflows/playwright.yml"
31162
+ describe: "Do not copy optional CI workflow files (e.g. .github/workflows/playwright.yml)"
30336
31163
  }),
30337
31164
  handleAddStage
30338
31165
  ).command(
@@ -30427,7 +31254,7 @@ var envCommand = {
30427
31254
  }
30428
31255
  };
30429
31256
  async function handleInit(argv) {
30430
- const repoPath = path33.resolve(argv.repo);
31257
+ const repoPath = path40.resolve(argv.repo);
30431
31258
  header("har env init");
30432
31259
  info(`Repository: ${repoPath}`);
30433
31260
  try {
@@ -30486,7 +31313,7 @@ async function handleInit(argv) {
30486
31313
  }
30487
31314
  }
30488
31315
  async function handleMaintain(argv) {
30489
- const repoPath = path33.resolve(argv.repo);
31316
+ const repoPath = path40.resolve(argv.repo);
30490
31317
  header("har env maintain");
30491
31318
  info(`Repository: ${repoPath}`);
30492
31319
  try {
@@ -30582,7 +31409,7 @@ async function handleAgentMdProposal(repoPath, autoYes) {
30582
31409
  if (autoYes) {
30583
31410
  const proposal = readAgentMdProposal(repoPath);
30584
31411
  if (proposal) {
30585
- writeFileSafe(path33.join(repoPath, "AGENT.md"), proposal.content);
31412
+ writeFileSafe(path40.join(repoPath, "AGENT.md"), proposal.content);
30586
31413
  clearAgentMdProposal(repoPath);
30587
31414
  info("Applied AGENT.md proposal (--yes)");
30588
31415
  }
@@ -30591,16 +31418,64 @@ async function handleAgentMdProposal(repoPath, autoYes) {
30591
31418
  await promptApplyAgentMdProposal(repoPath);
30592
31419
  }
30593
31420
  async function handleAddStage(argv) {
30594
- const repoPath = path33.resolve(argv.repo);
30595
- if (argv.template !== "playwright") {
30596
- error(`Unknown stage template: ${argv.template ?? "(missing)"}. Available: playwright`);
31421
+ const repoPath = path40.resolve(argv.repo);
31422
+ const available = listStageTemplateIds();
31423
+ if (argv.list) {
31424
+ for (const id of available) {
31425
+ console.log(id);
31426
+ }
31427
+ return;
31428
+ }
31429
+ if (argv.custom) {
31430
+ if (!argv.template) {
31431
+ error(
31432
+ 'Missing stage id. Usage: har env add-stage <id> --custom (--command "npm test" | --script) [--kind test] [--verification]'
31433
+ );
31434
+ process.exit(1);
31435
+ }
31436
+ header("har env add-stage --custom");
31437
+ info(`Repository: ${repoPath}`);
31438
+ info(`Stage: ${argv.template}`);
31439
+ try {
31440
+ const result = addCustomStage(repoPath, {
31441
+ id: argv.template,
31442
+ kind: argv.kind,
31443
+ command: argv.command,
31444
+ script: argv.script,
31445
+ description: argv.description,
31446
+ verification: argv.verification,
31447
+ force: argv.force
31448
+ });
31449
+ divider();
31450
+ success(`Custom stage registered: ${result.stageId} (kind: ${result.kind}, ${result.mode})`);
31451
+ for (const file of result.filesWritten) {
31452
+ info(` + ${file}`);
31453
+ }
31454
+ console.error("");
31455
+ console.error(" Next steps:");
31456
+ for (const step of result.nextSteps) {
31457
+ console.error(` ${step}`);
31458
+ }
31459
+ console.error("");
31460
+ console.error(" Docs: .har/STAGES.md");
31461
+ console.error("");
31462
+ } catch (err) {
31463
+ error(err.message);
31464
+ process.exit(1);
31465
+ }
31466
+ return;
31467
+ }
31468
+ if (!argv.template || !available.includes(argv.template)) {
31469
+ error(
31470
+ `Unknown stage template: ${argv.template ?? "(missing)"}. Available: ${available.join(", ")}. For a project-specific stage, use: har env add-stage <id> --custom`
31471
+ );
30597
31472
  process.exit(1);
30598
31473
  }
30599
31474
  header("har env add-stage");
30600
31475
  info(`Repository: ${repoPath}`);
30601
31476
  info(`Template: ${argv.template}`);
30602
31477
  try {
30603
- const result = addStageTemplate(repoPath, "playwright", {
31478
+ const result = addStageTemplate(repoPath, argv.template, {
30604
31479
  force: argv.force,
30605
31480
  skipCi: argv.skipCi
30606
31481
  });
@@ -30612,7 +31487,7 @@ async function handleAddStage(argv) {
30612
31487
  console.error(` ${step}`);
30613
31488
  }
30614
31489
  console.error("");
30615
- console.error(" Docs: .har/stages/PLAYWRIGHT.md");
31490
+ console.error(` Docs: ${result.docsPath}`);
30616
31491
  console.error("");
30617
31492
  } catch (err) {
30618
31493
  error(err.message);
@@ -30620,7 +31495,7 @@ async function handleAddStage(argv) {
30620
31495
  }
30621
31496
  }
30622
31497
  async function handlePreflight(argv) {
30623
- const repo = path33.resolve(argv.repo);
31498
+ const repo = path40.resolve(argv.repo);
30624
31499
  const agentId = validateAgentId(argv.id, repo);
30625
31500
  const result = await preflightEnvironment({
30626
31501
  repoPath: repo,
@@ -30638,7 +31513,7 @@ async function handlePreflight(argv) {
30638
31513
  process.exit(result.code);
30639
31514
  }
30640
31515
  async function handleLaunch(argv) {
30641
- const repo = path33.resolve(argv.repo);
31516
+ const repo = path40.resolve(argv.repo);
30642
31517
  const agentId = validateAgentId(argv.id, repo);
30643
31518
  let confirmReplace = argv.replace;
30644
31519
  let force = argv.force;
@@ -30650,8 +31525,8 @@ async function handleLaunch(argv) {
30650
31525
  console.error(guard.reason ?? "");
30651
31526
  const readline5 = await import("readline");
30652
31527
  const rl = readline5.createInterface({ input: process.stdin, output: process.stderr });
30653
- const answer = await new Promise((resolve27) => {
30654
- rl.question("Replace this slot? [y/N] ", resolve27);
31528
+ const answer = await new Promise((resolve33) => {
31529
+ rl.question("Replace this slot? [y/N] ", resolve33);
30655
31530
  });
30656
31531
  rl.close();
30657
31532
  if (!/^[Yy]$/.test(answer.trim())) {
@@ -30667,8 +31542,8 @@ async function handleLaunch(argv) {
30667
31542
  warn("Worktree has uncommitted changes.");
30668
31543
  const readline5 = await import("readline");
30669
31544
  const rl = readline5.createInterface({ input: process.stdin, output: process.stderr });
30670
- const answer = await new Promise((resolve27) => {
30671
- rl.question("Discard uncommitted changes? [y/N] ", resolve27);
31545
+ const answer = await new Promise((resolve33) => {
31546
+ rl.question("Discard uncommitted changes? [y/N] ", resolve33);
30672
31547
  });
30673
31548
  rl.close();
30674
31549
  if (!/^[Yy]$/.test(answer.trim())) {
@@ -30697,6 +31572,13 @@ async function handleLaunch(argv) {
30697
31572
  process.exit(result.code || 2);
30698
31573
  }
30699
31574
  if (result.code === 0 && result.workDir) {
31575
+ if (result.stderr) {
31576
+ for (const line of result.stderr.split("\n").filter(Boolean)) {
31577
+ if (/telemetry|mission control|otel/i.test(line)) {
31578
+ info(line);
31579
+ }
31580
+ }
31581
+ }
30700
31582
  divider();
30701
31583
  success(`Session ready \u2014 make ALL file edits under: ${result.workDir}`);
30702
31584
  if (result.branch) info(`Branch: ${result.branch}`);
@@ -30715,7 +31597,7 @@ async function handleRecover(argv) {
30715
31597
  });
30716
31598
  }
30717
31599
  async function handleVerify(argv) {
30718
- const repo = path33.resolve(argv.repo);
31600
+ const repo = path40.resolve(argv.repo);
30719
31601
  const agentId = validateAgentId(argv.id, repo);
30720
31602
  const result = await runVerification({
30721
31603
  repoPath: repo,
@@ -30727,7 +31609,7 @@ async function handleVerify(argv) {
30727
31609
  process.exit(result.code);
30728
31610
  }
30729
31611
  async function handleTeardown(argv) {
30730
- const repo = path33.resolve(argv.repo);
31612
+ const repo = path40.resolve(argv.repo);
30731
31613
  const agentId = validateAgentId(argv.id, repo);
30732
31614
  const result = await teardownEnvironment({
30733
31615
  repoPath: repo,
@@ -30738,7 +31620,7 @@ async function handleTeardown(argv) {
30738
31620
  process.exit(result.code);
30739
31621
  }
30740
31622
  async function handleComplete(argv) {
30741
- const repo = path33.resolve(argv.repo);
31623
+ const repo = path40.resolve(argv.repo);
30742
31624
  const agentId = validateAgentId(argv.id, repo);
30743
31625
  const result = await completeEnvironment({
30744
31626
  repoPath: repo,
@@ -30759,7 +31641,7 @@ async function handleComplete(argv) {
30759
31641
  process.exit(result.code);
30760
31642
  }
30761
31643
  async function handleStatus(argv) {
30762
- const repoPath = path33.resolve(argv.repo);
31644
+ const repoPath = path40.resolve(argv.repo);
30763
31645
  if (argv.json) {
30764
31646
  const status = collectEnvironmentStatus(repoPath);
30765
31647
  const output = EnvironmentStatusSchema.parse(status);
@@ -30772,7 +31654,7 @@ async function handleStatus(argv) {
30772
31654
  });
30773
31655
  }
30774
31656
  async function handleRunsList(argv) {
30775
- const repoPath = path33.resolve(argv.repo);
31657
+ const repoPath = path40.resolve(argv.repo);
30776
31658
  const runs = listRuns(repoPath, { stageId: argv.stage, limit: argv.limit });
30777
31659
  if (argv.json) {
30778
31660
  process.stdout.write(JSON.stringify({ runs }, null, 2) + "\n");
@@ -30789,7 +31671,7 @@ async function handleRunsList(argv) {
30789
31671
  }
30790
31672
  }
30791
31673
  async function handleRunsGet(argv) {
30792
- const run2 = getRun(path33.resolve(argv.repo), argv.runId);
31674
+ const run2 = getRun(path40.resolve(argv.repo), argv.runId);
30793
31675
  if (!run2) {
30794
31676
  error(`Run not found: ${argv.runId}`);
30795
31677
  process.exit(1);
@@ -30862,102 +31744,7 @@ function printNextSteps(auto2) {
30862
31744
  }
30863
31745
 
30864
31746
  // src/cli/commands/control.ts
30865
- var path35 = __toESM(require("path"));
30866
-
30867
- // src/core/control-lifecycle.ts
30868
- var import_child_process8 = require("child_process");
30869
- var path34 = __toESM(require("path"));
30870
-
30871
- // src/core/control-image.ts
30872
- var DEFAULT_CONTROL_IMAGE = "theosfactory/har-control";
30873
- function getControlImageName() {
30874
- return process.env.HAR_CONTROL_IMAGE ?? DEFAULT_CONTROL_IMAGE;
30875
- }
30876
- function getControlImageTag() {
30877
- return process.env.HAR_CONTROL_IMAGE_TAG ?? getHarPackageVersion();
30878
- }
30879
- function getControlImageRef() {
30880
- return `${getControlImageName()}:${getControlImageTag()}`;
30881
- }
30882
- function shouldBuildControlLocally() {
30883
- return process.env.HAR_CONTROL_BUILD === "true";
30884
- }
30885
-
30886
- // src/core/control-lifecycle.ts
30887
- function resolveControlDir() {
30888
- return path34.resolve(__dirname, "..", "control");
30889
- }
30890
- function resolveControlComposeFiles(options) {
30891
- const controlDir = resolveControlDir();
30892
- const files = [path34.join(controlDir, "docker-compose.yml")];
30893
- if (options?.build ?? shouldBuildControlLocally()) {
30894
- files.push(path34.join(controlDir, "docker-compose.build.yml"));
30895
- }
30896
- return files;
30897
- }
30898
- function buildDockerComposeEnv() {
30899
- return {
30900
- ...process.env,
30901
- HAR_CONTROL_IMAGE: getControlImageName(),
30902
- HAR_CONTROL_IMAGE_TAG: getControlImageTag()
30903
- };
30904
- }
30905
- function runDockerCompose(args, options) {
30906
- const controlDir = resolveControlDir();
30907
- const composeFiles = resolveControlComposeFiles(options);
30908
- const composeArgs = composeFiles.flatMap((file) => ["-f", file]);
30909
- const result = (0, import_child_process8.spawnSync)("docker", ["compose", ...composeArgs, ...args], {
30910
- cwd: controlDir,
30911
- stdio: "inherit",
30912
- env: buildDockerComposeEnv()
30913
- });
30914
- if (result.error) {
30915
- throw result.error;
30916
- }
30917
- return result.status ?? 1;
30918
- }
30919
- async function startMissionControl(options) {
30920
- const build = options.build ?? shouldBuildControlLocally();
30921
- const imageRef = getControlImageRef();
30922
- if (!build) {
30923
- const pullCode = runDockerCompose(["pull"], { build: false });
30924
- if (pullCode !== 0) {
30925
- return { code: pullCode, apiUrl: getControlApiUrl(), imageRef };
30926
- }
30927
- }
30928
- const detach = options.detach !== false;
30929
- const upArgs = detach ? ["up", "-d"] : ["up"];
30930
- if (build) {
30931
- upArgs.push("--build");
30932
- }
30933
- const code = runDockerCompose(upArgs, { build });
30934
- return { code, apiUrl: getControlApiUrl(), imageRef };
30935
- }
30936
- async function syncReposAfterControlStart(cwd) {
30937
- if (!isControlEnabled()) {
30938
- return { synced: 0, failed: 0, apiReady: false };
30939
- }
30940
- const apiUrl = getControlApiUrl();
30941
- const apiReady = await waitForControlApi(apiUrl);
30942
- if (!apiReady) {
30943
- return { synced: 0, failed: 0, apiReady: false };
30944
- }
30945
- const { synced, failed } = await syncAllKnownReposWithControl({ apiUrl, cwd });
30946
- return { synced, failed, apiReady: true };
30947
- }
30948
- async function startControlAndSync(options) {
30949
- const { code, apiUrl, imageRef } = await startMissionControl({
30950
- detach: options?.detach,
30951
- build: options?.build
30952
- });
30953
- if (code !== 0) {
30954
- return { code, apiUrl, imageRef, synced: 0, failed: 0, apiReady: false };
30955
- }
30956
- const { synced, failed, apiReady } = await syncReposAfterControlStart(options?.cwd ?? process.cwd());
30957
- return { code, apiUrl, imageRef, synced, failed, apiReady };
30958
- }
30959
-
30960
- // src/cli/commands/control.ts
31747
+ var path41 = __toESM(require("path"));
30961
31748
  var controlCommand = {
30962
31749
  command: "control <subcommand>",
30963
31750
  describe: "Mission Control dashboard (local)",
@@ -31060,7 +31847,7 @@ async function handleDown() {
31060
31847
  process.exit(code);
31061
31848
  }
31062
31849
  async function handleRegister(argv) {
31063
- const repoPath = path35.resolve(argv.repo);
31850
+ const repoPath = path41.resolve(argv.repo);
31064
31851
  header("har control register");
31065
31852
  info(`Repository: ${repoPath}`);
31066
31853
  try {
@@ -31080,7 +31867,7 @@ async function handleRegister(argv) {
31080
31867
  }
31081
31868
  }
31082
31869
  async function handleSync(argv) {
31083
- const repoPath = path35.resolve(argv.repo);
31870
+ const repoPath = path41.resolve(argv.repo);
31084
31871
  try {
31085
31872
  await syncRepoWithControl({
31086
31873
  repoPath,
@@ -31119,7 +31906,7 @@ async function handleWatch(argv) {
31119
31906
  };
31120
31907
  const tick = async () => {
31121
31908
  if (argv.repo) {
31122
- await syncOne(path35.resolve(argv.repo));
31909
+ await syncOne(path41.resolve(argv.repo));
31123
31910
  return;
31124
31911
  }
31125
31912
  try {
@@ -31150,12 +31937,12 @@ async function handleLogin(argv) {
31150
31937
  }
31151
31938
 
31152
31939
  // src/cli/commands/hooks.ts
31153
- var path38 = __toESM(require("path"));
31940
+ var path44 = __toESM(require("path"));
31154
31941
 
31155
31942
  // src/core/hooks.ts
31156
- var fs32 = __toESM(require("fs"));
31157
- var os9 = __toESM(require("os"));
31158
- var path36 = __toESM(require("path"));
31943
+ var fs37 = __toESM(require("fs"));
31944
+ var os12 = __toESM(require("os"));
31945
+ var path42 = __toESM(require("path"));
31159
31946
  var MARKER_START = "# >>> har commit gate (managed by `har hooks`) >>>";
31160
31947
  var MARKER_END = "# <<< har commit gate <<<";
31161
31948
  var PRE_COMMIT_BLOCK = [
@@ -31180,7 +31967,7 @@ function resolveCheckoutRoot(cwd) {
31180
31967
  function resolveHooksDir(checkoutDir) {
31181
31968
  const hooksPath = tryGit2(checkoutDir, "rev-parse --git-path hooks");
31182
31969
  if (!hooksPath) throw new Error(`Not a git checkout: ${checkoutDir}`);
31183
- return path36.resolve(checkoutDir, hooksPath);
31970
+ return path42.resolve(checkoutDir, hooksPath);
31184
31971
  }
31185
31972
  function getConfiguredHooksPath(checkoutDir) {
31186
31973
  return tryGit2(checkoutDir, "config core.hooksPath") || void 0;
@@ -31188,9 +31975,9 @@ function getConfiguredHooksPath(checkoutDir) {
31188
31975
  function defaultHarInvocation() {
31189
31976
  const entry = process.argv[1];
31190
31977
  if (entry && entry.endsWith(".js")) {
31191
- return `"${process.execPath}" "${path36.resolve(entry)}"`;
31978
+ return `"${process.execPath}" "${path42.resolve(entry)}"`;
31192
31979
  }
31193
- if (entry) return `"${path36.resolve(entry)}"`;
31980
+ if (entry) return `"${path42.resolve(entry)}"`;
31194
31981
  return "har";
31195
31982
  }
31196
31983
  function buildHookScript(harInvocation, subcommand, failOpenNotice) {
@@ -31211,40 +31998,40 @@ exit 0
31211
31998
  `;
31212
31999
  }
31213
32000
  function upsertMarkedBlock(filePath, block) {
31214
- if (!fs32.existsSync(filePath)) {
31215
- fs32.writeFileSync(filePath, `#!/bin/sh
32001
+ if (!fs37.existsSync(filePath)) {
32002
+ fs37.writeFileSync(filePath, `#!/bin/sh
31216
32003
  ${block}
31217
32004
  `, { mode: 493 });
31218
32005
  return "created";
31219
32006
  }
31220
- const content = fs32.readFileSync(filePath, "utf8");
32007
+ const content = fs37.readFileSync(filePath, "utf8");
31221
32008
  if (content.includes(MARKER_START)) {
31222
32009
  const pattern = new RegExp(
31223
32010
  `${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}`
31224
32011
  );
31225
- fs32.writeFileSync(filePath, content.replace(pattern, block));
31226
- fs32.chmodSync(filePath, 493);
32012
+ fs37.writeFileSync(filePath, content.replace(pattern, block));
32013
+ fs37.chmodSync(filePath, 493);
31227
32014
  return "updated";
31228
32015
  }
31229
32016
  const suffix = content.endsWith("\n") ? "" : "\n";
31230
- fs32.writeFileSync(filePath, `${content}${suffix}
32017
+ fs37.writeFileSync(filePath, `${content}${suffix}
31231
32018
  ${block}
31232
32019
  `);
31233
- fs32.chmodSync(filePath, 493);
32020
+ fs37.chmodSync(filePath, 493);
31234
32021
  return "appended";
31235
32022
  }
31236
32023
  function removeMarkedBlock(filePath) {
31237
- if (!fs32.existsSync(filePath)) return false;
31238
- const content = fs32.readFileSync(filePath, "utf8");
32024
+ if (!fs37.existsSync(filePath)) return false;
32025
+ const content = fs37.readFileSync(filePath, "utf8");
31239
32026
  if (!content.includes(MARKER_START)) return false;
31240
32027
  const pattern = new RegExp(
31241
32028
  `\\n?${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}\\n?`
31242
32029
  );
31243
32030
  const stripped = content.replace(pattern, "\n");
31244
32031
  if (stripped.replace(/^#!\/bin\/sh\n?/, "").trim() === "") {
31245
- fs32.rmSync(filePath);
32032
+ fs37.rmSync(filePath);
31246
32033
  } else {
31247
- fs32.writeFileSync(filePath, stripped);
32034
+ fs37.writeFileSync(filePath, stripped);
31248
32035
  }
31249
32036
  return true;
31250
32037
  }
@@ -31266,20 +32053,20 @@ Or re-run with --force to write into that directory anyway.`
31266
32053
  );
31267
32054
  }
31268
32055
  const hooksDir = resolveHooksDir(checkout);
31269
- fs32.mkdirSync(hooksDir, { recursive: true });
32056
+ fs37.mkdirSync(hooksDir, { recursive: true });
31270
32057
  const invocation = options.harInvocation ?? defaultHarInvocation();
31271
- fs32.writeFileSync(
31272
- path36.join(hooksDir, "har-pre-commit"),
32058
+ fs37.writeFileSync(
32059
+ path42.join(hooksDir, "har-pre-commit"),
31273
32060
  buildHookScript(invocation, "check", "har: binary not found; skipping commit gate (reinstall with har hooks install)"),
31274
32061
  { mode: 493 }
31275
32062
  );
31276
- fs32.writeFileSync(
31277
- path36.join(hooksDir, "har-post-commit"),
32063
+ fs37.writeFileSync(
32064
+ path42.join(hooksDir, "har-post-commit"),
31278
32065
  buildHookScript(invocation, "record-commit", "har: binary not found; skipping commit association"),
31279
32066
  { mode: 493 }
31280
32067
  );
31281
- const preCommit = upsertMarkedBlock(path36.join(hooksDir, "pre-commit"), PRE_COMMIT_BLOCK);
31282
- const postCommit = upsertMarkedBlock(path36.join(hooksDir, "post-commit"), POST_COMMIT_BLOCK);
32068
+ const preCommit = upsertMarkedBlock(path42.join(hooksDir, "pre-commit"), PRE_COMMIT_BLOCK);
32069
+ const postCommit = upsertMarkedBlock(path42.join(hooksDir, "post-commit"), POST_COMMIT_BLOCK);
31283
32070
  ensureValidationsIgnored(checkout);
31284
32071
  return { hooksDir, preCommit, postCommit };
31285
32072
  }
@@ -31288,12 +32075,12 @@ function uninstallHooks(repoPath) {
31288
32075
  if (!checkout) throw new Error(`Not a git repository: ${repoPath}`);
31289
32076
  const hooksDir = resolveHooksDir(checkout);
31290
32077
  let removed = false;
31291
- removed = removeMarkedBlock(path36.join(hooksDir, "pre-commit")) || removed;
31292
- removed = removeMarkedBlock(path36.join(hooksDir, "post-commit")) || removed;
32078
+ removed = removeMarkedBlock(path42.join(hooksDir, "pre-commit")) || removed;
32079
+ removed = removeMarkedBlock(path42.join(hooksDir, "post-commit")) || removed;
31293
32080
  for (const file of ["har-pre-commit", "har-post-commit"]) {
31294
- const full = path36.join(hooksDir, file);
31295
- if (fs32.existsSync(full)) {
31296
- fs32.rmSync(full);
32081
+ const full = path42.join(hooksDir, file);
32082
+ if (fs37.existsSync(full)) {
32083
+ fs37.rmSync(full);
31297
32084
  removed = true;
31298
32085
  }
31299
32086
  }
@@ -31310,9 +32097,9 @@ function getCommitGateConfig(checkoutDir) {
31310
32097
  function isAgentWorktree(checkoutDir) {
31311
32098
  const branch = getCurrentBranch(checkoutDir);
31312
32099
  if (branch && /^har-agent-\d+$/.test(branch)) return true;
31313
- const worktreesRoot = path36.join(os9.homedir(), "worktrees");
31314
- const resolved = path36.resolve(checkoutDir);
31315
- return resolved.startsWith(`${worktreesRoot}${path36.sep}`) && /-agent-\d+$/.test(path36.basename(resolved));
32100
+ const worktreesRoot = path42.join(os12.homedir(), "worktrees");
32101
+ const resolved = path42.resolve(checkoutDir);
32102
+ return resolved.startsWith(`${worktreesRoot}${path42.sep}`) && /-agent-\d+$/.test(path42.basename(resolved));
31316
32103
  }
31317
32104
  function resolveEffectiveMode(checkoutDir, gate) {
31318
32105
  if (!gate.enabled) return "off";
@@ -31325,15 +32112,15 @@ function getHooksStatus(repoPath) {
31325
32112
  const hooksDir = resolveHooksDir(checkout);
31326
32113
  const gate = getCommitGateConfig(checkout);
31327
32114
  const hasBlock = (name) => {
31328
- const file = path36.join(hooksDir, name);
31329
- return fs32.existsSync(file) && fs32.readFileSync(file, "utf8").includes(MARKER_START);
32115
+ const file = path42.join(hooksDir, name);
32116
+ return fs37.existsSync(file) && fs37.readFileSync(file, "utf8").includes(MARKER_START);
31330
32117
  };
31331
32118
  return {
31332
32119
  checkout,
31333
32120
  hooksDir,
31334
32121
  configuredHooksPath: getConfiguredHooksPath(checkout),
31335
- preCommitInstalled: hasBlock("pre-commit") && fs32.existsSync(path36.join(hooksDir, "har-pre-commit")),
31336
- postCommitInstalled: hasBlock("post-commit") && fs32.existsSync(path36.join(hooksDir, "har-post-commit")),
32122
+ preCommitInstalled: hasBlock("pre-commit") && fs37.existsSync(path42.join(hooksDir, "har-pre-commit")),
32123
+ postCommitInstalled: hasBlock("post-commit") && fs37.existsSync(path42.join(hooksDir, "har-post-commit")),
31337
32124
  gate,
31338
32125
  effectiveMode: resolveEffectiveMode(checkout, gate)
31339
32126
  };
@@ -31344,7 +32131,7 @@ function checkCommitGate(cwd) {
31344
32131
  }
31345
32132
  const checkout = resolveCheckoutRoot(cwd);
31346
32133
  if (!checkout) return { exitCode: 0, messages: [] };
31347
- if (!fs32.existsSync(path36.join(checkout, ".har", "stages.json"))) {
32134
+ if (!fs37.existsSync(path42.join(checkout, ".har", "stages.json"))) {
31348
32135
  return { exitCode: 0, messages: [] };
31349
32136
  }
31350
32137
  const gate = getCommitGateConfig(checkout);
@@ -31415,8 +32202,8 @@ async function recordCommitAssociation(cwd) {
31415
32202
  }
31416
32203
 
31417
32204
  // src/core/claude-hooks.ts
31418
- var fs33 = __toESM(require("fs"));
31419
- var path37 = __toESM(require("path"));
32205
+ var fs38 = __toESM(require("fs"));
32206
+ var path43 = __toESM(require("path"));
31420
32207
  var CLAUDE_GUARD_RELATIVE_PATH = ".har/hooks/claude-worktree-guard.sh";
31421
32208
  var CLAUDE_SETTINGS_RELATIVE_PATH = ".claude/settings.json";
31422
32209
  var GUARD_MATCHER = "Edit|Write|MultiEdit|NotebookEdit";
@@ -31427,8 +32214,8 @@ function isHarGuardEntry(entry) {
31427
32214
  );
31428
32215
  }
31429
32216
  function readSettings(settingsPath) {
31430
- if (!fs33.existsSync(settingsPath)) return {};
31431
- const raw = fs33.readFileSync(settingsPath, "utf8").trim();
32217
+ if (!fs38.existsSync(settingsPath)) return {};
32218
+ const raw = fs38.readFileSync(settingsPath, "utf8").trim();
31432
32219
  if (raw === "") return {};
31433
32220
  const parsed = JSON.parse(raw);
31434
32221
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -31437,19 +32224,19 @@ function readSettings(settingsPath) {
31437
32224
  return parsed;
31438
32225
  }
31439
32226
  function writeSettings(settingsPath, settings) {
31440
- fs33.mkdirSync(path37.dirname(settingsPath), { recursive: true });
31441
- fs33.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
32227
+ fs38.mkdirSync(path43.dirname(settingsPath), { recursive: true });
32228
+ fs38.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
31442
32229
  }
31443
32230
  function installClaudeGuard(repoPath) {
31444
32231
  const templatePath = resolveTemplateFile("claude-worktree-guard.sh.template");
31445
32232
  if (!templatePath) {
31446
32233
  throw new Error("Guard template not found (claude-worktree-guard.sh.template). Run npm run build.");
31447
32234
  }
31448
- const guardScript = path37.join(repoPath, CLAUDE_GUARD_RELATIVE_PATH);
31449
- fs33.mkdirSync(path37.dirname(guardScript), { recursive: true });
31450
- fs33.copyFileSync(templatePath, guardScript);
31451
- fs33.chmodSync(guardScript, 493);
31452
- const settingsPath = path37.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
32235
+ const guardScript = path43.join(repoPath, CLAUDE_GUARD_RELATIVE_PATH);
32236
+ fs38.mkdirSync(path43.dirname(guardScript), { recursive: true });
32237
+ fs38.copyFileSync(templatePath, guardScript);
32238
+ fs38.chmodSync(guardScript, 493);
32239
+ const settingsPath = path43.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
31453
32240
  const settings = readSettings(settingsPath);
31454
32241
  const hooks = settings.hooks ?? {};
31455
32242
  const preToolUse = (hooks.PreToolUse ?? []).filter((entry) => !isHarGuardEntry(entry));
@@ -31463,15 +32250,15 @@ function installClaudeGuard(repoPath) {
31463
32250
  }
31464
32251
  function uninstallClaudeGuard(repoPath) {
31465
32252
  let removed = false;
31466
- const guardScript = path37.join(repoPath, CLAUDE_GUARD_RELATIVE_PATH);
31467
- if (fs33.existsSync(guardScript)) {
31468
- fs33.rmSync(guardScript);
32253
+ const guardScript = path43.join(repoPath, CLAUDE_GUARD_RELATIVE_PATH);
32254
+ if (fs38.existsSync(guardScript)) {
32255
+ fs38.rmSync(guardScript);
31469
32256
  removed = true;
31470
- const parent = path37.dirname(guardScript);
31471
- if (fs33.existsSync(parent) && fs33.readdirSync(parent).length === 0) fs33.rmdirSync(parent);
32257
+ const parent = path43.dirname(guardScript);
32258
+ if (fs38.existsSync(parent) && fs38.readdirSync(parent).length === 0) fs38.rmdirSync(parent);
31472
32259
  }
31473
- const settingsPath = path37.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
31474
- if (fs33.existsSync(settingsPath)) {
32260
+ const settingsPath = path43.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
32261
+ if (fs38.existsSync(settingsPath)) {
31475
32262
  const settings = readSettings(settingsPath);
31476
32263
  const preToolUse = settings.hooks?.PreToolUse;
31477
32264
  if (preToolUse) {
@@ -31496,8 +32283,8 @@ function uninstallClaudeGuard(repoPath) {
31496
32283
  return { removed };
31497
32284
  }
31498
32285
  function claudeGuardInstalled(repoPath) {
31499
- const settingsPath = path37.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
31500
- if (!fs33.existsSync(settingsPath)) return false;
32286
+ const settingsPath = path43.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
32287
+ if (!fs38.existsSync(settingsPath)) return false;
31501
32288
  try {
31502
32289
  const settings = readSettings(settingsPath);
31503
32290
  return (settings.hooks?.PreToolUse ?? []).some(isHarGuardEntry);
@@ -31513,7 +32300,7 @@ function repoOption(y2) {
31513
32300
  function handleInstall2(argv) {
31514
32301
  try {
31515
32302
  if (argv.claude) {
31516
- const result2 = installClaudeGuard(path38.resolve(argv.repo));
32303
+ const result2 = installClaudeGuard(path44.resolve(argv.repo));
31517
32304
  success("Claude Code worktree guard installed.");
31518
32305
  info(`Guard script: ${result2.guardScript}`);
31519
32306
  info(`Settings: ${result2.settingsPath} (PreToolUse: Edit|Write|MultiEdit|NotebookEdit)`);
@@ -31521,7 +32308,7 @@ function handleInstall2(argv) {
31521
32308
  info("Commit both files so the guard travels with the repo.");
31522
32309
  return;
31523
32310
  }
31524
- const result = installHooks({ repoPath: path38.resolve(argv.repo), force: argv.force });
32311
+ const result = installHooks({ repoPath: path44.resolve(argv.repo), force: argv.force });
31525
32312
  success(`Commit gate installed in ${result.hooksDir}`);
31526
32313
  info(`pre-commit: ${result.preCommit}, post-commit: ${result.postCommit}`);
31527
32314
  info("Commits of unverified change batches will be blocked in agent worktrees.");
@@ -31534,7 +32321,7 @@ function handleInstall2(argv) {
31534
32321
  function handleUninstall(argv) {
31535
32322
  try {
31536
32323
  if (argv.claude) {
31537
- const result2 = uninstallClaudeGuard(path38.resolve(argv.repo));
32324
+ const result2 = uninstallClaudeGuard(path44.resolve(argv.repo));
31538
32325
  if (result2.removed) {
31539
32326
  success("Claude Code worktree guard removed.");
31540
32327
  } else {
@@ -31542,7 +32329,7 @@ function handleUninstall(argv) {
31542
32329
  }
31543
32330
  return;
31544
32331
  }
31545
- const result = uninstallHooks(path38.resolve(argv.repo));
32332
+ const result = uninstallHooks(path44.resolve(argv.repo));
31546
32333
  if (result.removed) {
31547
32334
  success(`Commit gate removed from ${result.hooksDir}`);
31548
32335
  } else {
@@ -31555,7 +32342,7 @@ function handleUninstall(argv) {
31555
32342
  }
31556
32343
  function handleStatus2(argv) {
31557
32344
  try {
31558
- const status = getHooksStatus(path38.resolve(argv.repo));
32345
+ const status = getHooksStatus(path44.resolve(argv.repo));
31559
32346
  if (argv.json) {
31560
32347
  process.stdout.write(`${JSON.stringify(status, null, 2)}
31561
32348
  `);
@@ -31573,7 +32360,7 @@ function handleStatus2(argv) {
31573
32360
  );
31574
32361
  info(`Effective: ${status.effectiveMode} (in this checkout)`);
31575
32362
  info(
31576
- `Claude guard: ${claudeGuardInstalled(path38.resolve(argv.repo)) ? `installed (${CLAUDE_GUARD_RELATIVE_PATH})` : "not installed"}`
32363
+ `Claude guard: ${claudeGuardInstalled(path44.resolve(argv.repo)) ? `installed (${CLAUDE_GUARD_RELATIVE_PATH})` : "not installed"}`
31577
32364
  );
31578
32365
  } catch (err) {
31579
32366
  error(err instanceof Error ? err.message : String(err));
@@ -31834,10 +32621,10 @@ function assignProp(target, prop, value) {
31834
32621
  configurable: true
31835
32622
  });
31836
32623
  }
31837
- function getElementAtPath(obj, path40) {
31838
- if (!path40)
32624
+ function getElementAtPath(obj, path47) {
32625
+ if (!path47)
31839
32626
  return obj;
31840
- return path40.reduce((acc, key) => acc?.[key], obj);
32627
+ return path47.reduce((acc, key) => acc?.[key], obj);
31841
32628
  }
31842
32629
  function promiseAllObject(promisesObj) {
31843
32630
  const keys = Object.keys(promisesObj);
@@ -32157,11 +32944,11 @@ function aborted(x2, startIndex = 0) {
32157
32944
  }
32158
32945
  return false;
32159
32946
  }
32160
- function prefixIssues(path40, issues) {
32947
+ function prefixIssues(path47, issues) {
32161
32948
  return issues.map((iss) => {
32162
32949
  var _a3;
32163
32950
  (_a3 = iss).path ?? (_a3.path = []);
32164
- iss.path.unshift(path40);
32951
+ iss.path.unshift(path47);
32165
32952
  return iss;
32166
32953
  });
32167
32954
  }
@@ -37484,7 +38271,7 @@ var Protocol = class {
37484
38271
  return;
37485
38272
  }
37486
38273
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
37487
- await new Promise((resolve27) => setTimeout(resolve27, pollInterval));
38274
+ await new Promise((resolve33) => setTimeout(resolve33, pollInterval));
37488
38275
  options?.signal?.throwIfAborted();
37489
38276
  }
37490
38277
  } catch (error3) {
@@ -37501,7 +38288,7 @@ var Protocol = class {
37501
38288
  */
37502
38289
  request(request, resultSchema, options) {
37503
38290
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
37504
- return new Promise((resolve27, reject) => {
38291
+ return new Promise((resolve33, reject) => {
37505
38292
  const earlyReject = (error3) => {
37506
38293
  reject(error3);
37507
38294
  };
@@ -37579,7 +38366,7 @@ var Protocol = class {
37579
38366
  if (!parseResult.success) {
37580
38367
  reject(parseResult.error);
37581
38368
  } else {
37582
- resolve27(parseResult.data);
38369
+ resolve33(parseResult.data);
37583
38370
  }
37584
38371
  } catch (error3) {
37585
38372
  reject(error3);
@@ -37840,12 +38627,12 @@ var Protocol = class {
37840
38627
  }
37841
38628
  } catch {
37842
38629
  }
37843
- return new Promise((resolve27, reject) => {
38630
+ return new Promise((resolve33, reject) => {
37844
38631
  if (signal.aborted) {
37845
38632
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
37846
38633
  return;
37847
38634
  }
37848
- const timeoutId = setTimeout(resolve27, interval);
38635
+ const timeoutId = setTimeout(resolve33, interval);
37849
38636
  signal.addEventListener("abort", () => {
37850
38637
  clearTimeout(timeoutId);
37851
38638
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -38635,7 +39422,7 @@ var Server = class extends Protocol {
38635
39422
  };
38636
39423
 
38637
39424
  // src/mcp/server.ts
38638
- var path39 = __toESM(require("path"));
39425
+ var path45 = __toESM(require("path"));
38639
39426
 
38640
39427
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
38641
39428
  var import_node_process2 = __toESM(require("node:process"), 1);
@@ -38718,12 +39505,12 @@ var StdioServerTransport = class {
38718
39505
  this.onclose?.();
38719
39506
  }
38720
39507
  send(message) {
38721
- return new Promise((resolve27) => {
39508
+ return new Promise((resolve33) => {
38722
39509
  const json = serializeMessage(message);
38723
39510
  if (this._stdout.write(json)) {
38724
- resolve27();
39511
+ resolve33();
38725
39512
  } else {
38726
- this._stdout.once("drain", resolve27);
39513
+ this._stdout.once("drain", resolve33);
38727
39514
  }
38728
39515
  });
38729
39516
  }
@@ -39267,7 +40054,7 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
39267
40054
  const input = ControlUpInputSchema.parse({ ...args, repo });
39268
40055
  const result = await startControlAndSync({
39269
40056
  detach: input.detach,
39270
- cwd: path39.resolve(input.repo)
40057
+ cwd: path45.resolve(input.repo)
39271
40058
  });
39272
40059
  if (result.code !== 0) {
39273
40060
  return {
@@ -39329,9 +40116,231 @@ var mcpCommand = {
39329
40116
  }
39330
40117
  };
39331
40118
 
40119
+ // src/cli/commands/telemetry.ts
40120
+ var fs39 = __toESM(require("fs"));
40121
+ var path46 = __toESM(require("path"));
40122
+ function printSignals() {
40123
+ info("Collected when enabled (local Mission Control only):");
40124
+ for (const signal of TELEMETRY_SIGNALS) {
40125
+ info(` \u2022 ${signal}`);
40126
+ }
40127
+ info("Disable anytime: har telemetry off");
40128
+ }
40129
+ async function handleStatus3(argv) {
40130
+ const preference = readTelemetryPreference();
40131
+ const enabled = isTelemetryEnabled();
40132
+ const apiUrl = getControlApiUrl();
40133
+ const reachable = enabled ? await isControlApiReachable(apiUrl) : false;
40134
+ const payload = {
40135
+ enabled,
40136
+ preferenceEnabled: preference.enabled,
40137
+ preferencePath: getTelemetryPreferencePath(),
40138
+ updatedAt: preference.updatedAt ?? null,
40139
+ envOverride: process.env.HAR_TELEMETRY ?? null,
40140
+ controlApiUrl: apiUrl,
40141
+ controlReachable: reachable,
40142
+ otelEndpoint: `${apiUrl.replace(/\/$/, "")}/api/otel`,
40143
+ signals: [...TELEMETRY_SIGNALS]
40144
+ };
40145
+ if (argv.json) {
40146
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
40147
+ `);
40148
+ return;
40149
+ }
40150
+ header("har telemetry status");
40151
+ info(`Enabled: ${enabled ? "yes" : "no"}${process.env.HAR_TELEMETRY ? " (HAR_TELEMETRY override)" : ""}`);
40152
+ info(`Preference: ${getTelemetryPreferencePath()}${preference.updatedAt ? ` (updated ${preference.updatedAt})` : " (default on)"}`);
40153
+ info(`Mission Control: ${apiUrl} (${reachable ? "reachable" : "not reachable"})`);
40154
+ info(`OTLP endpoint: ${payload.otelEndpoint}`);
40155
+ printSignals();
40156
+ }
40157
+ async function handleOn() {
40158
+ header("har telemetry on");
40159
+ writeTelemetryPreference(true);
40160
+ success("Telemetry enabled (opt-out). Agent usage will be stored in local Mission Control.");
40161
+ printSignals();
40162
+ const result = await ensureTelemetryInfrastructure({ startIfNeeded: true });
40163
+ if (result.message) success(result.message);
40164
+ if (result.warning) warn(result.warning);
40165
+ if (result.otelReady) {
40166
+ info(`OTLP ingest: ${result.apiUrl.replace(/\/$/, "")}/api/otel`);
40167
+ info("Usage appears under Mission Control \u2192 Worktrees. Disable: har telemetry off");
40168
+ }
40169
+ }
40170
+ async function handleOff() {
40171
+ header("har telemetry off");
40172
+ writeTelemetryPreference(false);
40173
+ success("Telemetry disabled.");
40174
+ info("Future launches will not inject OTEL exporters or auto-start Mission Control.");
40175
+ info("Existing usage rows in Mission Control are kept. Historical data is not deleted.");
40176
+ }
40177
+ async function handleWriteEnv(argv) {
40178
+ const repoPath = path46.resolve(argv.repo);
40179
+ const harnessRoot = resolveHarnessRoot(repoPath);
40180
+ const session = readSlotRegistry(harnessRoot, argv.agentId);
40181
+ const workDir = argv.workDir ?? session?.workDir ?? repoPath;
40182
+ const branch = argv.branch ?? session?.branch;
40183
+ const suffix = argv.suffix ?? session?.suffix;
40184
+ const purpose = argv.purpose ?? session?.purpose;
40185
+ const sessionKey = argv.sessionKey ?? buildSessionKey({
40186
+ branch,
40187
+ agentId: argv.agentId,
40188
+ suffix,
40189
+ createdAt: session?.createdAt
40190
+ });
40191
+ const envFile = argv.envFile ?? path46.join(workDir, `.env.agent.${argv.agentId}`);
40192
+ let otelReady = argv.otelReady;
40193
+ if (otelReady === void 0 && isTelemetryEnabled()) {
40194
+ const ensured = await ensureTelemetryInfrastructure({ startIfNeeded: false });
40195
+ otelReady = ensured.otelReady;
40196
+ }
40197
+ const attrs = {
40198
+ sessionKey,
40199
+ agentId: argv.agentId,
40200
+ repoPath,
40201
+ workDir,
40202
+ branch,
40203
+ suffix,
40204
+ purpose
40205
+ };
40206
+ appendTelemetryEnvToFile(envFile, attrs, { otelReady: otelReady !== false && isTelemetryEnabled() });
40207
+ success(`Wrote telemetry env to ${envFile}`);
40208
+ }
40209
+ function handlePrintEnv(argv) {
40210
+ const repoPath = path46.resolve(argv.repo);
40211
+ const workDir = argv.workDir ?? repoPath;
40212
+ const sessionKey = buildSessionKey({
40213
+ branch: argv.branch,
40214
+ agentId: argv.agentId,
40215
+ suffix: argv.suffix
40216
+ });
40217
+ process.stdout.write(
40218
+ buildTelemetryEnvBlock(
40219
+ {
40220
+ sessionKey,
40221
+ agentId: argv.agentId,
40222
+ repoPath,
40223
+ workDir,
40224
+ branch: argv.branch,
40225
+ suffix: argv.suffix
40226
+ },
40227
+ { otelReady: argv.otelReady && isTelemetryEnabled() }
40228
+ )
40229
+ );
40230
+ }
40231
+ function handleCodexSnippet(argv) {
40232
+ const repoPath = path46.resolve(argv.repo);
40233
+ const harnessRoot = resolveHarnessRoot(repoPath);
40234
+ const session = readSlotRegistry(harnessRoot, argv.agentId);
40235
+ const workDir = argv.workDir ?? session?.workDir ?? repoPath;
40236
+ const branch = argv.branch ?? session?.branch;
40237
+ const suffix = argv.suffix ?? session?.suffix;
40238
+ const sessionKey = buildSessionKey({
40239
+ branch,
40240
+ agentId: argv.agentId,
40241
+ suffix,
40242
+ createdAt: session?.createdAt
40243
+ });
40244
+ const snippet = buildCodexOtelSnippet({
40245
+ sessionKey,
40246
+ agentId: argv.agentId,
40247
+ repoPath,
40248
+ workDir,
40249
+ branch,
40250
+ suffix,
40251
+ purpose: session?.purpose
40252
+ });
40253
+ if (argv.write) {
40254
+ const outDir = path46.join(harnessRoot, ".har", "telemetry");
40255
+ fs39.mkdirSync(outDir, { recursive: true });
40256
+ const outPath = path46.join(outDir, "codex.otel.toml.snippet");
40257
+ fs39.writeFileSync(outPath, snippet);
40258
+ success(`Wrote ${outPath}`);
40259
+ info("Merge the [otel] table into ~/.codex/config.toml manually.");
40260
+ return;
40261
+ }
40262
+ process.stdout.write(snippet);
40263
+ }
40264
+ var telemetryCommand = {
40265
+ command: "telemetry <subcommand>",
40266
+ describe: "Agent usage telemetry (Claude Code / Codex \u2192 Mission Control)",
40267
+ builder: (yargs) => yargs.command(
40268
+ "status",
40269
+ "Show telemetry preference and Mission Control reachability",
40270
+ (y2) => y2.option("json", { type: "boolean", default: false }),
40271
+ (argv) => {
40272
+ void handleStatus3({ json: argv.json });
40273
+ }
40274
+ ).command(
40275
+ "on",
40276
+ "Enable telemetry (default) and ensure Mission Control is running",
40277
+ () => {
40278
+ },
40279
+ () => {
40280
+ void handleOn();
40281
+ }
40282
+ ).command(
40283
+ "off",
40284
+ "Disable telemetry (no OTEL injection, no MC auto-start)",
40285
+ () => {
40286
+ },
40287
+ () => {
40288
+ void handleOff();
40289
+ }
40290
+ ).command(
40291
+ "write-env",
40292
+ "Append session + OTEL env vars to .env.agent.<id>",
40293
+ (y2) => y2.option("agent-id", { type: "number", demandOption: true }).option("repo", { type: "string", default: "." }).option("env-file", { type: "string" }).option("work-dir", { type: "string" }).option("branch", { type: "string" }).option("suffix", { type: "string" }).option("purpose", { type: "string" }).option("session-key", { type: "string" }).option("otel-ready", { type: "boolean" }),
40294
+ (argv) => handleWriteEnv({
40295
+ agentId: argv["agent-id"],
40296
+ repo: argv.repo,
40297
+ envFile: argv["env-file"],
40298
+ workDir: argv["work-dir"],
40299
+ branch: argv.branch,
40300
+ suffix: argv.suffix,
40301
+ purpose: argv.purpose,
40302
+ sessionKey: argv["session-key"],
40303
+ otelReady: argv["otel-ready"]
40304
+ }).catch((err) => {
40305
+ error(err instanceof Error ? err.message : String(err));
40306
+ process.exitCode = 1;
40307
+ })
40308
+ ).command(
40309
+ "print-env",
40310
+ "Print telemetry env block to stdout",
40311
+ (y2) => y2.option("agent-id", { type: "number", demandOption: true }).option("repo", { type: "string", default: "." }).option("work-dir", { type: "string" }).option("branch", { type: "string" }).option("suffix", { type: "string" }).option("otel-ready", { type: "boolean", default: true }),
40312
+ (argv) => handlePrintEnv({
40313
+ agentId: argv["agent-id"],
40314
+ repo: argv.repo,
40315
+ workDir: argv["work-dir"],
40316
+ branch: argv.branch,
40317
+ suffix: argv.suffix,
40318
+ otelReady: argv["otel-ready"]
40319
+ })
40320
+ ).command(
40321
+ "codex-snippet",
40322
+ "Print or write a Codex [otel] config snippet",
40323
+ (y2) => y2.option("agent-id", { type: "number", demandOption: true }).option("repo", { type: "string", default: "." }).option("work-dir", { type: "string" }).option("branch", { type: "string" }).option("suffix", { type: "string" }).option("write", {
40324
+ type: "boolean",
40325
+ default: false,
40326
+ describe: "Write to .har/telemetry/codex.otel.toml.snippet"
40327
+ }),
40328
+ (argv) => handleCodexSnippet({
40329
+ agentId: argv["agent-id"],
40330
+ repo: argv.repo,
40331
+ workDir: argv["work-dir"],
40332
+ branch: argv.branch,
40333
+ suffix: argv.suffix,
40334
+ write: argv.write
40335
+ })
40336
+ ).demandCommand(1, "Please specify a subcommand: status, on, off, write-env, print-env, codex-snippet"),
40337
+ handler: () => {
40338
+ }
40339
+ };
40340
+
39332
40341
  // src/cli/index.ts
39333
40342
  async function runCli() {
39334
- await yargs_default(hideBin(process.argv)).scriptName("har").usage("$0 <command> [options]").command(envCommand).command(agentsCommand).command(controlCommand).command(hooksCommand).command(mcpCommand).demandCommand(1, "Please specify a command. Try: har env init").strict().help().version(getHarPackageVersion()).parse();
40343
+ await yargs_default(hideBin(process.argv)).scriptName("har").usage("$0 <command> [options]").command(envCommand).command(agentsCommand).command(controlCommand).command(hooksCommand).command(mcpCommand).command(telemetryCommand).demandCommand(1, "Please specify a command. Try: har env init").strict().help().version(getHarPackageVersion()).parse();
39335
40344
  }
39336
40345
 
39337
40346
  // src/index.ts