@outcomeeng/spx 0.5.4 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1046,6 +1046,7 @@ var STATE_STORE_PATH = {
1046
1046
  BRANCH_SCOPE: "branch",
1047
1047
  WORKTREE_SCOPE: "worktree",
1048
1048
  SESSIONS_SCOPE: "sessions",
1049
+ WORKTREES_SCOPE: "worktrees",
1049
1050
  RUNS_DIR: "runs",
1050
1051
  RUN_FILE_PREFIX: "run-",
1051
1052
  JSONL_EXTENSION: ".jsonl"
@@ -1109,6 +1110,16 @@ function worktreeScopeDir(productDir) {
1109
1110
  function sessionsScopeDir(productDir) {
1110
1111
  return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
1111
1112
  }
1113
+ async function resolveWorktreesScopeDir(options = {}) {
1114
+ const gitResult = await detectGitCommonDirProductRoot(options.cwd, options.deps);
1115
+ return {
1116
+ worktreesDir: worktreesScopeDir(gitResult.productDir),
1117
+ warning: gitResult.warning
1118
+ };
1119
+ }
1120
+ function worktreesScopeDir(productDir) {
1121
+ return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.WORKTREES_SCOPE);
1122
+ }
1112
1123
  function composeScopeDir(baseScopeDir, ...tokens) {
1113
1124
  const segments = [];
1114
1125
  for (const token of tokens) {
@@ -2516,8 +2527,8 @@ var HIDDEN_PREFIX_LAYER = "hidden-prefix";
2516
2527
  var LAYER2 = HIDDEN_PREFIX_LAYER;
2517
2528
  function hiddenPrefixPredicate(path6, config) {
2518
2529
  const segments = path6.split("/");
2519
- const basename4 = segments[segments.length - 1] ?? "";
2520
- const matched = basename4.startsWith(config.hiddenPrefix);
2530
+ const basename5 = segments[segments.length - 1] ?? "";
2531
+ const matched = basename5.startsWith(config.hiddenPrefix);
2521
2532
  return { matched, layer: LAYER2 };
2522
2533
  }
2523
2534
 
@@ -4865,17 +4876,17 @@ async function readStdin() {
4865
4876
  if (process.stdin.isTTY) {
4866
4877
  return void 0;
4867
4878
  }
4868
- return new Promise((resolve5) => {
4879
+ return new Promise((resolve6) => {
4869
4880
  let data = "";
4870
4881
  process.stdin.setEncoding("utf-8");
4871
4882
  process.stdin.on("data", (chunk) => {
4872
4883
  data += chunk;
4873
4884
  });
4874
4885
  process.stdin.on("end", () => {
4875
- resolve5(data.length === 0 ? void 0 : data);
4886
+ resolve6(data.length === 0 ? void 0 : data);
4876
4887
  });
4877
4888
  process.stdin.on("error", () => {
4878
- resolve5(void 0);
4889
+ resolve6(void 0);
4879
4890
  });
4880
4891
  });
4881
4892
  }
@@ -7813,39 +7824,39 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7813
7824
  const token = _scanner.scan();
7814
7825
  switch (_scanner.getTokenError()) {
7815
7826
  case 4:
7816
- handleError2(
7827
+ handleError3(
7817
7828
  14
7818
7829
  /* ParseErrorCode.InvalidUnicode */
7819
7830
  );
7820
7831
  break;
7821
7832
  case 5:
7822
- handleError2(
7833
+ handleError3(
7823
7834
  15
7824
7835
  /* ParseErrorCode.InvalidEscapeCharacter */
7825
7836
  );
7826
7837
  break;
7827
7838
  case 3:
7828
- handleError2(
7839
+ handleError3(
7829
7840
  13
7830
7841
  /* ParseErrorCode.UnexpectedEndOfNumber */
7831
7842
  );
7832
7843
  break;
7833
7844
  case 1:
7834
7845
  if (!disallowComments) {
7835
- handleError2(
7846
+ handleError3(
7836
7847
  11
7837
7848
  /* ParseErrorCode.UnexpectedEndOfComment */
7838
7849
  );
7839
7850
  }
7840
7851
  break;
7841
7852
  case 2:
7842
- handleError2(
7853
+ handleError3(
7843
7854
  12
7844
7855
  /* ParseErrorCode.UnexpectedEndOfString */
7845
7856
  );
7846
7857
  break;
7847
7858
  case 6:
7848
- handleError2(
7859
+ handleError3(
7849
7860
  16
7850
7861
  /* ParseErrorCode.InvalidCharacter */
7851
7862
  );
@@ -7855,7 +7866,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7855
7866
  case 12:
7856
7867
  case 13:
7857
7868
  if (disallowComments) {
7858
- handleError2(
7869
+ handleError3(
7859
7870
  10
7860
7871
  /* ParseErrorCode.InvalidCommentToken */
7861
7872
  );
@@ -7864,7 +7875,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7864
7875
  }
7865
7876
  break;
7866
7877
  case 16:
7867
- handleError2(
7878
+ handleError3(
7868
7879
  1
7869
7880
  /* ParseErrorCode.InvalidSymbol */
7870
7881
  );
@@ -7877,7 +7888,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7877
7888
  }
7878
7889
  }
7879
7890
  }
7880
- function handleError2(error, skipUntilAfter = [], skipUntil = []) {
7891
+ function handleError3(error, skipUntilAfter = [], skipUntil = []) {
7881
7892
  onError(error);
7882
7893
  if (skipUntilAfter.length + skipUntil.length > 0) {
7883
7894
  let token = _scanner.getToken();
@@ -7909,7 +7920,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7909
7920
  const tokenValue = _scanner.getTokenValue();
7910
7921
  let value = Number(tokenValue);
7911
7922
  if (isNaN(value)) {
7912
- handleError2(
7923
+ handleError3(
7913
7924
  2
7914
7925
  /* ParseErrorCode.InvalidNumberFormat */
7915
7926
  );
@@ -7934,7 +7945,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7934
7945
  }
7935
7946
  function parseProperty() {
7936
7947
  if (_scanner.getToken() !== 10) {
7937
- handleError2(3, [], [
7948
+ handleError3(3, [], [
7938
7949
  2,
7939
7950
  5
7940
7951
  /* SyntaxKind.CommaToken */
@@ -7946,14 +7957,14 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7946
7957
  onSeparator(":");
7947
7958
  scanNext();
7948
7959
  if (!parseValue()) {
7949
- handleError2(4, [], [
7960
+ handleError3(4, [], [
7950
7961
  2,
7951
7962
  5
7952
7963
  /* SyntaxKind.CommaToken */
7953
7964
  ]);
7954
7965
  }
7955
7966
  } else {
7956
- handleError2(5, [], [
7967
+ handleError3(5, [], [
7957
7968
  2,
7958
7969
  5
7959
7970
  /* SyntaxKind.CommaToken */
@@ -7969,7 +7980,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7969
7980
  while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
7970
7981
  if (_scanner.getToken() === 5) {
7971
7982
  if (!needsComma) {
7972
- handleError2(4, [], []);
7983
+ handleError3(4, [], []);
7973
7984
  }
7974
7985
  onSeparator(",");
7975
7986
  scanNext();
@@ -7977,10 +7988,10 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7977
7988
  break;
7978
7989
  }
7979
7990
  } else if (needsComma) {
7980
- handleError2(6, [], []);
7991
+ handleError3(6, [], []);
7981
7992
  }
7982
7993
  if (!parseProperty()) {
7983
- handleError2(4, [], [
7994
+ handleError3(4, [], [
7984
7995
  2,
7985
7996
  5
7986
7997
  /* SyntaxKind.CommaToken */
@@ -7990,7 +8001,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7990
8001
  }
7991
8002
  onObjectEnd();
7992
8003
  if (_scanner.getToken() !== 2) {
7993
- handleError2(7, [
8004
+ handleError3(7, [
7994
8005
  2
7995
8006
  /* SyntaxKind.CloseBraceToken */
7996
8007
  ], []);
@@ -8007,7 +8018,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
8007
8018
  while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
8008
8019
  if (_scanner.getToken() === 5) {
8009
8020
  if (!needsComma) {
8010
- handleError2(4, [], []);
8021
+ handleError3(4, [], []);
8011
8022
  }
8012
8023
  onSeparator(",");
8013
8024
  scanNext();
@@ -8015,7 +8026,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
8015
8026
  break;
8016
8027
  }
8017
8028
  } else if (needsComma) {
8018
- handleError2(6, [], []);
8029
+ handleError3(6, [], []);
8019
8030
  }
8020
8031
  if (isFirstElement) {
8021
8032
  _jsonPath.push(0);
@@ -8024,7 +8035,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
8024
8035
  _jsonPath[_jsonPath.length - 1]++;
8025
8036
  }
8026
8037
  if (!parseValue()) {
8027
- handleError2(4, [], [
8038
+ handleError3(4, [], [
8028
8039
  4,
8029
8040
  5
8030
8041
  /* SyntaxKind.CommaToken */
@@ -8037,7 +8048,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
8037
8048
  _jsonPath.pop();
8038
8049
  }
8039
8050
  if (_scanner.getToken() !== 4) {
8040
- handleError2(8, [
8051
+ handleError3(8, [
8041
8052
  4
8042
8053
  /* SyntaxKind.CloseBracketToken */
8043
8054
  ], []);
@@ -8063,15 +8074,15 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
8063
8074
  if (options.allowEmptyContent) {
8064
8075
  return true;
8065
8076
  }
8066
- handleError2(4, [], []);
8077
+ handleError3(4, [], []);
8067
8078
  return false;
8068
8079
  }
8069
8080
  if (!parseValue()) {
8070
- handleError2(4, [], []);
8081
+ handleError3(4, [], []);
8071
8082
  return false;
8072
8083
  }
8073
8084
  if (_scanner.getToken() !== 17) {
8074
- handleError2(9, [], []);
8085
+ handleError3(9, [], []);
8075
8086
  }
8076
8087
  return true;
8077
8088
  }
@@ -8526,13 +8537,13 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
8526
8537
  knipProcess.stderr?.on("data", (data) => {
8527
8538
  knipError += data.toString();
8528
8539
  });
8529
- return new Promise((resolve5) => {
8540
+ return new Promise((resolve6) => {
8530
8541
  const resolveAfterCleanup = (result) => {
8531
8542
  if (resultResolved) {
8532
8543
  return;
8533
8544
  }
8534
8545
  resultResolved = true;
8535
- void cleanupOnce().finally(() => resolve5(result));
8546
+ void cleanupOnce().finally(() => resolve6(result));
8536
8547
  };
8537
8548
  knipProcess.on("close", (code) => {
8538
8549
  if (code === 0) {
@@ -8970,7 +8981,7 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
8970
8981
  scope,
8971
8982
  scopeConfig: context.scopeConfig
8972
8983
  });
8973
- return new Promise((resolve5) => {
8984
+ return new Promise((resolve6) => {
8974
8985
  const localBin = join26(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
8975
8986
  const binary = existsSync5(localBin) ? localBin : "npx";
8976
8987
  const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
@@ -8980,13 +8991,13 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
8980
8991
  forwardValidationSubprocessOutput(eslintProcess, outputStreams);
8981
8992
  eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
8982
8993
  if (code === 0) {
8983
- resolve5({ success: true });
8994
+ resolve6({ success: true });
8984
8995
  } else {
8985
- resolve5({ success: false, error: `ESLint exited with code ${code}` });
8996
+ resolve6({ success: false, error: `ESLint exited with code ${code}` });
8986
8997
  }
8987
8998
  });
8988
8999
  eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
8989
- resolve5({ success: false, error: error.message });
9000
+ resolve6({ success: false, error: error.message });
8990
9001
  });
8991
9002
  });
8992
9003
  }
@@ -9864,7 +9875,7 @@ async function validateTypeScript(context, options = {}) {
9864
9875
  if (files && files.length > 0) {
9865
9876
  const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, projectRoot, deps);
9866
9877
  try {
9867
- return await new Promise((resolve5) => {
9878
+ return await new Promise((resolve6) => {
9868
9879
  const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
9869
9880
  const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
9870
9881
  const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
@@ -9875,14 +9886,14 @@ async function validateTypeScript(context, options = {}) {
9875
9886
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
9876
9887
  cleanup();
9877
9888
  if (code === 0) {
9878
- resolve5({ success: true, skipped: false });
9889
+ resolve6({ success: true, skipped: false });
9879
9890
  } else {
9880
- resolve5({ success: false, error: `TypeScript exited with code ${code}` });
9891
+ resolve6({ success: false, error: `TypeScript exited with code ${code}` });
9881
9892
  }
9882
9893
  });
9883
9894
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
9884
9895
  cleanup();
9885
- resolve5({ success: false, error: error.message });
9896
+ resolve6({ success: false, error: error.message });
9886
9897
  });
9887
9898
  });
9888
9899
  } catch (error) {
@@ -9901,7 +9912,7 @@ async function validateTypeScript(context, options = {}) {
9901
9912
  const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
9902
9913
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
9903
9914
  tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
9904
- return new Promise((resolve5) => {
9915
+ return new Promise((resolve6) => {
9905
9916
  const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
9906
9917
  cwd: projectRoot
9907
9918
  });
@@ -9909,14 +9920,14 @@ async function validateTypeScript(context, options = {}) {
9909
9920
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
9910
9921
  cleanup();
9911
9922
  if (code === 0) {
9912
- resolve5({ success: true, skipped: false });
9923
+ resolve6({ success: true, skipped: false });
9913
9924
  } else {
9914
- resolve5({ success: false, error: `TypeScript exited with code ${code}` });
9925
+ resolve6({ success: false, error: `TypeScript exited with code ${code}` });
9915
9926
  }
9916
9927
  });
9917
9928
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
9918
9929
  cleanup();
9919
- resolve5({ success: false, error: error.message });
9930
+ resolve6({ success: false, error: error.message });
9920
9931
  });
9921
9932
  });
9922
9933
  } else {
@@ -9925,20 +9936,20 @@ async function validateTypeScript(context, options = {}) {
9925
9936
  const rawArgs = buildTypeScriptArgs({ scope, configFile });
9926
9937
  tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
9927
9938
  }
9928
- return new Promise((resolve5) => {
9939
+ return new Promise((resolve6) => {
9929
9940
  const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
9930
9941
  cwd: projectRoot
9931
9942
  });
9932
9943
  forwardValidationSubprocessOutput(tscProcess, outputStreams);
9933
9944
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
9934
9945
  if (code === 0) {
9935
- resolve5({ success: true, skipped: false });
9946
+ resolve6({ success: true, skipped: false });
9936
9947
  } else {
9937
- resolve5({ success: false, error: `TypeScript exited with code ${code}` });
9948
+ resolve6({ success: false, error: `TypeScript exited with code ${code}` });
9938
9949
  }
9939
9950
  });
9940
9951
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
9941
- resolve5({ success: false, error: error.message });
9952
+ resolve6({ success: false, error: error.message });
9942
9953
  });
9943
9954
  });
9944
9955
  }
@@ -10523,6 +10534,393 @@ var validationDomain = {
10523
10534
  }
10524
10535
  };
10525
10536
 
10537
+ // src/domains/worktree/controlling-process.ts
10538
+ var CONTROLLING_PID_ENV = "SPX_WORKTREE_CONTROLLING_PID";
10539
+ var AGENT_RUNTIME_NAMES = ["claude", "codex"];
10540
+ var AGENT_COMMAND_PATTERN = new RegExp(`\\b(?:${AGENT_RUNTIME_NAMES.join("|")})\\b`, "i");
10541
+ var CONTROLLING_PROCESS_ERROR = {
10542
+ UNRESOLVED: "worktree controlling process could not be resolved"
10543
+ };
10544
+ var MAX_ANCESTRY_DEPTH = 64;
10545
+ var PID_RADIX = 10;
10546
+ function resolveControllingProcess(selfPid, table, env) {
10547
+ const host = table.currentHost();
10548
+ for (const pid of controllingPidCandidates(selfPid, table, env)) {
10549
+ const startedAt = table.startTimeOf(pid);
10550
+ if (startedAt !== void 0) return { ok: true, value: { pid, startedAt, host } };
10551
+ }
10552
+ return { ok: false, error: CONTROLLING_PROCESS_ERROR.UNRESOLVED };
10553
+ }
10554
+ function* controllingPidCandidates(selfPid, table, env) {
10555
+ const override = parsePid(env[CONTROLLING_PID_ENV]);
10556
+ if (override !== void 0) yield override;
10557
+ const agent = findAgentAncestor(selfPid, table);
10558
+ if (agent !== void 0) yield agent;
10559
+ const parent = table.parentOf(selfPid);
10560
+ if (parent !== void 0) yield parent;
10561
+ }
10562
+ function findAgentAncestor(selfPid, table) {
10563
+ let pid = table.parentOf(selfPid);
10564
+ for (let depth = 0; pid !== void 0 && depth < MAX_ANCESTRY_DEPTH; depth += 1) {
10565
+ const command = table.commandOf(pid);
10566
+ if (command !== void 0 && AGENT_COMMAND_PATTERN.test(command)) return pid;
10567
+ pid = table.parentOf(pid);
10568
+ }
10569
+ return void 0;
10570
+ }
10571
+ function parsePid(value) {
10572
+ if (value === void 0 || value.length === 0) return void 0;
10573
+ const parsed = Number.parseInt(value, PID_RADIX);
10574
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
10575
+ }
10576
+
10577
+ // src/domains/worktree/occupancy-store.ts
10578
+ import {
10579
+ mkdir as nodeMkdir2,
10580
+ readFile as nodeReadFile2,
10581
+ rename as nodeRename,
10582
+ rm as nodeRm,
10583
+ writeFile as nodeWriteFile2
10584
+ } from "fs/promises";
10585
+ import { join as join30 } from "path";
10586
+ var OCCUPANCY_STATUS = {
10587
+ UNCLAIMED: "unclaimed",
10588
+ OCCUPIED: "occupied",
10589
+ STALE: "stale"
10590
+ };
10591
+ var OCCUPANCY_CLAIM = {
10592
+ FILE_EXTENSION: ".claim",
10593
+ TEMP_EXTENSION: ".tmp"
10594
+ };
10595
+ var OCCUPANCY_ERROR = {
10596
+ INVALID_NAME: "worktree occupancy claim name must be a safe path segment",
10597
+ CLAIM_WRITE_FAILED: "worktree occupancy claim write failed",
10598
+ CLAIM_READ_FAILED: "worktree occupancy claim read failed",
10599
+ CLAIM_REMOVE_FAILED: "worktree occupancy claim remove failed",
10600
+ CLAIM_MALFORMED: "worktree occupancy claim record is malformed"
10601
+ };
10602
+ var ERROR_DETAIL_SEPARATOR2 = ": ";
10603
+ var defaultOccupancyFileSystem = {
10604
+ mkdir: async (path6, options) => {
10605
+ await nodeMkdir2(path6, options);
10606
+ },
10607
+ writeFile: async (path6, data) => {
10608
+ await nodeWriteFile2(path6, data);
10609
+ },
10610
+ rename: nodeRename,
10611
+ readFile: nodeReadFile2,
10612
+ rm: async (path6, options) => {
10613
+ await nodeRm(path6, options);
10614
+ }
10615
+ };
10616
+ function claimFileName(name) {
10617
+ return `${name}${OCCUPANCY_CLAIM.FILE_EXTENSION}`;
10618
+ }
10619
+ function claimFilePath(worktreesDir, name) {
10620
+ const validated = validateScopeToken(name);
10621
+ if (!validated.ok) return { ok: false, error: OCCUPANCY_ERROR.INVALID_NAME };
10622
+ return { ok: true, value: join30(worktreesDir, claimFileName(validated.value)) };
10623
+ }
10624
+ function classifyOccupancy(claim, probe) {
10625
+ if (claim === void 0) return OCCUPANCY_STATUS.UNCLAIMED;
10626
+ if (claim.host !== probe.currentHost()) return OCCUPANCY_STATUS.STALE;
10627
+ if (!probe.isAlive(claim.pid)) return OCCUPANCY_STATUS.STALE;
10628
+ const liveStartTime = probe.startTimeOf(claim.pid);
10629
+ if (liveStartTime !== void 0 && liveStartTime !== claim.startedAt) return OCCUPANCY_STATUS.STALE;
10630
+ return OCCUPANCY_STATUS.OCCUPIED;
10631
+ }
10632
+ async function writeClaim(worktreesDir, name, record, options = {}) {
10633
+ const fs7 = options.fs ?? defaultOccupancyFileSystem;
10634
+ const pathResult = claimFilePath(worktreesDir, name);
10635
+ if (!pathResult.ok) return pathResult;
10636
+ const claimPath = pathResult.value;
10637
+ const tempPath = `${claimPath}${OCCUPANCY_CLAIM.TEMP_EXTENSION}`;
10638
+ try {
10639
+ await fs7.mkdir(worktreesDir, { recursive: true });
10640
+ await fs7.writeFile(tempPath, serializeClaim(record));
10641
+ await fs7.rename(tempPath, claimPath);
10642
+ return { ok: true, value: claimPath };
10643
+ } catch (error) {
10644
+ return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_WRITE_FAILED, toErrorMessage3(error)) };
10645
+ }
10646
+ }
10647
+ async function readClaim(worktreesDir, name, options = {}) {
10648
+ const fs7 = options.fs ?? defaultOccupancyFileSystem;
10649
+ const pathResult = claimFilePath(worktreesDir, name);
10650
+ if (!pathResult.ok) return pathResult;
10651
+ let content;
10652
+ try {
10653
+ content = await fs7.readFile(pathResult.value, "utf8");
10654
+ } catch (error) {
10655
+ if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return { ok: true, value: void 0 };
10656
+ return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_READ_FAILED, toErrorMessage3(error)) };
10657
+ }
10658
+ return parseClaim(content);
10659
+ }
10660
+ async function removeClaim(worktreesDir, name, options = {}) {
10661
+ const fs7 = options.fs ?? defaultOccupancyFileSystem;
10662
+ const pathResult = claimFilePath(worktreesDir, name);
10663
+ if (!pathResult.ok) return pathResult;
10664
+ try {
10665
+ await fs7.rm(pathResult.value, { force: true });
10666
+ return { ok: true, value: void 0 };
10667
+ } catch (error) {
10668
+ return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_REMOVE_FAILED, toErrorMessage3(error)) };
10669
+ }
10670
+ }
10671
+ async function readOccupancy(worktreesDir, name, probe, options = {}) {
10672
+ const claimResult = await readClaim(worktreesDir, name, options);
10673
+ if (!claimResult.ok) return claimResult;
10674
+ return { ok: true, value: classifyOccupancy(claimResult.value, probe) };
10675
+ }
10676
+ function serializeClaim(record) {
10677
+ return JSON.stringify({
10678
+ sessionId: record.sessionId,
10679
+ host: record.host,
10680
+ pid: record.pid,
10681
+ startedAt: record.startedAt
10682
+ });
10683
+ }
10684
+ function parseClaim(content) {
10685
+ let parsed;
10686
+ try {
10687
+ parsed = JSON.parse(content);
10688
+ } catch {
10689
+ return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
10690
+ }
10691
+ if (!isWorktreeClaimRecord(parsed)) return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
10692
+ return { ok: true, value: parsed };
10693
+ }
10694
+ function isWorktreeClaimRecord(value) {
10695
+ if (typeof value !== "object" || value === null) return false;
10696
+ const candidate = value;
10697
+ return typeof candidate.sessionId === "string" && typeof candidate.host === "string" && typeof candidate.pid === "number" && typeof candidate.startedAt === "string";
10698
+ }
10699
+ function formatOccupancyError(code, detail) {
10700
+ return `${code}${ERROR_DETAIL_SEPARATOR2}${detail}`;
10701
+ }
10702
+ function toErrorMessage3(error) {
10703
+ return error instanceof Error ? error.message : String(error);
10704
+ }
10705
+
10706
+ // src/domains/worktree/process-table.ts
10707
+ import { spawnSync } from "child_process";
10708
+ import { hostname } from "os";
10709
+ var PS_COMMAND = "/bin/ps";
10710
+ var PS_FIELD = {
10711
+ START_TIME: "lstart",
10712
+ PARENT_PID: "ppid",
10713
+ // The full command line, not `comm` — `comm` reports only the executable
10714
+ // basename, so a shebang-installed agent run through an interpreter reads as
10715
+ // the interpreter (e.g. `node`) and the agent-runtime match misses it. `args`
10716
+ // carries the script path, so both native and interpreted agents are matched.
10717
+ COMMAND: "args"
10718
+ };
10719
+ var SIGNAL_LIVENESS_PROBE = 0;
10720
+ var PROCESS_EXISTS_NO_PERMISSION = "EPERM";
10721
+ var PID_RADIX2 = 10;
10722
+ var STABLE_PS_ENV = { ...process.env, TZ: "UTC", LC_ALL: "C" };
10723
+ function psField(pid, field) {
10724
+ const result = spawnSync(PS_COMMAND, ["-o", `${field}=`, "-p", String(pid)], {
10725
+ encoding: "utf8",
10726
+ env: STABLE_PS_ENV
10727
+ });
10728
+ if (result.status !== 0 || typeof result.stdout !== "string") return void 0;
10729
+ const value = result.stdout.trim();
10730
+ return value.length > 0 ? value : void 0;
10731
+ }
10732
+ var defaultProcessTable = {
10733
+ currentHost: () => hostname(),
10734
+ isAlive: (pid) => {
10735
+ try {
10736
+ process.kill(pid, SIGNAL_LIVENESS_PROBE);
10737
+ return true;
10738
+ } catch (error) {
10739
+ return hasErrorCode(error, PROCESS_EXISTS_NO_PERMISSION);
10740
+ }
10741
+ },
10742
+ startTimeOf: (pid) => psField(pid, PS_FIELD.START_TIME),
10743
+ parentOf: (pid) => {
10744
+ const value = psField(pid, PS_FIELD.PARENT_PID);
10745
+ if (value === void 0) return void 0;
10746
+ const parsed = Number.parseInt(value, PID_RADIX2);
10747
+ return Number.isInteger(parsed) ? parsed : void 0;
10748
+ },
10749
+ commandOf: (pid) => psField(pid, PS_FIELD.COMMAND)
10750
+ };
10751
+
10752
+ // src/commands/worktree/resolve.ts
10753
+ import { stat as stat4 } from "fs/promises";
10754
+ import { dirname as dirname8, resolve as resolve5 } from "path";
10755
+
10756
+ // src/domains/worktree/worktree-name.ts
10757
+ import { basename as basename4 } from "path";
10758
+ var SEPARATOR_CHARACTER = /[^a-z0-9_]/;
10759
+ var NAME_SEPARATOR = "-";
10760
+ function worktreeClaimName(worktreeRoot) {
10761
+ return basename4(worktreeRoot).toLowerCase().split(SEPARATOR_CHARACTER).filter((segment) => segment.length > 0).join(NAME_SEPARATOR);
10762
+ }
10763
+
10764
+ // src/commands/worktree/resolve.ts
10765
+ var WORKTREE_RESOLVE_ERROR = {
10766
+ NOT_A_WORKTREE: "path resolves to no worktree"
10767
+ };
10768
+ async function resolveWorktreesDir(options) {
10769
+ if (options.worktreesDir !== void 0) return options.worktreesDir;
10770
+ const resolved = await resolveWorktreesScopeDir({ cwd: options.cwd, deps: options.gitDeps });
10771
+ options.onWarning?.(resolved.warning);
10772
+ return resolved.worktreesDir;
10773
+ }
10774
+ async function resolveCurrentWorktreeName(options) {
10775
+ const worktree = await detectWorktreeProductRoot(options.cwd, options.gitDeps);
10776
+ return worktreeClaimName(worktree.productDir);
10777
+ }
10778
+ async function resolveTargetWorktree(options) {
10779
+ const base = options.cwd ?? process.cwd();
10780
+ const targetPath = options.worktree === void 0 ? base : resolve5(base, options.worktree);
10781
+ const targetGitPath = await isExistingNonDirectory(targetPath) ? dirname8(targetPath) : targetPath;
10782
+ const worktree = await detectWorktreeProductRoot(targetGitPath, options.gitDeps);
10783
+ if (!worktree.isGitRepo) {
10784
+ return { ok: false, error: `${WORKTREE_RESOLVE_ERROR.NOT_A_WORKTREE}: ${options.worktree ?? base}` };
10785
+ }
10786
+ return { ok: true, value: { name: worktreeClaimName(worktree.productDir), worktreeRoot: worktree.productDir } };
10787
+ }
10788
+ async function isExistingNonDirectory(path6) {
10789
+ try {
10790
+ const pathStats = await stat4(path6);
10791
+ return !pathStats.isDirectory();
10792
+ } catch {
10793
+ return false;
10794
+ }
10795
+ }
10796
+
10797
+ // src/commands/worktree/claim.ts
10798
+ async function claimCommand(options) {
10799
+ const table = options.processTable ?? defaultProcessTable;
10800
+ const controlling = resolveControllingProcess(options.selfPid ?? process.pid, table, options.env ?? process.env);
10801
+ if (!controlling.ok) return controlling;
10802
+ const worktreesDir = await resolveWorktreesDir(options);
10803
+ const name = await resolveCurrentWorktreeName(options);
10804
+ return writeClaim(
10805
+ worktreesDir,
10806
+ name,
10807
+ {
10808
+ sessionId: options.sessionId,
10809
+ host: controlling.value.host,
10810
+ pid: controlling.value.pid,
10811
+ startedAt: controlling.value.startedAt
10812
+ },
10813
+ { fs: options.fs }
10814
+ );
10815
+ }
10816
+
10817
+ // src/commands/worktree/release.ts
10818
+ async function releaseCommand2(options) {
10819
+ const worktreesDir = await resolveWorktreesDir(options);
10820
+ const name = await resolveCurrentWorktreeName(options);
10821
+ return removeClaim(worktreesDir, name, { fs: options.fs });
10822
+ }
10823
+
10824
+ // src/commands/worktree/status.ts
10825
+ var WORKTREE_STATUS_FORMAT = {
10826
+ JSON: "json",
10827
+ TEXT: "text"
10828
+ };
10829
+ async function statusCommand2(options) {
10830
+ const table = options.processTable ?? defaultProcessTable;
10831
+ const multiTargetRequest = options.worktrees !== void 0 && options.worktrees.length > 1;
10832
+ const targets = await resolveStatusTargets(options);
10833
+ if (!targets.ok) return targets;
10834
+ const records = [];
10835
+ for (const target of targets.value) {
10836
+ const worktreesDir = await resolveWorktreesDir({ ...options, cwd: target.worktreeRoot });
10837
+ const occupancy = await readOccupancy(worktreesDir, target.name, table, { fs: options.fs });
10838
+ if (!occupancy.ok) return occupancy;
10839
+ records.push({ worktree: target.name, status: occupancy.value });
10840
+ }
10841
+ return { ok: true, value: renderStatus(records, options.format, multiTargetRequest) };
10842
+ }
10843
+ async function resolveStatusTargets(options) {
10844
+ const requested = options.worktrees;
10845
+ if (requested === void 0 || requested.length === 0) {
10846
+ const target = await resolveTargetWorktree(options);
10847
+ if (!target.ok) return target;
10848
+ return { ok: true, value: [target.value] };
10849
+ }
10850
+ const targets = [];
10851
+ let firstError;
10852
+ for (const worktree of requested) {
10853
+ const target = await resolveTargetWorktree({ ...options, worktree });
10854
+ if (target.ok) {
10855
+ targets.push(target.value);
10856
+ } else {
10857
+ firstError ??= target.error;
10858
+ }
10859
+ }
10860
+ if (targets.length === 0) {
10861
+ return { ok: false, error: firstError ?? "no worktree status targets resolved" };
10862
+ }
10863
+ return { ok: true, value: targets };
10864
+ }
10865
+ function renderStatus(records, format2, multiTargetRequest) {
10866
+ if (format2 === WORKTREE_STATUS_FORMAT.JSON) {
10867
+ return JSON.stringify(multiTargetRequest ? records : records[0]);
10868
+ }
10869
+ return records.map(renderTextStatus).join("\n");
10870
+ }
10871
+ function renderTextStatus(record) {
10872
+ return `${record.worktree} ${record.status}`;
10873
+ }
10874
+
10875
+ // src/interfaces/cli/worktree.ts
10876
+ var WORKTREE_CLI = {
10877
+ COMMAND: "worktree",
10878
+ CLAIM: "claim",
10879
+ STATUS: "status",
10880
+ RELEASE: "release",
10881
+ WORKTREE_ARGUMENT: "[worktrees...]",
10882
+ SESSION_ID_FLAG: "--session-id",
10883
+ FORMAT_FLAG: "--format",
10884
+ WORKTREES_DIR_FLAG: "--worktrees-dir"
10885
+ };
10886
+ var WORKTREE_DOMAIN_DESCRIPTION = "Coordinate worktree occupancy across a bare-repository pool";
10887
+ function handleError2(error) {
10888
+ console.error("Error:", error);
10889
+ process.exit(1);
10890
+ }
10891
+ function registerWorktreeCommands(worktreeCmd) {
10892
+ worktreeCmd.command(WORKTREE_CLI.CLAIM).description("Record a worktree-occupancy claim for the running worktree").requiredOption(`${WORKTREE_CLI.SESSION_ID_FLAG} <id>`, "Claiming agent session id").option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (options) => {
10893
+ const result = await claimCommand({
10894
+ sessionId: options.sessionId,
10895
+ worktreesDir: options.worktreesDir,
10896
+ onWarning: writeWarning
10897
+ });
10898
+ if (!result.ok) handleError2(result.error);
10899
+ });
10900
+ worktreeCmd.command(`${WORKTREE_CLI.STATUS} ${WORKTREE_CLI.WORKTREE_ARGUMENT}`).description("Report a worktree's occupancy (occupied | unclaimed | stale)").option(`${WORKTREE_CLI.FORMAT_FLAG} <format>`, "Output format (text|json)", WORKTREE_STATUS_FORMAT.TEXT).option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (worktrees, options) => {
10901
+ const result = await statusCommand2({
10902
+ worktrees,
10903
+ format: options.format,
10904
+ worktreesDir: options.worktreesDir,
10905
+ onWarning: writeWarning
10906
+ });
10907
+ if (!result.ok) handleError2(result.error);
10908
+ console.log(result.value);
10909
+ });
10910
+ worktreeCmd.command(WORKTREE_CLI.RELEASE).description("Release the running worktree's occupancy claim").option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (options) => {
10911
+ const result = await releaseCommand2({ worktreesDir: options.worktreesDir, onWarning: writeWarning });
10912
+ if (!result.ok) handleError2(result.error);
10913
+ });
10914
+ }
10915
+ var worktreeDomain = {
10916
+ name: WORKTREE_CLI.COMMAND,
10917
+ description: WORKTREE_DOMAIN_DESCRIPTION,
10918
+ register: (program2) => {
10919
+ const worktreeCmd = program2.command(WORKTREE_CLI.COMMAND).description(WORKTREE_DOMAIN_DESCRIPTION);
10920
+ registerWorktreeCommands(worktreeCmd);
10921
+ }
10922
+ };
10923
+
10526
10924
  // src/interfaces/cli/registry.ts
10527
10925
  var CLI_DOMAINS = [
10528
10926
  claudeDomain,
@@ -10531,7 +10929,8 @@ var CLI_DOMAINS = [
10531
10929
  sessionDomain,
10532
10930
  specDomain,
10533
10931
  testingDomain,
10534
- validationDomain
10932
+ validationDomain,
10933
+ worktreeDomain
10535
10934
  ];
10536
10935
 
10537
10936
  // src/cli.ts