@osfactory/har 0.57.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +772 -234
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -553,8 +553,8 @@ var init_parseUtil = __esm({
553
553
  init_errors();
554
554
  init_en();
555
555
  makeIssue = (params) => {
556
- const { data, path: path65, errorMaps, issueData } = params;
557
- const fullPath = [...path65, ...issueData.path || []];
556
+ const { data, path: path66, errorMaps, issueData } = params;
557
+ const fullPath = [...path66, ...issueData.path || []];
558
558
  const fullIssue = {
559
559
  ...issueData,
560
560
  path: fullPath
@@ -862,11 +862,11 @@ var init_types = __esm({
862
862
  init_parseUtil();
863
863
  init_util();
864
864
  ParseInputLazyPath = class {
865
- constructor(parent, value, path65, key) {
865
+ constructor(parent, value, path66, key) {
866
866
  this._cachedPath = [];
867
867
  this.parent = parent;
868
868
  this.data = value;
869
- this._path = path65;
869
+ this._path = path66;
870
870
  this._key = key;
871
871
  }
872
872
  get path() {
@@ -4248,7 +4248,7 @@ var init_zod = __esm({
4248
4248
  });
4249
4249
 
4250
4250
  // packages/schemas/src/schema.ts
4251
- var HAR_STAGE_KINDS, HAR_AGENT_SLOT_MIN, AgentSkillTargetSchema, ScaffoldedAgentFileSchema, HarnessManifestSchema, HarnessStageKindSchema, HarnessArtifactKindSchema, HarnessArtifactSchema, HarnessStageIdSchema, HarnessStageSchema, HarnessAgentSlotsSchema, CommitGateConfigSchema, OnboardingPreferencesSchema, HarnessStageRegistrySchema, HarnessVerificationStepSchema, ShellRunOutputSchema, HarnessVerificationResultSchema, HarnessStageRunStatusSchema, HarnessStageLogSchema, HarnessStageUrlSchema, HarnessStageRunResultSchema, StageKindSchema, VerificationResultSchema, StageResultSchema, RunRecordTriggerSchema, WorkUnitIdSchema, WorkAttemptIdSchema, WorkUnitMetadataSchema, WorkUnitOutcomeSchema, WorkUnitRecordSchema, WorkAttemptRecordSchema, RunRecordSchema, SlotRegistryEntrySchema, AgentSlotHarnessUsageSchema, PreflightBlockerSchema, SlotReadinessSchema, AgentSlotStatusSchema, EnvironmentStatusSchema, RegisterRepoInputSchema, UnregisterRepoInputSchema, UnregisterWorktreeResultSchema, UnregisterRepoResultSchema, ResetMissionControlInputSchema, HarnessScrubResultSchema, ResetMissionControlResultSchema, DeleteWorktreesInputSchema, DeleteWorktreeResultSchema, DeleteWorktreesResultSchema, SyncRunsInputSchema, SyncSlotsInputSchema, SyncWorkUnitsInputSchema, ChangedFileStatusSchema, ChangedFileSchema, ValidationRecordSchema, SyncValidationsInputSchema, ValidationBindingRecordSchema, SyncValidationBindingsInputSchema, AgentToolSchema, UsageSourceSchema, AgentTrajectorySourceSchema, AgentTrajectoryContentDisclosureSchema, AgentTrajectoryRecordV1Schema, AgentSessionUsageSchema, SyncUsageInputSchema, AgentSessionEventSchema, SyncSessionEventsInputSchema;
4251
+ var HAR_STAGE_KINDS, HAR_AGENT_SLOT_MIN, AgentSkillTargetSchema, ScaffoldedAgentFileSchema, HarnessManifestSchema, HarnessStageKindSchema, HarnessArtifactKindSchema, HarnessArtifactSchema, HarnessStageIdSchema, HarnessStageSchema, HarnessAgentSlotsSchema, CommitGateConfigSchema, OnboardingPreferencesSchema, HarnessStageRegistrySchema, HarnessVerificationStepSchema, ShellRunOutputSchema, HarnessVerificationResultSchema, HarnessStageRunStatusSchema, HarnessStageLogSchema, HarnessStageUrlSchema, HarnessStageRunResultSchema, StageKindSchema, VerificationResultSchema, StageResultSchema, RunRecordTriggerSchema, WorkUnitIdSchema, WorkAttemptIdSchema, WorkUnitRelatedLinkSchema, WorkUnitMetadataSchema, WorkUnitOutcomeSchema, WorkUnitRecordSchema, WorkAttemptRecordSchema, RunRecordSchema, SlotRegistryEntrySchema, AgentSlotHarnessUsageSchema, PreflightBlockerSchema, SlotReadinessSchema, AgentSlotStatusSchema, EnvironmentStatusSchema, RegisterRepoInputSchema, UnregisterRepoInputSchema, UnregisterWorktreeResultSchema, UnregisterRepoResultSchema, ResetMissionControlInputSchema, HarnessScrubResultSchema, ResetMissionControlResultSchema, DeleteWorktreesInputSchema, DeleteWorktreeResultSchema, DeleteWorktreesResultSchema, SyncRunsInputSchema, SyncSlotsInputSchema, SyncWorkUnitsInputSchema, ChangedFileStatusSchema, ChangedFileSchema, ValidationRecordSchema, SyncValidationsInputSchema, ValidationBindingRecordSchema, SyncValidationBindingsInputSchema, AgentToolSchema, UsageSourceSchema, AgentTrajectorySourceSchema, AgentTrajectoryContentDisclosureSchema, AgentTrajectoryRecordV1Schema, AgentSessionUsageSchema, SyncUsageInputSchema, AgentSessionEventSchema, SyncSessionEventsInputSchema;
4252
4252
  var init_schema = __esm({
4253
4253
  "packages/schemas/src/schema.ts"() {
4254
4254
  "use strict";
@@ -4413,12 +4413,18 @@ var init_schema = __esm({
4413
4413
  "work unit id must start with an alphanumeric character and contain only stable identifier characters"
4414
4414
  );
4415
4415
  WorkAttemptIdSchema = external_exports.string().uuid();
4416
+ WorkUnitRelatedLinkSchema = external_exports.object({
4417
+ source: external_exports.string().min(1).max(64),
4418
+ url: external_exports.string().url(),
4419
+ label: external_exports.string().min(1).max(128).optional()
4420
+ });
4416
4421
  WorkUnitMetadataSchema = external_exports.object({
4417
4422
  workUnitId: WorkUnitIdSchema,
4418
4423
  source: external_exports.string().min(1).max(64).optional(),
4419
4424
  sourceUrl: external_exports.string().url().optional(),
4420
4425
  title: external_exports.string().min(1).max(256).optional(),
4421
- parentWorkUnitId: WorkUnitIdSchema.optional()
4426
+ parentWorkUnitId: WorkUnitIdSchema.optional(),
4427
+ relatedLinks: external_exports.array(WorkUnitRelatedLinkSchema).optional()
4422
4428
  });
4423
4429
  WorkUnitOutcomeSchema = external_exports.object({
4424
4430
  decision: external_exports.enum(["completed", "abandoned"]),
@@ -28758,7 +28764,7 @@ function runScript(scriptPath, args = [], options = {}) {
28758
28764
  function runScriptCapture(scriptPath, args = [], options = {}) {
28759
28765
  const stream = options.stream ?? false;
28760
28766
  const { stream: _stream, ...spawnOptions } = options;
28761
- return new Promise((resolve49) => {
28767
+ return new Promise((resolve50) => {
28762
28768
  const proc = (0, import_child_process.spawn)("bash", [scriptPath, ...args], {
28763
28769
  ...spawnOptions,
28764
28770
  stdio: ["inherit", "pipe", "pipe"]
@@ -28773,13 +28779,13 @@ function runScriptCapture(scriptPath, args = [], options = {}) {
28773
28779
  stderr += d;
28774
28780
  if (stream) process.stderr.write(d);
28775
28781
  });
28776
- proc.on("close", (code) => resolve49({ stdout, stderr, code: code ?? 0 }));
28782
+ proc.on("close", (code) => resolve50({ stdout, stderr, code: code ?? 0 }));
28777
28783
  });
28778
28784
  }
28779
28785
  function runShellCommand(command2, options = {}) {
28780
28786
  const stream = options.stream ?? false;
28781
28787
  const { stream: _stream, ...spawnOptions } = options;
28782
- return new Promise((resolve49) => {
28788
+ return new Promise((resolve50) => {
28783
28789
  const proc = (0, import_child_process.spawn)("bash", ["-lc", command2], {
28784
28790
  ...spawnOptions,
28785
28791
  stdio: ["inherit", "pipe", "pipe"]
@@ -28794,7 +28800,7 @@ function runShellCommand(command2, options = {}) {
28794
28800
  stderr += d;
28795
28801
  if (stream) process.stderr.write(d);
28796
28802
  });
28797
- proc.on("close", (code) => resolve49({ stdout, stderr, code: code ?? 0 }));
28803
+ proc.on("close", (code) => resolve50({ stdout, stderr, code: code ?? 0 }));
28798
28804
  });
28799
28805
  }
28800
28806
  var import_child_process;
@@ -30904,6 +30910,29 @@ function listRecords(harnessRoot, dir, parse4) {
30904
30910
  }
30905
30911
  return records;
30906
30912
  }
30913
+ function parseWorkLinkSpec(spec) {
30914
+ const parts = spec.split("|");
30915
+ if (parts.length < 2) {
30916
+ throw new Error(
30917
+ `Invalid work link "${spec}" \u2014 use source|url or source|url|label (example: github|https://github.com/org/repo/pull/42|PR #42)`
30918
+ );
30919
+ }
30920
+ const [source, url, ...labelParts] = parts;
30921
+ const label = labelParts.length > 0 ? labelParts.join("|") : void 0;
30922
+ return WorkUnitRelatedLinkSchema.parse({ source, url, label });
30923
+ }
30924
+ function mergeRelatedLinks(existing, incoming, primaryUrl) {
30925
+ if (!incoming?.length) return existing?.length ? existing : void 0;
30926
+ const seen = /* @__PURE__ */ new Set();
30927
+ const merged = [];
30928
+ for (const link of [...existing ?? [], ...incoming]) {
30929
+ if (primaryUrl && link.url === primaryUrl) continue;
30930
+ if (seen.has(link.url)) continue;
30931
+ seen.add(link.url);
30932
+ merged.push(link);
30933
+ }
30934
+ return merged.length > 0 ? merged : void 0;
30935
+ }
30907
30936
  function findWorkUnit(harnessRoot, workUnitId) {
30908
30937
  const metadata = WorkUnitMetadataSchema.parse({ workUnitId });
30909
30938
  const file = hashedRecordPath(harnessRoot, WORK_UNITS_DIR, metadata.workUnitId);
@@ -30933,13 +30962,19 @@ function upsertWorkUnit(harnessRoot, input) {
30933
30962
  }
30934
30963
  }
30935
30964
  const now = (/* @__PURE__ */ new Date()).toISOString();
30965
+ const sourceUrl = existing?.sourceUrl ?? metadata.sourceUrl;
30936
30966
  const record2 = WorkUnitRecordSchema.parse({
30937
30967
  ...existing,
30938
30968
  workUnitId: metadata.workUnitId,
30939
30969
  source: existing?.source ?? metadata.source,
30940
- sourceUrl: existing?.sourceUrl ?? metadata.sourceUrl,
30970
+ sourceUrl,
30941
30971
  title: existing?.title ?? metadata.title,
30942
30972
  parentWorkUnitId: existing?.parentWorkUnitId ?? metadata.parentWorkUnitId,
30973
+ relatedLinks: mergeRelatedLinks(
30974
+ existing?.relatedLinks,
30975
+ metadata.relatedLinks,
30976
+ sourceUrl
30977
+ ),
30943
30978
  version: 1,
30944
30979
  createdAt: existing?.createdAt ?? now,
30945
30980
  updatedAt: now,
@@ -30952,6 +30987,23 @@ function upsertWorkUnit(harnessRoot, input) {
30952
30987
  markDirty(harnessRoot);
30953
30988
  return record2;
30954
30989
  }
30990
+ function addWorkUnitLinks(harnessRoot, workUnitId, links) {
30991
+ const existing = findWorkUnit(harnessRoot, workUnitId);
30992
+ if (!existing) throw new Error(`Unknown work unit: ${workUnitId}`);
30993
+ const parsed = links.map((link) => WorkUnitRelatedLinkSchema.parse(link));
30994
+ const relatedLinks = mergeRelatedLinks(existing.relatedLinks, parsed, existing.sourceUrl);
30995
+ const record2 = WorkUnitRecordSchema.parse({
30996
+ ...existing,
30997
+ relatedLinks,
30998
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
30999
+ });
31000
+ writeRecord2(
31001
+ hashedRecordPath(harnessRoot, WORK_UNITS_DIR, workUnitId),
31002
+ record2
31003
+ );
31004
+ markDirty(harnessRoot);
31005
+ return record2;
31006
+ }
30955
31007
  function listWorkUnits(harnessRoot) {
30956
31008
  return listRecords(
30957
31009
  harnessRoot,
@@ -32431,7 +32483,7 @@ async function postForResponse(url, headers, payload) {
32431
32483
  } catch (err) {
32432
32484
  lastErr = err;
32433
32485
  if (attempt < MAX_FETCH_ATTEMPTS) {
32434
- await new Promise((resolve49) => setTimeout(resolve49, attempt * 100));
32486
+ await new Promise((resolve50) => setTimeout(resolve50, attempt * 100));
32435
32487
  }
32436
32488
  }
32437
32489
  }
@@ -32631,7 +32683,7 @@ async function waitForControlApi(apiUrl = getControlApiUrl(), timeoutMs = 6e4, i
32631
32683
  const deadline = Date.now() + timeoutMs;
32632
32684
  while (Date.now() < deadline) {
32633
32685
  if (await isControlApiReachable(apiUrl)) return true;
32634
- await new Promise((resolve49) => setTimeout(resolve49, intervalMs));
32686
+ await new Promise((resolve50) => setTimeout(resolve50, intervalMs));
32635
32687
  }
32636
32688
  return false;
32637
32689
  }
@@ -34128,15 +34180,15 @@ var require_route = __commonJS({
34128
34180
  };
34129
34181
  }
34130
34182
  function wrapConversion(toModel, graph) {
34131
- const path65 = [graph[toModel].parent, toModel];
34183
+ const path66 = [graph[toModel].parent, toModel];
34132
34184
  let fn = conversions[graph[toModel].parent][toModel];
34133
34185
  let cur = graph[toModel].parent;
34134
34186
  while (graph[cur].parent) {
34135
- path65.unshift(graph[cur].parent);
34187
+ path66.unshift(graph[cur].parent);
34136
34188
  fn = link(conversions[graph[cur].parent][cur], fn);
34137
34189
  cur = graph[cur].parent;
34138
34190
  }
34139
- fn.conversion = path65;
34191
+ fn.conversion = path66;
34140
34192
  return fn;
34141
34193
  }
34142
34194
  module2.exports = function(fromModel) {
@@ -40066,7 +40118,7 @@ var require_iso2022 = __commonJS({
40066
40118
  // node_modules/chardet/index.js
40067
40119
  var require_chardet = __commonJS({
40068
40120
  "node_modules/chardet/index.js"(exports2, module2) {
40069
- var fs51 = require("fs");
40121
+ var fs52 = require("fs");
40070
40122
  var utf8 = require_utf8();
40071
40123
  var unicode = require_unicode();
40072
40124
  var mbcs = require_mbcs();
@@ -40140,28 +40192,28 @@ var require_chardet = __commonJS({
40140
40192
  var fd;
40141
40193
  var handler = function(err, buffer) {
40142
40194
  if (fd) {
40143
- fs51.closeSync(fd);
40195
+ fs52.closeSync(fd);
40144
40196
  }
40145
40197
  if (err) return cb(err, null);
40146
40198
  cb(null, self.detect(buffer, opts));
40147
40199
  };
40148
40200
  if (opts && opts.sampleSize) {
40149
- fd = fs51.openSync(filepath, "r"), sample = Buffer.allocUnsafe(opts.sampleSize);
40150
- fs51.read(fd, sample, 0, opts.sampleSize, null, function(err) {
40201
+ fd = fs52.openSync(filepath, "r"), sample = Buffer.allocUnsafe(opts.sampleSize);
40202
+ fs52.read(fd, sample, 0, opts.sampleSize, null, function(err) {
40151
40203
  handler(err, sample);
40152
40204
  });
40153
40205
  return;
40154
40206
  }
40155
- fs51.readFile(filepath, handler);
40207
+ fs52.readFile(filepath, handler);
40156
40208
  };
40157
40209
  module2.exports.detectFileSync = function(filepath, opts) {
40158
40210
  if (opts && opts.sampleSize) {
40159
- var fd = fs51.openSync(filepath, "r"), sample2 = Buffer.allocUnsafe(opts.sampleSize);
40160
- fs51.readSync(fd, sample2, 0, opts.sampleSize);
40161
- fs51.closeSync(fd);
40211
+ var fd = fs52.openSync(filepath, "r"), sample2 = Buffer.allocUnsafe(opts.sampleSize);
40212
+ fs52.readSync(fd, sample2, 0, opts.sampleSize);
40213
+ fs52.closeSync(fd);
40162
40214
  return self.detect(sample2, opts);
40163
40215
  }
40164
- return self.detect(fs51.readFileSync(filepath), opts);
40216
+ return self.detect(fs52.readFileSync(filepath), opts);
40165
40217
  };
40166
40218
  module2.exports.detectAll = function(buffer, opts) {
40167
40219
  if (typeof opts !== "object") {
@@ -43626,16 +43678,16 @@ var require_os_tmpdir = __commonJS({
43626
43678
  var isWindows = process.platform === "win32";
43627
43679
  var trailingSlashRe = isWindows ? /[^:]\\$/ : /.\/$/;
43628
43680
  module2.exports = function() {
43629
- var path65;
43681
+ var path66;
43630
43682
  if (isWindows) {
43631
- path65 = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
43683
+ path66 = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
43632
43684
  } else {
43633
- path65 = process.env.TMPDIR || process.env.TMP || process.env.TEMP || "/tmp";
43685
+ path66 = process.env.TMPDIR || process.env.TMP || process.env.TEMP || "/tmp";
43634
43686
  }
43635
- if (trailingSlashRe.test(path65)) {
43636
- path65 = path65.slice(0, -1);
43687
+ if (trailingSlashRe.test(path66)) {
43688
+ path66 = path66.slice(0, -1);
43637
43689
  }
43638
- return path65;
43690
+ return path66;
43639
43691
  };
43640
43692
  }
43641
43693
  });
@@ -43643,8 +43695,8 @@ var require_os_tmpdir = __commonJS({
43643
43695
  // node_modules/tmp/lib/tmp.js
43644
43696
  var require_tmp = __commonJS({
43645
43697
  "node_modules/tmp/lib/tmp.js"(exports2, module2) {
43646
- var fs51 = require("fs");
43647
- var path65 = require("path");
43698
+ var fs52 = require("fs");
43699
+ var path66 = require("path");
43648
43700
  var crypto6 = require("crypto");
43649
43701
  var osTmpDir = require_os_tmpdir();
43650
43702
  var _c2 = process.binding("constants");
@@ -43686,7 +43738,7 @@ var require_tmp = __commonJS({
43686
43738
  }
43687
43739
  function _generateTmpName(opts) {
43688
43740
  if (opts.name) {
43689
- return path65.join(opts.dir || tmpDir, opts.name);
43741
+ return path66.join(opts.dir || tmpDir, opts.name);
43690
43742
  }
43691
43743
  if (opts.template) {
43692
43744
  return opts.template.replace(TEMPLATE_PATTERN, _randomChars(6));
@@ -43697,7 +43749,7 @@ var require_tmp = __commonJS({
43697
43749
  _randomChars(12),
43698
43750
  opts.postfix || ""
43699
43751
  ].join("");
43700
- return path65.join(opts.dir || tmpDir, name);
43752
+ return path66.join(opts.dir || tmpDir, name);
43701
43753
  }
43702
43754
  function tmpName(options, callback) {
43703
43755
  var args = _parseArguments(options, callback), opts = args[0], cb = args[1], tries = opts.name ? 1 : opts.tries || DEFAULT_TRIES;
@@ -43707,7 +43759,7 @@ var require_tmp = __commonJS({
43707
43759
  return cb(new Error("Invalid template provided"));
43708
43760
  (function _getUniqueName() {
43709
43761
  const name = _generateTmpName(opts);
43710
- fs51.stat(name, function(err) {
43762
+ fs52.stat(name, function(err) {
43711
43763
  if (!err) {
43712
43764
  if (tries-- > 0) return _getUniqueName();
43713
43765
  return cb(new Error("Could not get a unique tmp filename, max tries reached " + name));
@@ -43725,7 +43777,7 @@ var require_tmp = __commonJS({
43725
43777
  do {
43726
43778
  const name = _generateTmpName(opts);
43727
43779
  try {
43728
- fs51.statSync(name);
43780
+ fs52.statSync(name);
43729
43781
  } catch (e) {
43730
43782
  return name;
43731
43783
  }
@@ -43737,13 +43789,13 @@ var require_tmp = __commonJS({
43737
43789
  opts.postfix = _isUndefined(opts.postfix) ? ".tmp" : opts.postfix;
43738
43790
  tmpName(opts, function _tmpNameCreated(err, name) {
43739
43791
  if (err) return cb(err);
43740
- fs51.open(name, CREATE_FLAGS, opts.mode || FILE_MODE, function _fileCreated(err2, fd) {
43792
+ fs52.open(name, CREATE_FLAGS, opts.mode || FILE_MODE, function _fileCreated(err2, fd) {
43741
43793
  if (err2) return cb(err2);
43742
43794
  if (opts.discardDescriptor) {
43743
- return fs51.close(fd, function _discardCallback(err3) {
43795
+ return fs52.close(fd, function _discardCallback(err3) {
43744
43796
  if (err3) {
43745
43797
  try {
43746
- fs51.unlinkSync(name);
43798
+ fs52.unlinkSync(name);
43747
43799
  } catch (e) {
43748
43800
  if (!isENOENT(e)) {
43749
43801
  err3 = e;
@@ -43766,9 +43818,9 @@ var require_tmp = __commonJS({
43766
43818
  opts.postfix = opts.postfix || ".tmp";
43767
43819
  const discardOrDetachDescriptor = opts.discardDescriptor || opts.detachDescriptor;
43768
43820
  const name = tmpNameSync(opts);
43769
- var fd = fs51.openSync(name, CREATE_FLAGS, opts.mode || FILE_MODE);
43821
+ var fd = fs52.openSync(name, CREATE_FLAGS, opts.mode || FILE_MODE);
43770
43822
  if (opts.discardDescriptor) {
43771
- fs51.closeSync(fd);
43823
+ fs52.closeSync(fd);
43772
43824
  fd = void 0;
43773
43825
  }
43774
43826
  return {
@@ -43780,9 +43832,9 @@ var require_tmp = __commonJS({
43780
43832
  function _rmdirRecursiveSync(root) {
43781
43833
  const dirs = [root];
43782
43834
  do {
43783
- var dir2 = dirs.pop(), deferred = false, files = fs51.readdirSync(dir2);
43835
+ var dir2 = dirs.pop(), deferred = false, files = fs52.readdirSync(dir2);
43784
43836
  for (var i = 0, length = files.length; i < length; i++) {
43785
- var file2 = path65.join(dir2, files[i]), stat = fs51.lstatSync(file2);
43837
+ var file2 = path66.join(dir2, files[i]), stat = fs52.lstatSync(file2);
43786
43838
  if (stat.isDirectory()) {
43787
43839
  if (!deferred) {
43788
43840
  deferred = true;
@@ -43790,11 +43842,11 @@ var require_tmp = __commonJS({
43790
43842
  }
43791
43843
  dirs.push(file2);
43792
43844
  } else {
43793
- fs51.unlinkSync(file2);
43845
+ fs52.unlinkSync(file2);
43794
43846
  }
43795
43847
  }
43796
43848
  if (!deferred) {
43797
- fs51.rmdirSync(dir2);
43849
+ fs52.rmdirSync(dir2);
43798
43850
  }
43799
43851
  } while (dirs.length !== 0);
43800
43852
  }
@@ -43802,7 +43854,7 @@ var require_tmp = __commonJS({
43802
43854
  var args = _parseArguments(options, callback), opts = args[0], cb = args[1];
43803
43855
  tmpName(opts, function _tmpNameCreated(err, name) {
43804
43856
  if (err) return cb(err);
43805
- fs51.mkdir(name, opts.mode || DIR_MODE, function _dirCreated(err2) {
43857
+ fs52.mkdir(name, opts.mode || DIR_MODE, function _dirCreated(err2) {
43806
43858
  if (err2) return cb(err2);
43807
43859
  cb(null, name, _prepareTmpDirRemoveCallback(name, opts));
43808
43860
  });
@@ -43811,7 +43863,7 @@ var require_tmp = __commonJS({
43811
43863
  function dirSync(options) {
43812
43864
  var args = _parseArguments(options), opts = args[0];
43813
43865
  const name = tmpNameSync(opts);
43814
- fs51.mkdirSync(name, opts.mode || DIR_MODE);
43866
+ fs52.mkdirSync(name, opts.mode || DIR_MODE);
43815
43867
  return {
43816
43868
  name,
43817
43869
  removeCallback: _prepareTmpDirRemoveCallback(name, opts)
@@ -43821,7 +43873,7 @@ var require_tmp = __commonJS({
43821
43873
  const removeCallback = _prepareRemoveCallback(function _removeCallback(fdPath) {
43822
43874
  try {
43823
43875
  if (0 <= fdPath[0]) {
43824
- fs51.closeSync(fdPath[0]);
43876
+ fs52.closeSync(fdPath[0]);
43825
43877
  }
43826
43878
  } catch (e) {
43827
43879
  if (!isEBADF(e) && !isENOENT(e)) {
@@ -43829,7 +43881,7 @@ var require_tmp = __commonJS({
43829
43881
  }
43830
43882
  }
43831
43883
  try {
43832
- fs51.unlinkSync(fdPath[1]);
43884
+ fs52.unlinkSync(fdPath[1]);
43833
43885
  } catch (e) {
43834
43886
  if (!isENOENT(e)) {
43835
43887
  throw e;
@@ -43842,7 +43894,7 @@ var require_tmp = __commonJS({
43842
43894
  return removeCallback;
43843
43895
  }
43844
43896
  function _prepareTmpDirRemoveCallback(name, opts) {
43845
- const removeFunction = opts.unsafeCleanup ? _rmdirRecursiveSync : fs51.rmdirSync.bind(fs51);
43897
+ const removeFunction = opts.unsafeCleanup ? _rmdirRecursiveSync : fs52.rmdirSync.bind(fs52);
43846
43898
  const removeCallback = _prepareRemoveCallback(removeFunction, name);
43847
43899
  if (!opts.keep) {
43848
43900
  _removeObjects.unshift(removeCallback);
@@ -44993,7 +45045,7 @@ var require_Observable = __commonJS({
44993
45045
  Observable2.prototype.forEach = function(next, promiseCtor) {
44994
45046
  var _this = this;
44995
45047
  promiseCtor = getPromiseCtor(promiseCtor);
44996
- return new promiseCtor(function(resolve49, reject) {
45048
+ return new promiseCtor(function(resolve50, reject) {
44997
45049
  var subscriber = new Subscriber_1.SafeSubscriber({
44998
45050
  next: function(value) {
44999
45051
  try {
@@ -45004,7 +45056,7 @@ var require_Observable = __commonJS({
45004
45056
  }
45005
45057
  },
45006
45058
  error: reject,
45007
- complete: resolve49
45059
+ complete: resolve50
45008
45060
  });
45009
45061
  _this.subscribe(subscriber);
45010
45062
  });
@@ -45026,14 +45078,14 @@ var require_Observable = __commonJS({
45026
45078
  Observable2.prototype.toPromise = function(promiseCtor) {
45027
45079
  var _this = this;
45028
45080
  promiseCtor = getPromiseCtor(promiseCtor);
45029
- return new promiseCtor(function(resolve49, reject) {
45081
+ return new promiseCtor(function(resolve50, reject) {
45030
45082
  var value;
45031
45083
  _this.subscribe(function(x) {
45032
45084
  return value = x;
45033
45085
  }, function(err) {
45034
45086
  return reject(err);
45035
45087
  }, function() {
45036
- return resolve49(value);
45088
+ return resolve50(value);
45037
45089
  });
45038
45090
  });
45039
45091
  };
@@ -47129,11 +47181,11 @@ var require_innerFrom = __commonJS({
47129
47181
  "use strict";
47130
47182
  var __awaiter = exports2 && exports2.__awaiter || function(thisArg, _arguments, P2, generator) {
47131
47183
  function adopt(value) {
47132
- return value instanceof P2 ? value : new P2(function(resolve49) {
47133
- resolve49(value);
47184
+ return value instanceof P2 ? value : new P2(function(resolve50) {
47185
+ resolve50(value);
47134
47186
  });
47135
47187
  }
47136
- return new (P2 || (P2 = Promise))(function(resolve49, reject) {
47188
+ return new (P2 || (P2 = Promise))(function(resolve50, reject) {
47137
47189
  function fulfilled(value) {
47138
47190
  try {
47139
47191
  step(generator.next(value));
@@ -47149,7 +47201,7 @@ var require_innerFrom = __commonJS({
47149
47201
  }
47150
47202
  }
47151
47203
  function step(result) {
47152
- result.done ? resolve49(result.value) : adopt(result.value).then(fulfilled, rejected);
47204
+ result.done ? resolve50(result.value) : adopt(result.value).then(fulfilled, rejected);
47153
47205
  }
47154
47206
  step((generator = generator.apply(thisArg, _arguments || [])).next());
47155
47207
  });
@@ -47231,14 +47283,14 @@ var require_innerFrom = __commonJS({
47231
47283
  }, i);
47232
47284
  function verb(n) {
47233
47285
  i[n] = o[n] && function(v2) {
47234
- return new Promise(function(resolve49, reject) {
47235
- v2 = o[n](v2), settle(resolve49, reject, v2.done, v2.value);
47286
+ return new Promise(function(resolve50, reject) {
47287
+ v2 = o[n](v2), settle(resolve50, reject, v2.done, v2.value);
47236
47288
  });
47237
47289
  };
47238
47290
  }
47239
- function settle(resolve49, reject, d, v2) {
47291
+ function settle(resolve50, reject, d, v2) {
47240
47292
  Promise.resolve(v2).then(function(v3) {
47241
- resolve49({ value: v3, done: d });
47293
+ resolve50({ value: v3, done: d });
47242
47294
  }, reject);
47243
47295
  }
47244
47296
  };
@@ -47857,7 +47909,7 @@ var require_lastValueFrom = __commonJS({
47857
47909
  var EmptyError_1 = require_EmptyError();
47858
47910
  function lastValueFrom2(source, config2) {
47859
47911
  var hasConfig = typeof config2 === "object";
47860
- return new Promise(function(resolve49, reject) {
47912
+ return new Promise(function(resolve50, reject) {
47861
47913
  var _hasValue = false;
47862
47914
  var _value;
47863
47915
  source.subscribe({
@@ -47868,9 +47920,9 @@ var require_lastValueFrom = __commonJS({
47868
47920
  error: reject,
47869
47921
  complete: function() {
47870
47922
  if (_hasValue) {
47871
- resolve49(_value);
47923
+ resolve50(_value);
47872
47924
  } else if (hasConfig) {
47873
- resolve49(config2.defaultValue);
47925
+ resolve50(config2.defaultValue);
47874
47926
  } else {
47875
47927
  reject(new EmptyError_1.EmptyError());
47876
47928
  }
@@ -47892,16 +47944,16 @@ var require_firstValueFrom = __commonJS({
47892
47944
  var Subscriber_1 = require_Subscriber();
47893
47945
  function firstValueFrom(source, config2) {
47894
47946
  var hasConfig = typeof config2 === "object";
47895
- return new Promise(function(resolve49, reject) {
47947
+ return new Promise(function(resolve50, reject) {
47896
47948
  var subscriber = new Subscriber_1.SafeSubscriber({
47897
47949
  next: function(value) {
47898
- resolve49(value);
47950
+ resolve50(value);
47899
47951
  subscriber.unsubscribe();
47900
47952
  },
47901
47953
  error: reject,
47902
47954
  complete: function() {
47903
47955
  if (hasConfig) {
47904
- resolve49(config2.defaultValue);
47956
+ resolve50(config2.defaultValue);
47905
47957
  } else {
47906
47958
  reject(new EmptyError_1.EmptyError());
47907
47959
  }
@@ -53694,14 +53746,14 @@ var require_run_async = __commonJS({
53694
53746
  return function() {
53695
53747
  var args = arguments;
53696
53748
  var originalThis = this;
53697
- var promise = new Promise(function(resolve49, reject) {
53749
+ var promise = new Promise(function(resolve50, reject) {
53698
53750
  var resolved = false;
53699
53751
  const wrappedResolve = function(value) {
53700
53752
  if (resolved) {
53701
53753
  console.warn("Run-async promise already resolved.");
53702
53754
  }
53703
53755
  resolved = true;
53704
- resolve49(value);
53756
+ resolve50(value);
53705
53757
  };
53706
53758
  var rejected = false;
53707
53759
  const wrappedReject = function(value) {
@@ -57213,7 +57265,7 @@ var require_compile = __commonJS({
57213
57265
  const schOrFunc = root.refs[ref];
57214
57266
  if (schOrFunc)
57215
57267
  return schOrFunc;
57216
- let _sch = resolve49.call(this, root, ref);
57268
+ let _sch = resolve50.call(this, root, ref);
57217
57269
  if (_sch === void 0) {
57218
57270
  const schema = (_a2 = root.localRefs) === null || _a2 === void 0 ? void 0 : _a2[ref];
57219
57271
  const { schemaId } = this.opts;
@@ -57240,7 +57292,7 @@ var require_compile = __commonJS({
57240
57292
  function sameSchemaEnv(s1, s2) {
57241
57293
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
57242
57294
  }
57243
- function resolve49(root, ref) {
57295
+ function resolve50(root, ref) {
57244
57296
  let sch;
57245
57297
  while (typeof (sch = this.refs[ref]) == "string")
57246
57298
  ref = sch;
@@ -57458,8 +57510,8 @@ var require_utils = __commonJS({
57458
57510
  }
57459
57511
  return ind;
57460
57512
  }
57461
- function removeDotSegments(path65) {
57462
- let input = path65;
57513
+ function removeDotSegments(path66) {
57514
+ let input = path66;
57463
57515
  const output = [];
57464
57516
  let nextSlash = -1;
57465
57517
  let len = 0;
@@ -57711,8 +57763,8 @@ var require_schemes = __commonJS({
57711
57763
  wsComponent.secure = void 0;
57712
57764
  }
57713
57765
  if (wsComponent.resourceName) {
57714
- const [path65, query] = wsComponent.resourceName.split("?");
57715
- wsComponent.path = path65 && path65 !== "/" ? path65 : void 0;
57766
+ const [path66, query] = wsComponent.resourceName.split("?");
57767
+ wsComponent.path = path66 && path66 !== "/" ? path66 : void 0;
57716
57768
  wsComponent.query = query;
57717
57769
  wsComponent.resourceName = void 0;
57718
57770
  }
@@ -57871,7 +57923,7 @@ var require_fast_uri = __commonJS({
57871
57923
  }
57872
57924
  return uri;
57873
57925
  }
57874
- function resolve49(baseURI, relativeURI, options) {
57926
+ function resolve50(baseURI, relativeURI, options) {
57875
57927
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
57876
57928
  const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
57877
57929
  schemelessOptions.skipEscape = true;
@@ -58129,7 +58181,7 @@ var require_fast_uri = __commonJS({
58129
58181
  var fastUri = {
58130
58182
  SCHEMES,
58131
58183
  normalize: normalize3,
58132
- resolve: resolve49,
58184
+ resolve: resolve50,
58133
58185
  resolveComponent,
58134
58186
  equal,
58135
58187
  serialize,
@@ -61105,12 +61157,12 @@ var require_dist = __commonJS({
61105
61157
  throw new Error(`Unknown format "${name}"`);
61106
61158
  return f;
61107
61159
  };
61108
- function addFormats(ajv, list, fs51, exportName) {
61160
+ function addFormats(ajv, list, fs52, exportName) {
61109
61161
  var _a2;
61110
61162
  var _b2;
61111
61163
  (_a2 = (_b2 = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b2.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
61112
61164
  for (const f of list)
61113
- ajv.addFormat(f, fs51[f]);
61165
+ ajv.addFormat(f, fs52[f]);
61114
61166
  }
61115
61167
  module2.exports = exports2 = formatsPlugin;
61116
61168
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -62394,11 +62446,11 @@ var parser = new YargsParser({
62394
62446
  resolve: import_path2.resolve,
62395
62447
  // TODO: figure out a way to combine ESM and CJS coverage, such that
62396
62448
  // we can exercise all the lines below:
62397
- require: (path65) => {
62449
+ require: (path66) => {
62398
62450
  if (typeof require !== "undefined") {
62399
- return require(path65);
62400
- } else if (path65.match(/\.json$/)) {
62401
- return JSON.parse((0, import_fs2.readFileSync)(path65, "utf8"));
62451
+ return require(path66);
62452
+ } else if (path66.match(/\.json$/)) {
62453
+ return JSON.parse((0, import_fs2.readFileSync)(path66, "utf8"));
62402
62454
  } else {
62403
62455
  throw Error("only .json config files are supported in ESM");
62404
62456
  }
@@ -64924,12 +64976,12 @@ var YargsInstance = class {
64924
64976
  async getCompletion(args, done) {
64925
64977
  argsert("<array> [function]", [args, done], arguments.length);
64926
64978
  if (!done) {
64927
- return new Promise((resolve49, reject) => {
64979
+ return new Promise((resolve50, reject) => {
64928
64980
  __classPrivateFieldGet(this, _YargsInstance_completion, "f").getCompletion(args, (err, completions) => {
64929
64981
  if (err)
64930
64982
  reject(err);
64931
64983
  else
64932
- resolve49(completions);
64984
+ resolve50(completions);
64933
64985
  });
64934
64986
  });
64935
64987
  } else {
@@ -66218,16 +66270,16 @@ async function handleAgentSkills(options) {
66218
66270
  }
66219
66271
  async function askYesNo(question) {
66220
66272
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
66221
- return new Promise((resolve49) => {
66273
+ return new Promise((resolve50) => {
66222
66274
  process.stderr.write(`${question} `);
66223
66275
  rl.once("line", (answer) => {
66224
66276
  rl.close();
66225
66277
  const trimmed = answer.trim();
66226
66278
  if (trimmed === "") {
66227
- resolve49(true);
66279
+ resolve50(true);
66228
66280
  return;
66229
66281
  }
66230
- resolve49(/^y(es)?$/i.test(trimmed));
66282
+ resolve50(/^y(es)?$/i.test(trimmed));
66231
66283
  });
66232
66284
  });
66233
66285
  }
@@ -66307,7 +66359,7 @@ function handleRemove(argv) {
66307
66359
  }
66308
66360
 
66309
66361
  // src/cli/commands/env.ts
66310
- var path54 = __toESM(require("path"));
66362
+ var path55 = __toESM(require("path"));
66311
66363
 
66312
66364
  // src/cli/finish-command.ts
66313
66365
  init_sync_context();
@@ -67632,13 +67684,13 @@ var PromisePolyfill = class extends Promise {
67632
67684
  // Available starting from Node 22
67633
67685
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
67634
67686
  static withResolver() {
67635
- let resolve49;
67687
+ let resolve50;
67636
67688
  let reject;
67637
67689
  const promise = new Promise((res, rej) => {
67638
- resolve49 = res;
67690
+ resolve50 = res;
67639
67691
  reject = rej;
67640
67692
  });
67641
- return { promise, resolve: resolve49, reject };
67693
+ return { promise, resolve: resolve50, reject };
67642
67694
  }
67643
67695
  };
67644
67696
 
@@ -67655,7 +67707,7 @@ function createPrompt(view) {
67655
67707
  output
67656
67708
  });
67657
67709
  const screen = new ScreenManager(rl);
67658
- const { promise, resolve: resolve49, reject } = PromisePolyfill.withResolver();
67710
+ const { promise, resolve: resolve50, reject } = PromisePolyfill.withResolver();
67659
67711
  const cancel = () => reject(new CancelPromptError());
67660
67712
  if (signal) {
67661
67713
  const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
@@ -67679,7 +67731,7 @@ function createPrompt(view) {
67679
67731
  cycle(() => {
67680
67732
  try {
67681
67733
  const nextView = view(config2, (value) => {
67682
- setImmediate(() => resolve49(value));
67734
+ setImmediate(() => resolve50(value));
67683
67735
  });
67684
67736
  const [content, bottomContent] = typeof nextView === "string" ? [nextView] : nextView;
67685
67737
  screen.render(content, bottomContent);
@@ -68666,9 +68718,9 @@ var import_run_async = __toESM(require_run_async(), 1);
68666
68718
  var import_mute_stream2 = __toESM(require_lib(), 1);
68667
68719
  var import_ansi_escapes5 = __toESM(require_ansi_escapes(), 1);
68668
68720
  var _ = {
68669
- set: (obj, path65 = "", value) => {
68721
+ set: (obj, path66 = "", value) => {
68670
68722
  let pointer = obj;
68671
- path65.split(".").forEach((key, index, arr) => {
68723
+ path66.split(".").forEach((key, index, arr) => {
68672
68724
  if (key === "__proto__" || key === "constructor")
68673
68725
  return;
68674
68726
  if (index === arr.length - 1) {
@@ -68679,8 +68731,8 @@ var _ = {
68679
68731
  pointer = pointer[key];
68680
68732
  });
68681
68733
  },
68682
- get: (obj, path65 = "", defaultValue) => {
68683
- const travel = (regexp) => String.prototype.split.call(path65, regexp).filter(Boolean).reduce(
68734
+ get: (obj, path66 = "", defaultValue) => {
68735
+ const travel = (regexp) => String.prototype.split.call(path66, regexp).filter(Boolean).reduce(
68684
68736
  // @ts-expect-error implicit any on res[key]
68685
68737
  (res, key) => res !== null && res !== void 0 ? res[key] : res,
68686
68738
  obj
@@ -68794,7 +68846,7 @@ var PromptsRunner = class {
68794
68846
  throw new Error(`Prompt for type ${question.type} not found`);
68795
68847
  }
68796
68848
  let cleanupSignal;
68797
- const promptFn = isPromptConstructor(prompt2) ? (q, { signal: signal2 } = {}) => new Promise((resolve49, reject) => {
68849
+ const promptFn = isPromptConstructor(prompt2) ? (q, { signal: signal2 } = {}) => new Promise((resolve50, reject) => {
68798
68850
  const rl = import_node_readline.default.createInterface(setupReadlineOptions(this.opt));
68799
68851
  rl.resume();
68800
68852
  const onClose = () => {
@@ -68830,7 +68882,7 @@ var PromptsRunner = class {
68830
68882
  cleanupSignal = void 0;
68831
68883
  };
68832
68884
  }
68833
- activePrompt.run().then(resolve49, reject).finally(cleanup);
68885
+ activePrompt.run().then(resolve50, reject).finally(cleanup);
68834
68886
  }) : prompt2;
68835
68887
  const { signal: moduleSignal } = this.opt;
68836
68888
  if (moduleSignal?.aborted) {
@@ -71185,12 +71237,12 @@ function printAdaptationPrompt(content) {
71185
71237
  }
71186
71238
  function askYesNo2(question) {
71187
71239
  const rl = readline5.createInterface({ input: process.stdin, output: process.stderr });
71188
- return new Promise((resolve49) => {
71240
+ return new Promise((resolve50) => {
71189
71241
  process.stderr.write(`${question} `);
71190
71242
  rl.once("line", (answer) => {
71191
71243
  rl.close();
71192
71244
  const trimmed = answer.trim();
71193
- resolve49(trimmed === "" || /^y(es)?$/i.test(trimmed));
71245
+ resolve50(trimmed === "" || /^y(es)?$/i.test(trimmed));
71194
71246
  });
71195
71247
  });
71196
71248
  }
@@ -71279,16 +71331,16 @@ async function promptScaffoldCursorRule(exists) {
71279
71331
  }
71280
71332
  async function askYesNo3(question) {
71281
71333
  const rl = readline6.createInterface({ input: process.stdin, output: process.stderr });
71282
- return new Promise((resolve49) => {
71334
+ return new Promise((resolve50) => {
71283
71335
  process.stderr.write(`${question} `);
71284
71336
  rl.once("line", (answer) => {
71285
71337
  rl.close();
71286
71338
  const trimmed = answer.trim();
71287
71339
  if (trimmed === "") {
71288
- resolve49(true);
71340
+ resolve50(true);
71289
71341
  return;
71290
71342
  }
71291
- resolve49(/^y(es)?$/i.test(trimmed));
71343
+ resolve50(/^y(es)?$/i.test(trimmed));
71292
71344
  });
71293
71345
  });
71294
71346
  }
@@ -71728,7 +71780,8 @@ Usage from Cursor / Claude / Codex (@osfactory/otel-hook) will appear under Work
71728
71780
  source: options.source,
71729
71781
  sourceUrl: options.sourceUrl,
71730
71782
  title: options.title,
71731
- parentWorkUnitId: options.parentWorkUnitId
71783
+ parentWorkUnitId: options.parentWorkUnitId,
71784
+ relatedLinks: options.relatedLinks
71732
71785
  });
71733
71786
  createWorkAttempt(harnessRoot, {
71734
71787
  attemptId,
@@ -72005,21 +72058,319 @@ var getEnvironmentLogs = defaultRunService.getEnvironmentLogs.bind(defaultRunSer
72005
72058
 
72006
72059
  // src/cli/commands/env.ts
72007
72060
  init_runs();
72061
+ init_work_units();
72062
+ init_manifest();
72008
72063
  init_slot_status();
72009
72064
 
72065
+ // src/core/cleanup-service.ts
72066
+ var fs46 = __toESM(require("fs"));
72067
+ var os18 = __toESM(require("os"));
72068
+ var path53 = __toESM(require("path"));
72069
+ var import_child_process14 = require("child_process");
72070
+ init_control_sync();
72071
+ init_control_repo_path();
72072
+ init_slot_status();
72073
+ init_manifest();
72074
+ var DEFAULT_STALE_DAYS = 7;
72075
+ function getWorktreesRoot() {
72076
+ if (process.env.HAR_WORKTREES_ROOT) {
72077
+ return path53.resolve(process.env.HAR_WORKTREES_ROOT);
72078
+ }
72079
+ return path53.join(process.env.HOME || os18.homedir(), "worktrees");
72080
+ }
72081
+ function runGit3(cwd, args) {
72082
+ try {
72083
+ return (0, import_child_process14.execSync)(`git ${args}`, {
72084
+ cwd,
72085
+ encoding: "utf8",
72086
+ stdio: ["pipe", "pipe", "ignore"]
72087
+ }).trim();
72088
+ } catch {
72089
+ return void 0;
72090
+ }
72091
+ }
72092
+ function basenameRepo(repoPath) {
72093
+ return path53.basename(canonicalizeControlRepoPath(repoPath));
72094
+ }
72095
+ function ageDaysFrom(iso) {
72096
+ if (!iso) return void 0;
72097
+ const created = Date.parse(iso);
72098
+ if (Number.isNaN(created)) return void 0;
72099
+ return Math.floor((Date.now() - created) / (24 * 60 * 60 * 1e3));
72100
+ }
72101
+ function matchesKeep(candidate, keep) {
72102
+ if (!keep?.length) return false;
72103
+ for (const pin of keep) {
72104
+ if (pin.worktreePath && candidate.worktreePath) {
72105
+ if (path53.resolve(pin.worktreePath) === path53.resolve(candidate.worktreePath)) return true;
72106
+ }
72107
+ if (pin.repoPath && pin.agentId !== void 0 && candidate.agentId !== void 0) {
72108
+ const repo = canonicalizeControlRepoPath(pin.repoPath);
72109
+ const candidateRepo = path53.resolve(candidate.repoPath);
72110
+ if ((repo === candidateRepo || path53.resolve(pin.repoPath) === candidateRepo) && pin.agentId === candidate.agentId) {
72111
+ return true;
72112
+ }
72113
+ }
72114
+ if (pin.repoPath && pin.agentId === void 0 && !pin.worktreePath) {
72115
+ if (canonicalizeControlRepoPath(pin.repoPath) === candidate.repoPath) return true;
72116
+ }
72117
+ }
72118
+ return false;
72119
+ }
72120
+ function classifySlotCandidate(repoPath, projectName, slot, options) {
72121
+ if (!slot.active && !slot.worktreePath) return void 0;
72122
+ const candidate = {
72123
+ kind: "active_slot",
72124
+ repoPath,
72125
+ projectName,
72126
+ agentId: slot.agentId,
72127
+ worktreePath: slot.worktreePath ?? slot.workDir,
72128
+ branch: slot.branch,
72129
+ active: slot.active,
72130
+ dirty: slot.dirty,
72131
+ ageDays: ageDaysFrom(slot.sessionCreatedAt),
72132
+ sessionCreatedAt: slot.sessionCreatedAt,
72133
+ recommendation: "review",
72134
+ reason: "active session"
72135
+ };
72136
+ if (matchesKeep(candidate, options?.keep)) {
72137
+ candidate.recommendation = "keep";
72138
+ candidate.reason = "pinned with --keep";
72139
+ return candidate;
72140
+ }
72141
+ if (slot.dirty) {
72142
+ candidate.recommendation = "review";
72143
+ candidate.reason = "uncommitted changes \u2014 review before teardown";
72144
+ return candidate;
72145
+ }
72146
+ if (slot.sessionStatus === "failed" || slot.sessionStatus === "starting") {
72147
+ candidate.recommendation = "review";
72148
+ candidate.reason = "failed or resumable session \u2014 recover or teardown manually";
72149
+ return candidate;
72150
+ }
72151
+ const staleDays = options?.staleDays ?? DEFAULT_STALE_DAYS;
72152
+ const age = candidate.ageDays;
72153
+ if (age !== void 0 && age >= staleDays) {
72154
+ candidate.recommendation = "teardown";
72155
+ candidate.reason = `idle ${age}d, clean`;
72156
+ return candidate;
72157
+ }
72158
+ if (slot.active) {
72159
+ candidate.recommendation = "review";
72160
+ candidate.reason = age !== void 0 ? `active session (${age}d) \u2014 keep unless finished` : "active session \u2014 keep unless finished";
72161
+ }
72162
+ return candidate;
72163
+ }
72164
+ function resolveRepoForWorktree(worktreePath) {
72165
+ if (!fs46.existsSync(worktreePath)) return void 0;
72166
+ const commonDir = runGit3(worktreePath, "rev-parse --git-common-dir");
72167
+ if (!commonDir) return void 0;
72168
+ const gitRoot = path53.resolve(worktreePath, commonDir, "..");
72169
+ if (!readManifest(gitRoot)) return void 0;
72170
+ return canonicalizeControlRepoPath(gitRoot);
72171
+ }
72172
+ function discoverOrphanWorktrees(knownPaths) {
72173
+ const worktreesRoot = getWorktreesRoot();
72174
+ if (!fs46.existsSync(worktreesRoot)) return [];
72175
+ const orphans = [];
72176
+ for (const entry of fs46.readdirSync(worktreesRoot, { withFileTypes: true })) {
72177
+ if (!entry.isDirectory() || !entry.name.includes("-har-agent-")) continue;
72178
+ const worktreePath = path53.resolve(path53.join(worktreesRoot, entry.name));
72179
+ if (knownPaths.has(worktreePath)) continue;
72180
+ const repoPath = resolveRepoForWorktree(worktreePath) ?? worktreesRoot;
72181
+ const porcelain = runGit3(worktreePath, "status --porcelain");
72182
+ const dirty = porcelain !== void 0 ? porcelain.length > 0 : void 0;
72183
+ const branch = runGit3(worktreePath, "rev-parse --abbrev-ref HEAD");
72184
+ orphans.push({
72185
+ kind: "orphan_worktree",
72186
+ repoPath,
72187
+ projectName: basenameRepo(repoPath),
72188
+ worktreePath,
72189
+ branch: branch && branch !== "HEAD" ? branch : void 0,
72190
+ active: false,
72191
+ dirty,
72192
+ recommendation: dirty ? "review" : "remove_orphan",
72193
+ reason: dirty ? "orphan worktree with uncommitted changes" : "orphan worktree \u2014 no slot registry"
72194
+ });
72195
+ }
72196
+ return orphans.sort((a, b2) => (a.worktreePath ?? "").localeCompare(b2.worktreePath ?? ""));
72197
+ }
72198
+ async function discoverCleanupCandidates(options = {}) {
72199
+ const repoPaths = options.repoPaths ?? await discoverHarRepos({ cwd: options.cwd ?? process.cwd() });
72200
+ const candidates = [];
72201
+ const knownWorktreePaths = /* @__PURE__ */ new Set();
72202
+ for (const repoPath of repoPaths) {
72203
+ if (!fs46.existsSync(repoPath) || !readManifest(repoPath)) continue;
72204
+ const harnessRoot = resolveHarnessRoot(repoPath);
72205
+ const canonicalRepo = canonicalizeControlRepoPath(harnessRoot);
72206
+ const status = collectEnvironmentStatus(harnessRoot);
72207
+ const projectName = basenameRepo(harnessRoot);
72208
+ for (const slot of status.slots) {
72209
+ if (slot.worktreePath) knownWorktreePaths.add(path53.resolve(slot.worktreePath));
72210
+ if (slot.workDir) knownWorktreePaths.add(path53.resolve(slot.workDir));
72211
+ const candidate = classifySlotCandidate(canonicalRepo, projectName, slot, {
72212
+ staleDays: options.staleDays,
72213
+ keep: options.keep
72214
+ });
72215
+ if (!candidate) continue;
72216
+ if (candidate.recommendation === "review" && !options.includeReview) {
72217
+ }
72218
+ candidates.push(candidate);
72219
+ }
72220
+ }
72221
+ if (options.orphans !== false) {
72222
+ for (const orphan of discoverOrphanWorktrees(knownWorktreePaths)) {
72223
+ if (matchesKeep(orphan, options.keep)) {
72224
+ orphan.recommendation = "keep";
72225
+ orphan.reason = "pinned with --keep";
72226
+ }
72227
+ candidates.push(orphan);
72228
+ }
72229
+ }
72230
+ return {
72231
+ candidates: candidates.sort((a, b2) => {
72232
+ const repo = a.repoPath.localeCompare(b2.repoPath);
72233
+ if (repo !== 0) return repo;
72234
+ return (a.agentId ?? 0) - (b2.agentId ?? 0);
72235
+ }),
72236
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
72237
+ };
72238
+ }
72239
+ function selectAutoApprovedCandidates(plan, options) {
72240
+ return plan.candidates.filter((candidate) => {
72241
+ if (candidate.recommendation === "keep") return false;
72242
+ if (candidate.recommendation === "review") return options?.includeReview === true;
72243
+ return candidate.recommendation === "teardown" || candidate.recommendation === "remove_orphan";
72244
+ });
72245
+ }
72246
+ function formatCleanupPlan(plan) {
72247
+ if (plan.candidates.length === 0) {
72248
+ return "No session worktrees or orphans found.";
72249
+ }
72250
+ const lines2 = [];
72251
+ lines2.push(
72252
+ `${"REC".padEnd(8)} ${"REPO".padEnd(18)} ${"SLOT".padEnd(6)} ${"AGE".padEnd(5)} WORKTREE / REASON`
72253
+ );
72254
+ lines2.push("-".repeat(100));
72255
+ for (const candidate of plan.candidates) {
72256
+ const slot = candidate.agentId !== void 0 ? String(candidate.agentId) : "-";
72257
+ const age = candidate.ageDays !== void 0 ? `${candidate.ageDays}d` : "-";
72258
+ const target = candidate.worktreePath ?? candidate.repoPath;
72259
+ lines2.push(
72260
+ `${candidate.recommendation.padEnd(8)} ${candidate.projectName.padEnd(18)} ${slot.padEnd(6)} ${age.padEnd(5)} ${target}`
72261
+ );
72262
+ lines2.push(` ${candidate.reason}`);
72263
+ }
72264
+ return lines2.join("\n");
72265
+ }
72266
+ async function askYesNo4(question) {
72267
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
72268
+ const readline8 = await import("readline");
72269
+ const rl = readline8.createInterface({ input: process.stdin, output: process.stderr });
72270
+ const answer = await new Promise((resolve50) => {
72271
+ rl.question(question, resolve50);
72272
+ });
72273
+ rl.close();
72274
+ return /^[Yy]$/.test(answer.trim());
72275
+ }
72276
+ async function confirmCleanupSelection(candidates) {
72277
+ if (candidates.length === 0) return [];
72278
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
72279
+ throw new Error("Non-interactive terminal \u2014 pass --yes (and optionally --include-review)");
72280
+ }
72281
+ const approved = [];
72282
+ for (const candidate of candidates) {
72283
+ if (candidate.recommendation === "keep") continue;
72284
+ const label = candidate.kind === "orphan_worktree" ? `orphan ${candidate.worktreePath}` : `${candidate.projectName} agent ${candidate.agentId}`;
72285
+ const action = candidate.recommendation === "remove_orphan" ? "remove" : "teardown";
72286
+ const ok = await askYesNo4(`${action} ${label}? [${candidate.recommendation === "teardown" || candidate.recommendation === "remove_orphan" ? "Y" : "y"}/n] `);
72287
+ if (ok) approved.push(candidate);
72288
+ }
72289
+ return approved;
72290
+ }
72291
+ async function executeCleanupCandidates(candidates, options = {}) {
72292
+ const service = createRunService();
72293
+ const outcomes = [];
72294
+ for (const candidate of candidates) {
72295
+ if (candidate.recommendation === "keep") continue;
72296
+ if (options.dryRun) {
72297
+ outcomes.push({ candidate, ok: true });
72298
+ continue;
72299
+ }
72300
+ try {
72301
+ if (candidate.kind === "active_slot" && candidate.agentId !== void 0) {
72302
+ const result = await service.teardownEnvironment({
72303
+ repoPath: candidate.repoPath,
72304
+ agentId: candidate.agentId,
72305
+ capture: false,
72306
+ trigger: "cli"
72307
+ });
72308
+ outcomes.push({
72309
+ candidate,
72310
+ ok: result.code === 0,
72311
+ error: result.code === 0 ? void 0 : result.stderr?.trim() || `exit ${result.code}`
72312
+ });
72313
+ continue;
72314
+ }
72315
+ if (candidate.kind === "orphan_worktree" && candidate.worktreePath) {
72316
+ if (fs46.existsSync(candidate.worktreePath)) {
72317
+ const repoRoot = candidate.repoPath !== getWorktreesRoot() ? candidate.repoPath : resolveRepoForWorktree(candidate.worktreePath);
72318
+ if (repoRoot) {
72319
+ try {
72320
+ (0, import_child_process14.execSync)(`git worktree remove --force ${JSON.stringify(candidate.worktreePath)}`, {
72321
+ cwd: repoRoot,
72322
+ stdio: ["pipe", "pipe", "ignore"]
72323
+ });
72324
+ } catch {
72325
+ }
72326
+ }
72327
+ if (fs46.existsSync(candidate.worktreePath)) {
72328
+ fs46.rmSync(candidate.worktreePath, { recursive: true, force: true });
72329
+ }
72330
+ }
72331
+ outcomes.push({ candidate, ok: true });
72332
+ continue;
72333
+ }
72334
+ outcomes.push({ candidate, ok: false, error: "unsupported candidate" });
72335
+ } catch (err) {
72336
+ outcomes.push({
72337
+ candidate,
72338
+ ok: false,
72339
+ error: err instanceof Error ? err.message : String(err)
72340
+ });
72341
+ }
72342
+ }
72343
+ return outcomes;
72344
+ }
72345
+ function parseCleanupKeepPins(values) {
72346
+ if (!values?.length) return [];
72347
+ return values.map((raw) => {
72348
+ const agentMatch = raw.match(/^(.+):(\d+)$/);
72349
+ if (agentMatch) {
72350
+ return { repoPath: agentMatch[1], agentId: Number(agentMatch[2]) };
72351
+ }
72352
+ if (raw.startsWith("/") || raw.startsWith("~")) {
72353
+ return {
72354
+ worktreePath: raw.startsWith("~") ? path53.join(process.env.HOME || os18.homedir(), raw.slice(2)) : path53.resolve(raw)
72355
+ };
72356
+ }
72357
+ return { repoPath: raw };
72358
+ });
72359
+ }
72360
+
72010
72361
  // src/core/commit-gate-onboarding.ts
72011
72362
  var readline7 = __toESM(require("readline"));
72012
72363
  init_hooks();
72013
72364
  init_stages();
72014
72365
  init_logging();
72015
- function askYesNo4(question) {
72366
+ function askYesNo5(question) {
72016
72367
  const rl = readline7.createInterface({ input: process.stdin, output: process.stderr });
72017
- return new Promise((resolve49) => {
72368
+ return new Promise((resolve50) => {
72018
72369
  process.stderr.write(`${question} `);
72019
72370
  rl.once("line", (answer) => {
72020
72371
  rl.close();
72021
72372
  const trimmed = answer.trim();
72022
- resolve49(trimmed === "" || /^y(es)?$/i.test(trimmed));
72373
+ resolve50(trimmed === "" || /^y(es)?$/i.test(trimmed));
72023
72374
  });
72024
72375
  });
72025
72376
  }
@@ -72044,7 +72395,7 @@ async function handleCommitGateOnboarding(options) {
72044
72395
  const status = getHooksStatus(options.repoPath);
72045
72396
  let shouldInstall = status.preCommitInstalled || status.postCommitInstalled || options.install === "always" || options.autoYes === true;
72046
72397
  if (!shouldInstall && options.install === "prompt" && process.stdin.isTTY && process.stdout.isTTY) {
72047
- shouldInstall = await askYesNo4("Install the HAR commit gate for this repository? [Y/n]");
72398
+ shouldInstall = await askYesNo5("Install the HAR commit gate for this repository? [Y/n]");
72048
72399
  }
72049
72400
  if (!shouldInstall) {
72050
72401
  info("Commit gate is not installed; run `har hooks install` when ready.");
@@ -72061,21 +72412,21 @@ async function handleCommitGateOnboarding(options) {
72061
72412
  }
72062
72413
 
72063
72414
  // src/core/onboarding-preferences.ts
72064
- var fs46 = __toESM(require("fs"));
72065
- var os18 = __toESM(require("os"));
72066
- var path53 = __toESM(require("path"));
72415
+ var fs47 = __toESM(require("fs"));
72416
+ var os19 = __toESM(require("os"));
72417
+ var path54 = __toESM(require("path"));
72067
72418
  init_schema2();
72068
72419
  function getOnboardingPreferencesPath() {
72069
72420
  if (process.env.HAR_PREFERENCES_PATH) {
72070
- return path53.resolve(process.env.HAR_PREFERENCES_PATH);
72421
+ return path54.resolve(process.env.HAR_PREFERENCES_PATH);
72071
72422
  }
72072
- return path53.join(os18.homedir(), ".har", "preferences.json");
72423
+ return path54.join(os19.homedir(), ".har", "preferences.json");
72073
72424
  }
72074
72425
  function readOnboardingPreferences() {
72075
72426
  const preferencePath = getOnboardingPreferencesPath();
72076
72427
  try {
72077
- if (!fs46.existsSync(preferencePath)) return OnboardingPreferencesSchema.parse({});
72078
- return OnboardingPreferencesSchema.parse(JSON.parse(fs46.readFileSync(preferencePath, "utf8")));
72428
+ if (!fs47.existsSync(preferencePath)) return OnboardingPreferencesSchema.parse({});
72429
+ return OnboardingPreferencesSchema.parse(JSON.parse(fs47.readFileSync(preferencePath, "utf8")));
72079
72430
  } catch {
72080
72431
  return OnboardingPreferencesSchema.parse({});
72081
72432
  }
@@ -72100,8 +72451,8 @@ function writeOnboardingPreferences(overrides) {
72100
72451
  };
72101
72452
  const parsed = OnboardingPreferencesSchema.parse(preference);
72102
72453
  const preferencePath = getOnboardingPreferencesPath();
72103
- fs46.mkdirSync(path53.dirname(preferencePath), { recursive: true });
72104
- fs46.writeFileSync(preferencePath, `${JSON.stringify(parsed, null, 2)}
72454
+ fs47.mkdirSync(path54.dirname(preferencePath), { recursive: true });
72455
+ fs47.writeFileSync(preferencePath, `${JSON.stringify(parsed, null, 2)}
72105
72456
  `);
72106
72457
  return parsed;
72107
72458
  }
@@ -72165,6 +72516,14 @@ feature branch.
72165
72516
  Failed launch: har env recover 1 # or: har env launch 1 --resume`;
72166
72517
 
72167
72518
  // src/cli/commands/env.ts
72519
+ var workLinkOptions = (y) => y.option("repo", { type: "string", default: ".", describe: "Path to the repository" }).option("work-id", {
72520
+ type: "string",
72521
+ demandOption: true,
72522
+ describe: "Repo-scoped work unit id (same as --work-id at launch)"
72523
+ }).option("link", {
72524
+ type: "string",
72525
+ describe: "Link as source|url or source|url|label"
72526
+ }).option("source", { type: "string", describe: "Tracker provider (e.g. github, jira)" }).option("url", { type: "string", describe: "Canonical URL for the link" }).option("label", { type: "string", describe: "Optional display label" });
72168
72527
  var envCommand = {
72169
72528
  command: "env <subcommand>",
72170
72529
  describe: "Manage agent environments (launch \u2192 verify \u2192 complete)",
@@ -72324,8 +72683,17 @@ var envCommand = {
72324
72683
  }).option("parent-work-id", {
72325
72684
  type: "string",
72326
72685
  describe: "Optional parent work unit identifier"
72686
+ }).option("work-link", {
72687
+ type: "array",
72688
+ string: true,
72689
+ describe: "Additional tracker link: source|url or source|url|label (repeatable)"
72327
72690
  }).epilog(LAUNCH_EPILOG),
72328
72691
  handleLaunch
72692
+ ).command(
72693
+ "work-link",
72694
+ "Append a related external link to an existing work unit",
72695
+ workLinkOptions,
72696
+ handleWorkLink
72329
72697
  ).command(
72330
72698
  "recover <id>",
72331
72699
  "Resume a failed or partial launch (alias for launch --resume)",
@@ -72364,6 +72732,38 @@ var envCommand = {
72364
72732
  "Show status of all running agents",
72365
72733
  (y) => y.option("repo", { type: "string", default: "." }).option("json", { type: "boolean", default: false, describe: "Structured JSON output" }),
72366
72734
  handleStatus
72735
+ ).command(
72736
+ "cleanup",
72737
+ "Discover stale session worktrees across registered repos and tear them down",
72738
+ (y) => y.option("repo", {
72739
+ type: "string",
72740
+ describe: "Limit cleanup scan to one repository path"
72741
+ }).option("dry-run", {
72742
+ type: "boolean",
72743
+ default: false,
72744
+ describe: "Show the cleanup plan without executing"
72745
+ }).option("yes", {
72746
+ alias: "y",
72747
+ type: "boolean",
72748
+ default: false,
72749
+ describe: "Approve teardown/remove for recommended rows without prompting"
72750
+ }).option("keep", {
72751
+ type: "string",
72752
+ describe: "Pin sessions to keep (comma-separated repoPath:agentId or worktree paths)"
72753
+ }).option("stale", {
72754
+ type: "number",
72755
+ default: 7,
72756
+ describe: "Age in days before a clean idle session is recommended for teardown"
72757
+ }).option("orphans", {
72758
+ type: "boolean",
72759
+ default: true,
72760
+ describe: "Include orphan worktree directories under ~/worktrees"
72761
+ }).option("include-review", {
72762
+ type: "boolean",
72763
+ default: false,
72764
+ describe: "With --yes, also teardown/remove rows marked review (dirty or recent active)"
72765
+ }).option("json", { type: "boolean", default: false, describe: "Structured JSON output" }),
72766
+ handleCleanup
72367
72767
  ).command(
72368
72768
  "runs",
72369
72769
  "Inspect harness run history",
@@ -72386,13 +72786,13 @@ var envCommand = {
72386
72786
  }
72387
72787
  ).demandCommand(
72388
72788
  1,
72389
- "Please specify a subcommand: init, maintain, add-stage, launch, recover, verify, complete, teardown, status, runs"
72789
+ "Please specify a subcommand: init, maintain, add-stage, launch, recover, verify, complete, teardown, status, cleanup, runs"
72390
72790
  ).epilog(HAR_ENV_EPILOG),
72391
72791
  handler: () => {
72392
72792
  }
72393
72793
  };
72394
72794
  async function handleInit(argv) {
72395
- const repoPath = path54.resolve(argv.repo);
72795
+ const repoPath = path55.resolve(argv.repo);
72396
72796
  const onboarding = resolveOnboardingOptions(argv);
72397
72797
  header("har env init");
72398
72798
  info(`Repository: ${repoPath}`);
@@ -72438,7 +72838,7 @@ async function handleInit(argv) {
72438
72838
  }
72439
72839
  }
72440
72840
  async function handleMaintain(argv) {
72441
- const repoPath = path54.resolve(argv.repo);
72841
+ const repoPath = path55.resolve(argv.repo);
72442
72842
  const onboarding = resolveOnboardingOptions(argv);
72443
72843
  header("har env maintain");
72444
72844
  info(`Repository: ${repoPath}`);
@@ -72569,7 +72969,7 @@ async function applyAgentIntegrations(options) {
72569
72969
  }
72570
72970
  }
72571
72971
  async function handleAddPlugin(argv) {
72572
- const repoPath = path54.resolve(argv.repo);
72972
+ const repoPath = path55.resolve(argv.repo);
72573
72973
  const available = listPluginIds();
72574
72974
  if (argv.list) {
72575
72975
  for (const id of available) {
@@ -72609,7 +73009,7 @@ async function handleAddPlugin(argv) {
72609
73009
  }
72610
73010
  }
72611
73011
  async function handleAddStage(argv) {
72612
- const repoPath = path54.resolve(argv.repo);
73012
+ const repoPath = path55.resolve(argv.repo);
72613
73013
  const available = listPluginIds();
72614
73014
  if (argv.list) {
72615
73015
  warn("har env add-stage --list is deprecated; prefer: har env add-plugin --list");
@@ -72675,7 +73075,7 @@ async function handleAddStage(argv) {
72675
73075
  });
72676
73076
  }
72677
73077
  async function handlePreflight(argv) {
72678
- const repo = path54.resolve(argv.repo);
73078
+ const repo = path55.resolve(argv.repo);
72679
73079
  const agentId = validateAgentId(argv.id, repo);
72680
73080
  const result = await preflightEnvironment({
72681
73081
  repoPath: repo,
@@ -72691,7 +73091,7 @@ async function handlePreflight(argv) {
72691
73091
  return finishCommand(result.code);
72692
73092
  }
72693
73093
  async function handleLaunch(argv) {
72694
- const repo = path54.resolve(argv.repo);
73094
+ const repo = path55.resolve(argv.repo);
72695
73095
  const agentId = validateAgentId(argv.id, repo);
72696
73096
  if (!argv.resume) {
72697
73097
  const guard = checkLaunchGuard2(repo, agentId, { worktree: argv.worktree });
@@ -72714,6 +73114,7 @@ async function handleLaunch(argv) {
72714
73114
  sourceUrl: argv.workUrl,
72715
73115
  title: argv.workTitle,
72716
73116
  parentWorkUnitId: argv.parentWorkId,
73117
+ relatedLinks: argv.workLink?.map(parseWorkLinkSpec),
72717
73118
  capture: false
72718
73119
  });
72719
73120
  if (result.blocked) {
@@ -72734,6 +73135,22 @@ async function handleLaunch(argv) {
72734
73135
  }
72735
73136
  return finishCommand(result.code);
72736
73137
  }
73138
+ async function handleWorkLink(argv) {
73139
+ if (!argv.link && !(argv.source && argv.url)) {
73140
+ error("Provide --link or both --source and --url");
73141
+ return finishCommand(1);
73142
+ }
73143
+ const harnessRoot = resolveHarnessRoot(path55.resolve(argv.repo));
73144
+ const links = argv.link ? [parseWorkLinkSpec(argv.link)] : [{ source: argv.source, url: argv.url, label: argv.label }];
73145
+ try {
73146
+ const record2 = addWorkUnitLinks(harnessRoot, argv.workId, links);
73147
+ success(`Added link to work unit ${record2.workUnitId}`);
73148
+ return finishCommand(0);
73149
+ } catch (err) {
73150
+ error(err instanceof Error ? err.message : String(err));
73151
+ return finishCommand(1);
73152
+ }
73153
+ }
72737
73154
  async function handleRecover(argv) {
72738
73155
  return handleLaunch({
72739
73156
  id: argv.id,
@@ -72749,7 +73166,7 @@ async function handleRecover(argv) {
72749
73166
  });
72750
73167
  }
72751
73168
  async function handleVerify(argv) {
72752
- const repo = path54.resolve(argv.repo);
73169
+ const repo = path55.resolve(argv.repo);
72753
73170
  const agentId = validateAgentId(argv.id, repo);
72754
73171
  const result = await runVerification({
72755
73172
  repoPath: repo,
@@ -72761,7 +73178,7 @@ async function handleVerify(argv) {
72761
73178
  return finishCommand(result.code);
72762
73179
  }
72763
73180
  async function handleTeardown(argv) {
72764
- const repo = path54.resolve(argv.repo);
73181
+ const repo = path55.resolve(argv.repo);
72765
73182
  const agentId = validateAgentId(argv.id, repo);
72766
73183
  const result = await teardownEnvironment({
72767
73184
  repoPath: repo,
@@ -72772,7 +73189,7 @@ async function handleTeardown(argv) {
72772
73189
  return finishCommand(result.code);
72773
73190
  }
72774
73191
  async function handleComplete(argv) {
72775
- const repo = path54.resolve(argv.repo);
73192
+ const repo = path55.resolve(argv.repo);
72776
73193
  const agentId = validateAgentId(argv.id, repo);
72777
73194
  const result = await completeEnvironment({
72778
73195
  repoPath: repo,
@@ -72793,7 +73210,7 @@ async function handleComplete(argv) {
72793
73210
  return finishCommand(result.code);
72794
73211
  }
72795
73212
  async function handleStatus(argv) {
72796
- const repoPath = path54.resolve(argv.repo);
73213
+ const repoPath = path55.resolve(argv.repo);
72797
73214
  if (argv.json) {
72798
73215
  const status = collectEnvironmentStatus(repoPath);
72799
73216
  const output = EnvironmentStatusSchema.parse(status);
@@ -72805,8 +73222,59 @@ async function handleStatus(argv) {
72805
73222
  capture: false
72806
73223
  });
72807
73224
  }
73225
+ async function handleCleanup(argv) {
73226
+ const keepValues = argv.keep?.split(",").map((value) => value.trim()).filter(Boolean);
73227
+ const keep = parseCleanupKeepPins(keepValues);
73228
+ const repoPaths = argv.repo ? [path55.resolve(argv.repo)] : void 0;
73229
+ const plan = await discoverCleanupCandidates({
73230
+ cwd: process.cwd(),
73231
+ repoPaths,
73232
+ keep,
73233
+ staleDays: argv.stale,
73234
+ orphans: argv.orphans
73235
+ });
73236
+ if (argv.json) {
73237
+ process.stdout.write(JSON.stringify(plan, null, 2) + "\n");
73238
+ if (argv.dryRun) return;
73239
+ } else {
73240
+ header("har env cleanup");
73241
+ process.stdout.write(`${formatCleanupPlan(plan)}
73242
+
73243
+ `);
73244
+ if (argv.dryRun) {
73245
+ info("Dry run \u2014 no changes made.");
73246
+ return;
73247
+ }
73248
+ }
73249
+ let selected = selectAutoApprovedCandidates(plan, { includeReview: argv.includeReview });
73250
+ if (!argv.yes) {
73251
+ try {
73252
+ const interactive = await confirmCleanupSelection(plan.candidates);
73253
+ selected = interactive;
73254
+ } catch (err) {
73255
+ error(err instanceof Error ? err.message : String(err));
73256
+ return finishCommand(1);
73257
+ }
73258
+ }
73259
+ if (selected.length === 0) {
73260
+ info("Nothing selected for cleanup.");
73261
+ return;
73262
+ }
73263
+ const outcomes = await executeCleanupCandidates(selected, { dryRun: false });
73264
+ let failed = 0;
73265
+ for (const outcome of outcomes) {
73266
+ const label = outcome.candidate.kind === "orphan_worktree" ? outcome.candidate.worktreePath : `${outcome.candidate.projectName} agent ${outcome.candidate.agentId}`;
73267
+ if (outcome.ok) {
73268
+ success(`Cleaned ${label}`);
73269
+ } else {
73270
+ failed++;
73271
+ error(`${label}: ${outcome.error ?? "failed"}`);
73272
+ }
73273
+ }
73274
+ return finishCommand(failed > 0 ? 1 : 0);
73275
+ }
72808
73276
  async function handleRunsList(argv) {
72809
- const repoPath = path54.resolve(argv.repo);
73277
+ const repoPath = path55.resolve(argv.repo);
72810
73278
  const runs = listRuns(repoPath, { stageId: argv.stage, limit: argv.limit });
72811
73279
  if (argv.json) {
72812
73280
  process.stdout.write(JSON.stringify({ runs }, null, 2) + "\n");
@@ -72823,7 +73291,7 @@ async function handleRunsList(argv) {
72823
73291
  }
72824
73292
  }
72825
73293
  async function handleRunsGet(argv) {
72826
- const run2 = getRun(path54.resolve(argv.repo), argv.runId);
73294
+ const run2 = getRun(path55.resolve(argv.repo), argv.runId);
72827
73295
  if (!run2) {
72828
73296
  error(`Run not found: ${argv.runId}`);
72829
73297
  return finishCommand(1);
@@ -72905,27 +73373,27 @@ function printNextSteps() {
72905
73373
  }
72906
73374
 
72907
73375
  // src/cli/commands/control.ts
72908
- var path58 = __toESM(require("path"));
73376
+ var path59 = __toESM(require("path"));
72909
73377
  init_control_config();
72910
73378
  init_control_port();
72911
73379
  init_control_sync();
72912
73380
 
72913
73381
  // src/core/control-sync-selection.ts
72914
- var fs47 = __toESM(require("fs"));
72915
- var os19 = __toESM(require("os"));
72916
- var path55 = __toESM(require("path"));
73382
+ var fs48 = __toESM(require("fs"));
73383
+ var os20 = __toESM(require("os"));
73384
+ var path56 = __toESM(require("path"));
72917
73385
  init_control_repo_path();
72918
73386
  function getSelectionPath() {
72919
73387
  if (process.env.HAR_CONTROL_SYNC_SELECTION_PATH) {
72920
- return path55.resolve(process.env.HAR_CONTROL_SYNC_SELECTION_PATH);
73388
+ return path56.resolve(process.env.HAR_CONTROL_SYNC_SELECTION_PATH);
72921
73389
  }
72922
- return path55.join(os19.homedir(), ".har", "sync-selection.json");
73390
+ return path56.join(os20.homedir(), ".har", "sync-selection.json");
72923
73391
  }
72924
73392
  function readSyncSelection() {
72925
73393
  const selectionPath = getSelectionPath();
72926
73394
  try {
72927
- if (!fs47.existsSync(selectionPath)) return null;
72928
- const parsed = JSON.parse(fs47.readFileSync(selectionPath, "utf8"));
73395
+ if (!fs48.existsSync(selectionPath)) return null;
73396
+ const parsed = JSON.parse(fs48.readFileSync(selectionPath, "utf8"));
72929
73397
  if (!Array.isArray(parsed.repos)) return null;
72930
73398
  return parsed.repos.map((repoPath) => canonicalizeControlRepoPath(repoPath));
72931
73399
  } catch {
@@ -72935,8 +73403,8 @@ function readSyncSelection() {
72935
73403
  function writeSyncSelection(repos) {
72936
73404
  const selectionPath = getSelectionPath();
72937
73405
  const canonical = Array.from(new Set(repos.map((repoPath) => canonicalizeControlRepoPath(repoPath))));
72938
- fs47.mkdirSync(path55.dirname(selectionPath), { recursive: true });
72939
- fs47.writeFileSync(selectionPath, JSON.stringify({ repos: canonical }, null, 2) + "\n");
73406
+ fs48.mkdirSync(path56.dirname(selectionPath), { recursive: true });
73407
+ fs48.writeFileSync(selectionPath, JSON.stringify({ repos: canonical }, null, 2) + "\n");
72940
73408
  }
72941
73409
  function resolveSyncSelection(input) {
72942
73410
  const { discovered, stored, forceSelect, isTTY } = input;
@@ -72968,13 +73436,13 @@ init_portal_credentials();
72968
73436
  // src/core/portal-login.ts
72969
73437
  var http = __toESM(require("http"));
72970
73438
  var import_crypto = require("crypto");
72971
- var import_child_process14 = require("child_process");
73439
+ var import_child_process15 = require("child_process");
72972
73440
  var CALLBACK_PATH = "/callback";
72973
73441
  var LOGIN_TIMEOUT_MS = 3 * 6e4;
72974
73442
  function openBrowser(url) {
72975
73443
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
72976
73444
  try {
72977
- const child = (0, import_child_process14.spawn)(cmd, args, { stdio: "ignore", detached: true });
73445
+ const child = (0, import_child_process15.spawn)(cmd, args, { stdio: "ignore", detached: true });
72978
73446
  child.on("error", () => {
72979
73447
  });
72980
73448
  child.unref();
@@ -72983,7 +73451,7 @@ function openBrowser(url) {
72983
73451
  }
72984
73452
  async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
72985
73453
  const state = (0, import_crypto.randomUUID)();
72986
- return new Promise((resolve49, reject) => {
73454
+ return new Promise((resolve50, reject) => {
72987
73455
  let timer;
72988
73456
  const server = http.createServer((req, res) => {
72989
73457
  const requestUrl = new URL(req.url ?? "", "http://127.0.0.1");
@@ -73008,7 +73476,7 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
73008
73476
  res.writeHead(200, { "Content-Type": "text/html" });
73009
73477
  res.end("<h1>Logged in</h1><p>You can close this tab and return to the terminal.</p>");
73010
73478
  cleanup();
73011
- resolve49({
73479
+ resolve50({
73012
73480
  portalUrl,
73013
73481
  token,
73014
73482
  workspace,
@@ -73043,8 +73511,8 @@ If your browser did not open, paste that URL.
73043
73511
  }
73044
73512
 
73045
73513
  // src/core/control-unregister.ts
73046
- var fs48 = __toESM(require("fs"));
73047
- var path56 = __toESM(require("path"));
73514
+ var fs49 = __toESM(require("fs"));
73515
+ var path57 = __toESM(require("path"));
73048
73516
  init_control_repo_path();
73049
73517
  init_control_registry();
73050
73518
  init_control_config();
@@ -73054,9 +73522,9 @@ function listUnregisterWorktreeCandidates(repoPath) {
73054
73522
  const harnessRoot = canonicalizeControlRepoPath(repoPath);
73055
73523
  const byPath = /* @__PURE__ */ new Map();
73056
73524
  const add = (partial2) => {
73057
- const resolved = path56.resolve(partial2.worktreePath);
73525
+ const resolved = path57.resolve(partial2.worktreePath);
73058
73526
  if (!resolved || resolved === harnessRoot) return;
73059
- const exists = partial2.exists ?? fs48.existsSync(resolved);
73527
+ const exists = partial2.exists ?? fs49.existsSync(resolved);
73060
73528
  const prev = byPath.get(resolved);
73061
73529
  if (prev) {
73062
73530
  byPath.set(resolved, {
@@ -73103,12 +73571,12 @@ function listUnregisterWorktreeCandidates(repoPath) {
73103
73571
  }
73104
73572
  return [...byPath.values()].sort((a, b2) => a.agentId - b2.agentId);
73105
73573
  }
73106
- async function askYesNo5(question) {
73574
+ async function askYesNo6(question) {
73107
73575
  if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
73108
73576
  const readline8 = await import("readline");
73109
73577
  const rl = readline8.createInterface({ input: process.stdin, output: process.stderr });
73110
- const answer = await new Promise((resolve49) => {
73111
- rl.question(question, resolve49);
73578
+ const answer = await new Promise((resolve50) => {
73579
+ rl.question(question, resolve50);
73112
73580
  });
73113
73581
  rl.close();
73114
73582
  return /^[Yy]$/.test(answer.trim());
@@ -73120,7 +73588,7 @@ async function confirmUnregister(options) {
73120
73588
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
73121
73589
  throw new Error("Non-interactive terminal \u2014 pass --yes (and optionally --delete-worktrees)");
73122
73590
  }
73123
- const proceed = await askYesNo5("Unregister this repository from Mission Control? [y/N] ");
73591
+ const proceed = await askYesNo6("Unregister this repository from Mission Control? [y/N] ");
73124
73592
  if (!proceed) return { proceed: false, deleteWorktrees: false };
73125
73593
  const existing = options.worktrees.filter((w2) => w2.exists);
73126
73594
  if (existing.length === 0) {
@@ -73132,7 +73600,7 @@ async function confirmUnregister(options) {
73132
73600
  process.stderr.write(` agent-${wt.agentId}: ${wt.worktreePath}${dirty}
73133
73601
  `);
73134
73602
  }
73135
- const deleteWorktrees = await askYesNo5(
73603
+ const deleteWorktrees = await askYesNo6(
73136
73604
  `Also delete ${existing.length} session worktree(s) on disk? [y/N] `
73137
73605
  );
73138
73606
  return { proceed: true, deleteWorktrees };
@@ -73170,28 +73638,28 @@ async function teardownWorktrees(repoPath, candidates, dryRun) {
73170
73638
  trigger: "cli"
73171
73639
  });
73172
73640
  if (result.code !== 0) {
73173
- if (fs48.existsSync(candidate.worktreePath)) {
73174
- fs48.rmSync(candidate.worktreePath, { recursive: true, force: true });
73641
+ if (fs49.existsSync(candidate.worktreePath)) {
73642
+ fs49.rmSync(candidate.worktreePath, { recursive: true, force: true });
73175
73643
  }
73176
73644
  }
73177
73645
  outcomes.push({
73178
73646
  path: candidate.worktreePath,
73179
73647
  agentId: candidate.agentId,
73180
- deleted: !fs48.existsSync(candidate.worktreePath),
73181
- error: result.code !== 0 && fs48.existsSync(candidate.worktreePath) ? result.stderr : void 0
73648
+ deleted: !fs49.existsSync(candidate.worktreePath),
73649
+ error: result.code !== 0 && fs49.existsSync(candidate.worktreePath) ? result.stderr : void 0
73182
73650
  });
73183
73651
  } catch (err) {
73184
73652
  const message = err instanceof Error ? err.message : String(err);
73185
73653
  try {
73186
- if (fs48.existsSync(candidate.worktreePath)) {
73187
- fs48.rmSync(candidate.worktreePath, { recursive: true, force: true });
73654
+ if (fs49.existsSync(candidate.worktreePath)) {
73655
+ fs49.rmSync(candidate.worktreePath, { recursive: true, force: true });
73188
73656
  }
73189
73657
  } catch {
73190
73658
  }
73191
73659
  outcomes.push({
73192
73660
  path: candidate.worktreePath,
73193
73661
  agentId: candidate.agentId,
73194
- deleted: !fs48.existsSync(candidate.worktreePath),
73662
+ deleted: !fs49.existsSync(candidate.worktreePath),
73195
73663
  error: message
73196
73664
  });
73197
73665
  }
@@ -73254,16 +73722,16 @@ init_control_config();
73254
73722
  init_control_registry();
73255
73723
 
73256
73724
  // src/core/harness-scrub.ts
73257
- var fs49 = __toESM(require("fs"));
73258
- var path57 = __toESM(require("path"));
73725
+ var fs50 = __toESM(require("fs"));
73726
+ var path58 = __toESM(require("path"));
73259
73727
  var HARNESS_SCRUB_DIRS = ["runs", "validations", "state", "slots"];
73260
73728
  function scrubLocalHarnessDirs(repoPath) {
73261
73729
  const results = [];
73262
- const resolved = path57.resolve(repoPath);
73263
- if (!fs49.existsSync(resolved)) {
73730
+ const resolved = path58.resolve(repoPath);
73731
+ if (!fs50.existsSync(resolved)) {
73264
73732
  for (const directory of HARNESS_SCRUB_DIRS) {
73265
73733
  results.push({
73266
- path: path57.join(resolved, ".har", directory),
73734
+ path: path58.join(resolved, ".har", directory),
73267
73735
  directory,
73268
73736
  deleted: false,
73269
73737
  error: "repository path does not exist"
@@ -73272,24 +73740,24 @@ function scrubLocalHarnessDirs(repoPath) {
73272
73740
  return results;
73273
73741
  }
73274
73742
  for (const directory of HARNESS_SCRUB_DIRS) {
73275
- const target = path57.join(resolved, ".har", directory);
73276
- if (!fs49.existsSync(target)) {
73743
+ const target = path58.join(resolved, ".har", directory);
73744
+ if (!fs50.existsSync(target)) {
73277
73745
  results.push({ path: target, directory, deleted: true });
73278
73746
  continue;
73279
73747
  }
73280
73748
  try {
73281
- fs49.rmSync(target, { recursive: true, force: true });
73749
+ fs50.rmSync(target, { recursive: true, force: true });
73282
73750
  results.push({
73283
73751
  path: target,
73284
73752
  directory,
73285
- deleted: !fs49.existsSync(target),
73286
- error: fs49.existsSync(target) ? "failed to remove directory" : void 0
73753
+ deleted: !fs50.existsSync(target),
73754
+ error: fs50.existsSync(target) ? "failed to remove directory" : void 0
73287
73755
  });
73288
73756
  } catch (err) {
73289
73757
  results.push({
73290
73758
  path: target,
73291
73759
  directory,
73292
- deleted: !fs49.existsSync(target),
73760
+ deleted: !fs50.existsSync(target),
73293
73761
  error: err instanceof Error ? err.message : String(err)
73294
73762
  });
73295
73763
  }
@@ -73300,7 +73768,7 @@ function scrubLocalHarnessForRepos(repoPaths) {
73300
73768
  const results = [];
73301
73769
  const seen = /* @__PURE__ */ new Set();
73302
73770
  for (const repoPath of repoPaths) {
73303
- const key = path57.resolve(repoPath);
73771
+ const key = path58.resolve(repoPath);
73304
73772
  if (seen.has(key)) continue;
73305
73773
  seen.add(key);
73306
73774
  results.push(...scrubLocalHarnessDirs(key));
@@ -73309,12 +73777,12 @@ function scrubLocalHarnessForRepos(repoPaths) {
73309
73777
  }
73310
73778
 
73311
73779
  // src/core/control-reset.ts
73312
- async function askYesNo6(question) {
73780
+ async function askYesNo7(question) {
73313
73781
  if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
73314
73782
  const readline8 = await import("readline");
73315
73783
  const rl = readline8.createInterface({ input: process.stdin, output: process.stderr });
73316
- const answer = await new Promise((resolve49) => {
73317
- rl.question(question, resolve49);
73784
+ const answer = await new Promise((resolve50) => {
73785
+ rl.question(question, resolve50);
73318
73786
  });
73319
73787
  rl.close();
73320
73788
  return /^[Yy]$/.test(answer.trim());
@@ -73324,7 +73792,7 @@ async function confirmControlReset(options) {
73324
73792
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
73325
73793
  throw new Error("Non-interactive terminal \u2014 pass --yes");
73326
73794
  }
73327
- return askYesNo6(
73795
+ return askYesNo7(
73328
73796
  `Clear Mission Control data for ${options.repoCount} registered repositor${options.repoCount === 1 ? "y" : "ies"} (and scrub local .har history)? [y/N] `
73329
73797
  );
73330
73798
  }
@@ -73559,7 +74027,7 @@ async function handleDown() {
73559
74027
  return finishCommand(code);
73560
74028
  }
73561
74029
  async function handleRegister(argv) {
73562
- const repoPath = path58.resolve(argv.repo);
74030
+ const repoPath = path59.resolve(argv.repo);
73563
74031
  header("har control register");
73564
74032
  info(`Repository: ${repoPath}`);
73565
74033
  try {
@@ -73651,7 +74119,7 @@ async function handleReset(argv) {
73651
74119
  }
73652
74120
  }
73653
74121
  async function handleUnregister(argv) {
73654
- const repoPath = path58.resolve(argv.repo);
74122
+ const repoPath = path59.resolve(argv.repo);
73655
74123
  const worktrees = listUnregisterWorktreeCandidates(repoPath);
73656
74124
  try {
73657
74125
  let deleteWorktrees = argv.deleteWorktrees;
@@ -73833,12 +74301,12 @@ async function handleLogin(argv) {
73833
74301
  }
73834
74302
 
73835
74303
  // src/cli/commands/hooks.ts
73836
- var path60 = __toESM(require("path"));
74304
+ var path61 = __toESM(require("path"));
73837
74305
  init_hooks();
73838
74306
 
73839
74307
  // src/core/claude-hooks.ts
73840
- var fs50 = __toESM(require("fs"));
73841
- var path59 = __toESM(require("path"));
74308
+ var fs51 = __toESM(require("fs"));
74309
+ var path60 = __toESM(require("path"));
73842
74310
  var CLAUDE_GUARD_RELATIVE_PATH = ".har/hooks/claude-worktree-guard.sh";
73843
74311
  var CLAUDE_SETTINGS_RELATIVE_PATH = ".claude/settings.json";
73844
74312
  var GUARD_MATCHER = "Edit|Write|MultiEdit|NotebookEdit";
@@ -73849,8 +74317,8 @@ function isHarGuardEntry(entry) {
73849
74317
  );
73850
74318
  }
73851
74319
  function readSettings(settingsPath) {
73852
- if (!fs50.existsSync(settingsPath)) return {};
73853
- const raw = fs50.readFileSync(settingsPath, "utf8").trim();
74320
+ if (!fs51.existsSync(settingsPath)) return {};
74321
+ const raw = fs51.readFileSync(settingsPath, "utf8").trim();
73854
74322
  if (raw === "") return {};
73855
74323
  const parsed = JSON.parse(raw);
73856
74324
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -73859,19 +74327,19 @@ function readSettings(settingsPath) {
73859
74327
  return parsed;
73860
74328
  }
73861
74329
  function writeSettings(settingsPath, settings) {
73862
- fs50.mkdirSync(path59.dirname(settingsPath), { recursive: true });
73863
- fs50.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
74330
+ fs51.mkdirSync(path60.dirname(settingsPath), { recursive: true });
74331
+ fs51.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
73864
74332
  }
73865
74333
  function installClaudeGuard(repoPath) {
73866
74334
  const templatePath = resolveTemplateFile("claude-worktree-guard.sh.template");
73867
74335
  if (!templatePath) {
73868
74336
  throw new Error("Guard template not found (claude-worktree-guard.sh.template). Run npm run build.");
73869
74337
  }
73870
- const guardScript = path59.join(repoPath, CLAUDE_GUARD_RELATIVE_PATH);
73871
- fs50.mkdirSync(path59.dirname(guardScript), { recursive: true });
73872
- fs50.copyFileSync(templatePath, guardScript);
73873
- fs50.chmodSync(guardScript, 493);
73874
- const settingsPath = path59.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
74338
+ const guardScript = path60.join(repoPath, CLAUDE_GUARD_RELATIVE_PATH);
74339
+ fs51.mkdirSync(path60.dirname(guardScript), { recursive: true });
74340
+ fs51.copyFileSync(templatePath, guardScript);
74341
+ fs51.chmodSync(guardScript, 493);
74342
+ const settingsPath = path60.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
73875
74343
  const settings = readSettings(settingsPath);
73876
74344
  const hooks = settings.hooks ?? {};
73877
74345
  const preToolUse = (hooks.PreToolUse ?? []).filter((entry) => !isHarGuardEntry(entry));
@@ -73885,15 +74353,15 @@ function installClaudeGuard(repoPath) {
73885
74353
  }
73886
74354
  function uninstallClaudeGuard(repoPath) {
73887
74355
  let removed = false;
73888
- const guardScript = path59.join(repoPath, CLAUDE_GUARD_RELATIVE_PATH);
73889
- if (fs50.existsSync(guardScript)) {
73890
- fs50.rmSync(guardScript);
74356
+ const guardScript = path60.join(repoPath, CLAUDE_GUARD_RELATIVE_PATH);
74357
+ if (fs51.existsSync(guardScript)) {
74358
+ fs51.rmSync(guardScript);
73891
74359
  removed = true;
73892
- const parent = path59.dirname(guardScript);
73893
- if (fs50.existsSync(parent) && fs50.readdirSync(parent).length === 0) fs50.rmdirSync(parent);
74360
+ const parent = path60.dirname(guardScript);
74361
+ if (fs51.existsSync(parent) && fs51.readdirSync(parent).length === 0) fs51.rmdirSync(parent);
73894
74362
  }
73895
- const settingsPath = path59.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
73896
- if (fs50.existsSync(settingsPath)) {
74363
+ const settingsPath = path60.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
74364
+ if (fs51.existsSync(settingsPath)) {
73897
74365
  const settings = readSettings(settingsPath);
73898
74366
  const preToolUse = settings.hooks?.PreToolUse;
73899
74367
  if (preToolUse) {
@@ -73918,8 +74386,8 @@ function uninstallClaudeGuard(repoPath) {
73918
74386
  return { removed };
73919
74387
  }
73920
74388
  function claudeGuardInstalled(repoPath) {
73921
- const settingsPath = path59.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
73922
- if (!fs50.existsSync(settingsPath)) return false;
74389
+ const settingsPath = path60.join(repoPath, CLAUDE_SETTINGS_RELATIVE_PATH);
74390
+ if (!fs51.existsSync(settingsPath)) return false;
73923
74391
  try {
73924
74392
  const settings = readSettings(settingsPath);
73925
74393
  return (settings.hooks?.PreToolUse ?? []).some(isHarGuardEntry);
@@ -73936,7 +74404,7 @@ function repoOption(y) {
73936
74404
  function handleInstall2(argv) {
73937
74405
  try {
73938
74406
  if (argv.claude) {
73939
- const result2 = installClaudeGuard(path60.resolve(argv.repo));
74407
+ const result2 = installClaudeGuard(path61.resolve(argv.repo));
73940
74408
  success("Claude Code worktree guard installed.");
73941
74409
  info(`Guard script: ${result2.guardScript}`);
73942
74410
  info(`Settings: ${result2.settingsPath} (PreToolUse: Edit|Write|MultiEdit|NotebookEdit)`);
@@ -73944,7 +74412,7 @@ function handleInstall2(argv) {
73944
74412
  info("Commit both files so the guard travels with the repo.");
73945
74413
  return;
73946
74414
  }
73947
- const result = installHooks({ repoPath: path60.resolve(argv.repo), force: argv.force });
74415
+ const result = installHooks({ repoPath: path61.resolve(argv.repo), force: argv.force });
73948
74416
  success(`Commit gate installed in ${result.hooksDir}`);
73949
74417
  info(`pre-commit: ${result.preCommit}, post-commit: ${result.postCommit}`);
73950
74418
  info("Commits of unverified change batches will be blocked in agent worktrees.");
@@ -73957,7 +74425,7 @@ function handleInstall2(argv) {
73957
74425
  function handleUninstall(argv) {
73958
74426
  try {
73959
74427
  if (argv.claude) {
73960
- const result2 = uninstallClaudeGuard(path60.resolve(argv.repo));
74428
+ const result2 = uninstallClaudeGuard(path61.resolve(argv.repo));
73961
74429
  if (result2.removed) {
73962
74430
  success("Claude Code worktree guard removed.");
73963
74431
  } else {
@@ -73965,7 +74433,7 @@ function handleUninstall(argv) {
73965
74433
  }
73966
74434
  return;
73967
74435
  }
73968
- const result = uninstallHooks(path60.resolve(argv.repo));
74436
+ const result = uninstallHooks(path61.resolve(argv.repo));
73969
74437
  if (result.removed) {
73970
74438
  success(`Commit gate removed from ${result.hooksDir}`);
73971
74439
  } else {
@@ -73978,7 +74446,7 @@ function handleUninstall(argv) {
73978
74446
  }
73979
74447
  function handleStatus2(argv) {
73980
74448
  try {
73981
- const status = getHooksStatus(path60.resolve(argv.repo));
74449
+ const status = getHooksStatus(path61.resolve(argv.repo));
73982
74450
  if (argv.json) {
73983
74451
  process.stdout.write(`${JSON.stringify(status, null, 2)}
73984
74452
  `);
@@ -73996,7 +74464,7 @@ function handleStatus2(argv) {
73996
74464
  );
73997
74465
  info(`Effective: ${status.effectiveMode} (in this checkout)`);
73998
74466
  info(
73999
- `Claude guard: ${claudeGuardInstalled(path60.resolve(argv.repo)) ? `installed (${CLAUDE_GUARD_RELATIVE_PATH})` : "not installed"}`
74467
+ `Claude guard: ${claudeGuardInstalled(path61.resolve(argv.repo)) ? `installed (${CLAUDE_GUARD_RELATIVE_PATH})` : "not installed"}`
74000
74468
  );
74001
74469
  } catch (err) {
74002
74470
  error(err instanceof Error ? err.message : String(err));
@@ -74257,10 +74725,10 @@ function assignProp(target, prop, value) {
74257
74725
  configurable: true
74258
74726
  });
74259
74727
  }
74260
- function getElementAtPath(obj, path65) {
74261
- if (!path65)
74728
+ function getElementAtPath(obj, path66) {
74729
+ if (!path66)
74262
74730
  return obj;
74263
- return path65.reduce((acc, key) => acc?.[key], obj);
74731
+ return path66.reduce((acc, key) => acc?.[key], obj);
74264
74732
  }
74265
74733
  function promiseAllObject(promisesObj) {
74266
74734
  const keys = Object.keys(promisesObj);
@@ -74580,11 +75048,11 @@ function aborted(x, startIndex = 0) {
74580
75048
  }
74581
75049
  return false;
74582
75050
  }
74583
- function prefixIssues(path65, issues) {
75051
+ function prefixIssues(path66, issues) {
74584
75052
  return issues.map((iss) => {
74585
75053
  var _a2;
74586
75054
  (_a2 = iss).path ?? (_a2.path = []);
74587
- iss.path.unshift(path65);
75055
+ iss.path.unshift(path66);
74588
75056
  return iss;
74589
75057
  });
74590
75058
  }
@@ -79907,7 +80375,7 @@ var Protocol = class {
79907
80375
  return;
79908
80376
  }
79909
80377
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
79910
- await new Promise((resolve49) => setTimeout(resolve49, pollInterval));
80378
+ await new Promise((resolve50) => setTimeout(resolve50, pollInterval));
79911
80379
  options?.signal?.throwIfAborted();
79912
80380
  }
79913
80381
  } catch (error3) {
@@ -79924,7 +80392,7 @@ var Protocol = class {
79924
80392
  */
79925
80393
  request(request, resultSchema, options) {
79926
80394
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
79927
- return new Promise((resolve49, reject) => {
80395
+ return new Promise((resolve50, reject) => {
79928
80396
  const earlyReject = (error3) => {
79929
80397
  reject(error3);
79930
80398
  };
@@ -80002,7 +80470,7 @@ var Protocol = class {
80002
80470
  if (!parseResult.success) {
80003
80471
  reject(parseResult.error);
80004
80472
  } else {
80005
- resolve49(parseResult.data);
80473
+ resolve50(parseResult.data);
80006
80474
  }
80007
80475
  } catch (error3) {
80008
80476
  reject(error3);
@@ -80263,12 +80731,12 @@ var Protocol = class {
80263
80731
  }
80264
80732
  } catch {
80265
80733
  }
80266
- return new Promise((resolve49, reject) => {
80734
+ return new Promise((resolve50, reject) => {
80267
80735
  if (signal.aborted) {
80268
80736
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
80269
80737
  return;
80270
80738
  }
80271
- const timeoutId = setTimeout(resolve49, interval);
80739
+ const timeoutId = setTimeout(resolve50, interval);
80272
80740
  signal.addEventListener("abort", () => {
80273
80741
  clearTimeout(timeoutId);
80274
80742
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -81058,7 +81526,7 @@ var Server = class extends Protocol {
81058
81526
  };
81059
81527
 
81060
81528
  // src/mcp/server.ts
81061
- var path61 = __toESM(require("path"));
81529
+ var path62 = __toESM(require("path"));
81062
81530
 
81063
81531
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
81064
81532
  var import_node_process3 = __toESM(require("node:process"), 1);
@@ -81141,12 +81609,12 @@ var StdioServerTransport = class {
81141
81609
  this.onclose?.();
81142
81610
  }
81143
81611
  send(message) {
81144
- return new Promise((resolve49) => {
81612
+ return new Promise((resolve50) => {
81145
81613
  const json = serializeMessage(message);
81146
81614
  if (this._stdout.write(json)) {
81147
- resolve49();
81615
+ resolve50();
81148
81616
  } else {
81149
- this._stdout.once("drain", resolve49);
81617
+ this._stdout.once("drain", resolve50);
81150
81618
  }
81151
81619
  });
81152
81620
  }
@@ -81155,6 +81623,8 @@ var StdioServerTransport = class {
81155
81623
  // src/mcp/server.ts
81156
81624
  init_telemetry_config();
81157
81625
  init_runs();
81626
+ init_work_units();
81627
+ init_manifest();
81158
81628
 
81159
81629
  // src/mcp/schema-tools.ts
81160
81630
  init_schema2();
@@ -81239,7 +81709,21 @@ var LaunchEnvironmentInputSchema = external_exports.object({
81239
81709
  source: external_exports.string().min(1).max(64).optional(),
81240
81710
  sourceUrl: external_exports.string().url().optional(),
81241
81711
  title: external_exports.string().min(1).max(256).optional(),
81242
- parentWorkUnitId: external_exports.string().min(1).max(128).optional()
81712
+ parentWorkUnitId: external_exports.string().min(1).max(128).optional(),
81713
+ relatedLinks: external_exports.array(
81714
+ external_exports.object({
81715
+ source: external_exports.string().min(1).max(64),
81716
+ url: external_exports.string().url(),
81717
+ label: external_exports.string().min(1).max(128).optional()
81718
+ })
81719
+ ).optional()
81720
+ });
81721
+ var AddWorkUnitLinkInputSchema = external_exports.object({
81722
+ repo: external_exports.string().default("."),
81723
+ workUnitId: external_exports.string().min(1).max(128),
81724
+ source: external_exports.string().min(1).max(64),
81725
+ url: external_exports.string().url(),
81726
+ label: external_exports.string().min(1).max(128).optional()
81243
81727
  });
81244
81728
  var LaunchEnvironmentOutputSchema = ShellRunOutputSchema.extend({
81245
81729
  previewUrls: external_exports.record(external_exports.string()).optional(),
@@ -81386,11 +81870,41 @@ var HAR_MCP_TOOLS = [
81386
81870
  parentWorkUnitId: {
81387
81871
  type: "string",
81388
81872
  description: "Optional parent work unit identifier."
81873
+ },
81874
+ relatedLinks: {
81875
+ type: "array",
81876
+ description: "Additional tracker links (append-only). Each item: source, url, optional label.",
81877
+ items: {
81878
+ type: "object",
81879
+ properties: {
81880
+ source: { type: "string" },
81881
+ url: { type: "string" },
81882
+ label: { type: "string" }
81883
+ },
81884
+ required: ["source", "url"]
81885
+ }
81389
81886
  }
81390
81887
  },
81391
81888
  ["agentId"]
81392
81889
  )
81393
81890
  },
81891
+ {
81892
+ name: "har_add_work_unit_link",
81893
+ description: "Append a related external link (PR, mirrored issue, alternate tracker) to an existing work unit.",
81894
+ inputSchema: objectJsonSchema(
81895
+ {
81896
+ repo: repoJsonProperty,
81897
+ workUnitId: {
81898
+ type: "string",
81899
+ description: "Repo-scoped work unit id bound at launch."
81900
+ },
81901
+ source: { type: "string", description: "Tracker provider (e.g. github, jira)." },
81902
+ url: { type: "string", description: "Canonical URL for the link." },
81903
+ label: { type: "string", description: "Optional display label." }
81904
+ },
81905
+ ["workUnitId", "source", "url"]
81906
+ )
81907
+ },
81394
81908
  {
81395
81909
  name: "har_recover_environment",
81396
81910
  description: "Resume a failed or partial agent launch without replacing the worktree. Alias for har_launch_environment with resume=true.",
@@ -81561,6 +82075,7 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
81561
82075
  sourceUrl: input.sourceUrl,
81562
82076
  title: input.title,
81563
82077
  parentWorkUnitId: input.parentWorkUnitId,
82078
+ relatedLinks: input.relatedLinks,
81564
82079
  capture: true
81565
82080
  });
81566
82081
  const parsed = LaunchEnvironmentOutputSchema.parse(result);
@@ -81583,6 +82098,7 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
81583
82098
  sourceUrl: input.sourceUrl,
81584
82099
  title: input.title,
81585
82100
  parentWorkUnitId: input.parentWorkUnitId,
82101
+ relatedLinks: input.relatedLinks,
81586
82102
  capture: true
81587
82103
  });
81588
82104
  const parsed = LaunchEnvironmentOutputSchema.parse(result);
@@ -81591,6 +82107,28 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
81591
82107
  ...result.blocked ? { isError: true } : {}
81592
82108
  };
81593
82109
  }
82110
+ case "har_add_work_unit_link": {
82111
+ const input = AddWorkUnitLinkInputSchema.parse({ ...args, repo });
82112
+ const harnessRoot = resolveHarnessRoot(repo);
82113
+ try {
82114
+ const record2 = addWorkUnitLinks(harnessRoot, input.workUnitId, [
82115
+ { source: input.source, url: input.url, label: input.label }
82116
+ ]);
82117
+ return jsonContent({
82118
+ ok: true,
82119
+ workUnitId: record2.workUnitId,
82120
+ relatedLinks: record2.relatedLinks
82121
+ });
82122
+ } catch (err) {
82123
+ return {
82124
+ ...jsonContent({
82125
+ ok: false,
82126
+ error: err instanceof Error ? err.message : String(err)
82127
+ }),
82128
+ isError: true
82129
+ };
82130
+ }
82131
+ }
81594
82132
  case "har_preflight_environment": {
81595
82133
  const input = PreflightEnvironmentInputSchema.parse({ ...args, repo });
81596
82134
  const agentId = validateAgentId(input.agentId, repo);
@@ -81701,7 +82239,7 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
81701
82239
  const input = ControlUpInputSchema.parse({ ...args, repo });
81702
82240
  const result = await startControlAndSync({
81703
82241
  detach: input.detach,
81704
- cwd: path61.resolve(input.repo)
82242
+ cwd: path62.resolve(input.repo)
81705
82243
  });
81706
82244
  if (result.code !== 0) {
81707
82245
  return {
@@ -81765,11 +82303,11 @@ var mcpCommand = {
81765
82303
  };
81766
82304
 
81767
82305
  // src/cli/commands/onboard.ts
81768
- var path63 = __toESM(require("path"));
82306
+ var path64 = __toESM(require("path"));
81769
82307
  init_stages();
81770
82308
 
81771
82309
  // src/core/onboarding.ts
81772
- var path62 = __toESM(require("path"));
82310
+ var path63 = __toESM(require("path"));
81773
82311
  init_control_config();
81774
82312
  init_control_sync();
81775
82313
  init_otel_hooks();
@@ -81905,7 +82443,7 @@ async function defaultEnsureControl(options) {
81905
82443
  return { started: true, apiUrl: result.apiUrl };
81906
82444
  }
81907
82445
  async function runOnboarding(options, deps = {}) {
81908
- const repoPath = path62.resolve(options.repoPath);
82446
+ const repoPath = path63.resolve(options.repoPath);
81909
82447
  const applyTelemetry = deps.applyTelemetry ?? applyOnboardingTelemetry;
81910
82448
  const ensureControl = deps.ensureControl ?? defaultEnsureControl;
81911
82449
  const init = deps.initHarness ?? initHarness;
@@ -82093,7 +82631,7 @@ async function promptChoices(args) {
82093
82631
  };
82094
82632
  }
82095
82633
  const pluginChoices = listPluginChoices();
82096
- const repoPath = path63.resolve(args.repo);
82634
+ const repoPath = path64.resolve(args.repo);
82097
82635
  const alreadyPresent = harnessExists(repoPath);
82098
82636
  const needProfile = !alreadyPresent && !args.skipInit && args.profile === void 0;
82099
82637
  const needAgentSlots = shouldConfigureSlots && flaggedSlots === void 0;
@@ -82210,7 +82748,7 @@ function printSummary(result) {
82210
82748
  console.error("");
82211
82749
  }
82212
82750
  async function handleOnboard(argv) {
82213
- const repoPath = path63.resolve(argv.repo);
82751
+ const repoPath = path64.resolve(argv.repo);
82214
82752
  header("har onboard");
82215
82753
  info(`Repository: ${repoPath}`);
82216
82754
  try {
@@ -82441,7 +82979,7 @@ var preferencesCommand = {
82441
82979
  };
82442
82980
 
82443
82981
  // src/cli/commands/telemetry.ts
82444
- var path64 = __toESM(require("path"));
82982
+ var path65 = __toESM(require("path"));
82445
82983
  init_control_config();
82446
82984
  init_control_sync();
82447
82985
  init_manifest();
@@ -82534,7 +83072,7 @@ async function handleOff() {
82534
83072
  info("Mission Control will not auto-start. Existing usage rows are kept.");
82535
83073
  }
82536
83074
  async function handleWriteEnv(argv) {
82537
- const repoPath = path64.resolve(argv.repo);
83075
+ const repoPath = path65.resolve(argv.repo);
82538
83076
  const harnessRoot = resolveHarnessRoot(repoPath);
82539
83077
  const session = readSlotRegistry(harnessRoot, argv.agentId);
82540
83078
  const workDir = argv.workDir ?? session?.workDir ?? repoPath;
@@ -82546,7 +83084,7 @@ async function handleWriteEnv(argv) {
82546
83084
  suffix,
82547
83085
  createdAt: session?.createdAt
82548
83086
  });
82549
- const envFile = argv.envFile ?? path64.join(workDir, `.env.agent.${argv.agentId}`);
83087
+ const envFile = argv.envFile ?? path65.join(workDir, `.env.agent.${argv.agentId}`);
82550
83088
  const attrs = {
82551
83089
  sessionKey,
82552
83090
  agentId: argv.agentId,
@@ -82562,7 +83100,7 @@ async function handleWriteEnv(argv) {
82562
83100
  success(`Wrote session attribution to ${envFile}`);
82563
83101
  }
82564
83102
  function handlePrintEnv(argv) {
82565
- const repoPath = path64.resolve(argv.repo);
83103
+ const repoPath = path65.resolve(argv.repo);
82566
83104
  const workDir = argv.workDir ?? repoPath;
82567
83105
  const sessionKey = buildSessionKey({
82568
83106
  branch: argv.branch,