@outcomeeng/spx 0.5.4 → 0.5.6

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"
@@ -1065,11 +1066,13 @@ var STATE_STORE_ERROR = {
1065
1066
  RECORD_WRITE_FAILED: "state-store record write failed",
1066
1067
  RECORD_READ_FAILED: "state-store record read failed"
1067
1068
  };
1069
+ var STATE_STORE_RUN_TOKEN = {
1070
+ ID_BYTES: 6,
1071
+ TIMESTAMP_MILLISECOND_DIGITS: 3
1072
+ };
1068
1073
  var HEX_ENCODING = "hex";
1069
- var RUN_ID_BYTES = 6;
1070
1074
  var RUN_FILE_CREATE_ATTEMPTS = 10;
1071
1075
  var RUN_TIMESTAMP_SEPARATOR = "_";
1072
- var RUN_TIMESTAMP_MILLISECOND_DIGITS = 3;
1073
1076
  var SLUG_SEPARATOR = "-";
1074
1077
  var EMPTY_STRING = "";
1075
1078
  var RUN_TOKEN_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[a-f0-9]{12}$/;
@@ -1109,6 +1112,16 @@ function worktreeScopeDir(productDir) {
1109
1112
  function sessionsScopeDir(productDir) {
1110
1113
  return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
1111
1114
  }
1115
+ async function resolveWorktreesScopeDir(options = {}) {
1116
+ const gitResult = await detectGitCommonDirProductRoot(options.cwd, options.deps);
1117
+ return {
1118
+ worktreesDir: worktreesScopeDir(gitResult.productDir),
1119
+ warning: gitResult.warning
1120
+ };
1121
+ }
1122
+ function worktreesScopeDir(productDir) {
1123
+ return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.WORKTREES_SCOPE);
1124
+ }
1112
1125
  function composeScopeDir(baseScopeDir, ...tokens) {
1113
1126
  const segments = [];
1114
1127
  for (const token of tokens) {
@@ -1133,11 +1146,19 @@ function formatRunTimestamp(date) {
1133
1146
  const hours = String(date.getUTCHours()).padStart(2, "0");
1134
1147
  const minutes = String(date.getUTCMinutes()).padStart(2, "0");
1135
1148
  const seconds = String(date.getUTCSeconds()).padStart(2, "0");
1136
- const milliseconds = String(date.getUTCMilliseconds()).padStart(RUN_TIMESTAMP_MILLISECOND_DIGITS, "0");
1149
+ const milliseconds = String(date.getUTCMilliseconds()).padStart(
1150
+ STATE_STORE_RUN_TOKEN.TIMESTAMP_MILLISECOND_DIGITS,
1151
+ "0"
1152
+ );
1137
1153
  return `${year}-${month}-${day}${RUN_TIMESTAMP_SEPARATOR}${hours}-${minutes}-${seconds}-${milliseconds}`;
1138
1154
  }
1139
1155
  function generateRunId(randomBytes = nodeRandomBytes) {
1140
- return randomBytes(RUN_ID_BYTES).toString(HEX_ENCODING);
1156
+ return randomBytes(STATE_STORE_RUN_TOKEN.ID_BYTES).toString(HEX_ENCODING);
1157
+ }
1158
+ function createStateStoreRunToken(options) {
1159
+ const startedAt = formatRunTimestamp(options.date);
1160
+ const runId = generateRunId(options.randomBytes);
1161
+ return { runToken: `${startedAt}${SLUG_SEPARATOR}${runId}`, runId, startedAt };
1141
1162
  }
1142
1163
  function runFileName(runToken) {
1143
1164
  return `${STATE_STORE_PATH.RUN_FILE_PREFIX}${runToken}${STATE_STORE_PATH.JSONL_EXTENSION}`;
@@ -1154,7 +1175,6 @@ async function createJsonlRunFile(scopeDir, domainName, options = {}) {
1154
1175
  if (!domainRunsDir.ok) return domainRunsDir;
1155
1176
  const maxAttempts = options.maxAttempts ?? RUN_FILE_CREATE_ATTEMPTS;
1156
1177
  const startedDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
1157
- const startedAt = formatRunTimestamp(startedDate);
1158
1178
  const randomBytes = options.randomBytes ?? nodeRandomBytes;
1159
1179
  try {
1160
1180
  await fs7.mkdir(domainRunsDir.value, { recursive: true });
@@ -1165,15 +1185,14 @@ async function createJsonlRunFile(scopeDir, domainName, options = {}) {
1165
1185
  };
1166
1186
  }
1167
1187
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
1168
- const runId = generateRunId(randomBytes);
1169
- const runToken = `${startedAt}${SLUG_SEPARATOR}${runId}`;
1170
- const name = runFileName(runToken);
1188
+ const token = createStateStoreRunToken({ date: startedDate, randomBytes });
1189
+ const name = runFileName(token.runToken);
1171
1190
  const path6 = join2(domainRunsDir.value, name);
1172
1191
  try {
1173
1192
  await fs7.writeFile(path6, EMPTY_STRING, { flag: EXCLUSIVE_CREATE_FLAG });
1174
1193
  return {
1175
1194
  ok: true,
1176
- value: { runsDir: domainRunsDir.value, runFilePath: path6, runFileName: name, runToken, runId, startedAt }
1195
+ value: { runsDir: domainRunsDir.value, runFilePath: path6, runFileName: name, ...token }
1177
1196
  };
1178
1197
  } catch (error) {
1179
1198
  if (hasErrorCode(error, ERROR_CODE_FILE_EXISTS)) continue;
@@ -2371,11 +2390,6 @@ function knipAdapter(scope, config) {
2371
2390
  return scope.excluded.flatMap((entry) => [config.ignoreFlag, entry.path]);
2372
2391
  }
2373
2392
 
2374
- // src/lib/file-inclusion/adapters/madge.ts
2375
- function madgeAdapter(scope, config) {
2376
- return scope.excluded.flatMap((entry) => [config.ignoreFlag, entry.path]);
2377
- }
2378
-
2379
2393
  // src/lib/file-inclusion/adapters/markdownlint.ts
2380
2394
  function markdownlintAdapter(scope, config) {
2381
2395
  return scope.excluded.flatMap((entry) => [config.ignoreFlag, entry.path]);
@@ -2400,7 +2414,6 @@ function vitestAdapter(scope, config) {
2400
2414
  var TOOL_DEFAULT_FLAGS = {
2401
2415
  eslint: "--ignore-pattern",
2402
2416
  tsc: "--exclude",
2403
- madge: "--exclude",
2404
2417
  knip: "--exclude",
2405
2418
  markdownlint: "--ignore",
2406
2419
  pytest: "--ignore",
@@ -2409,7 +2422,6 @@ var TOOL_DEFAULT_FLAGS = {
2409
2422
  var ADAPTER_MAP = {
2410
2423
  eslint: eslintAdapter,
2411
2424
  tsc: tscAdapter,
2412
- madge: madgeAdapter,
2413
2425
  knip: knipAdapter,
2414
2426
  markdownlint: markdownlintAdapter,
2415
2427
  pytest: pytestAdapter,
@@ -2516,8 +2528,8 @@ var HIDDEN_PREFIX_LAYER = "hidden-prefix";
2516
2528
  var LAYER2 = HIDDEN_PREFIX_LAYER;
2517
2529
  function hiddenPrefixPredicate(path6, config) {
2518
2530
  const segments = path6.split("/");
2519
- const basename4 = segments[segments.length - 1] ?? "";
2520
- const matched = basename4.startsWith(config.hiddenPrefix);
2531
+ const basename5 = segments[segments.length - 1] ?? "";
2532
+ const matched = basename5.startsWith(config.hiddenPrefix);
2521
2533
  return { matched, layer: LAYER2 };
2522
2534
  }
2523
2535
 
@@ -3488,9 +3500,640 @@ var configDomain = {
3488
3500
  }
3489
3501
  };
3490
3502
 
3503
+ // src/lib/sanitize-cli-argument.ts
3504
+ var MAX_CLI_ARGUMENT_DISPLAY_LENGTH = 120;
3505
+ var ELLIPSIS_TOKEN = "...";
3506
+ var SENTINEL_UNDEFINED = "<undefined>";
3507
+ var SENTINEL_NULL = "<null>";
3508
+ var SENTINEL_EMPTY = "<empty>";
3509
+ var CONTROL_CHAR_UPPER_BOUND = 31;
3510
+ var DEL_CHAR_CODE = 127;
3511
+ var HEX_RADIX = 16;
3512
+ var HEX_PAD = 2;
3513
+ function formatHexEscape(code) {
3514
+ return `\\x${code.toString(HEX_RADIX).padStart(HEX_PAD, "0")}`;
3515
+ }
3516
+ function nonStringSentinel(type) {
3517
+ return `<non-string:${type}>`;
3518
+ }
3519
+ function sanitizeCliArgument(input) {
3520
+ if (input === void 0) return SENTINEL_UNDEFINED;
3521
+ if (input === null) return SENTINEL_NULL;
3522
+ if (typeof input !== "string") return nonStringSentinel(typeof input);
3523
+ if (input.length === 0) return SENTINEL_EMPTY;
3524
+ const escaped = escapeControlCharacters(input);
3525
+ return truncate(escaped);
3526
+ }
3527
+ function escapeControlCharacters(value) {
3528
+ let out = "";
3529
+ for (const char of value) {
3530
+ const code = char.codePointAt(0);
3531
+ if (code === void 0) continue;
3532
+ if (code <= CONTROL_CHAR_UPPER_BOUND || code === DEL_CHAR_CODE) {
3533
+ out += formatHexEscape(code);
3534
+ } else {
3535
+ out += char;
3536
+ }
3537
+ }
3538
+ return out;
3539
+ }
3540
+ function truncate(value) {
3541
+ if (value.length <= MAX_CLI_ARGUMENT_DISPLAY_LENGTH) return value;
3542
+ return value.slice(0, MAX_CLI_ARGUMENT_DISPLAY_LENGTH - ELLIPSIS_TOKEN.length) + ELLIPSIS_TOKEN;
3543
+ }
3544
+
3545
+ // src/interfaces/hooks/session-start.ts
3546
+ import { appendFile as nodeAppendFile2 } from "fs/promises";
3547
+
3548
+ // src/domains/worktree/occupancy-store.ts
3549
+ import { join as join6 } from "path";
3550
+ var OCCUPANCY_STATUS = {
3551
+ UNCLAIMED: "unclaimed",
3552
+ OCCUPIED: "occupied",
3553
+ STALE: "stale"
3554
+ };
3555
+ var OCCUPANCY_CLAIM = {
3556
+ FILE_EXTENSION: ".claim",
3557
+ TEMP_EXTENSION: ".tmp",
3558
+ UNREADABLE_STARTED_AT_PREFIX: "unreadable:"
3559
+ };
3560
+ var OCCUPANCY_ERROR = {
3561
+ INVALID_NAME: "worktree occupancy claim name must be a safe path segment",
3562
+ INVALID_WRITE_TOKEN: "worktree occupancy claim write token must be a safe path segment",
3563
+ CLAIM_WRITE_FAILED: "worktree occupancy claim write failed",
3564
+ CLAIM_READ_FAILED: "worktree occupancy claim read failed",
3565
+ CLAIM_REMOVE_FAILED: "worktree occupancy claim remove failed",
3566
+ CLAIM_MALFORMED: "worktree occupancy claim record is malformed"
3567
+ };
3568
+ var ERROR_DETAIL_SEPARATOR2 = ": ";
3569
+ function claimFileName(name) {
3570
+ return `${name}${OCCUPANCY_CLAIM.FILE_EXTENSION}`;
3571
+ }
3572
+ function claimFilePath(worktreesDir, name) {
3573
+ const validated = validateScopeToken(name);
3574
+ if (!validated.ok) return { ok: false, error: OCCUPANCY_ERROR.INVALID_NAME };
3575
+ return { ok: true, value: join6(worktreesDir, claimFileName(validated.value)) };
3576
+ }
3577
+ function claimTempFilePath(claimPath, writeToken) {
3578
+ const validated = validateScopeToken(writeToken);
3579
+ if (!validated.ok) return { ok: false, error: OCCUPANCY_ERROR.INVALID_WRITE_TOKEN };
3580
+ return { ok: true, value: `${claimPath}.${validated.value}${OCCUPANCY_CLAIM.TEMP_EXTENSION}` };
3581
+ }
3582
+ function classifyOccupancy(claim, probe) {
3583
+ if (claim === void 0) return OCCUPANCY_STATUS.UNCLAIMED;
3584
+ if (claim.host !== probe.currentHost()) return OCCUPANCY_STATUS.STALE;
3585
+ if (!probe.isAlive(claim.pid)) return OCCUPANCY_STATUS.STALE;
3586
+ const liveStartTime = probe.startTimeOf(claim.pid);
3587
+ if (claim.startedAt === unreadableStartedAt(claim.pid)) return OCCUPANCY_STATUS.OCCUPIED;
3588
+ if (liveStartTime !== void 0 && liveStartTime !== claim.startedAt) return OCCUPANCY_STATUS.STALE;
3589
+ return OCCUPANCY_STATUS.OCCUPIED;
3590
+ }
3591
+ function unreadableStartedAt(pid) {
3592
+ return `${OCCUPANCY_CLAIM.UNREADABLE_STARTED_AT_PREFIX}${pid}`;
3593
+ }
3594
+ async function writeClaim(worktreesDir, name, record, options) {
3595
+ const pathResult = claimFilePath(worktreesDir, name);
3596
+ if (!pathResult.ok) return pathResult;
3597
+ const claimPath = pathResult.value;
3598
+ const tempPath = claimTempFilePath(claimPath, options.writeToken);
3599
+ if (!tempPath.ok) return tempPath;
3600
+ try {
3601
+ await options.fs.mkdir(worktreesDir, { recursive: true });
3602
+ await options.fs.writeFile(tempPath.value, serializeClaim(record));
3603
+ await options.fs.rename(tempPath.value, claimPath);
3604
+ return { ok: true, value: claimPath };
3605
+ } catch (error) {
3606
+ return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_WRITE_FAILED, toErrorMessage2(error)) };
3607
+ }
3608
+ }
3609
+ async function readClaim(worktreesDir, name, options) {
3610
+ const pathResult = claimFilePath(worktreesDir, name);
3611
+ if (!pathResult.ok) return pathResult;
3612
+ let content;
3613
+ try {
3614
+ content = await options.fs.readFile(pathResult.value, "utf8");
3615
+ } catch (error) {
3616
+ if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return { ok: true, value: void 0 };
3617
+ return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_READ_FAILED, toErrorMessage2(error)) };
3618
+ }
3619
+ return parseClaim(content);
3620
+ }
3621
+ async function removeClaim(worktreesDir, name, options) {
3622
+ const pathResult = claimFilePath(worktreesDir, name);
3623
+ if (!pathResult.ok) return pathResult;
3624
+ try {
3625
+ await options.fs.rm(pathResult.value, { force: true });
3626
+ return { ok: true, value: void 0 };
3627
+ } catch (error) {
3628
+ return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_REMOVE_FAILED, toErrorMessage2(error)) };
3629
+ }
3630
+ }
3631
+ async function readOccupancy(worktreesDir, name, probe, options) {
3632
+ const claimResult = await readClaim(worktreesDir, name, options);
3633
+ if (!claimResult.ok) return claimResult;
3634
+ return { ok: true, value: classifyOccupancy(claimResult.value, probe) };
3635
+ }
3636
+ function serializeClaim(record) {
3637
+ return JSON.stringify({
3638
+ sessionId: record.sessionId,
3639
+ host: record.host,
3640
+ pid: record.pid,
3641
+ startedAt: record.startedAt
3642
+ });
3643
+ }
3644
+ function parseClaim(content) {
3645
+ let parsed;
3646
+ try {
3647
+ parsed = JSON.parse(content);
3648
+ } catch {
3649
+ return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
3650
+ }
3651
+ if (!isWorktreeClaimRecord(parsed)) return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
3652
+ return { ok: true, value: parsed };
3653
+ }
3654
+ function isWorktreeClaimRecord(value) {
3655
+ if (typeof value !== "object" || value === null) return false;
3656
+ const candidate = value;
3657
+ return typeof candidate.sessionId === "string" && typeof candidate.host === "string" && typeof candidate.pid === "number" && typeof candidate.startedAt === "string";
3658
+ }
3659
+ function formatOccupancyError(code, detail) {
3660
+ return `${code}${ERROR_DETAIL_SEPARATOR2}${detail}`;
3661
+ }
3662
+ function toErrorMessage2(error) {
3663
+ return error instanceof Error ? error.message : String(error);
3664
+ }
3665
+
3666
+ // src/domains/worktree/controlling-process.ts
3667
+ var CONTROLLING_PID_ENV = "SPX_WORKTREE_CONTROLLING_PID";
3668
+ var AGENT_RUNTIME_NAMES = ["claude", "codex"];
3669
+ var AGENT_COMMAND_PATTERN = new RegExp(`\\b(?:${AGENT_RUNTIME_NAMES.join("|")})\\b`, "i");
3670
+ var CONTROLLING_PROCESS_ERROR = {
3671
+ UNRESOLVED: "worktree controlling process could not be resolved"
3672
+ };
3673
+ var MAX_ANCESTRY_DEPTH = 64;
3674
+ var MIN_VALID_PID = 1;
3675
+ var PID_RADIX = 10;
3676
+ function resolveControllingProcess(selfPid, table, env) {
3677
+ const host = table.currentHost();
3678
+ for (const pid of controllingPidCandidates(selfPid, table, env)) {
3679
+ const startedAt = table.startTimeOf(pid);
3680
+ if (startedAt !== void 0) return { ok: true, value: { pid, startedAt, host } };
3681
+ if (table.isAlive(pid)) return { ok: true, value: { pid, startedAt: unreadableStartedAt(pid), host } };
3682
+ }
3683
+ return { ok: false, error: CONTROLLING_PROCESS_ERROR.UNRESOLVED };
3684
+ }
3685
+ function* controllingPidCandidates(selfPid, table, env) {
3686
+ const override = parsePid(env[CONTROLLING_PID_ENV]);
3687
+ if (override !== void 0) yield override;
3688
+ const agent = findAgentAncestor(selfPid, table);
3689
+ if (agent !== void 0) yield agent;
3690
+ const parent = table.parentOf(selfPid);
3691
+ if (parent !== void 0 && isValidPid(parent)) yield parent;
3692
+ }
3693
+ function findAgentAncestor(selfPid, table) {
3694
+ let pid = table.parentOf(selfPid);
3695
+ for (let depth = 0; pid !== void 0 && isValidPid(pid) && depth < MAX_ANCESTRY_DEPTH; depth += 1) {
3696
+ const command = table.commandOf(pid);
3697
+ if (command !== void 0 && AGENT_COMMAND_PATTERN.test(command)) return pid;
3698
+ pid = table.parentOf(pid);
3699
+ }
3700
+ return void 0;
3701
+ }
3702
+ function parsePid(value) {
3703
+ if (value === void 0 || value.length === 0) return void 0;
3704
+ const parsed = Number.parseInt(value, PID_RADIX);
3705
+ return isValidPid(parsed) ? parsed : void 0;
3706
+ }
3707
+ function isValidPid(pid) {
3708
+ return Number.isInteger(pid) && pid >= MIN_VALID_PID;
3709
+ }
3710
+
3711
+ // src/domains/worktree/resolve.ts
3712
+ import { dirname as dirname3, resolve as resolve3 } from "path";
3713
+
3714
+ // src/domains/worktree/worktree-name.ts
3715
+ import { basename as basename2 } from "path";
3716
+ var SEPARATOR_CHARACTER = /[^a-z0-9_]/;
3717
+ var NAME_SEPARATOR = "-";
3718
+ function worktreeClaimName(worktreeRoot) {
3719
+ return basename2(worktreeRoot).toLowerCase().split(SEPARATOR_CHARACTER).filter((segment) => segment.length > 0).join(NAME_SEPARATOR);
3720
+ }
3721
+
3722
+ // src/domains/worktree/resolve.ts
3723
+ var WORKTREE_RESOLVE_ERROR = {
3724
+ NOT_A_WORKTREE: "path resolves to no worktree"
3725
+ };
3726
+ async function resolveWorktreesDir(options) {
3727
+ if (options.worktreesDir !== void 0) return options.worktreesDir;
3728
+ const resolved = await resolveWorktreesScopeDir({ cwd: options.cwd, deps: options.gitDeps });
3729
+ options.onWarning?.(resolved.warning);
3730
+ return resolved.worktreesDir;
3731
+ }
3732
+ async function resolveCurrentWorktreeName(options) {
3733
+ const worktree = await detectWorktreeProductRoot(options.cwd, options.gitDeps);
3734
+ return worktreeClaimName(worktree.productDir);
3735
+ }
3736
+ async function resolveTargetWorktree(options) {
3737
+ const base = options.cwd;
3738
+ const targetPath = options.worktree === void 0 ? base : resolve3(base, options.worktree);
3739
+ const targetGitPath = await options.pathInfo.isExistingNonDirectory(targetPath) ? dirname3(targetPath) : targetPath;
3740
+ const worktree = await detectWorktreeProductRoot(targetGitPath, options.gitDeps);
3741
+ if (!worktree.isGitRepo) {
3742
+ return { ok: false, error: `${WORKTREE_RESOLVE_ERROR.NOT_A_WORKTREE}: ${options.worktree ?? base}` };
3743
+ }
3744
+ return { ok: true, value: { name: worktreeClaimName(worktree.productDir), worktreeRoot: worktree.productDir } };
3745
+ }
3746
+
3747
+ // src/domains/worktree/claim.ts
3748
+ async function claimWorktreeOccupancy(options) {
3749
+ const controlling = resolveControllingProcess(options.selfPid, options.processTable, options.env);
3750
+ if (!controlling.ok) return controlling;
3751
+ const worktreesDir = await resolveWorktreesDir(options);
3752
+ const name = await resolveCurrentWorktreeName(options);
3753
+ return writeClaim(
3754
+ worktreesDir,
3755
+ name,
3756
+ {
3757
+ sessionId: options.sessionId,
3758
+ host: controlling.value.host,
3759
+ pid: controlling.value.pid,
3760
+ startedAt: controlling.value.startedAt
3761
+ },
3762
+ { fs: options.fs, writeToken: options.claimWriteToken }
3763
+ );
3764
+ }
3765
+
3766
+ // src/domains/hooks/session-start.ts
3767
+ var HOOK_SESSION_START_PAYLOAD = {
3768
+ CWD: "cwd",
3769
+ SESSION_ID: "session_id"
3770
+ };
3771
+ var HOOK_SESSION_START_ENV = {
3772
+ CLAUDE_ENV_FILE: "CLAUDE_ENV_FILE",
3773
+ CLAUDE_PROJECT_DIR: "CLAUDE_PROJECT_DIR",
3774
+ CLAUDE_SESSION_ID: "CLAUDE_SESSION_ID",
3775
+ CLAUDE_WORKTREE_CLAIMED: "CLAUDE_WORKTREE_CLAIMED",
3776
+ CODEX_THREAD_ID: "CODEX_THREAD_ID",
3777
+ PROJECT_DIR: "PROJECT_DIR"
3778
+ };
3779
+ var HOOK_SESSION_START_CLAIMED = {
3780
+ FALSE: "0",
3781
+ TRUE: "1"
3782
+ };
3783
+ var HOOK_ENV_FILE = {
3784
+ ENCODING: "utf8",
3785
+ EXPORT_PREFIX: "export "
3786
+ };
3787
+ var HOOK_SESSION_START_ERROR = {
3788
+ ENV_FILE_WRITE_FAILED: "hook session-start env file write failed",
3789
+ PAYLOAD_MALFORMED: "hook session-start payload must be a JSON object"
3790
+ };
3791
+ var ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
3792
+ var SAFE_SHELL_VALUE_PATTERN = /^[A-Za-z0-9_@%+=:,.-]+$/;
3793
+ var SINGLE_QUOTE = "'";
3794
+ var SHELL_SINGLE_QUOTE_ESCAPE = `'"'"'`;
3795
+ var ENV_FILE_HEADER = "# Managed by spx hook run session-start";
3796
+ var LINE_SEPARATOR = "\n";
3797
+ function parseHookSessionStartPayload(content) {
3798
+ if (content === void 0 || content.trim().length === 0) return { ok: true, value: {} };
3799
+ let parsed;
3800
+ try {
3801
+ parsed = JSON.parse(content);
3802
+ } catch {
3803
+ return { ok: false, error: HOOK_SESSION_START_ERROR.PAYLOAD_MALFORMED };
3804
+ }
3805
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3806
+ return { ok: false, error: HOOK_SESSION_START_ERROR.PAYLOAD_MALFORMED };
3807
+ }
3808
+ const record = parsed;
3809
+ return {
3810
+ ok: true,
3811
+ value: {
3812
+ cwd: nonEmptyString(record[HOOK_SESSION_START_PAYLOAD.CWD]),
3813
+ sessionId: nonEmptyString(record[HOOK_SESSION_START_PAYLOAD.SESSION_ID])
3814
+ }
3815
+ };
3816
+ }
3817
+ function resolveHookSessionStartSessionId(payload, env) {
3818
+ return payload.sessionId ?? nonEmptyString(env[HOOK_SESSION_START_ENV.CLAUDE_SESSION_ID]) ?? nonEmptyString(env[HOOK_SESSION_START_ENV.CODEX_THREAD_ID]);
3819
+ }
3820
+ function resolveHookSessionStartProductDir(payload, cwd) {
3821
+ return payload.cwd ?? cwd;
3822
+ }
3823
+ function resolveHookSessionStartEnvFile(env, explicitEnvFile) {
3824
+ return nonEmptyString(explicitEnvFile) ?? nonEmptyString(env[HOOK_SESSION_START_ENV.CLAUDE_ENV_FILE]);
3825
+ }
3826
+ function renderHookSessionStartEnvFile(input) {
3827
+ return [
3828
+ "",
3829
+ ENV_FILE_HEADER,
3830
+ ...input.sessionId === void 0 ? [] : [renderExportLine(HOOK_SESSION_START_ENV.CLAUDE_SESSION_ID, input.sessionId)],
3831
+ renderExportLine(HOOK_SESSION_START_ENV.CLAUDE_PROJECT_DIR, input.productDir),
3832
+ renderExportLine(HOOK_SESSION_START_ENV.PROJECT_DIR, input.productDir),
3833
+ renderExportLine(
3834
+ HOOK_SESSION_START_ENV.CLAUDE_WORKTREE_CLAIMED,
3835
+ input.claimed ? HOOK_SESSION_START_CLAIMED.TRUE : HOOK_SESSION_START_CLAIMED.FALSE
3836
+ ),
3837
+ ""
3838
+ ].join(LINE_SEPARATOR);
3839
+ }
3840
+ function renderExportLine(name, value) {
3841
+ if (!ENV_NAME_PATTERN.test(name)) throw new Error(`invalid environment variable name: ${name}`);
3842
+ return `${HOOK_ENV_FILE.EXPORT_PREFIX}${name}=${shellQuote(value)}`;
3843
+ }
3844
+ function shellQuote(value) {
3845
+ if (SAFE_SHELL_VALUE_PATTERN.test(value)) return value;
3846
+ return `${SINGLE_QUOTE}${value.replaceAll(SINGLE_QUOTE, SHELL_SINGLE_QUOTE_ESCAPE)}${SINGLE_QUOTE}`;
3847
+ }
3848
+ function nonEmptyString(value) {
3849
+ if (typeof value !== "string") return void 0;
3850
+ const trimmed = value.trim();
3851
+ return trimmed.length === 0 ? void 0 : trimmed;
3852
+ }
3853
+
3854
+ // src/interfaces/hooks/session-start.ts
3855
+ var defaultHookEnvFileSystem = {
3856
+ appendFile: async (path6, data, encoding) => {
3857
+ await nodeAppendFile2(path6, data, encoding);
3858
+ }
3859
+ };
3860
+ var ERROR_DETAIL_SEPARATOR3 = ": ";
3861
+ var EMPTY_HOOK_STDOUT = "";
3862
+ async function runSessionStartHook(options) {
3863
+ const diagnostics = [];
3864
+ const payloadResult = parseHookSessionStartPayload(options.content);
3865
+ if (!payloadResult.ok) {
3866
+ diagnostics.push(payloadResult.error);
3867
+ }
3868
+ const payload = payloadResult.ok ? payloadResult.value : {};
3869
+ const productDir = resolveHookSessionStartProductDir(payload, options.cwd);
3870
+ const sessionId = resolveHookSessionStartSessionId(payload, options.env);
3871
+ let claimed = false;
3872
+ if (sessionId !== void 0) {
3873
+ const claim = await claimWorktreeOccupancy({
3874
+ ...options,
3875
+ cwd: productDir,
3876
+ sessionId
3877
+ });
3878
+ if (claim.ok) {
3879
+ claimed = true;
3880
+ } else {
3881
+ diagnostics.push(claim.error);
3882
+ }
3883
+ }
3884
+ const envFile = resolveHookSessionStartEnvFile(options.env, options.envFile);
3885
+ const envFileWritten = await writeEnvFileIfConfigured({
3886
+ claimed,
3887
+ envFile,
3888
+ productDir,
3889
+ sessionId,
3890
+ envFileSystem: options.envFileSystem ?? defaultHookEnvFileSystem,
3891
+ diagnostics
3892
+ });
3893
+ return {
3894
+ ok: true,
3895
+ value: {
3896
+ claimed,
3897
+ diagnostics,
3898
+ envFileWritten,
3899
+ productDir,
3900
+ stdout: EMPTY_HOOK_STDOUT,
3901
+ ...sessionId === void 0 ? {} : { sessionId }
3902
+ }
3903
+ };
3904
+ }
3905
+ async function writeEnvFileIfConfigured(options) {
3906
+ if (options.envFile === void 0) return false;
3907
+ try {
3908
+ await options.envFileSystem.appendFile(
3909
+ options.envFile,
3910
+ renderHookSessionStartEnvFile({
3911
+ claimed: options.claimed,
3912
+ productDir: options.productDir,
3913
+ sessionId: options.sessionId
3914
+ }),
3915
+ HOOK_ENV_FILE.ENCODING
3916
+ );
3917
+ return true;
3918
+ } catch (error) {
3919
+ options.diagnostics.push(
3920
+ `${HOOK_SESSION_START_ERROR.ENV_FILE_WRITE_FAILED}${ERROR_DETAIL_SEPARATOR3}${error instanceof Error ? error.message : String(error)}`
3921
+ );
3922
+ return false;
3923
+ }
3924
+ }
3925
+
3926
+ // src/interfaces/hooks/registry.ts
3927
+ var HOOK_EVENT = {
3928
+ SESSION_START: "session-start"
3929
+ };
3930
+ var HOOK_ERROR = {
3931
+ UNKNOWN_EVENT: "unknown hook event"
3932
+ };
3933
+ function isHookEvent(event) {
3934
+ return Object.values(HOOK_EVENT).includes(event);
3935
+ }
3936
+ async function runHookEvent(options) {
3937
+ if (options.event === HOOK_EVENT.SESSION_START) return runSessionStartHook(options);
3938
+ return { ok: false, error: `${HOOK_ERROR.UNKNOWN_EVENT}: ${options.event}` };
3939
+ }
3940
+
3941
+ // src/interfaces/hooks/cli-runner.ts
3942
+ var HOOK_PROCESS_IO_EVENT = {
3943
+ DATA: "data",
3944
+ END: "end",
3945
+ ERROR: "error"
3946
+ };
3947
+ var LINE_SEPARATOR2 = "\n";
3948
+ var ERROR_DETAIL_SEPARATOR4 = ": ";
3949
+ var STDIN_READ_ERROR = "hook stdin read failed";
3950
+ async function runHookCli(options) {
3951
+ if (!isHookEvent(options.event)) {
3952
+ options.io.writeStderr(`${HOOK_ERROR.UNKNOWN_EVENT}: ${sanitizeCliArgument(options.event)}`);
3953
+ return { ok: false, error: HOOK_ERROR.UNKNOWN_EVENT };
3954
+ }
3955
+ const diagnostics = [];
3956
+ const stdin = await options.io.readStdin();
3957
+ const content = stdin.ok ? stdin.value : void 0;
3958
+ if (!stdin.ok) diagnostics.push(stdin.error);
3959
+ const runEvent = options.runEvent ?? runHookEvent;
3960
+ const result = await runEvent({
3961
+ claimWriteToken: options.claimWriteToken,
3962
+ content,
3963
+ cwd: options.cwd,
3964
+ env: options.env,
3965
+ envFile: options.envFile,
3966
+ event: options.event,
3967
+ fs: options.fs,
3968
+ gitDeps: options.gitDeps,
3969
+ onWarning: options.onWarning,
3970
+ processTable: options.processTable,
3971
+ selfPid: options.selfPid,
3972
+ worktreesDir: options.worktreesDir
3973
+ });
3974
+ for (const diagnostic of diagnostics) {
3975
+ options.io.writeStderr(diagnostic);
3976
+ }
3977
+ if (!result.ok) {
3978
+ options.io.writeStderr(result.error);
3979
+ return result;
3980
+ }
3981
+ for (const diagnostic of result.value.diagnostics) {
3982
+ options.io.writeStderr(diagnostic);
3983
+ }
3984
+ if (result.value.stdout.length > 0) {
3985
+ options.io.writeStdout(result.value.stdout);
3986
+ }
3987
+ return { ok: true, value: void 0 };
3988
+ }
3989
+ function createProcessHookIo(streams) {
3990
+ return {
3991
+ readStdin: async () => {
3992
+ if (streams.stdin.isTTY) return { ok: true, value: void 0 };
3993
+ return new Promise((resolve6) => {
3994
+ let data = "";
3995
+ streams.stdin.setEncoding("utf-8");
3996
+ streams.stdin.on(HOOK_PROCESS_IO_EVENT.DATA, (chunk) => {
3997
+ data += chunk;
3998
+ });
3999
+ streams.stdin.on(HOOK_PROCESS_IO_EVENT.END, () => {
4000
+ resolve6({ ok: true, value: data.length === 0 ? void 0 : data });
4001
+ });
4002
+ streams.stdin.on(HOOK_PROCESS_IO_EVENT.ERROR, (error) => {
4003
+ resolve6({ ok: false, error: formatStdinReadError(error) });
4004
+ });
4005
+ });
4006
+ },
4007
+ writeStdout: (content) => {
4008
+ streams.stdout.write(`${content}${LINE_SEPARATOR2}`);
4009
+ },
4010
+ writeStderr: (content) => {
4011
+ streams.stderr.write(`${content}${LINE_SEPARATOR2}`);
4012
+ }
4013
+ };
4014
+ }
4015
+ function formatStdinReadError(error) {
4016
+ return `${STDIN_READ_ERROR}${ERROR_DETAIL_SEPARATOR4}${error instanceof Error ? error.message : String(error)}`;
4017
+ }
4018
+ var processHookIo = createProcessHookIo({
4019
+ stdin: process.stdin,
4020
+ stdout: process.stdout,
4021
+ stderr: process.stderr
4022
+ });
4023
+
4024
+ // src/lib/worktree-claim-write-token.ts
4025
+ import { randomBytes as nodeRandomBytes2 } from "crypto";
4026
+ var CLAIM_WRITE_TOKEN_BYTES = 8;
4027
+ function createClaimWriteToken(randomBytes = nodeRandomBytes2) {
4028
+ return randomBytes(CLAIM_WRITE_TOKEN_BYTES).toString("hex");
4029
+ }
4030
+
4031
+ // src/lib/worktree-occupancy-file-system.ts
4032
+ import {
4033
+ mkdir as nodeMkdir2,
4034
+ readFile as nodeReadFile2,
4035
+ rename as nodeRename,
4036
+ rm as nodeRm,
4037
+ writeFile as nodeWriteFile2
4038
+ } from "fs/promises";
4039
+ var defaultOccupancyFileSystem = {
4040
+ mkdir: async (path6, options) => {
4041
+ await nodeMkdir2(path6, options);
4042
+ },
4043
+ writeFile: async (path6, data) => {
4044
+ await nodeWriteFile2(path6, data);
4045
+ },
4046
+ rename: nodeRename,
4047
+ readFile: nodeReadFile2,
4048
+ rm: async (path6, options) => {
4049
+ await nodeRm(path6, options);
4050
+ }
4051
+ };
4052
+
4053
+ // src/lib/worktree-process-table.ts
4054
+ import { spawnSync } from "child_process";
4055
+ import { hostname } from "os";
4056
+ var PS_COMMAND = "/bin/ps";
4057
+ var PS_FIELD = {
4058
+ START_TIME: "lstart",
4059
+ PARENT_PID: "ppid",
4060
+ // The full command line, not `comm`: interpreted agent runtimes otherwise
4061
+ // report only the interpreter basename and miss the agent-command match.
4062
+ COMMAND: "args"
4063
+ };
4064
+ var SIGNAL_LIVENESS_PROBE = 0;
4065
+ var PROCESS_EXISTS_NO_PERMISSION = "EPERM";
4066
+ var PID_RADIX2 = 10;
4067
+ var STABLE_PS_ENV = { ...process.env, TZ: "UTC", LC_ALL: "C" };
4068
+ function psField(pid, field) {
4069
+ const result = spawnSync(PS_COMMAND, ["-o", `${field}=`, "-p", String(pid)], {
4070
+ encoding: "utf8",
4071
+ env: STABLE_PS_ENV
4072
+ });
4073
+ if (result.status !== 0 || typeof result.stdout !== "string") return void 0;
4074
+ const value = result.stdout.trim();
4075
+ return value.length > 0 ? value : void 0;
4076
+ }
4077
+ var defaultProcessTable = {
4078
+ currentHost: () => hostname(),
4079
+ isAlive: (pid) => {
4080
+ try {
4081
+ process.kill(pid, SIGNAL_LIVENESS_PROBE);
4082
+ return true;
4083
+ } catch (error) {
4084
+ return hasErrorCode(error, PROCESS_EXISTS_NO_PERMISSION);
4085
+ }
4086
+ },
4087
+ startTimeOf: (pid) => psField(pid, PS_FIELD.START_TIME),
4088
+ parentOf: (pid) => {
4089
+ const value = psField(pid, PS_FIELD.PARENT_PID);
4090
+ if (value === void 0) return void 0;
4091
+ const parsed = Number.parseInt(value, PID_RADIX2);
4092
+ return Number.isInteger(parsed) ? parsed : void 0;
4093
+ },
4094
+ commandOf: (pid) => psField(pid, PS_FIELD.COMMAND)
4095
+ };
4096
+
4097
+ // src/interfaces/cli/hook.ts
4098
+ var HOOK_CLI = {
4099
+ COMMAND: "hook",
4100
+ RUN: "run",
4101
+ EVENT_ARGUMENT: "<event>",
4102
+ ENV_FILE_FLAG: "--hook-env-file",
4103
+ WORKTREES_DIR_FLAG: "--worktrees-dir"
4104
+ };
4105
+ var HOOK_DOMAIN_DESCRIPTION = "Run host lifecycle hook events";
4106
+ function registerHookCommands(hookCmd) {
4107
+ hookCmd.command(`${HOOK_CLI.RUN} ${HOOK_CLI.EVENT_ARGUMENT}`).description("Run a hook lifecycle event").option(`${HOOK_CLI.ENV_FILE_FLAG} <path>`, "Hook env file to append; defaults to $CLAUDE_ENV_FILE").option(`${HOOK_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (event, options) => {
4108
+ const result = await runHookCli({
4109
+ claimWriteToken: createClaimWriteToken(),
4110
+ cwd: process.cwd(),
4111
+ env: process.env,
4112
+ envFile: options.hookEnvFile,
4113
+ event,
4114
+ fs: defaultOccupancyFileSystem,
4115
+ gitDeps: defaultGitDependencies,
4116
+ io: processHookIo,
4117
+ onWarning: writeWarning,
4118
+ processTable: defaultProcessTable,
4119
+ selfPid: process.pid,
4120
+ worktreesDir: options.worktreesDir
4121
+ });
4122
+ if (!result.ok) process.exit(1);
4123
+ });
4124
+ }
4125
+ var hookDomain = {
4126
+ name: HOOK_CLI.COMMAND,
4127
+ description: HOOK_DOMAIN_DESCRIPTION,
4128
+ register: (program2) => {
4129
+ const hookCmd = program2.command(HOOK_CLI.COMMAND).description(HOOK_DOMAIN_DESCRIPTION);
4130
+ registerHookCommands(hookCmd);
4131
+ }
4132
+ };
4133
+
3491
4134
  // src/commands/session/archive.ts
3492
4135
  import { mkdir, rename, stat } from "fs/promises";
3493
- import { dirname as dirname3, join as join7 } from "path";
4136
+ import { dirname as dirname4, join as join8 } from "path";
3494
4137
 
3495
4138
  // src/domains/session/archive.ts
3496
4139
  var SESSION_FILE_EXTENSION = ".md";
@@ -3722,7 +4365,7 @@ var SessionInvalidJsonHeaderError = class extends SessionError {
3722
4365
  };
3723
4366
 
3724
4367
  // src/commands/session/resolve-config.ts
3725
- import { join as join6 } from "path";
4368
+ import { join as join7 } from "path";
3726
4369
 
3727
4370
  // src/config/defaults.ts
3728
4371
  var DEFAULT_CONFIG = {
@@ -3742,18 +4385,18 @@ async function resolveSessionConfig(options = {}) {
3742
4385
  if (sessionsDir) {
3743
4386
  return {
3744
4387
  config: {
3745
- todoDir: join6(sessionsDir, statusDirs2.todo),
3746
- doingDir: join6(sessionsDir, statusDirs2.doing),
3747
- archiveDir: join6(sessionsDir, statusDirs2.archive)
4388
+ todoDir: join7(sessionsDir, statusDirs2.todo),
4389
+ doingDir: join7(sessionsDir, statusDirs2.doing),
4390
+ archiveDir: join7(sessionsDir, statusDirs2.archive)
3748
4391
  }
3749
4392
  };
3750
4393
  }
3751
4394
  const { sessionsDir: baseDir, warning } = await resolveSessionsScopeDir({ cwd, deps });
3752
4395
  return {
3753
4396
  config: {
3754
- todoDir: join6(baseDir, statusDirs2.todo),
3755
- doingDir: join6(baseDir, statusDirs2.doing),
3756
- archiveDir: join6(baseDir, statusDirs2.archive)
4397
+ todoDir: join7(baseDir, statusDirs2.todo),
4398
+ doingDir: join7(baseDir, statusDirs2.doing),
4399
+ archiveDir: join7(baseDir, statusDirs2.archive)
3757
4400
  },
3758
4401
  warning
3759
4402
  };
@@ -3790,9 +4433,9 @@ async function fileExists(path6) {
3790
4433
  }
3791
4434
  async function probeSessionPaths(sessionId, config) {
3792
4435
  const filename = `${sessionId}${SESSION_FILE_EXTENSION}`;
3793
- const todoPath = join7(config.todoDir, filename);
3794
- const doingPath = join7(config.doingDir, filename);
3795
- const archivePath = join7(config.archiveDir, filename);
4436
+ const todoPath = join8(config.todoDir, filename);
4437
+ const doingPath = join8(config.doingDir, filename);
4438
+ const archivePath = join8(config.archiveDir, filename);
3796
4439
  return {
3797
4440
  todo: await fileExists(todoPath) ? todoPath : null,
3798
4441
  doing: await fileExists(doingPath) ? doingPath : null,
@@ -3813,7 +4456,7 @@ async function resolveArchivePaths(sessionId, config) {
3813
4456
  }
3814
4457
  async function archiveSingle(sessionId, config) {
3815
4458
  const { source, target } = await resolveArchivePaths(sessionId, config);
3816
- await mkdir(dirname3(target), { recursive: true });
4459
+ await mkdir(dirname4(target), { recursive: true });
3817
4460
  await rename(source, target);
3818
4461
  return `${SESSION_ARCHIVE_OUTPUT.ARCHIVED}: ${sessionId}
3819
4462
  ${SESSION_ARCHIVE_OUTPUT.ARCHIVE_LOCATION}: ${target}`;
@@ -3836,7 +4479,7 @@ function resolveDeletePath(sessionId, existingPaths) {
3836
4479
  }
3837
4480
 
3838
4481
  // src/domains/session/show.ts
3839
- import { join as join8 } from "path";
4482
+ import { join as join9 } from "path";
3840
4483
 
3841
4484
  // src/domains/session/list.ts
3842
4485
  import { parse as parseYaml2 } from "yaml";
@@ -3887,6 +4530,7 @@ var SESSION_PRIORITY = {
3887
4530
  };
3888
4531
  var SESSION_STATUSES = ["todo", "doing", "archive"];
3889
4532
  var DEFAULT_LIST_STATUSES = ["doing", "todo"];
4533
+ var CLAIMABLE_STATUS = SESSION_STATUSES[0];
3890
4534
  var PRIORITY_ORDER = {
3891
4535
  [SESSION_PRIORITY.HIGH]: 0,
3892
4536
  [SESSION_PRIORITY.MEDIUM]: 1,
@@ -4053,12 +4697,12 @@ var SESSION_SHOW_LABEL = {
4053
4697
  };
4054
4698
  var SESSION_SHOW_SEPARATOR_CHAR = "\u2500";
4055
4699
  var SESSION_SHOW_SEPARATOR_WIDTH = 40;
4056
- var sessionsBaseDir = join8(STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
4700
+ var sessionsBaseDir = join9(STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
4057
4701
  var { statusDirs } = DEFAULT_CONFIG.sessions;
4058
4702
  var DEFAULT_SESSION_CONFIG = {
4059
- todoDir: join8(sessionsBaseDir, statusDirs.todo),
4060
- doingDir: join8(sessionsBaseDir, statusDirs.doing),
4061
- archiveDir: join8(sessionsBaseDir, statusDirs.archive)
4703
+ todoDir: join9(sessionsBaseDir, statusDirs.todo),
4704
+ doingDir: join9(sessionsBaseDir, statusDirs.doing),
4705
+ archiveDir: join9(sessionsBaseDir, statusDirs.archive)
4062
4706
  };
4063
4707
  var SEARCH_ORDER = [...SESSION_STATUSES];
4064
4708
  function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
@@ -4116,7 +4760,7 @@ async function deleteCommand(options) {
4116
4760
 
4117
4761
  // src/commands/session/handoff.ts
4118
4762
  import { mkdir as mkdir2, writeFile } from "fs/promises";
4119
- import { join as join9, resolve as resolve3 } from "path";
4763
+ import { join as join10, resolve as resolve4 } from "path";
4120
4764
  import { stringify as stringifyYaml3 } from "yaml";
4121
4765
 
4122
4766
  // src/domains/session/create.ts
@@ -4394,8 +5038,8 @@ async function handoffCommand(options) {
4394
5038
  const yaml = stringifyYaml3(frontMatterObject, { defaultStringType: "QUOTE_DOUBLE" }).trimEnd();
4395
5039
  const fullContent = `${SESSION_FRONT_MATTER_OPEN}${yaml}${SESSION_FRONT_MATTER_CLOSE}${body}`;
4396
5040
  const filename = `${sessionId}.md`;
4397
- const sessionPath = join9(config.todoDir, filename);
4398
- const absolutePath = resolve3(sessionPath);
5041
+ const sessionPath = join10(config.todoDir, filename);
5042
+ const absolutePath = resolve4(sessionPath);
4399
5043
  await mkdir2(config.todoDir, { recursive: true });
4400
5044
  await writeFile(sessionPath, fullContent, "utf-8");
4401
5045
  const output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
@@ -4405,7 +5049,7 @@ async function handoffCommand(options) {
4405
5049
 
4406
5050
  // src/commands/session/list.ts
4407
5051
  import { readdir, readFile as readFile3 } from "fs/promises";
4408
- import { join as join10 } from "path";
5052
+ import { join as join11 } from "path";
4409
5053
  var SESSION_LIST_FORMAT = {
4410
5054
  TEXT: "text",
4411
5055
  JSON: "json"
@@ -4419,7 +5063,7 @@ async function loadSessionsFromDir(dir, status) {
4419
5063
  for (const file of files) {
4420
5064
  if (!file.endsWith(".md")) continue;
4421
5065
  const id = file.replace(".md", "");
4422
- const filePath = join10(dir, file);
5066
+ const filePath = join11(dir, file);
4423
5067
  const content = await readFile3(filePath, "utf-8");
4424
5068
  const metadata = parseSessionMetadata(content);
4425
5069
  sessions.push({
@@ -4492,7 +5136,7 @@ async function listCommand(options) {
4492
5136
 
4493
5137
  // src/commands/session/pickup.ts
4494
5138
  import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rename as rename2 } from "fs/promises";
4495
- import { join as join11 } from "path";
5139
+ import { join as join12 } from "path";
4496
5140
 
4497
5141
  // src/domains/session/pickup.ts
4498
5142
  function buildClaimPaths(sessionId, config) {
@@ -4528,7 +5172,6 @@ function selectBestSession(sessions) {
4528
5172
  }
4529
5173
 
4530
5174
  // src/commands/session/pickup.ts
4531
- var PICKUP_SOURCE_STATUS = SESSION_STATUSES[0];
4532
5175
  var PICKUP_TARGET_STATUS = SESSION_STATUSES[1];
4533
5176
  async function loadTodoSessions(config) {
4534
5177
  try {
@@ -4537,12 +5180,12 @@ async function loadTodoSessions(config) {
4537
5180
  for (const file of files) {
4538
5181
  if (!file.endsWith(".md")) continue;
4539
5182
  const id = file.replace(".md", "");
4540
- const filePath = join11(config.todoDir, file);
5183
+ const filePath = join12(config.todoDir, file);
4541
5184
  const content = await readFile4(filePath, "utf-8");
4542
5185
  const metadata = parseSessionMetadata(content);
4543
5186
  sessions.push({
4544
5187
  id,
4545
- status: PICKUP_SOURCE_STATUS,
5188
+ status: CLAIMABLE_STATUS,
4546
5189
  path: filePath,
4547
5190
  metadata
4548
5191
  });
@@ -4591,9 +5234,15 @@ async function pickupCommand(options) {
4591
5234
  return processBatch(options.sessionIds, (id) => pickupSingle(id, config));
4592
5235
  }
4593
5236
 
5237
+ // src/commands/session/pick.ts
5238
+ async function loadPickCandidates(options) {
5239
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
5240
+ return loadTodoSessions(config);
5241
+ }
5242
+
4594
5243
  // src/commands/session/prune.ts
4595
5244
  import { readdir as readdir3, readFile as readFile5, unlink as unlink2 } from "fs/promises";
4596
- import { join as join12 } from "path";
5245
+ import { join as join13 } from "path";
4597
5246
 
4598
5247
  // src/domains/session/prune.ts
4599
5248
  var DEFAULT_KEEP_COUNT = 5;
@@ -4642,7 +5291,7 @@ async function loadArchiveSessions(config) {
4642
5291
  for (const file of files) {
4643
5292
  if (!file.endsWith(".md")) continue;
4644
5293
  const id = file.replace(".md", "");
4645
- const filePath = join12(config.archiveDir, file);
5294
+ const filePath = join13(config.archiveDir, file);
4646
5295
  const content = await readFile5(filePath, "utf-8");
4647
5296
  const metadata = parseSessionMetadata(content);
4648
5297
  sessions.push({
@@ -4860,22 +5509,206 @@ Output:
4860
5509
  <PICKUP_ID>session-id</PICKUP_ID> tag for each claimed session
4861
5510
  `;
4862
5511
 
5512
+ // src/interfaces/cli/session/pick/run-picker.tsx
5513
+ import { render } from "ink";
5514
+
5515
+ // src/interfaces/cli/session/pick/SessionPicker.tsx
5516
+ import { Box, Text, useInput } from "ink";
5517
+ import { useState } from "react";
5518
+
5519
+ // src/domains/session/pick-model.ts
5520
+ var PICKER_ACTION = {
5521
+ MOVE: "move",
5522
+ FILTER_APPEND: "filter-append",
5523
+ FILTER_DELETE: "filter-delete",
5524
+ CLAIM: "claim",
5525
+ CANCEL: "cancel"
5526
+ };
5527
+ function buildCandidates(sessions) {
5528
+ const claimable = sessions.filter((session) => session.status === CLAIMABLE_STATUS);
5529
+ return sortSessions(claimable);
5530
+ }
5531
+ function filterCandidates(candidates, query) {
5532
+ const needle = query.trim().toLowerCase();
5533
+ if (needle.length === 0) {
5534
+ return [...candidates];
5535
+ }
5536
+ return candidates.filter((session) => {
5537
+ const haystack = `${session.id}
5538
+ ${session.metadata.goal}
5539
+ ${session.metadata.next_step}`.toLowerCase();
5540
+ return haystack.includes(needle);
5541
+ });
5542
+ }
5543
+ function visibleCandidates(state) {
5544
+ return filterCandidates(state.candidates, state.query);
5545
+ }
5546
+ function selectedSession(state) {
5547
+ const visible = visibleCandidates(state);
5548
+ return visible[state.selectedIndex] ?? null;
5549
+ }
5550
+ function initialPickerState(sessions) {
5551
+ return { candidates: buildCandidates(sessions), query: "", selectedIndex: 0 };
5552
+ }
5553
+ function keyToAction(key) {
5554
+ if (key.downArrow) return { type: PICKER_ACTION.MOVE, delta: 1 };
5555
+ if (key.upArrow) return { type: PICKER_ACTION.MOVE, delta: -1 };
5556
+ if (key.return) return { type: PICKER_ACTION.CLAIM };
5557
+ if (key.escape) return { type: PICKER_ACTION.CANCEL };
5558
+ if (key.backspace || key.delete) return { type: PICKER_ACTION.FILTER_DELETE };
5559
+ if (key.input.length > 0) return { type: PICKER_ACTION.FILTER_APPEND, char: key.input };
5560
+ return null;
5561
+ }
5562
+ function clampIndex(index, count) {
5563
+ const upper = Math.max(0, count - 1);
5564
+ if (index < 0) return 0;
5565
+ if (index > upper) return upper;
5566
+ return index;
5567
+ }
5568
+ function reducePicker(state, action) {
5569
+ switch (action.type) {
5570
+ case PICKER_ACTION.MOVE: {
5571
+ const count = visibleCandidates(state).length;
5572
+ return { ...state, selectedIndex: clampIndex(state.selectedIndex + action.delta, count) };
5573
+ }
5574
+ case PICKER_ACTION.FILTER_APPEND: {
5575
+ const query = state.query + action.char;
5576
+ const count = filterCandidates(state.candidates, query).length;
5577
+ return { ...state, query, selectedIndex: clampIndex(state.selectedIndex, count) };
5578
+ }
5579
+ case PICKER_ACTION.FILTER_DELETE: {
5580
+ const query = state.query.slice(0, -1);
5581
+ const count = filterCandidates(state.candidates, query).length;
5582
+ return { ...state, query, selectedIndex: clampIndex(state.selectedIndex, count) };
5583
+ }
5584
+ case PICKER_ACTION.CLAIM:
5585
+ case PICKER_ACTION.CANCEL:
5586
+ return state;
5587
+ }
5588
+ }
5589
+
5590
+ // src/interfaces/cli/session/pick/SessionPicker.tsx
5591
+ import { jsx, jsxs } from "react/jsx-runtime";
5592
+ var SESSION_PICKER_EMPTY_TEXT = "No claimable sessions.";
5593
+ var PRIORITY_COLOR = {
5594
+ [SESSION_PRIORITY.HIGH]: "red",
5595
+ [SESSION_PRIORITY.MEDIUM]: "yellow",
5596
+ [SESSION_PRIORITY.LOW]: "gray"
5597
+ };
5598
+ function toPickerKey(input, key) {
5599
+ return {
5600
+ input,
5601
+ upArrow: key.upArrow,
5602
+ downArrow: key.downArrow,
5603
+ return: key.return,
5604
+ escape: key.escape,
5605
+ backspace: key.backspace,
5606
+ delete: key.delete
5607
+ };
5608
+ }
5609
+ function SessionRow({ session, selected }) {
5610
+ const priority = session.metadata.priority;
5611
+ const marker = selected ? "\u276F" : " ";
5612
+ const badge = priority === DEFAULT_PRIORITY ? "" : ` [${priority}]`;
5613
+ return /* @__PURE__ */ jsxs(Text, { color: selected ? "cyan" : void 0, children: [
5614
+ marker,
5615
+ " ",
5616
+ session.id,
5617
+ /* @__PURE__ */ jsx(Text, { color: PRIORITY_COLOR[priority], children: badge }),
5618
+ " ",
5619
+ session.metadata.goal
5620
+ ] });
5621
+ }
5622
+ function PreviewPane({ session }) {
5623
+ if (session === null) {
5624
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: SESSION_PICKER_EMPTY_TEXT }) });
5625
+ }
5626
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
5627
+ /* @__PURE__ */ jsxs(Text, { children: [
5628
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "goal:" }),
5629
+ session.metadata.goal
5630
+ ] }),
5631
+ /* @__PURE__ */ jsxs(Text, { children: [
5632
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "next:" }),
5633
+ session.metadata.next_step
5634
+ ] })
5635
+ ] });
5636
+ }
5637
+ function SessionPicker({ sessions, onClaim, onCancel }) {
5638
+ const [state, setState] = useState(() => initialPickerState(sessions));
5639
+ useInput((input, key) => {
5640
+ const action = keyToAction(toPickerKey(input, key));
5641
+ if (action === null) return;
5642
+ if (action.type === PICKER_ACTION.CLAIM) {
5643
+ const selected2 = selectedSession(state);
5644
+ if (selected2 !== null) onClaim(selected2);
5645
+ return;
5646
+ }
5647
+ if (action.type === PICKER_ACTION.CANCEL) {
5648
+ onCancel();
5649
+ return;
5650
+ }
5651
+ setState((previous) => reducePicker(previous, action));
5652
+ });
5653
+ const visible = visibleCandidates(state);
5654
+ const selected = selectedSession(state);
5655
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
5656
+ /* @__PURE__ */ jsxs(Text, { children: [
5657
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Pick a session to claim" }),
5658
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(type to filter, \u2191\u2193 to move, \u23CE to claim, esc to cancel)" })
5659
+ ] }),
5660
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
5661
+ "filter: ",
5662
+ state.query
5663
+ ] }),
5664
+ visible.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: SESSION_PICKER_EMPTY_TEXT }) : visible.map((session, index) => /* @__PURE__ */ jsx(SessionRow, { session, selected: index === state.selectedIndex }, session.id)),
5665
+ /* @__PURE__ */ jsx(PreviewPane, { session: selected })
5666
+ ] });
5667
+ }
5668
+
5669
+ // src/interfaces/cli/session/pick/run-picker.tsx
5670
+ import { jsx as jsx2 } from "react/jsx-runtime";
5671
+ var PICK_NON_TTY_MESSAGE = "session pick requires an interactive terminal. Use `spx session pickup --auto` or `spx session pickup <id>` in a non-interactive context.";
5672
+ async function runPicker(sessions) {
5673
+ let claimed = null;
5674
+ let unmount = () => {
5675
+ };
5676
+ const instance = render(
5677
+ /* @__PURE__ */ jsx2(
5678
+ SessionPicker,
5679
+ {
5680
+ sessions,
5681
+ onClaim: (session) => {
5682
+ claimed = session;
5683
+ unmount();
5684
+ },
5685
+ onCancel: () => {
5686
+ unmount();
5687
+ }
5688
+ }
5689
+ )
5690
+ );
5691
+ unmount = instance.unmount;
5692
+ await instance.waitUntilExit();
5693
+ return claimed;
5694
+ }
5695
+
4863
5696
  // src/interfaces/cli/session.ts
4864
5697
  async function readStdin() {
4865
5698
  if (process.stdin.isTTY) {
4866
5699
  return void 0;
4867
5700
  }
4868
- return new Promise((resolve5) => {
5701
+ return new Promise((resolve6) => {
4869
5702
  let data = "";
4870
5703
  process.stdin.setEncoding("utf-8");
4871
5704
  process.stdin.on("data", (chunk) => {
4872
5705
  data += chunk;
4873
5706
  });
4874
5707
  process.stdin.on("end", () => {
4875
- resolve5(data.length === 0 ? void 0 : data);
5708
+ resolve6(data.length === 0 ? void 0 : data);
4876
5709
  });
4877
5710
  process.stdin.on("error", () => {
4878
- resolve5(void 0);
5711
+ resolve6(void 0);
4879
5712
  });
4880
5713
  });
4881
5714
  }
@@ -4898,6 +5731,29 @@ function registerSessionCommands(sessionCmd) {
4898
5731
  handleError(error);
4899
5732
  }
4900
5733
  });
5734
+ sessionCmd.command("pick").description("Interactively pick and claim a session (move from todo to doing)").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
5735
+ try {
5736
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
5737
+ console.error(PICK_NON_TTY_MESSAGE);
5738
+ process.exit(1);
5739
+ }
5740
+ const sessions = await loadPickCandidates({
5741
+ sessionsDir: options.sessionsDir,
5742
+ onWarning: writeWarning
5743
+ });
5744
+ const claimed = await runPicker(sessions);
5745
+ if (claimed !== null) {
5746
+ const output = await pickupCommand({
5747
+ sessionIds: [claimed.id],
5748
+ sessionsDir: options.sessionsDir,
5749
+ onWarning: writeWarning
5750
+ });
5751
+ console.log(output);
5752
+ }
5753
+ } catch (error) {
5754
+ handleError(error);
5755
+ }
5756
+ });
4901
5757
  sessionCmd.command("todo").description("List todo sessions").option("--json", "Output as JSON").option("--fields <fields>", "Comma-separated fields to emit as JSON (implies --json)").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
4902
5758
  try {
4903
5759
  const output = await listCommand({
@@ -5032,7 +5888,7 @@ var sessionDomain = {
5032
5888
 
5033
5889
  // src/lib/spec-tree/index.ts
5034
5890
  import { readdir as readdir5, readFile as readFile7 } from "fs/promises";
5035
- import { join as join13 } from "path";
5891
+ import { join as join14 } from "path";
5036
5892
  var SPEC_TREE_FIELD_KEY = {
5037
5893
  VERSION: "version",
5038
5894
  PRODUCT: "product",
@@ -5116,7 +5972,7 @@ function createFilesystemSpecTreeSource(options) {
5116
5972
  if (ref.path === void 0) {
5117
5973
  throw new Error("Filesystem source refs require a path");
5118
5974
  }
5119
- return readFile7(join13(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
5975
+ return readFile7(join14(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
5120
5976
  }
5121
5977
  };
5122
5978
  }
@@ -5398,7 +6254,7 @@ function compareOrderedEntries(left, right) {
5398
6254
  }
5399
6255
  async function* readFilesystemSourceEntries(productDir, registry, schemaVersions, includePath) {
5400
6256
  yield* walkFilesystemDirectory({
5401
- absolutePath: join13(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY),
6257
+ absolutePath: join14(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY),
5402
6258
  relativePath: SPEC_TREE_EMPTY_RELATIVE_PATH,
5403
6259
  registry,
5404
6260
  schemaVersions,
@@ -5427,7 +6283,7 @@ async function* walkFilesystemDirectory(context) {
5427
6283
  if (sourceEntry !== null) yield sourceEntry;
5428
6284
  if (entry.isDirectory() && shouldDescendIntoDirectory(sourceEntry)) {
5429
6285
  yield* walkFilesystemDirectory({
5430
- absolutePath: join13(context.absolutePath, entry.name),
6286
+ absolutePath: join14(context.absolutePath, entry.name),
5431
6287
  relativePath,
5432
6288
  registry: context.registry,
5433
6289
  schemaVersions: context.schemaVersions,
@@ -5573,14 +6429,14 @@ function formatNextNode(node) {
5573
6429
 
5574
6430
  // src/commands/testing/discovery.ts
5575
6431
  import { readdir as readdir6 } from "fs/promises";
5576
- import { join as join14, relative, sep } from "path";
6432
+ import { join as join15, relative, sep } from "path";
5577
6433
  var TESTS_DIRECTORY_NAME = SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME;
5578
6434
  var SPEC_ROOT_DIRECTORY = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
5579
6435
  var POSIX_SEPARATOR = "/";
5580
6436
  var ERROR_CODE_NOT_FOUND2 = "ENOENT";
5581
6437
  async function discoverTestFiles(productDir) {
5582
6438
  const found = [];
5583
- await collectTestFiles(join14(productDir, SPEC_ROOT_DIRECTORY), false, found);
6439
+ await collectTestFiles(join15(productDir, SPEC_ROOT_DIRECTORY), false, found);
5584
6440
  return found.map((absolute) => toPosixRelative(productDir, absolute)).sort(compareAscii);
5585
6441
  }
5586
6442
  async function collectTestFiles(directory, insideTestsDir, found) {
@@ -5592,7 +6448,7 @@ async function collectTestFiles(directory, insideTestsDir, found) {
5592
6448
  throw error;
5593
6449
  }
5594
6450
  for (const entry of entries) {
5595
- const childPath = join14(directory, entry.name);
6451
+ const childPath = join15(directory, entry.name);
5596
6452
  if (entry.isDirectory()) {
5597
6453
  await collectTestFiles(childPath, insideTestsDir || entry.name === TESTS_DIRECTORY_NAME, found);
5598
6454
  } else if (insideTestsDir && entry.isFile()) {
@@ -5674,11 +6530,11 @@ async function runTests(options, deps) {
5674
6530
  }
5675
6531
 
5676
6532
  // src/commands/testing/run-command.ts
5677
- import { join as join16 } from "path";
6533
+ import { join as join17 } from "path";
5678
6534
 
5679
6535
  // src/testing/run-state.ts
5680
6536
  import { createHash as createHash4 } from "crypto";
5681
- import { join as join15 } from "path";
6537
+ import { join as join16 } from "path";
5682
6538
  var TEST_RUN_STATE_STATUS = {
5683
6539
  PASSED: "passed",
5684
6540
  FAILED: "failed",
@@ -5765,12 +6621,12 @@ async function readTestingRuns(productDir, options = {}) {
5765
6621
  if (hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) {
5766
6622
  return { ok: true, value: { terminalRuns: [], incompleteRuns: [] } };
5767
6623
  }
5768
- return { ok: false, error: toErrorMessage2(error) };
6624
+ return { ok: false, error: toErrorMessage3(error) };
5769
6625
  }
5770
6626
  const terminalRuns = [];
5771
6627
  const incompleteRuns = [];
5772
6628
  for (const entry of entries.filter(isTestRunFileEntry)) {
5773
- const runFilePath = join15(runsDir2, entry.name);
6629
+ const runFilePath = join16(runsDir2, entry.name);
5774
6630
  const stateResult = await readTestRunStatePath(runFilePath, fs7);
5775
6631
  if (stateResult.ok) {
5776
6632
  terminalRuns.push({ runFileName: entry.name, runFilePath, state: stateResult.value });
@@ -5847,7 +6703,7 @@ async function readTestRunStatePath(runFilePath, fs7) {
5847
6703
  return {
5848
6704
  ok: false,
5849
6705
  reason: hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND) ? TESTING_RUN_STATE_INCOMPLETE_REASON.MISSING_STATE : TESTING_RUN_STATE_INCOMPLETE_REASON.IO_ERROR,
5850
- error: toErrorMessage2(error)
6706
+ error: toErrorMessage3(error)
5851
6707
  };
5852
6708
  }
5853
6709
  const latest = latestNonEmptyJsonlLine(content);
@@ -5999,7 +6855,7 @@ function isRecord4(value) {
5999
6855
  function sha256Hex(value) {
6000
6856
  return createHash4(SHA256_ALGORITHM2).update(value).digest(HEX_ENCODING3);
6001
6857
  }
6002
- function toErrorMessage2(error) {
6858
+ function toErrorMessage3(error) {
6003
6859
  return error instanceof Error ? error.message : String(error);
6004
6860
  }
6005
6861
 
@@ -6036,7 +6892,7 @@ function coveredTestPaths(dispatch) {
6036
6892
  async function readCoveredContents(productDir, paths, fs7) {
6037
6893
  const entries = [];
6038
6894
  for (const path6 of paths) {
6039
- entries.push({ path: path6, content: await fs7.readFile(join16(productDir, path6), TEXT_ENCODING) });
6895
+ entries.push({ path: path6, content: await fs7.readFile(join17(productDir, path6), TEXT_ENCODING) });
6040
6896
  }
6041
6897
  return entries;
6042
6898
  }
@@ -6047,7 +6903,7 @@ async function readProductInputEntries(productDir, paths, fs7) {
6047
6903
  entries.push({
6048
6904
  [PRODUCT_INPUT_FIELDS.PATH]: path6,
6049
6905
  [PRODUCT_INPUT_FIELDS.PRESENT]: true,
6050
- [PRODUCT_INPUT_FIELDS.CONTENT]: await fs7.readFile(join16(productDir, path6), TEXT_ENCODING)
6906
+ [PRODUCT_INPUT_FIELDS.CONTENT]: await fs7.readFile(join17(productDir, path6), TEXT_ENCODING)
6051
6907
  });
6052
6908
  } catch (error) {
6053
6909
  if (!hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) throw error;
@@ -6230,11 +7086,11 @@ function serializeNodeStatus(state) {
6230
7086
  }
6231
7087
 
6232
7088
  // src/lib/node-status/provider.ts
6233
- import { join as join18 } from "path";
7089
+ import { join as join19 } from "path";
6234
7090
 
6235
7091
  // src/lib/node-status/read.ts
6236
7092
  import { readFileSync as readFileSync2 } from "fs";
6237
- import { join as join17 } from "path";
7093
+ import { join as join18 } from "path";
6238
7094
  var NODE_STATUS_FILENAME = "spx.status.json";
6239
7095
  var NODE_STATUS_VALUES = new Set(Object.values(SPEC_TREE_NODE_STATE));
6240
7096
  function isNodeError2(error) {
@@ -6244,7 +7100,7 @@ function isSpecTreeNodeState(value) {
6244
7100
  return typeof value === "string" && NODE_STATUS_VALUES.has(value);
6245
7101
  }
6246
7102
  function readNodeStatus(nodeDir) {
6247
- const filePath = join17(nodeDir, NODE_STATUS_FILENAME);
7103
+ const filePath = join18(nodeDir, NODE_STATUS_FILENAME);
6248
7104
  let content;
6249
7105
  try {
6250
7106
  content = readFileSync2(filePath, "utf8");
@@ -6266,7 +7122,7 @@ function readNodeStatus(nodeDir) {
6266
7122
 
6267
7123
  // src/lib/node-status/provider.ts
6268
7124
  function nodeDirectory(productDir, node) {
6269
- return join18(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
7125
+ return join19(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
6270
7126
  }
6271
7127
  function createNodeStatusProvider(productDir) {
6272
7128
  return {
@@ -6278,7 +7134,7 @@ function createNodeStatusProvider(productDir) {
6278
7134
 
6279
7135
  // src/lib/node-status/update.ts
6280
7136
  import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
6281
- import { dirname as dirname4, join as join19 } from "path";
7137
+ import { dirname as dirname5, join as join20 } from "path";
6282
7138
  var NODE_STATUS_TEXT_ENCODING = "utf8";
6283
7139
  async function updateNodeStatus(options) {
6284
7140
  const { productDir, resolveOutcome } = options;
@@ -6316,8 +7172,8 @@ function isNodeExcluded(ignoreReader, node) {
6316
7172
  return ignoreReader.isUnderIgnoreSource(reference);
6317
7173
  }
6318
7174
  async function writeNodeStatus(productDir, nodeId, state) {
6319
- const filePath = join19(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
6320
- await mkdir4(dirname4(filePath), { recursive: true });
7175
+ const filePath = join20(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
7176
+ await mkdir4(dirname5(filePath), { recursive: true });
6321
7177
  await writeFile2(filePath, serializeNodeStatus(state), NODE_STATUS_TEXT_ENCODING);
6322
7178
  }
6323
7179
 
@@ -6431,7 +7287,7 @@ function formatNodeLabel(node) {
6431
7287
  }
6432
7288
 
6433
7289
  // src/testing/languages/python.ts
6434
- import { basename as basename2, dirname as dirname5, join as join20 } from "path/posix";
7290
+ import { basename as basename3, dirname as dirname6, join as join21 } from "path/posix";
6435
7291
 
6436
7292
  // src/validation/discovery/language-finder.ts
6437
7293
  import fs5 from "fs";
@@ -6496,16 +7352,16 @@ var PYTHON_TEST_FILE_PREFIX = "test_";
6496
7352
  var PYTHON_TEST_FILE_EXTENSION = ".py";
6497
7353
  var PYTHON_TEST_FILE_PATTERNS = [`${PYTHON_TEST_FILE_PREFIX}*${PYTHON_TEST_FILE_EXTENSION}`];
6498
7354
  function matchesTestFile(filePath) {
6499
- return basename2(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
7355
+ return basename3(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
6500
7356
  }
6501
7357
  function coveredProductInputPaths(coveredTestPaths2) {
6502
7358
  const paths = /* @__PURE__ */ new Set();
6503
7359
  for (const testPath of coveredTestPaths2) {
6504
7360
  if (!matchesTestFile(testPath)) continue;
6505
- let directory = dirname5(testPath);
7361
+ let directory = dirname6(testPath);
6506
7362
  while (directory !== "." && directory.length > 0) {
6507
- paths.add(join20(directory, PYTHON_PRODUCT_INPUT_PATH.CONFTEST));
6508
- const parent = dirname5(directory);
7363
+ paths.add(join21(directory, PYTHON_PRODUCT_INPUT_PATH.CONFTEST));
7364
+ const parent = dirname6(directory);
6509
7365
  if (parent === directory) break;
6510
7366
  directory = parent;
6511
7367
  }
@@ -6986,7 +7842,7 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
6986
7842
 
6987
7843
  // src/validation/steps/markdown.ts
6988
7844
  import { existsSync, statSync } from "fs";
6989
- import { basename as basename3, dirname as dirname6, join as join21, relative as pathRelative } from "path";
7845
+ import { basename as basename4, dirname as dirname7, join as join22, relative as pathRelative } from "path";
6990
7846
  import { main as markdownlintMain } from "markdownlint-cli2";
6991
7847
  import relativeLinksRule from "markdownlint-rule-relative-links";
6992
7848
  var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
@@ -7025,7 +7881,7 @@ function buildMarkdownlintConfig(directoryName) {
7025
7881
  };
7026
7882
  }
7027
7883
  function getDefaultDirectories(projectRoot) {
7028
- return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join21(projectRoot, name)).filter((dir) => existsSync(dir));
7884
+ return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join22(projectRoot, name)).filter((dir) => existsSync(dir));
7029
7885
  }
7030
7886
  function resolveMarkdownValidationTarget(path6, deps = defaultMarkdownValidationTargetDeps) {
7031
7887
  if (isExistingDirectory(path6, deps)) {
@@ -7084,7 +7940,7 @@ async function validateMarkdown(options) {
7084
7940
  async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
7085
7941
  const errors = [];
7086
7942
  const directory = targetDirectory(target);
7087
- const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename3(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
7943
+ const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename4(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
7088
7944
  const { customRules, ...markdownlintConfig } = config;
7089
7945
  const optionsOverride = {
7090
7946
  config: {
@@ -7108,7 +7964,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
7108
7964
  if (parsed) {
7109
7965
  errors.push({
7110
7966
  ...parsed,
7111
- file: join21(directory, parsed.file)
7967
+ file: join22(directory, parsed.file)
7112
7968
  });
7113
7969
  }
7114
7970
  }
@@ -7116,7 +7972,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
7116
7972
  return errors;
7117
7973
  }
7118
7974
  function targetDirectory(target) {
7119
- return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname6(target.path) : target.path;
7975
+ return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname7(target.path) : target.path;
7120
7976
  }
7121
7977
  function hasMarkdownExtension(path6) {
7122
7978
  const lastDot = path6.lastIndexOf(".");
@@ -7144,7 +8000,7 @@ function markdownlintConfigDirectoryName(directory, projectRoot) {
7144
8000
  return rootSegment;
7145
8001
  }
7146
8002
  }
7147
- return basename3(directory);
8003
+ return basename4(directory);
7148
8004
  }
7149
8005
 
7150
8006
  // src/commands/validation/messages.ts
@@ -7158,6 +8014,7 @@ var VALIDATION_STAGE_DISPLAY_NAMES = {
7158
8014
  };
7159
8015
  var VALIDATION_SKIP_LABELS = {
7160
8016
  VERB: "Skipping",
8017
+ CIRCULAR_REASON: "skip-circular",
7161
8018
  LITERAL_REASON: "skip-literal",
7162
8019
  TYPESCRIPT_ABSENT_REASON: "TypeScript not detected in project",
7163
8020
  VALIDATION_PATHS_NO_TARGETS_REASON: "validation paths matched no files",
@@ -7179,6 +8036,11 @@ var VALIDATION_COMMAND_OUTPUT = {
7179
8036
  MARKDOWN_NO_ISSUES: `${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: No issues found`,
7180
8037
  MARKDOWN_ERROR_SUMMARY_SUFFIX: "error(s) found"
7181
8038
  };
8039
+ var CIRCULAR_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR}: skipped (--${VALIDATION_SKIP_LABELS.CIRCULAR_REASON})`;
8040
+ var CIRCULAR_SKIP_JSON_OUTPUT = JSON.stringify({
8041
+ skipped: true,
8042
+ reason: VALIDATION_SKIP_LABELS.CIRCULAR_REASON
8043
+ });
7182
8044
  var LITERAL_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.LITERAL}: skipped (--${VALIDATION_SKIP_LABELS.LITERAL_REASON})`;
7183
8045
  var LITERAL_SKIP_JSON_OUTPUT = JSON.stringify({
7184
8046
  skipped: true,
@@ -7813,39 +8675,39 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7813
8675
  const token = _scanner.scan();
7814
8676
  switch (_scanner.getTokenError()) {
7815
8677
  case 4:
7816
- handleError2(
8678
+ handleError3(
7817
8679
  14
7818
8680
  /* ParseErrorCode.InvalidUnicode */
7819
8681
  );
7820
8682
  break;
7821
8683
  case 5:
7822
- handleError2(
8684
+ handleError3(
7823
8685
  15
7824
8686
  /* ParseErrorCode.InvalidEscapeCharacter */
7825
8687
  );
7826
8688
  break;
7827
8689
  case 3:
7828
- handleError2(
8690
+ handleError3(
7829
8691
  13
7830
8692
  /* ParseErrorCode.UnexpectedEndOfNumber */
7831
8693
  );
7832
8694
  break;
7833
8695
  case 1:
7834
8696
  if (!disallowComments) {
7835
- handleError2(
8697
+ handleError3(
7836
8698
  11
7837
8699
  /* ParseErrorCode.UnexpectedEndOfComment */
7838
8700
  );
7839
8701
  }
7840
8702
  break;
7841
8703
  case 2:
7842
- handleError2(
8704
+ handleError3(
7843
8705
  12
7844
8706
  /* ParseErrorCode.UnexpectedEndOfString */
7845
8707
  );
7846
8708
  break;
7847
8709
  case 6:
7848
- handleError2(
8710
+ handleError3(
7849
8711
  16
7850
8712
  /* ParseErrorCode.InvalidCharacter */
7851
8713
  );
@@ -7855,7 +8717,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7855
8717
  case 12:
7856
8718
  case 13:
7857
8719
  if (disallowComments) {
7858
- handleError2(
8720
+ handleError3(
7859
8721
  10
7860
8722
  /* ParseErrorCode.InvalidCommentToken */
7861
8723
  );
@@ -7864,7 +8726,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7864
8726
  }
7865
8727
  break;
7866
8728
  case 16:
7867
- handleError2(
8729
+ handleError3(
7868
8730
  1
7869
8731
  /* ParseErrorCode.InvalidSymbol */
7870
8732
  );
@@ -7877,7 +8739,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7877
8739
  }
7878
8740
  }
7879
8741
  }
7880
- function handleError2(error, skipUntilAfter = [], skipUntil = []) {
8742
+ function handleError3(error, skipUntilAfter = [], skipUntil = []) {
7881
8743
  onError(error);
7882
8744
  if (skipUntilAfter.length + skipUntil.length > 0) {
7883
8745
  let token = _scanner.getToken();
@@ -7909,7 +8771,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7909
8771
  const tokenValue = _scanner.getTokenValue();
7910
8772
  let value = Number(tokenValue);
7911
8773
  if (isNaN(value)) {
7912
- handleError2(
8774
+ handleError3(
7913
8775
  2
7914
8776
  /* ParseErrorCode.InvalidNumberFormat */
7915
8777
  );
@@ -7934,7 +8796,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7934
8796
  }
7935
8797
  function parseProperty() {
7936
8798
  if (_scanner.getToken() !== 10) {
7937
- handleError2(3, [], [
8799
+ handleError3(3, [], [
7938
8800
  2,
7939
8801
  5
7940
8802
  /* SyntaxKind.CommaToken */
@@ -7946,14 +8808,14 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7946
8808
  onSeparator(":");
7947
8809
  scanNext();
7948
8810
  if (!parseValue()) {
7949
- handleError2(4, [], [
8811
+ handleError3(4, [], [
7950
8812
  2,
7951
8813
  5
7952
8814
  /* SyntaxKind.CommaToken */
7953
8815
  ]);
7954
8816
  }
7955
8817
  } else {
7956
- handleError2(5, [], [
8818
+ handleError3(5, [], [
7957
8819
  2,
7958
8820
  5
7959
8821
  /* SyntaxKind.CommaToken */
@@ -7969,7 +8831,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7969
8831
  while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
7970
8832
  if (_scanner.getToken() === 5) {
7971
8833
  if (!needsComma) {
7972
- handleError2(4, [], []);
8834
+ handleError3(4, [], []);
7973
8835
  }
7974
8836
  onSeparator(",");
7975
8837
  scanNext();
@@ -7977,10 +8839,10 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7977
8839
  break;
7978
8840
  }
7979
8841
  } else if (needsComma) {
7980
- handleError2(6, [], []);
8842
+ handleError3(6, [], []);
7981
8843
  }
7982
8844
  if (!parseProperty()) {
7983
- handleError2(4, [], [
8845
+ handleError3(4, [], [
7984
8846
  2,
7985
8847
  5
7986
8848
  /* SyntaxKind.CommaToken */
@@ -7990,7 +8852,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7990
8852
  }
7991
8853
  onObjectEnd();
7992
8854
  if (_scanner.getToken() !== 2) {
7993
- handleError2(7, [
8855
+ handleError3(7, [
7994
8856
  2
7995
8857
  /* SyntaxKind.CloseBraceToken */
7996
8858
  ], []);
@@ -8007,7 +8869,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
8007
8869
  while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
8008
8870
  if (_scanner.getToken() === 5) {
8009
8871
  if (!needsComma) {
8010
- handleError2(4, [], []);
8872
+ handleError3(4, [], []);
8011
8873
  }
8012
8874
  onSeparator(",");
8013
8875
  scanNext();
@@ -8015,7 +8877,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
8015
8877
  break;
8016
8878
  }
8017
8879
  } else if (needsComma) {
8018
- handleError2(6, [], []);
8880
+ handleError3(6, [], []);
8019
8881
  }
8020
8882
  if (isFirstElement) {
8021
8883
  _jsonPath.push(0);
@@ -8024,7 +8886,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
8024
8886
  _jsonPath[_jsonPath.length - 1]++;
8025
8887
  }
8026
8888
  if (!parseValue()) {
8027
- handleError2(4, [], [
8889
+ handleError3(4, [], [
8028
8890
  4,
8029
8891
  5
8030
8892
  /* SyntaxKind.CommaToken */
@@ -8037,7 +8899,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
8037
8899
  _jsonPath.pop();
8038
8900
  }
8039
8901
  if (_scanner.getToken() !== 4) {
8040
- handleError2(8, [
8902
+ handleError3(8, [
8041
8903
  4
8042
8904
  /* SyntaxKind.CloseBracketToken */
8043
8905
  ], []);
@@ -8063,15 +8925,15 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
8063
8925
  if (options.allowEmptyContent) {
8064
8926
  return true;
8065
8927
  }
8066
- handleError2(4, [], []);
8928
+ handleError3(4, [], []);
8067
8929
  return false;
8068
8930
  }
8069
8931
  if (!parseValue()) {
8070
- handleError2(4, [], []);
8932
+ handleError3(4, [], []);
8071
8933
  return false;
8072
8934
  }
8073
8935
  if (_scanner.getToken() !== 17) {
8074
- handleError2(9, [], []);
8936
+ handleError3(9, [], []);
8075
8937
  }
8076
8938
  return true;
8077
8939
  }
@@ -8130,7 +8992,7 @@ var ParseErrorCode;
8130
8992
 
8131
8993
  // src/validation/config/scope.ts
8132
8994
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
8133
- import { isAbsolute as isAbsolute3, join as join22 } from "path";
8995
+ import { isAbsolute as isAbsolute3, join as join23 } from "path";
8134
8996
  var TSCONFIG_FILES = {
8135
8997
  full: "tsconfig.json",
8136
8998
  production: "tsconfig.production.json"
@@ -8144,7 +9006,7 @@ var defaultScopeDeps = {
8144
9006
  readdirSync
8145
9007
  };
8146
9008
  function resolveProjectPath(projectRoot, path6) {
8147
- return isAbsolute3(path6) ? path6 : join22(projectRoot, path6);
9009
+ return isAbsolute3(path6) ? path6 : join23(projectRoot, path6);
8148
9010
  }
8149
9011
  function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
8150
9012
  try {
@@ -8188,7 +9050,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
8188
9050
  if (hasDirectTsFiles) return true;
8189
9051
  const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
8190
9052
  for (const subdir of subdirs.slice(0, 5)) {
8191
- if (hasTypeScriptFilesRecursive(join22(dirPath, subdir.name), maxDepth - 1, deps)) {
9053
+ if (hasTypeScriptFilesRecursive(join23(dirPath, subdir.name), maxDepth - 1, deps)) {
8192
9054
  return true;
8193
9055
  }
8194
9056
  }
@@ -8211,7 +9073,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
8211
9073
  });
8212
9074
  if (!isExcluded) {
8213
9075
  try {
8214
- const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join22(projectRoot, dir), 2, deps);
9076
+ const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join23(projectRoot, dir), 2, deps);
8215
9077
  if (hasTypeScriptFiles) {
8216
9078
  directories.add(dir);
8217
9079
  }
@@ -8242,13 +9104,13 @@ function getLiteralTopLevelPatternDirectory(pattern) {
8242
9104
  function filterActiveIncludePatterns(patterns, excludePatterns, projectRoot, deps) {
8243
9105
  return patterns.filter((pattern) => {
8244
9106
  const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
8245
- return topLevelDir === null || deps.existsSync(join22(projectRoot, topLevelDir));
9107
+ return topLevelDir === null || deps.existsSync(join23(projectRoot, topLevelDir));
8246
9108
  }).filter((pattern) => pathPassesValidationFilter(pattern, { exclude: excludePatterns }));
8247
9109
  }
8248
9110
  function getValidationDirectories(scope, projectRoot, deps = defaultScopeDeps) {
8249
9111
  const config = resolveTypeScriptConfig(scope, projectRoot, deps);
8250
9112
  const configDirectories = getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps);
8251
- const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join22(projectRoot, dir)));
9113
+ const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join23(projectRoot, dir)));
8252
9114
  return existingDirectories;
8253
9115
  }
8254
9116
  function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
@@ -8262,10 +9124,10 @@ function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
8262
9124
  }
8263
9125
 
8264
9126
  // src/validation/discovery/tool-finder.ts
8265
- import { execSync as execSync2 } from "child_process";
8266
9127
  import fs6 from "fs";
8267
9128
  import { createRequire } from "module";
8268
9129
  import path5 from "path";
9130
+ import { fileURLToPath } from "url";
8269
9131
 
8270
9132
  // src/validation/discovery/constants.ts
8271
9133
  var TOOL_DISCOVERY = {
@@ -8299,6 +9161,50 @@ var TOOL_DISCOVERY = {
8299
9161
 
8300
9162
  // src/validation/discovery/tool-finder.ts
8301
9163
  var require2 = createRequire(import.meta.url);
9164
+ var FILE_URL_PROTOCOL = new URL(import.meta.url).protocol;
9165
+ var PACKAGE_MANIFEST_FILENAME = "package.json";
9166
+ var PATH_ENVIRONMENT_VARIABLE = "PATH";
9167
+ var WINDOWS_EXECUTABLE_EXTENSIONS_VARIABLE = "PATHEXT";
9168
+ var WINDOWS_DEFAULT_EXECUTABLE_EXTENSIONS = [".COM", ".EXE", ".BAT", ".CMD"];
9169
+ function executableExtensions() {
9170
+ if (process.platform !== "win32") {
9171
+ return [""];
9172
+ }
9173
+ const pathExtensions = process.env[WINDOWS_EXECUTABLE_EXTENSIONS_VARIABLE]?.split(path5.delimiter).filter(Boolean);
9174
+ return pathExtensions && pathExtensions.length > 0 ? pathExtensions : WINDOWS_DEFAULT_EXECUTABLE_EXTENSIONS;
9175
+ }
9176
+ function executableCandidateNames(tool) {
9177
+ if (process.platform !== "win32" || path5.extname(tool) !== "") {
9178
+ return [tool];
9179
+ }
9180
+ return executableExtensions().map((extension) => `${tool}${extension}`);
9181
+ }
9182
+ function isExecutableFile(filePath) {
9183
+ try {
9184
+ fs6.accessSync(filePath, fs6.constants.X_OK);
9185
+ return true;
9186
+ } catch {
9187
+ return false;
9188
+ }
9189
+ }
9190
+ function findExecutableOnPath(tool) {
9191
+ const pathValue = process.env[PATH_ENVIRONMENT_VARIABLE];
9192
+ if (!pathValue) {
9193
+ return null;
9194
+ }
9195
+ for (const directory of pathValue.split(path5.delimiter)) {
9196
+ if (!directory) {
9197
+ continue;
9198
+ }
9199
+ for (const candidateName of executableCandidateNames(tool)) {
9200
+ const candidatePath = path5.join(directory, candidateName);
9201
+ if (isExecutableFile(candidatePath)) {
9202
+ return candidatePath;
9203
+ }
9204
+ }
9205
+ }
9206
+ return null;
9207
+ }
8302
9208
  var defaultToolDiscoveryDeps = {
8303
9209
  resolveModule: (modulePath) => {
8304
9210
  try {
@@ -8307,28 +9213,50 @@ var defaultToolDiscoveryDeps = {
8307
9213
  return null;
8308
9214
  }
8309
9215
  },
8310
- existsSync: fs6.existsSync,
8311
- whichSync: (tool) => {
9216
+ resolveImport: (modulePath) => {
8312
9217
  try {
8313
- const result = execSync2(`which ${tool}`, {
8314
- encoding: "utf-8",
8315
- stdio: ["pipe", "pipe", "pipe"]
8316
- });
8317
- return result.trim() || null;
9218
+ return import.meta.resolve(modulePath);
8318
9219
  } catch {
8319
9220
  return null;
8320
9221
  }
8321
- }
9222
+ },
9223
+ existsSync: fs6.existsSync,
9224
+ whichSync: findExecutableOnPath
8322
9225
  };
9226
+ function resolvedModulePath(resolvedPath) {
9227
+ try {
9228
+ const resolvedUrl = new URL(resolvedPath);
9229
+ return resolvedUrl.protocol === FILE_URL_PROTOCOL ? fileURLToPath(resolvedUrl) : resolvedPath;
9230
+ } catch {
9231
+ return resolvedPath;
9232
+ }
9233
+ }
9234
+ function nearestPackageRoot(filePath, existsSync7) {
9235
+ let currentDirectory = path5.dirname(filePath);
9236
+ while (true) {
9237
+ if (existsSync7(path5.join(currentDirectory, PACKAGE_MANIFEST_FILENAME))) {
9238
+ return currentDirectory;
9239
+ }
9240
+ const parentDirectory = path5.dirname(currentDirectory);
9241
+ if (parentDirectory === currentDirectory) {
9242
+ return null;
9243
+ }
9244
+ currentDirectory = parentDirectory;
9245
+ }
9246
+ }
9247
+ function bundledToolPath(resolvedPath, existsSync7) {
9248
+ const bundledFilePath = resolvedModulePath(resolvedPath);
9249
+ return nearestPackageRoot(bundledFilePath, existsSync7) ?? path5.dirname(bundledFilePath);
9250
+ }
8323
9251
  async function discoverTool(tool, options = {}) {
8324
9252
  const { projectRoot = process.cwd(), deps = defaultToolDiscoveryDeps } = options;
8325
- const bundledPath = deps.resolveModule(`${tool}/package.json`);
9253
+ const bundledPath = deps.resolveModule(`${tool}/package.json`) ?? deps.resolveImport?.(tool);
8326
9254
  if (bundledPath) {
8327
9255
  return {
8328
9256
  found: true,
8329
9257
  location: {
8330
9258
  tool,
8331
- path: path5.dirname(bundledPath),
9259
+ path: bundledToolPath(bundledPath, deps.existsSync),
8332
9260
  source: TOOL_DISCOVERY.SOURCES.BUNDLED
8333
9261
  }
8334
9262
  };
@@ -8371,33 +9299,180 @@ function formatSkipMessage(stepName, result) {
8371
9299
  }
8372
9300
 
8373
9301
  // src/validation/steps/circular.ts
8374
- import madge from "madge";
8375
- import { join as join23 } from "path";
9302
+ import {
9303
+ cruise as dependencyCruiser
9304
+ } from "dependency-cruiser";
9305
+ import extractTypeScriptConfig from "dependency-cruiser/config-utl/extract-ts-config";
9306
+ import { join as join24 } from "path";
9307
+ var DEPENDENCY_CRUISER_MODULE_SYSTEMS = ["es6", "cjs"];
9308
+ var DEPENDENCY_CRUISER_TYPESCRIPT_SOURCE_GLOB_SUFFIXES = ["**/*.ts", "**/*.tsx"];
9309
+ var DEPENDENCY_CRUISER_TYPESCRIPT_SOURCE_PATTERN = String.raw`\.tsx?$`;
9310
+ var DEPENDENCY_CRUISER_TYPESCRIPT_RESOLVE_EXTENSIONS = [".ts", ".tsx", ".d.ts"];
9311
+ var TSCONFIG_EXCLUDE_SUFFIX_PATTERN = /\/\*\*?\/\*$/u;
9312
+ var REGEX_SPECIAL_CHARACTER_PATTERN = /[.*+?^${}()|[\]\\]/gu;
9313
+ var REGEX_ESCAPE_REPLACEMENT = String.raw`\$&`;
9314
+ var CYCLE_KEY_SEPARATOR = "\0";
9315
+ var DEPENDENCY_CRUISER_PACKAGE_EXCLUDE_PATTERN = "(^|/)node_modules(/|$)";
9316
+ var DEPENDENCY_CRUISER_DEPENDENCY_TYPES = {
9317
+ AMD_DEFINE: "amd-define",
9318
+ AMD_EXOTIC_REQUIRE: "amd-exotic-require",
9319
+ AMD_REQUIRE: "amd-require",
9320
+ DYNAMIC_IMPORT: "dynamic-import",
9321
+ EXPORT: "export",
9322
+ EXOTIC_REQUIRE: "exotic-require",
9323
+ IMPORT: "import",
9324
+ IMPORT_EQUALS: "import-equals",
9325
+ LOCAL: "local",
9326
+ PRE_COMPILATION_ONLY: "pre-compilation-only",
9327
+ REQUIRE: "require",
9328
+ TYPE_IMPORT: "type-import",
9329
+ // dependency-cruiser 16.10.4 emits this for TypeScript `import type` edges.
9330
+ TYPE_ONLY: "type-only"
9331
+ };
9332
+ var DEPENDENCY_CRUISER_TS_PRE_COMPILATION_DEPS = "specify";
9333
+ var ERASURE_ONLY_DEPENDENCY_TYPES = /* @__PURE__ */ new Set([
9334
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.PRE_COMPILATION_ONLY,
9335
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.TYPE_IMPORT,
9336
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.TYPE_ONLY
9337
+ ]);
9338
+ var RUNTIME_DEPENDENCY_TYPES = /* @__PURE__ */ new Set([
9339
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.AMD_DEFINE,
9340
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.AMD_EXOTIC_REQUIRE,
9341
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.AMD_REQUIRE,
9342
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.DYNAMIC_IMPORT,
9343
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.EXPORT,
9344
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.EXOTIC_REQUIRE,
9345
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.IMPORT,
9346
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.IMPORT_EQUALS,
9347
+ DEPENDENCY_CRUISER_DEPENDENCY_TYPES.REQUIRE
9348
+ ]);
8376
9349
  var CIRCULAR_DEPS_KEYS = {
8377
- MADGE: "madge"
9350
+ DEPENDENCY_CRUISER: "dependencyCruiser",
9351
+ EXTRACT_TYPESCRIPT_CONFIG: "extractTypeScriptConfig"
8378
9352
  };
8379
9353
  var defaultCircularDeps = {
8380
- [CIRCULAR_DEPS_KEYS.MADGE]: madge
9354
+ [CIRCULAR_DEPS_KEYS.DEPENDENCY_CRUISER]: dependencyCruiser,
9355
+ [CIRCULAR_DEPS_KEYS.EXTRACT_TYPESCRIPT_CONFIG]: extractTypeScriptConfig
8381
9356
  };
9357
+ function toDependencyCruiserExcludePatterns(patterns) {
9358
+ return patterns.map((pattern) => {
9359
+ const cleanPattern = pattern.replace(TSCONFIG_EXCLUDE_SUFFIX_PATTERN, "");
9360
+ return cleanPattern.replace(REGEX_SPECIAL_CHARACTER_PATTERN, REGEX_ESCAPE_REPLACEMENT);
9361
+ });
9362
+ }
9363
+ function buildDependencyCruiserOptions(typescriptScope, projectRoot, tsConfigFile) {
9364
+ const excludePatterns = [
9365
+ DEPENDENCY_CRUISER_PACKAGE_EXCLUDE_PATTERN,
9366
+ ...toDependencyCruiserExcludePatterns(typescriptScope.excludePatterns)
9367
+ ];
9368
+ return {
9369
+ baseDir: projectRoot,
9370
+ enhancedResolveOptions: { extensions: [...DEPENDENCY_CRUISER_TYPESCRIPT_RESOLVE_EXTENSIONS] },
9371
+ exclude: { path: excludePatterns },
9372
+ includeOnly: { path: DEPENDENCY_CRUISER_TYPESCRIPT_SOURCE_PATTERN },
9373
+ moduleSystems: [...DEPENDENCY_CRUISER_MODULE_SYSTEMS],
9374
+ tsConfig: { fileName: tsConfigFile },
9375
+ tsPreCompilationDeps: DEPENDENCY_CRUISER_TS_PRE_COMPILATION_DEPS
9376
+ };
9377
+ }
9378
+ function toDependencyCruiserSourcePatterns(directories) {
9379
+ return directories.flatMap(
9380
+ (directory) => DEPENDENCY_CRUISER_TYPESCRIPT_SOURCE_GLOB_SUFFIXES.map((suffix) => `${directory}/${suffix}`)
9381
+ );
9382
+ }
9383
+ function isCruiseResult(output) {
9384
+ return typeof output === "object" && output !== null && "modules" in output && "summary" in output;
9385
+ }
9386
+ function closeCycle(cycle) {
9387
+ const first = cycle[0];
9388
+ const last = cycle.at(-1);
9389
+ if (first === void 0) {
9390
+ return [];
9391
+ }
9392
+ return last === first ? [...cycle] : [...cycle, first];
9393
+ }
9394
+ function dependencyTypesSurviveRuntime(dependencyTypes) {
9395
+ if (dependencyTypes.includes(DEPENDENCY_CRUISER_DEPENDENCY_TYPES.PRE_COMPILATION_ONLY)) {
9396
+ return false;
9397
+ }
9398
+ if (dependencyTypes.some((dependencyType) => RUNTIME_DEPENDENCY_TYPES.has(dependencyType))) {
9399
+ return true;
9400
+ }
9401
+ return dependencyTypes.every((dependencyType) => !ERASURE_ONLY_DEPENDENCY_TYPES.has(dependencyType));
9402
+ }
9403
+ function dependencySurvivesRuntime(dependency) {
9404
+ return !dependency.typeOnly && !dependency.preCompilationOnly;
9405
+ }
9406
+ function dependencyCycleSurvivesRuntime(dependency) {
9407
+ return dependencySurvivesRuntime(dependency) && (dependency.cycle?.every((cycleDependency) => dependencyTypesSurviveRuntime(cycleDependency.dependencyTypes)) ?? true);
9408
+ }
9409
+ function dependencyCycleForModule(moduleSource, dependency) {
9410
+ if (!dependency.circular || !dependencyCycleSurvivesRuntime(dependency)) {
9411
+ return null;
9412
+ }
9413
+ const cycleTail = dependency.cycle?.map((cycleDependency) => cycleDependency.name) ?? [dependency.resolved];
9414
+ return closeCycle([moduleSource, ...cycleTail]);
9415
+ }
9416
+ function openCycle(cycle) {
9417
+ if (cycle.length > 1 && cycle[0] === cycle.at(-1)) {
9418
+ return cycle.slice(0, -1);
9419
+ }
9420
+ return [...cycle];
9421
+ }
9422
+ function cycleRotations(cycle) {
9423
+ return cycle.map(
9424
+ (_, index) => [
9425
+ ...cycle.slice(index),
9426
+ ...cycle.slice(0, index)
9427
+ ].join(CYCLE_KEY_SEPARATOR)
9428
+ );
9429
+ }
9430
+ function canonicalCycleKey(cycle) {
9431
+ const opened = openCycle(cycle);
9432
+ const reversed = [...opened].reverse();
9433
+ const keys = [...cycleRotations(opened), ...cycleRotations(reversed)].sort(
9434
+ (left, right) => left.localeCompare(right)
9435
+ );
9436
+ return keys[0] ?? "";
9437
+ }
9438
+ function uniqueCycles(cycles) {
9439
+ const seen = /* @__PURE__ */ new Set();
9440
+ const result = [];
9441
+ for (const cycle of cycles) {
9442
+ const key = canonicalCycleKey(cycle);
9443
+ if (!seen.has(key)) {
9444
+ seen.add(key);
9445
+ result.push(cycle);
9446
+ }
9447
+ }
9448
+ return result;
9449
+ }
9450
+ function circularDependencyCycles(result) {
9451
+ const cycles = result.modules.flatMap(
9452
+ (module) => module.dependencies.flatMap((dependency) => {
9453
+ const cycle = dependencyCycleForModule(module.source, dependency);
9454
+ return cycle === null ? [] : [cycle];
9455
+ })
9456
+ );
9457
+ return uniqueCycles(cycles);
9458
+ }
8382
9459
  async function validateCircularDependencies(scope, typescriptScope, projectRoot, deps = defaultCircularDeps) {
8383
9460
  try {
8384
- const analyzeDirectories = typescriptScope.directories.map((directory) => join23(projectRoot, directory));
8385
- if (analyzeDirectories.length === 0) {
9461
+ const analyzeSourcePatterns = toDependencyCruiserSourcePatterns(typescriptScope.directories);
9462
+ if (analyzeSourcePatterns.length === 0) {
8386
9463
  return { success: true };
8387
9464
  }
8388
- const tsConfigFile = join23(projectRoot, TSCONFIG_FILES[scope]);
8389
- const excludeRegExps = typescriptScope.excludePatterns.map((pattern) => {
8390
- const cleanPattern = pattern.replace(/\/\*\*?\/\*$/, "");
8391
- const escaped = cleanPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8392
- return new RegExp(escaped);
8393
- });
8394
- const result = await deps.madge(analyzeDirectories, {
8395
- baseDir: projectRoot,
8396
- fileExtensions: ["ts", "tsx"],
8397
- tsConfig: tsConfigFile,
8398
- excludeRegExp: excludeRegExps
8399
- });
8400
- const circular = result.circular();
9465
+ const tsConfigFile = join24(projectRoot, TSCONFIG_FILES[scope]);
9466
+ const result = await deps.dependencyCruiser(
9467
+ analyzeSourcePatterns,
9468
+ buildDependencyCruiserOptions(typescriptScope, projectRoot, tsConfigFile),
9469
+ void 0,
9470
+ { tsConfig: deps.extractTypeScriptConfig(tsConfigFile) }
9471
+ );
9472
+ if (!isCruiseResult(result.output)) {
9473
+ return { success: false, error: "dependency-cruiser returned non-structured output" };
9474
+ }
9475
+ const circular = circularDependencyCycles(result.output);
8401
9476
  if (circular.length === 0) {
8402
9477
  return { success: true };
8403
9478
  } else {
@@ -8430,7 +9505,7 @@ async function circularCommand(options) {
8430
9505
  durationMs: Date.now() - startTime
8431
9506
  };
8432
9507
  }
8433
- const toolResult = await discoverTool("madge", { projectRoot: cwd });
9508
+ const toolResult = await discoverTool("dependency-cruiser", { projectRoot: cwd });
8434
9509
  if (!toolResult.found) {
8435
9510
  const skipMessage = formatSkipMessage("circular dependency check", toolResult);
8436
9511
  return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
@@ -8468,7 +9543,7 @@ ${cycles}`;
8468
9543
  import { existsSync as existsSync3 } from "fs";
8469
9544
  import { mkdtemp, rm, writeFile as writeFile3 } from "fs/promises";
8470
9545
  import { tmpdir } from "os";
8471
- import { isAbsolute as isAbsolute4, join as join24 } from "path";
9546
+ import { isAbsolute as isAbsolute4, join as join25 } from "path";
8472
9547
  var defaultKnipProcessRunner = lifecycleProcessRunner;
8473
9548
  var KNIP_COMMAND_TOKENS = {
8474
9549
  COMMAND: "knip",
@@ -8496,7 +9571,7 @@ async function validateKnip2(context, runner = defaultKnipProcessRunner, deps =
8496
9571
  }
8497
9572
  async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
8498
9573
  const scopedTsconfig = typescriptScope.filteredByValidationPaths ? await createScopedKnipTsconfig(projectRoot, typescriptScope, deps) : void 0;
8499
- const localBin = join24(projectRoot, "node_modules", ".bin", "knip");
9574
+ const localBin = join25(projectRoot, "node_modules", ".bin", "knip");
8500
9575
  const binary = deps.existsSync(localBin) ? localBin : "npx";
8501
9576
  const baseArgs = scopedTsconfig === void 0 ? [] : [
8502
9577
  KNIP_COMMAND_TOKENS.USE_TSCONFIG_FILES_FLAG,
@@ -8526,13 +9601,13 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
8526
9601
  knipProcess.stderr?.on("data", (data) => {
8527
9602
  knipError += data.toString();
8528
9603
  });
8529
- return new Promise((resolve5) => {
9604
+ return new Promise((resolve6) => {
8530
9605
  const resolveAfterCleanup = (result) => {
8531
9606
  if (resultResolved) {
8532
9607
  return;
8533
9608
  }
8534
9609
  resultResolved = true;
8535
- void cleanupOnce().finally(() => resolve5(result));
9610
+ void cleanupOnce().finally(() => resolve6(result));
8536
9611
  };
8537
9612
  knipProcess.on("close", (code) => {
8538
9613
  if (code === 0) {
@@ -8551,12 +9626,12 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
8551
9626
  });
8552
9627
  }
8553
9628
  async function createScopedKnipTsconfig(projectRoot, typescriptScope, deps) {
8554
- const tempDir = await deps.mkdtemp(join24(tmpdir(), "validate-knip-"));
8555
- const configPath = join24(tempDir, TSCONFIG_FILES.full);
8556
- const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join24(projectRoot, pattern);
9629
+ const tempDir = await deps.mkdtemp(join25(tmpdir(), "validate-knip-"));
9630
+ const configPath = join25(tempDir, TSCONFIG_FILES.full);
9631
+ const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join25(projectRoot, pattern);
8557
9632
  const project = typescriptScope.filePatterns.length > 0 ? typescriptScope.filePatterns : typescriptScope.directories.map((directory) => `${directory}/**/*.{js,ts,tsx}`);
8558
9633
  const config = {
8559
- extends: join24(projectRoot, TSCONFIG_FILES.full),
9634
+ extends: join25(projectRoot, TSCONFIG_FILES.full),
8560
9635
  include: project.map(toProjectPathPattern),
8561
9636
  exclude: typescriptScope.excludePatterns.map(toProjectPathPattern)
8562
9637
  };
@@ -8606,12 +9681,12 @@ async function knipCommand(options) {
8606
9681
 
8607
9682
  // src/validation/steps/eslint.ts
8608
9683
  import { existsSync as existsSync5 } from "fs";
8609
- import { join as join26 } from "path";
9684
+ import { join as join27 } from "path";
8610
9685
 
8611
9686
  // src/validation/lint-policy.ts
8612
9687
  import { execFileSync } from "child_process";
8613
9688
  import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
8614
- import { join as join25 } from "path";
9689
+ import { join as join26 } from "path";
8615
9690
 
8616
9691
  // src/validation/lint-policy-constants.ts
8617
9692
  var LINT_POLICY_MANIFESTS = {
@@ -8659,17 +9734,17 @@ function isSpecTreeNodePath(entry) {
8659
9734
  }
8660
9735
  function readManifest(productDir, file, key) {
8661
9736
  return parseLintPolicyManifest(
8662
- readFileSync4(join25(productDir, file), "utf-8"),
9737
+ readFileSync4(join26(productDir, file), "utf-8"),
8663
9738
  file,
8664
9739
  key
8665
9740
  );
8666
9741
  }
8667
9742
  function manifestExists(productDir, file) {
8668
- return existsSync4(join25(productDir, file));
9743
+ return existsSync4(join26(productDir, file));
8669
9744
  }
8670
9745
  function findDeprecatedSpecNodePath(productDir) {
8671
9746
  function visit2(relativeDirectory) {
8672
- const absoluteDirectory = join25(productDir, relativeDirectory);
9747
+ const absoluteDirectory = join26(productDir, relativeDirectory);
8673
9748
  for (const entry of readdirSync2(absoluteDirectory, { withFileTypes: true })) {
8674
9749
  if (!entry.isDirectory()) {
8675
9750
  continue;
@@ -8685,7 +9760,7 @@ function findDeprecatedSpecNodePath(productDir) {
8685
9760
  }
8686
9761
  return void 0;
8687
9762
  }
8688
- const specTreeRootPath = join25(productDir, SPEC_TREE_ROOT);
9763
+ const specTreeRootPath = join26(productDir, SPEC_TREE_ROOT);
8689
9764
  if (!existsSync4(specTreeRootPath)) {
8690
9765
  return void 0;
8691
9766
  }
@@ -8711,7 +9786,7 @@ function assertManifestEntries(productDir, file, entries, suffixPredicate, suffi
8711
9786
  if (!suffixPredicate(entry)) {
8712
9787
  throw new Error(`${file} entry must ${suffixDescription}: ${entry}`);
8713
9788
  }
8714
- const absoluteEntry = join25(productDir, entry);
9789
+ const absoluteEntry = join26(productDir, entry);
8715
9790
  if (!existsSync4(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
8716
9791
  throw new Error(`${file} entry does not exist as a directory: ${entry}`);
8717
9792
  }
@@ -8970,8 +10045,8 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
8970
10045
  scope,
8971
10046
  scopeConfig: context.scopeConfig
8972
10047
  });
8973
- return new Promise((resolve5) => {
8974
- const localBin = join26(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
10048
+ return new Promise((resolve6) => {
10049
+ const localBin = join27(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
8975
10050
  const binary = existsSync5(localBin) ? localBin : "npx";
8976
10051
  const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
8977
10052
  const eslintProcess = spawnManagedSubprocess(runner, binary, spawnArgs, {
@@ -8980,13 +10055,13 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
8980
10055
  forwardValidationSubprocessOutput(eslintProcess, outputStreams);
8981
10056
  eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
8982
10057
  if (code === 0) {
8983
- resolve5({ success: true });
10058
+ resolve6({ success: true });
8984
10059
  } else {
8985
- resolve5({ success: false, error: `ESLint exited with code ${code}` });
10060
+ resolve6({ success: false, error: `ESLint exited with code ${code}` });
8986
10061
  }
8987
10062
  });
8988
10063
  eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
8989
- resolve5({ success: false, error: error.message });
10064
+ resolve6({ success: false, error: error.message });
8990
10065
  });
8991
10066
  });
8992
10067
  }
@@ -9073,7 +10148,7 @@ async function lintCommand(options) {
9073
10148
 
9074
10149
  // src/validation/literal/index.ts
9075
10150
  import { readFile as readFile8 } from "fs/promises";
9076
- import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve4 } from "path";
10151
+ import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve5 } from "path";
9077
10152
 
9078
10153
  // src/lib/file-inclusion/predicates/ignore-source.ts
9079
10154
  var IGNORE_SOURCE_LAYER = "ignore-source";
@@ -9108,7 +10183,7 @@ var ignoreSourceLayer = makeLayer(
9108
10183
 
9109
10184
  // src/lib/file-inclusion/pipeline.ts
9110
10185
  import { readdir as readdir7 } from "fs/promises";
9111
- import { join as join27, relative as relative4, sep as sep2 } from "path";
10186
+ import { join as join28, relative as relative4, sep as sep2 } from "path";
9112
10187
  var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
9113
10188
  function isNodeError3(err) {
9114
10189
  return err instanceof Error && "code" in err;
@@ -9126,10 +10201,10 @@ async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
9126
10201
  for (const entry of dirEntries) {
9127
10202
  if (entry.isDirectory()) {
9128
10203
  if (artifactDirs.has(entry.name)) continue;
9129
- const absolutePath = join27(absoluteDir, entry.name);
10204
+ const absolutePath = join28(absoluteDir, entry.name);
9130
10205
  await collectPaths(absolutePath, projectRoot, result, artifactDirs);
9131
10206
  } else if (entry.isFile()) {
9132
- const absolutePath = join27(absoluteDir, entry.name);
10207
+ const absolutePath = join28(absoluteDir, entry.name);
9133
10208
  const rel = relative4(projectRoot, absolutePath);
9134
10209
  result.push(sep2 === "/" ? rel : rel.split(sep2).join("/"));
9135
10210
  }
@@ -9536,7 +10611,7 @@ async function validateLiteralReuse(input) {
9536
10611
  const config = input.config ?? literalConfigDescriptor.defaults;
9537
10612
  const request = input.files ? {
9538
10613
  explicit: input.files.map((f) => {
9539
- const abs = isAbsolute5(f) ? f : resolve4(input.productDir, f);
10614
+ const abs = isAbsolute5(f) ? f : resolve5(input.productDir, f);
9540
10615
  return relative5(input.productDir, abs).split(/[\\/]/g).join("/");
9541
10616
  })
9542
10617
  } : { walkRoot: input.productDir };
@@ -9548,7 +10623,7 @@ async function validateLiteralReuse(input) {
9548
10623
  EMPTY_IGNORE_READER
9549
10624
  );
9550
10625
  const filtered = applyPathFilter2(scope.included, input.pathConfig);
9551
- const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve4(input.productDir, entry.path));
10626
+ const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve5(input.productDir, entry.path));
9552
10627
  const collectOptions = {
9553
10628
  visitorKeys: defaultVisitorKeys,
9554
10629
  minStringLength: config.minStringLength,
@@ -9789,7 +10864,7 @@ function formatLoc(loc) {
9789
10864
  // src/validation/steps/typescript.ts
9790
10865
  import { existsSync as existsSync6, mkdirSync, rmSync, writeFileSync } from "fs";
9791
10866
  import { mkdtemp as mkdtemp2 } from "fs/promises";
9792
- import { isAbsolute as isAbsolute6, join as join28 } from "path";
10867
+ import { isAbsolute as isAbsolute6, join as join29 } from "path";
9793
10868
  var defaultTypeScriptProcessRunner = lifecycleProcessRunner;
9794
10869
  var defaultTypeScriptDeps = {
9795
10870
  mkdtemp: mkdtemp2,
@@ -9802,9 +10877,9 @@ var TEMPORARY_TSCONFIG_COMPILER_OPTIONS = { noEmit: true };
9802
10877
  var TEMPORARY_TSCONFIG_PARENT_SEGMENTS = ["node_modules", ".cache", "spx"];
9803
10878
  var TEMPORARY_TSCONFIG_DIR_PREFIX = "validate-ts-";
9804
10879
  async function createTemporaryTsconfigDir(projectRoot, deps) {
9805
- const parent = join28(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
10880
+ const parent = join29(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
9806
10881
  deps.mkdirSync(parent, { recursive: true });
9807
- return deps.mkdtemp(join28(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
10882
+ return deps.mkdtemp(join29(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
9808
10883
  }
9809
10884
  function buildTypeScriptArgs(context) {
9810
10885
  const { scope, configFile } = context;
@@ -9812,11 +10887,11 @@ function buildTypeScriptArgs(context) {
9812
10887
  }
9813
10888
  async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defaultTypeScriptDeps) {
9814
10889
  const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
9815
- const configPath = join28(tempDir, "tsconfig.json");
10890
+ const configPath = join29(tempDir, "tsconfig.json");
9816
10891
  const baseConfigFile = TSCONFIG_FILES[scope];
9817
- const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join28(projectRoot, file));
10892
+ const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join29(projectRoot, file));
9818
10893
  const tempConfig = {
9819
- extends: join28(projectRoot, baseConfigFile),
10894
+ extends: join29(projectRoot, baseConfigFile),
9820
10895
  files: absoluteFiles,
9821
10896
  include: [],
9822
10897
  exclude: [],
@@ -9833,11 +10908,11 @@ async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defa
9833
10908
  }
9834
10909
  async function createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps = defaultTypeScriptDeps) {
9835
10910
  const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
9836
- const configPath = join28(tempDir, "tsconfig.json");
10911
+ const configPath = join29(tempDir, "tsconfig.json");
9837
10912
  const baseConfigFile = TSCONFIG_FILES[scope];
9838
- const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join28(projectRoot, pattern);
10913
+ const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join29(projectRoot, pattern);
9839
10914
  const tempConfig = {
9840
- extends: join28(projectRoot, baseConfigFile),
10915
+ extends: join29(projectRoot, baseConfigFile),
9841
10916
  include: scopeConfig.filePatterns.map(toProjectPathPattern),
9842
10917
  exclude: scopeConfig.excludePatterns.map(toProjectPathPattern),
9843
10918
  compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
@@ -9864,8 +10939,8 @@ async function validateTypeScript(context, options = {}) {
9864
10939
  if (files && files.length > 0) {
9865
10940
  const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, projectRoot, deps);
9866
10941
  try {
9867
- return await new Promise((resolve5) => {
9868
- const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
10942
+ return await new Promise((resolve6) => {
10943
+ const tscBin = join29(projectRoot, "node_modules", ".bin", "tsc");
9869
10944
  const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
9870
10945
  const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
9871
10946
  const tscProcess = spawnManagedSubprocess(runner, tscBinary, tscArgs2, {
@@ -9875,14 +10950,14 @@ async function validateTypeScript(context, options = {}) {
9875
10950
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
9876
10951
  cleanup();
9877
10952
  if (code === 0) {
9878
- resolve5({ success: true, skipped: false });
10953
+ resolve6({ success: true, skipped: false });
9879
10954
  } else {
9880
- resolve5({ success: false, error: `TypeScript exited with code ${code}` });
10955
+ resolve6({ success: false, error: `TypeScript exited with code ${code}` });
9881
10956
  }
9882
10957
  });
9883
10958
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
9884
10959
  cleanup();
9885
- resolve5({ success: false, error: error.message });
10960
+ resolve6({ success: false, error: error.message });
9886
10961
  });
9887
10962
  });
9888
10963
  } catch (error) {
@@ -9898,10 +10973,10 @@ async function validateTypeScript(context, options = {}) {
9898
10973
  return { success: true, skipped: true };
9899
10974
  }
9900
10975
  const { configPath, cleanup } = await createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps);
9901
- const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
10976
+ const tscBin = join29(projectRoot, "node_modules", ".bin", "tsc");
9902
10977
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
9903
10978
  tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
9904
- return new Promise((resolve5) => {
10979
+ return new Promise((resolve6) => {
9905
10980
  const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
9906
10981
  cwd: projectRoot
9907
10982
  });
@@ -9909,36 +10984,36 @@ async function validateTypeScript(context, options = {}) {
9909
10984
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
9910
10985
  cleanup();
9911
10986
  if (code === 0) {
9912
- resolve5({ success: true, skipped: false });
10987
+ resolve6({ success: true, skipped: false });
9913
10988
  } else {
9914
- resolve5({ success: false, error: `TypeScript exited with code ${code}` });
10989
+ resolve6({ success: false, error: `TypeScript exited with code ${code}` });
9915
10990
  }
9916
10991
  });
9917
10992
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
9918
10993
  cleanup();
9919
- resolve5({ success: false, error: error.message });
10994
+ resolve6({ success: false, error: error.message });
9920
10995
  });
9921
10996
  });
9922
10997
  } else {
9923
- const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
10998
+ const tscBin = join29(projectRoot, "node_modules", ".bin", "tsc");
9924
10999
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
9925
11000
  const rawArgs = buildTypeScriptArgs({ scope, configFile });
9926
11001
  tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
9927
11002
  }
9928
- return new Promise((resolve5) => {
11003
+ return new Promise((resolve6) => {
9929
11004
  const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
9930
11005
  cwd: projectRoot
9931
11006
  });
9932
11007
  forwardValidationSubprocessOutput(tscProcess, outputStreams);
9933
11008
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
9934
11009
  if (code === 0) {
9935
- resolve5({ success: true, skipped: false });
11010
+ resolve6({ success: true, skipped: false });
9936
11011
  } else {
9937
- resolve5({ success: false, error: `TypeScript exited with code ${code}` });
11012
+ resolve6({ success: false, error: `TypeScript exited with code ${code}` });
9938
11013
  }
9939
11014
  });
9940
11015
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
9941
- resolve5({ success: false, error: error.message });
11016
+ resolve6({ success: false, error: error.message });
9942
11017
  });
9943
11018
  });
9944
11019
  }
@@ -10013,6 +11088,13 @@ async function typescriptCommand(options) {
10013
11088
 
10014
11089
  // src/validation/languages/typescript.ts
10015
11090
  var TYPESCRIPT_LANGUAGE_NAME = "typescript";
11091
+ async function runCircularStage(context) {
11092
+ if (context.skipCircular) {
11093
+ const skipOutput = context.json ? CIRCULAR_SKIP_JSON_OUTPUT : CIRCULAR_SKIP_OUTPUT;
11094
+ return { exitCode: 0, output: context.quiet ? "" : skipOutput };
11095
+ }
11096
+ return circularCommand({ cwd: context.cwd, quiet: context.quiet, json: context.json });
11097
+ }
10016
11098
  async function runLiteralStage(context) {
10017
11099
  if (context.skipLiteral) {
10018
11100
  const skipOutput = context.json ? LITERAL_SKIP_JSON_OUTPUT : LITERAL_SKIP_OUTPUT;
@@ -10031,7 +11113,7 @@ var typescriptValidationLanguage = {
10031
11113
  {
10032
11114
  name: VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR,
10033
11115
  failsPipeline: true,
10034
- run: (context) => circularCommand({ cwd: context.cwd, quiet: context.quiet, json: context.json })
11116
+ run: runCircularStage
10035
11117
  },
10036
11118
  {
10037
11119
  name: VALIDATION_STAGE_DISPLAY_NAMES.KNIP,
@@ -10111,11 +11193,11 @@ function formatStepWithTiming(stepNumber, result, quiet) {
10111
11193
  return `[${stepNumber}/${VALIDATION_PIPELINE_TOTAL_STEPS}] ${result.output}${timing}`;
10112
11194
  }
10113
11195
  async function allCommand(options) {
10114
- const { cwd, scope, files, fix, quiet = false, json, skipLiteral = false } = options;
11196
+ const { cwd, scope, files, fix, quiet = false, json, skipCircular = false, skipLiteral = false } = options;
10115
11197
  const startTime = Date.now();
10116
11198
  const outputs = [];
10117
11199
  let hasFailure = false;
10118
- const context = { cwd, scope, files, fix, quiet, json, skipLiteral };
11200
+ const context = { cwd, scope, files, fix, quiet, json, skipCircular, skipLiteral };
10119
11201
  let stepNumber = 0;
10120
11202
  for (const stage of validationPipelineStages) {
10121
11203
  stepNumber += 1;
@@ -10136,51 +11218,9 @@ async function allCommand(options) {
10136
11218
  };
10137
11219
  }
10138
11220
 
10139
- // src/interfaces/cli/sanitize.ts
10140
- var MAX_CLI_ARGUMENT_DISPLAY_LENGTH = 120;
10141
- var ELLIPSIS_TOKEN = "...";
10142
- var SENTINEL_UNDEFINED = "<undefined>";
10143
- var SENTINEL_NULL = "<null>";
10144
- var SENTINEL_EMPTY = "<empty>";
10145
- var CONTROL_CHAR_UPPER_BOUND = 31;
10146
- var DEL_CHAR_CODE = 127;
10147
- var HEX_RADIX = 16;
10148
- var HEX_PAD = 2;
10149
- function formatHexEscape(code) {
10150
- return `\\x${code.toString(HEX_RADIX).padStart(HEX_PAD, "0")}`;
10151
- }
10152
- function nonStringSentinel(type) {
10153
- return `<non-string:${type}>`;
10154
- }
10155
- function sanitizeCliArgument(input) {
10156
- if (input === void 0) return SENTINEL_UNDEFINED;
10157
- if (input === null) return SENTINEL_NULL;
10158
- if (typeof input !== "string") return nonStringSentinel(typeof input);
10159
- if (input.length === 0) return SENTINEL_EMPTY;
10160
- const escaped = escapeControlCharacters(input);
10161
- return truncate(escaped);
10162
- }
10163
- function escapeControlCharacters(value) {
10164
- let out = "";
10165
- for (const char of value) {
10166
- const code = char.codePointAt(0);
10167
- if (code === void 0) continue;
10168
- if (code <= CONTROL_CHAR_UPPER_BOUND || code === DEL_CHAR_CODE) {
10169
- out += formatHexEscape(code);
10170
- } else {
10171
- out += char;
10172
- }
10173
- }
10174
- return out;
10175
- }
10176
- function truncate(value) {
10177
- if (value.length <= MAX_CLI_ARGUMENT_DISPLAY_LENGTH) return value;
10178
- return value.slice(0, MAX_CLI_ARGUMENT_DISPLAY_LENGTH - ELLIPSIS_TOKEN.length) + ELLIPSIS_TOKEN;
10179
- }
10180
-
10181
11221
  // src/validation/literal/allowlist-existing.ts
10182
11222
  import { rename as rename4, writeFile as writeFile4 } from "fs/promises";
10183
- import { dirname as dirname7, join as join29 } from "path";
11223
+ import { dirname as dirname8, join as join30 } from "path";
10184
11224
  var EXIT_OK = 0;
10185
11225
  var EXIT_ERROR = 1;
10186
11226
  var INCLUDE_FIELD = "include";
@@ -10200,9 +11240,9 @@ var productionReader = {
10200
11240
  };
10201
11241
  var productionWriter = {
10202
11242
  async write(filePath, content) {
10203
- const dir = dirname7(filePath);
11243
+ const dir = dirname8(filePath);
10204
11244
  const random = Math.random().toString(RANDOM_BASE).slice(RANDOM_PREFIX_SLICE, RANDOM_PREFIX_SLICE + RANDOM_TOKEN_LENGTH);
10205
- const tmpPath = join29(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
11245
+ const tmpPath = join30(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
10206
11246
  await writeFile4(tmpPath, content, "utf8");
10207
11247
  await rename4(tmpPath, filePath);
10208
11248
  }
@@ -10352,6 +11392,10 @@ var literalValidationCliOptions = {
10352
11392
  }
10353
11393
  };
10354
11394
  var allValidationCliOptions = {
11395
+ skipCircular: {
11396
+ flag: "--skip-circular",
11397
+ description: "Skip circular dependency detection for this validation all run"
11398
+ },
10355
11399
  skipLiteral: {
10356
11400
  flag: "--skip-literal",
10357
11401
  description: "Skip literal reuse detection for this validation all run"
@@ -10480,12 +11524,13 @@ function registerValidationCommands(validationCmd) {
10480
11524
  process.exit(result.exitCode);
10481
11525
  });
10482
11526
  addCommonOptions(markdownCmd);
10483
- const allCmd = addValidationSubcommand(validationCmd, subcommands.all).option("--fix", "Auto-fix ESLint issues").option(allValidationCliOptions.skipLiteral.flag, allValidationCliOptions.skipLiteral.description).action(async (options) => {
11527
+ const allCmd = addValidationSubcommand(validationCmd, subcommands.all).option("--fix", "Auto-fix ESLint issues").option(allValidationCliOptions.skipCircular.flag, allValidationCliOptions.skipCircular.description).option(allValidationCliOptions.skipLiteral.flag, allValidationCliOptions.skipLiteral.description).action(async (options) => {
10484
11528
  const result = await allCommand({
10485
11529
  cwd: process.cwd(),
10486
11530
  scope: options.scope,
10487
11531
  files: options.files,
10488
11532
  fix: options.fix,
11533
+ skipCircular: options.skipCircular,
10489
11534
  skipLiteral: options.skipLiteral,
10490
11535
  quiet: options.quiet,
10491
11536
  json: options.json
@@ -10523,15 +11568,159 @@ var validationDomain = {
10523
11568
  }
10524
11569
  };
10525
11570
 
11571
+ // src/commands/worktree/claim.ts
11572
+ async function claimCommand(options) {
11573
+ return claimWorktreeOccupancy(options);
11574
+ }
11575
+
11576
+ // src/commands/worktree/release.ts
11577
+ async function releaseCommand2(options) {
11578
+ const worktreesDir = await resolveWorktreesDir(options);
11579
+ const name = await resolveCurrentWorktreeName(options);
11580
+ return removeClaim(worktreesDir, name, { fs: options.fs });
11581
+ }
11582
+
11583
+ // src/commands/worktree/status.ts
11584
+ var WORKTREE_STATUS_FORMAT = {
11585
+ JSON: "json",
11586
+ TEXT: "text"
11587
+ };
11588
+ async function statusCommand2(options) {
11589
+ const multiTargetRequest = options.worktrees !== void 0 && options.worktrees.length > 1;
11590
+ const targets = await resolveStatusTargets(options);
11591
+ if (!targets.ok) return targets;
11592
+ const records = [];
11593
+ for (const target of targets.value) {
11594
+ const worktreesDir = await resolveWorktreesDir({ ...options, cwd: target.worktreeRoot });
11595
+ const occupancy = await readOccupancy(worktreesDir, target.name, options.processTable, { fs: options.fs });
11596
+ if (!occupancy.ok) return occupancy;
11597
+ records.push({ worktree: target.name, status: occupancy.value });
11598
+ }
11599
+ return { ok: true, value: renderStatus(records, options.format, multiTargetRequest) };
11600
+ }
11601
+ async function resolveStatusTargets(options) {
11602
+ const requested = options.worktrees;
11603
+ if (requested === void 0 || requested.length === 0) {
11604
+ const target = await resolveTargetWorktree(options);
11605
+ if (!target.ok) return target;
11606
+ return { ok: true, value: [target.value] };
11607
+ }
11608
+ const targets = [];
11609
+ let firstError;
11610
+ for (const worktree of requested) {
11611
+ const target = await resolveTargetWorktree({ ...options, worktree });
11612
+ if (target.ok) {
11613
+ targets.push(target.value);
11614
+ } else {
11615
+ firstError ??= target.error;
11616
+ }
11617
+ }
11618
+ if (targets.length === 0) {
11619
+ return { ok: false, error: firstError ?? "no worktree status targets resolved" };
11620
+ }
11621
+ return { ok: true, value: targets };
11622
+ }
11623
+ function renderStatus(records, format2, multiTargetRequest) {
11624
+ if (format2 === WORKTREE_STATUS_FORMAT.JSON) {
11625
+ return JSON.stringify(multiTargetRequest ? records : records[0]);
11626
+ }
11627
+ return records.map(renderTextStatus).join("\n");
11628
+ }
11629
+ function renderTextStatus(record) {
11630
+ return `${record.worktree} ${record.status}`;
11631
+ }
11632
+
11633
+ // src/lib/worktree-path-info.ts
11634
+ import { stat as stat4 } from "fs/promises";
11635
+ var defaultWorktreePathInfo = {
11636
+ isExistingNonDirectory: async (path6) => {
11637
+ try {
11638
+ const pathStats = await stat4(path6);
11639
+ return !pathStats.isDirectory();
11640
+ } catch {
11641
+ return false;
11642
+ }
11643
+ }
11644
+ };
11645
+
11646
+ // src/interfaces/cli/worktree.ts
11647
+ var WORKTREE_CLI = {
11648
+ COMMAND: "worktree",
11649
+ CLAIM: "claim",
11650
+ STATUS: "status",
11651
+ RELEASE: "release",
11652
+ WORKTREE_ARGUMENT: "[worktrees...]",
11653
+ SESSION_ID_FLAG: "--session-id",
11654
+ FORMAT_FLAG: "--format",
11655
+ WORKTREES_DIR_FLAG: "--worktrees-dir"
11656
+ };
11657
+ var WORKTREE_DOMAIN_DESCRIPTION = "Coordinate worktree occupancy across a bare-repository pool";
11658
+ function handleError2(error) {
11659
+ console.error("Error:", error);
11660
+ process.exit(1);
11661
+ }
11662
+ function registerWorktreeCommands(worktreeCmd) {
11663
+ 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) => {
11664
+ const result = await claimCommand({
11665
+ claimWriteToken: createClaimWriteToken(),
11666
+ cwd: process.cwd(),
11667
+ env: process.env,
11668
+ fs: defaultOccupancyFileSystem,
11669
+ gitDeps: defaultGitDependencies,
11670
+ processTable: defaultProcessTable,
11671
+ selfPid: process.pid,
11672
+ sessionId: options.sessionId,
11673
+ worktreesDir: options.worktreesDir,
11674
+ onWarning: writeWarning
11675
+ });
11676
+ if (!result.ok) handleError2(result.error);
11677
+ });
11678
+ 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) => {
11679
+ const result = await statusCommand2({
11680
+ cwd: process.cwd(),
11681
+ fs: defaultOccupancyFileSystem,
11682
+ gitDeps: defaultGitDependencies,
11683
+ worktrees,
11684
+ format: options.format,
11685
+ pathInfo: defaultWorktreePathInfo,
11686
+ processTable: defaultProcessTable,
11687
+ worktreesDir: options.worktreesDir,
11688
+ onWarning: writeWarning
11689
+ });
11690
+ if (!result.ok) handleError2(result.error);
11691
+ console.log(result.value);
11692
+ });
11693
+ 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) => {
11694
+ const result = await releaseCommand2({
11695
+ cwd: process.cwd(),
11696
+ fs: defaultOccupancyFileSystem,
11697
+ gitDeps: defaultGitDependencies,
11698
+ worktreesDir: options.worktreesDir,
11699
+ onWarning: writeWarning
11700
+ });
11701
+ if (!result.ok) handleError2(result.error);
11702
+ });
11703
+ }
11704
+ var worktreeDomain = {
11705
+ name: WORKTREE_CLI.COMMAND,
11706
+ description: WORKTREE_DOMAIN_DESCRIPTION,
11707
+ register: (program2) => {
11708
+ const worktreeCmd = program2.command(WORKTREE_CLI.COMMAND).description(WORKTREE_DOMAIN_DESCRIPTION);
11709
+ registerWorktreeCommands(worktreeCmd);
11710
+ }
11711
+ };
11712
+
10526
11713
  // src/interfaces/cli/registry.ts
10527
11714
  var CLI_DOMAINS = [
10528
11715
  claudeDomain,
10529
11716
  compactDomain,
10530
11717
  configDomain,
11718
+ hookDomain,
10531
11719
  sessionDomain,
10532
11720
  specDomain,
10533
11721
  testingDomain,
10534
- validationDomain
11722
+ validationDomain,
11723
+ worktreeDomain
10535
11724
  ];
10536
11725
 
10537
11726
  // src/cli.ts