@outcomeeng/spx 0.6.3 → 0.6.4

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
@@ -1,11 +1,100 @@
1
1
  // src/cli.ts
2
- import { Command } from "commander";
3
2
  import { createRequire as createRequire2 } from "module";
4
3
 
4
+ // src/interfaces/cli/program.ts
5
+ import { Command } from "commander";
6
+
7
+ // src/domains/config/cwd.ts
8
+ var CONFIG_PROCESS_CWD = {
9
+ read: () => process.cwd()
10
+ };
11
+
12
+ // src/domains/config/root.ts
13
+ import { execFileSync } from "child_process";
14
+ import { resolve } from "path";
15
+ var GIT_EXECUTABLE = "git";
16
+ var GIT_TOPLEVEL_ARGS = ["rev-parse", "--show-toplevel"];
17
+ var GIT_NOT_REPO_MARKER = "not a git repository";
18
+ function resolveProductDir(cwd, deps = DEFAULT_PRODUCT_DIR_RESOLVER_DEPS) {
19
+ const resolvedCwd = resolve(cwd);
20
+ const toplevel = deps.readGitToplevel(resolvedCwd);
21
+ if (toplevel !== void 0 && toplevel.length > 0) {
22
+ return { productDir: resolve(toplevel) };
23
+ }
24
+ return {
25
+ productDir: resolvedCwd,
26
+ warning: `warning: ${resolvedCwd} is not inside a git worktree \u2014 falling back to the current working directory. ${GIT_NOT_REPO_MARKER}.`
27
+ };
28
+ }
29
+ var DEFAULT_PRODUCT_DIR_RESOLVER_DEPS = {
30
+ readGitToplevel: (cwd) => {
31
+ try {
32
+ return execFileSync(GIT_EXECUTABLE, [...GIT_TOPLEVEL_ARGS], {
33
+ // NOSONAR - spx intentionally uses the caller's git executable.
34
+ cwd,
35
+ stdio: ["ignore", "pipe", "pipe"],
36
+ encoding: "utf8"
37
+ }).trim();
38
+ } catch {
39
+ return void 0;
40
+ }
41
+ }
42
+ };
43
+
44
+ // src/interfaces/cli/product-context.ts
45
+ import { resolve as resolve2 } from "path";
46
+ var SPX_GLOBAL_OPTIONS = {
47
+ directory: {
48
+ short: "-C",
49
+ long: "--directory",
50
+ operand: "<path>",
51
+ flags: "-C, --directory <path>",
52
+ description: "Run as if spx was started in <path>"
53
+ }
54
+ };
55
+ function createCliInvocation(options) {
56
+ let cachedEffectiveInvocationDir;
57
+ let cachedContext;
58
+ const resolveEffectiveInvocationDir = () => {
59
+ cachedEffectiveInvocationDir ??= resolve2(options.processCwd(), options.readDirectoryOption() ?? "");
60
+ return cachedEffectiveInvocationDir;
61
+ };
62
+ return {
63
+ io: options.io,
64
+ resolveEffectiveInvocationDir,
65
+ resolveProductContext: () => {
66
+ if (cachedContext !== void 0) {
67
+ return cachedContext;
68
+ }
69
+ const effectiveInvocationDir = resolveEffectiveInvocationDir();
70
+ const resolved = (options.resolveProductDir ?? resolveProductDir)(effectiveInvocationDir);
71
+ cachedContext = {
72
+ effectiveInvocationDir,
73
+ productDir: resolved.productDir,
74
+ ...resolved.warning === void 0 ? {} : { warning: resolved.warning }
75
+ };
76
+ options.writeWarning(cachedContext.warning);
77
+ return cachedContext;
78
+ }
79
+ };
80
+ }
81
+ var DEFAULT_CLI_IO = {
82
+ writeStdout: (output) => {
83
+ process.stdout.write(output);
84
+ },
85
+ writeStderr: (output) => {
86
+ process.stderr.write(output);
87
+ },
88
+ setExitCode: (exitCode) => {
89
+ process.exitCode = exitCode;
90
+ },
91
+ exit: (exitCode) => process.exit(exitCode)
92
+ };
93
+
5
94
  // src/commands/claude/init.ts
6
95
  import { execa } from "execa";
7
96
  async function initCommand(options = {}) {
8
- const cwd = options.cwd || process.cwd();
97
+ const cwd = options.cwd || CONFIG_PROCESS_CWD.read();
9
98
  try {
10
99
  const { stdout: listOutput } = await execa(
11
100
  "claude",
@@ -115,7 +204,7 @@ import { dirname as dirname2, join as join2 } from "path";
115
204
 
116
205
  // src/git/root.ts
117
206
  import { execa as execa2 } from "execa";
118
- import { basename, dirname, isAbsolute, join, resolve } from "path";
207
+ import { basename, dirname, isAbsolute, join, resolve as resolve3 } from "path";
119
208
 
120
209
  // src/git/environment.ts
121
210
  function withoutGitEnvironment(env) {
@@ -223,7 +312,7 @@ var GIT_WORKTREE_PORCELAIN_BARE_LINE = "bare";
223
312
  var GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE = "prunable";
224
313
  var GIT_WORKTREE_PORCELAIN_PRUNABLE_PREFIX = `${GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE} `;
225
314
  var TRAILING_PATH_SEPARATORS_PATTERN = /[\\/]+$/;
226
- async function detectWorktreeProductRoot(cwd = process.cwd(), deps = defaultGitDependencies) {
315
+ async function detectWorktreeProductRoot(cwd = CONFIG_PROCESS_CWD.read(), deps = defaultGitDependencies) {
227
316
  try {
228
317
  const result = await deps.execa(
229
318
  GIT_ROOT_COMMAND.EXECUTABLE,
@@ -259,7 +348,7 @@ function trimStdout(stdout) {
259
348
  const str = typeof stdout === "string" ? stdout : String(stdout);
260
349
  return str.trim();
261
350
  }
262
- async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = defaultGitDependencies) {
351
+ async function detectGitCommonDirProductRoot(cwd = CONFIG_PROCESS_CWD.read(), deps = defaultGitDependencies) {
263
352
  try {
264
353
  const toplevelResult = await deps.execa(
265
354
  GIT_ROOT_COMMAND.EXECUTABLE,
@@ -288,7 +377,7 @@ async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = default
288
377
  };
289
378
  }
290
379
  const commonDir = extractStdout(commonDirResult.stdout);
291
- const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve(toplevel, commonDir);
380
+ const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve3(toplevel, commonDir);
292
381
  const gitCommonDirProductRoot = dirname(absoluteCommonDir);
293
382
  return {
294
383
  productDir: gitCommonDirProductRoot,
@@ -304,7 +393,7 @@ async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = default
304
393
  };
305
394
  }
306
395
  }
307
- async function resolveDefaultBranch(cwd = process.cwd(), deps = defaultGitDependencies) {
396
+ async function resolveDefaultBranch(cwd = CONFIG_PROCESS_CWD.read(), deps = defaultGitDependencies) {
308
397
  const result = await deps.execa(
309
398
  GIT_ROOT_COMMAND.EXECUTABLE,
310
399
  [...GIT_ORIGIN_HEAD_REF_ARGS],
@@ -316,7 +405,7 @@ async function resolveDefaultBranch(cwd = process.cwd(), deps = defaultGitDepend
316
405
  const branch = ref.slice(ORIGIN_REF_PREFIX.length);
317
406
  return branch.length === 0 ? null : branch;
318
407
  }
319
- async function getCurrentBranch(cwd = process.cwd(), deps = defaultGitDependencies) {
408
+ async function getCurrentBranch(cwd = CONFIG_PROCESS_CWD.read(), deps = defaultGitDependencies) {
320
409
  const result = await deps.execa(
321
410
  GIT_ROOT_COMMAND.EXECUTABLE,
322
411
  [...GIT_CURRENT_BRANCH_ARGS],
@@ -327,7 +416,7 @@ async function getCurrentBranch(cwd = process.cwd(), deps = defaultGitDependenci
327
416
  if (branch.length === 0 || branch === GIT_ROOT_COMMAND.HEAD) return null;
328
417
  return branch;
329
418
  }
330
- async function getHeadSha(cwd = process.cwd(), deps = defaultGitDependencies) {
419
+ async function getHeadSha(cwd = CONFIG_PROCESS_CWD.read(), deps = defaultGitDependencies) {
331
420
  const result = await deps.execa(
332
421
  GIT_ROOT_COMMAND.EXECUTABLE,
333
422
  [...GIT_HEAD_SHA_ARGS],
@@ -337,7 +426,7 @@ async function getHeadSha(cwd = process.cwd(), deps = defaultGitDependencies) {
337
426
  const sha = extractStdout(result.stdout);
338
427
  return sha.length === 0 ? null : sha;
339
428
  }
340
- async function resolveRefSha(ref, cwd = process.cwd(), deps = defaultGitDependencies) {
429
+ async function resolveRefSha(ref, cwd = CONFIG_PROCESS_CWD.read(), deps = defaultGitDependencies) {
341
430
  const result = await deps.execa(
342
431
  GIT_ROOT_COMMAND.EXECUTABLE,
343
432
  [GIT_ROOT_COMMAND.REV_PARSE, ref],
@@ -347,7 +436,7 @@ async function resolveRefSha(ref, cwd = process.cwd(), deps = defaultGitDependen
347
436
  const sha = extractStdout(result.stdout);
348
437
  return sha.length === 0 ? null : sha;
349
438
  }
350
- async function originBranchExists(branch, cwd = process.cwd(), deps = defaultGitDependencies) {
439
+ async function originBranchExists(branch, cwd = CONFIG_PROCESS_CWD.read(), deps = defaultGitDependencies) {
351
440
  if (branch === GIT_ROOT_COMMAND.HEAD) return false;
352
441
  const result = await deps.execa(
353
442
  GIT_ROOT_COMMAND.EXECUTABLE,
@@ -361,7 +450,7 @@ async function originBranchExists(branch, cwd = process.cwd(), deps = defaultGit
361
450
  );
362
451
  return result.exitCode === 0;
363
452
  }
364
- async function isWorkingTreeClean(cwd = process.cwd(), deps = defaultGitDependencies) {
453
+ async function isWorkingTreeClean(cwd = CONFIG_PROCESS_CWD.read(), deps = defaultGitDependencies) {
365
454
  const result = await deps.execa(
366
455
  GIT_ROOT_COMMAND.EXECUTABLE,
367
456
  [...GIT_STATUS_PORCELAIN_ARGS],
@@ -427,7 +516,7 @@ function mainCheckoutPath(facts) {
427
516
  const candidate = join(commonDirParent, name);
428
517
  return isObservedWorktreeRoot(facts.worktreeRoots, candidate) ? candidate : null;
429
518
  }
430
- async function gatherGitFacts(cwd = process.cwd(), deps = defaultGitDependencies) {
519
+ async function gatherGitFacts(cwd = CONFIG_PROCESS_CWD.read(), deps = defaultGitDependencies) {
431
520
  try {
432
521
  const [toplevelResult, commonDirResult, originResult, worktreeListResult] = await Promise.all([
433
522
  deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_SHOW_TOPLEVEL_ARGS], { cwd, reject: false }),
@@ -451,7 +540,7 @@ async function gatherGitFacts(cwd = process.cwd(), deps = defaultGitDependencies
451
540
  };
452
541
  }
453
542
  const rawCommonDir = extractStdout(commonDirResult.stdout);
454
- const commonDir = isAbsolute(rawCommonDir) ? rawCommonDir : resolve(worktreeRoot, rawCommonDir);
543
+ const commonDir = isAbsolute(rawCommonDir) ? rawCommonDir : resolve3(worktreeRoot, rawCommonDir);
455
544
  const bareResult = await deps.execa(
456
545
  GIT_ROOT_COMMAND.EXECUTABLE,
457
546
  [...GIT_CORE_BARE_ARGS],
@@ -1307,17 +1396,35 @@ Searched for: **/.claude/settings.local.json`;
1307
1396
  }
1308
1397
 
1309
1398
  // src/interfaces/cli/claude.ts
1310
- function registerClaudeCommands(claudeCmd) {
1399
+ function formatError(error) {
1400
+ if (error instanceof Error) {
1401
+ return error.message;
1402
+ }
1403
+ if (typeof error === "string") {
1404
+ return error;
1405
+ }
1406
+ return "Unknown error";
1407
+ }
1408
+ function writeOutput(io, output) {
1409
+ io.writeStdout(`${output}
1410
+ `);
1411
+ }
1412
+ function writeError(io, output) {
1413
+ io.writeStderr(`${output}
1414
+ `);
1415
+ }
1416
+ function exitWithError(io, error) {
1417
+ writeError(io, `Error: ${formatError(error)}`);
1418
+ return io.exit(1);
1419
+ }
1420
+ function registerClaudeCommands(claudeCmd, invocation) {
1421
+ const productDir = () => invocation.resolveProductContext().productDir;
1311
1422
  claudeCmd.command("init").description("Initialize or update outcomeeng marketplace plugin").action(async () => {
1312
1423
  try {
1313
- const output = await initCommand({ cwd: process.cwd() });
1314
- console.log(output);
1424
+ const output = await initCommand({ cwd: productDir() });
1425
+ writeOutput(invocation.io, output);
1315
1426
  } catch (error) {
1316
- console.error(
1317
- "Error:",
1318
- error instanceof Error ? error.message : String(error)
1319
- );
1320
- process.exit(1);
1427
+ exitWithError(invocation.io, error);
1321
1428
  }
1322
1429
  });
1323
1430
  const settingsCmd = claudeCmd.command("settings").description("Manage Claude Code settings");
@@ -1336,10 +1443,11 @@ function registerClaudeCommands(claudeCmd) {
1336
1443
  async (options) => {
1337
1444
  try {
1338
1445
  if (options.write && options.outputFile) {
1339
- console.error(
1446
+ writeError(
1447
+ invocation.io,
1340
1448
  "Error: --write and --output-file are mutually exclusive\nUse --write to modify global settings, or --output-file to write to a different location"
1341
1449
  );
1342
- process.exit(1);
1450
+ return invocation.io.exit(1);
1343
1451
  }
1344
1452
  const output = await consolidateCommand({
1345
1453
  write: options.write,
@@ -1347,13 +1455,9 @@ function registerClaudeCommands(claudeCmd) {
1347
1455
  root: options.root,
1348
1456
  globalSettings: options.globalSettings
1349
1457
  });
1350
- console.log(output);
1458
+ writeOutput(invocation.io, output);
1351
1459
  } catch (error) {
1352
- console.error(
1353
- "Error:",
1354
- error instanceof Error ? error.message : String(error)
1355
- );
1356
- process.exit(1);
1460
+ exitWithError(invocation.io, error);
1357
1461
  }
1358
1462
  }
1359
1463
  );
@@ -1361,9 +1465,9 @@ function registerClaudeCommands(claudeCmd) {
1361
1465
  var claudeDomain = {
1362
1466
  name: "claude",
1363
1467
  description: "Manage Claude Code settings and plugins",
1364
- register: (program2) => {
1365
- const claudeCmd = program2.command("claude").description("Manage Claude Code settings and plugins");
1366
- registerClaudeCommands(claudeCmd);
1468
+ register: (program, invocation) => {
1469
+ const claudeCmd = program.command("claude").description("Manage Claude Code settings and plugins");
1470
+ registerClaudeCommands(claudeCmd, invocation);
1367
1471
  }
1368
1472
  };
1369
1473
 
@@ -1520,8 +1624,8 @@ function readTargetPath(value, startIndex) {
1520
1624
  return value.slice(startIndex, endIndex);
1521
1625
  }
1522
1626
  function isTargetPathCharacter(character) {
1523
- const code = character.codePointAt(0);
1524
- return code !== void 0 && (code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || character === "." || character === "_" || character === "-" || character === "/");
1627
+ const code = character.codePointAt(0) ?? 0;
1628
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || character === "." || character === "_" || character === "-" || character === "/";
1525
1629
  }
1526
1630
 
1527
1631
  // src/commands/compact/retrieve.ts
@@ -1576,14 +1680,15 @@ var COMPACT_CLI = {
1576
1680
  var compactDomain = {
1577
1681
  name: COMPACT_CLI.commandName,
1578
1682
  description: COMPACT_CLI.description,
1579
- register: (program2) => {
1580
- const compactCmd = program2.command(COMPACT_CLI.commandName).description(COMPACT_CLI.description);
1683
+ register: (program, invocation) => {
1684
+ const effectiveInvocationDir = () => invocation.resolveEffectiveInvocationDir();
1685
+ const compactCmd = program.command(COMPACT_CLI.commandName).description(COMPACT_CLI.description);
1581
1686
  compactCmd.command(COMPACT_CLI.storeCommandName).description("Store compact resume state").requiredOption(COMPACT_CLI.transcriptOption, "Transcript JSONL path").option(COMPACT_CLI.sessionIdOption, COMPACT_CLI.sessionIdDescription).action(async (options) => {
1582
- process.exit(
1687
+ invocation.io.exit(
1583
1688
  await compactStoreCommand({
1584
1689
  transcript: options.transcript,
1585
1690
  sessionId: options.sessionId,
1586
- cwd: process.cwd(),
1691
+ cwd: effectiveInvocationDir(),
1587
1692
  env: process.env
1588
1693
  })
1589
1694
  );
@@ -1591,13 +1696,13 @@ var compactDomain = {
1591
1696
  compactCmd.command(COMPACT_CLI.retrieveCommandName).description("Retrieve compact resume state").option(COMPACT_CLI.sessionIdOption, COMPACT_CLI.sessionIdDescription).action(async (options) => {
1592
1697
  const result = await compactRetrieveCommand({
1593
1698
  sessionId: options.sessionId,
1594
- cwd: process.cwd(),
1699
+ cwd: effectiveInvocationDir(),
1595
1700
  env: process.env
1596
1701
  });
1597
1702
  if (result.output.length > 0) {
1598
- process.stdout.write(result.output);
1703
+ invocation.io.writeStdout(result.output);
1599
1704
  }
1600
- process.exitCode = result.exitCode;
1705
+ invocation.io.setExitCode(result.exitCode);
1601
1706
  });
1602
1707
  }
1603
1708
  };
@@ -1614,7 +1719,8 @@ function toMessage(error) {
1614
1719
  if (typeof error === "string") return error;
1615
1720
  const fallback = `[${typeof error}]`;
1616
1721
  try {
1617
- return JSON.stringify(error) ?? fallback;
1722
+ const serialized = JSON.stringify(error);
1723
+ return typeof serialized === "string" ? serialized : fallback;
1618
1724
  } catch {
1619
1725
  return fallback;
1620
1726
  }
@@ -2290,11 +2396,11 @@ function compareNamingSchemaVersions(left, right) {
2290
2396
  return compareNumericVersionIdentifiers(left.version, right.version);
2291
2397
  }
2292
2398
  function canonicalNamingSchemaVersion(versions) {
2293
- const [first, ...rest] = versions;
2399
+ const first = versions.at(0);
2294
2400
  if (first === void 0) {
2295
2401
  throw new Error("Naming-schema version tuple must declare at least one version");
2296
2402
  }
2297
- return rest.reduce(
2403
+ return versions.slice(1).reduce(
2298
2404
  (max, version2) => compareNamingSchemaVersions(version2, max) > VERSION_ORDER_EQUAL ? version2 : max,
2299
2405
  first
2300
2406
  );
@@ -3475,9 +3581,6 @@ function defaultsCommand(options, deps) {
3475
3581
  function formatConfig(config, asJson) {
3476
3582
  const format2 = asJson ? CONFIG_FILE_FORMAT.JSON : DEFAULT_CONFIG_FILE_FORMAT;
3477
3583
  const serialized = serializeConfigFileSections(format2, config);
3478
- if (!serialized.ok) {
3479
- throw new Error(serialized.error);
3480
- }
3481
3584
  return serialized.value;
3482
3585
  }
3483
3586
 
@@ -3503,9 +3606,6 @@ async function showCommand(options, deps) {
3503
3606
  function formatConfig2(config, asJson) {
3504
3607
  const format2 = asJson ? CONFIG_FILE_FORMAT.JSON : DEFAULT_CONFIG_FILE_FORMAT;
3505
3608
  const serialized = serializeConfigFileSections(format2, config);
3506
- if (!serialized.ok) {
3507
- throw new Error(serialized.error);
3508
- }
3509
3609
  return serialized.value;
3510
3610
  }
3511
3611
 
@@ -3549,82 +3649,55 @@ async function validateCommand(_options, deps) {
3549
3649
  return { stdout, stderr: "", exitCode: 0 };
3550
3650
  }
3551
3651
 
3552
- // src/domains/config/root.ts
3553
- import { execSync } from "child_process";
3554
- import { resolve as resolve2 } from "path";
3555
- var GIT_TOPLEVEL_CMD = "git rev-parse --show-toplevel";
3556
- var GIT_NOT_REPO_MARKER = "not a git repository";
3557
- function resolveProductDir(cwd = process.cwd()) {
3558
- const resolvedCwd = resolve2(cwd);
3559
- try {
3560
- const stdout = execSync(GIT_TOPLEVEL_CMD, {
3561
- cwd: resolvedCwd,
3562
- stdio: ["ignore", "pipe", "pipe"],
3563
- encoding: "utf8"
3564
- });
3565
- const toplevel = stdout.trim();
3566
- if (toplevel.length > 0) {
3567
- return { productDir: resolve2(toplevel) };
3568
- }
3569
- } catch {
3570
- }
3571
- return {
3572
- productDir: resolvedCwd,
3573
- warning: `warning: ${resolvedCwd} is not inside a git worktree \u2014 falling back to the current working directory. ${GIT_NOT_REPO_MARKER}.`
3574
- };
3575
- }
3576
-
3577
- // src/interfaces/cli/write-warning.ts
3578
- function writeWarning(warning) {
3579
- if (warning === void 0) {
3580
- return;
3581
- }
3582
- process.stderr.write(`${warning}
3583
- `);
3584
- }
3585
-
3586
3652
  // src/interfaces/cli/config.ts
3587
- function buildDefaultDeps() {
3653
+ var CONFIG_CLI = {
3654
+ commandName: "config",
3655
+ commands: {
3656
+ defaults: "defaults",
3657
+ show: "show",
3658
+ validate: "validate"
3659
+ },
3660
+ flags: {
3661
+ json: "--json"
3662
+ }
3663
+ };
3664
+ function buildDefaultDeps(invocation) {
3588
3665
  return {
3589
3666
  resolveConfig,
3590
3667
  readProductConfigFile,
3591
3668
  resolveConfigFromReadResult,
3592
- resolveProductDir: () => {
3593
- const resolved = resolveProductDir();
3594
- writeWarning(resolved.warning);
3595
- return resolved.productDir;
3596
- },
3669
+ resolveProductDir: () => invocation.resolveProductContext().productDir,
3597
3670
  descriptors: productionRegistry
3598
3671
  };
3599
3672
  }
3600
- async function emit(result) {
3673
+ async function emit(result, io) {
3601
3674
  if (result.stdout.length > 0) {
3602
- process.stdout.write(result.stdout);
3675
+ io.writeStdout(result.stdout);
3603
3676
  }
3604
3677
  if (result.stderr.length > 0) {
3605
- process.stderr.write(result.stderr);
3678
+ io.writeStderr(result.stderr);
3606
3679
  }
3607
- return process.exit(result.exitCode);
3680
+ return io.exit(result.exitCode);
3608
3681
  }
3609
- function registerConfigCommands(configCmd) {
3610
- configCmd.command("show").description(
3611
- `Print the resolved configuration as ${DEFAULT_CONFIG_FILE_FORMAT.toUpperCase()} (or ${CONFIG_FILE_FORMAT.JSON.toUpperCase()} with --json)`
3612
- ).option("--json", `Output as ${CONFIG_FILE_FORMAT.JSON.toUpperCase()}`).action(async (options) => {
3613
- await emit(await showCommand(options, buildDefaultDeps()));
3682
+ function registerConfigCommands(configCmd, invocation) {
3683
+ configCmd.command(CONFIG_CLI.commands.show).description(
3684
+ `Print the resolved configuration as ${DEFAULT_CONFIG_FILE_FORMAT.toUpperCase()} (or ${CONFIG_FILE_FORMAT.JSON.toUpperCase()} with ${CONFIG_CLI.flags.json})`
3685
+ ).option(CONFIG_CLI.flags.json, `Output as ${CONFIG_FILE_FORMAT.JSON.toUpperCase()}`).action(async (options) => {
3686
+ await emit(await showCommand(options, buildDefaultDeps(invocation)), invocation.io);
3614
3687
  });
3615
- configCmd.command("validate").description(`Verify that ${DEFAULT_CONFIG_FILENAME} passes every registered descriptor's validator`).action(async (options) => {
3616
- await emit(await validateCommand(options, buildDefaultDeps()));
3688
+ configCmd.command(CONFIG_CLI.commands.validate).description(`Verify that ${DEFAULT_CONFIG_FILENAME} passes every registered descriptor's validator`).action(async (options) => {
3689
+ await emit(await validateCommand(options, buildDefaultDeps(invocation)), invocation.io);
3617
3690
  });
3618
- configCmd.command("defaults").description(`Print each registered descriptor's defaults; ignores ${DEFAULT_CONFIG_FILENAME}`).option("--json", `Output as ${CONFIG_FILE_FORMAT.JSON.toUpperCase()}`).action(async (options) => {
3619
- await emit(await defaultsCommand(options, buildDefaultDeps()));
3691
+ configCmd.command(CONFIG_CLI.commands.defaults).description(`Print each registered descriptor's defaults; ignores ${DEFAULT_CONFIG_FILENAME}`).option(CONFIG_CLI.flags.json, `Output as ${CONFIG_FILE_FORMAT.JSON.toUpperCase()}`).action(async (options) => {
3692
+ await emit(await defaultsCommand(options, buildDefaultDeps(invocation)), invocation.io);
3620
3693
  });
3621
3694
  }
3622
3695
  var configDomain = {
3623
3696
  name: "config",
3624
3697
  description: "Inspect and validate the resolved spx configuration",
3625
- register: (program2) => {
3626
- const configCmd = program2.command("config").description("Inspect and validate the resolved spx configuration");
3627
- registerConfigCommands(configCmd);
3698
+ register: (program, invocation) => {
3699
+ const configCmd = program.command(CONFIG_CLI.commandName).description("Inspect and validate the resolved spx configuration");
3700
+ registerConfigCommands(configCmd, invocation);
3628
3701
  }
3629
3702
  };
3630
3703
 
@@ -3918,8 +3991,48 @@ async function diagnoseCommand(options) {
3918
3991
  }
3919
3992
 
3920
3993
  // src/commands/diagnose/probes.ts
3994
+ import { dirname as dirname3 } from "path";
3921
3995
  import { execa as execa3 } from "execa";
3922
3996
 
3997
+ // src/domains/diagnose/checks/session-store.ts
3998
+ var SESSION_STORE_VERDICT = {
3999
+ CONSISTENT: "consistent",
4000
+ ORPHANED_CLAIMS: "orphaned-claims",
4001
+ UNKNOWN: "unknown"
4002
+ };
4003
+ function doingSessionBackedByClaim(session, claimedSessionIds2) {
4004
+ if (claimedSessionIds2.has(normalizeAgentSessionToken(session.id))) return true;
4005
+ return session.agent_session_id !== void 0 && claimedSessionIds2.has(normalizeAgentSessionToken(session.agent_session_id));
4006
+ }
4007
+ var REMEDIATION = {
4008
+ [SESSION_STORE_VERDICT.CONSISTENT]: "Session store is consistent; no action needed.",
4009
+ [SESSION_STORE_VERDICT.ORPHANED_CLAIMS]: "Release or reclaim doing sessions whose backing worktree reads free or is absent (spx session release).",
4010
+ [SESSION_STORE_VERDICT.UNKNOWN]: "Re-run diagnose; if it persists, inspect spx session list and spx worktree status."
4011
+ };
4012
+ function record(verdict, bucket, reading) {
4013
+ return {
4014
+ name: CHECK_NAME.SESSION_STORE,
4015
+ verdict,
4016
+ bucket,
4017
+ readings: {
4018
+ orphaned: String(reading.orphanedClaims)
4019
+ },
4020
+ remediation: REMEDIATION[verdict]
4021
+ };
4022
+ }
4023
+ function classifySessionStore(reading) {
4024
+ if (reading.errored) {
4025
+ return record(SESSION_STORE_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading);
4026
+ }
4027
+ if (reading.orphanedClaims > 0) {
4028
+ return record(SESSION_STORE_VERDICT.ORPHANED_CLAIMS, VERDICT_BUCKET.DEGRADED, reading);
4029
+ }
4030
+ return record(SESSION_STORE_VERDICT.CONSISTENT, VERDICT_BUCKET.HEALTHY, reading);
4031
+ }
4032
+ function sessionStoreRunner(probe) {
4033
+ return async () => classifySessionStore(await probe.probe());
4034
+ }
4035
+
3923
4036
  // src/domains/hooks/session-start.ts
3924
4037
  var HOOK_SESSION_START_PAYLOAD = {
3925
4038
  CWD: "cwd",
@@ -4117,10 +4230,7 @@ function formatOccupancyError(code, detail) {
4117
4230
  return `${code}${ERROR_DETAIL_SEPARATOR2}${detail}`;
4118
4231
  }
4119
4232
  function toErrorMessage2(error) {
4120
- if (error instanceof Error) return error.message;
4121
- if (typeof error === "string") return error;
4122
- if (typeof error === "number" || typeof error === "bigint" || typeof error === "boolean") return error.toString();
4123
- return JSON.stringify(error) ?? "unknown error";
4233
+ return toMessage(error);
4124
4234
  }
4125
4235
 
4126
4236
  // src/domains/worktree/worktree-name.ts
@@ -4273,13 +4383,12 @@ var defaultWorktreePoolProbe = {
4273
4383
  running: 0,
4274
4384
  free: 0
4275
4385
  };
4276
- const root = await detectGitCommonDirProductRoot();
4277
4386
  const facts = await gatherGitFacts();
4278
- if (!root.isGitRepo || !facts?.worktreeListRead) return errored;
4387
+ if (!facts?.worktreeListRead) return errored;
4279
4388
  const bareRepository = facts.commonDirIsBare;
4280
4389
  const paths = facts.worktreeRoots;
4281
4390
  const linkedWorktrees = !bareRepository && paths.length > 1;
4282
- const worktreesDir = worktreesScopeDir(root.productDir);
4391
+ const worktreesDir = worktreesScopeDir(dirname3(facts.commonDir));
4283
4392
  let running = 0;
4284
4393
  let free = 0;
4285
4394
  for (const path7 of paths) {
@@ -4316,11 +4425,9 @@ var defaultSessionEnvironmentProbe = {
4316
4425
  }
4317
4426
  };
4318
4427
  async function claimedSessionIds() {
4319
- const root = await detectGitCommonDirProductRoot();
4320
- if (!root.isGitRepo) return null;
4321
- const worktreesDir = worktreesScopeDir(root.productDir);
4322
4428
  const facts = await gatherGitFacts();
4323
4429
  if (!facts?.worktreeListRead) return null;
4430
+ const worktreesDir = worktreesScopeDir(dirname3(facts.commonDir));
4324
4431
  const sessionIds = /* @__PURE__ */ new Set();
4325
4432
  for (const path7 of facts.worktreeRoots) {
4326
4433
  const claim = await readClaim(worktreesDir, worktreeClaimName(path7), { fs: defaultOccupancyFileSystem });
@@ -4347,8 +4454,7 @@ var defaultSessionStoreProbe = {
4347
4454
  }
4348
4455
  let orphanedClaims = 0;
4349
4456
  for (const session of doing) {
4350
- const sessionId = session.agent_session_id;
4351
- if (sessionId !== void 0 && !claimed.has(sessionId)) orphanedClaims += 1;
4457
+ if (!doingSessionBackedByClaim(session, claimed)) orphanedClaims += 1;
4352
4458
  }
4353
4459
  return { errored: false, orphanedClaims };
4354
4460
  }
@@ -4473,14 +4579,14 @@ var MARKETPLACE_INSTALL_VERDICT = {
4473
4579
  NOT_APPLICABLE: "not-applicable",
4474
4580
  UNKNOWN: "unknown"
4475
4581
  };
4476
- var REMEDIATION = {
4582
+ var REMEDIATION2 = {
4477
4583
  [MARKETPLACE_INSTALL_VERDICT.INSTALLED]: "Marketplace and expected plugins are installed and enabled; no action needed.",
4478
4584
  [MARKETPLACE_INSTALL_VERDICT.DRIFTED]: "Install or enable the expected plugins on the drifted surface.",
4479
4585
  [MARKETPLACE_INSTALL_VERDICT.UNREGISTERED]: "Register the methodology marketplace on the present plugin surface.",
4480
4586
  [MARKETPLACE_INSTALL_VERDICT.NOT_APPLICABLE]: "No plugin CLI surface present; no action needed.",
4481
4587
  [MARKETPLACE_INSTALL_VERDICT.UNKNOWN]: "Re-run diagnose; if it persists, inspect the claude/codex plugin CLI output."
4482
4588
  };
4483
- function record(verdict, bucket, reading) {
4589
+ function record2(verdict, bucket, reading) {
4484
4590
  return {
4485
4591
  name: CHECK_NAME.MARKETPLACE_INSTALL,
4486
4592
  verdict,
@@ -4490,23 +4596,23 @@ function record(verdict, bucket, reading) {
4490
4596
  unregistered: String(reading.unregistered),
4491
4597
  drifted: String(reading.drifted)
4492
4598
  },
4493
- remediation: REMEDIATION[verdict]
4599
+ remediation: REMEDIATION2[verdict]
4494
4600
  };
4495
4601
  }
4496
4602
  function classifyMarketplaceInstall(reading) {
4497
4603
  if (reading.errored) {
4498
- return record(MARKETPLACE_INSTALL_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading);
4604
+ return record2(MARKETPLACE_INSTALL_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading);
4499
4605
  }
4500
4606
  if (!reading.surfacePresent) {
4501
- return record(MARKETPLACE_INSTALL_VERDICT.NOT_APPLICABLE, VERDICT_BUCKET.NOT_APPLICABLE, reading);
4607
+ return record2(MARKETPLACE_INSTALL_VERDICT.NOT_APPLICABLE, VERDICT_BUCKET.NOT_APPLICABLE, reading);
4502
4608
  }
4503
4609
  if (reading.unregistered) {
4504
- return record(MARKETPLACE_INSTALL_VERDICT.UNREGISTERED, VERDICT_BUCKET.BROKEN, reading);
4610
+ return record2(MARKETPLACE_INSTALL_VERDICT.UNREGISTERED, VERDICT_BUCKET.BROKEN, reading);
4505
4611
  }
4506
4612
  if (reading.drifted) {
4507
- return record(MARKETPLACE_INSTALL_VERDICT.DRIFTED, VERDICT_BUCKET.DEGRADED, reading);
4613
+ return record2(MARKETPLACE_INSTALL_VERDICT.DRIFTED, VERDICT_BUCKET.DEGRADED, reading);
4508
4614
  }
4509
- return record(MARKETPLACE_INSTALL_VERDICT.INSTALLED, VERDICT_BUCKET.HEALTHY, reading);
4615
+ return record2(MARKETPLACE_INSTALL_VERDICT.INSTALLED, VERDICT_BUCKET.HEALTHY, reading);
4510
4616
  }
4511
4617
  function marketplaceInstallRunner(probe) {
4512
4618
  return async (manifest) => {
@@ -4525,14 +4631,14 @@ var SESSION_ENVIRONMENT_VERDICT = {
4525
4631
  NOT_APPLICABLE: "not-applicable",
4526
4632
  UNKNOWN: "unknown"
4527
4633
  };
4528
- var REMEDIATION2 = {
4634
+ var REMEDIATION3 = {
4529
4635
  [SESSION_ENVIRONMENT_VERDICT.WORKING]: "Session environment is established; no action needed.",
4530
4636
  [SESSION_ENVIRONMENT_VERDICT.IDENTITY_ONLY]: "The SessionStart hook set the session identity but did not claim the worktree; check the worktree-claim step.",
4531
4637
  [SESSION_ENVIRONMENT_VERDICT.SILENT_NO_OP]: "The SessionStart hook ran without effect; verify the hook resolves spx and the agent session id.",
4532
- [SESSION_ENVIRONMENT_VERDICT.NOT_APPLICABLE]: "No spec-tree SessionStart hook on this runtime; no action needed.",
4638
+ [SESSION_ENVIRONMENT_VERDICT.NOT_APPLICABLE]: "No SessionStart hook signal or agent session identity was observed; confirm a spec-tree SessionStart hook is configured and enabled.",
4533
4639
  [SESSION_ENVIRONMENT_VERDICT.UNKNOWN]: "Re-run diagnose; if it persists, inspect the agent session id and spx worktree status."
4534
4640
  };
4535
- function record2(verdict, bucket, reading) {
4641
+ function record3(verdict, bucket, reading) {
4536
4642
  return {
4537
4643
  name: CHECK_NAME.SESSION_ENVIRONMENT,
4538
4644
  verdict,
@@ -4542,66 +4648,31 @@ function record2(verdict, bucket, reading) {
4542
4648
  identity: String(reading.sessionIdentity),
4543
4649
  claimed: String(reading.worktreeClaimed)
4544
4650
  },
4545
- remediation: REMEDIATION2[verdict]
4651
+ remediation: REMEDIATION3[verdict]
4546
4652
  };
4547
4653
  }
4548
4654
  function classifySessionEnvironment(reading) {
4549
- if (!reading.hookPresent) {
4550
- return record2(SESSION_ENVIRONMENT_VERDICT.NOT_APPLICABLE, VERDICT_BUCKET.NOT_APPLICABLE, reading);
4551
- }
4552
4655
  if (reading.errored) {
4553
- return record2(SESSION_ENVIRONMENT_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading);
4656
+ return record3(SESSION_ENVIRONMENT_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading);
4554
4657
  }
4555
4658
  if (reading.sessionIdentity && reading.worktreeClaimed) {
4556
- return record2(SESSION_ENVIRONMENT_VERDICT.WORKING, VERDICT_BUCKET.HEALTHY, reading);
4659
+ return record3(SESSION_ENVIRONMENT_VERDICT.WORKING, VERDICT_BUCKET.HEALTHY, reading);
4557
4660
  }
4558
4661
  if (reading.sessionIdentity) {
4559
- return record2(SESSION_ENVIRONMENT_VERDICT.IDENTITY_ONLY, VERDICT_BUCKET.DEGRADED, reading);
4662
+ return record3(SESSION_ENVIRONMENT_VERDICT.IDENTITY_ONLY, VERDICT_BUCKET.DEGRADED, reading);
4560
4663
  }
4561
- if (!reading.worktreeClaimed) {
4562
- return record2(SESSION_ENVIRONMENT_VERDICT.SILENT_NO_OP, VERDICT_BUCKET.BROKEN, reading);
4664
+ if (reading.hookPresent && !reading.worktreeClaimed) {
4665
+ return record3(SESSION_ENVIRONMENT_VERDICT.SILENT_NO_OP, VERDICT_BUCKET.BROKEN, reading);
4563
4666
  }
4564
- return record2(SESSION_ENVIRONMENT_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading);
4667
+ if (!reading.hookPresent && !reading.worktreeClaimed) {
4668
+ return record3(SESSION_ENVIRONMENT_VERDICT.NOT_APPLICABLE, VERDICT_BUCKET.NOT_APPLICABLE, reading);
4669
+ }
4670
+ return record3(SESSION_ENVIRONMENT_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading);
4565
4671
  }
4566
4672
  function sessionEnvironmentRunner(probe) {
4567
4673
  return async () => classifySessionEnvironment(await probe.probe());
4568
4674
  }
4569
4675
 
4570
- // src/domains/diagnose/checks/session-store.ts
4571
- var SESSION_STORE_VERDICT = {
4572
- CONSISTENT: "consistent",
4573
- ORPHANED_CLAIMS: "orphaned-claims",
4574
- UNKNOWN: "unknown"
4575
- };
4576
- var REMEDIATION3 = {
4577
- [SESSION_STORE_VERDICT.CONSISTENT]: "Session store is consistent; no action needed.",
4578
- [SESSION_STORE_VERDICT.ORPHANED_CLAIMS]: "Release or reclaim doing sessions whose backing worktree reads free or is absent (spx session release).",
4579
- [SESSION_STORE_VERDICT.UNKNOWN]: "Re-run diagnose; if it persists, inspect spx session list and spx worktree status."
4580
- };
4581
- function record3(verdict, bucket, reading) {
4582
- return {
4583
- name: CHECK_NAME.SESSION_STORE,
4584
- verdict,
4585
- bucket,
4586
- readings: {
4587
- orphaned: String(reading.orphanedClaims)
4588
- },
4589
- remediation: REMEDIATION3[verdict]
4590
- };
4591
- }
4592
- function classifySessionStore(reading) {
4593
- if (reading.errored) {
4594
- return record3(SESSION_STORE_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading);
4595
- }
4596
- if (reading.orphanedClaims > 0) {
4597
- return record3(SESSION_STORE_VERDICT.ORPHANED_CLAIMS, VERDICT_BUCKET.DEGRADED, reading);
4598
- }
4599
- return record3(SESSION_STORE_VERDICT.CONSISTENT, VERDICT_BUCKET.HEALTHY, reading);
4600
- }
4601
- function sessionStoreRunner(probe) {
4602
- return async () => classifySessionStore(await probe.probe());
4603
- }
4604
-
4605
4676
  // src/domains/diagnose/checks/spx-reachability.ts
4606
4677
  var SPX_REACHABILITY_VERDICT = {
4607
4678
  REACHABLE: "reachable",
@@ -4617,7 +4688,7 @@ function parseSemver(value) {
4617
4688
  major: Number(match[1]),
4618
4689
  minor: Number(match[2]),
4619
4690
  patch: Number(match[3]),
4620
- prerelease: match[4] === void 0 ? null : match[4].slice(1)
4691
+ prerelease: match.at(4) === void 0 ? null : match.at(4)?.slice(1) ?? null
4621
4692
  };
4622
4693
  }
4623
4694
  function isNumericIdentifier(identifier) {
@@ -4795,15 +4866,16 @@ var DEFAULT_REGISTRY = {
4795
4866
  [CHECK_NAME.SESSION_STORE]: sessionStoreRunner(defaultSessionStoreProbe),
4796
4867
  [CHECK_NAME.MARKETPLACE_INSTALL]: marketplaceInstallRunner(defaultMarketplaceInstallProbe)
4797
4868
  };
4798
- function handleError(error) {
4799
- console.error("Error:", sanitizeCliArgument(error));
4800
- process.exit(1);
4869
+ function handleError(error, io) {
4870
+ io.writeStderr(`Error: ${sanitizeCliArgument(error)}
4871
+ `);
4872
+ return io.exit(1);
4801
4873
  }
4802
4874
  var diagnoseDomain = {
4803
4875
  name: DIAGNOSE_CLI.COMMAND,
4804
4876
  description: DIAGNOSE_DOMAIN_DESCRIPTION,
4805
- register: (program2) => {
4806
- program2.command(DIAGNOSE_CLI.COMMAND).description(DIAGNOSE_DOMAIN_DESCRIPTION).option(
4877
+ register: (program, invocation) => {
4878
+ program.command(DIAGNOSE_CLI.COMMAND).description(DIAGNOSE_DOMAIN_DESCRIPTION).option(
4807
4879
  `${DIAGNOSE_CLI.MANIFEST_FLAG} <path>`,
4808
4880
  "Path to a declarative diagnose manifest that fully instruments the diagnosis"
4809
4881
  ).addOption(
@@ -4811,7 +4883,7 @@ var diagnoseDomain = {
4811
4883
  ).addOption(new Option(`${DIAGNOSE_CLI.COLOR_FLAG}`, "Force colored output")).addOption(new Option(`${DIAGNOSE_CLI.NO_COLOR_FLAG}`, "Disable colored output")).action(async (options) => {
4812
4884
  const result = await diagnoseCommand({
4813
4885
  manifestPath: options.manifest,
4814
- productDir: process.cwd(),
4886
+ productDir: invocation.resolveProductContext().productDir,
4815
4887
  format: options.format,
4816
4888
  color: resolveColorChoice({
4817
4889
  flag: options.color,
@@ -4821,9 +4893,12 @@ var diagnoseDomain = {
4821
4893
  registry: DEFAULT_REGISTRY,
4822
4894
  fs: { readFile: (path7) => readFile3(path7, "utf8") }
4823
4895
  });
4824
- if (!result.ok) handleError(result.error);
4825
- console.log(result.value.output);
4826
- process.exit(result.value.exitCode);
4896
+ if (!result.ok) {
4897
+ handleError(result.error, invocation.io);
4898
+ }
4899
+ invocation.io.writeStdout(`${result.value.output}
4900
+ `);
4901
+ invocation.io.setExitCode(result.value.exitCode);
4827
4902
  });
4828
4903
  }
4829
4904
  };
@@ -4877,7 +4952,7 @@ function isValidPid(pid) {
4877
4952
  }
4878
4953
 
4879
4954
  // src/domains/worktree/resolve.ts
4880
- import { dirname as dirname3, resolve as resolve3 } from "path";
4955
+ import { dirname as dirname4, resolve as resolve4 } from "path";
4881
4956
  var WORKTREE_RESOLVE_ERROR = {
4882
4957
  NOT_A_WORKTREE: "path resolves to no worktree"
4883
4958
  };
@@ -4893,8 +4968,8 @@ async function resolveCurrentWorktreeName(options) {
4893
4968
  }
4894
4969
  async function resolveTargetWorktree(options) {
4895
4970
  const base = options.cwd;
4896
- const targetPath = options.worktree === void 0 ? base : resolve3(base, options.worktree);
4897
- const targetGitPath = await options.pathInfo.isExistingNonDirectory(targetPath) ? dirname3(targetPath) : targetPath;
4971
+ const targetPath = options.worktree === void 0 ? base : resolve4(base, options.worktree);
4972
+ const targetGitPath = await options.pathInfo.isExistingNonDirectory(targetPath) ? dirname4(targetPath) : targetPath;
4898
4973
  const worktree = await detectWorktreeProductRoot(targetGitPath, options.gitDeps);
4899
4974
  if (!worktree.isGitRepo) {
4900
4975
  return { ok: false, error: `${WORKTREE_RESOLVE_ERROR.NOT_A_WORKTREE}: ${options.worktree ?? base}` };
@@ -5060,17 +5135,17 @@ function createProcessHookIo(streams) {
5060
5135
  return {
5061
5136
  readStdin: async () => {
5062
5137
  if (streams.stdin.isTTY) return { ok: true, value: void 0 };
5063
- return new Promise((resolve8) => {
5138
+ return new Promise((resolve10) => {
5064
5139
  let data = "";
5065
5140
  streams.stdin.setEncoding("utf-8");
5066
5141
  streams.stdin.on(HOOK_PROCESS_IO_EVENT.DATA, (chunk) => {
5067
5142
  data += chunk;
5068
5143
  });
5069
5144
  streams.stdin.on(HOOK_PROCESS_IO_EVENT.END, () => {
5070
- resolve8({ ok: true, value: data.length === 0 ? void 0 : data });
5145
+ resolve10({ ok: true, value: data.length === 0 ? void 0 : data });
5071
5146
  });
5072
5147
  streams.stdin.on(HOOK_PROCESS_IO_EVENT.ERROR, (error) => {
5073
- resolve8({ ok: false, error: formatStdinReadError(error) });
5148
+ resolve10({ ok: false, error: formatStdinReadError(error) });
5074
5149
  });
5075
5150
  });
5076
5151
  },
@@ -5089,7 +5164,8 @@ function describeStdinReadError(error) {
5089
5164
  if (error instanceof Error) return error.message;
5090
5165
  if (typeof error === "string") return error;
5091
5166
  try {
5092
- return JSON.stringify(error) ?? `${typeof error}`;
5167
+ const serialized = JSON.stringify(error);
5168
+ return typeof serialized === "string" ? serialized : `${typeof error}`;
5093
5169
  } catch {
5094
5170
  return `${typeof error}`;
5095
5171
  }
@@ -5116,31 +5192,41 @@ var HOOK_CLI = {
5116
5192
  WORKTREES_DIR_FLAG: "--worktrees-dir"
5117
5193
  };
5118
5194
  var HOOK_DOMAIN_DESCRIPTION = "Run host lifecycle hook events";
5119
- function registerHookCommands(hookCmd) {
5195
+ function writeError2(invocation, output) {
5196
+ invocation.io.writeStderr(`${output}
5197
+ `);
5198
+ }
5199
+ function writeInvocationWarning(invocation, warning) {
5200
+ if (warning !== void 0) {
5201
+ writeError2(invocation, warning);
5202
+ }
5203
+ }
5204
+ function registerHookCommands(hookCmd, invocation) {
5205
+ const effectiveInvocationDir = () => invocation.resolveEffectiveInvocationDir();
5120
5206
  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) => {
5121
5207
  const result = await runHookCli({
5122
5208
  claimWriteToken: createClaimWriteToken(),
5123
- cwd: process.cwd(),
5209
+ cwd: effectiveInvocationDir(),
5124
5210
  env: process.env,
5125
5211
  envFile: options.hookEnvFile,
5126
5212
  event,
5127
5213
  fs: defaultOccupancyFileSystem,
5128
5214
  gitDeps: defaultGitDependencies,
5129
5215
  io: processHookIo,
5130
- onWarning: writeWarning,
5216
+ onWarning: (warning) => writeInvocationWarning(invocation, warning),
5131
5217
  processTable: defaultProcessTable,
5132
5218
  selfPid: process.pid,
5133
5219
  worktreesDir: options.worktreesDir
5134
5220
  });
5135
- if (!result.ok) process.exit(1);
5221
+ if (!result.ok) invocation.io.exit(1);
5136
5222
  });
5137
5223
  }
5138
5224
  var hookDomain = {
5139
5225
  name: HOOK_CLI.COMMAND,
5140
5226
  description: HOOK_DOMAIN_DESCRIPTION,
5141
- register: (program2) => {
5142
- const hookCmd = program2.command(HOOK_CLI.COMMAND).description(HOOK_DOMAIN_DESCRIPTION);
5143
- registerHookCommands(hookCmd);
5227
+ register: (program, invocation) => {
5228
+ const hookCmd = program.command(HOOK_CLI.COMMAND).description(HOOK_DOMAIN_DESCRIPTION);
5229
+ registerHookCommands(hookCmd, invocation);
5144
5230
  }
5145
5231
  };
5146
5232
 
@@ -5175,6 +5261,14 @@ function resolveJournalBackend(env) {
5175
5261
  return { ok: true, value: JOURNAL_BACKEND.LOCAL };
5176
5262
  }
5177
5263
 
5264
+ // src/lib/verification-env.ts
5265
+ var SPX_VERIFY_ENV = {
5266
+ BRANCH: "SPX_VERIFY_BRANCH"
5267
+ };
5268
+ var SPX_VERIFY_HEAD_SHA = {
5269
+ MISSING: "unknown"
5270
+ };
5271
+
5178
5272
  // src/lib/agent-run-journal/index.ts
5179
5273
  var CLOUDEVENTS_SPECVERSION = "1.0";
5180
5274
  var JOURNAL_SEQ_BASE = 1;
@@ -5462,7 +5556,7 @@ function journalRunFilePath(scope2) {
5462
5556
  }
5463
5557
 
5464
5558
  // src/lib/appendable-journal-store/index.ts
5465
- import { dirname as dirname4 } from "path";
5559
+ import { dirname as dirname5 } from "path";
5466
5560
  var SEAL_MARKER_SUFFIX = ".sealed";
5467
5561
  var LINE_SEPARATOR3 = "\n";
5468
5562
  function appendableJournalSealMarkerPath(runFilePath) {
@@ -5502,7 +5596,7 @@ function createAppendableJournalStore(options) {
5502
5596
  },
5503
5597
  readAll,
5504
5598
  async seal() {
5505
- await fs8.mkdir(dirname4(sealMarkerPath), { recursive: true });
5599
+ await fs8.mkdir(dirname5(sealMarkerPath), { recursive: true });
5506
5600
  await fs8.writeFile(sealMarkerPath, "");
5507
5601
  },
5508
5602
  async isSealed() {
@@ -5643,7 +5737,7 @@ var JOURNAL_CLI_EXIT_CODE = {
5643
5737
  };
5644
5738
  var JOURNAL_CLI_ENV = {
5645
5739
  BACKEND: "SPX_VERIFY_BACKEND",
5646
- BRANCH: "SPX_VERIFY_BRANCH",
5740
+ BRANCH: SPX_VERIFY_ENV.BRANCH,
5647
5741
  CONTINUOUS_INTEGRATION: "CI",
5648
5742
  GITHUB_EVENT_NAME: "GITHUB_EVENT_NAME",
5649
5743
  GITHUB_REF: "GITHUB_REF",
@@ -5656,7 +5750,6 @@ var JOURNAL_CLI_ERROR = {
5656
5750
  INVALID_EVENT_INPUT: "journal append event input is missing a required CloudEvents field",
5657
5751
  INVALID_CURSOR: "journal read cursor must be a whole non-negative integer"
5658
5752
  };
5659
- var MISSING_HEAD_SHA_FALLBACK = "unknown";
5660
5753
  var CURSOR_PATTERN = /^\d+$/;
5661
5754
  var JOURNAL_EVENT_INPUT_STRING_FIELDS = ["id", "source", "type", "time"];
5662
5755
  var JOURNAL_COMMENT_MARKER_PREFIX = "spx-journal-run:";
@@ -5684,7 +5777,7 @@ function readJournalCliEnvironment(processEnv) {
5684
5777
  };
5685
5778
  }
5686
5779
  async function resolveJournalRunContext(scope2, deps) {
5687
- const cwd = deps.cwd ?? process.cwd();
5780
+ const cwd = deps.cwd ?? CONFIG_PROCESS_CWD.read();
5688
5781
  const git = deps.git ?? defaultGitDependencies;
5689
5782
  const cliEnvironment = readJournalCliEnvironment(deps.processEnv ?? process.env);
5690
5783
  const environment = deps.env ?? cliEnvironment.backend;
@@ -5693,7 +5786,7 @@ async function resolveJournalRunContext(scope2, deps) {
5693
5786
  const product = await detectGitCommonDirProductRoot(cwd, git);
5694
5787
  const probedBranch = product.isGitRepo ? await getCurrentBranch(cwd, git) ?? void 0 : void 0;
5695
5788
  const branchName = deps.branch ?? cliEnvironment.branch ?? probedBranch;
5696
- const headSha = (product.isGitRepo ? await getHeadSha(cwd, git) : null) ?? MISSING_HEAD_SHA_FALLBACK;
5789
+ const headSha = (product.isGitRepo ? await getHeadSha(cwd, git) : null) ?? SPX_VERIFY_HEAD_SHA.MISSING;
5697
5790
  const branchIdentity = resolveBranchIdentity({ ...branchName === void 0 ? {} : { branchName }, headSha });
5698
5791
  return {
5699
5792
  ok: true,
@@ -5827,284 +5920,269 @@ async function journalRenderCommand(scope2, deps = {}) {
5827
5920
  return okResult(JSON.stringify(rendered.value));
5828
5921
  }
5829
5922
 
5830
- // src/lib/process-lifecycle/exit-codes.ts
5831
- var SIGINT_EXIT_CODE = 130;
5832
- var SIGTERM_EXIT_CODE = 143;
5833
- var EPIPE_EXIT_CODE = 0;
5834
- var UNCAUGHT_EXIT_CODE = 1;
5835
-
5836
- // src/lib/process-lifecycle/foreground-handoff.ts
5837
- var FOREGROUND_SIGNALS = ["SIGINT", "SIGTERM"];
5838
- function createSignalSuspender(target) {
5923
+ // src/interfaces/cli/journal.ts
5924
+ var JOURNAL_CLI = {
5925
+ commandName: "journal",
5926
+ description: "Record and stream an agentic verification run journal",
5927
+ openCommandName: "open",
5928
+ appendCommandName: "append",
5929
+ readCommandName: "read",
5930
+ sealCommandName: "seal",
5931
+ renderCommandName: "render",
5932
+ typeOption: "--type <type>",
5933
+ runOption: "--run <token>",
5934
+ fromOption: "--from <cursor>"
5935
+ };
5936
+ var STREAM_LINE_SEPARATOR = "\n";
5937
+ var MALFORMED_EVENT_INPUT_ERROR = "journal append event input is not valid JSON";
5938
+ function scope(options) {
5939
+ return { type: options.type };
5940
+ }
5941
+ function runScope(options) {
5942
+ return { ...scope(options), runToken: options.run };
5943
+ }
5944
+ var journalDomain = {
5945
+ name: JOURNAL_CLI.commandName,
5946
+ description: JOURNAL_CLI.description,
5947
+ register: (program, invocation) => {
5948
+ const journalDeps = () => ({ cwd: invocation.resolveEffectiveInvocationDir() });
5949
+ const journalCmd = program.command(JOURNAL_CLI.commandName).description(JOURNAL_CLI.description);
5950
+ journalCmd.command(JOURNAL_CLI.openCommandName).description("Open a new run journal and report its run token").requiredOption(JOURNAL_CLI.typeOption, "Opaque verification-type scope segment").action(async (options) => {
5951
+ report(await journalOpenCommand(scope(options), journalDeps()), invocation.io);
5952
+ });
5953
+ journalCmd.command(JOURNAL_CLI.appendCommandName).description("Append a JSON event read from standard input and stream it").requiredOption(JOURNAL_CLI.typeOption, "Opaque verification-type scope segment").requiredOption(JOURNAL_CLI.runOption, "Run token reported by open").action(async (options) => {
5954
+ const input = await readStdinEventInput();
5955
+ if (!input.ok) {
5956
+ report({ exitCode: JOURNAL_CLI_EXIT_CODE.ERROR, output: input.error }, invocation.io);
5957
+ return;
5958
+ }
5959
+ const result = await journalAppendCommand(
5960
+ runScope(options),
5961
+ input.value,
5962
+ streamBinding(invocation.io),
5963
+ journalDeps()
5964
+ );
5965
+ if (result.exitCode === JOURNAL_CLI_EXIT_CODE.OK) invocation.io.setExitCode(JOURNAL_CLI_EXIT_CODE.OK);
5966
+ else report(result, invocation.io);
5967
+ });
5968
+ journalCmd.command(JOURNAL_CLI.readCommandName).description("Read the run's events at or after a cursor").requiredOption(JOURNAL_CLI.typeOption, "Opaque verification-type scope segment").requiredOption(JOURNAL_CLI.runOption, "Run token reported by open").requiredOption(JOURNAL_CLI.fromOption, "Sequence cursor; events at or after it are returned").action(async (options) => {
5969
+ report(await journalReadCommand(runScope(options), options.from, journalDeps()), invocation.io);
5970
+ });
5971
+ journalCmd.command(JOURNAL_CLI.sealCommandName).description("Seal the run journal so further appends are rejected").requiredOption(JOURNAL_CLI.typeOption, "Opaque verification-type scope segment").requiredOption(JOURNAL_CLI.runOption, "Run token reported by open").action(async (options) => {
5972
+ report(await journalSealCommand(runScope(options), journalDeps()), invocation.io);
5973
+ });
5974
+ journalCmd.command(JOURNAL_CLI.renderCommandName).description("Render the run's event-prefix projection").requiredOption(JOURNAL_CLI.typeOption, "Opaque verification-type scope segment").requiredOption(JOURNAL_CLI.runOption, "Run token reported by open").action(async (options) => {
5975
+ report(await journalRenderCommand(runScope(options), journalDeps()), invocation.io);
5976
+ });
5977
+ }
5978
+ };
5979
+ function stdoutStreamSink(io) {
5839
5980
  return {
5840
- suspend() {
5841
- const ignore = () => {
5842
- };
5843
- const restorers = FOREGROUND_SIGNALS.map((signal) => {
5844
- const originals = target.listeners(signal);
5845
- for (const listener of originals) target.removeListener(signal, listener);
5846
- target.on(signal, ignore);
5847
- return () => {
5848
- target.removeListener(signal, ignore);
5849
- for (const listener of originals) target.on(signal, listener);
5850
- };
5851
- });
5852
- return () => {
5853
- for (const restore of restorers) restore();
5854
- };
5981
+ async emit(event) {
5982
+ io.writeStdout(`${JSON.stringify(event)}${STREAM_LINE_SEPARATOR}`);
5855
5983
  }
5856
5984
  };
5857
5985
  }
5986
+ function streamBinding(io) {
5987
+ const repository = process.env[JOURNAL_CLI_ENV.GITHUB_REPOSITORY] ?? "";
5988
+ return {
5989
+ localSink: stdoutStreamSink(io),
5990
+ githubClient: createGithubPullRequestCommentClient({ repository, run: runGhApi }),
5991
+ githubRepository: repository
5992
+ };
5993
+ }
5994
+ async function readStdinEventInput() {
5995
+ const chunks = [];
5996
+ for await (const chunk of process.stdin) {
5997
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
5998
+ }
5999
+ try {
6000
+ return { ok: true, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) };
6001
+ } catch {
6002
+ return { ok: false, error: MALFORMED_EVENT_INPUT_ERROR };
6003
+ }
6004
+ }
6005
+ function report(result, io) {
6006
+ const output = `${result.output}${STREAM_LINE_SEPARATOR}`;
6007
+ if (result.exitCode === JOURNAL_CLI_EXIT_CODE.OK) io.writeStdout(output);
6008
+ else io.writeStderr(output);
6009
+ io.setExitCode(result.exitCode);
6010
+ }
5858
6011
 
5859
- // src/lib/process-lifecycle/handlers.ts
5860
- var SIGINT_NAME = "SIGINT";
5861
- var SIGTERM_NAME = "SIGTERM";
5862
- function createHandlers(deps) {
5863
- let cleanupOnce = false;
5864
- function killEachChild(signal) {
5865
- deps.registry.forEach((child) => {
5866
- if (!child.killed) child.kill(signal);
5867
- });
5868
- }
5869
- function exitOnce(code, killSignal) {
5870
- if (cleanupOnce) {
5871
- deps.exitController.exit(code);
5872
- return;
5873
- }
5874
- cleanupOnce = true;
5875
- if (killSignal !== void 0) killEachChild(killSignal);
5876
- deps.exitController.exit(code);
5877
- }
5878
- return {
5879
- onSigint() {
5880
- exitOnce(SIGINT_EXIT_CODE, SIGINT_NAME);
6012
+ // src/interfaces/cli/session/definition.ts
6013
+ var sessionCliDefinition = {
6014
+ domain: { commandName: "session", description: "Manage session workflow" },
6015
+ subcommands: {
6016
+ list: {
6017
+ commandName: "list",
6018
+ description: "List active sessions (doing + todo by default)"
5881
6019
  },
5882
- onSigterm() {
5883
- exitOnce(SIGTERM_EXIT_CODE, SIGTERM_NAME);
6020
+ pick: {
6021
+ commandName: "pick",
6022
+ description: "Interactively pick a session and launch claude or codex to resume it"
5884
6023
  },
5885
- onEpipe() {
5886
- exitOnce(EPIPE_EXIT_CODE, SIGTERM_NAME);
6024
+ todo: {
6025
+ commandName: "todo",
6026
+ description: "List todo sessions"
5887
6027
  },
5888
- onUncaught(_error) {
5889
- exitOnce(UNCAUGHT_EXIT_CODE, SIGTERM_NAME);
5890
- }
5891
- };
5892
- }
5893
-
5894
- // src/lib/process-lifecycle/install.ts
5895
- import { spawn } from "child_process";
5896
-
5897
- // src/lib/process-lifecycle/registry.ts
5898
- function createRegistry() {
5899
- const tracked = /* @__PURE__ */ new Set();
5900
- return {
5901
- add(child) {
5902
- tracked.add(child);
6028
+ show: {
6029
+ commandName: "show",
6030
+ operand: "<id...>",
6031
+ description: "Show session content"
5903
6032
  },
5904
- remove(child) {
5905
- tracked.delete(child);
6033
+ pickup: {
6034
+ commandName: "pickup",
6035
+ operand: "[ids...]",
6036
+ description: "Claim one or more sessions (move from todo to doing)"
5906
6037
  },
5907
- forEach(fn) {
5908
- for (const child of tracked) fn(child);
6038
+ release: {
6039
+ commandName: "release",
6040
+ operand: "[ids...]",
6041
+ description: "Release one or more sessions (move from doing to todo)"
5909
6042
  },
5910
- get size() {
5911
- return tracked.size;
5912
- }
5913
- };
5914
- }
5915
-
5916
- // src/lib/process-lifecycle/runner.ts
5917
- function createLifecycleRunner(deps) {
5918
- return {
5919
- spawn(command, args, options) {
5920
- const child = deps.spawn(command, args, options);
5921
- deps.registry.add(child);
5922
- child.on("exit", () => deps.registry.remove(child));
5923
- return child;
6043
+ handoff: {
6044
+ commandName: "handoff",
6045
+ description: "Create a handoff session (reads JSON header + body from stdin)"
6046
+ },
6047
+ delete: {
6048
+ commandName: "delete",
6049
+ operand: "<id...>",
6050
+ description: "Delete one or more sessions"
6051
+ },
6052
+ prune: {
6053
+ commandName: "prune",
6054
+ description: "Remove old todo sessions, keeping the most recent N"
6055
+ },
6056
+ archive: {
6057
+ commandName: "archive",
6058
+ operand: "<id...>",
6059
+ description: "Move one or more sessions to the archive directory"
5924
6060
  }
5925
- };
5926
- }
5927
-
5928
- // src/lib/process-lifecycle/install.ts
5929
- var moduleRegistry = createRegistry();
5930
- var moduleExitController = {
5931
- exit(code) {
5932
- process.exit(code);
5933
- }
5934
- };
5935
- var installed = false;
5936
- var lifecycleProcessRunner = createLifecycleRunner({
5937
- registry: moduleRegistry,
5938
- spawn
5939
- });
5940
- var foregroundProcessRunner = { spawn };
5941
- var processSignalTarget = {
5942
- listeners: (signal) => process.listeners(signal),
5943
- on: (signal, listener) => {
5944
- process.on(signal, listener);
5945
6061
  },
5946
- removeListener: (signal, listener) => {
5947
- process.removeListener(signal, listener);
5948
- }
5949
- };
5950
- var lifecycleSignalSuspender = createSignalSuspender(processSignalTarget);
5951
- var EPIPE_CODE = "EPIPE";
5952
- var UNCAUGHT_PREFIX = "Uncaught: ";
5953
- var NEWLINE = "\n";
5954
- function formatUncaught(error) {
5955
- if (error instanceof Error && error.stack !== void 0) {
5956
- return UNCAUGHT_PREFIX + error.stack + NEWLINE;
5957
- }
5958
- return UNCAUGHT_PREFIX + String(error) + NEWLINE;
5959
- }
5960
- function logUncaughtToStderr(error) {
5961
- try {
5962
- process.stderr.write(formatUncaught(error));
5963
- } catch {
5964
- }
5965
- }
5966
- function installLifecycle() {
5967
- if (installed) return;
5968
- installed = true;
5969
- const handlers = createHandlers({
5970
- registry: moduleRegistry,
5971
- exitController: moduleExitController
5972
- });
5973
- process.on("uncaughtException", (error) => {
5974
- logUncaughtToStderr(error);
5975
- handlers.onUncaught(error);
5976
- });
5977
- process.on("unhandledRejection", (reason) => {
5978
- logUncaughtToStderr(reason);
5979
- handlers.onUncaught(reason);
5980
- });
5981
- process.on("SIGTERM", () => handlers.onSigterm());
5982
- process.on("SIGINT", () => handlers.onSigint());
5983
- process.stdout.on("error", (error) => {
5984
- if (error.code === EPIPE_CODE) {
5985
- handlers.onEpipe();
5986
- return;
5987
- }
5988
- handlers.onUncaught(error);
5989
- });
5990
- process.stderr.on("error", (error) => {
5991
- if (error.code === EPIPE_CODE) {
5992
- handlers.onEpipe();
5993
- return;
5994
- }
5995
- handlers.onUncaught(error);
5996
- });
5997
- }
5998
-
5999
- // src/lib/process-lifecycle/managed-subprocess.ts
6000
- var MANAGED_SUBPROCESS_STDIO = "pipe";
6001
- function spawnManagedSubprocess(runner, command, args, options) {
6002
- return runner.spawn(command, args, {
6003
- ...options,
6004
- stdio: MANAGED_SUBPROCESS_STDIO
6005
- });
6006
- }
6007
-
6008
- // src/interfaces/cli/journal.ts
6009
- var JOURNAL_CLI = {
6010
- commandName: "journal",
6011
- description: "Record and stream an agentic verification run journal",
6012
- openCommandName: "open",
6013
- appendCommandName: "append",
6014
- readCommandName: "read",
6015
- sealCommandName: "seal",
6016
- renderCommandName: "render",
6017
- typeOption: "--type <type>",
6018
- runOption: "--run <token>",
6019
- fromOption: "--from <cursor>"
6020
- };
6021
- var STREAM_LINE_SEPARATOR = "\n";
6022
- var MALFORMED_EVENT_INPUT_ERROR = "journal append event input is not valid JSON";
6023
- function scope(options) {
6024
- return { type: options.type };
6025
- }
6026
- function runScope(options) {
6027
- return { ...scope(options), runToken: options.run };
6028
- }
6029
- var journalDomain = {
6030
- name: JOURNAL_CLI.commandName,
6031
- description: JOURNAL_CLI.description,
6032
- register: (program2) => {
6033
- const journalCmd = program2.command(JOURNAL_CLI.commandName).description(JOURNAL_CLI.description);
6034
- journalCmd.command(JOURNAL_CLI.openCommandName).description("Open a new run journal and report its run token").requiredOption(JOURNAL_CLI.typeOption, "Opaque verification-type scope segment").action(async (options) => {
6035
- await report(await journalOpenCommand(scope(options)));
6036
- });
6037
- journalCmd.command(JOURNAL_CLI.appendCommandName).description("Append a JSON event read from standard input and stream it").requiredOption(JOURNAL_CLI.typeOption, "Opaque verification-type scope segment").requiredOption(JOURNAL_CLI.runOption, "Run token reported by open").action(async (options) => {
6038
- const input = await readStdinEventInput();
6039
- if (!input.ok) {
6040
- await report({ exitCode: JOURNAL_CLI_EXIT_CODE.ERROR, output: input.error });
6041
- return;
6042
- }
6043
- const result = await journalAppendCommand(runScope(options), input.value, streamBinding());
6044
- if (result.exitCode === JOURNAL_CLI_EXIT_CODE.OK) process.exit(JOURNAL_CLI_EXIT_CODE.OK);
6045
- else await report(result);
6046
- });
6047
- journalCmd.command(JOURNAL_CLI.readCommandName).description("Read the run's events at or after a cursor").requiredOption(JOURNAL_CLI.typeOption, "Opaque verification-type scope segment").requiredOption(JOURNAL_CLI.runOption, "Run token reported by open").requiredOption(JOURNAL_CLI.fromOption, "Sequence cursor; events at or after it are returned").action(async (options) => {
6048
- await report(await journalReadCommand(runScope(options), options.from));
6049
- });
6050
- journalCmd.command(JOURNAL_CLI.sealCommandName).description("Seal the run journal so further appends are rejected").requiredOption(JOURNAL_CLI.typeOption, "Opaque verification-type scope segment").requiredOption(JOURNAL_CLI.runOption, "Run token reported by open").action(async (options) => {
6051
- await report(await journalSealCommand(runScope(options)));
6052
- });
6053
- journalCmd.command(JOURNAL_CLI.renderCommandName).description("Render the run's event-prefix projection").requiredOption(JOURNAL_CLI.typeOption, "Opaque verification-type scope segment").requiredOption(JOURNAL_CLI.runOption, "Run token reported by open").action(async (options) => {
6054
- await report(await journalRenderCommand(runScope(options)));
6055
- });
6056
- }
6057
- };
6058
- function stdoutStreamSink() {
6059
- return {
6060
- async emit(event) {
6061
- await writeOutput(process.stdout, `${JSON.stringify(event)}${STREAM_LINE_SEPARATOR}`);
6062
- }
6063
- };
6064
- }
6065
- function streamBinding() {
6066
- const repository = process.env[JOURNAL_CLI_ENV.GITHUB_REPOSITORY] ?? "";
6067
- return {
6068
- localSink: stdoutStreamSink(),
6069
- githubClient: createGithubPullRequestCommentClient({ repository, run: runGhApi }),
6070
- githubRepository: repository
6071
- };
6072
- }
6073
- async function readStdinEventInput() {
6074
- const chunks = [];
6075
- for await (const chunk of process.stdin) {
6076
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
6077
- }
6078
- try {
6079
- return { ok: true, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) };
6080
- } catch {
6081
- return { ok: false, error: MALFORMED_EVENT_INPUT_ERROR };
6062
+ options: {
6063
+ status: {
6064
+ flag: "--status",
6065
+ placeholder: "<status>",
6066
+ description: "Filter by status (todo|doing|archive); defaults to doing + todo"
6067
+ },
6068
+ json: {
6069
+ flag: "--json",
6070
+ description: "Output as JSON"
6071
+ },
6072
+ fields: {
6073
+ flag: "--fields",
6074
+ placeholder: "<fields>",
6075
+ description: "Comma-separated fields to emit as JSON (implies --json)"
6076
+ },
6077
+ color: {
6078
+ flag: "--color",
6079
+ description: "Force colored text output"
6080
+ },
6081
+ noColor: {
6082
+ flag: "--no-color",
6083
+ description: "Disable colored text output"
6084
+ },
6085
+ sessionsDir: {
6086
+ flag: "--sessions-dir",
6087
+ placeholder: "<path>",
6088
+ description: "Custom sessions directory"
6089
+ },
6090
+ auto: {
6091
+ flag: "--auto",
6092
+ description: "Auto-select highest priority session"
6093
+ },
6094
+ noInject: {
6095
+ flag: "--no-inject",
6096
+ description: "Skip printing files listed in session specs/files metadata"
6097
+ },
6098
+ keep: {
6099
+ flag: "--keep",
6100
+ placeholder: "<count>",
6101
+ description: "Number of sessions to keep (default: 5)",
6102
+ defaultValue: "5"
6103
+ },
6104
+ dryRun: {
6105
+ flag: "--dry-run",
6106
+ description: "Show what would be deleted without deleting"
6107
+ }
6108
+ }
6109
+ };
6110
+ var sessionSubcommandOptions = [
6111
+ {
6112
+ subcommand: sessionCliDefinition.subcommands.list,
6113
+ options: [
6114
+ sessionCliDefinition.options.status,
6115
+ sessionCliDefinition.options.json,
6116
+ sessionCliDefinition.options.fields,
6117
+ sessionCliDefinition.options.color,
6118
+ sessionCliDefinition.options.noColor,
6119
+ sessionCliDefinition.options.sessionsDir
6120
+ ]
6121
+ },
6122
+ {
6123
+ subcommand: sessionCliDefinition.subcommands.pick,
6124
+ options: [sessionCliDefinition.options.sessionsDir]
6125
+ },
6126
+ {
6127
+ subcommand: sessionCliDefinition.subcommands.todo,
6128
+ options: [
6129
+ sessionCliDefinition.options.json,
6130
+ sessionCliDefinition.options.fields,
6131
+ sessionCliDefinition.options.color,
6132
+ sessionCliDefinition.options.noColor,
6133
+ sessionCliDefinition.options.sessionsDir
6134
+ ]
6135
+ },
6136
+ {
6137
+ subcommand: sessionCliDefinition.subcommands.show,
6138
+ options: [sessionCliDefinition.options.json, sessionCliDefinition.options.sessionsDir]
6139
+ },
6140
+ {
6141
+ subcommand: sessionCliDefinition.subcommands.pickup,
6142
+ options: [
6143
+ sessionCliDefinition.options.auto,
6144
+ sessionCliDefinition.options.noInject,
6145
+ sessionCliDefinition.options.sessionsDir
6146
+ ]
6147
+ },
6148
+ {
6149
+ subcommand: sessionCliDefinition.subcommands.release,
6150
+ options: [sessionCliDefinition.options.sessionsDir]
6151
+ },
6152
+ {
6153
+ subcommand: sessionCliDefinition.subcommands.handoff,
6154
+ options: [sessionCliDefinition.options.sessionsDir]
6155
+ },
6156
+ {
6157
+ subcommand: sessionCliDefinition.subcommands.delete,
6158
+ options: [sessionCliDefinition.options.sessionsDir]
6159
+ },
6160
+ {
6161
+ subcommand: sessionCliDefinition.subcommands.prune,
6162
+ options: [
6163
+ sessionCliDefinition.options.keep,
6164
+ sessionCliDefinition.options.dryRun,
6165
+ sessionCliDefinition.options.sessionsDir
6166
+ ]
6167
+ },
6168
+ {
6169
+ subcommand: sessionCliDefinition.subcommands.archive,
6170
+ options: [sessionCliDefinition.options.sessionsDir]
6082
6171
  }
6172
+ ];
6173
+ function sessionOptionsForSubcommand(subcommand) {
6174
+ return sessionSubcommandOptions.find((entry) => entry.subcommand === subcommand)?.options ?? [];
6083
6175
  }
6084
- async function report(result) {
6085
- const stream = result.exitCode === JOURNAL_CLI_EXIT_CODE.OK ? process.stdout : process.stderr;
6086
- const completed = await writeOutput(stream, `${result.output}${STREAM_LINE_SEPARATOR}`);
6087
- if (completed) process.exit(result.exitCode);
6176
+ function sessionCommandToken(subcommand) {
6177
+ return subcommand.operand === void 0 ? subcommand.commandName : `${subcommand.commandName} ${subcommand.operand}`;
6088
6178
  }
6089
- function writeOutput(stream, output) {
6090
- return new Promise((resolve8, reject) => {
6091
- stream.write(output, (error) => {
6092
- if (error === void 0 || error === null) {
6093
- resolve8(true);
6094
- return;
6095
- }
6096
- if (error.code === EPIPE_CODE) {
6097
- resolve8(false);
6098
- return;
6099
- }
6100
- reject(error);
6101
- });
6102
- });
6179
+ function sessionOptionToken(option) {
6180
+ return option.placeholder === void 0 ? option.flag : `${option.flag} ${option.placeholder}`;
6103
6181
  }
6104
6182
 
6105
6183
  // src/commands/session/archive.ts
6106
6184
  import { mkdir, rename, stat } from "fs/promises";
6107
- import { dirname as dirname5, join as join9 } from "path";
6185
+ import { dirname as dirname6, join as join9 } from "path";
6108
6186
 
6109
6187
  // src/domains/session/archive.ts
6110
6188
  var SESSION_FILE_EXTENSION = ".md";
@@ -6370,8 +6448,8 @@ async function resolveSessionConfig(options = {}) {
6370
6448
  warning
6371
6449
  };
6372
6450
  }
6373
- async function resolveSessionConfigSurfacingWarning(sessionsDir, onWarning) {
6374
- const { config, warning } = await resolveSessionConfig({ sessionsDir });
6451
+ async function resolveSessionConfigSurfacingWarning(sessionsDir, onWarning, cwd) {
6452
+ const { config, warning } = await resolveSessionConfig({ sessionsDir, cwd });
6375
6453
  if (warning !== void 0) {
6376
6454
  onWarning?.(warning);
6377
6455
  }
@@ -6425,13 +6503,13 @@ async function resolveArchivePaths(sessionId, config) {
6425
6503
  }
6426
6504
  async function archiveSingle(sessionId, config) {
6427
6505
  const { source, target } = await resolveArchivePaths(sessionId, config);
6428
- await mkdir(dirname5(target), { recursive: true });
6506
+ await mkdir(dirname6(target), { recursive: true });
6429
6507
  await rename(source, target);
6430
6508
  return `${SESSION_ARCHIVE_OUTPUT.ARCHIVED}: ${sessionId}
6431
6509
  ${SESSION_ARCHIVE_OUTPUT.ARCHIVE_LOCATION}: ${target}`;
6432
6510
  }
6433
6511
  async function archiveCommand(options) {
6434
- const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
6512
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning, options.cwd);
6435
6513
  return processBatch(options.sessionIds, (id) => archiveSingle(id, config));
6436
6514
  }
6437
6515
 
@@ -6454,6 +6532,123 @@ import { join as join10 } from "path";
6454
6532
  import { Chalk as Chalk2 } from "chalk";
6455
6533
  import { parse as parseYaml2 } from "yaml";
6456
6534
 
6535
+ // src/domains/session/display-width.ts
6536
+ var CONTROL_CODE_MAX = 31;
6537
+ var DELETE_CODE = 127;
6538
+ var C1_CONTROL_MIN = 128;
6539
+ var C1_CONTROL_MAX = 159;
6540
+ var ZERO_WIDTH_JOINER = 8205;
6541
+ var ZERO_WIDTH_RANGES = [
6542
+ { start: 768, end: 879 },
6543
+ { start: 6832, end: 6911 },
6544
+ { start: 7616, end: 7679 },
6545
+ { start: 8400, end: 8447 },
6546
+ { start: 65024, end: 65039 },
6547
+ { start: 65056, end: 65071 },
6548
+ { start: 917760, end: 917999 }
6549
+ ];
6550
+ var WIDE_RANGES = [
6551
+ { start: 4352, end: 4447 },
6552
+ { start: 8986, end: 8987 },
6553
+ { start: 9001, end: 9002 },
6554
+ { start: 9193, end: 9196 },
6555
+ { start: 9200, end: 9200 },
6556
+ { start: 9203, end: 9203 },
6557
+ { start: 9725, end: 9726 },
6558
+ { start: 9748, end: 9749 },
6559
+ { start: 9800, end: 9811 },
6560
+ { start: 9855, end: 9855 },
6561
+ { start: 9875, end: 9875 },
6562
+ { start: 9889, end: 9889 },
6563
+ { start: 9898, end: 9899 },
6564
+ { start: 9917, end: 9918 },
6565
+ { start: 9924, end: 9925 },
6566
+ { start: 9934, end: 9934 },
6567
+ { start: 9940, end: 9940 },
6568
+ { start: 9962, end: 9962 },
6569
+ { start: 9970, end: 9971 },
6570
+ { start: 9973, end: 9973 },
6571
+ { start: 9978, end: 9978 },
6572
+ { start: 9981, end: 9981 },
6573
+ { start: 9989, end: 9989 },
6574
+ { start: 9994, end: 9995 },
6575
+ { start: 10024, end: 10024 },
6576
+ { start: 10060, end: 10060 },
6577
+ { start: 10062, end: 10062 },
6578
+ { start: 10067, end: 10069 },
6579
+ { start: 10071, end: 10071 },
6580
+ { start: 10133, end: 10135 },
6581
+ { start: 10160, end: 10160 },
6582
+ { start: 10175, end: 10175 },
6583
+ { start: 11035, end: 11036 },
6584
+ { start: 11088, end: 11088 },
6585
+ { start: 11093, end: 11093 },
6586
+ { start: 11904, end: 12350 },
6587
+ { start: 12352, end: 42191 },
6588
+ { start: 44032, end: 55203 },
6589
+ { start: 63744, end: 64255 },
6590
+ { start: 65040, end: 65049 },
6591
+ { start: 65072, end: 65135 },
6592
+ { start: 65280, end: 65376 },
6593
+ { start: 65504, end: 65510 },
6594
+ { start: 126980, end: 126980 },
6595
+ { start: 127183, end: 127183 },
6596
+ { start: 127374, end: 127374 },
6597
+ { start: 127377, end: 127386 },
6598
+ { start: 127488, end: 127490 },
6599
+ { start: 127504, end: 127547 },
6600
+ { start: 127552, end: 127560 },
6601
+ { start: 127568, end: 127569 },
6602
+ { start: 127744, end: 128591 },
6603
+ { start: 128640, end: 128767 },
6604
+ { start: 129280, end: 129535 },
6605
+ { start: 131072, end: 262141 }
6606
+ ];
6607
+ var ZERO_WIDTH_COLUMNS = 0;
6608
+ var NARROW_COLUMNS = 1;
6609
+ var WIDE_COLUMNS = 2;
6610
+ function isInRange(codePoint, range) {
6611
+ return codePoint >= range.start && codePoint <= range.end;
6612
+ }
6613
+ function isControlCode(codePoint) {
6614
+ return codePoint <= CONTROL_CODE_MAX || codePoint === DELETE_CODE || codePoint >= C1_CONTROL_MIN && codePoint <= C1_CONTROL_MAX;
6615
+ }
6616
+ function codePointWidth(codePoint) {
6617
+ if (isControlCode(codePoint) || codePoint === ZERO_WIDTH_JOINER) {
6618
+ return ZERO_WIDTH_COLUMNS;
6619
+ }
6620
+ if (ZERO_WIDTH_RANGES.some((range) => isInRange(codePoint, range))) {
6621
+ return ZERO_WIDTH_COLUMNS;
6622
+ }
6623
+ if (WIDE_RANGES.some((range) => isInRange(codePoint, range))) {
6624
+ return WIDE_COLUMNS;
6625
+ }
6626
+ return NARROW_COLUMNS;
6627
+ }
6628
+ function visibleWidth(text) {
6629
+ let width = ZERO_WIDTH_COLUMNS;
6630
+ for (const symbol of Array.from(text)) {
6631
+ width += codePointWidth(symbol.codePointAt(0) ?? ZERO_WIDTH_COLUMNS);
6632
+ }
6633
+ return width;
6634
+ }
6635
+ function takeVisibleColumns(text, maxColumns) {
6636
+ if (maxColumns <= ZERO_WIDTH_COLUMNS) {
6637
+ return "";
6638
+ }
6639
+ let width = ZERO_WIDTH_COLUMNS;
6640
+ let result = "";
6641
+ for (const symbol of Array.from(text)) {
6642
+ const nextWidth = codePointWidth(symbol.codePointAt(0) ?? ZERO_WIDTH_COLUMNS);
6643
+ if (width + nextWidth > maxColumns) {
6644
+ break;
6645
+ }
6646
+ result += symbol;
6647
+ width += nextWidth;
6648
+ }
6649
+ return result;
6650
+ }
6651
+
6457
6652
  // src/domains/session/timestamp.ts
6458
6653
  var SESSION_ID_PATTERN = /^(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})$/;
6459
6654
  var SESSION_ID_SEPARATOR = "_";
@@ -6474,12 +6669,12 @@ function parseSessionId(id) {
6474
6669
  return null;
6475
6670
  }
6476
6671
  const [, yearStr, monthStr, dayStr, hoursStr, minutesStr, secondsStr] = match;
6477
- const year = parseInt(yearStr, 10);
6478
- const month = parseInt(monthStr, 10) - 1;
6479
- const day = parseInt(dayStr, 10);
6480
- const hours = parseInt(hoursStr, 10);
6481
- const minutes = parseInt(minutesStr, 10);
6482
- const seconds = parseInt(secondsStr, 10);
6672
+ const year = Number.parseInt(yearStr, 10);
6673
+ const month = Number.parseInt(monthStr, 10) - 1;
6674
+ const day = Number.parseInt(dayStr, 10);
6675
+ const hours = Number.parseInt(hoursStr, 10);
6676
+ const minutes = Number.parseInt(minutesStr, 10);
6677
+ const seconds = Number.parseInt(secondsStr, 10);
6483
6678
  if (month < 0 || month > 11) return null;
6484
6679
  if (day < 1 || day > 31) return null;
6485
6680
  if (hours < 0 || hours > 23) return null;
@@ -6500,6 +6695,18 @@ var SESSION_PRIORITY = {
6500
6695
  };
6501
6696
  var SESSION_STATUSES = ["todo", "doing", "archive"];
6502
6697
  var DEFAULT_LIST_STATUSES = ["doing", "todo"];
6698
+ var SESSION_FILE_ENCODING = "utf-8";
6699
+ var SESSION_FILE_ERROR_CODE = {
6700
+ NOT_FOUND: "ENOENT"
6701
+ };
6702
+ var SESSION_OUTPUT_MARKER = {
6703
+ HANDOFF_ID: "HANDOFF_ID",
6704
+ PICKUP_ID: "PICKUP_ID",
6705
+ SESSION_FILE: "SESSION_FILE"
6706
+ };
6707
+ function formatSessionOutputMarker(marker, value) {
6708
+ return `<${marker}>${value}</${marker}>`;
6709
+ }
6503
6710
  var CLAIMABLE_STATUS = SESSION_STATUSES[0];
6504
6711
  var PRIORITY_ORDER = {
6505
6712
  [SESSION_PRIORITY.HIGH]: 0,
@@ -6537,7 +6744,7 @@ function defaultSessionMetadata() {
6537
6744
  };
6538
6745
  }
6539
6746
  function isValidPriority(value) {
6540
- return typeof value === "string" && SESSION_PRIORITY_VALUES.some((priority) => priority === value);
6747
+ return typeof value === "string" && SESSION_PRIORITY_VALUES.includes(value);
6541
6748
  }
6542
6749
  function parseSessionMetadata(content) {
6543
6750
  const match = FRONT_MATTER_PATTERN.exec(content);
@@ -6545,10 +6752,11 @@ function parseSessionMetadata(content) {
6545
6752
  return defaultSessionMetadata();
6546
6753
  }
6547
6754
  try {
6548
- const parsed = parseYaml2(match[1]);
6549
- if (!parsed || typeof parsed !== "object") {
6755
+ const parsedDocument = parseYaml2(match[1]);
6756
+ if (!parsedDocument || typeof parsedDocument !== "object") {
6550
6757
  return defaultSessionMetadata();
6551
6758
  }
6759
+ const parsed = parsedDocument;
6552
6760
  const rawPriority = parsed[SESSION_FRONT_MATTER.PRIORITY];
6553
6761
  const priority = isValidPriority(rawPriority) ? rawPriority : DEFAULT_PRIORITY;
6554
6762
  const metadata = {
@@ -6685,13 +6893,13 @@ function formatSessionLine(session, width, chalk) {
6685
6893
  const { id, metadata } = session;
6686
6894
  const badge = metadata.priority === DEFAULT_PRIORITY ? "" : ` [${metadata.priority}]`;
6687
6895
  const summary = metadata.goal.length > 0 && metadata.next_step.length > 0 ? ` ${metadata.goal}${LIST_SUMMARY_SEPARATOR}${metadata.next_step}` : "";
6688
- let remaining = Math.max(0, width - LIST_INDENT.length);
6689
- const idShown = id.slice(0, remaining);
6690
- remaining -= idShown.length;
6691
- const badgeShown = badge.slice(0, remaining);
6692
- remaining -= badgeShown.length;
6693
- const summaryShown = summary.slice(0, remaining);
6694
- const styledBadge = badgeShown.length > 0 ? PRIORITY_STYLE[metadata.priority](chalk, badgeShown) : "";
6896
+ let remaining = Math.max(0, width - visibleWidth(LIST_INDENT));
6897
+ const idShown = takeVisibleColumns(id, remaining);
6898
+ remaining -= visibleWidth(idShown);
6899
+ const badgeShown = takeVisibleColumns(badge, remaining);
6900
+ remaining -= visibleWidth(badgeShown);
6901
+ const summaryShown = takeVisibleColumns(summary, remaining);
6902
+ const styledBadge = visibleWidth(badgeShown) > 0 ? PRIORITY_STYLE[metadata.priority](chalk, badgeShown) : "";
6695
6903
  return `${LIST_INDENT}${chalk.dim(idShown)}${styledBadge}${chalk.dim(summaryShown)}`;
6696
6904
  }
6697
6905
  function formatSessionListText(sessions, opts) {
@@ -6772,13 +6980,13 @@ async function deleteSingle(sessionId, config) {
6772
6980
  return `${SESSION_DELETE_OUTPUT.DELETED}: ${sessionId}`;
6773
6981
  }
6774
6982
  async function deleteCommand(options) {
6775
- const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
6983
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning, options.cwd);
6776
6984
  return processBatch(options.sessionIds, (id) => deleteSingle(id, config));
6777
6985
  }
6778
6986
 
6779
6987
  // src/commands/session/handoff.ts
6780
6988
  import { mkdir as mkdir2, writeFile } from "fs/promises";
6781
- import { join as join11, resolve as resolve4 } from "path";
6989
+ import { join as join11, resolve as resolve5 } from "path";
6782
6990
  import { stringify as stringifyYaml3 } from "yaml";
6783
6991
 
6784
6992
  // src/domains/session/create.ts
@@ -6849,12 +7057,12 @@ function resolveWorkBranchGitRef(workBranch, existsOnOrigin) {
6849
7057
  // src/domains/session/parse-handoff-input.ts
6850
7058
  var LEGACY_FRONTMATTER_PREFIX = /^---\r?\n/;
6851
7059
  var JSON_OBJECT_OPEN_CHAR = "{";
6852
- var CARRIAGE_RETURN_CHAR_CODE = "\r".charCodeAt(0);
6853
- var NEWLINE_CHAR_CODE = "\n".charCodeAt(0);
6854
- var BACKSLASH_CHAR_CODE = "\\".charCodeAt(0);
6855
- var DOUBLE_QUOTE_CHAR_CODE = '"'.charCodeAt(0);
6856
- var OPEN_BRACE_CHAR_CODE = "{".charCodeAt(0);
6857
- var CLOSE_BRACE_CHAR_CODE = "}".charCodeAt(0);
7060
+ var CARRIAGE_RETURN_CHAR_CODE = "\r".codePointAt(0) ?? 0;
7061
+ var NEWLINE_CHAR_CODE = "\n".codePointAt(0) ?? 0;
7062
+ var BACKSLASH_CHAR_CODE = "\\".codePointAt(0) ?? 0;
7063
+ var DOUBLE_QUOTE_CHAR_CODE = '"'.codePointAt(0) ?? 0;
7064
+ var OPEN_BRACE_CHAR_CODE = "{".codePointAt(0) ?? 0;
7065
+ var CLOSE_BRACE_CHAR_CODE = "}".codePointAt(0) ?? 0;
6858
7066
  var UNBALANCED_HEADER_END = -1;
6859
7067
  var LF_SEPARATOR_LENGTH = 1;
6860
7068
  var CRLF_SEPARATOR_LENGTH = 2;
@@ -6887,10 +7095,10 @@ function parseHandoffInput(stdin) {
6887
7095
  return { header, body: stdin.slice(bodyStart) };
6888
7096
  }
6889
7097
  function consumeOptionalBodySeparator(stdin, bodyStart) {
6890
- if (stdin.charCodeAt(bodyStart) === CARRIAGE_RETURN_CHAR_CODE && stdin.charCodeAt(bodyStart + LF_SEPARATOR_LENGTH) === NEWLINE_CHAR_CODE) {
7098
+ if (stdin.codePointAt(bodyStart) === CARRIAGE_RETURN_CHAR_CODE && stdin.codePointAt(bodyStart + LF_SEPARATOR_LENGTH) === NEWLINE_CHAR_CODE) {
6891
7099
  return bodyStart + CRLF_SEPARATOR_LENGTH;
6892
7100
  }
6893
- if (stdin.charCodeAt(bodyStart) === NEWLINE_CHAR_CODE) {
7101
+ if (stdin.codePointAt(bodyStart) === NEWLINE_CHAR_CODE) {
6894
7102
  return bodyStart + LF_SEPARATOR_LENGTH;
6895
7103
  }
6896
7104
  return bodyStart;
@@ -6899,32 +7107,33 @@ function findJsonObjectEnd(stdin) {
6899
7107
  let depth = 0;
6900
7108
  let inString = false;
6901
7109
  for (let i = 0; i < stdin.length; i++) {
6902
- const code = stdin.charCodeAt(i);
7110
+ const code = stdin.codePointAt(i);
6903
7111
  if (inString) {
6904
- if (code === BACKSLASH_CHAR_CODE) {
6905
- i += 1;
6906
- continue;
6907
- }
6908
- if (code === DOUBLE_QUOTE_CHAR_CODE) {
6909
- inString = false;
6910
- }
7112
+ const stringState = scanJsonStringCharacter(code);
7113
+ inString = stringState.inString;
7114
+ if (stringState.skipNext) i += 1;
6911
7115
  continue;
6912
7116
  }
6913
7117
  if (code === DOUBLE_QUOTE_CHAR_CODE) {
6914
7118
  inString = true;
6915
7119
  continue;
6916
7120
  }
6917
- if (code === OPEN_BRACE_CHAR_CODE) {
6918
- depth += 1;
6919
- continue;
6920
- }
6921
- if (code === CLOSE_BRACE_CHAR_CODE) {
6922
- depth -= 1;
6923
- if (depth === 0) return i;
6924
- }
7121
+ depth = nextJsonObjectDepth(depth, code);
7122
+ if (depth === 0 && code === CLOSE_BRACE_CHAR_CODE) return i;
6925
7123
  }
6926
7124
  return UNBALANCED_HEADER_END;
6927
7125
  }
7126
+ function scanJsonStringCharacter(code) {
7127
+ if (code === BACKSLASH_CHAR_CODE) {
7128
+ return { inString: true, skipNext: true };
7129
+ }
7130
+ return { inString: code !== DOUBLE_QUOTE_CHAR_CODE, skipNext: false };
7131
+ }
7132
+ function nextJsonObjectDepth(depth, code) {
7133
+ if (code === OPEN_BRACE_CHAR_CODE) return depth + 1;
7134
+ if (code === CLOSE_BRACE_CHAR_CODE) return depth - 1;
7135
+ return depth;
7136
+ }
6928
7137
  function validateHandoffHeader(parsed) {
6929
7138
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
6930
7139
  throw new SessionInvalidJsonHeaderError("header must be a JSON object");
@@ -6989,13 +7198,16 @@ async function resolveSessionGitRef(cwd, deps) {
6989
7198
  gatherGitFacts(cwd, deps)
6990
7199
  ]);
6991
7200
  const isGitRepo = facts !== null;
6992
- const currentWorktreePath = facts?.worktreeRoot ?? cwd ?? process.cwd();
7201
+ const currentWorktreePath = facts?.worktreeRoot ?? cwd ?? CONFIG_PROCESS_CWD.read();
6993
7202
  const isMain = facts !== null && isMainCheckout(facts);
6994
- const designatedMainCheckout = facts !== null ? mainCheckoutPath(facts) : null;
7203
+ let designatedMainCheckout = null;
7204
+ if (facts !== null) {
7205
+ designatedMainCheckout = mainCheckoutPath(facts);
7206
+ }
6995
7207
  let isClean = false;
6996
7208
  let defaultBranch = null;
6997
7209
  let defaultTipSha = null;
6998
- if (isGitRepo && !isMain) {
7210
+ if (isGitRepo && isMain === false) {
6999
7211
  [isClean, defaultBranch] = await Promise.all([
7000
7212
  isWorkingTreeClean(cwd, deps),
7001
7213
  resolveDefaultBranch(cwd, deps)
@@ -7057,11 +7269,11 @@ async function handoffCommand(options) {
7057
7269
  const fullContent = `${SESSION_FRONT_MATTER_OPEN}${yaml}${SESSION_FRONT_MATTER_CLOSE}${body}`;
7058
7270
  const filename = `${sessionId}.md`;
7059
7271
  const sessionPath = join11(config.todoDir, filename);
7060
- const absolutePath = resolve4(sessionPath);
7272
+ const absolutePath = resolve5(sessionPath);
7061
7273
  await mkdir2(config.todoDir, { recursive: true });
7062
- await writeFile(sessionPath, fullContent, "utf-8");
7063
- const output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
7064
- <SESSION_FILE>${absolutePath}</SESSION_FILE>`;
7274
+ await writeFile(sessionPath, fullContent, SESSION_FILE_ENCODING);
7275
+ const output = `Created handoff session ${formatSessionOutputMarker(SESSION_OUTPUT_MARKER.HANDOFF_ID, sessionId)}
7276
+ ${formatSessionOutputMarker(SESSION_OUTPUT_MARKER.SESSION_FILE, absolutePath)}`;
7065
7277
  return { output };
7066
7278
  }
7067
7279
 
@@ -7081,7 +7293,7 @@ async function loadSessionsFromDir(dir, status) {
7081
7293
  if (!file.endsWith(".md")) continue;
7082
7294
  const id = file.replace(".md", "");
7083
7295
  const filePath = join12(dir, file);
7084
- const content = await readFile4(filePath, "utf-8");
7296
+ const content = await readFile4(filePath, SESSION_FILE_ENCODING);
7085
7297
  const metadata = parseSessionMetadata(content);
7086
7298
  sessions.push({
7087
7299
  id,
@@ -7092,7 +7304,7 @@ async function loadSessionsFromDir(dir, status) {
7092
7304
  }
7093
7305
  return sessions;
7094
7306
  } catch (error) {
7095
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7307
+ if (error instanceof Error && "code" in error && error.code === SESSION_FILE_ERROR_CODE.NOT_FOUND) {
7096
7308
  return [];
7097
7309
  }
7098
7310
  throw error;
@@ -7112,9 +7324,9 @@ function validateStatus(input) {
7112
7324
  );
7113
7325
  }
7114
7326
  async function listCommand(options) {
7115
- const fieldSelection = options.fields !== void 0 ? parseFieldSelection(options.fields) : void 0;
7116
- const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
7117
- const statuses = options.status !== void 0 ? [validateStatus(options.status)] : DEFAULT_LIST_STATUSES;
7327
+ const fieldSelection = options.fields === void 0 ? void 0 : parseFieldSelection(options.fields);
7328
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning, options.cwd);
7329
+ const statuses = options.status === void 0 ? DEFAULT_LIST_STATUSES : [validateStatus(options.status)];
7118
7330
  const sessionsByStatus = {};
7119
7331
  for (const status of statuses) {
7120
7332
  const dirKey = STATUS_DIR_KEY[status];
@@ -7127,7 +7339,7 @@ async function listCommand(options) {
7127
7339
  for (const status of statuses) {
7128
7340
  recordsByStatus[status] = (sessionsByStatus[status] ?? []).map((session) => {
7129
7341
  const record6 = toSessionRecord(session);
7130
- return fieldSelection !== void 0 ? projectSessionRecord(record6, fieldSelection) : record6;
7342
+ return fieldSelection === void 0 ? record6 : projectSessionRecord(record6, fieldSelection);
7131
7343
  });
7132
7344
  }
7133
7345
  return JSON.stringify(recordsByStatus, null, 2);
@@ -7145,7 +7357,7 @@ async function listCommand(options) {
7145
7357
 
7146
7358
  // src/commands/session/pickup.ts
7147
7359
  import { mkdir as mkdir3, readdir as readdir2, readFile as readFile5, rename as rename2 } from "fs/promises";
7148
- import { join as join13 } from "path";
7360
+ import { join as join13, resolve as resolve6 } from "path";
7149
7361
 
7150
7362
  // src/domains/session/pickup.ts
7151
7363
  function buildClaimPaths(sessionId, config) {
@@ -7155,7 +7367,7 @@ function buildClaimPaths(sessionId, config) {
7155
7367
  };
7156
7368
  }
7157
7369
  function classifyClaimError(error, sessionId) {
7158
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7370
+ if (error instanceof Error && "code" in error && error.code === SESSION_FILE_ERROR_CODE.NOT_FOUND) {
7159
7371
  return new SessionNotAvailableError(sessionId);
7160
7372
  }
7161
7373
  throw error;
@@ -7182,15 +7394,24 @@ function selectBestSession(sessions) {
7182
7394
 
7183
7395
  // src/commands/session/pickup.ts
7184
7396
  var PICKUP_TARGET_STATUS = SESSION_STATUSES[1];
7397
+ var PICKUP_DEPS = {
7398
+ mkdir: mkdir3,
7399
+ readdir: readdir2,
7400
+ readFile: readFile5,
7401
+ rename: rename2
7402
+ };
7185
7403
  async function loadTodoSessions(config) {
7404
+ return loadTodoSessionsWithDeps(config, PICKUP_DEPS);
7405
+ }
7406
+ async function loadTodoSessionsWithDeps(config, deps) {
7186
7407
  try {
7187
- const files = await readdir2(config.todoDir);
7408
+ const files = await deps.readdir(config.todoDir);
7188
7409
  const sessions = [];
7189
7410
  for (const file of files) {
7190
7411
  if (!file.endsWith(".md")) continue;
7191
7412
  const id = file.replace(".md", "");
7192
7413
  const filePath = join13(config.todoDir, file);
7193
- const content = await readFile5(filePath, "utf-8");
7414
+ const content = await deps.readFile(filePath, SESSION_FILE_ENCODING);
7194
7415
  const metadata = parseSessionMetadata(content);
7195
7416
  sessions.push({
7196
7417
  id,
@@ -7201,51 +7422,84 @@ async function loadTodoSessions(config) {
7201
7422
  }
7202
7423
  return sessions;
7203
7424
  } catch (error) {
7204
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7425
+ if (error instanceof Error && "code" in error && error.code === SESSION_FILE_ERROR_CODE.NOT_FOUND) {
7205
7426
  return [];
7206
7427
  }
7207
7428
  throw error;
7208
7429
  }
7209
7430
  }
7210
- async function pickupSingle(sessionId, config) {
7431
+ var SESSION_INJECTION_SECTION_PREFIX = "Injected file";
7432
+ var SESSION_INJECTION_MISSING_WARNING_PREFIX = "Warning: missing session injection file";
7433
+ function injectionPath(cwd, filePath) {
7434
+ return resolve6(cwd, filePath);
7435
+ }
7436
+ function formatInjectedFile(listedPath, content) {
7437
+ return `${SESSION_INJECTION_SECTION_PREFIX}: ${listedPath}
7438
+ ${content}`;
7439
+ }
7440
+ async function readInjectedFiles(metadata, cwd, deps, onWarning) {
7441
+ const sections = [];
7442
+ for (const listedPath of [...metadata.specs, ...metadata.files]) {
7443
+ try {
7444
+ const content = await deps.readFile(injectionPath(cwd, listedPath), SESSION_FILE_ENCODING);
7445
+ sections.push(formatInjectedFile(listedPath, content));
7446
+ } catch (error) {
7447
+ if (error instanceof Error && "code" in error && error.code === SESSION_FILE_ERROR_CODE.NOT_FOUND) {
7448
+ onWarning?.(`${SESSION_INJECTION_MISSING_WARNING_PREFIX}: ${listedPath}`);
7449
+ continue;
7450
+ }
7451
+ throw error;
7452
+ }
7453
+ }
7454
+ return sections;
7455
+ }
7456
+ async function pickupSingle(sessionId, config, cwd, deps, noInject, onWarning) {
7211
7457
  const paths = buildClaimPaths(sessionId, config);
7212
- await mkdir3(config.doingDir, { recursive: true });
7458
+ await deps.mkdir(config.doingDir, { recursive: true });
7213
7459
  try {
7214
- await rename2(paths.source, paths.target);
7460
+ await deps.rename(paths.source, paths.target);
7215
7461
  } catch (error) {
7216
7462
  throw classifyClaimError(error, sessionId);
7217
7463
  }
7218
- const content = await readFile5(paths.target, "utf-8");
7464
+ const content = await deps.readFile(paths.target, SESSION_FILE_ENCODING);
7465
+ const metadata = parseSessionMetadata(content);
7219
7466
  const output = formatShowOutput(content, { status: PICKUP_TARGET_STATUS });
7220
- return `Claimed session <PICKUP_ID>${sessionId}</PICKUP_ID>
7467
+ const injected = noInject ? [] : await readInjectedFiles(metadata, cwd, deps, onWarning);
7468
+ const injectionOutput = injected.length === 0 ? "" : `
7221
7469
 
7222
- ${output}`;
7470
+ ${injected.join("\n\n")}`;
7471
+ return `Claimed session ${formatSessionOutputMarker(SESSION_OUTPUT_MARKER.PICKUP_ID, sessionId)}
7472
+
7473
+ ${output}${injectionOutput}`;
7223
7474
  }
7224
7475
  async function pickupCommand(options) {
7225
- const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
7476
+ const deps = options.deps ?? PICKUP_DEPS;
7477
+ const cwd = options.cwd ?? CONFIG_PROCESS_CWD.read();
7478
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning, cwd);
7479
+ const noInject = options.noInject === true;
7226
7480
  if (options.auto) {
7227
7481
  if (options.sessionIds.length > 0) {
7228
7482
  throw new Error("Session IDs cannot be combined with --auto");
7229
7483
  }
7230
- const sessions = await loadTodoSessions(config);
7484
+ const sessions = await loadTodoSessionsWithDeps(config, deps);
7231
7485
  const selected = selectBestSession(sessions);
7232
7486
  if (!selected) {
7233
7487
  throw new NoSessionsAvailableError();
7234
7488
  }
7235
- return pickupSingle(selected.id, config);
7489
+ return pickupSingle(selected.id, config, cwd, deps, noInject, options.onWarning);
7236
7490
  }
7237
7491
  if (options.sessionIds.length === 0) {
7238
7492
  throw new Error("Either session ID or --auto flag is required");
7239
7493
  }
7240
7494
  if (options.sessionIds.length === 1) {
7241
- return pickupSingle(options.sessionIds[0], config);
7495
+ return pickupSingle(options.sessionIds[0], config, cwd, deps, noInject, options.onWarning);
7242
7496
  }
7243
- return processBatch(options.sessionIds, (id) => pickupSingle(id, config));
7497
+ return processBatch(options.sessionIds, (id) => pickupSingle(id, config, cwd, deps, noInject, options.onWarning));
7244
7498
  }
7245
7499
 
7246
7500
  // src/commands/session/pick.ts
7247
7501
  async function loadPickCandidates(options) {
7248
- const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
7502
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning, options.cwd);
7249
7503
  return loadTodoSessions(config);
7250
7504
  }
7251
7505
 
@@ -7301,7 +7555,7 @@ async function loadArchiveSessions(config) {
7301
7555
  if (!file.endsWith(".md")) continue;
7302
7556
  const id = file.replace(".md", "");
7303
7557
  const filePath = join14(config.archiveDir, file);
7304
- const content = await readFile6(filePath, "utf-8");
7558
+ const content = await readFile6(filePath, SESSION_FILE_ENCODING);
7305
7559
  const metadata = parseSessionMetadata(content);
7306
7560
  sessions.push({
7307
7561
  id,
@@ -7312,7 +7566,7 @@ async function loadArchiveSessions(config) {
7312
7566
  }
7313
7567
  return sessions;
7314
7568
  } catch (error) {
7315
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7569
+ if (error instanceof Error && "code" in error && error.code === SESSION_FILE_ERROR_CODE.NOT_FOUND) {
7316
7570
  return [];
7317
7571
  }
7318
7572
  throw error;
@@ -7322,7 +7576,7 @@ async function pruneCommand(options) {
7322
7576
  validatePruneOptions(options);
7323
7577
  const keep = options.keep ?? DEFAULT_KEEP_COUNT;
7324
7578
  const dryRun = options.dryRun ?? false;
7325
- const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
7579
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning, options.cwd);
7326
7580
  const sessions = await loadArchiveSessions(config);
7327
7581
  const toPrune = selectSessionsToDelete(sessions, { keep });
7328
7582
  if (toPrune.length === 0) {
@@ -7384,7 +7638,7 @@ async function loadDoingSessions(config) {
7384
7638
  const files = await readdir4(config.doingDir);
7385
7639
  return files.filter((file) => file.endsWith(".md")).map((file) => ({ id: file.replace(".md", "") }));
7386
7640
  } catch (error) {
7387
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7641
+ if (error instanceof Error && "code" in error && error.code === SESSION_FILE_ERROR_CODE.NOT_FOUND) {
7388
7642
  return [];
7389
7643
  }
7390
7644
  throw error;
@@ -7395,7 +7649,7 @@ async function releaseSingle(sessionId, config) {
7395
7649
  try {
7396
7650
  await rename3(paths.source, paths.target);
7397
7651
  } catch (error) {
7398
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7652
+ if (error instanceof Error && "code" in error && error.code === SESSION_FILE_ERROR_CODE.NOT_FOUND) {
7399
7653
  throw new SessionNotClaimedError(sessionId);
7400
7654
  }
7401
7655
  throw error;
@@ -7404,7 +7658,7 @@ async function releaseSingle(sessionId, config) {
7404
7658
  ${SESSION_RELEASE_OUTPUT.RETURNED_TO_TODO}`;
7405
7659
  }
7406
7660
  async function releaseCommand(options) {
7407
- const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
7661
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning, options.cwd);
7408
7662
  let ids = options.sessionIds;
7409
7663
  if (ids.length === 0) {
7410
7664
  const sessions = await loadDoingSessions(config);
@@ -7438,7 +7692,7 @@ async function resolveSession(sessionId, config) {
7438
7692
  if (!found) {
7439
7693
  throw new SessionNotFoundError(sessionId);
7440
7694
  }
7441
- const content = await readFile7(found.path, "utf-8");
7695
+ const content = await readFile7(found.path, SESSION_FILE_ENCODING);
7442
7696
  return { status: found.status, path: found.path, content };
7443
7697
  }
7444
7698
  async function showSingle(sessionId, config) {
@@ -7471,7 +7725,7 @@ ${failureLines}`;
7471
7725
  return JSON.stringify(payload, null, 2);
7472
7726
  }
7473
7727
  async function showCommand2(options) {
7474
- const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
7728
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning, options.cwd);
7475
7729
  if (options.format === SESSION_LIST_FORMAT.JSON) {
7476
7730
  return showJson(options.sessionIds, config);
7477
7731
  }
@@ -7551,7 +7805,7 @@ Output:
7551
7805
  `;
7552
7806
 
7553
7807
  // src/domains/session/pick-model.ts
7554
- import { resolve as resolve5 } from "path";
7808
+ import { resolve as resolve7 } from "path";
7555
7809
  var ELLIPSIS = "\u2026";
7556
7810
  var PICKER_RUNTIME = {
7557
7811
  CLAUDE: "claude",
@@ -7561,21 +7815,24 @@ var RUNTIME_SKILL_PREFIX = {
7561
7815
  [PICKER_RUNTIME.CLAUDE]: "/",
7562
7816
  [PICKER_RUNTIME.CODEX]: "$"
7563
7817
  };
7818
+ var PICKER_PICKUP_COMMAND_NAME = "pickup";
7819
+ var PICKER_AUTO_CONTINUE_FLAG = "--auto-continue";
7564
7820
  function buildPickupCommand(runtime, autoContinue, reference) {
7565
- const prompt = `${RUNTIME_SKILL_PREFIX[runtime]}pickup ${reference}${autoContinue ? " --auto-continue" : ""}`;
7821
+ const prompt = `${RUNTIME_SKILL_PREFIX[runtime]}${PICKER_PICKUP_COMMAND_NAME} ${reference}${autoContinue ? ` ${PICKER_AUTO_CONTINUE_FLAG}` : ""}`;
7566
7822
  return { command: runtime, args: [prompt] };
7567
7823
  }
7568
7824
  function pickupReference(session, sessionsDir, cwd) {
7569
- return sessionsDir === void 0 ? session.id : resolve5(cwd, session.path);
7825
+ return sessionsDir === void 0 ? session.id : resolve7(cwd, session.path);
7570
7826
  }
7571
7827
  function toSingleLine(text) {
7572
- return text.replace(/\s+/g, " ").trim();
7828
+ return text.replaceAll(/\s+/g, " ").trim();
7573
7829
  }
7574
7830
  function truncateToWidth(text, max) {
7575
7831
  if (max <= 0) return "";
7576
- if (text.length <= max) return text;
7577
- if (max === 1) return ELLIPSIS;
7578
- return text.slice(0, max - 1) + ELLIPSIS;
7832
+ if (visibleWidth(text) <= max) return text;
7833
+ const ellipsisWidth = visibleWidth(ELLIPSIS);
7834
+ if (max <= ellipsisWidth) return takeVisibleColumns(ELLIPSIS, max);
7835
+ return `${takeVisibleColumns(text, max - ellipsisWidth)}${ELLIPSIS}`;
7579
7836
  }
7580
7837
  var PICKER_MODE = {
7581
7838
  BROWSE: "browse",
@@ -7680,17 +7937,195 @@ function reducePicker(state, action) {
7680
7937
  }
7681
7938
  }
7682
7939
 
7940
+ // src/lib/process-lifecycle/exit-codes.ts
7941
+ var SIGINT_EXIT_CODE = 130;
7942
+ var SIGTERM_EXIT_CODE = 143;
7943
+ var EPIPE_EXIT_CODE = 0;
7944
+ var UNCAUGHT_EXIT_CODE = 1;
7945
+
7946
+ // src/lib/process-lifecycle/foreground-handoff.ts
7947
+ var FOREGROUND_SIGNALS = ["SIGINT", "SIGTERM"];
7948
+ function createSignalSuspender(target) {
7949
+ return {
7950
+ suspend() {
7951
+ const ignore = () => {
7952
+ };
7953
+ const restorers = FOREGROUND_SIGNALS.map((signal) => {
7954
+ const originals = target.listeners(signal);
7955
+ for (const listener of originals) target.removeListener(signal, listener);
7956
+ target.on(signal, ignore);
7957
+ return () => {
7958
+ target.removeListener(signal, ignore);
7959
+ for (const listener of originals) target.on(signal, listener);
7960
+ };
7961
+ });
7962
+ return () => {
7963
+ for (const restore of restorers) restore();
7964
+ };
7965
+ }
7966
+ };
7967
+ }
7968
+
7969
+ // src/lib/process-lifecycle/handlers.ts
7970
+ var SIGINT_NAME = "SIGINT";
7971
+ var SIGTERM_NAME = "SIGTERM";
7972
+ function createHandlers(deps) {
7973
+ let cleanupOnce = false;
7974
+ function killEachChild(signal) {
7975
+ deps.registry.forEach((child) => {
7976
+ if (!child.killed) child.kill(signal);
7977
+ });
7978
+ }
7979
+ function exitOnce(code, killSignal) {
7980
+ if (cleanupOnce) {
7981
+ deps.exitController.exit(code);
7982
+ return;
7983
+ }
7984
+ cleanupOnce = true;
7985
+ if (killSignal !== void 0) killEachChild(killSignal);
7986
+ deps.exitController.exit(code);
7987
+ }
7988
+ return {
7989
+ onSigint() {
7990
+ exitOnce(SIGINT_EXIT_CODE, SIGINT_NAME);
7991
+ },
7992
+ onSigterm() {
7993
+ exitOnce(SIGTERM_EXIT_CODE, SIGTERM_NAME);
7994
+ },
7995
+ onEpipe() {
7996
+ exitOnce(EPIPE_EXIT_CODE, SIGTERM_NAME);
7997
+ },
7998
+ onUncaught(_error) {
7999
+ exitOnce(UNCAUGHT_EXIT_CODE, SIGTERM_NAME);
8000
+ }
8001
+ };
8002
+ }
8003
+
8004
+ // src/lib/process-lifecycle/install.ts
8005
+ import { spawn } from "child_process";
8006
+
8007
+ // src/lib/process-lifecycle/registry.ts
8008
+ function createRegistry() {
8009
+ const tracked = /* @__PURE__ */ new Set();
8010
+ return {
8011
+ add(child) {
8012
+ tracked.add(child);
8013
+ },
8014
+ remove(child) {
8015
+ tracked.delete(child);
8016
+ },
8017
+ forEach(fn) {
8018
+ for (const child of tracked) fn(child);
8019
+ },
8020
+ get size() {
8021
+ return tracked.size;
8022
+ }
8023
+ };
8024
+ }
8025
+
8026
+ // src/lib/process-lifecycle/runner.ts
8027
+ function createLifecycleRunner(deps) {
8028
+ return {
8029
+ spawn(command, args, options) {
8030
+ const child = deps.spawn(command, args, options);
8031
+ deps.registry.add(child);
8032
+ child.on("exit", () => deps.registry.remove(child));
8033
+ return child;
8034
+ }
8035
+ };
8036
+ }
8037
+
8038
+ // src/lib/process-lifecycle/install.ts
8039
+ var moduleRegistry = createRegistry();
8040
+ var moduleExitController = {
8041
+ exit(code) {
8042
+ process.exit(code);
8043
+ }
8044
+ };
8045
+ var installed = false;
8046
+ var lifecycleProcessRunner = createLifecycleRunner({
8047
+ registry: moduleRegistry,
8048
+ spawn
8049
+ });
8050
+ var foregroundProcessRunner = { spawn };
8051
+ var processSignalTarget = {
8052
+ listeners: (signal) => process.listeners(signal),
8053
+ on: (signal, listener) => {
8054
+ process.on(signal, listener);
8055
+ },
8056
+ removeListener: (signal, listener) => {
8057
+ process.removeListener(signal, listener);
8058
+ }
8059
+ };
8060
+ var lifecycleSignalSuspender = createSignalSuspender(processSignalTarget);
8061
+ var EPIPE_CODE = "EPIPE";
8062
+ var UNCAUGHT_PREFIX = "Uncaught: ";
8063
+ var NEWLINE = "\n";
8064
+ function formatUncaught(error) {
8065
+ if (error instanceof Error && error.stack !== void 0) {
8066
+ return UNCAUGHT_PREFIX + error.stack + NEWLINE;
8067
+ }
8068
+ return UNCAUGHT_PREFIX + String(error) + NEWLINE;
8069
+ }
8070
+ function logUncaughtToStderr(error) {
8071
+ try {
8072
+ process.stderr.write(formatUncaught(error));
8073
+ } catch {
8074
+ }
8075
+ }
8076
+ function installLifecycle() {
8077
+ if (installed) return;
8078
+ installed = true;
8079
+ const handlers = createHandlers({
8080
+ registry: moduleRegistry,
8081
+ exitController: moduleExitController
8082
+ });
8083
+ process.on("uncaughtException", (error) => {
8084
+ logUncaughtToStderr(error);
8085
+ handlers.onUncaught(error);
8086
+ });
8087
+ process.on("unhandledRejection", (reason) => {
8088
+ logUncaughtToStderr(reason);
8089
+ handlers.onUncaught(reason);
8090
+ });
8091
+ process.on("SIGTERM", () => handlers.onSigterm());
8092
+ process.on("SIGINT", () => handlers.onSigint());
8093
+ process.stdout.on("error", (error) => {
8094
+ if (error.code === EPIPE_CODE) {
8095
+ handlers.onEpipe();
8096
+ return;
8097
+ }
8098
+ handlers.onUncaught(error);
8099
+ });
8100
+ process.stderr.on("error", (error) => {
8101
+ if (error.code === EPIPE_CODE) {
8102
+ handlers.onEpipe();
8103
+ return;
8104
+ }
8105
+ handlers.onUncaught(error);
8106
+ });
8107
+ }
8108
+
8109
+ // src/lib/process-lifecycle/managed-subprocess.ts
8110
+ var MANAGED_SUBPROCESS_STDIO = "pipe";
8111
+ function spawnManagedSubprocess(runner, command, args, options) {
8112
+ return runner.spawn(command, args, {
8113
+ ...options,
8114
+ stdio: MANAGED_SUBPROCESS_STDIO
8115
+ });
8116
+ }
8117
+
7683
8118
  // src/interfaces/cli/session/pick/launch-agent.ts
7684
8119
  var LAUNCH_FAILURE_STATUS = 1;
7685
8120
  function launchAgent(runner, suspender, command) {
7686
8121
  const restoreSignals = suspender.suspend();
7687
- return new Promise((resolve8) => {
8122
+ return new Promise((resolve10) => {
7688
8123
  let settled = false;
7689
8124
  const settle = (status) => {
7690
8125
  if (settled) return;
7691
8126
  settled = true;
7692
8127
  restoreSignals();
7693
- resolve8(status);
8128
+ resolve10(status);
7694
8129
  };
7695
8130
  const child = runner.spawn(command.command, command.args, { stdio: "inherit" });
7696
8131
  child.once("exit", (code) => settle(code ?? LAUNCH_FAILURE_STATUS));
@@ -7735,7 +8170,7 @@ function SessionRow({ session, selected, columns }) {
7735
8170
  const priority = session.metadata.priority;
7736
8171
  const marker = selected ? SESSION_PICKER_SELECTED_MARKER : " ";
7737
8172
  const badge = priority === DEFAULT_PRIORITY ? "" : ` [${priority}]`;
7738
- const reserved = marker.length + 1 + session.id.length + badge.length + 1;
8173
+ const reserved = visibleWidth(marker) + visibleWidth(" ") + visibleWidth(session.id) + visibleWidth(badge) + visibleWidth(" ");
7739
8174
  const goal = truncateToWidth(toSingleLine(session.metadata.goal), Math.max(MIN_GOAL_WIDTH, columns - reserved));
7740
8175
  return /* @__PURE__ */ jsxs(Text, { wrap: "truncate", color: selected ? "cyan" : void 0, children: [
7741
8176
  marker,
@@ -7760,7 +8195,8 @@ function PreviewPane({ session }) {
7760
8195
  }
7761
8196
  function SessionPicker({ sessions, onLaunch, onQuit, columns: columnsProp }) {
7762
8197
  const { stdout } = useStdout();
7763
- const columns = columnsProp ?? stdout?.columns ?? FALLBACK_COLUMNS;
8198
+ const stdoutColumns = Reflect.get(stdout, "columns");
8199
+ const columns = columnsProp ?? (typeof stdoutColumns === "number" ? stdoutColumns : FALLBACK_COLUMNS);
7764
8200
  const [state, setState] = useState(() => initialPickerState(sessions));
7765
8201
  useInput((input, key) => {
7766
8202
  const action = keyToAction(toPickerKey(input, key), state.mode);
@@ -7824,23 +8260,42 @@ async function readStdin() {
7824
8260
  if (process.stdin.isTTY) {
7825
8261
  return void 0;
7826
8262
  }
7827
- return new Promise((resolve8) => {
8263
+ return new Promise((resolve10) => {
7828
8264
  let data = "";
7829
- process.stdin.setEncoding("utf-8");
8265
+ process.stdin.setEncoding(SESSION_FILE_ENCODING);
7830
8266
  process.stdin.on("data", (chunk) => {
7831
8267
  data += chunk;
7832
8268
  });
7833
8269
  process.stdin.on("end", () => {
7834
- resolve8(data.length === 0 ? void 0 : data);
8270
+ resolve10(data.length === 0 ? void 0 : data);
7835
8271
  });
7836
8272
  process.stdin.on("error", () => {
7837
- resolve8(void 0);
8273
+ resolve10(void 0);
7838
8274
  });
7839
8275
  });
7840
8276
  }
7841
- function handleError2(error) {
7842
- console.error("Error:", error instanceof Error ? `${error.name}: ${error.message}` : String(error));
7843
- process.exit(1);
8277
+ function writeOutput2(invocation, output) {
8278
+ invocation.io.writeStdout(`${output}
8279
+ `);
8280
+ }
8281
+ function writeError3(invocation, output) {
8282
+ invocation.io.writeStderr(`${output}
8283
+ `);
8284
+ }
8285
+ function writeInvocationWarning2(invocation, warning) {
8286
+ if (warning !== void 0) {
8287
+ writeError3(invocation, warning);
8288
+ }
8289
+ }
8290
+ function formatError2(error) {
8291
+ if (error instanceof Error) {
8292
+ return `${error.name}: ${error.message}`;
8293
+ }
8294
+ return toMessage(error);
8295
+ }
8296
+ function handleError2(invocation, error) {
8297
+ writeError3(invocation, `Error: ${formatError2(error)}`);
8298
+ return invocation.io.exit(1);
7844
8299
  }
7845
8300
  function colorFlagFromOption(colorOption) {
7846
8301
  if (colorOption === true) {
@@ -7860,10 +8315,26 @@ function resolveListColorDecision(colorOption) {
7860
8315
  });
7861
8316
  }
7862
8317
  function resolveListWidth() {
7863
- return Math.max(LIST_TEXT_MIN_WIDTH, process.stdout.columns ?? DEFAULT_LIST_WIDTH);
8318
+ const columns = Reflect.get(process.stdout, "columns");
8319
+ const resolvedColumns = typeof columns === "number" ? columns : DEFAULT_LIST_WIDTH;
8320
+ return Math.max(LIST_TEXT_MIN_WIDTH, resolvedColumns);
8321
+ }
8322
+ function addSessionOptions(command, options) {
8323
+ for (const option of options) {
8324
+ if (option.defaultValue === void 0) {
8325
+ command.option(sessionOptionToken(option), option.description);
8326
+ } else {
8327
+ command.option(sessionOptionToken(option), option.description, option.defaultValue);
8328
+ }
8329
+ }
8330
+ return command;
7864
8331
  }
7865
- function registerSessionCommands(sessionCmd) {
7866
- sessionCmd.command("list").description("List active sessions (doing + todo by default)").option("--status <status>", "Filter by status (todo|doing|archive); defaults to doing + todo").option("--json", "Output as JSON").option("--fields <fields>", "Comma-separated fields to emit as JSON (implies --json)").option("--color", "Force colored text output").option("--no-color", "Disable colored text output").option("--sessions-dir <path>", "Custom sessions directory").action(
8332
+ function registerSessionCommands(sessionCmd, invocation) {
8333
+ const effectiveInvocationDir = () => invocation.resolveEffectiveInvocationDir();
8334
+ addSessionOptions(
8335
+ sessionCmd.command(sessionCommandToken(sessionCliDefinition.subcommands.list)).description(sessionCliDefinition.subcommands.list.description),
8336
+ sessionOptionsForSubcommand(sessionCliDefinition.subcommands.list)
8337
+ ).action(
7867
8338
  async (options) => {
7868
8339
  try {
7869
8340
  const output = await listCommand({
@@ -7873,36 +8344,44 @@ function registerSessionCommands(sessionCmd) {
7873
8344
  color: resolveListColorDecision(options.color),
7874
8345
  width: resolveListWidth(),
7875
8346
  sessionsDir: options.sessionsDir,
7876
- onWarning: writeWarning
8347
+ cwd: effectiveInvocationDir(),
8348
+ onWarning: (warning) => writeInvocationWarning2(invocation, warning)
7877
8349
  });
7878
- console.log(output);
8350
+ writeOutput2(invocation, output);
7879
8351
  } catch (error) {
7880
- handleError2(error);
8352
+ handleError2(invocation, error);
7881
8353
  }
7882
8354
  }
7883
8355
  );
7884
- sessionCmd.command("pick").description("Interactively pick a session and launch claude or codex to resume it").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
8356
+ addSessionOptions(
8357
+ sessionCmd.command(sessionCommandToken(sessionCliDefinition.subcommands.pick)).description(sessionCliDefinition.subcommands.pick.description),
8358
+ sessionOptionsForSubcommand(sessionCliDefinition.subcommands.pick)
8359
+ ).action(async (options) => {
7885
8360
  try {
7886
8361
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
7887
- console.error(PICK_NON_TTY_MESSAGE);
7888
- process.exit(1);
8362
+ writeError3(invocation, PICK_NON_TTY_MESSAGE);
8363
+ invocation.io.exit(1);
7889
8364
  }
7890
8365
  const sessions = await loadPickCandidates({
7891
8366
  sessionsDir: options.sessionsDir,
7892
- onWarning: writeWarning
8367
+ cwd: effectiveInvocationDir(),
8368
+ onWarning: (warning) => writeInvocationWarning2(invocation, warning)
7893
8369
  });
7894
8370
  const choice = await runPicker(sessions);
7895
8371
  if (choice !== null) {
7896
- const reference = pickupReference(choice.session, options.sessionsDir, process.cwd());
8372
+ const reference = pickupReference(choice.session, options.sessionsDir, effectiveInvocationDir());
7897
8373
  const command = buildPickupCommand(choice.runtime, choice.autoContinue, reference);
7898
8374
  const code = await launchAgent(foregroundProcessRunner, lifecycleSignalSuspender, command);
7899
- process.exit(code);
8375
+ invocation.io.exit(code);
7900
8376
  }
7901
8377
  } catch (error) {
7902
- handleError2(error);
8378
+ handleError2(invocation, error);
7903
8379
  }
7904
8380
  });
7905
- 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("--color", "Force colored text output").option("--no-color", "Disable colored text output").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
8381
+ addSessionOptions(
8382
+ sessionCmd.command(sessionCommandToken(sessionCliDefinition.subcommands.todo)).description(sessionCliDefinition.subcommands.todo.description),
8383
+ sessionOptionsForSubcommand(sessionCliDefinition.subcommands.todo)
8384
+ ).action(async (options) => {
7906
8385
  try {
7907
8386
  const output = await listCommand({
7908
8387
  status: SESSION_STATUSES[0],
@@ -7911,129 +8390,159 @@ function registerSessionCommands(sessionCmd) {
7911
8390
  color: resolveListColorDecision(options.color),
7912
8391
  width: resolveListWidth(),
7913
8392
  sessionsDir: options.sessionsDir,
7914
- onWarning: writeWarning
8393
+ cwd: effectiveInvocationDir(),
8394
+ onWarning: (warning) => writeInvocationWarning2(invocation, warning)
7915
8395
  });
7916
- console.log(output);
8396
+ writeOutput2(invocation, output);
7917
8397
  } catch (error) {
7918
- handleError2(error);
8398
+ handleError2(invocation, error);
7919
8399
  }
7920
8400
  });
7921
- sessionCmd.command("show <id...>").description("Show session content").option("--json", "Output parsed session frontmatter as JSON (a record per id)").option("--sessions-dir <path>", "Custom sessions directory").action(async (ids, options) => {
8401
+ addSessionOptions(
8402
+ sessionCmd.command(sessionCommandToken(sessionCliDefinition.subcommands.show)).description(sessionCliDefinition.subcommands.show.description),
8403
+ sessionOptionsForSubcommand(sessionCliDefinition.subcommands.show)
8404
+ ).action(async (ids, options) => {
7922
8405
  try {
7923
8406
  const output = await showCommand2({
7924
8407
  sessionIds: ids,
7925
8408
  format: options.json ? SESSION_LIST_FORMAT.JSON : SESSION_LIST_FORMAT.TEXT,
7926
8409
  sessionsDir: options.sessionsDir,
7927
- onWarning: writeWarning
8410
+ cwd: effectiveInvocationDir(),
8411
+ onWarning: (warning) => writeInvocationWarning2(invocation, warning)
7928
8412
  });
7929
- console.log(output);
8413
+ writeOutput2(invocation, output);
7930
8414
  } catch (error) {
7931
- handleError2(error);
8415
+ handleError2(invocation, error);
7932
8416
  }
7933
8417
  });
7934
- sessionCmd.command("pickup [ids...]").description("Claim one or more sessions (move from todo to doing)").option("--auto", "Auto-select highest priority session").option("--sessions-dir <path>", "Custom sessions directory").addHelpText("after", PICKUP_SELECTION_HELP).action(async (ids, options) => {
8418
+ addSessionOptions(
8419
+ sessionCmd.command(sessionCommandToken(sessionCliDefinition.subcommands.pickup)).description(sessionCliDefinition.subcommands.pickup.description),
8420
+ sessionOptionsForSubcommand(sessionCliDefinition.subcommands.pickup)
8421
+ ).addHelpText("after", PICKUP_SELECTION_HELP).action(async (ids, options) => {
7935
8422
  try {
7936
8423
  if (ids.length === 0 && !options.auto) {
7937
- console.error("Error: Either session ID or --auto flag is required");
7938
- process.exit(1);
8424
+ writeError3(invocation, "Error: Either session ID or --auto flag is required");
8425
+ invocation.io.exit(1);
7939
8426
  }
7940
8427
  const output = await pickupCommand({
7941
8428
  sessionIds: ids,
7942
8429
  auto: options.auto,
8430
+ noInject: options.inject === false,
7943
8431
  sessionsDir: options.sessionsDir,
7944
- onWarning: writeWarning
8432
+ cwd: effectiveInvocationDir(),
8433
+ onWarning: (warning) => writeInvocationWarning2(invocation, warning)
7945
8434
  });
7946
- console.log(output);
8435
+ writeOutput2(invocation, output);
7947
8436
  } catch (error) {
7948
- handleError2(error);
8437
+ handleError2(invocation, error);
7949
8438
  }
7950
8439
  });
7951
- sessionCmd.command("release [ids...]").description("Release one or more sessions (move from doing to todo)").option("--sessions-dir <path>", "Custom sessions directory").action(async (ids, options) => {
8440
+ addSessionOptions(
8441
+ sessionCmd.command(sessionCommandToken(sessionCliDefinition.subcommands.release)).description(sessionCliDefinition.subcommands.release.description),
8442
+ sessionOptionsForSubcommand(sessionCliDefinition.subcommands.release)
8443
+ ).action(async (ids, options) => {
7952
8444
  try {
7953
8445
  const output = await releaseCommand({
7954
8446
  sessionIds: ids,
7955
8447
  sessionsDir: options.sessionsDir,
7956
- onWarning: writeWarning
8448
+ cwd: effectiveInvocationDir(),
8449
+ onWarning: (warning) => writeInvocationWarning2(invocation, warning)
7957
8450
  });
7958
- console.log(output);
8451
+ writeOutput2(invocation, output);
7959
8452
  } catch (error) {
7960
- handleError2(error);
8453
+ handleError2(invocation, error);
7961
8454
  }
7962
8455
  });
7963
- sessionCmd.command("handoff").description("Create a handoff session (reads JSON header + body from stdin)").option("--sessions-dir <path>", "Custom sessions directory").addHelpText("after", HANDOFF_FRONTMATTER_HELP).action(async (options) => {
8456
+ addSessionOptions(
8457
+ sessionCmd.command(sessionCommandToken(sessionCliDefinition.subcommands.handoff)).description(sessionCliDefinition.subcommands.handoff.description),
8458
+ sessionOptionsForSubcommand(sessionCliDefinition.subcommands.handoff)
8459
+ ).addHelpText("after", HANDOFF_FRONTMATTER_HELP).action(async (options) => {
7964
8460
  try {
7965
8461
  const content = await readStdin();
7966
8462
  const result = await handoffCommand({
7967
8463
  content,
7968
8464
  sessionsDir: options.sessionsDir,
8465
+ cwd: effectiveInvocationDir(),
7969
8466
  env: process.env
7970
8467
  });
7971
- console.log(result.output);
8468
+ writeOutput2(invocation, result.output);
7972
8469
  } catch (error) {
7973
8470
  if (error instanceof SessionHandoffBaseError) {
7974
8471
  if (error.checklist !== null) {
7975
- console.error(renderHandoffBaseChecklist(error.checklist));
8472
+ writeError3(invocation, renderHandoffBaseChecklist(error.checklist));
7976
8473
  } else if (!error.silent) {
7977
- console.error("Error:", `${error.name}: ${error.message}`);
8474
+ writeError3(invocation, `Error: ${error.name}: ${error.message}`);
7978
8475
  }
7979
- process.exit(1);
8476
+ invocation.io.exit(1);
7980
8477
  }
7981
- handleError2(error);
8478
+ handleError2(invocation, error);
7982
8479
  }
7983
8480
  });
7984
- sessionCmd.command("delete <id...>").description("Delete one or more sessions").option("--sessions-dir <path>", "Custom sessions directory").action(async (ids, options) => {
8481
+ addSessionOptions(
8482
+ sessionCmd.command(sessionCommandToken(sessionCliDefinition.subcommands.delete)).description(sessionCliDefinition.subcommands.delete.description),
8483
+ sessionOptionsForSubcommand(sessionCliDefinition.subcommands.delete)
8484
+ ).action(async (ids, options) => {
7985
8485
  try {
7986
8486
  const output = await deleteCommand({
7987
8487
  sessionIds: ids,
7988
8488
  sessionsDir: options.sessionsDir,
7989
- onWarning: writeWarning
8489
+ cwd: effectiveInvocationDir(),
8490
+ onWarning: (warning) => writeInvocationWarning2(invocation, warning)
7990
8491
  });
7991
- console.log(output);
8492
+ writeOutput2(invocation, output);
7992
8493
  } catch (error) {
7993
- handleError2(error);
8494
+ handleError2(invocation, error);
7994
8495
  }
7995
8496
  });
7996
- sessionCmd.command("prune").description("Remove old todo sessions, keeping the most recent N").option("--keep <count>", "Number of sessions to keep (default: 5)", "5").option("--dry-run", "Show what would be deleted without deleting").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
8497
+ addSessionOptions(
8498
+ sessionCmd.command(sessionCommandToken(sessionCliDefinition.subcommands.prune)).description(sessionCliDefinition.subcommands.prune.description),
8499
+ sessionOptionsForSubcommand(sessionCliDefinition.subcommands.prune)
8500
+ ).action(async (options) => {
7997
8501
  try {
7998
8502
  const keep = options.keep ? Number.parseInt(options.keep, 10) : void 0;
7999
8503
  const output = await pruneCommand({
8000
8504
  keep,
8001
8505
  dryRun: options.dryRun,
8002
8506
  sessionsDir: options.sessionsDir,
8003
- onWarning: writeWarning
8507
+ cwd: effectiveInvocationDir(),
8508
+ onWarning: (warning) => writeInvocationWarning2(invocation, warning)
8004
8509
  });
8005
- console.log(output);
8510
+ writeOutput2(invocation, output);
8006
8511
  } catch (error) {
8007
8512
  if (error instanceof PruneValidationError) {
8008
- console.error("Error:", error.message);
8009
- process.exit(1);
8513
+ writeError3(invocation, `Error: ${error.message}`);
8514
+ invocation.io.exit(1);
8010
8515
  }
8011
- handleError2(error);
8516
+ handleError2(invocation, error);
8012
8517
  }
8013
8518
  });
8014
- sessionCmd.command("archive <id...>").description("Move one or more sessions to the archive directory").option("--sessions-dir <path>", "Custom sessions directory").action(async (ids, options) => {
8519
+ addSessionOptions(
8520
+ sessionCmd.command(sessionCommandToken(sessionCliDefinition.subcommands.archive)).description(sessionCliDefinition.subcommands.archive.description),
8521
+ sessionOptionsForSubcommand(sessionCliDefinition.subcommands.archive)
8522
+ ).action(async (ids, options) => {
8015
8523
  try {
8016
8524
  const output = await archiveCommand({
8017
8525
  sessionIds: ids,
8018
8526
  sessionsDir: options.sessionsDir,
8019
- onWarning: writeWarning
8527
+ cwd: effectiveInvocationDir(),
8528
+ onWarning: (warning) => writeInvocationWarning2(invocation, warning)
8020
8529
  });
8021
- console.log(output);
8530
+ writeOutput2(invocation, output);
8022
8531
  } catch (error) {
8023
8532
  if (error instanceof SessionAlreadyArchivedError) {
8024
- console.error("Error:", error.message);
8025
- process.exit(1);
8533
+ writeError3(invocation, `Error: ${error.message}`);
8534
+ invocation.io.exit(1);
8026
8535
  }
8027
- handleError2(error);
8536
+ handleError2(invocation, error);
8028
8537
  }
8029
8538
  });
8030
8539
  }
8031
8540
  var sessionDomain = {
8032
- name: "session",
8033
- description: "Manage session workflow",
8034
- register: (program2) => {
8035
- const sessionCmd = program2.command("session").description("Manage session workflow").addHelpText("after", SESSION_FORMAT_HELP);
8036
- registerSessionCommands(sessionCmd);
8541
+ name: sessionCliDefinition.domain.commandName,
8542
+ description: sessionCliDefinition.domain.description,
8543
+ register: (program, invocation) => {
8544
+ const sessionCmd = program.command(sessionCliDefinition.domain.commandName).description(sessionCliDefinition.domain.description).addHelpText("after", SESSION_FORMAT_HELP);
8545
+ registerSessionCommands(sessionCmd, invocation);
8037
8546
  }
8038
8547
  };
8039
8548
 
@@ -8142,7 +8651,7 @@ function recognizeSpecTreeFilesystemEntry(record6, options = {}) {
8142
8651
  if (record6.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.DIRECTORY) {
8143
8652
  return recognizeDirectoryRecord(record6, name, registry, schemaVersions);
8144
8653
  }
8145
- if (record6.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE && record6.parentId !== void 0 && isEvidenceFile(record6.relativePath)) {
8654
+ if (record6.parentId !== void 0 && isEvidenceFile(record6.relativePath)) {
8146
8655
  return {
8147
8656
  type: SPEC_TREE_ENTRY_TYPE.EVIDENCE,
8148
8657
  id: record6.relativePath,
@@ -8151,22 +8660,19 @@ function recognizeSpecTreeFilesystemEntry(record6, options = {}) {
8151
8660
  ref: sourceRefForRelativePath(record6.relativePath)
8152
8661
  };
8153
8662
  }
8154
- if (record6.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE) {
8155
- const decisionMatch = matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.DECISION);
8156
- if (decisionMatch === null) return null;
8157
- const parsed = parseOrderedSlug(stripSuffix(name, decisionMatch.definition.suffix));
8158
- if (parsed === null) return null;
8159
- return {
8160
- type: SPEC_TREE_ENTRY_TYPE.DECISION,
8161
- kind: decisionMatch.kind,
8162
- id: record6.relativePath,
8163
- order: parsed.order,
8164
- slug: parsed.slug,
8165
- parentId: record6.parentId,
8166
- ref: sourceRefForRelativePath(record6.relativePath)
8167
- };
8168
- }
8169
- return null;
8663
+ const decisionMatch = matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.DECISION);
8664
+ if (decisionMatch === null) return null;
8665
+ const parsed = parseOrderedSlug(stripSuffix(name, decisionMatch.definition.suffix));
8666
+ if (parsed === null) return null;
8667
+ return {
8668
+ type: SPEC_TREE_ENTRY_TYPE.DECISION,
8669
+ kind: decisionMatch.kind,
8670
+ id: record6.relativePath,
8671
+ order: parsed.order,
8672
+ slug: parsed.slug,
8673
+ parentId: record6.parentId,
8674
+ ref: sourceRefForRelativePath(record6.relativePath)
8675
+ };
8170
8676
  }
8171
8677
  function recognizeDirectoryRecord(record6, name, registry, schemaVersions) {
8172
8678
  const canonical = canonicalNamingSchemaVersion(schemaVersions);
@@ -8195,6 +8701,7 @@ function recognizeDirectoryRecord(record6, name, registry, schemaVersions) {
8195
8701
  ref: sourceRefForRelativePath(record6.relativePath)
8196
8702
  };
8197
8703
  }
8704
+ if (matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.DECISION) !== null) return null;
8198
8705
  if (parseOrderedSlug(name) !== null) {
8199
8706
  return {
8200
8707
  type: SPEC_TREE_ENTRY_TYPE.INVALID,
@@ -8355,7 +8862,8 @@ function groupDecisions(entries) {
8355
8862
  if (entry.parentId === void 0) continue;
8356
8863
  const group = grouped.get(entry.parentId) ?? [];
8357
8864
  group.push(entry);
8358
- grouped.set(entry.parentId, group.sort(compareOrderedEntries));
8865
+ group.sort(compareOrderedEntries);
8866
+ grouped.set(entry.parentId, group);
8359
8867
  }
8360
8868
  return grouped;
8361
8869
  }
@@ -8425,7 +8933,7 @@ async function* walkFilesystemDirectory(context) {
8425
8933
  const relativePath = joinSpecTreePath(context.relativePath, entry.name);
8426
8934
  const refPath = joinSpecTreePath(SPEC_TREE_CONFIG.ROOT_DIRECTORY, relativePath);
8427
8935
  if (!await context.includePath(refPath)) continue;
8428
- const recordType = entry.isDirectory() ? SPEC_TREE_FILESYSTEM_RECORD_TYPE.DIRECTORY : entry.isFile() ? SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE : void 0;
8936
+ const recordType = filesystemRecordType(entry);
8429
8937
  if (recordType === void 0) continue;
8430
8938
  const sourceEntry = recognizeSpecTreeFilesystemEntry(
8431
8939
  { type: recordType, relativePath, parentId: context.parentId },
@@ -8439,11 +8947,19 @@ async function* walkFilesystemDirectory(context) {
8439
8947
  registry: context.registry,
8440
8948
  schemaVersions: context.schemaVersions,
8441
8949
  includePath: context.includePath,
8442
- parentId: sourceEntry?.type === SPEC_TREE_ENTRY_TYPE.NODE ? sourceEntry.id : context.parentId
8950
+ parentId: childParentId(context, sourceEntry)
8443
8951
  });
8444
8952
  }
8445
8953
  }
8446
8954
  }
8955
+ function filesystemRecordType(entry) {
8956
+ if (entry.isDirectory()) return SPEC_TREE_FILESYSTEM_RECORD_TYPE.DIRECTORY;
8957
+ if (entry.isFile()) return SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE;
8958
+ return void 0;
8959
+ }
8960
+ function childParentId(context, sourceEntry) {
8961
+ return sourceEntry?.type === SPEC_TREE_ENTRY_TYPE.NODE ? sourceEntry.id : context.parentId;
8962
+ }
8447
8963
  function matchKindSuffix(name, registry, category) {
8448
8964
  for (const [kind, definition] of Object.entries(registry)) {
8449
8965
  if (definition.category === category && name.endsWith(definition.suffix)) {
@@ -8470,8 +8986,8 @@ function isProductFile(relativePath) {
8470
8986
  function isEvidenceFile(relativePath) {
8471
8987
  const segments = relativePath.split(SPEC_TREE_PATH_SEPARATOR);
8472
8988
  if (segments.length < SPEC_TREE_MIN_EVIDENCE_PATH_SEGMENTS) return false;
8473
- const filename = segments[segments.length - 1] ?? "";
8474
- const directoryName = segments[segments.length - SPEC_TREE_PARENT_SEGMENT_OFFSET];
8989
+ const filename = segments.at(-1) ?? "";
8990
+ const directoryName = segments.at(-SPEC_TREE_PARENT_SEGMENT_OFFSET);
8475
8991
  const filenameSegments = filename.split(SPEC_TREE_EVIDENCE_FILE.SEGMENT_SEPARATOR);
8476
8992
  return directoryName === SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME && SPEC_TREE_EVIDENCE_FILE_TAILS.some(
8477
8993
  (tail) => SPEC_TREE_EVIDENCE_FILE.MODES.some(
@@ -8510,7 +9026,7 @@ function stripSuffix(value, suffix) {
8510
9026
  }
8511
9027
  function readLastPathSegment(relativePath) {
8512
9028
  const segments = relativePath.split(SPEC_TREE_PATH_SEPARATOR);
8513
- return segments[segments.length - 1] ?? relativePath;
9029
+ return segments.at(-1) ?? relativePath;
8514
9030
  }
8515
9031
  function joinSpecTreePath(...segments) {
8516
9032
  return segments.filter((segment) => segment.length > 0).join(SPEC_TREE_PATH_SEPARATOR);
@@ -8550,7 +9066,7 @@ async function nextCommand(options = {}) {
8550
9066
  return formatNextSpecTreeNode(snapshot2);
8551
9067
  }
8552
9068
  const productDir = await resolveSpecProductDir(
8553
- options.cwd ?? process.cwd(),
9069
+ options.cwd ?? CONFIG_PROCESS_CWD.read(),
8554
9070
  options.gitDependencies,
8555
9071
  options.onWarning
8556
9072
  );
@@ -9436,7 +9952,7 @@ function createNodeStatusProvider(productDir) {
9436
9952
 
9437
9953
  // src/lib/node-status/update.ts
9438
9954
  import { mkdir as mkdir4, readdir as readdir7, rm, writeFile as writeFile2 } from "fs/promises";
9439
- import { dirname as dirname6, join as join21 } from "path";
9955
+ import { dirname as dirname7, join as join21 } from "path";
9440
9956
  var NODE_STATUS_TEXT_ENCODING = "utf8";
9441
9957
  async function updateNodeStatus(options) {
9442
9958
  const { productDir, resolveOutcome } = options;
@@ -9498,7 +10014,7 @@ function evidencePath(entry) {
9498
10014
  return entry.ref?.path ?? entry.id;
9499
10015
  }
9500
10016
  async function writeNodeStatus(filePath, verification) {
9501
- await mkdir4(dirname6(filePath), { recursive: true });
10017
+ await mkdir4(dirname7(filePath), { recursive: true });
9502
10018
  await writeFile2(filePath, serializeNodeStatus(createNodeStatusFile(verification)), NODE_STATUS_TEXT_ENCODING);
9503
10019
  }
9504
10020
  function nodeStatusPath(productDir, nodeId) {
@@ -9634,12 +10150,21 @@ var SPEC_STATUS_TABLE_HEADER = formatTableRow([
9634
10150
  TABLE_HEADER.PATH,
9635
10151
  TABLE_HEADER.STATE
9636
10152
  ]);
10153
+ var SpecStatusUpdateRequiresProductDirError = class extends Error {
10154
+ constructor() {
10155
+ super("Cannot update spec status for an injected in-memory source");
10156
+ this.name = "SpecStatusUpdateRequiresProductDirError";
10157
+ }
10158
+ };
9637
10159
  async function statusCommand(options = {}) {
9638
10160
  if (options.source !== void 0) {
10161
+ if (options.update === true) {
10162
+ throw new SpecStatusUpdateRequiresProductDirError();
10163
+ }
9639
10164
  return renderSpecStatus(projectSpecTree(await readSpecTree({ source: options.source })), options.format);
9640
10165
  }
9641
10166
  const productDir = await resolveSpecProductDir(
9642
- options.cwd ?? process.cwd(),
10167
+ options.cwd ?? CONFIG_PROCESS_CWD.read(),
9643
10168
  options.gitDependencies,
9644
10169
  options.onWarning
9645
10170
  );
@@ -9706,7 +10231,8 @@ function flattenProjectionNodes(nodes) {
9706
10231
  return nodes.flatMap((node) => [node, ...flattenProjectionNodes(node.children)]);
9707
10232
  }
9708
10233
  function formatTableRow(values) {
9709
- return `${TABLE_SEPARATOR} ${values.join(` ${TABLE_SEPARATOR} `)} ${TABLE_SEPARATOR}`;
10234
+ const separator = ` ${TABLE_SEPARATOR} `;
10235
+ return `${TABLE_SEPARATOR} ${values.join(separator)} ${TABLE_SEPARATOR}`;
9710
10236
  }
9711
10237
  function formatNodeLabel(node) {
9712
10238
  return [
@@ -9717,7 +10243,7 @@ function formatNodeLabel(node) {
9717
10243
  }
9718
10244
 
9719
10245
  // src/test/languages/python.ts
9720
- import { basename as basename3, dirname as dirname7, join as join22 } from "path/posix";
10246
+ import { basename as basename3, dirname as dirname8, join as join22 } from "path/posix";
9721
10247
 
9722
10248
  // src/validation/discovery/language-finder.ts
9723
10249
  import fs6 from "fs";
@@ -9788,10 +10314,10 @@ function coveredProductInputPaths(coveredTestPaths2) {
9788
10314
  const paths = /* @__PURE__ */ new Set();
9789
10315
  for (const testPath of coveredTestPaths2) {
9790
10316
  if (!matchesTestFile(testPath)) continue;
9791
- let directory = dirname7(testPath);
10317
+ let directory = dirname8(testPath);
9792
10318
  while (directory !== "." && directory.length > 0) {
9793
10319
  paths.add(join22(directory, PYTHON_PRODUCT_INPUT_PATH.CONFTEST));
9794
- const parent = dirname7(directory);
10320
+ const parent = dirname8(directory);
9795
10321
  if (parent === directory) break;
9796
10322
  directory = parent;
9797
10323
  }
@@ -10015,7 +10541,17 @@ var VALID_STATUS_FORMATS = [
10015
10541
  OUTPUT_FORMAT.TABLE
10016
10542
  ];
10017
10543
  var UNPRINTABLE_ERROR_MESSAGE = "unprintable error";
10018
- function handleCommandError(error) {
10544
+ function writeOutput3(io, output) {
10545
+ io.writeStdout(`${output}
10546
+ `);
10547
+ }
10548
+ function writeInvocationWarning3(io, warning) {
10549
+ if (warning !== void 0) {
10550
+ io.writeStderr(`${warning}
10551
+ `);
10552
+ }
10553
+ }
10554
+ function handleCommandError(io, error) {
10019
10555
  let message;
10020
10556
  if (error instanceof Error) {
10021
10557
  message = error.message;
@@ -10028,8 +10564,9 @@ function handleCommandError(error) {
10028
10564
  message = UNPRINTABLE_ERROR_MESSAGE;
10029
10565
  }
10030
10566
  }
10031
- console.error("Error:", message);
10032
- process.exit(1);
10567
+ io.writeStderr(`Error: ${message}
10568
+ `);
10569
+ return io.exit(1);
10033
10570
  }
10034
10571
  function resolveStatusFormat(options) {
10035
10572
  if (options.json === true) {
@@ -10045,41 +10582,43 @@ function resolveStatusFormat(options) {
10045
10582
  `${SPEC_STATUS_FORMAT_MESSAGE.INVALID_PREFIX} "${options.format}". Must be one of: ${VALID_STATUS_FORMATS.join(", ")}`
10046
10583
  );
10047
10584
  }
10048
- function registerSpecCommands(specCmd) {
10585
+ function registerSpecCommands(specCmd, invocation) {
10586
+ const productDir = () => invocation.resolveProductContext().productDir;
10587
+ const onWarning = (warning) => writeInvocationWarning3(invocation.io, warning);
10049
10588
  specCmd.command(SPEC_DOMAIN_CLI.STATUS_COMMAND).description("Get product status").option(SPEC_DOMAIN_CLI.JSON_OPTION, "Output as JSON").option(SPEC_DOMAIN_CLI.FORMAT_OPTION_DEFINITION, "Output format (text|json|markdown|table)").option(SPEC_DOMAIN_CLI.UPDATE_OPTION, "Refresh each node's spx.status.json before reporting").action(async (options) => {
10050
10589
  try {
10051
10590
  const format2 = resolveStatusFormat(options);
10052
10591
  const output = options.update === true ? await statusCommand({
10053
- cwd: process.cwd(),
10592
+ cwd: productDir(),
10054
10593
  format: format2,
10055
- onWarning: writeWarning,
10594
+ onWarning,
10056
10595
  update: true,
10057
- resolveOutcomeFor: (productDir) => createNodeOutcomeResolver({
10058
- productDir,
10596
+ resolveOutcomeFor: (productDir2) => createNodeOutcomeResolver({
10597
+ productDir: productDir2,
10059
10598
  registry: testingRegistry,
10060
- runnerDepsFor: createRunnerDepsFor(productDir, process.stderr)
10599
+ runnerDepsFor: createRunnerDepsFor(productDir2, process.stderr)
10061
10600
  })
10062
- }) : await statusCommand({ cwd: process.cwd(), format: format2, onWarning: writeWarning });
10063
- console.log(output);
10601
+ }) : await statusCommand({ cwd: productDir(), format: format2, onWarning });
10602
+ writeOutput3(invocation.io, output);
10064
10603
  } catch (error) {
10065
- handleCommandError(error);
10604
+ handleCommandError(invocation.io, error);
10066
10605
  }
10067
10606
  });
10068
10607
  specCmd.command(SPEC_DOMAIN_CLI.NEXT_COMMAND).description("Find next spec-tree node to work on").action(async () => {
10069
10608
  try {
10070
- const output = await nextCommand({ cwd: process.cwd(), onWarning: writeWarning });
10071
- console.log(output);
10609
+ const output = await nextCommand({ cwd: productDir(), onWarning });
10610
+ writeOutput3(invocation.io, output);
10072
10611
  } catch (error) {
10073
- handleCommandError(error);
10612
+ handleCommandError(invocation.io, error);
10074
10613
  }
10075
10614
  });
10076
10615
  }
10077
10616
  var specDomain = {
10078
10617
  name: "spec",
10079
10618
  description: "Manage spec workflow",
10080
- register: (program2) => {
10081
- const specCmd = program2.command(SPEC_DOMAIN_CLI.COMMAND).description("Manage spec workflow");
10082
- registerSpecCommands(specCmd);
10619
+ register: (program, invocation) => {
10620
+ const specCmd = program.command(SPEC_DOMAIN_CLI.COMMAND).description("Manage spec workflow");
10621
+ registerSpecCommands(specCmd, invocation);
10083
10622
  }
10084
10623
  };
10085
10624
 
@@ -10193,33 +10732,33 @@ var UNRESOLVED_TARGETS_WARNING = "No tests matched these operands";
10193
10732
  var TESTING_PRODUCT_DIR_WARNING = {
10194
10733
  NOT_GIT_REPOSITORY: `Warning: Not in a git repository. Reading ${SPEC_TREE_CONFIG.ROOT_DIRECTORY} tests relative to the current working directory.`
10195
10734
  };
10196
- async function resolveTestProductDir() {
10197
- const { productDir, isGitRepo } = await detectWorktreeProductRoot(process.cwd());
10735
+ async function resolveTestProductDir(cwd, writeWarning) {
10736
+ const { productDir, isGitRepo } = await detectWorktreeProductRoot(cwd);
10198
10737
  writeWarning(isGitRepo ? void 0 : TESTING_PRODUCT_DIR_WARNING.NOT_GIT_REPOSITORY);
10199
10738
  return productDir;
10200
10739
  }
10201
- async function runTestsThroughCommand(productDir, passing, targets) {
10740
+ async function runTestsThroughCommand(productDir, passing, io, targets) {
10202
10741
  try {
10203
10742
  return await runTestsCommand(
10204
10743
  { productDir, passing, targets },
10205
10744
  { registry: testingRegistry, runnerDepsFor: createRunnerDepsFor(productDir) }
10206
10745
  );
10207
10746
  } catch (error) {
10208
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}
10747
+ io.writeStderr(`${error instanceof Error ? error.message : String(error)}
10209
10748
  `);
10210
- process.exit(PROCESS_FAILURE_EXIT_CODE);
10749
+ io.exit(PROCESS_FAILURE_EXIT_CODE);
10211
10750
  }
10212
10751
  }
10213
- async function runAgentTestsThroughCommand(productDir, passing, targets) {
10752
+ async function runAgentTestsThroughCommand(productDir, passing, io, targets) {
10214
10753
  try {
10215
10754
  return await runTestsCommand(
10216
10755
  { productDir, passing, targets },
10217
10756
  { registry: testingRegistry, runnerDepsFor: createAgentRunnerDepsFor(productDir) }
10218
10757
  );
10219
10758
  } catch (error) {
10220
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}
10759
+ io.writeStderr(`${error instanceof Error ? error.message : String(error)}
10221
10760
  `);
10222
- process.exit(PROCESS_FAILURE_EXIT_CODE);
10761
+ io.exit(PROCESS_FAILURE_EXIT_CODE);
10223
10762
  }
10224
10763
  }
10225
10764
  function unreportedGroups2(result) {
@@ -10258,19 +10797,23 @@ function targetSelection(targets, options, command) {
10258
10797
  const parentOptions = command.parent?.opts() ?? {};
10259
10798
  return { operands: targets, recursive: options.recursive === true || parentOptions.recursive === true };
10260
10799
  }
10261
- var defaultTestingCliDependencies = {
10262
- resolveProductDir: resolveTestProductDir,
10263
- runTests: runTestsThroughCommand,
10264
- runAgentTests: runAgentTestsThroughCommand,
10265
- writeStdout: (output) => {
10266
- process.stdout.write(output);
10267
- },
10268
- writeWarning,
10269
- setExitCode: (exitCode) => {
10270
- process.exitCode = exitCode;
10271
- },
10272
- exit: (exitCode) => process.exit(exitCode)
10273
- };
10800
+ function defaultTestingCliDependencies(invocation) {
10801
+ const writeWarning = (warning) => {
10802
+ if (warning !== void 0) {
10803
+ invocation.io.writeStderr(`${warning}
10804
+ `);
10805
+ }
10806
+ };
10807
+ return {
10808
+ resolveProductDir: () => resolveTestProductDir(invocation.resolveEffectiveInvocationDir(), writeWarning),
10809
+ runTests: (productDir, passing, targets) => runTestsThroughCommand(productDir, passing, invocation.io, targets),
10810
+ runAgentTests: (productDir, passing, targets) => runAgentTestsThroughCommand(productDir, passing, invocation.io, targets),
10811
+ writeStdout: (output) => invocation.io.writeStdout(output),
10812
+ writeWarning,
10813
+ setExitCode: (exitCode) => invocation.io.setExitCode(exitCode),
10814
+ exit: (exitCode) => invocation.io.exit(exitCode)
10815
+ };
10816
+ }
10274
10817
  async function runTestingAction(deps, passing, options, targets) {
10275
10818
  const productDir = await deps.resolveProductDir();
10276
10819
  if (options.agent === true) {
@@ -10282,12 +10825,13 @@ async function runTestingAction(deps, passing, options, targets) {
10282
10825
  const result = await deps.runTests(productDir, passing, targets);
10283
10826
  reportAndExit(result.dispatch, deps);
10284
10827
  }
10285
- function createTestingDomain(deps = defaultTestingCliDependencies) {
10828
+ function createTestingDomain(deps) {
10286
10829
  return {
10287
10830
  name: TESTING_CLI.commandName,
10288
10831
  description: TESTING_CLI.description,
10289
- register: (program2) => {
10290
- const testCmd = program2.command(TESTING_CLI.commandName).description(TESTING_CLI.description);
10832
+ register: (program, invocation) => {
10833
+ const actionDeps = deps ?? defaultTestingCliDependencies(invocation);
10834
+ const testCmd = program.command(TESTING_CLI.commandName).description(TESTING_CLI.description);
10291
10835
  testCmd.option(TESTING_CLI.agentOption, TESTING_CLI.agentDescription);
10292
10836
  testCmd.option(
10293
10837
  `${TESTING_CLI.recursiveShortFlag}, ${TESTING_CLI.recursiveLongFlag}`,
@@ -10296,7 +10840,7 @@ function createTestingDomain(deps = defaultTestingCliDependencies) {
10296
10840
  testCmd.argument(TESTING_CLI.targetsArgument, TESTING_CLI.targetsDescription);
10297
10841
  testCmd.action(async (targets, options, command) => {
10298
10842
  await runTestingAction(
10299
- deps,
10843
+ actionDeps,
10300
10844
  false,
10301
10845
  { agent: requestsAgentMode(options, command) },
10302
10846
  targetSelection(targets, options, command)
@@ -10304,7 +10848,7 @@ function createTestingDomain(deps = defaultTestingCliDependencies) {
10304
10848
  });
10305
10849
  testCmd.command(TESTING_CLI.passingSubcommand).description(TESTING_CLI.passingDescription).option(TESTING_CLI.agentOption, TESTING_CLI.agentDescription).option(`${TESTING_CLI.recursiveShortFlag}, ${TESTING_CLI.recursiveLongFlag}`, TESTING_CLI.recursiveDescription).argument(TESTING_CLI.targetsArgument, TESTING_CLI.targetsDescription).action(async (targets, options, command) => {
10306
10850
  await runTestingAction(
10307
- deps,
10851
+ actionDeps,
10308
10852
  true,
10309
10853
  { agent: requestsAgentMode(options, command) },
10310
10854
  targetSelection(targets, options, command)
@@ -10454,7 +10998,7 @@ function buildDprintCheckArgs(options) {
10454
10998
  }
10455
10999
  async function validateFormatting(context, runner = defaultFormattingProcessRunner) {
10456
11000
  const args = buildDprintCheckArgs({ files: context.files });
10457
- return new Promise((resolve8) => {
11001
+ return new Promise((resolve10) => {
10458
11002
  const child = spawnManagedSubprocess(runner, DPRINT_COMMAND, args, { cwd: context.projectRoot });
10459
11003
  const chunks = [];
10460
11004
  const capture = (chunk) => {
@@ -10463,10 +11007,10 @@ async function validateFormatting(context, runner = defaultFormattingProcessRunn
10463
11007
  child.stdout?.on(VALIDATION_SUBPROCESS_EVENTS.DATA, capture);
10464
11008
  child.stderr?.on(VALIDATION_SUBPROCESS_EVENTS.DATA, capture);
10465
11009
  child.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
10466
- resolve8({ success: code === 0, output: chunks.join("") });
11010
+ resolve10({ success: code === 0, output: chunks.join("") });
10467
11011
  });
10468
11012
  child.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
10469
- resolve8({ success: false, output: chunks.join(""), error: error.message });
11013
+ resolve10({ success: false, output: chunks.join(""), error: error.message });
10470
11014
  });
10471
11015
  });
10472
11016
  }
@@ -10587,7 +11131,7 @@ import { relative as relative4 } from "path";
10587
11131
 
10588
11132
  // src/validation/steps/markdown.ts
10589
11133
  import { existsSync as existsSync2, statSync } from "fs";
10590
- import { basename as basename4, dirname as dirname8, join as join25, relative as pathRelative } from "path";
11134
+ import { basename as basename4, dirname as dirname9, join as join25, relative as pathRelative } from "path";
10591
11135
  import { main as markdownlintMain } from "markdownlint-cli2";
10592
11136
  import relativeLinksRule from "markdownlint-rule-relative-links";
10593
11137
  var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
@@ -10717,7 +11261,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
10717
11261
  return errors;
10718
11262
  }
10719
11263
  function targetDirectory(target) {
10720
- return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname8(target.path) : target.path;
11264
+ return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname9(target.path) : target.path;
10721
11265
  }
10722
11266
  function hasMarkdownExtension(path7) {
10723
11267
  const lastDot = path7.lastIndexOf(".");
@@ -10772,10 +11316,10 @@ async function markdownCommand(options) {
10772
11316
  VALIDATION_PATH_TOOL_SUBSECTIONS.MARKDOWN
10773
11317
  );
10774
11318
  const targetResolutions = files && files.length > 0 ? files.map((filePath) => resolveMarkdownValidationTarget(filePath)) : void 0;
10775
- const unfilteredTargets = targetResolutions !== void 0 ? targetResolutions.map((resolution) => resolution.target).filter((target) => target !== void 0) : getDefaultDirectories(cwd).map((path7) => ({
11319
+ const unfilteredTargets = targetResolutions === void 0 ? getDefaultDirectories(cwd).map((path7) => ({
10776
11320
  kind: MARKDOWN_VALIDATION_TARGET_KIND.DIRECTORY,
10777
11321
  path: path7
10778
- }));
11322
+ })) : targetResolutions.map((resolution) => resolution.target).filter((target) => target !== void 0);
10779
11323
  const targets = unfilteredTargets.filter(
10780
11324
  (target) => pathPassesValidationFilter(relative4(cwd, target.path), pathFilter)
10781
11325
  );
@@ -10794,24 +11338,26 @@ async function markdownCommand(options) {
10794
11338
  projectRoot: cwd
10795
11339
  });
10796
11340
  const durationMs = Date.now() - startTime;
10797
- if (result.success) {
10798
- const output = quiet ? "" : [...skippedOutput, MARKDOWN_COMMAND_OUTPUT.NO_ISSUES].join("\n");
10799
- return { exitCode: 0, output, durationMs };
10800
- } else {
10801
- const errorLines = result.errors.map(
10802
- (error) => ` ${error.file}:${error.line} ${error.detail}`
10803
- );
10804
- const output = [
10805
- ...skippedOutput,
10806
- `Markdown: ${result.errors.length} ${MARKDOWN_COMMAND_OUTPUT.ERROR_SUMMARY_SUFFIX}`,
10807
- ...errorLines
10808
- ].join("\n");
10809
- return { exitCode: 1, output, durationMs };
10810
- }
11341
+ return formatMarkdownResult(result, skippedOutput, quiet, durationMs);
10811
11342
  }
10812
11343
  function formatSkippedFileScope(target) {
10813
11344
  return `${MARKDOWN_COMMAND_OUTPUT.SKIPPED_FILE_SCOPE_PREFIX}: ${target.path} (${target.reason})`;
10814
11345
  }
11346
+ function formatMarkdownResult(result, skippedOutput, quiet, durationMs) {
11347
+ if (result.success) {
11348
+ const output2 = quiet ? "" : [...skippedOutput, MARKDOWN_COMMAND_OUTPUT.NO_ISSUES].join("\n");
11349
+ return { exitCode: 0, output: output2, durationMs };
11350
+ }
11351
+ const errorLines = result.errors.map(
11352
+ (error) => ` ${error.file}:${error.line} ${error.detail}`
11353
+ );
11354
+ const output = [
11355
+ ...skippedOutput,
11356
+ `Markdown: ${result.errors.length} ${MARKDOWN_COMMAND_OUTPUT.ERROR_SUMMARY_SUFFIX}`,
11357
+ ...errorLines
11358
+ ].join("\n");
11359
+ return { exitCode: 1, output, durationMs };
11360
+ }
10815
11361
 
10816
11362
  // src/validation/languages/markdown.ts
10817
11363
  var MARKDOWN_LANGUAGE_NAME = "markdown";
@@ -11687,7 +12233,7 @@ var ParseErrorCode;
11687
12233
 
11688
12234
  // src/validation/config/scope.ts
11689
12235
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
11690
- import { isAbsolute as isAbsolute4, join as join26, relative as relative5, resolve as resolve6 } from "path";
12236
+ import { isAbsolute as isAbsolute4, join as join26, relative as relative5, resolve as resolve8 } from "path";
11691
12237
  var TSCONFIG_FILES = {
11692
12238
  full: "tsconfig.json",
11693
12239
  production: "tsconfig.production.json"
@@ -11913,7 +12459,7 @@ function globPatternCanMatchInsideDirectory(patternSegments, directorySegments,
11913
12459
  );
11914
12460
  } else {
11915
12461
  const directorySegment = directorySegments[directoryIndex];
11916
- result = directorySegment !== void 0 && globSegmentMatchesPathSegment(patternSegment, directorySegment) && globPatternCanMatchInsideDirectory(
12462
+ result = globSegmentMatchesPathSegment(patternSegment, directorySegment) && globPatternCanMatchInsideDirectory(
11917
12463
  patternSegments,
11918
12464
  directorySegments,
11919
12465
  patternIndex + 1,
@@ -12061,13 +12607,13 @@ function pathPassesTypeScriptScope(path7, scopeConfig) {
12061
12607
  return included && !excluded;
12062
12608
  }
12063
12609
  function pathStaysInsideTypeScriptScopeRoot(projectRoot, path7) {
12064
- const resolvedPath = isAbsolute4(path7) ? resolve6(path7) : resolve6(projectRoot, path7);
12610
+ const resolvedPath = isAbsolute4(path7) ? resolve8(path7) : resolve8(projectRoot, path7);
12065
12611
  const relativePath = relative5(projectRoot, resolvedPath);
12066
12612
  const segments = normalizeTypeScriptScopePath(relativePath).split(PATH_SEGMENT_SEPARATOR3);
12067
12613
  return relativePath.length === 0 || !segments.includes("..") && !isAbsolute4(relativePath);
12068
12614
  }
12069
12615
  function toProjectRelativeTypeScriptScopePath(projectRoot, path7) {
12070
- const resolvedPath = isAbsolute4(path7) ? resolve6(path7) : resolve6(projectRoot, path7);
12616
+ const resolvedPath = isAbsolute4(path7) ? resolve8(path7) : resolve8(projectRoot, path7);
12071
12617
  const relativePath = relative5(projectRoot, resolvedPath);
12072
12618
  return relativePath.length === 0 ? TYPESCRIPT_SCOPE_PROJECT_ROOT : normalizeTypeScriptScopePath(relativePath);
12073
12619
  }
@@ -12303,7 +12849,7 @@ function resolvedModulePath(resolvedPath) {
12303
12849
  }
12304
12850
  function nearestPackageRoot(filePath, existsSync8) {
12305
12851
  let currentDirectory = path6.dirname(filePath);
12306
- while (true) {
12852
+ for (; ; ) {
12307
12853
  if (existsSync8(path6.join(currentDirectory, PACKAGE_MANIFEST_FILENAME))) {
12308
12854
  return currentDirectory;
12309
12855
  }
@@ -12319,7 +12865,7 @@ function bundledToolPath(resolvedPath, existsSync8) {
12319
12865
  return nearestPackageRoot(bundledFilePath, existsSync8) ?? path6.dirname(bundledFilePath);
12320
12866
  }
12321
12867
  async function discoverTool(tool, options = {}) {
12322
- const { projectRoot = process.cwd(), deps = defaultToolDiscoveryDeps } = options;
12868
+ const { projectRoot = CONFIG_PROCESS_CWD.read(), deps = defaultToolDiscoveryDeps } = options;
12323
12869
  const bundledPath = deps.resolveModule(`${tool}/package.json`) ?? deps.resolveImport?.(tool);
12324
12870
  if (bundledPath) {
12325
12871
  return {
@@ -12488,7 +13034,7 @@ function dependencyCruiserGlobSegmentToRegExpSource(segment) {
12488
13034
  } else if (character === SINGLE_CHARACTER_GLOB_MARKER) {
12489
13035
  source += `[^${DEPENDENCY_CRUISER_PATH_SEGMENT_SEPARATOR}]`;
12490
13036
  } else {
12491
- source += character?.replaceAll(LITERAL_REGEX_SPECIAL_CHARACTER_PATTERN, REGEX_ESCAPE_REPLACEMENT2) ?? "";
13037
+ source += character.replaceAll(LITERAL_REGEX_SPECIAL_CHARACTER_PATTERN, REGEX_ESCAPE_REPLACEMENT2);
12492
13038
  }
12493
13039
  }
12494
13040
  return source;
@@ -12534,10 +13080,10 @@ function patternIsCoveredByDirectory(pattern, directories) {
12534
13080
  });
12535
13081
  }
12536
13082
  function isCruiseResult(output) {
12537
- return typeof output === "object" && output !== null && "modules" in output && "summary" in output;
13083
+ return output !== null && typeof output === "object" && "modules" in output && "summary" in output;
12538
13084
  }
12539
13085
  function closeCycle(cycle) {
12540
- const first = cycle[0];
13086
+ const first = cycle.at(0);
12541
13087
  const last = cycle.at(-1);
12542
13088
  if (first === void 0) {
12543
13089
  return [];
@@ -12798,13 +13344,13 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
12798
13344
  knipProcess.stderr?.on("data", (data) => {
12799
13345
  knipError += data.toString();
12800
13346
  });
12801
- return new Promise((resolve8) => {
13347
+ return new Promise((resolve10) => {
12802
13348
  const resolveAfterCleanup = (result) => {
12803
13349
  if (resultResolved) {
12804
13350
  return;
12805
13351
  }
12806
13352
  resultResolved = true;
12807
- void cleanupOnce().finally(() => resolve8(result));
13353
+ void cleanupOnce().finally(() => resolve10(result));
12808
13354
  };
12809
13355
  knipProcess.on("close", (code) => {
12810
13356
  if (code === 0) {
@@ -12905,7 +13451,7 @@ import { existsSync as existsSync6 } from "fs";
12905
13451
  import { join as join30 } from "path";
12906
13452
 
12907
13453
  // src/validation/lint-policy.ts
12908
- import { execFileSync } from "child_process";
13454
+ import { execFileSync as execFileSync2 } from "child_process";
12909
13455
  import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
12910
13456
  import { join as join29 } from "path";
12911
13457
 
@@ -13020,7 +13566,7 @@ function readBaselineManifest(productDir, file, key) {
13020
13566
  }
13021
13567
  let content;
13022
13568
  try {
13023
- content = execFileSync("git", ["show", `${baselineRef}:${file}`], {
13569
+ content = execFileSync2("git", ["show", `${baselineRef}:${file}`], {
13024
13570
  cwd: productDir,
13025
13571
  encoding: "utf-8",
13026
13572
  env: withoutGitEnvironment(process.env),
@@ -13060,7 +13606,7 @@ function readMergeBase(productDir, baseBranchRef) {
13060
13606
  }
13061
13607
  function readGitRef(productDir, args) {
13062
13608
  try {
13063
- const output = execFileSync("git", [...args], {
13609
+ const output = execFileSync2("git", [...args], {
13064
13610
  cwd: productDir,
13065
13611
  encoding: "utf-8",
13066
13612
  env: withoutGitEnvironment(process.env),
@@ -13222,7 +13768,7 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
13222
13768
  scope: scope2,
13223
13769
  scopeConfig: context.scopeConfig
13224
13770
  });
13225
- return new Promise((resolve8) => {
13771
+ return new Promise((resolve10) => {
13226
13772
  const localBin = join30(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
13227
13773
  const binary = existsSync6(localBin) ? localBin : "npx";
13228
13774
  const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
@@ -13232,13 +13778,13 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
13232
13778
  forwardValidationSubprocessOutput(eslintProcess, outputStreams);
13233
13779
  eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
13234
13780
  if (code === 0) {
13235
- resolve8({ success: true });
13781
+ resolve10({ success: true });
13236
13782
  } else {
13237
- resolve8({ success: false, error: `ESLint exited with code ${code}` });
13783
+ resolve10({ success: false, error: `ESLint exited with code ${code}` });
13238
13784
  }
13239
13785
  });
13240
13786
  eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
13241
- resolve8({ success: false, error: error.message });
13787
+ resolve10({ success: false, error: error.message });
13242
13788
  });
13243
13789
  });
13244
13790
  }
@@ -13311,21 +13857,24 @@ async function lintCommand(options) {
13311
13857
  };
13312
13858
  const result = await validateESLint(context, void 0, outputStreams);
13313
13859
  const durationMs = Date.now() - startTime;
13860
+ return formatLintResult(result, quiet, durationMs);
13861
+ }
13862
+ function formatLintResult(result, quiet, durationMs) {
13314
13863
  if (result.skipped) {
13315
- const output = quiet ? "" : VALIDATION_PATHS_NO_TARGETS_MESSAGE;
13316
- return { exitCode: 0, output, durationMs };
13317
- } else if (result.success) {
13318
- const output = quiet ? "" : VALIDATION_COMMAND_OUTPUT.ESLINT_SUCCESS;
13319
- return { exitCode: 0, output, durationMs };
13320
- } else {
13321
- const output = result.error ?? VALIDATION_COMMAND_OUTPUT.ESLINT_FAILURE;
13322
- return { exitCode: 1, output, durationMs };
13864
+ const output2 = quiet ? "" : VALIDATION_PATHS_NO_TARGETS_MESSAGE;
13865
+ return { exitCode: 0, output: output2, durationMs };
13323
13866
  }
13867
+ if (result.success) {
13868
+ const output2 = quiet ? "" : VALIDATION_COMMAND_OUTPUT.ESLINT_SUCCESS;
13869
+ return { exitCode: 0, output: output2, durationMs };
13870
+ }
13871
+ const output = result.error ?? VALIDATION_COMMAND_OUTPUT.ESLINT_FAILURE;
13872
+ return { exitCode: 1, output, durationMs };
13324
13873
  }
13325
13874
 
13326
13875
  // src/validation/literal/index.ts
13327
13876
  import { readFile as readFile9 } from "fs/promises";
13328
- import { isAbsolute as isAbsolute6, relative as relative7, resolve as resolve7 } from "path";
13877
+ import { isAbsolute as isAbsolute6, relative as relative7, resolve as resolve9 } from "path";
13329
13878
 
13330
13879
  // src/lib/file-inclusion/predicates/ignore-source.ts
13331
13880
  var IGNORE_SOURCE_LAYER = "ignore-source";
@@ -13409,22 +13958,33 @@ async function runPipeline(sequence, projectRoot, request, config, ignoreReader)
13409
13958
  await collectPaths(request.walkRoot, projectRoot, allPaths, artifactDirs);
13410
13959
  for (const path7 of allPaths) {
13411
13960
  if (explicitPathSet.has(path7)) continue;
13412
- const trail = [];
13413
- for (const { entry, layerConfig } of layerPairs) {
13414
- const decision = entry.predicate(path7, layerConfig);
13415
- if (decision.matched) {
13416
- trail.push(decision);
13417
- }
13418
- }
13419
- if (trail.length > 0) {
13420
- excluded.push({ path: path7, decisionTrail: trail });
13961
+ const classified = classifyPath(path7, layerPairs);
13962
+ if (classified.excluded) {
13963
+ excluded.push(classified.entry);
13421
13964
  } else {
13422
- included.push({ path: path7, decisionTrail: [] });
13965
+ included.push(classified.entry);
13423
13966
  }
13424
13967
  }
13425
13968
  }
13426
13969
  return { included, excluded };
13427
13970
  }
13971
+ function classifyPath(path7, layerPairs) {
13972
+ const trail = decisionTrailForPath(path7, layerPairs);
13973
+ return {
13974
+ excluded: trail.length > 0,
13975
+ entry: { path: path7, decisionTrail: trail }
13976
+ };
13977
+ }
13978
+ function decisionTrailForPath(path7, layerPairs) {
13979
+ const trail = [];
13980
+ for (const { entry, layerConfig } of layerPairs) {
13981
+ const decision = entry.predicate(path7, layerConfig);
13982
+ if (decision.matched) {
13983
+ trail.push(decision);
13984
+ }
13985
+ }
13986
+ return trail;
13987
+ }
13428
13988
 
13429
13989
  // src/validation/literal/detector.ts
13430
13990
  import { parse as parseTypeScript } from "@typescript-eslint/parser";
@@ -13485,7 +14045,12 @@ var FIXTURE_DATA_ROLE_SEGMENTS = /* @__PURE__ */ new Set([
13485
14045
  "source"
13486
14046
  ]);
13487
14047
  var FIXTURE_DATA_CONTEXT_SEGMENTS = /* @__PURE__ */ new Set(["path", "tree"]);
13488
- var IDENTIFIER_SEGMENT_PATTERN = /[A-Z]+(?=[A-Z][a-z]|$)|[A-Z]?[a-z]+|[0-9]+/g;
14048
+ var IDENTIFIER_CHAR_KIND = {
14049
+ UPPER: "upper",
14050
+ LOWER: "lower",
14051
+ DIGIT: "digit",
14052
+ OTHER: "other"
14053
+ };
13489
14054
  function collectLiterals(source, filename, options) {
13490
14055
  const ast = parseTypeScript(source, {
13491
14056
  loc: true,
@@ -13539,26 +14104,28 @@ function emitLiteral(node, context, ancestors, options, out) {
13539
14104
  }
13540
14105
  const line = node.loc?.start?.line ?? 0;
13541
14106
  if (node.type === LITERAL_TYPE) {
13542
- if (typeof node.value === "string") {
13543
- if (node.value.length >= options.minStringLength) {
13544
- out.push({ kind: "string", value: node.value, loc: { file: context.filename, line } });
13545
- }
13546
- } else if (typeof node.value === "number") {
13547
- const raw = typeof node.raw === "string" ? node.raw : String(node.value);
13548
- if (isMeaningfulNumber(raw, options.minNumberDigits)) {
13549
- out.push({ kind: "number", value: String(node.value), loc: { file: context.filename, line } });
13550
- }
13551
- }
14107
+ emitEstreeLiteral(node, context, line, options, out);
13552
14108
  return;
13553
14109
  }
13554
- if (node.type === TEMPLATE_ELEMENT_TYPE) {
13555
- const value = node.value;
13556
- const cooked = value?.cooked ?? "";
13557
- if (cooked.length >= options.minStringLength) {
13558
- out.push({ kind: "string", value: cooked, loc: { file: context.filename, line } });
14110
+ emitTemplateElementLiteral(node, context, line, options, out);
14111
+ }
14112
+ function emitEstreeLiteral(node, context, line, options, out) {
14113
+ if (typeof node.value === "string" && node.value.length >= options.minStringLength) {
14114
+ out.push({ kind: "string", value: node.value, loc: { file: context.filename, line } });
14115
+ } else if (typeof node.value === "number") {
14116
+ const raw = typeof node.raw === "string" ? node.raw : String(node.value);
14117
+ if (isMeaningfulNumber(raw, options.minNumberDigits)) {
14118
+ out.push({ kind: "number", value: String(node.value), loc: { file: context.filename, line } });
13559
14119
  }
13560
14120
  }
13561
14121
  }
14122
+ function emitTemplateElementLiteral(node, context, line, options, out) {
14123
+ const value = node.value;
14124
+ const cooked = value?.cooked ?? "";
14125
+ if (cooked.length >= options.minStringLength) {
14126
+ out.push({ kind: "string", value: cooked, loc: { file: context.filename, line } });
14127
+ }
14128
+ }
13562
14129
  function isTestLikeFile(filename) {
13563
14130
  return filename.includes(TEST_PATH_SEGMENT) || filename.includes(WINDOWS_TEST_PATH_SEGMENT) || filename.includes(TEST_FILE_MARKER);
13564
14131
  }
@@ -13620,7 +14187,7 @@ function getFixtureDataDeclaratorName(node) {
13620
14187
  return getIdentifierName(node.init);
13621
14188
  }
13622
14189
  function isFixtureDataVariableName(variableName) {
13623
- const segments = splitIdentifierName(variableName);
14190
+ const segments = fixtureClassificationSegments(variableName);
13624
14191
  if (segments.length === 0) {
13625
14192
  return false;
13626
14193
  }
@@ -13633,11 +14200,92 @@ function isFixtureDataVariableName(variableName) {
13633
14200
  if (segments.every((segment) => FIXTURE_DATA_ROLE_SEGMENTS.has(segment))) {
13634
14201
  return true;
13635
14202
  }
13636
- const finalSegment = segments[segments.length - 1];
14203
+ const finalSegment = segments.at(-1);
13637
14204
  return finalSegment !== void 0 && FIXTURE_DATA_CONTEXT_SEGMENTS.has(finalSegment);
13638
14205
  }
13639
14206
  function splitIdentifierName(variableName) {
13640
- return variableName.split("_").flatMap((part) => part.match(IDENTIFIER_SEGMENT_PATTERN) ?? []).map((segment) => segment.toLowerCase());
14207
+ return variableName.split("_").flatMap(splitIdentifierPart).map((segment) => segment.toLowerCase());
14208
+ }
14209
+ function fixtureClassificationSegments(variableName) {
14210
+ const segments = splitIdentifierName(variableName);
14211
+ if (!isScreamingSnakeIdentifier(variableName)) {
14212
+ return segments;
14213
+ }
14214
+ let first = 0;
14215
+ let last = segments.length;
14216
+ while (first < last && isSingleLetterSegment(segments[first])) {
14217
+ first += 1;
14218
+ }
14219
+ while (last > first && isSingleLetterSegment(segments[last - 1])) {
14220
+ last -= 1;
14221
+ }
14222
+ return segments.slice(first, last);
14223
+ }
14224
+ function isScreamingSnakeIdentifier(variableName) {
14225
+ return variableName.includes("_") && variableName.split("_").every((part) => part !== "" && part === part.toUpperCase() && part !== part.toLowerCase());
14226
+ }
14227
+ function isSingleLetterSegment(segment) {
14228
+ return segment.length === 1 && classifyIdentifierCharacter(segment) === IDENTIFIER_CHAR_KIND.LOWER;
14229
+ }
14230
+ function splitIdentifierPart(identifierPart) {
14231
+ const segments = [];
14232
+ let currentSegment = "";
14233
+ for (const character of identifierPart) {
14234
+ const kind = classifyIdentifierCharacter(character);
14235
+ if (kind === IDENTIFIER_CHAR_KIND.OTHER) {
14236
+ pushIdentifierSegment(segments, currentSegment);
14237
+ currentSegment = "";
14238
+ continue;
14239
+ }
14240
+ if (currentSegment === "") {
14241
+ currentSegment = character;
14242
+ continue;
14243
+ }
14244
+ if (startsNewIdentifierSegment(currentSegment, kind)) {
14245
+ currentSegment = startNextIdentifierSegment(segments, currentSegment, character, kind);
14246
+ continue;
14247
+ }
14248
+ currentSegment = `${currentSegment}${character}`;
14249
+ }
14250
+ pushIdentifierSegment(segments, currentSegment);
14251
+ return segments;
14252
+ }
14253
+ function startNextIdentifierSegment(segments, currentSegment, character, kind) {
14254
+ if (kind === IDENTIFIER_CHAR_KIND.DIGIT && isUppercaseSegment(currentSegment)) {
14255
+ return character;
14256
+ }
14257
+ if (kind === IDENTIFIER_CHAR_KIND.LOWER && isUppercaseRun(currentSegment)) {
14258
+ const prefix = currentSegment.slice(0, -1);
14259
+ pushIdentifierSegment(segments, prefix);
14260
+ return `${currentSegment.at(-1) ?? ""}${character}`;
14261
+ }
14262
+ pushIdentifierSegment(segments, currentSegment);
14263
+ return character;
14264
+ }
14265
+ function classifyIdentifierCharacter(character) {
14266
+ if (character >= "0" && character <= "9") return IDENTIFIER_CHAR_KIND.DIGIT;
14267
+ const lower = character.toLowerCase();
14268
+ const upper = character.toUpperCase();
14269
+ if (lower === upper) return IDENTIFIER_CHAR_KIND.OTHER;
14270
+ return character === upper ? IDENTIFIER_CHAR_KIND.UPPER : IDENTIFIER_CHAR_KIND.LOWER;
14271
+ }
14272
+ function startsNewIdentifierSegment(currentSegment, nextKind) {
14273
+ const currentKind = classifyIdentifierCharacter(currentSegment.at(-1) ?? "");
14274
+ if (nextKind === IDENTIFIER_CHAR_KIND.DIGIT) return currentKind !== IDENTIFIER_CHAR_KIND.DIGIT;
14275
+ if (currentKind === IDENTIFIER_CHAR_KIND.DIGIT) return true;
14276
+ if (nextKind === IDENTIFIER_CHAR_KIND.UPPER) return currentKind === IDENTIFIER_CHAR_KIND.LOWER;
14277
+ return currentKind === IDENTIFIER_CHAR_KIND.UPPER && isUppercaseRun(currentSegment);
14278
+ }
14279
+ function isUppercaseRun(value) {
14280
+ return value.length > 1 && Array.from(value).every((character) => classifyIdentifierCharacter(character) === IDENTIFIER_CHAR_KIND.UPPER);
14281
+ }
14282
+ function isUppercaseSegment(value) {
14283
+ return Array.from(value).every((character) => classifyIdentifierCharacter(character) === IDENTIFIER_CHAR_KIND.UPPER);
14284
+ }
14285
+ function pushIdentifierSegment(segments, segment) {
14286
+ if (segment !== "") {
14287
+ segments.push(segment);
14288
+ }
13641
14289
  }
13642
14290
  function getCallName(node) {
13643
14291
  const callee = node.callee;
@@ -13669,7 +14317,7 @@ function getLiteralString(value) {
13669
14317
  return typeof value.value === "string" ? value.value : void 0;
13670
14318
  }
13671
14319
  function isMeaningfulNumber(raw, minDigits) {
13672
- const digits = raw.replace(/[^0-9]/g, "");
14320
+ const digits = raw.replaceAll(/\D/g, "");
13673
14321
  return digits.length >= minDigits;
13674
14322
  }
13675
14323
  function buildIndex(occurrences) {
@@ -13695,51 +14343,59 @@ function splitKey(key) {
13695
14343
  function detectReuse(input) {
13696
14344
  const srcReuse = [];
13697
14345
  const testDupe = [];
13698
- const testIndex = /* @__PURE__ */ new Map();
13699
- for (const [file, occurrences] of input.testOccurrencesByFile) {
13700
- for (const occ of occurrences) {
13701
- if (input.allowlist.has(occ.value)) continue;
13702
- const key = makeKey(occ.kind, occ.value);
13703
- let byFile = testIndex.get(key);
13704
- if (!byFile) {
13705
- byFile = /* @__PURE__ */ new Map();
13706
- testIndex.set(key, byFile);
13707
- }
13708
- const locsInFile = byFile.get(file);
13709
- if (locsInFile) locsInFile.push(occ.loc);
13710
- else byFile.set(file, [occ.loc]);
13711
- }
13712
- }
14346
+ const testIndex = buildTestOccurrenceIndex(input.testOccurrencesByFile, input.allowlist);
13713
14347
  for (const [key, byFile] of testIndex) {
13714
14348
  const { kind, value } = splitKey(key);
13715
14349
  const srcLocs = input.srcIndex.get(key);
13716
14350
  const allTestLocs = [];
13717
14351
  for (const locs of byFile.values()) allTestLocs.push(...locs);
13718
14352
  if (srcLocs && srcLocs.length > 0) {
13719
- for (const testLoc of allTestLocs) {
13720
- srcReuse.push({
13721
- test: testLoc,
13722
- kind,
13723
- value,
13724
- src: srcLocs,
13725
- remediation: REMEDIATION6.IMPORT_FROM_SOURCE
13726
- });
13727
- }
14353
+ srcReuse.push(...reuseFindings(kind, value, srcLocs, allTestLocs));
13728
14354
  } else if (byFile.size >= 2) {
13729
- for (let i = 0; i < allTestLocs.length; i += 1) {
13730
- const otherTests = [...allTestLocs.slice(0, i), ...allTestLocs.slice(i + 1)];
13731
- testDupe.push({
13732
- test: allTestLocs[i],
13733
- kind,
13734
- value,
13735
- otherTests,
13736
- remediation: REMEDIATION6.REFACTOR_TO_SOURCE_OR_GENERATOR
13737
- });
13738
- }
14355
+ testDupe.push(...dupeFindings(kind, value, allTestLocs));
13739
14356
  }
13740
14357
  }
13741
14358
  return { srcReuse, testDupe };
13742
14359
  }
14360
+ function buildTestOccurrenceIndex(occurrencesByFile, allowlist) {
14361
+ const testIndex = /* @__PURE__ */ new Map();
14362
+ for (const [file, occurrences] of occurrencesByFile) {
14363
+ for (const occ of occurrences) {
14364
+ if (allowlist.has(occ.value)) continue;
14365
+ addTestOccurrence(testIndex, file, occ);
14366
+ }
14367
+ }
14368
+ return testIndex;
14369
+ }
14370
+ function addTestOccurrence(testIndex, file, occurrence) {
14371
+ const key = makeKey(occurrence.kind, occurrence.value);
14372
+ let byFile = testIndex.get(key);
14373
+ if (!byFile) {
14374
+ byFile = /* @__PURE__ */ new Map();
14375
+ testIndex.set(key, byFile);
14376
+ }
14377
+ const locsInFile = byFile.get(file);
14378
+ if (locsInFile) locsInFile.push(occurrence.loc);
14379
+ else byFile.set(file, [occurrence.loc]);
14380
+ }
14381
+ function reuseFindings(kind, value, srcLocs, allTestLocs) {
14382
+ return allTestLocs.map((testLoc) => ({
14383
+ test: testLoc,
14384
+ kind,
14385
+ value,
14386
+ src: srcLocs,
14387
+ remediation: REMEDIATION6.IMPORT_FROM_SOURCE
14388
+ }));
14389
+ }
14390
+ function dupeFindings(kind, value, allTestLocs) {
14391
+ return allTestLocs.map((test, index) => ({
14392
+ test,
14393
+ kind,
14394
+ value,
14395
+ otherTests: [...allTestLocs.slice(0, index), ...allTestLocs.slice(index + 1)],
14396
+ remediation: REMEDIATION6.REFACTOR_TO_SOURCE_OR_GENERATOR
14397
+ }));
14398
+ }
13743
14399
 
13744
14400
  // src/validation/literal/walker.ts
13745
14401
  var TYPESCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx"]);
@@ -13788,7 +14444,7 @@ async function validateLiteralReuse(input) {
13788
14444
  const config = input.config ?? literalConfigDescriptor.defaults;
13789
14445
  const request = input.files ? {
13790
14446
  explicit: input.files.map((f) => {
13791
- const abs = isAbsolute6(f) ? f : resolve7(input.productDir, f);
14447
+ const abs = isAbsolute6(f) ? f : resolve9(input.productDir, f);
13792
14448
  return relative7(input.productDir, abs).split(/[\\/]/g).join("/");
13793
14449
  })
13794
14450
  } : { walkRoot: input.productDir };
@@ -13800,7 +14456,7 @@ async function validateLiteralReuse(input) {
13800
14456
  EMPTY_IGNORE_READER
13801
14457
  );
13802
14458
  const filtered = applyPathFilter2(scope2.included, input.pathConfig);
13803
- const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve7(input.productDir, entry.path));
14459
+ const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve9(input.productDir, entry.path));
13804
14460
  const collectOptions = {
13805
14461
  visitorKeys: defaultVisitorKeys,
13806
14462
  minStringLength: config.minStringLength,
@@ -14083,12 +14739,7 @@ async function createFileSpecificTsconfig(scope2, files, projectRoot, deps = def
14083
14739
  compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
14084
14740
  };
14085
14741
  deps.writeFileSync(configPath, JSON.stringify(tempConfig, null, 2));
14086
- const cleanup = () => {
14087
- try {
14088
- deps.rmSync(tempDir, { recursive: true, force: true });
14089
- } catch {
14090
- }
14091
- };
14742
+ const cleanup = createTemporaryTsconfigCleanup(tempDir, deps);
14092
14743
  return { configPath, tempDir, cleanup };
14093
14744
  }
14094
14745
  async function createScopeFilteredTsconfig(scope2, projectRoot, scopeConfig, deps = defaultTypeScriptDeps) {
@@ -14103,13 +14754,16 @@ async function createScopeFilteredTsconfig(scope2, projectRoot, scopeConfig, dep
14103
14754
  compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
14104
14755
  };
14105
14756
  deps.writeFileSync(configPath, JSON.stringify(tempConfig, null, 2));
14106
- const cleanup = () => {
14757
+ const cleanup = createTemporaryTsconfigCleanup(tempDir, deps);
14758
+ return { configPath, tempDir, cleanup };
14759
+ }
14760
+ function createTemporaryTsconfigCleanup(tempDir, deps) {
14761
+ return () => {
14107
14762
  try {
14108
14763
  deps.rmSync(tempDir, { recursive: true, force: true });
14109
14764
  } catch {
14110
14765
  }
14111
14766
  };
14112
- return { configPath, tempDir, cleanup };
14113
14767
  }
14114
14768
  async function validateTypeScript(context, options = {}) {
14115
14769
  const { scope: scope2, projectRoot, files, scopeConfig } = context;
@@ -14119,32 +14773,16 @@ async function validateTypeScript(context, options = {}) {
14119
14773
  outputStreams = defaultValidationSubprocessOutputStreams
14120
14774
  } = options;
14121
14775
  const configFile = TSCONFIG_FILES[scope2];
14122
- let tool;
14123
- let tscArgs;
14124
14776
  if (files && files.length > 0) {
14125
14777
  const { configPath, cleanup } = await createFileSpecificTsconfig(scope2, files, projectRoot, deps);
14126
14778
  try {
14127
- return await new Promise((resolve8) => {
14128
- const tscBin = join32(projectRoot, "node_modules", ".bin", "tsc");
14129
- const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
14130
- const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
14131
- const tscProcess = spawnManagedSubprocess(runner, tscBinary, tscArgs2, {
14132
- cwd: projectRoot
14133
- });
14134
- forwardValidationSubprocessOutput(tscProcess, outputStreams);
14135
- tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
14136
- cleanup();
14137
- if (code === 0) {
14138
- resolve8({ success: true, skipped: false });
14139
- } else {
14140
- resolve8({ success: false, error: `TypeScript exited with code ${code}` });
14141
- }
14142
- });
14143
- tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
14144
- cleanup();
14145
- resolve8({ success: false, error: error.message });
14146
- });
14147
- });
14779
+ return await runTypeScriptInvocation(
14780
+ projectRoot,
14781
+ resolveProjectTscInvocation(projectRoot, deps, ["--project", configPath]),
14782
+ runner,
14783
+ outputStreams,
14784
+ cleanup
14785
+ );
14148
14786
  } catch (error) {
14149
14787
  cleanup();
14150
14788
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -14158,47 +14796,47 @@ async function validateTypeScript(context, options = {}) {
14158
14796
  return { success: true, skipped: true };
14159
14797
  }
14160
14798
  const { configPath, cleanup } = await createScopeFilteredTsconfig(scope2, projectRoot, scopeConfig, deps);
14161
- const tscBin = join32(projectRoot, "node_modules", ".bin", "tsc");
14162
- tool = deps.existsSync(tscBin) ? tscBin : "npx";
14163
- tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
14164
- return new Promise((resolve8) => {
14165
- const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
14166
- cwd: projectRoot
14167
- });
14168
- forwardValidationSubprocessOutput(tscProcess, outputStreams);
14169
- tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
14170
- cleanup();
14171
- if (code === 0) {
14172
- resolve8({ success: true, skipped: false });
14173
- } else {
14174
- resolve8({ success: false, error: `TypeScript exited with code ${code}` });
14175
- }
14176
- });
14177
- tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
14178
- cleanup();
14179
- resolve8({ success: false, error: error.message });
14180
- });
14181
- });
14182
- } else {
14183
- const tscBin = join32(projectRoot, "node_modules", ".bin", "tsc");
14184
- tool = deps.existsSync(tscBin) ? tscBin : "npx";
14185
- const rawArgs = buildTypeScriptArgs({ scope: scope2, configFile });
14186
- tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
14799
+ return runTypeScriptInvocation(
14800
+ projectRoot,
14801
+ resolveProjectTscInvocation(projectRoot, deps, ["--project", configPath]),
14802
+ runner,
14803
+ outputStreams,
14804
+ cleanup
14805
+ );
14187
14806
  }
14188
- return new Promise((resolve8) => {
14189
- const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
14807
+ return runTypeScriptInvocation(
14808
+ projectRoot,
14809
+ resolveProjectTscInvocation(projectRoot, deps, buildTypeScriptArgs({ scope: scope2, configFile }).slice(1)),
14810
+ runner,
14811
+ outputStreams
14812
+ );
14813
+ }
14814
+ function resolveProjectTscInvocation(projectRoot, deps, tscArgs) {
14815
+ const tscBin = join32(projectRoot, "node_modules", ".bin", "tsc");
14816
+ const tool = deps.existsSync(tscBin) ? tscBin : "npx";
14817
+ return {
14818
+ tool,
14819
+ args: tool === "npx" ? ["tsc", ...tscArgs] : tscArgs
14820
+ };
14821
+ }
14822
+ function runTypeScriptInvocation(projectRoot, invocation, runner, outputStreams, cleanup = () => {
14823
+ }) {
14824
+ return new Promise((resolve10) => {
14825
+ const tscProcess = spawnManagedSubprocess(runner, invocation.tool, invocation.args, {
14190
14826
  cwd: projectRoot
14191
14827
  });
14192
14828
  forwardValidationSubprocessOutput(tscProcess, outputStreams);
14193
14829
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
14830
+ cleanup();
14194
14831
  if (code === 0) {
14195
- resolve8({ success: true, skipped: false });
14832
+ resolve10({ success: true, skipped: false });
14196
14833
  } else {
14197
- resolve8({ success: false, error: `TypeScript exited with code ${code}` });
14834
+ resolve10({ success: false, error: `TypeScript exited with code ${code}` });
14198
14835
  }
14199
14836
  });
14200
14837
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
14201
- resolve8({ success: false, error: error.message });
14838
+ cleanup();
14839
+ resolve10({ success: false, error: error.message });
14202
14840
  });
14203
14841
  });
14204
14842
  }
@@ -14259,16 +14897,19 @@ async function typescriptCommand(options) {
14259
14897
  scopeConfig
14260
14898
  });
14261
14899
  const durationMs = Date.now() - startTime;
14900
+ return formatTypeScriptResult(result, quiet, durationMs);
14901
+ }
14902
+ function formatTypeScriptResult(result, quiet, durationMs) {
14262
14903
  if (result.skipped) {
14263
- const output = quiet ? "" : TYPESCRIPT_VALIDATION_MESSAGES.NO_VALIDATION_PATH_TARGETS;
14264
- return { exitCode: 0, output, durationMs };
14265
- } else if (result.success) {
14266
- const output = quiet ? "" : TYPESCRIPT_VALIDATION_MESSAGES.SUCCESS;
14267
- return { exitCode: 0, output, durationMs };
14268
- } else {
14269
- const output = result.error ?? VALIDATION_COMMAND_OUTPUT.TYPESCRIPT_FAILURE;
14270
- return { exitCode: 1, output, durationMs };
14904
+ const output2 = quiet ? "" : TYPESCRIPT_VALIDATION_MESSAGES.NO_VALIDATION_PATH_TARGETS;
14905
+ return { exitCode: 0, output: output2, durationMs };
14906
+ }
14907
+ if (result.success) {
14908
+ const output2 = quiet ? "" : TYPESCRIPT_VALIDATION_MESSAGES.SUCCESS;
14909
+ return { exitCode: 0, output: output2, durationMs };
14271
14910
  }
14911
+ const output = result.error ?? VALIDATION_COMMAND_OUTPUT.TYPESCRIPT_FAILURE;
14912
+ return { exitCode: 1, output, durationMs };
14272
14913
  }
14273
14914
 
14274
14915
  // src/validation/languages/typescript.ts
@@ -14425,7 +15066,7 @@ async function allCommand(options) {
14425
15066
  // src/validation/literal/allowlist-existing.ts
14426
15067
  import { randomBytes } from "crypto";
14427
15068
  import { rename as rename4, writeFile as writeFile4 } from "fs/promises";
14428
- import { dirname as dirname9, join as join33 } from "path";
15069
+ import { dirname as dirname10, join as join33 } from "path";
14429
15070
  var EXIT_OK = 0;
14430
15071
  var EXIT_ERROR = 1;
14431
15072
  var INCLUDE_FIELD = "include";
@@ -14443,7 +15084,7 @@ var productionReader = {
14443
15084
  };
14444
15085
  var productionWriter = {
14445
15086
  async write(filePath, content) {
14446
- const dir = dirname9(filePath);
15087
+ const dir = dirname10(filePath);
14447
15088
  const random = randomBytes(RANDOM_TOKEN_BYTES).toString("hex");
14448
15089
  const tmpPath = join33(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
14449
15090
  await writeFile4(tmpPath, content, "utf8");
@@ -14620,6 +15261,13 @@ var validationKnownOperands = /* @__PURE__ */ new Set([
14620
15261
  ...Object.values(validationCliDefinition.commanderHelpOperands)
14621
15262
  ]);
14622
15263
  var validationOptionPrefix = validationCliDefinition.commanderHelpOperands.longFlag.slice(0, 1);
15264
+ function emitValidationResult(result, io) {
15265
+ if (result.output.length > 0) {
15266
+ io.writeStdout(`${result.output}
15267
+ `);
15268
+ }
15269
+ return io.exit(result.exitCode);
15270
+ }
14623
15271
  function addCommonOptions(cmd) {
14624
15272
  return cmd.option("--scope <scope>", "Validation scope (full|production)", "full").option("--files <paths...>", "Specific files/directories to validate").option("--quiet", "Suppress progress output").option("--json", "Output results as JSON");
14625
15273
  }
@@ -14630,55 +15278,52 @@ function addValidationSubcommand(validationCmd, definition) {
14630
15278
  }
14631
15279
  return subcommand;
14632
15280
  }
14633
- function registerValidationCommands(validationCmd) {
15281
+ function registerValidationCommands(validationCmd, invocation) {
14634
15282
  const { subcommands } = validationCliDefinition;
15283
+ const productDir = () => invocation.resolveProductContext().productDir;
14635
15284
  const tsCmd = addValidationSubcommand(validationCmd, subcommands.typescript).action(async (options) => {
14636
15285
  const result = await typescriptCommand({
14637
- cwd: process.cwd(),
15286
+ cwd: productDir(),
14638
15287
  scope: options.scope,
14639
15288
  files: options.files,
14640
15289
  quiet: options.quiet,
14641
15290
  json: options.json
14642
15291
  });
14643
- if (result.output) console.log(result.output);
14644
- process.exit(result.exitCode);
15292
+ emitValidationResult(result, invocation.io);
14645
15293
  });
14646
15294
  addCommonOptions(tsCmd);
14647
15295
  const lintCmd = addValidationSubcommand(validationCmd, subcommands.lint).option("--fix", "Auto-fix issues").action(async (options) => {
14648
15296
  const result = await lintCommand({
14649
- cwd: process.cwd(),
15297
+ cwd: productDir(),
14650
15298
  scope: options.scope,
14651
15299
  files: options.files,
14652
15300
  fix: options.fix,
14653
15301
  quiet: options.quiet,
14654
15302
  json: options.json
14655
15303
  });
14656
- if (result.output) console.log(result.output);
14657
- process.exit(result.exitCode);
15304
+ emitValidationResult(result, invocation.io);
14658
15305
  });
14659
15306
  addCommonOptions(lintCmd);
14660
15307
  const circularCmd = addValidationSubcommand(validationCmd, subcommands.circular).action(async (options) => {
14661
15308
  const result = await circularCommand({
14662
- cwd: process.cwd(),
15309
+ cwd: productDir(),
14663
15310
  scope: options.scope,
14664
15311
  files: options.files,
14665
15312
  quiet: options.quiet,
14666
15313
  json: options.json
14667
15314
  });
14668
- if (result.output) console.log(result.output);
14669
- process.exit(result.exitCode);
15315
+ emitValidationResult(result, invocation.io);
14670
15316
  });
14671
15317
  addCommonOptions(circularCmd);
14672
15318
  const knipCmd = addValidationSubcommand(validationCmd, subcommands.knip).action(async (options) => {
14673
15319
  const result = await knipCommand({
14674
- cwd: process.cwd(),
15320
+ cwd: productDir(),
14675
15321
  scope: options.scope,
14676
15322
  files: options.files,
14677
15323
  quiet: options.quiet,
14678
15324
  json: options.json
14679
15325
  });
14680
- if (result.output) console.log(result.output);
14681
- process.exit(result.exitCode);
15326
+ emitValidationResult(result, invocation.io);
14682
15327
  });
14683
15328
  addCommonOptions(knipCmd);
14684
15329
  const literalCmd = addValidationSubcommand(validationCmd, subcommands.literal).option(
@@ -14692,24 +15337,23 @@ function registerValidationCommands(validationCmd) {
14692
15337
  "\nEnabled for TypeScript projects by default. Set validation.literal.enabled=false\nin spx.config.* to skip during migration."
14693
15338
  ).action(async (options) => {
14694
15339
  if (options.allowlistExisting) {
14695
- const result2 = await allowlistExisting({ productDir: process.cwd() });
14696
- if (result2.output) console.log(result2.output);
14697
- process.exit(result2.exitCode);
15340
+ const result2 = await allowlistExisting({ productDir: productDir() });
15341
+ emitValidationResult(result2, invocation.io);
14698
15342
  }
14699
15343
  let kind;
14700
15344
  if (options.kind !== void 0) {
14701
15345
  kind = parseLiteralProblemKind(options.kind);
14702
15346
  if (kind === void 0) {
14703
15347
  const { unknownLiteralProblemKind } = validationCliDefinition.diagnostics;
14704
- process.stderr.write(
15348
+ invocation.io.writeStderr(
14705
15349
  `spx validation literal: ${unknownLiteralProblemKind.messageLabel}: ${sanitizeCliArgument(options.kind)}
14706
15350
  `
14707
15351
  );
14708
- process.exit(unknownLiteralProblemKind.exitCode);
15352
+ invocation.io.exit(unknownLiteralProblemKind.exitCode);
14709
15353
  }
14710
15354
  }
14711
15355
  const result = await literalCommand({
14712
- cwd: process.cwd(),
15356
+ cwd: productDir(),
14713
15357
  files: options.files,
14714
15358
  kind,
14715
15359
  filesWithProblems: options.filesWithProblems,
@@ -14718,8 +15362,7 @@ function registerValidationCommands(validationCmd) {
14718
15362
  quiet: options.quiet,
14719
15363
  json: options.json
14720
15364
  });
14721
- if (result.output) console.log(result.output);
14722
- process.exit(result.exitCode);
15365
+ emitValidationResult(result, invocation.io);
14723
15366
  });
14724
15367
  addCommonOptions(literalCmd);
14725
15368
  const markdownCmd = addValidationSubcommand(validationCmd, subcommands.markdown).addHelpText(
@@ -14727,27 +15370,25 @@ function registerValidationCommands(validationCmd) {
14727
15370
  "\nValidates spx/ and docs/ by default. Nodes listed in spx/EXCLUDE are\nskipped \u2014 use this for declared-state nodes whose [test] links point\nto files that do not exist yet."
14728
15371
  ).action(async (options) => {
14729
15372
  const result = await markdownCommand({
14730
- cwd: process.cwd(),
15373
+ cwd: productDir(),
14731
15374
  files: options.files,
14732
15375
  quiet: options.quiet
14733
15376
  });
14734
- if (result.output) console.log(result.output);
14735
- process.exit(result.exitCode);
15377
+ emitValidationResult(result, invocation.io);
14736
15378
  });
14737
15379
  addCommonOptions(markdownCmd);
14738
15380
  const formatCmd = addValidationSubcommand(validationCmd, subcommands.format).action(async (options) => {
14739
15381
  const result = await formattingCommand({
14740
- cwd: process.cwd(),
15382
+ cwd: productDir(),
14741
15383
  files: options.files,
14742
15384
  quiet: options.quiet
14743
15385
  });
14744
- if (result.output) console.log(result.output);
14745
- process.exit(result.exitCode);
15386
+ emitValidationResult(result, invocation.io);
14746
15387
  });
14747
15388
  addCommonOptions(formatCmd);
14748
15389
  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) => {
14749
15390
  const result = await allCommand({
14750
- cwd: process.cwd(),
15391
+ cwd: productDir(),
14751
15392
  scope: options.scope,
14752
15393
  files: options.files,
14753
15394
  fix: options.fix,
@@ -14756,8 +15397,7 @@ function registerValidationCommands(validationCmd) {
14756
15397
  quiet: options.quiet,
14757
15398
  json: options.json
14758
15399
  });
14759
- if (result.output) console.log(result.output);
14760
- process.exit(result.exitCode);
15400
+ emitValidationResult(result, invocation.io);
14761
15401
  });
14762
15402
  addCommonOptions(allCmd);
14763
15403
  }
@@ -14767,25 +15407,264 @@ function parseLiteralProblemKind(value) {
14767
15407
  }
14768
15408
  return void 0;
14769
15409
  }
14770
- function handleUnknownSubcommand(operands) {
15410
+ function handleUnknownSubcommand(operands, io) {
14771
15411
  const [first] = operands;
14772
15412
  const sanitized = sanitizeCliArgument(first);
14773
15413
  const { domain, diagnostics } = validationCliDefinition;
14774
15414
  const { unknownSubcommand } = diagnostics;
14775
- process.stderr.write(`spx ${domain.commandName}: ${unknownSubcommand.messageLabel}: ${sanitized}
15415
+ io.writeStderr(`spx ${domain.commandName}: ${unknownSubcommand.messageLabel}: ${sanitized}
14776
15416
  `);
14777
- process.exit(unknownSubcommand.exitCode);
15417
+ return io.exit(unknownSubcommand.exitCode);
14778
15418
  }
14779
15419
  var validationDomain = {
14780
15420
  name: validationCliDefinition.domain.commandName,
14781
15421
  description: validationCliDefinition.domain.description,
14782
- register: (program2) => {
15422
+ register: (program, invocation) => {
14783
15423
  const { domain } = validationCliDefinition;
14784
- const validationCmd = program2.command(domain.commandName).alias(domain.alias).description(domain.description);
15424
+ const validationCmd = program.command(domain.commandName).alias(domain.alias).description(domain.description);
14785
15425
  validationCmd.on("command:*", (operands) => {
14786
- handleUnknownSubcommand(operands);
15426
+ handleUnknownSubcommand(operands, invocation.io);
15427
+ });
15428
+ registerValidationCommands(validationCmd, invocation);
15429
+ }
15430
+ };
15431
+
15432
+ // src/commands/verification-context/cli.ts
15433
+ import { isAbsolute as isAbsolute8, win32 } from "path";
15434
+
15435
+ // src/domains/verification-context/context.ts
15436
+ var VERIFICATION_CONTEXT_SCHEMA_VERSION = "verification-context.v1";
15437
+ var VERIFICATION_CONTEXT_SUBJECT_KIND = {
15438
+ FILE: "file",
15439
+ CHANGESET: "changeset"
15440
+ };
15441
+ var VERIFICATION_CONTEXT_PERSISTENCE = {
15442
+ kind: "state-store",
15443
+ scope: "branch",
15444
+ domain: "verification-context",
15445
+ format: "canonical-json"
15446
+ };
15447
+ var VERIFICATION_CONTEXT_DIGEST_PATH = "verification context";
15448
+ function createVerificationContextDocument(payload) {
15449
+ const digest = digestDescriptorSection(payload, VERIFICATION_CONTEXT_DIGEST_PATH);
15450
+ if (!digest.ok) return digest;
15451
+ const document = {
15452
+ digest: digest.value.sha256,
15453
+ context: payload
15454
+ };
15455
+ const canonical = canonicalDescriptorJson(document, VERIFICATION_CONTEXT_DIGEST_PATH);
15456
+ if (!canonical.ok) return canonical;
15457
+ return {
15458
+ ok: true,
15459
+ value: {
15460
+ ...document,
15461
+ canonicalJson: canonical.value
15462
+ }
15463
+ };
15464
+ }
15465
+
15466
+ // src/commands/verification-context/runtime.ts
15467
+ import { dirname as dirname11 } from "path";
15468
+
15469
+ // src/domains/verification-context/path.ts
15470
+ import { join as join34 } from "path";
15471
+ var VERIFICATION_CONTEXT_STATE_DOMAIN = VERIFICATION_CONTEXT_PERSISTENCE.domain;
15472
+ var VERIFICATION_CONTEXT_STATE_PATH = {
15473
+ CONTEXTS_DIR: "contexts",
15474
+ FILE_PREFIX: "context-",
15475
+ JSON_EXTENSION: ".json"
15476
+ };
15477
+ function verificationContextFileName(digest) {
15478
+ return `${VERIFICATION_CONTEXT_STATE_PATH.FILE_PREFIX}${digest}${VERIFICATION_CONTEXT_STATE_PATH.JSON_EXTENSION}`;
15479
+ }
15480
+ function verificationContextFilePath(scope2) {
15481
+ const branchScope = branchScopeDir(scope2.productDir, scope2.branchSlug);
15482
+ if (!branchScope.ok) return branchScope;
15483
+ const contextsDir = composeScopeDir(
15484
+ branchScope.value,
15485
+ VERIFICATION_CONTEXT_STATE_DOMAIN,
15486
+ VERIFICATION_CONTEXT_STATE_PATH.CONTEXTS_DIR
15487
+ );
15488
+ if (!contextsDir.ok) return contextsDir;
15489
+ const digest = validateScopeToken(scope2.digest);
15490
+ if (!digest.ok) return digest;
15491
+ return { ok: true, value: join34(contextsDir.value, verificationContextFileName(digest.value)) };
15492
+ }
15493
+
15494
+ // src/commands/verification-context/runtime.ts
15495
+ var VERIFICATION_CONTEXT_RUNTIME_ERROR = {
15496
+ READ_FAILED: "verification context read failed",
15497
+ WRITE_FAILED: "verification context write failed",
15498
+ CONTENT_MISMATCH: "verification context already exists with different content"
15499
+ };
15500
+ async function persistVerificationContext(scope2, document, options = {}) {
15501
+ const contextPath = verificationContextFilePath(scope2);
15502
+ if (!contextPath.ok) return contextPath;
15503
+ const fs8 = options.fs ?? defaultFileSystem;
15504
+ try {
15505
+ await fs8.mkdir(dirname11(contextPath.value), { recursive: true });
15506
+ await fs8.writeFile(contextPath.value, document.canonicalJson, { flag: EXCLUSIVE_CREATE_FLAG });
15507
+ } catch (error) {
15508
+ if (hasErrorCode(error, ERROR_CODE_FILE_EXISTS)) {
15509
+ const existing = await readExistingContext(fs8, contextPath.value);
15510
+ if (!existing.ok) return existing;
15511
+ if (existing.value === document.canonicalJson) {
15512
+ return { ok: true, value: { digest: document.digest, contextPath: contextPath.value } };
15513
+ }
15514
+ return { ok: false, error: VERIFICATION_CONTEXT_RUNTIME_ERROR.CONTENT_MISMATCH };
15515
+ }
15516
+ return { ok: false, error: `${VERIFICATION_CONTEXT_RUNTIME_ERROR.WRITE_FAILED}: ${toMessage(error)}` };
15517
+ }
15518
+ return { ok: true, value: { digest: document.digest, contextPath: contextPath.value } };
15519
+ }
15520
+ async function readExistingContext(fs8, contextPath) {
15521
+ try {
15522
+ return { ok: true, value: await fs8.readFile(contextPath, STATE_STORE_TEXT_ENCODING) };
15523
+ } catch (error) {
15524
+ return { ok: false, error: `${VERIFICATION_CONTEXT_RUNTIME_ERROR.READ_FAILED}: ${toMessage(error)}` };
15525
+ }
15526
+ }
15527
+
15528
+ // src/commands/verification-context/cli.ts
15529
+ var VERIFICATION_CONTEXT_CLI_EXIT_CODE = {
15530
+ OK: 0,
15531
+ ERROR: 1
15532
+ };
15533
+ var VERIFICATION_CONTEXT_CLI_ENV = {
15534
+ BRANCH: SPX_VERIFY_ENV.BRANCH
15535
+ };
15536
+ var VERIFICATION_CONTEXT_CLI_ERROR = {
15537
+ INVALID_SUBJECT: "verification context subject must be file or changeset",
15538
+ FILE_PATH_REQUIRED: "verification context file subject requires --path",
15539
+ FILE_PATH_UNSAFE: "verification context file subject path must be product-relative",
15540
+ CHANGESET_REFS_REQUIRED: "verification context changeset subject requires --base and --head"
15541
+ };
15542
+ var VERIFICATION_CONTEXT_FILE_SUBJECT_PATH = {
15543
+ PARENT_DIRECTORY: {
15544
+ SEGMENT: "..",
15545
+ PREFIX: "../"
15546
+ },
15547
+ SEPARATOR: {
15548
+ CANONICAL: "/",
15549
+ WINDOWS: "\\"
15550
+ }
15551
+ };
15552
+ async function resolveCommandScope(deps) {
15553
+ const cwd = deps.cwd ?? CONFIG_PROCESS_CWD.read();
15554
+ const git = deps.git ?? defaultGitDependencies;
15555
+ const product = await detectGitCommonDirProductRoot(cwd, git);
15556
+ const processEnv = deps.processEnv ?? process.env;
15557
+ const probedBranch = product.isGitRepo ? await getCurrentBranch(cwd, git) ?? void 0 : void 0;
15558
+ const headSha = (product.isGitRepo ? await getHeadSha(cwd, git) : null) ?? SPX_VERIFY_HEAD_SHA.MISSING;
15559
+ const branchIdentity = resolveBranchIdentity({
15560
+ branchName: deps.branch ?? processEnv[VERIFICATION_CONTEXT_CLI_ENV.BRANCH] ?? probedBranch,
15561
+ headSha
15562
+ });
15563
+ return {
15564
+ storageProductDir: product.productDir,
15565
+ launchProductDir: product.worktreeRoot,
15566
+ branchSlug: slugBranchIdentity(branchIdentity),
15567
+ branchIdentity,
15568
+ headSha
15569
+ };
15570
+ }
15571
+ function okResult2(output) {
15572
+ return { exitCode: VERIFICATION_CONTEXT_CLI_EXIT_CODE.OK, output };
15573
+ }
15574
+ function errorResult2(error) {
15575
+ return { exitCode: VERIFICATION_CONTEXT_CLI_EXIT_CODE.ERROR, output: error };
15576
+ }
15577
+ function normalizeFileSubjectPath(path7) {
15578
+ const windowsRoot = win32.parse(path7).root;
15579
+ const normalized = win32.normalize(path7).replaceAll(
15580
+ VERIFICATION_CONTEXT_FILE_SUBJECT_PATH.SEPARATOR.WINDOWS,
15581
+ VERIFICATION_CONTEXT_FILE_SUBJECT_PATH.SEPARATOR.CANONICAL
15582
+ );
15583
+ const segments = normalized.split(VERIFICATION_CONTEXT_FILE_SUBJECT_PATH.SEPARATOR.CANONICAL);
15584
+ if (isAbsolute8(path7) || win32.isAbsolute(path7) || windowsRoot.length > 0 || segments.includes(VERIFICATION_CONTEXT_FILE_SUBJECT_PATH.PARENT_DIRECTORY.SEGMENT)) {
15585
+ return void 0;
15586
+ }
15587
+ return normalized;
15588
+ }
15589
+ function resolveSubject(options) {
15590
+ if (options.subject === VERIFICATION_CONTEXT_SUBJECT_KIND.FILE) {
15591
+ if (options.path === void 0 || options.path.length === 0) {
15592
+ return VERIFICATION_CONTEXT_CLI_ERROR.FILE_PATH_REQUIRED;
15593
+ }
15594
+ const path7 = normalizeFileSubjectPath(options.path);
15595
+ if (path7 === void 0) return VERIFICATION_CONTEXT_CLI_ERROR.FILE_PATH_UNSAFE;
15596
+ return { kind: VERIFICATION_CONTEXT_SUBJECT_KIND.FILE, path: path7 };
15597
+ }
15598
+ if (options.subject === VERIFICATION_CONTEXT_SUBJECT_KIND.CHANGESET) {
15599
+ if (options.base === void 0 || options.base.length === 0 || options.head === void 0 || options.head.length === 0) {
15600
+ return VERIFICATION_CONTEXT_CLI_ERROR.CHANGESET_REFS_REQUIRED;
15601
+ }
15602
+ return { kind: VERIFICATION_CONTEXT_SUBJECT_KIND.CHANGESET, base: options.base, head: options.head };
15603
+ }
15604
+ return VERIFICATION_CONTEXT_CLI_ERROR.INVALID_SUBJECT;
15605
+ }
15606
+ async function verificationContextCreateCommand(options, deps = {}) {
15607
+ const subject = resolveSubject(options);
15608
+ if (typeof subject === "string") return errorResult2(subject);
15609
+ const scope2 = await resolveCommandScope(deps);
15610
+ const document = createVerificationContextDocument({
15611
+ schemaVersion: VERIFICATION_CONTEXT_SCHEMA_VERSION,
15612
+ subject,
15613
+ predicate: options.predicate,
15614
+ workflow: { name: options.workflow },
15615
+ launch: {
15616
+ productDir: scope2.launchProductDir,
15617
+ branchSlug: scope2.branchSlug,
15618
+ branchIdentity: scope2.branchIdentity,
15619
+ headSha: scope2.headSha,
15620
+ createdAt: (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
15621
+ },
15622
+ persistence: VERIFICATION_CONTEXT_PERSISTENCE
15623
+ });
15624
+ if (!document.ok) return errorResult2(document.error);
15625
+ const persisted = await persistVerificationContext(
15626
+ { productDir: scope2.storageProductDir, branchSlug: scope2.branchSlug, digest: document.value.digest },
15627
+ document.value,
15628
+ { ...deps.fs === void 0 ? {} : { fs: deps.fs } }
15629
+ );
15630
+ if (!persisted.ok) return errorResult2(persisted.error);
15631
+ return okResult2(JSON.stringify(persisted.value));
15632
+ }
15633
+
15634
+ // src/interfaces/cli/lib/stream-report.ts
15635
+ var CLI_STREAM_REPORT = {
15636
+ LINE_SEPARATOR: "\n"
15637
+ };
15638
+ function reportCliResult(result, io) {
15639
+ const output = `${result.output}${CLI_STREAM_REPORT.LINE_SEPARATOR}`;
15640
+ if (result.exitCode === 0) io.writeStdout(output);
15641
+ else io.writeStderr(output);
15642
+ io.setExitCode(result.exitCode);
15643
+ }
15644
+
15645
+ // src/interfaces/cli/verification-context.ts
15646
+ var VERIFICATION_CONTEXT_CLI = {
15647
+ commandName: "verification-context",
15648
+ description: "Create an immutable verification context document",
15649
+ createCommandName: "create",
15650
+ subjectOption: "--subject <subject>",
15651
+ pathOption: "--path <path>",
15652
+ baseOption: "--base <ref>",
15653
+ headOption: "--head <ref>",
15654
+ predicateOption: "--predicate <predicate>",
15655
+ workflowOption: "--workflow <workflow>"
15656
+ };
15657
+ var verificationContextDomain = {
15658
+ name: VERIFICATION_CONTEXT_CLI.commandName,
15659
+ description: VERIFICATION_CONTEXT_CLI.description,
15660
+ register: (program, invocation) => {
15661
+ const command = program.command(VERIFICATION_CONTEXT_CLI.commandName).description(VERIFICATION_CONTEXT_CLI.description);
15662
+ command.command(VERIFICATION_CONTEXT_CLI.createCommandName).description("Create a canonical verification context and report its path and digest").requiredOption(VERIFICATION_CONTEXT_CLI.subjectOption, "Subject kind: file or changeset").option(VERIFICATION_CONTEXT_CLI.pathOption, "Product-relative file path for a file subject").option(VERIFICATION_CONTEXT_CLI.baseOption, "Base ref for a changeset subject").option(VERIFICATION_CONTEXT_CLI.headOption, "Head ref for a changeset subject").requiredOption(VERIFICATION_CONTEXT_CLI.predicateOption, "Caller-supplied predicate identifier").requiredOption(VERIFICATION_CONTEXT_CLI.workflowOption, "Caller-supplied workflow identifier").action(async (options) => {
15663
+ reportCliResult(
15664
+ await verificationContextCreateCommand(options, { cwd: invocation.resolveEffectiveInvocationDir() }),
15665
+ invocation.io
15666
+ );
14787
15667
  });
14788
- registerValidationCommands(validationCmd);
14789
15668
  }
14790
15669
  };
14791
15670
 
@@ -14890,15 +15769,29 @@ var WORKTREE_CLI = {
14890
15769
  WORKTREES_DIR_FLAG: "--worktrees-dir"
14891
15770
  };
14892
15771
  var WORKTREE_DOMAIN_DESCRIPTION = "Coordinate worktree occupancy across a bare-repository pool";
14893
- function handleError3(error) {
14894
- console.error("Error:", error);
14895
- process.exit(1);
15772
+ function writeOutput4(invocation, output) {
15773
+ invocation.io.writeStdout(`${output}
15774
+ `);
15775
+ }
15776
+ function writeError4(invocation, output) {
15777
+ invocation.io.writeStderr(`${output}
15778
+ `);
15779
+ }
15780
+ function writeInvocationWarning4(invocation, warning) {
15781
+ if (warning !== void 0) {
15782
+ writeError4(invocation, warning);
15783
+ }
14896
15784
  }
14897
- function registerWorktreeCommands(worktreeCmd) {
15785
+ function handleError3(invocation, error) {
15786
+ writeError4(invocation, `Error: ${error}`);
15787
+ return invocation.io.exit(1);
15788
+ }
15789
+ function registerWorktreeCommands(worktreeCmd, invocation) {
15790
+ const effectiveInvocationDir = () => invocation.resolveEffectiveInvocationDir();
14898
15791
  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) => {
14899
15792
  const result = await claimCommand({
14900
15793
  claimWriteToken: createClaimWriteToken(),
14901
- cwd: process.cwd(),
15794
+ cwd: effectiveInvocationDir(),
14902
15795
  env: process.env,
14903
15796
  fs: defaultOccupancyFileSystem,
14904
15797
  gitDeps: defaultGitDependencies,
@@ -14906,13 +15799,13 @@ function registerWorktreeCommands(worktreeCmd) {
14906
15799
  selfPid: process.pid,
14907
15800
  sessionId: options.sessionId,
14908
15801
  worktreesDir: options.worktreesDir,
14909
- onWarning: writeWarning
15802
+ onWarning: (warning) => writeInvocationWarning4(invocation, warning)
14910
15803
  });
14911
- if (!result.ok) handleError3(result.error);
15804
+ if (!result.ok) handleError3(invocation, result.error);
14912
15805
  });
14913
15806
  worktreeCmd.command(`${WORKTREE_CLI.STATUS} ${WORKTREE_CLI.WORKTREE_ARGUMENT}`).description("Report a worktree's occupancy (running | free)").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) => {
14914
15807
  const result = await statusCommand2({
14915
- cwd: process.cwd(),
15808
+ cwd: effectiveInvocationDir(),
14916
15809
  fs: defaultOccupancyFileSystem,
14917
15810
  gitDeps: defaultGitDependencies,
14918
15811
  worktrees,
@@ -14920,28 +15813,28 @@ function registerWorktreeCommands(worktreeCmd) {
14920
15813
  pathInfo: defaultWorktreePathInfo,
14921
15814
  processTable: defaultProcessTable,
14922
15815
  worktreesDir: options.worktreesDir,
14923
- onWarning: writeWarning
15816
+ onWarning: (warning) => writeInvocationWarning4(invocation, warning)
14924
15817
  });
14925
- if (!result.ok) handleError3(result.error);
14926
- console.log(result.value);
15818
+ if (!result.ok) handleError3(invocation, result.error);
15819
+ writeOutput4(invocation, result.value);
14927
15820
  });
14928
15821
  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) => {
14929
15822
  const result = await releaseCommand2({
14930
- cwd: process.cwd(),
15823
+ cwd: effectiveInvocationDir(),
14931
15824
  fs: defaultOccupancyFileSystem,
14932
15825
  gitDeps: defaultGitDependencies,
14933
15826
  worktreesDir: options.worktreesDir,
14934
- onWarning: writeWarning
15827
+ onWarning: (warning) => writeInvocationWarning4(invocation, warning)
14935
15828
  });
14936
- if (!result.ok) handleError3(result.error);
15829
+ if (!result.ok) handleError3(invocation, result.error);
14937
15830
  });
14938
15831
  }
14939
15832
  var worktreeDomain = {
14940
15833
  name: WORKTREE_CLI.COMMAND,
14941
15834
  description: WORKTREE_DOMAIN_DESCRIPTION,
14942
- register: (program2) => {
14943
- const worktreeCmd = program2.command(WORKTREE_CLI.COMMAND).description(WORKTREE_DOMAIN_DESCRIPTION);
14944
- registerWorktreeCommands(worktreeCmd);
15835
+ register: (program, invocation) => {
15836
+ const worktreeCmd = program.command(WORKTREE_CLI.COMMAND).description(WORKTREE_DOMAIN_DESCRIPTION);
15837
+ registerWorktreeCommands(worktreeCmd, invocation);
14945
15838
  }
14946
15839
  };
14947
15840
 
@@ -14957,17 +15850,46 @@ var CLI_DOMAINS = [
14957
15850
  specDomain,
14958
15851
  testingDomain,
14959
15852
  validationDomain,
15853
+ verificationContextDomain,
14960
15854
  worktreeDomain
14961
15855
  ];
14962
15856
 
15857
+ // src/interfaces/cli/program.ts
15858
+ var SPX_PROGRAM_NAME = "spx";
15859
+ var SPX_PROGRAM_DESCRIPTION = "Fast, deterministic CLI tool for spec workflow management";
15860
+ function createCliProgram(options = {}) {
15861
+ const program = new Command();
15862
+ const io = {
15863
+ writeStdout: options.writeStdout ?? DEFAULT_CLI_IO.writeStdout,
15864
+ writeStderr: options.writeStderr ?? DEFAULT_CLI_IO.writeStderr,
15865
+ setExitCode: options.setExitCode ?? DEFAULT_CLI_IO.setExitCode,
15866
+ exit: options.exit ?? DEFAULT_CLI_IO.exit
15867
+ };
15868
+ program.name(SPX_PROGRAM_NAME).description(SPX_PROGRAM_DESCRIPTION).option(SPX_GLOBAL_OPTIONS.directory.flags, SPX_GLOBAL_OPTIONS.directory.description);
15869
+ if (options.version !== void 0) {
15870
+ program.version(options.version);
15871
+ }
15872
+ const invocation = createCliInvocation({
15873
+ readDirectoryOption: () => program.opts().directory,
15874
+ processCwd: options.processCwd ?? CONFIG_PROCESS_CWD.read,
15875
+ resolveProductDir,
15876
+ writeWarning: (warning) => {
15877
+ if (warning !== void 0) {
15878
+ io.writeStderr(`${warning}
15879
+ `);
15880
+ }
15881
+ },
15882
+ io
15883
+ });
15884
+ for (const domain of options.domains ?? CLI_DOMAINS) {
15885
+ domain.register(program, invocation);
15886
+ }
15887
+ return program;
15888
+ }
15889
+
14963
15890
  // src/cli.ts
14964
15891
  installLifecycle();
14965
15892
  var require3 = createRequire2(import.meta.url);
14966
15893
  var { version } = require3("../package.json");
14967
- var program = new Command();
14968
- program.name("spx").description("Fast, deterministic CLI tool for spec workflow management").version(version);
14969
- for (const domain of CLI_DOMAINS) {
14970
- domain.register(program);
14971
- }
14972
- program.parse();
15894
+ createCliProgram({ version }).parse();
14973
15895
  //# sourceMappingURL=cli.js.map