@outcomeeng/spx 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1110 -629
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -49,39 +49,45 @@ async function findSettingsFiles(root, visited = /* @__PURE__ */ new Set()) {
49
49
  }
50
50
  visited.add(normalizedRoot);
51
51
  try {
52
- const stats = await fs.stat(normalizedRoot);
53
- if (!stats.isDirectory()) {
54
- throw new Error(`Path is not a directory: ${normalizedRoot}`);
55
- }
56
- const entries = await fs.readdir(normalizedRoot, { withFileTypes: true });
57
- const results = [];
58
- for (const entry of entries) {
59
- const fullPath = path.join(normalizedRoot, entry.name);
60
- if (entry.isDirectory() && entry.name === ".claude") {
61
- const settingsPath = path.join(fullPath, "settings.local.json");
62
- if (await isValidSettingsFile(settingsPath)) {
63
- results.push(settingsPath);
64
- }
65
- }
66
- if (entry.isDirectory() && entry.name !== ".claude") {
67
- const subFiles = await findSettingsFiles(fullPath, visited);
68
- results.push(...subFiles);
69
- }
70
- }
71
- return results;
52
+ return await findSettingsFilesInDirectory(normalizedRoot, visited);
72
53
  } catch (error) {
73
- if (error instanceof Error) {
74
- if (error.message.includes("ENOENT")) {
75
- throw new Error(`Directory not found: ${normalizedRoot}`);
76
- }
77
- if (error.message.includes("EACCES")) {
78
- throw new Error(`Permission denied: ${normalizedRoot}`);
79
- }
80
- throw new Error(`Failed to search directory "${normalizedRoot}": ${error.message}`);
81
- }
82
- throw error;
54
+ throw discoveryError(normalizedRoot, error);
83
55
  }
84
56
  }
57
+ async function findSettingsFilesInDirectory(normalizedRoot, visited) {
58
+ const stats = await fs.stat(normalizedRoot);
59
+ if (!stats.isDirectory()) {
60
+ throw new Error(`Path is not a directory: ${normalizedRoot}`);
61
+ }
62
+ const entries = await fs.readdir(normalizedRoot, { withFileTypes: true });
63
+ const results = [];
64
+ for (const entry of entries) {
65
+ results.push(...await settingsFilesForEntry(normalizedRoot, entry, visited));
66
+ }
67
+ return results;
68
+ }
69
+ async function settingsFilesForEntry(normalizedRoot, entry, visited) {
70
+ if (!entry.isDirectory()) return [];
71
+ const fullPath = path.join(normalizedRoot, entry.name);
72
+ if (entry.name === ".claude") {
73
+ const settingsPath = path.join(fullPath, "settings.local.json");
74
+ return await isValidSettingsFile(settingsPath) ? [settingsPath] : [];
75
+ }
76
+ return findSettingsFiles(fullPath, visited);
77
+ }
78
+ function discoveryError(normalizedRoot, error) {
79
+ if (!(error instanceof Error)) return new Error(unknownErrorMessage(error));
80
+ if (error.message.includes("ENOENT")) return new Error(`Directory not found: ${normalizedRoot}`);
81
+ if (error.message.includes("EACCES")) return new Error(`Permission denied: ${normalizedRoot}`);
82
+ return new Error(`Failed to search directory "${normalizedRoot}": ${error.message}`);
83
+ }
84
+ function unknownErrorMessage(error) {
85
+ if (typeof error === "string") return error;
86
+ if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return error.toString();
87
+ if (typeof error === "symbol") return error.description ?? "Symbol()";
88
+ if (error === void 0) return "undefined";
89
+ return Object.prototype.toString.call(error);
90
+ }
85
91
  async function isValidSettingsFile(filePath) {
86
92
  try {
87
93
  await fs.access(filePath, fs.constants.R_OK);
@@ -801,6 +807,7 @@ function toErrorMessage(error) {
801
807
 
802
808
  // src/lib/claude/permissions/parser.ts
803
809
  import fs2 from "fs/promises";
810
+ var PERMISSION_PATTERN = /^([^(]+)\((.+)\)$/;
804
811
  async function parseSettingsFile(filePath) {
805
812
  try {
806
813
  const content = await fs2.readFile(filePath, "utf-8");
@@ -814,7 +821,7 @@ async function parseSettingsFile(filePath) {
814
821
  }
815
822
  }
816
823
  function parsePermission(raw, category) {
817
- const match = raw.match(/^([^(]+)\((.+)\)$/);
824
+ const match = PERMISSION_PATTERN.exec(raw);
818
825
  if (!match) {
819
826
  throw new Error(`Malformed permission string: "${raw}"`);
820
827
  }
@@ -1095,88 +1102,91 @@ async function createBackup(settingsPath) {
1095
1102
 
1096
1103
  // src/lib/claude/settings/reporter.ts
1097
1104
  function formatReport(result, previewOnly, globalSettingsPath, outputFile) {
1098
- const lines = [];
1099
- lines.push("Scanning for Claude Code settings files...");
1100
- lines.push("");
1101
- lines.push(`Found ${result.filesScanned} settings files`);
1102
- lines.push(` Processed: ${result.filesProcessed}`);
1103
- if (result.filesSkipped > 0) {
1104
- lines.push(` Skipped: ${result.filesSkipped} (no permissions)`);
1105
- }
1106
- lines.push("");
1107
1105
  const totalAdded = result.added.allow.length + result.added.deny.length + result.added.ask.length;
1108
- if (totalAdded > 0) {
1109
- lines.push(`Permissions to add: ${totalAdded}`);
1110
- if (result.added.allow.length > 0) {
1111
- lines.push("");
1112
- lines.push(" allow:");
1113
- for (const perm of result.added.allow) {
1114
- lines.push(` + ${perm}`);
1115
- }
1116
- }
1117
- if (result.added.deny.length > 0) {
1118
- lines.push("");
1119
- lines.push(" deny:");
1120
- for (const perm of result.added.deny) {
1121
- lines.push(` + ${perm}`);
1122
- }
1123
- }
1124
- if (result.added.ask.length > 0) {
1125
- lines.push("");
1126
- lines.push(" ask:");
1127
- for (const perm of result.added.ask) {
1128
- lines.push(` + ${perm}`);
1129
- }
1130
- }
1131
- } else {
1132
- lines.push("No new permissions to add (all permissions already in global settings)");
1133
- }
1134
- lines.push("");
1135
- if (result.subsumed.length > 0) {
1136
- lines.push(`Subsumed permissions removed: ${result.subsumed.length}`);
1137
- lines.push(" (narrower permissions replaced by broader ones)");
1138
- for (const perm of result.subsumed) {
1139
- lines.push(` - ${perm}`);
1140
- }
1141
- lines.push("");
1142
- }
1143
- if (result.conflictsResolved > 0) {
1144
- lines.push(`Conflicts resolved: ${result.conflictsResolved}`);
1145
- lines.push(" (permissions moved from allow to deny)");
1146
- lines.push("");
1147
- }
1148
- if (result.backupPath) {
1149
- lines.push(`Backup created: ${result.backupPath}`);
1150
- lines.push("");
1151
- }
1152
- lines.push("Summary:");
1153
- lines.push(` Files scanned: ${result.filesScanned}`);
1154
- lines.push(
1155
- ` Permissions added: ${result.added.allow.length} allow, ${result.added.deny.length} deny, ${result.added.ask.length} ask`
1156
- );
1157
- if (result.subsumed.length > 0) {
1158
- lines.push(` Subsumed removed: ${result.subsumed.length}`);
1159
- }
1160
- if (result.conflictsResolved > 0) {
1161
- lines.push(` Conflicts resolved: ${result.conflictsResolved}`);
1162
- }
1163
- lines.push("");
1106
+ return [
1107
+ ...headerLines(),
1108
+ ...filesSummaryLines(result),
1109
+ ...permissionLines(result, totalAdded),
1110
+ ...subsumptionLines(result),
1111
+ ...conflictLines(result),
1112
+ ...backupLines(result),
1113
+ ...summaryLines(result),
1114
+ ...finalStatusLines(result, previewOnly, globalSettingsPath, outputFile)
1115
+ ].join("\n");
1116
+ }
1117
+ function headerLines() {
1118
+ return ["Scanning for Claude Code settings files...", ""];
1119
+ }
1120
+ function filesSummaryLines(result) {
1121
+ const lines = [`Found ${result.filesScanned} settings files`, ` Processed: ${result.filesProcessed}`];
1122
+ if (result.filesSkipped > 0) lines.push(` Skipped: ${result.filesSkipped} (no permissions)`);
1123
+ return [...lines, ""];
1124
+ }
1125
+ function permissionLines(result, totalAdded) {
1126
+ if (totalAdded === 0) return ["No new permissions to add (all permissions already in global settings)", ""];
1127
+ return [
1128
+ `Permissions to add: ${totalAdded}`,
1129
+ ...permissionCategoryLines("allow", result.added.allow),
1130
+ ...permissionCategoryLines("deny", result.added.deny),
1131
+ ...permissionCategoryLines("ask", result.added.ask),
1132
+ ""
1133
+ ];
1134
+ }
1135
+ function permissionCategoryLines(label, permissions) {
1136
+ if (permissions.length === 0) return [];
1137
+ return ["", ` ${label}:`, ...permissions.map((permission) => ` + ${permission}`)];
1138
+ }
1139
+ function subsumptionLines(result) {
1140
+ if (result.subsumed.length === 0) return [];
1141
+ return [
1142
+ `Subsumed permissions removed: ${result.subsumed.length}`,
1143
+ " (narrower permissions replaced by broader ones)",
1144
+ ...result.subsumed.map((permission) => ` - ${permission}`),
1145
+ ""
1146
+ ];
1147
+ }
1148
+ function conflictLines(result) {
1149
+ if (result.conflictsResolved === 0) return [];
1150
+ return [`Conflicts resolved: ${result.conflictsResolved}`, " (permissions moved from allow to deny)", ""];
1151
+ }
1152
+ function backupLines(result) {
1153
+ return result.backupPath ? [`Backup created: ${result.backupPath}`, ""] : [];
1154
+ }
1155
+ function summaryLines(result) {
1156
+ return [
1157
+ "Summary:",
1158
+ ` Files scanned: ${result.filesScanned}`,
1159
+ ` Permissions added: ${result.added.allow.length} allow, ${result.added.deny.length} deny, ${result.added.ask.length} ask`,
1160
+ ...optionalSummaryLines(result),
1161
+ ""
1162
+ ];
1163
+ }
1164
+ function optionalSummaryLines(result) {
1165
+ return [
1166
+ ...result.subsumed.length > 0 ? [` Subsumed removed: ${result.subsumed.length}`] : [],
1167
+ ...result.conflictsResolved > 0 ? [` Conflicts resolved: ${result.conflictsResolved}`] : []
1168
+ ];
1169
+ }
1170
+ function finalStatusLines(result, previewOnly, globalSettingsPath, outputFile) {
1164
1171
  if (previewOnly) {
1165
- lines.push("\u2139\uFE0F Preview mode: No changes written");
1166
- lines.push("");
1167
- lines.push("To apply changes:");
1168
- lines.push(` \u2022 Modify global settings: spx claude settings consolidate --write`);
1169
- lines.push(` \u2022 Write to file: spx claude settings consolidate --output-file /path/to/file`);
1170
- } else if (outputFile) {
1171
- lines.push(`\u2713 Settings written to: ${result.outputPath || outputFile}`);
1172
- lines.push("");
1173
- lines.push("To apply to your global settings:");
1174
- lines.push(` \u2022 Review the file, then copy to: ${globalSettingsPath || "~/.claude/settings.json"}`);
1175
- lines.push(` \u2022 Or run: spx claude settings consolidate --write`);
1176
- } else {
1177
- lines.push(`\u2713 Global settings updated: ${globalSettingsPath || "~/.claude/settings.json"}`);
1172
+ return [
1173
+ "\u2139\uFE0F Preview mode: No changes written",
1174
+ "",
1175
+ "To apply changes:",
1176
+ " \u2022 Modify global settings: spx claude settings consolidate --write",
1177
+ " \u2022 Write to file: spx claude settings consolidate --output-file /path/to/file"
1178
+ ];
1178
1179
  }
1179
- return lines.join("\n");
1180
+ if (outputFile) {
1181
+ return [
1182
+ `\u2713 Settings written to: ${result.outputPath || outputFile}`,
1183
+ "",
1184
+ "To apply to your global settings:",
1185
+ ` \u2022 Review the file, then copy to: ${globalSettingsPath || "~/.claude/settings.json"}`,
1186
+ " \u2022 Or run: spx claude settings consolidate --write"
1187
+ ];
1188
+ }
1189
+ return [`\u2713 Global settings updated: ${globalSettingsPath || "~/.claude/settings.json"}`];
1180
1190
  }
1181
1191
 
1182
1192
  // src/lib/claude/settings/writer.ts
@@ -1403,7 +1413,7 @@ var COMPACT_MARKER = {
1403
1413
  FOUNDATION: "SPEC_TREE_FOUNDATION",
1404
1414
  CONTEXT: "SPEC_TREE_CONTEXT",
1405
1415
  TARGET_ATTRIBUTE: "target",
1406
- ESCAPED_TARGET_QUOTE: '\\"',
1416
+ ESCAPED_TARGET_QUOTE: String.raw`\"`,
1407
1417
  UNESCAPED_TARGET_QUOTE: '"'
1408
1418
  };
1409
1419
  var COMPACT_ERROR = {
@@ -1510,8 +1520,8 @@ function readTargetPath(value, startIndex) {
1510
1520
  return value.slice(startIndex, endIndex);
1511
1521
  }
1512
1522
  function isTargetPathCharacter(character) {
1513
- const code = character.charCodeAt(0);
1514
- return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || character === "." || character === "_" || character === "-" || 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 === "/");
1515
1525
  }
1516
1526
 
1517
1527
  // src/commands/compact/retrieve.ts
@@ -1598,6 +1608,18 @@ import { join as join5 } from "path";
1598
1608
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
1599
1609
  import { parse as parseYaml, parseDocument as parseYamlDocument, stringify as stringifyYaml } from "yaml";
1600
1610
 
1611
+ // src/lib/error-message.ts
1612
+ function toMessage(error) {
1613
+ if (error instanceof Error) return error.message;
1614
+ if (typeof error === "string") return error;
1615
+ const fallback = `[${typeof error}]`;
1616
+ try {
1617
+ return JSON.stringify(error) ?? fallback;
1618
+ } catch {
1619
+ return fallback;
1620
+ }
1621
+ }
1622
+
1601
1623
  // src/domains/agent-environment/config.ts
1602
1624
  var AGENT_ENVIRONMENT_SECTION = "agentEnvironment";
1603
1625
  var AGENT_RUNTIME = {
@@ -1794,15 +1816,15 @@ function validateRuntimes(raw) {
1794
1816
  }
1795
1817
  return { ok: true, value: runtimes };
1796
1818
  }
1797
- function validateRuntimeConfig(path7, raw, defaults4) {
1819
+ function validateRuntimeConfig(path7, raw, defaults5) {
1798
1820
  if (!isRecord(raw)) return { ok: false, error: `${path7} must be an object` };
1799
1821
  const unknown = rejectUnknownFields(path7, raw, AGENT_ENVIRONMENT_RUNTIME_CONFIG_ALLOWED_FIELDS);
1800
1822
  if (!unknown.ok) return unknown;
1801
1823
  const enabledRaw = raw[AGENT_ENVIRONMENT_CONFIG_FIELDS.ENABLED];
1802
- if (enabledRaw === void 0) return { ok: true, value: defaults4 };
1824
+ if (enabledRaw === void 0) return { ok: true, value: defaults5 };
1803
1825
  const enabled = validateBoolean(`${path7}.${AGENT_ENVIRONMENT_CONFIG_FIELDS.ENABLED}`, enabledRaw);
1804
1826
  if (!enabled.ok) return enabled;
1805
- return { ok: true, value: { ...defaults4, enabled: enabled.value } };
1827
+ return { ok: true, value: { ...defaults5, enabled: enabled.value } };
1806
1828
  }
1807
1829
  function validatePluginBootstrap(raw) {
1808
1830
  const sectionPath = `${AGENT_ENVIRONMENT_SECTION}.${AGENT_ENVIRONMENT_CONFIG_FIELDS.PLUGIN_BOOTSTRAP}`;
@@ -1866,8 +1888,8 @@ function validatePluginBootstrap(raw) {
1866
1888
  }
1867
1889
  };
1868
1890
  }
1869
- function validateEntryArray(path7, raw, validateEntry2, defaults4) {
1870
- if (raw === void 0) return { ok: true, value: defaults4 };
1891
+ function validateEntryArray(path7, raw, validateEntry2, defaults5) {
1892
+ if (raw === void 0) return { ok: true, value: defaults5 };
1871
1893
  if (!Array.isArray(raw)) return { ok: false, error: `${path7} must be an array` };
1872
1894
  const entries = [];
1873
1895
  for (const [index, entry] of raw.entries()) {
@@ -2050,6 +2072,82 @@ var agentEnvironmentConfigDescriptor = {
2050
2072
  validate
2051
2073
  };
2052
2074
 
2075
+ // src/domains/diagnose/facts.ts
2076
+ function isRecord2(value) {
2077
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2078
+ }
2079
+ function isNonEmptyString(value) {
2080
+ return typeof value === "string" && value.length > 0;
2081
+ }
2082
+ function isNonEmptyStringArray(value) {
2083
+ return Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString);
2084
+ }
2085
+ function validateMarketplaceIdentity(value, field) {
2086
+ if (!isRecord2(value) || !isNonEmptyString(value.name) || !isNonEmptyString(value.source)) {
2087
+ return { ok: false, error: `${field} must carry a non-empty \`name\` and \`source\`` };
2088
+ }
2089
+ return { ok: true, value: { name: value.name, source: value.source } };
2090
+ }
2091
+
2092
+ // src/domains/diagnose/config.ts
2093
+ var DIAGNOSE_SECTION = "diagnose";
2094
+ var DIAGNOSE_CONFIG_FIELDS = {
2095
+ SPX_FLOOR: "spxFloor",
2096
+ MARKETPLACE: "marketplace",
2097
+ EXPECTED_PLUGINS: "expectedPlugins",
2098
+ CHECKS: "checks"
2099
+ };
2100
+ var defaults = {};
2101
+ function validate2(value) {
2102
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2103
+ return { ok: false, error: `${DIAGNOSE_SECTION} section must be an object` };
2104
+ }
2105
+ const candidate = value;
2106
+ const resolved = {};
2107
+ const spxFloor = candidate[DIAGNOSE_CONFIG_FIELDS.SPX_FLOOR];
2108
+ if (spxFloor !== void 0) {
2109
+ if (!isNonEmptyString(spxFloor)) {
2110
+ return { ok: false, error: `${DIAGNOSE_SECTION}.${DIAGNOSE_CONFIG_FIELDS.SPX_FLOOR} must be a non-empty string` };
2111
+ }
2112
+ resolved.spxFloor = spxFloor;
2113
+ }
2114
+ const marketplace = candidate[DIAGNOSE_CONFIG_FIELDS.MARKETPLACE];
2115
+ if (marketplace !== void 0) {
2116
+ const result = validateMarketplaceIdentity(
2117
+ marketplace,
2118
+ `${DIAGNOSE_SECTION}.${DIAGNOSE_CONFIG_FIELDS.MARKETPLACE}`
2119
+ );
2120
+ if (!result.ok) return result;
2121
+ resolved.marketplace = result.value;
2122
+ }
2123
+ const expectedPlugins = candidate[DIAGNOSE_CONFIG_FIELDS.EXPECTED_PLUGINS];
2124
+ if (expectedPlugins !== void 0) {
2125
+ if (!isNonEmptyStringArray(expectedPlugins)) {
2126
+ return {
2127
+ ok: false,
2128
+ error: `${DIAGNOSE_SECTION}.${DIAGNOSE_CONFIG_FIELDS.EXPECTED_PLUGINS} must be a non-empty array of non-empty strings`
2129
+ };
2130
+ }
2131
+ resolved.expectedPlugins = expectedPlugins;
2132
+ }
2133
+ const checks = candidate[DIAGNOSE_CONFIG_FIELDS.CHECKS];
2134
+ if (checks !== void 0) {
2135
+ if (!isNonEmptyStringArray(checks)) {
2136
+ return {
2137
+ ok: false,
2138
+ error: `${DIAGNOSE_SECTION}.${DIAGNOSE_CONFIG_FIELDS.CHECKS} must be a non-empty array of non-empty strings`
2139
+ };
2140
+ }
2141
+ resolved.checks = checks;
2142
+ }
2143
+ return { ok: true, value: resolved };
2144
+ }
2145
+ var diagnoseConfigDescriptor = {
2146
+ section: DIAGNOSE_SECTION,
2147
+ defaults,
2148
+ validate: validate2
2149
+ };
2150
+
2053
2151
  // src/lib/spec-tree/config.ts
2054
2152
  var SPEC_TREE_KIND_CATEGORY_VALUES = {
2055
2153
  NODE: "node",
@@ -2229,7 +2327,7 @@ var SPEC_TREE_NODE_STATE = {
2229
2327
  };
2230
2328
  var SPEC_TREE_SECTION = SPEC_TREE_CONFIG.SECTION;
2231
2329
  function isKind(value) {
2232
- return Object.prototype.hasOwnProperty.call(KIND_REGISTRY, value);
2330
+ return Object.hasOwn(KIND_REGISTRY, value);
2233
2331
  }
2234
2332
  function buildDefaults() {
2235
2333
  return { kinds: { ...KIND_REGISTRY } };
@@ -2238,7 +2336,7 @@ function buildConfigFromKindNames(kindNames) {
2238
2336
  const entries = kindNames.map((kind) => [kind, KIND_REGISTRY[kind]]);
2239
2337
  return { kinds: Object.fromEntries(entries) };
2240
2338
  }
2241
- function validate2(value) {
2339
+ function validate3(value) {
2242
2340
  if (typeof value !== "object" || value === null) {
2243
2341
  return { ok: false, error: `${SPEC_TREE_SECTION} section must be an object` };
2244
2342
  }
@@ -2337,7 +2435,7 @@ function validateKindDefinitionMap(kindEntries) {
2337
2435
  var specTreeConfigDescriptor = {
2338
2436
  section: SPEC_TREE_SECTION,
2339
2437
  defaults: buildDefaults(),
2340
- validate: validate2
2438
+ validate: validate3
2341
2439
  };
2342
2440
 
2343
2441
  // src/lib/file-inclusion/adapters/eslint.ts
@@ -2513,11 +2611,11 @@ var DEFAULT_SCOPE_CONFIG = {
2513
2611
  var DEFAULT_TOOLS_CONFIG = Object.fromEntries(
2514
2612
  Object.entries(TOOL_DEFAULT_FLAGS).map(([name, flag]) => [name, { ignoreFlag: flag }])
2515
2613
  );
2516
- var defaults = {
2614
+ var defaults2 = {
2517
2615
  scope: DEFAULT_SCOPE_CONFIG,
2518
2616
  tools: DEFAULT_TOOLS_CONFIG
2519
2617
  };
2520
- function isRecord2(value) {
2618
+ function isRecord3(value) {
2521
2619
  return typeof value === "object" && value !== null && !Array.isArray(value);
2522
2620
  }
2523
2621
  function validateStringField(section, field, value) {
@@ -2540,7 +2638,7 @@ function rejectUnknownFields2(section, value, allowed) {
2540
2638
  return { ok: true, value: void 0 };
2541
2639
  }
2542
2640
  function validateScope(raw) {
2543
- if (!isRecord2(raw)) {
2641
+ if (!isRecord3(raw)) {
2544
2642
  return {
2545
2643
  ok: false,
2546
2644
  error: `${FILE_INCLUSION_SECTION}.${FILE_INCLUSION_CONFIG_FIELDS.SCOPE} must be an object`
@@ -2597,7 +2695,7 @@ function validateScope(raw) {
2597
2695
  };
2598
2696
  }
2599
2697
  function validateTools(raw) {
2600
- if (!isRecord2(raw)) {
2698
+ if (!isRecord3(raw)) {
2601
2699
  return {
2602
2700
  ok: false,
2603
2701
  error: `${FILE_INCLUSION_SECTION}.${FILE_INCLUSION_CONFIG_FIELDS.TOOLS} must be an object`
@@ -2612,7 +2710,7 @@ function validateTools(raw) {
2612
2710
  error: `${FILE_INCLUSION_SECTION}.${FILE_INCLUSION_CONFIG_FIELDS.TOOLS}.${toolName} is not a recognized tool`
2613
2711
  };
2614
2712
  }
2615
- if (!isRecord2(adapterRaw)) {
2713
+ if (!isRecord3(adapterRaw)) {
2616
2714
  return {
2617
2715
  ok: false,
2618
2716
  error: `${FILE_INCLUSION_SECTION}.${FILE_INCLUSION_CONFIG_FIELDS.TOOLS}.${toolName} must be an object`
@@ -2632,7 +2730,7 @@ function validateTools(raw) {
2632
2730
  }
2633
2731
  return { ok: true, value: tools };
2634
2732
  }
2635
- function validate3(value) {
2733
+ function validate4(value) {
2636
2734
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2637
2735
  return { ok: false, error: `${FILE_INCLUSION_SECTION} section must be an object` };
2638
2736
  }
@@ -2659,8 +2757,8 @@ function validate3(value) {
2659
2757
  }
2660
2758
  var fileInclusionConfigDescriptor = {
2661
2759
  section: FILE_INCLUSION_SECTION,
2662
- defaults,
2663
- validate: validate3
2760
+ defaults: defaults2,
2761
+ validate: validate4
2664
2762
  };
2665
2763
 
2666
2764
  // src/lib/precommit/config.ts
@@ -2669,7 +2767,7 @@ var PRECOMMIT_DEFAULTS = {
2669
2767
  sourceDirs: ["src/"],
2670
2768
  testPattern: ".test.ts"
2671
2769
  };
2672
- function validate4(value) {
2770
+ function validate5(value) {
2673
2771
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2674
2772
  return { ok: false, error: `${PRECOMMIT_SECTION} section must be an object` };
2675
2773
  }
@@ -2699,7 +2797,7 @@ function validate4(value) {
2699
2797
  var precommitConfigDescriptor = {
2700
2798
  section: PRECOMMIT_SECTION,
2701
2799
  defaults: PRECOMMIT_DEFAULTS,
2702
- validate: validate4
2800
+ validate: validate5
2703
2801
  };
2704
2802
 
2705
2803
  // src/config/primitives/path-filter.ts
@@ -2736,7 +2834,11 @@ function validatePathFilterConfig(raw, path7) {
2736
2834
  }
2737
2835
  var PATH_SEGMENT_SEPARATOR = "/";
2738
2836
  function normalizePathPrefix(value) {
2739
- return value.replace(/\/+$/, "");
2837
+ let end = value.length;
2838
+ while (end > 0 && value[end - 1] === PATH_SEGMENT_SEPARATOR) {
2839
+ end -= 1;
2840
+ }
2841
+ return value.slice(0, end);
2740
2842
  }
2741
2843
  function pathMatchesPrefix(path7, prefix) {
2742
2844
  const normalizedPath = normalizePathPrefix(path7);
@@ -2760,7 +2862,7 @@ var TESTING_CONFIG_FIELDS = {
2760
2862
  PASSING_SCOPE: "passingScope"
2761
2863
  };
2762
2864
  var DEFAULT_PASSING_SCOPE = resolveDefaultPassingScope();
2763
- var defaults2 = {
2865
+ var defaults3 = {
2764
2866
  passingScope: DEFAULT_PASSING_SCOPE
2765
2867
  };
2766
2868
  function resolveDefaultPassingScope() {
@@ -2773,14 +2875,14 @@ function resolveDefaultPassingScope() {
2773
2875
  }
2774
2876
  return result.value;
2775
2877
  }
2776
- function validate5(value) {
2878
+ function validate6(value) {
2777
2879
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2778
2880
  return { ok: false, error: `${TESTING_SECTION} section must be an object` };
2779
2881
  }
2780
2882
  const candidate = value;
2781
2883
  const passingScopeRaw = candidate[TESTING_CONFIG_FIELDS.PASSING_SCOPE];
2782
2884
  if (passingScopeRaw === void 0) {
2783
- return { ok: true, value: defaults2 };
2885
+ return { ok: true, value: defaults3 };
2784
2886
  }
2785
2887
  const passingScopeResult = validatePathFilterConfig(
2786
2888
  passingScopeRaw,
@@ -2796,8 +2898,8 @@ function validate5(value) {
2796
2898
  }
2797
2899
  var testingConfigDescriptor = {
2798
2900
  section: TESTING_SECTION,
2799
- defaults: defaults2,
2800
- validate: validate5
2901
+ defaults: defaults3,
2902
+ validate: validate6
2801
2903
  };
2802
2904
 
2803
2905
  // src/validation/literal/config.ts
@@ -2806,6 +2908,10 @@ var DEFAULT_MIN_STRING_LENGTH = 4;
2806
2908
  var DEFAULT_MIN_NUMBER_DIGITS = 4;
2807
2909
  var LEGACY_LITERAL_ALLOWLIST_FIELD = "allowlist";
2808
2910
  var LEGACY_LITERAL_ALLOWLIST_ERROR = "validation.literal.values.allowlist is no longer valid; move its contents up one level to validation.literal.values.{presets,include,exclude}";
2911
+ var LITERAL_STRING_LIST_FIELDS = {
2912
+ INCLUDE: "include",
2913
+ EXCLUDE: "exclude"
2914
+ };
2809
2915
  var PRESET_NAMES = {
2810
2916
  WEB: "web"
2811
2917
  };
@@ -2854,7 +2960,10 @@ var LITERAL_DEFAULTS = {
2854
2960
  minStringLength: DEFAULT_MIN_STRING_LENGTH,
2855
2961
  minNumberDigits: DEFAULT_MIN_NUMBER_DIGITS
2856
2962
  };
2857
- function validate6(value) {
2963
+ function isNonNegativeInteger(value) {
2964
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
2965
+ }
2966
+ function validate7(value) {
2858
2967
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2859
2968
  return { ok: false, error: `${LITERAL_SECTION} section must be an object` };
2860
2969
  }
@@ -2862,34 +2971,21 @@ function validate6(value) {
2862
2971
  if (candidate[LEGACY_LITERAL_ALLOWLIST_FIELD] !== void 0) {
2863
2972
  return { ok: false, error: LEGACY_LITERAL_ALLOWLIST_ERROR };
2864
2973
  }
2865
- const presets = candidate["presets"];
2866
- if (presets !== void 0) {
2867
- if (!Array.isArray(presets) || !presets.every((x) => typeof x === "string")) {
2868
- return { ok: false, error: `${LITERAL_SECTION}.presets must be an array of strings` };
2869
- }
2870
- for (const id of presets) {
2871
- if (!PRESET_REGISTRY.has(id)) {
2872
- return { ok: false, error: `${LITERAL_SECTION}.presets: unrecognized preset "${id}"` };
2873
- }
2874
- }
2875
- }
2876
- const include = candidate["include"];
2877
- if (include !== void 0 && (!Array.isArray(include) || !include.every((x) => typeof x === "string"))) {
2878
- return { ok: false, error: `${LITERAL_SECTION}.include must be an array of strings` };
2879
- }
2880
- const exclude = candidate["exclude"];
2881
- if (exclude !== void 0 && (!Array.isArray(exclude) || !exclude.every((x) => typeof x === "string"))) {
2882
- return { ok: false, error: `${LITERAL_SECTION}.exclude must be an array of strings` };
2883
- }
2974
+ const presets = readPresetList(candidate);
2975
+ if (!presets.ok) return presets;
2976
+ const include = readStringList(candidate, LITERAL_STRING_LIST_FIELDS.INCLUDE);
2977
+ if (!include.ok) return include;
2978
+ const exclude = readStringList(candidate, LITERAL_STRING_LIST_FIELDS.EXCLUDE);
2979
+ if (!exclude.ok) return exclude;
2884
2980
  const minStringLength = candidate["minStringLength"] ?? LITERAL_DEFAULTS.minStringLength;
2885
- if (typeof minStringLength !== "number" || !Number.isInteger(minStringLength) || minStringLength < 0) {
2981
+ if (!isNonNegativeInteger(minStringLength)) {
2886
2982
  return {
2887
2983
  ok: false,
2888
2984
  error: `${LITERAL_SECTION}.minStringLength must be a non-negative integer`
2889
2985
  };
2890
2986
  }
2891
2987
  const minNumberDigits = candidate["minNumberDigits"] ?? LITERAL_DEFAULTS.minNumberDigits;
2892
- if (typeof minNumberDigits !== "number" || !Number.isInteger(minNumberDigits) || minNumberDigits < 0) {
2988
+ if (!isNonNegativeInteger(minNumberDigits)) {
2893
2989
  return {
2894
2990
  ok: false,
2895
2991
  error: `${LITERAL_SECTION}.minNumberDigits must be a non-negative integer`
@@ -2898,18 +2994,40 @@ function validate6(value) {
2898
2994
  return {
2899
2995
  ok: true,
2900
2996
  value: {
2901
- presets,
2902
- include,
2903
- exclude,
2997
+ presets: presets.value,
2998
+ include: include.value,
2999
+ exclude: exclude.value,
2904
3000
  minStringLength,
2905
3001
  minNumberDigits
2906
3002
  }
2907
3003
  };
2908
3004
  }
3005
+ function readPresetList(candidate) {
3006
+ const presets = candidate["presets"];
3007
+ if (presets === void 0) return { ok: true, value: void 0 };
3008
+ if (!isStringArray(presets)) {
3009
+ return { ok: false, error: `${LITERAL_SECTION}.presets must be an array of strings` };
3010
+ }
3011
+ for (const id of presets) {
3012
+ if (!PRESET_REGISTRY.has(id)) {
3013
+ return { ok: false, error: `${LITERAL_SECTION}.presets: unrecognized preset "${id}"` };
3014
+ }
3015
+ }
3016
+ return { ok: true, value: presets };
3017
+ }
3018
+ function readStringList(candidate, field) {
3019
+ const value = candidate[field];
3020
+ if (value === void 0) return { ok: true, value: void 0 };
3021
+ if (!isStringArray(value)) return { ok: false, error: `${LITERAL_SECTION}.${field} must be an array of strings` };
3022
+ return { ok: true, value };
3023
+ }
3024
+ function isStringArray(value) {
3025
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
3026
+ }
2909
3027
  var literalConfigDescriptor = {
2910
3028
  section: LITERAL_SECTION,
2911
3029
  defaults: LITERAL_DEFAULTS,
2912
- validate: validate6
3030
+ validate: validate7
2913
3031
  };
2914
3032
 
2915
3033
  // src/validation/config/descriptor.ts
@@ -2925,9 +3043,10 @@ var VALIDATION_PATH_TOOL_SUBSECTIONS = {
2925
3043
  CIRCULAR: "circular",
2926
3044
  KNIP: "knip",
2927
3045
  MARKDOWN: "markdown",
2928
- LITERAL: "literal"
3046
+ LITERAL: "literal",
3047
+ FORMATTING: "formatting"
2929
3048
  };
2930
- var defaults3 = {
3049
+ var defaults4 = {
2931
3050
  paths: {},
2932
3051
  literal: {
2933
3052
  enabled: true,
@@ -2967,7 +3086,7 @@ function validateLiteral(raw) {
2967
3086
  };
2968
3087
  }
2969
3088
  const candidate = raw;
2970
- const enabledRaw = candidate[VALIDATION_ENABLED_FIELD] ?? defaults3.literal.enabled;
3089
+ const enabledRaw = candidate[VALIDATION_ENABLED_FIELD] ?? defaults4.literal.enabled;
2971
3090
  if (typeof enabledRaw !== "boolean") {
2972
3091
  return {
2973
3092
  ok: false,
@@ -2992,7 +3111,7 @@ function validateKnip(raw) {
2992
3111
  };
2993
3112
  }
2994
3113
  const candidate = raw;
2995
- const enabledRaw = candidate[VALIDATION_ENABLED_FIELD] ?? defaults3.knip.enabled;
3114
+ const enabledRaw = candidate[VALIDATION_ENABLED_FIELD] ?? defaults4.knip.enabled;
2996
3115
  if (typeof enabledRaw !== "boolean") {
2997
3116
  return {
2998
3117
  ok: false,
@@ -3001,7 +3120,7 @@ function validateKnip(raw) {
3001
3120
  }
3002
3121
  return { ok: true, value: { enabled: enabledRaw } };
3003
3122
  }
3004
- function validate7(value) {
3123
+ function validate8(value) {
3005
3124
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
3006
3125
  return { ok: false, error: `${VALIDATION_SECTION} section must be an object` };
3007
3126
  }
@@ -3019,8 +3138,8 @@ function validate7(value) {
3019
3138
  }
3020
3139
  var validationConfigDescriptor = {
3021
3140
  section: VALIDATION_SECTION,
3022
- defaults: defaults3,
3023
- validate: validate7
3141
+ defaults: defaults4,
3142
+ validate: validate8
3024
3143
  };
3025
3144
 
3026
3145
  // src/config/registry.ts
@@ -3030,7 +3149,8 @@ var productionRegistry = [
3030
3149
  testingConfigDescriptor,
3031
3150
  fileInclusionConfigDescriptor,
3032
3151
  precommitConfigDescriptor,
3033
- agentEnvironmentConfigDescriptor
3152
+ agentEnvironmentConfigDescriptor,
3153
+ diagnoseConfigDescriptor
3034
3154
  ];
3035
3155
 
3036
3156
  // src/config/descriptor-digest.ts
@@ -3328,7 +3448,7 @@ function setNested(target, path7, value) {
3328
3448
  cursor = fresh;
3329
3449
  }
3330
3450
  }
3331
- cursor[path7[path7.length - 1]] = value;
3451
+ cursor[path7.at(-1)] = value;
3332
3452
  }
3333
3453
  function validateParsedSections(filename, parsed) {
3334
3454
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
@@ -3339,9 +3459,6 @@ function validateParsedSections(filename, parsed) {
3339
3459
  function isFileNotFound(error) {
3340
3460
  return error instanceof Error && "code" in error && error.code === "ENOENT";
3341
3461
  }
3342
- function toMessage(error) {
3343
- return error instanceof Error ? error.message : String(error);
3344
- }
3345
3462
 
3346
3463
  // src/commands/config/defaults.ts
3347
3464
  function defaultsCommand(options, deps) {
@@ -3576,12 +3693,6 @@ var CHECK_NAME = {
3576
3693
  SESSION_STORE: "session-store",
3577
3694
  MARKETPLACE_INSTALL: "marketplace-install"
3578
3695
  };
3579
- function isRecord3(value) {
3580
- return typeof value === "object" && value !== null && !Array.isArray(value);
3581
- }
3582
- function isNonEmptyString(value) {
3583
- return typeof value === "string" && value.length > 0;
3584
- }
3585
3696
  function validateChecks(raw, available) {
3586
3697
  if (!Array.isArray(raw) || raw.length === 0) {
3587
3698
  return { ok: false, error: "manifest `checks` must be a non-empty array of check names" };
@@ -3595,12 +3706,6 @@ function validateChecks(raw, available) {
3595
3706
  }
3596
3707
  return { ok: true, value: raw };
3597
3708
  }
3598
- function validateMarketplace2(raw) {
3599
- if (!isRecord3(raw) || !isNonEmptyString(raw.name) || !isNonEmptyString(raw.source)) {
3600
- return { ok: false, error: "manifest `marketplace` must carry a non-empty `name` and `source`" };
3601
- }
3602
- return { ok: true, value: { name: raw.name, source: raw.source } };
3603
- }
3604
3709
  function parseManifest(rawJson, availableChecks) {
3605
3710
  let parsed;
3606
3711
  try {
@@ -3608,7 +3713,7 @@ function parseManifest(rawJson, availableChecks) {
3608
3713
  } catch (error) {
3609
3714
  return { ok: false, error: `manifest is not valid JSON: ${error.message}` };
3610
3715
  }
3611
- if (!isRecord3(parsed)) {
3716
+ if (!isRecord2(parsed)) {
3612
3717
  return { ok: false, error: "manifest must be a JSON object" };
3613
3718
  }
3614
3719
  const checks = validateChecks(parsed.checks, new Set(availableChecks));
@@ -3621,9 +3726,9 @@ function parseManifest(rawJson, availableChecks) {
3621
3726
  manifest.spxFloor = parsed.spx_floor;
3622
3727
  }
3623
3728
  if (checks.value.includes(CHECK_NAME.MARKETPLACE_INSTALL)) {
3624
- const marketplace = validateMarketplace2(parsed.marketplace);
3729
+ const marketplace = validateMarketplaceIdentity(parsed.marketplace, "manifest `marketplace`");
3625
3730
  if (!marketplace.ok) return marketplace;
3626
- if (!Array.isArray(parsed.expected_plugins) || !parsed.expected_plugins.every(isNonEmptyString)) {
3731
+ if (!isNonEmptyStringArray(parsed.expected_plugins)) {
3627
3732
  return { ok: false, error: "manifest selects `marketplace-install` but carries no `expected_plugins`" };
3628
3733
  }
3629
3734
  manifest.marketplace = marketplace.value;
@@ -3738,21 +3843,70 @@ function renderReport(report2, format2, options) {
3738
3843
  return format2 === DIAGNOSE_FORMAT.JSON ? renderReportJson(report2) : renderReportText(report2, options);
3739
3844
  }
3740
3845
 
3846
+ // src/domains/diagnose/resolve.ts
3847
+ var CHECK_NAMES = new Set(Object.values(CHECK_NAME));
3848
+ function isCheckName(value) {
3849
+ return CHECK_NAMES.has(value);
3850
+ }
3851
+ function resolveDiagnoseFacts(options) {
3852
+ if (options.manifest !== void 0) {
3853
+ return { ok: true, value: options.manifest };
3854
+ }
3855
+ const { config, availableChecks } = options;
3856
+ const configuredChecks = config.checks;
3857
+ let checks;
3858
+ if (configuredChecks === void 0) {
3859
+ checks = availableChecks;
3860
+ } else {
3861
+ const available = new Set(availableChecks);
3862
+ const unavailable = configuredChecks.filter((name) => !isCheckName(name) || !available.has(name));
3863
+ if (unavailable.length > 0) {
3864
+ return {
3865
+ ok: false,
3866
+ error: `diagnose config \`checks\` names checks not available in this build: ${unavailable.join(", ")}`
3867
+ };
3868
+ }
3869
+ checks = configuredChecks.filter(isCheckName);
3870
+ }
3871
+ return {
3872
+ ok: true,
3873
+ value: {
3874
+ checks,
3875
+ spxFloor: config.spxFloor,
3876
+ marketplace: config.marketplace,
3877
+ expectedPlugins: config.expectedPlugins
3878
+ }
3879
+ };
3880
+ }
3881
+
3741
3882
  // src/commands/diagnose/index.ts
3742
- async function diagnoseCommand(options) {
3883
+ async function resolveDiagnoseConfig(productDir) {
3884
+ const loaded = await resolveConfig(productDir, [diagnoseConfigDescriptor]);
3885
+ if (!loaded.ok) return loaded;
3886
+ return { ok: true, value: loaded.value[diagnoseConfigDescriptor.section] };
3887
+ }
3888
+ async function readManifest(fs8, manifestPath, availableChecks) {
3743
3889
  let raw;
3744
3890
  try {
3745
- raw = await options.fs.readFile(options.manifestPath);
3891
+ raw = await fs8.readFile(manifestPath);
3746
3892
  } catch (error) {
3747
- return {
3748
- ok: false,
3749
- error: `cannot read diagnose manifest at ${options.manifestPath}: ${error.message}`
3750
- };
3893
+ return { ok: false, error: `cannot read diagnose manifest at ${manifestPath}: ${error.message}` };
3751
3894
  }
3895
+ return parseManifest(raw, availableChecks);
3896
+ }
3897
+ async function diagnoseCommand(options) {
3752
3898
  const availableChecks = Object.keys(options.registry);
3753
- const manifest = parseManifest(raw, availableChecks);
3754
- if (!manifest.ok) return manifest;
3755
- const report2 = await runDiagnose(manifest.value, options.registry);
3899
+ const manifest = options.manifestPath === void 0 ? void 0 : await readManifest(options.fs, options.manifestPath, availableChecks);
3900
+ if (manifest !== void 0 && !manifest.ok) return manifest;
3901
+ const config = manifest === void 0 ? await resolveDiagnoseConfig(options.productDir) : { ok: true, value: {} };
3902
+ if (!config.ok) return config;
3903
+ const resolved = resolveDiagnoseFacts({
3904
+ manifest: manifest?.value,
3905
+ config: config.value,
3906
+ availableChecks
3907
+ });
3908
+ if (!resolved.ok) return resolved;
3909
+ const report2 = await runDiagnose(resolved.value, options.registry);
3756
3910
  if (!report2.ok) return report2;
3757
3911
  return {
3758
3912
  ok: true,
@@ -3857,9 +4011,8 @@ function nonEmptyString(value) {
3857
4011
  // src/domains/worktree/occupancy-store.ts
3858
4012
  import { join as join6 } from "path";
3859
4013
  var OCCUPANCY_STATUS = {
3860
- UNCLAIMED: "unclaimed",
3861
- OCCUPIED: "occupied",
3862
- STALE: "stale"
4014
+ FREE: "free",
4015
+ RUNNING: "running"
3863
4016
  };
3864
4017
  var OCCUPANCY_CLAIM = {
3865
4018
  FILE_EXTENSION: ".claim",
@@ -3889,13 +4042,13 @@ function claimTempFilePath(claimPath, writeToken) {
3889
4042
  return { ok: true, value: `${claimPath}.${validated.value}${OCCUPANCY_CLAIM.TEMP_EXTENSION}` };
3890
4043
  }
3891
4044
  function classifyOccupancy(claim, probe) {
3892
- if (claim === void 0) return OCCUPANCY_STATUS.UNCLAIMED;
3893
- if (claim.host !== probe.currentHost()) return OCCUPANCY_STATUS.STALE;
3894
- if (!probe.isAlive(claim.pid)) return OCCUPANCY_STATUS.STALE;
4045
+ if (claim === void 0) return OCCUPANCY_STATUS.FREE;
4046
+ if (claim.host !== probe.currentHost()) return OCCUPANCY_STATUS.FREE;
4047
+ if (!probe.isAlive(claim.pid)) return OCCUPANCY_STATUS.FREE;
4048
+ if (claim.startedAt === unreadableStartedAt(claim.pid)) return OCCUPANCY_STATUS.RUNNING;
3895
4049
  const liveStartTime = probe.startTimeOf(claim.pid);
3896
- if (claim.startedAt === unreadableStartedAt(claim.pid)) return OCCUPANCY_STATUS.OCCUPIED;
3897
- if (liveStartTime !== void 0 && liveStartTime !== claim.startedAt) return OCCUPANCY_STATUS.STALE;
3898
- return OCCUPANCY_STATUS.OCCUPIED;
4050
+ if (liveStartTime !== void 0 && liveStartTime !== claim.startedAt) return OCCUPANCY_STATUS.FREE;
4051
+ return OCCUPANCY_STATUS.RUNNING;
3899
4052
  }
3900
4053
  function unreadableStartedAt(pid) {
3901
4054
  return `${OCCUPANCY_CLAIM.UNREADABLE_STARTED_AT_PREFIX}${pid}`;
@@ -3937,11 +4090,6 @@ async function removeClaim(worktreesDir, name, options) {
3937
4090
  return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_REMOVE_FAILED, toErrorMessage2(error)) };
3938
4091
  }
3939
4092
  }
3940
- async function readOccupancy(worktreesDir, name, probe, options) {
3941
- const claimResult = await readClaim(worktreesDir, name, options);
3942
- if (!claimResult.ok) return claimResult;
3943
- return { ok: true, value: classifyOccupancy(claimResult.value, probe) };
3944
- }
3945
4093
  function serializeClaim(record6) {
3946
4094
  return JSON.stringify({
3947
4095
  sessionId: record6.sessionId,
@@ -3969,7 +4117,10 @@ function formatOccupancyError(code, detail) {
3969
4117
  return `${code}${ERROR_DETAIL_SEPARATOR2}${detail}`;
3970
4118
  }
3971
4119
  function toErrorMessage2(error) {
3972
- return error instanceof Error ? error.message : String(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";
3973
4124
  }
3974
4125
 
3975
4126
  // src/domains/worktree/worktree-name.ts
@@ -4119,27 +4270,25 @@ var defaultWorktreePoolProbe = {
4119
4270
  errored: true,
4120
4271
  bareRepository: false,
4121
4272
  linkedWorktrees: false,
4122
- staleClaim: false
4273
+ running: 0,
4274
+ free: 0
4123
4275
  };
4276
+ const root = await detectGitCommonDirProductRoot();
4124
4277
  const facts = await gatherGitFacts();
4125
- if (!facts?.worktreeListRead) return errored;
4278
+ if (!root.isGitRepo || !facts?.worktreeListRead) return errored;
4126
4279
  const bareRepository = facts.commonDirIsBare;
4127
4280
  const paths = facts.worktreeRoots;
4128
4281
  const linkedWorktrees = !bareRepository && paths.length > 1;
4129
- const spx = resolveSpx();
4130
- if (spx === null) return errored;
4131
- let staleClaim = false;
4282
+ const worktreesDir = worktreesScopeDir(root.productDir);
4283
+ let running = 0;
4284
+ let free = 0;
4132
4285
  for (const path7 of paths) {
4133
- const status = await runCapture(spx, ["worktree", "status", path7, "--format", "json"]);
4134
- if (!status.ok) return errored;
4135
- const occupancy = worktreeStatusOf(status.stdout);
4136
- if (occupancy === null) return errored;
4137
- if (occupancy === OCCUPANCY_STATUS.STALE) {
4138
- staleClaim = true;
4139
- break;
4140
- }
4286
+ const claim = await readClaim(worktreesDir, worktreeClaimName(path7), { fs: defaultOccupancyFileSystem });
4287
+ if (!claim.ok) return errored;
4288
+ if (classifyOccupancy(claim.value, defaultProcessTable) === OCCUPANCY_STATUS.RUNNING) running += 1;
4289
+ else free += 1;
4141
4290
  }
4142
- return { errored: false, bareRepository, linkedWorktrees, staleClaim };
4291
+ return { errored: false, bareRepository, linkedWorktrees, running, free };
4143
4292
  }
4144
4293
  };
4145
4294
  var defaultSessionEnvironmentProbe = {
@@ -4148,22 +4297,21 @@ var defaultSessionEnvironmentProbe = {
4148
4297
  const sessionIdentity = resolveAgentSessionId(process.env) !== void 0;
4149
4298
  const spx = resolveSpx();
4150
4299
  if (spx === null) {
4151
- return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false, roundTripStale: false };
4300
+ return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false };
4152
4301
  }
4153
4302
  const status = await runCapture(spx, ["worktree", "status", "--format", "json"]);
4154
4303
  if (!status.ok) {
4155
- return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false, roundTripStale: false };
4304
+ return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false };
4156
4305
  }
4157
4306
  const occupancy = worktreeStatusOf(status.stdout);
4158
4307
  if (occupancy === null) {
4159
- return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false, roundTripStale: false };
4308
+ return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false };
4160
4309
  }
4161
4310
  return {
4162
4311
  errored: false,
4163
4312
  hookPresent,
4164
4313
  sessionIdentity,
4165
- worktreeClaimed: occupancy === OCCUPANCY_STATUS.OCCUPIED,
4166
- roundTripStale: occupancy === OCCUPANCY_STATUS.STALE
4314
+ worktreeClaimed: occupancy === OCCUPANCY_STATUS.RUNNING
4167
4315
  };
4168
4316
  }
4169
4317
  };
@@ -4177,7 +4325,7 @@ async function claimedSessionIds() {
4177
4325
  for (const path7 of facts.worktreeRoots) {
4178
4326
  const claim = await readClaim(worktreesDir, worktreeClaimName(path7), { fs: defaultOccupancyFileSystem });
4179
4327
  if (!claim.ok || claim.value === void 0) continue;
4180
- if (classifyOccupancy(claim.value, defaultProcessTable) === OCCUPANCY_STATUS.OCCUPIED) {
4328
+ if (classifyOccupancy(claim.value, defaultProcessTable) === OCCUPANCY_STATUS.RUNNING) {
4181
4329
  sessionIds.add(normalizeAgentSessionToken(claim.value.sessionId));
4182
4330
  }
4183
4331
  }
@@ -4392,8 +4540,7 @@ function record2(verdict, bucket, reading) {
4392
4540
  readings: {
4393
4541
  hook: String(reading.hookPresent),
4394
4542
  identity: String(reading.sessionIdentity),
4395
- claimed: String(reading.worktreeClaimed),
4396
- stale: String(reading.roundTripStale)
4543
+ claimed: String(reading.worktreeClaimed)
4397
4544
  },
4398
4545
  remediation: REMEDIATION2[verdict]
4399
4546
  };
@@ -4402,7 +4549,7 @@ function classifySessionEnvironment(reading) {
4402
4549
  if (!reading.hookPresent) {
4403
4550
  return record2(SESSION_ENVIRONMENT_VERDICT.NOT_APPLICABLE, VERDICT_BUCKET.NOT_APPLICABLE, reading);
4404
4551
  }
4405
- if (reading.errored || reading.roundTripStale) {
4552
+ if (reading.errored) {
4406
4553
  return record2(SESSION_ENVIRONMENT_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading);
4407
4554
  }
4408
4555
  if (reading.sessionIdentity && reading.worktreeClaimed) {
@@ -4428,7 +4575,7 @@ var SESSION_STORE_VERDICT = {
4428
4575
  };
4429
4576
  var REMEDIATION3 = {
4430
4577
  [SESSION_STORE_VERDICT.CONSISTENT]: "Session store is consistent; no action needed.",
4431
- [SESSION_STORE_VERDICT.ORPHANED_CLAIMS]: "Release or reclaim doing sessions whose backing worktree is stale or absent (spx session release).",
4578
+ [SESSION_STORE_VERDICT.ORPHANED_CLAIMS]: "Release or reclaim doing sessions whose backing worktree reads free or is absent (spx session release).",
4432
4579
  [SESSION_STORE_VERDICT.UNKNOWN]: "Re-run diagnose; if it persists, inspect spx session list and spx worktree status."
4433
4580
  };
4434
4581
  function record3(verdict, bucket, reading) {
@@ -4458,6 +4605,7 @@ function sessionStoreRunner(probe) {
4458
4605
  // src/domains/diagnose/checks/spx-reachability.ts
4459
4606
  var SPX_REACHABILITY_VERDICT = {
4460
4607
  REACHABLE: "reachable",
4608
+ PRESENT: "present",
4461
4609
  BELOW_FLOOR: "below-floor",
4462
4610
  UNREACHABLE: "unreachable",
4463
4611
  UNKNOWN: "unknown"
@@ -4508,9 +4656,10 @@ function meetsFloor(version2, floor) {
4508
4656
  }
4509
4657
  var REMEDIATION4 = {
4510
4658
  [SPX_REACHABILITY_VERDICT.REACHABLE]: "spx is on PATH at or above the required floor; no action needed.",
4659
+ [SPX_REACHABILITY_VERDICT.PRESENT]: "spx is on PATH; no version floor is configured to compare against, so only presence is reported.",
4511
4660
  [SPX_REACHABILITY_VERDICT.BELOW_FLOOR]: "Update spx to at least the required floor (pnpm add -g @outcomeeng/spx).",
4512
4661
  [SPX_REACHABILITY_VERDICT.UNREACHABLE]: "Install spx and ensure it resolves on PATH (pnpm add -g @outcomeeng/spx).",
4513
- [SPX_REACHABILITY_VERDICT.UNKNOWN]: "Re-run diagnose; if it persists, verify the manifest carries a valid spx_floor, that spx is on PATH, and that spx --version reports a semver version."
4662
+ [SPX_REACHABILITY_VERDICT.UNKNOWN]: "Re-run diagnose; if it persists, verify the configured or manifest-supplied spx_floor is a valid semver, that spx is on PATH, and that spx --version reports a semver version."
4514
4663
  };
4515
4664
  function record4(verdict, bucket, reading, floor) {
4516
4665
  return {
@@ -4526,12 +4675,15 @@ function record4(verdict, bucket, reading, floor) {
4526
4675
  };
4527
4676
  }
4528
4677
  function classifySpxReachability(reading, floor) {
4529
- if (reading.errored || floor === void 0) {
4678
+ if (reading.errored) {
4530
4679
  return record4(SPX_REACHABILITY_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading, floor);
4531
4680
  }
4532
4681
  if (reading.resolvedPath === null) {
4533
4682
  return record4(SPX_REACHABILITY_VERDICT.UNREACHABLE, VERDICT_BUCKET.BROKEN, reading, floor);
4534
4683
  }
4684
+ if (floor === void 0) {
4685
+ return record4(SPX_REACHABILITY_VERDICT.PRESENT, VERDICT_BUCKET.HEALTHY, reading, floor);
4686
+ }
4535
4687
  if (reading.version === null) {
4536
4688
  return record4(SPX_REACHABILITY_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading, floor);
4537
4689
  }
@@ -4548,13 +4700,11 @@ function spxReachabilityRunner(probe) {
4548
4700
  // src/domains/diagnose/checks/worktree-pool.ts
4549
4701
  var WORKTREE_POOL_VERDICT = {
4550
4702
  COMPLIANT: "compliant",
4551
- STALE_CLAIMS: "stale-claims",
4552
4703
  NON_COMPLIANT: "non-compliant",
4553
4704
  UNKNOWN: "unknown"
4554
4705
  };
4555
4706
  var REMEDIATION5 = {
4556
4707
  [WORKTREE_POOL_VERDICT.COMPLIANT]: "Worktree layout is compliant; no action needed.",
4557
- [WORKTREE_POOL_VERDICT.STALE_CLAIMS]: "Release stale worktree claims (spx worktree release) or remove dead worktrees.",
4558
4708
  [WORKTREE_POOL_VERDICT.NON_COMPLIANT]: "Linked worktrees require a bare-repository pool; convert the layout or remove the linked worktrees.",
4559
4709
  [WORKTREE_POOL_VERDICT.UNKNOWN]: "Re-run diagnose; if it persists, inspect git worktree list and spx worktree status."
4560
4710
  };
@@ -4566,7 +4716,8 @@ function record5(verdict, bucket, reading) {
4566
4716
  readings: {
4567
4717
  bare: String(reading.bareRepository),
4568
4718
  linked: String(reading.linkedWorktrees),
4569
- stale: String(reading.staleClaim)
4719
+ running: String(reading.running),
4720
+ free: String(reading.free)
4570
4721
  },
4571
4722
  remediation: REMEDIATION5[verdict]
4572
4723
  };
@@ -4578,9 +4729,6 @@ function classifyWorktreePool(reading) {
4578
4729
  if (!reading.bareRepository && reading.linkedWorktrees) {
4579
4730
  return record5(WORKTREE_POOL_VERDICT.NON_COMPLIANT, VERDICT_BUCKET.BROKEN, reading);
4580
4731
  }
4581
- if (reading.staleClaim) {
4582
- return record5(WORKTREE_POOL_VERDICT.STALE_CLAIMS, VERDICT_BUCKET.DEGRADED, reading);
4583
- }
4584
4732
  return record5(WORKTREE_POOL_VERDICT.COMPLIANT, VERDICT_BUCKET.HEALTHY, reading);
4585
4733
  }
4586
4734
  function worktreePoolRunner(probe) {
@@ -4598,7 +4746,7 @@ var DEL_CHAR_CODE = 127;
4598
4746
  var HEX_RADIX = 16;
4599
4747
  var HEX_PAD = 2;
4600
4748
  function formatHexEscape(code) {
4601
- return `\\x${code.toString(HEX_RADIX).padStart(HEX_PAD, "0")}`;
4749
+ return String.raw`\x${code.toString(HEX_RADIX).padStart(HEX_PAD, "0")}`;
4602
4750
  }
4603
4751
  function nonStringSentinel(type) {
4604
4752
  return `<non-string:${type}>`;
@@ -4639,7 +4787,7 @@ var DIAGNOSE_CLI = {
4639
4787
  COLOR_FLAG: "--color",
4640
4788
  NO_COLOR_FLAG: "--no-color"
4641
4789
  };
4642
- var DIAGNOSE_DOMAIN_DESCRIPTION = "Run deterministic environment-diagnostics checks from a declarative manifest";
4790
+ var DIAGNOSE_DOMAIN_DESCRIPTION = "Run deterministic environment-diagnostics checks, resolving facts from spx.config or a --manifest";
4643
4791
  var DEFAULT_REGISTRY = {
4644
4792
  [CHECK_NAME.SPX_REACHABILITY]: spxReachabilityRunner(defaultSpxReachabilityProbe),
4645
4793
  [CHECK_NAME.SESSION_ENVIRONMENT]: sessionEnvironmentRunner(defaultSessionEnvironmentProbe),
@@ -4655,11 +4803,15 @@ var diagnoseDomain = {
4655
4803
  name: DIAGNOSE_CLI.COMMAND,
4656
4804
  description: DIAGNOSE_DOMAIN_DESCRIPTION,
4657
4805
  register: (program2) => {
4658
- program2.command(DIAGNOSE_CLI.COMMAND).description(DIAGNOSE_DOMAIN_DESCRIPTION).requiredOption(`${DIAGNOSE_CLI.MANIFEST_FLAG} <path>`, "Path to the declarative diagnose manifest").addOption(
4806
+ program2.command(DIAGNOSE_CLI.COMMAND).description(DIAGNOSE_DOMAIN_DESCRIPTION).option(
4807
+ `${DIAGNOSE_CLI.MANIFEST_FLAG} <path>`,
4808
+ "Path to a declarative diagnose manifest that fully instruments the diagnosis"
4809
+ ).addOption(
4659
4810
  new Option(`${DIAGNOSE_CLI.FORMAT_FLAG} <format>`, "Output format").choices([DIAGNOSE_FORMAT.TEXT, DIAGNOSE_FORMAT.JSON]).default(DIAGNOSE_FORMAT.TEXT)
4660
4811
  ).addOption(new Option(`${DIAGNOSE_CLI.COLOR_FLAG}`, "Force colored output")).addOption(new Option(`${DIAGNOSE_CLI.NO_COLOR_FLAG}`, "Disable colored output")).action(async (options) => {
4661
4812
  const result = await diagnoseCommand({
4662
4813
  manifestPath: options.manifest,
4814
+ productDir: process.cwd(),
4663
4815
  format: options.format,
4664
4816
  color: resolveColorChoice({
4665
4817
  flag: options.color,
@@ -4682,7 +4834,7 @@ import { appendFile as nodeAppendFile } from "fs/promises";
4682
4834
  // src/domains/worktree/controlling-process.ts
4683
4835
  var CONTROLLING_PID_ENV = "SPX_WORKTREE_CONTROLLING_PID";
4684
4836
  var AGENT_RUNTIME_NAMES = ["claude", "codex"];
4685
- var AGENT_COMMAND_PATTERN = new RegExp(`\\b(?:${AGENT_RUNTIME_NAMES.join("|")})\\b`, "i");
4837
+ var AGENT_COMMAND_PATTERN = new RegExp(String.raw`\b(?:${AGENT_RUNTIME_NAMES.join("|")})\b`, "i");
4686
4838
  var CONTROLLING_PROCESS_ERROR = {
4687
4839
  UNRESOLVED: "worktree controlling process could not be resolved"
4688
4840
  };
@@ -5159,6 +5311,84 @@ function createJournal(backend, identity) {
5159
5311
  };
5160
5312
  }
5161
5313
 
5314
+ // src/lib/github-snapshot-sink/gh-api-runner.ts
5315
+ import { execa as execa5 } from "execa";
5316
+ var GH_API = {
5317
+ executable: "gh"
5318
+ };
5319
+ var runGhApi = async (args, options) => {
5320
+ const result = await execa5(GH_API.executable, [...args], {
5321
+ ...options?.input === void 0 ? {} : { input: options.input }
5322
+ });
5323
+ return { stdout: typeof result.stdout === "string" ? result.stdout : "" };
5324
+ };
5325
+
5326
+ // src/lib/github-snapshot-sink/pull-request-comment-client.ts
5327
+ var GITHUB_API_ERROR = {
5328
+ SURFACE_UNSUPPORTED: "the pull-request comment client supports only the pull-request comment surface"
5329
+ };
5330
+ var GITHUB_API = {
5331
+ apiCommand: "api",
5332
+ fieldFlag: "-f",
5333
+ methodFlag: "-X",
5334
+ paginateFlag: "--paginate",
5335
+ slurpFlag: "--slurp",
5336
+ postMethod: "POST",
5337
+ patchMethod: "PATCH",
5338
+ bodyField: "body"
5339
+ };
5340
+ function githubCommentMarkerTag(marker) {
5341
+ return `<!-- ${marker} -->`;
5342
+ }
5343
+ function issueCommentsPath(repository, pullNumber) {
5344
+ return `repos/${repository}/issues/${pullNumber}/comments`;
5345
+ }
5346
+ function issueCommentPath(repository, commentId) {
5347
+ return `repos/${repository}/issues/comments/${commentId}`;
5348
+ }
5349
+ function createGithubPullRequestCommentClient(options) {
5350
+ const { run } = options;
5351
+ return {
5352
+ async upsertPullRequestComment({ pullNumber, marker, body }) {
5353
+ const markedBody = `${body}
5354
+ ${githubCommentMarkerTag(marker)}`;
5355
+ const listed = await run([
5356
+ GITHUB_API.apiCommand,
5357
+ issueCommentsPath(options.repository, pullNumber),
5358
+ GITHUB_API.paginateFlag,
5359
+ GITHUB_API.slurpFlag
5360
+ ]);
5361
+ const comments = JSON.parse(listed.stdout).flat();
5362
+ const existing = comments.find((comment) => comment.body.includes(githubCommentMarkerTag(marker)));
5363
+ if (existing === void 0) {
5364
+ await run([
5365
+ GITHUB_API.apiCommand,
5366
+ issueCommentsPath(options.repository, pullNumber),
5367
+ GITHUB_API.methodFlag,
5368
+ GITHUB_API.postMethod,
5369
+ GITHUB_API.fieldFlag,
5370
+ `${GITHUB_API.bodyField}=${markedBody}`
5371
+ ]);
5372
+ return;
5373
+ }
5374
+ await run([
5375
+ GITHUB_API.apiCommand,
5376
+ issueCommentPath(options.repository, existing.id),
5377
+ GITHUB_API.methodFlag,
5378
+ GITHUB_API.patchMethod,
5379
+ GITHUB_API.fieldFlag,
5380
+ `${GITHUB_API.bodyField}=${markedBody}`
5381
+ ]);
5382
+ },
5383
+ uploadActionsArtifact() {
5384
+ return Promise.reject(new Error(GITHUB_API_ERROR.SURFACE_UNSUPPORTED));
5385
+ },
5386
+ saveActionsCache() {
5387
+ return Promise.reject(new Error(GITHUB_API_ERROR.SURFACE_UNSUPPORTED));
5388
+ }
5389
+ };
5390
+ }
5391
+
5162
5392
  // src/lib/github-snapshot-sink/index.ts
5163
5393
  var SNAPSHOT_SURFACE_KIND = {
5164
5394
  PULL_REQUEST_COMMENT: "pull-request-comment",
@@ -5369,7 +5599,7 @@ async function appendJournalEvent(ref, input, sink, options = {}) {
5369
5599
  try {
5370
5600
  event = await bound.value.journal.append(input);
5371
5601
  } catch (error) {
5372
- return { ok: false, error: `${JOURNAL_RUNTIME_ERROR.APPEND_FAILED}: ${toMessage2(error)}` };
5602
+ return { ok: false, error: `${JOURNAL_RUNTIME_ERROR.APPEND_FAILED}: ${toMessage(error)}` };
5373
5603
  }
5374
5604
  try {
5375
5605
  await sink.emit(event);
@@ -5383,7 +5613,7 @@ async function readJournalEvents(ref, fromCursor, options = {}) {
5383
5613
  try {
5384
5614
  return { ok: true, value: await bound.value.journal.read(fromCursor) };
5385
5615
  } catch (error) {
5386
- return { ok: false, error: `${JOURNAL_RUNTIME_ERROR.READ_FAILED}: ${toMessage2(error)}` };
5616
+ return { ok: false, error: `${JOURNAL_RUNTIME_ERROR.READ_FAILED}: ${toMessage(error)}` };
5387
5617
  }
5388
5618
  }
5389
5619
  async function sealJournalRun(ref, options = {}) {
@@ -5393,7 +5623,7 @@ async function sealJournalRun(ref, options = {}) {
5393
5623
  await bound.value.journal.seal();
5394
5624
  return { ok: true, value: void 0 };
5395
5625
  } catch (error) {
5396
- return { ok: false, error: `${JOURNAL_RUNTIME_ERROR.SEAL_FAILED}: ${toMessage2(error)}` };
5626
+ return { ok: false, error: `${JOURNAL_RUNTIME_ERROR.SEAL_FAILED}: ${toMessage(error)}` };
5397
5627
  }
5398
5628
  }
5399
5629
  async function renderJournalRun(ref, projection, options = {}) {
@@ -5402,12 +5632,9 @@ async function renderJournalRun(ref, projection, options = {}) {
5402
5632
  try {
5403
5633
  return { ok: true, value: await bound.value.journal.render(projection) };
5404
5634
  } catch (error) {
5405
- return { ok: false, error: `${JOURNAL_RUNTIME_ERROR.RENDER_FAILED}: ${toMessage2(error)}` };
5635
+ return { ok: false, error: `${JOURNAL_RUNTIME_ERROR.RENDER_FAILED}: ${toMessage(error)}` };
5406
5636
  }
5407
5637
  }
5408
- function toMessage2(error) {
5409
- return error instanceof Error ? error.message : String(error);
5410
- }
5411
5638
 
5412
5639
  // src/commands/journal/cli.ts
5413
5640
  var JOURNAL_CLI_EXIT_CODE = {
@@ -5600,80 +5827,6 @@ async function journalRenderCommand(scope2, deps = {}) {
5600
5827
  return okResult(JSON.stringify(rendered.value));
5601
5828
  }
5602
5829
 
5603
- // src/commands/journal/github-client.ts
5604
- import { execa as execa5 } from "execa";
5605
- var GITHUB_CLI_ERROR = {
5606
- SURFACE_UNSUPPORTED: "the pull-request comment client supports only the pull-request comment surface"
5607
- };
5608
- var GITHUB_CLI = {
5609
- executable: "gh",
5610
- apiCommand: "api",
5611
- fieldFlag: "-f",
5612
- methodFlag: "-X",
5613
- paginateFlag: "--paginate",
5614
- slurpFlag: "--slurp",
5615
- postMethod: "POST",
5616
- patchMethod: "PATCH",
5617
- bodyField: "body"
5618
- };
5619
- var defaultGhRunner = async (args, options) => {
5620
- const result = await execa5(GITHUB_CLI.executable, [...args], {
5621
- ...options?.input === void 0 ? {} : { input: options.input }
5622
- });
5623
- return { stdout: typeof result.stdout === "string" ? result.stdout : "" };
5624
- };
5625
- function githubCommentMarkerTag(marker) {
5626
- return `<!-- ${marker} -->`;
5627
- }
5628
- function issueCommentsPath(repository, pullNumber) {
5629
- return `repos/${repository}/issues/${pullNumber}/comments`;
5630
- }
5631
- function issueCommentPath(repository, commentId) {
5632
- return `repos/${repository}/issues/comments/${commentId}`;
5633
- }
5634
- function createGithubPrCommentClient(options) {
5635
- const run = options.run ?? defaultGhRunner;
5636
- return {
5637
- async upsertPullRequestComment({ pullNumber, marker, body }) {
5638
- const markedBody = `${body}
5639
- ${githubCommentMarkerTag(marker)}`;
5640
- const listed = await run([
5641
- GITHUB_CLI.apiCommand,
5642
- issueCommentsPath(options.repository, pullNumber),
5643
- GITHUB_CLI.paginateFlag,
5644
- GITHUB_CLI.slurpFlag
5645
- ]);
5646
- const comments = JSON.parse(listed.stdout).flat();
5647
- const existing = comments.find((comment) => comment.body.includes(githubCommentMarkerTag(marker)));
5648
- if (existing === void 0) {
5649
- await run([
5650
- GITHUB_CLI.apiCommand,
5651
- issueCommentsPath(options.repository, pullNumber),
5652
- GITHUB_CLI.methodFlag,
5653
- GITHUB_CLI.postMethod,
5654
- GITHUB_CLI.fieldFlag,
5655
- `${GITHUB_CLI.bodyField}=${markedBody}`
5656
- ]);
5657
- return;
5658
- }
5659
- await run([
5660
- GITHUB_CLI.apiCommand,
5661
- issueCommentPath(options.repository, existing.id),
5662
- GITHUB_CLI.methodFlag,
5663
- GITHUB_CLI.patchMethod,
5664
- GITHUB_CLI.fieldFlag,
5665
- `${GITHUB_CLI.bodyField}=${markedBody}`
5666
- ]);
5667
- },
5668
- uploadActionsArtifact() {
5669
- return Promise.reject(new Error(GITHUB_CLI_ERROR.SURFACE_UNSUPPORTED));
5670
- },
5671
- saveActionsCache() {
5672
- return Promise.reject(new Error(GITHUB_CLI_ERROR.SURFACE_UNSUPPORTED));
5673
- }
5674
- };
5675
- }
5676
-
5677
5830
  // src/lib/process-lifecycle/exit-codes.ts
5678
5831
  var SIGINT_EXIT_CODE = 130;
5679
5832
  var SIGTERM_EXIT_CODE = 143;
@@ -5913,7 +6066,7 @@ function streamBinding() {
5913
6066
  const repository = process.env[JOURNAL_CLI_ENV.GITHUB_REPOSITORY] ?? "";
5914
6067
  return {
5915
6068
  localSink: stdoutStreamSink(),
5916
- githubClient: createGithubPrCommentClient({ repository }),
6069
+ githubClient: createGithubPullRequestCommentClient({ repository, run: runGhApi }),
5917
6070
  githubRepository: repository
5918
6071
  };
5919
6072
  }
@@ -6157,9 +6310,7 @@ var NoSessionsAvailableError = class extends SessionError {
6157
6310
  var SessionLegacyFrontmatterInputError = class extends SessionError {
6158
6311
  constructor() {
6159
6312
  super(
6160
- `Invalid handoff input: stdin opens with the YAML-frontmatter delimiter \`---\`. Use the JSON-prefix wire format: a single JSON object holding caller-supplied fields followed by the body bytes verbatim. Example:
6161
-
6162
- printf '%s\\n' '{"priority":"high","goal":"...","next_step":"..."}' '# Body' | spx session handoff`
6313
+ "Invalid handoff input: stdin opens with the YAML-frontmatter delimiter `---`. Use the JSON-prefix wire format: a single JSON object holding caller-supplied fields followed by the body bytes verbatim. Example:\n\n" + String.raw` printf '%s\n' '{"priority":"high","goal":"...","next_step":"..."}' '# Body' ` + "| spx session handoff"
6163
6314
  );
6164
6315
  this.name = "SessionLegacyFrontmatterInputError";
6165
6316
  }
@@ -6582,7 +6733,7 @@ function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
6582
6733
  }
6583
6734
  function formatShowOutput(content, options) {
6584
6735
  const metadata = parseSessionMetadata(content);
6585
- const headerLines = [
6736
+ const headerLines2 = [
6586
6737
  `${SESSION_SHOW_LABEL.STATUS}: ${options.status}`,
6587
6738
  `${SESSION_SHOW_LABEL.PRIORITY}: ${metadata.priority}`,
6588
6739
  `${SESSION_SHOW_LABEL.GIT_REF}: ${metadata.git_ref}`,
@@ -6591,7 +6742,7 @@ function formatShowOutput(content, options) {
6591
6742
  `${SESSION_SHOW_LABEL.CREATED}: ${metadata.created_at ?? ""}`,
6592
6743
  `${SESSION_SHOW_LABEL.AGENT_SESSION}: ${metadata.agent_session_id ?? ""}`
6593
6744
  ];
6594
- const header = headerLines.join("\n");
6745
+ const header = headerLines2.join("\n");
6595
6746
  const separator = "\n" + SESSION_SHOW_SEPARATOR_CHAR.repeat(SESSION_SHOW_SEPARATOR_WIDTH) + "\n\n";
6596
6747
  return header + separator + content;
6597
6748
  }
@@ -7351,13 +7502,13 @@ Workflow:
7351
7502
  4. archive - Move session to archive
7352
7503
  5. delete - Remove session permanently
7353
7504
  `;
7354
- var HANDOFF_FRONTMATTER_HELP = `
7505
+ var HANDOFF_FRONTMATTER_HELP = String.raw`
7355
7506
  Usage:
7356
7507
  Pipe a JSON header followed by the body bytes to stdin.
7357
7508
 
7358
7509
  The header is a single JSON object holding caller-supplied structured fields.
7359
7510
  A single LF or CRLF after the header is consumed as a separator. The body
7360
- is the remaining bytes verbatim \u2014 no YAML, no escape rules, no ambiguity
7511
+ is the remaining bytes verbatim no YAML, no escape rules, no ambiguity
7361
7512
  from leading characters like '#' or '---'.
7362
7513
 
7363
7514
  JSON Header Fields:
@@ -7380,11 +7531,11 @@ Output Tags (for automation):
7380
7531
  <SESSION_FILE>/path/to/file</SESSION_FILE> Absolute path to created file
7381
7532
 
7382
7533
  Canonical Invocation:
7383
- printf '%s\\n' \\
7384
- '{"priority":"high","goal":"Fix login","next_step":"Run validation","specs":[],"files":[]}' \\
7385
- '# Fix login' \\
7386
- '' \\
7387
- 'Body text \u2014 # symbols, --- delimiters, and code fences are literal.' \\
7534
+ printf '%s\n' \
7535
+ '{"priority":"high","goal":"Fix login","next_step":"Run validation","specs":[],"files":[]}' \
7536
+ '# Fix login' \
7537
+ '' \
7538
+ 'Body text # symbols, --- delimiters, and code fences are literal.' \
7388
7539
  | spx session handoff
7389
7540
  `;
7390
7541
  var PICKUP_SELECTION_HELP = `
@@ -9081,92 +9232,111 @@ async function runNodeCommand(options, deps) {
9081
9232
  return { dispatch, runFile, recorded };
9082
9233
  }
9083
9234
 
9084
- // src/commands/spec/node-outcome-resolver.ts
9085
- var PATH_SEPARATOR = "/";
9086
- function createNodeOutcomeResolver(deps) {
9087
- let evidence;
9088
- const currentInputsByCoveredPaths = /* @__PURE__ */ new Map();
9089
- const sharedEvidence = () => evidence ??= loadResolverEvidence(deps);
9090
- const refreshEvidence = () => {
9091
- evidence = loadResolverEvidence(deps);
9092
- };
9093
- const currentInputsFor = (coveredPaths) => {
9094
- const cacheKey = coveredPathCollectionKey(coveredPaths);
9095
- const cached = currentInputsByCoveredPaths.get(cacheKey);
9096
- if (cached !== void 0) {
9097
- return cached;
9098
- }
9099
- const current = currentStalenessInputs(deps.productDir, coveredPaths, deps);
9100
- currentInputsByCoveredPaths.set(cacheKey, current);
9101
- return current;
9102
- };
9103
- return async (nodeId) => {
9104
- const { discoveredTestPaths, terminalRuns } = await sharedEvidence();
9105
- const nodePath = `${SPEC_TREE_CONFIG.ROOT_DIRECTORY}${PATH_SEPARATOR}${nodeId}`;
9106
- const nodeTestPaths = filterNodeTestPaths(discoveredTestPaths, nodePath);
9107
- const usable = await usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor);
9108
- if (usable !== void 0) {
9109
- return usable;
9110
- }
9111
- const { dispatch, recorded } = await runNodeCommand({ productDir: deps.productDir, nodePath }, deps);
9112
- refreshEvidence();
9113
- return outcomesCoverPaths(dispatch.outcomes, nodeTestPaths) && recorded.status === TEST_RUN_STATE_STATUS.PASSED;
9114
- };
9115
- }
9116
- function coveredPathCollectionKey(coveredPaths) {
9117
- return JSON.stringify([...coveredPaths].sort(compareAsciiStrings));
9118
- }
9119
- async function loadResolverEvidence(deps) {
9120
- const discoveredTestPaths = await discoverTestFiles(deps.productDir);
9121
- const runs = await readTestingRuns(deps.productDir, deps);
9122
- return { discoveredTestPaths, terminalRuns: runs.ok ? runs.value.terminalRuns : [] };
9123
- }
9124
- function filterNodeTestPaths(discoveredTestPaths, nodePath) {
9125
- const prefix = `${nodePath}${PATH_SEPARATOR}`;
9126
- return discoveredTestPaths.filter((path7) => path7.startsWith(prefix));
9127
- }
9128
- async function usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor) {
9129
- const latest = selectLatestTerminalTestRunForNode(terminalRuns, nodeTestPaths);
9130
- if (latest === void 0) {
9131
- return void 0;
9132
- }
9133
- const runCoveredPaths = latest.state.runnerOutcomes.flatMap((outcome) => outcome.testPaths);
9134
- const presentTestPaths = new Set(discoveredTestPaths);
9135
- if (!runCoveredPaths.every((path7) => presentTestPaths.has(path7))) {
9136
- return void 0;
9137
- }
9138
- const current = await currentInputsFor(runCoveredPaths);
9139
- const fresh = isStalenessMatch(extractStalenessInputs(latest.state), current);
9140
- return fresh && latest.state.status === TEST_RUN_STATE_STATUS.PASSED ? true : void 0;
9141
- }
9235
+ // src/lib/node-status/ci-projection.ts
9236
+ import { parse as parseYaml3 } from "yaml";
9142
9237
 
9143
9238
  // src/lib/node-status/classify.ts
9144
- var NODE_STATUS_STATUS_KEY = "status";
9239
+ var NODE_STATUS_SCHEMA_VERSION = 1;
9145
9240
  var NODE_STATUS_JSON_INDENT = 2;
9241
+ var NODE_STATUS_FIELD = {
9242
+ SCHEMA_VERSION: "schemaVersion",
9243
+ VERIFICATION: "verification",
9244
+ OVERALL: "overall"
9245
+ };
9246
+ var NODE_STATUS_VERIFICATION_MECHANISM = {
9247
+ TEST: "test",
9248
+ EVAL: "eval",
9249
+ AUDIT: "audit"
9250
+ };
9251
+ var NODE_STATUS_EVIDENCE_OUTCOME = {
9252
+ PASSED: "passed",
9253
+ FAILED: "failed",
9254
+ NOT_RUN: "not-run"
9255
+ };
9256
+ var NODE_STATUS_MECHANISM_OVERALL = {
9257
+ PASSED: NODE_STATUS_EVIDENCE_OUTCOME.PASSED,
9258
+ FAILED: NODE_STATUS_EVIDENCE_OUTCOME.FAILED,
9259
+ PARTIAL: "partial",
9260
+ NOT_RUN: NODE_STATUS_EVIDENCE_OUTCOME.NOT_RUN
9261
+ };
9146
9262
  function classifyNodeStatus(facts) {
9147
- if (!facts.hasTests) return SPEC_TREE_NODE_STATE.DECLARED;
9263
+ if (!facts.hasVerificationReferences) return SPEC_TREE_NODE_STATE.DECLARED;
9148
9264
  if (facts.isExcluded) return SPEC_TREE_NODE_STATE.SPECIFIED;
9149
- if (facts.testsPass) return SPEC_TREE_NODE_STATE.PASSING;
9265
+ if (verificationPassed(facts.verification)) return SPEC_TREE_NODE_STATE.PASSING;
9150
9266
  return SPEC_TREE_NODE_STATE.FAILING;
9151
9267
  }
9152
- function serializeNodeStatus(state) {
9153
- return `${JSON.stringify({ [NODE_STATUS_STATUS_KEY]: state }, null, NODE_STATUS_JSON_INDENT)}
9268
+ function createNodeStatusFile(verification) {
9269
+ return {
9270
+ [NODE_STATUS_FIELD.SCHEMA_VERSION]: NODE_STATUS_SCHEMA_VERSION,
9271
+ [NODE_STATUS_FIELD.VERIFICATION]: verification
9272
+ };
9273
+ }
9274
+ function hasNodeStatusVerificationReferences(verification) {
9275
+ return Object.values(verification).some(
9276
+ (mechanism) => Object.keys(mechanism).some((reference) => reference !== NODE_STATUS_FIELD.OVERALL)
9277
+ );
9278
+ }
9279
+ function createNodeStatusMechanismRecord(outcomes) {
9280
+ return {
9281
+ [NODE_STATUS_FIELD.OVERALL]: rollupNodeStatusMechanism(outcomes),
9282
+ ...outcomes
9283
+ };
9284
+ }
9285
+ function rollupNodeStatusMechanism(outcomes) {
9286
+ const values = Object.values(outcomes);
9287
+ if (values.length === 0) return NODE_STATUS_MECHANISM_OVERALL.NOT_RUN;
9288
+ if (values.includes(NODE_STATUS_EVIDENCE_OUTCOME.FAILED)) {
9289
+ return NODE_STATUS_MECHANISM_OVERALL.FAILED;
9290
+ }
9291
+ if (values.every((outcome) => outcome === NODE_STATUS_EVIDENCE_OUTCOME.PASSED)) {
9292
+ return NODE_STATUS_MECHANISM_OVERALL.PASSED;
9293
+ }
9294
+ if (values.every((outcome) => outcome === NODE_STATUS_EVIDENCE_OUTCOME.NOT_RUN)) {
9295
+ return NODE_STATUS_MECHANISM_OVERALL.NOT_RUN;
9296
+ }
9297
+ return NODE_STATUS_MECHANISM_OVERALL.PARTIAL;
9298
+ }
9299
+ function serializeNodeStatus(status) {
9300
+ return `${JSON.stringify(status, null, NODE_STATUS_JSON_INDENT)}
9154
9301
  `;
9155
9302
  }
9303
+ function verificationPassed(verification) {
9304
+ if (verification === void 0) return false;
9305
+ const mechanisms = Object.values(verification);
9306
+ return mechanisms.length > 0 && mechanisms.every((mechanism) => mechanism[NODE_STATUS_FIELD.OVERALL] === NODE_STATUS_MECHANISM_OVERALL.PASSED);
9307
+ }
9156
9308
 
9157
9309
  // src/lib/node-status/provider.ts
9158
9310
  import { join as join20 } from "path";
9159
9311
 
9312
+ // src/lib/node-status/exclude.ts
9313
+ function isNodeStatusEntryExcluded(ignoreReader, node) {
9314
+ const reference = node.ref?.path;
9315
+ if (reference === void 0) return false;
9316
+ return ignoreReader.isUnderIgnoreSource(reference);
9317
+ }
9318
+
9160
9319
  // src/lib/node-status/read.ts
9161
9320
  import { readFileSync as readFileSync2 } from "fs";
9162
9321
  import { join as join19 } from "path";
9163
9322
  var NODE_STATUS_FILENAME = "spx.status.json";
9164
- var NODE_STATUS_VALUES = new Set(Object.values(SPEC_TREE_NODE_STATE));
9323
+ var NODE_STATUS_MECHANISMS = new Set(Object.values(NODE_STATUS_VERIFICATION_MECHANISM));
9324
+ var NODE_STATUS_EVIDENCE_OUTCOMES = new Set(Object.values(NODE_STATUS_EVIDENCE_OUTCOME));
9325
+ var NODE_STATUS_OVERALL_VALUES = new Set(Object.values(NODE_STATUS_MECHANISM_OVERALL));
9165
9326
  function isNodeError2(error) {
9166
9327
  return error instanceof Error && "code" in error;
9167
9328
  }
9168
- function isSpecTreeNodeState(value) {
9169
- return typeof value === "string" && NODE_STATUS_VALUES.has(value);
9329
+ function isObject(value) {
9330
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9331
+ }
9332
+ function isNodeStatusMechanism(value) {
9333
+ return NODE_STATUS_MECHANISMS.has(value);
9334
+ }
9335
+ function isNodeStatusEvidenceOutcome(value) {
9336
+ return typeof value === "string" && NODE_STATUS_EVIDENCE_OUTCOMES.has(value);
9337
+ }
9338
+ function isNodeStatusMechanismOverall(value) {
9339
+ return typeof value === "string" && NODE_STATUS_OVERALL_VALUES.has(value);
9170
9340
  }
9171
9341
  function readNodeStatus(nodeDir) {
9172
9342
  const filePath = join19(nodeDir, NODE_STATUS_FILENAME);
@@ -9179,14 +9349,67 @@ function readNodeStatus(nodeDir) {
9179
9349
  }
9180
9350
  throw error;
9181
9351
  }
9182
- const parsed = JSON.parse(content);
9183
- const status = parsed[NODE_STATUS_STATUS_KEY];
9184
- if (!isSpecTreeNodeState(status)) {
9352
+ return parseNodeStatusFile(JSON.parse(content), filePath);
9353
+ }
9354
+ function parseNodeStatusFile(candidate, source) {
9355
+ if (!isObject(candidate)) {
9356
+ throw new Error(`Invalid ${NODE_STATUS_FILENAME} at ${source}: expected a JSON object`);
9357
+ }
9358
+ if (candidate[NODE_STATUS_FIELD.SCHEMA_VERSION] !== NODE_STATUS_SCHEMA_VERSION) {
9359
+ throw new Error(
9360
+ `Invalid ${NODE_STATUS_FILENAME} at ${source}: schemaVersion must be ${NODE_STATUS_SCHEMA_VERSION}`
9361
+ );
9362
+ }
9363
+ const verification = parseVerification(candidate[NODE_STATUS_FIELD.VERIFICATION], source);
9364
+ return {
9365
+ [NODE_STATUS_FIELD.SCHEMA_VERSION]: NODE_STATUS_SCHEMA_VERSION,
9366
+ [NODE_STATUS_FIELD.VERIFICATION]: verification
9367
+ };
9368
+ }
9369
+ function parseVerification(candidate, source) {
9370
+ if (!isObject(candidate)) {
9371
+ throw new Error(`Invalid ${NODE_STATUS_FILENAME} at ${source}: verification must be an object`);
9372
+ }
9373
+ const verification = {};
9374
+ for (const [mechanism, rawRecord] of Object.entries(candidate)) {
9375
+ if (!isNodeStatusMechanism(mechanism)) {
9376
+ throw new Error(`Invalid ${NODE_STATUS_FILENAME} at ${source}: unknown verification mechanism "${mechanism}"`);
9377
+ }
9378
+ verification[mechanism] = parseMechanismRecord(rawRecord, source, mechanism);
9379
+ }
9380
+ return verification;
9381
+ }
9382
+ function parseMechanismRecord(candidate, source, mechanism) {
9383
+ if (!isObject(candidate)) {
9384
+ throw new Error(`Invalid ${NODE_STATUS_FILENAME} at ${source}: verification.${mechanism} must be an object`);
9385
+ }
9386
+ const overall = candidate[NODE_STATUS_FIELD.OVERALL];
9387
+ if (!isNodeStatusMechanismOverall(overall)) {
9388
+ throw new Error(
9389
+ `Invalid ${NODE_STATUS_FILENAME} at ${source}: verification.${mechanism}.overall is invalid`
9390
+ );
9391
+ }
9392
+ const parsed = {
9393
+ [NODE_STATUS_FIELD.OVERALL]: overall
9394
+ };
9395
+ const outcomes = {};
9396
+ for (const [reference, outcome] of Object.entries(candidate)) {
9397
+ if (reference === NODE_STATUS_FIELD.OVERALL) continue;
9398
+ if (!isNodeStatusEvidenceOutcome(outcome)) {
9399
+ throw new Error(
9400
+ `Invalid ${NODE_STATUS_FILENAME} at ${source}: verification.${mechanism}.${reference} is invalid`
9401
+ );
9402
+ }
9403
+ outcomes[reference] = outcome;
9404
+ parsed[reference] = outcome;
9405
+ }
9406
+ const expectedOverall = rollupNodeStatusMechanism(outcomes);
9407
+ if (overall !== expectedOverall) {
9185
9408
  throw new Error(
9186
- `Invalid ${NODE_STATUS_FILENAME} at ${filePath}: status "${String(status)}" is not a lifecycle state`
9409
+ `Invalid ${NODE_STATUS_FILENAME} at ${source}: verification.${mechanism}.overall does not match evidence outcomes`
9187
9410
  );
9188
9411
  }
9189
- return status;
9412
+ return parsed;
9190
9413
  }
9191
9414
 
9192
9415
  // src/lib/node-status/provider.ts
@@ -9194,56 +9417,194 @@ function nodeDirectory(productDir, node) {
9194
9417
  return join20(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
9195
9418
  }
9196
9419
  function createNodeStatusProvider(productDir) {
9420
+ const ignoreReader = createIgnoreSourceReader(productDir, {
9421
+ ignoreSourceFilename: IGNORE_SOURCE_FILENAME_DEFAULT,
9422
+ specTreeRootSegment: SPEC_TREE_CONFIG.ROOT_DIRECTORY
9423
+ });
9197
9424
  return {
9198
9425
  stateForNode(node) {
9199
- return readNodeStatus(nodeDirectory(productDir, node));
9426
+ const status = readNodeStatus(nodeDirectory(productDir, node));
9427
+ if (status === void 0) return void 0;
9428
+ return classifyNodeStatus({
9429
+ hasVerificationReferences: hasNodeStatusVerificationReferences(status.verification),
9430
+ isExcluded: isNodeStatusEntryExcluded(ignoreReader, node),
9431
+ verification: status.verification
9432
+ });
9433
+ }
9434
+ };
9435
+ }
9436
+
9437
+ // src/lib/node-status/update.ts
9438
+ import { mkdir as mkdir4, readdir as readdir7, rm, writeFile as writeFile2 } from "fs/promises";
9439
+ import { dirname as dirname6, join as join21 } from "path";
9440
+ var NODE_STATUS_TEXT_ENCODING = "utf8";
9441
+ async function updateNodeStatus(options) {
9442
+ const { productDir, resolveOutcome } = options;
9443
+ const snapshot = await readSpecTree({ source: createFilesystemSpecTreeSource({ productDir }) });
9444
+ const ignoreReader = createIgnoreSourceReader(productDir, {
9445
+ ignoreSourceFilename: IGNORE_SOURCE_FILENAME_DEFAULT,
9446
+ specTreeRootSegment: SPEC_TREE_CONFIG.ROOT_DIRECTORY
9447
+ });
9448
+ const evidenceByNode = collectEvidenceByNode(snapshot);
9449
+ const liveStatusPaths = /* @__PURE__ */ new Set();
9450
+ for (const node of snapshot.allNodes) {
9451
+ const evidence = evidenceByNode.get(node.id) ?? [];
9452
+ const verification = await resolveVerification(node, {
9453
+ evidence,
9454
+ isExcluded: isNodeStatusEntryExcluded(ignoreReader, node),
9455
+ resolveOutcome
9456
+ });
9457
+ const statusPath = nodeStatusPath(productDir, node.id);
9458
+ liveStatusPaths.add(statusPath);
9459
+ await writeNodeStatus(statusPath, verification);
9460
+ }
9461
+ await removeStaleNodeStatusFiles(productDir, liveStatusPaths);
9462
+ }
9463
+ async function resolveVerification(node, input) {
9464
+ if (input.evidence.length === 0) return {};
9465
+ if (input.isExcluded) return createTestVerification(input.evidence, NODE_STATUS_EVIDENCE_OUTCOME.NOT_RUN);
9466
+ return createTestVerificationFromOutcomes(
9467
+ input.evidence,
9468
+ await input.resolveOutcome(node.id, evidencePaths(input.evidence))
9469
+ );
9470
+ }
9471
+ function collectEvidenceByNode(snapshot) {
9472
+ const evidenceByNode = /* @__PURE__ */ new Map();
9473
+ for (const entry of snapshot.entries) {
9474
+ if (entry.type === SPEC_TREE_ENTRY_TYPE.EVIDENCE) {
9475
+ const entries = evidenceByNode.get(entry.parentId) ?? [];
9476
+ entries.push(entry);
9477
+ evidenceByNode.set(entry.parentId, entries);
9478
+ }
9479
+ }
9480
+ return evidenceByNode;
9481
+ }
9482
+ function createTestVerification(evidence, outcome) {
9483
+ const outcomes = Object.fromEntries(evidence.map((entry) => [evidencePath(entry), outcome]));
9484
+ return createTestVerificationFromOutcomes(evidence, outcomes);
9485
+ }
9486
+ function createTestVerificationFromOutcomes(evidence, resolvedOutcomes) {
9487
+ const outcomes = {};
9488
+ for (const entry of evidence) {
9489
+ const path7 = evidencePath(entry);
9490
+ outcomes[path7] = resolvedOutcomes[path7] ?? NODE_STATUS_EVIDENCE_OUTCOME.NOT_RUN;
9491
+ }
9492
+ return { [NODE_STATUS_VERIFICATION_MECHANISM.TEST]: createNodeStatusMechanismRecord(outcomes) };
9493
+ }
9494
+ function evidencePaths(evidence) {
9495
+ return evidence.map(evidencePath);
9496
+ }
9497
+ function evidencePath(entry) {
9498
+ return entry.ref?.path ?? entry.id;
9499
+ }
9500
+ async function writeNodeStatus(filePath, verification) {
9501
+ await mkdir4(dirname6(filePath), { recursive: true });
9502
+ await writeFile2(filePath, serializeNodeStatus(createNodeStatusFile(verification)), NODE_STATUS_TEXT_ENCODING);
9503
+ }
9504
+ function nodeStatusPath(productDir, nodeId) {
9505
+ return join21(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
9506
+ }
9507
+ async function removeStaleNodeStatusFiles(productDir, liveStatusPaths) {
9508
+ const statusPaths = await collectNodeStatusFiles(join21(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY));
9509
+ await Promise.all(statusPaths.filter((path7) => !liveStatusPaths.has(path7)).map((path7) => rm(path7, { force: true })));
9510
+ }
9511
+ async function collectNodeStatusFiles(directory) {
9512
+ const entries = await readDirectoryEntries(directory);
9513
+ const statusPaths = [];
9514
+ for (const entry of entries) {
9515
+ const entryPath = join21(directory, entry.name);
9516
+ if (entry.isDirectory()) {
9517
+ statusPaths.push(...await collectNodeStatusFiles(entryPath));
9518
+ } else if (entry.isFile() && entry.name === NODE_STATUS_FILENAME) {
9519
+ statusPaths.push(entryPath);
9520
+ }
9521
+ }
9522
+ return statusPaths;
9523
+ }
9524
+ async function readDirectoryEntries(directory) {
9525
+ try {
9526
+ return await readdir7(directory, { withFileTypes: true });
9527
+ } catch (error) {
9528
+ if (isNodeError3(error) && error.code === "ENOENT") return [];
9529
+ throw error;
9530
+ }
9531
+ }
9532
+ function isNodeError3(error) {
9533
+ return error instanceof Error && "code" in error;
9534
+ }
9535
+
9536
+ // src/commands/spec/node-outcome-resolver.ts
9537
+ var PATH_SEPARATOR = "/";
9538
+ var SUCCESS_EXIT_CODE2 = 0;
9539
+ function createNodeOutcomeResolver(deps) {
9540
+ let evidence;
9541
+ const currentInputsByCoveredPaths = /* @__PURE__ */ new Map();
9542
+ const sharedEvidence = () => evidence ??= loadResolverEvidence(deps);
9543
+ const refreshEvidence = () => {
9544
+ evidence = loadResolverEvidence(deps);
9545
+ };
9546
+ const currentInputsFor = (coveredPaths) => {
9547
+ const cacheKey = coveredPathCollectionKey(coveredPaths);
9548
+ const cached = currentInputsByCoveredPaths.get(cacheKey);
9549
+ if (cached !== void 0) {
9550
+ return cached;
9200
9551
  }
9552
+ const current = currentStalenessInputs(deps.productDir, coveredPaths, deps);
9553
+ currentInputsByCoveredPaths.set(cacheKey, current);
9554
+ return current;
9201
9555
  };
9556
+ return async (nodeId, evidencePaths2) => {
9557
+ const { discoveredTestPaths, terminalRuns } = await sharedEvidence();
9558
+ const nodePath = `${SPEC_TREE_CONFIG.ROOT_DIRECTORY}${PATH_SEPARATOR}${nodeId}`;
9559
+ const nodeTestPaths = filterNodeTestPaths(discoveredTestPaths, nodePath, evidencePaths2);
9560
+ const usable = await usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor);
9561
+ if (usable !== void 0) {
9562
+ return usable;
9563
+ }
9564
+ const { dispatch } = await runNodeCommand({ productDir: deps.productDir, nodePath }, deps);
9565
+ refreshEvidence();
9566
+ return outcomesForPaths(dispatch.outcomes, nodeTestPaths);
9567
+ };
9568
+ }
9569
+ function coveredPathCollectionKey(coveredPaths) {
9570
+ return JSON.stringify([...coveredPaths].sort(compareAsciiStrings));
9202
9571
  }
9203
-
9204
- // src/lib/node-status/update.ts
9205
- import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
9206
- import { dirname as dirname6, join as join21 } from "path";
9207
- var NODE_STATUS_TEXT_ENCODING = "utf8";
9208
- async function updateNodeStatus(options) {
9209
- const { productDir, resolveOutcome } = options;
9210
- const snapshot = await readSpecTree({ source: createFilesystemSpecTreeSource({ productDir }) });
9211
- const ignoreReader = createIgnoreSourceReader(productDir, {
9212
- ignoreSourceFilename: IGNORE_SOURCE_FILENAME_DEFAULT,
9213
- specTreeRootSegment: SPEC_TREE_CONFIG.ROOT_DIRECTORY
9214
- });
9215
- const nodesWithTests = collectNodesWithTests(snapshot);
9216
- for (const node of snapshot.allNodes) {
9217
- const facts = await classifyFacts(node, {
9218
- hasTests: nodesWithTests.has(node.id),
9219
- isExcluded: isNodeExcluded(ignoreReader, node),
9220
- resolveOutcome
9221
- });
9222
- await writeNodeStatus(productDir, node.id, classifyNodeStatus(facts));
9223
- }
9572
+ async function loadResolverEvidence(deps) {
9573
+ const discoveredTestPaths = await discoverTestFiles(deps.productDir);
9574
+ const runs = await readTestingRuns(deps.productDir, deps);
9575
+ return { discoveredTestPaths, terminalRuns: runs.ok ? runs.value.terminalRuns : [] };
9224
9576
  }
9225
- async function classifyFacts(node, input) {
9226
- const testsPass = input.hasTests && !input.isExcluded ? await input.resolveOutcome(node.id) : false;
9227
- return { hasTests: input.hasTests, isExcluded: input.isExcluded, testsPass };
9577
+ function filterNodeTestPaths(discoveredTestPaths, nodePath, evidencePaths2) {
9578
+ const prefix = `${nodePath}${PATH_SEPARATOR}`;
9579
+ const discovered = new Set(discoveredTestPaths.filter((path7) => path7.startsWith(prefix)));
9580
+ return evidencePaths2.filter((path7) => discovered.has(path7));
9228
9581
  }
9229
- function collectNodesWithTests(snapshot) {
9230
- const parents = /* @__PURE__ */ new Set();
9231
- for (const entry of snapshot.entries) {
9232
- if (entry.type === SPEC_TREE_ENTRY_TYPE.EVIDENCE) {
9233
- parents.add(entry.parentId);
9234
- }
9582
+ async function usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor) {
9583
+ const latest = selectLatestTerminalTestRunForNode(terminalRuns, nodeTestPaths);
9584
+ if (latest === void 0) {
9585
+ return void 0;
9586
+ }
9587
+ const runCoveredPaths = latest.state.runnerOutcomes.flatMap((outcome) => outcome.testPaths);
9588
+ const presentTestPaths = new Set(discoveredTestPaths);
9589
+ if (!runCoveredPaths.every((path7) => presentTestPaths.has(path7))) {
9590
+ return void 0;
9235
9591
  }
9236
- return parents;
9592
+ const current = await currentInputsFor(runCoveredPaths);
9593
+ const fresh = isStalenessMatch(extractStalenessInputs(latest.state), current);
9594
+ return fresh && latest.state.status === TEST_RUN_STATE_STATUS.PASSED ? outcomesForPaths(latest.state.runnerOutcomes, nodeTestPaths) : void 0;
9237
9595
  }
9238
- function isNodeExcluded(ignoreReader, node) {
9239
- const reference = node.ref?.path;
9240
- if (reference === void 0) return false;
9241
- return ignoreReader.isUnderIgnoreSource(reference);
9596
+ function outcomesForPaths(outcomes, nodeTestPaths) {
9597
+ return Object.fromEntries(
9598
+ nodeTestPaths.map((path7) => [path7, outcomeForPath(outcomes, path7)])
9599
+ );
9242
9600
  }
9243
- async function writeNodeStatus(productDir, nodeId, state) {
9244
- const filePath = join21(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
9245
- await mkdir4(dirname6(filePath), { recursive: true });
9246
- await writeFile2(filePath, serializeNodeStatus(state), NODE_STATUS_TEXT_ENCODING);
9601
+ function outcomeForPath(outcomes, path7) {
9602
+ const covering = outcomes.filter((outcome) => outcome.testPaths.includes(path7));
9603
+ if (covering.length === 0) return NODE_STATUS_EVIDENCE_OUTCOME.NOT_RUN;
9604
+ if (covering.every((outcome) => outcome.exitCode === SUCCESS_EXIT_CODE2)) {
9605
+ return NODE_STATUS_EVIDENCE_OUTCOME.PASSED;
9606
+ }
9607
+ return NODE_STATUS_EVIDENCE_OUTCOME.FAILED;
9247
9608
  }
9248
9609
 
9249
9610
  // src/commands/spec/status.ts
@@ -9954,8 +10315,9 @@ function createTestingDomain(deps = defaultTestingCliDependencies) {
9954
10315
  }
9955
10316
  var testingDomain = createTestingDomain();
9956
10317
 
9957
- // src/commands/validation/markdown.ts
9958
- import { relative as relative3 } from "path";
10318
+ // src/commands/validation/formatting.ts
10319
+ import { existsSync } from "fs";
10320
+ import { isAbsolute as isAbsolute3, join as join24, relative as relative3 } from "path";
9959
10321
 
9960
10322
  // src/validation/config/path-filter.ts
9961
10323
  import { isAbsolute as isAbsolute2, relative as relative2 } from "path";
@@ -10052,9 +10414,180 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
10052
10414
  };
10053
10415
  }
10054
10416
 
10417
+ // src/validation/steps/subprocess-output.ts
10418
+ var VALIDATION_SUBPROCESS_EVENTS = {
10419
+ CLOSE: "close",
10420
+ DATA: "data",
10421
+ DRAIN: "drain",
10422
+ ERROR: "error"
10423
+ };
10424
+ var defaultValidationSubprocessOutputStreams = {
10425
+ stdout: process.stdout,
10426
+ stderr: process.stderr
10427
+ };
10428
+ function forwardValidationSubprocessOutput(child, streams = defaultValidationSubprocessOutputStreams) {
10429
+ child.stdout?.on(VALIDATION_SUBPROCESS_EVENTS.DATA, (chunk) => {
10430
+ forwardChunkWithBackpressure(child.stdout, streams.stdout, chunk);
10431
+ });
10432
+ child.stderr?.on(VALIDATION_SUBPROCESS_EVENTS.DATA, (chunk) => {
10433
+ forwardChunkWithBackpressure(child.stderr, streams.stderr, chunk);
10434
+ });
10435
+ }
10436
+ function forwardChunkWithBackpressure(source, stream, chunk) {
10437
+ const ready = stream.write(chunk);
10438
+ if (ready || stream.once === void 0 || source === null || source === void 0) {
10439
+ return;
10440
+ }
10441
+ source.pause();
10442
+ stream.once(VALIDATION_SUBPROCESS_EVENTS.DRAIN, () => {
10443
+ source.resume();
10444
+ });
10445
+ }
10446
+
10447
+ // src/validation/steps/formatting.ts
10448
+ var DPRINT_COMMAND = "dprint";
10449
+ var DPRINT_CHECK_SUBCOMMAND = "check";
10450
+ var DPRINT_CONFIG_FILENAME = "dprint.jsonc";
10451
+ var defaultFormattingProcessRunner = lifecycleProcessRunner;
10452
+ function buildDprintCheckArgs(options) {
10453
+ return [DPRINT_CHECK_SUBCOMMAND, ...options.files ?? []];
10454
+ }
10455
+ async function validateFormatting(context, runner = defaultFormattingProcessRunner) {
10456
+ const args = buildDprintCheckArgs({ files: context.files });
10457
+ return new Promise((resolve8) => {
10458
+ const child = spawnManagedSubprocess(runner, DPRINT_COMMAND, args, { cwd: context.projectRoot });
10459
+ const chunks = [];
10460
+ const capture = (chunk) => {
10461
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString());
10462
+ };
10463
+ child.stdout?.on(VALIDATION_SUBPROCESS_EVENTS.DATA, capture);
10464
+ child.stderr?.on(VALIDATION_SUBPROCESS_EVENTS.DATA, capture);
10465
+ child.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
10466
+ resolve8({ success: code === 0, output: chunks.join("") });
10467
+ });
10468
+ child.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
10469
+ resolve8({ success: false, output: chunks.join(""), error: error.message });
10470
+ });
10471
+ });
10472
+ }
10473
+
10474
+ // src/commands/validation/messages.ts
10475
+ var VALIDATION_STAGE_DISPLAY_NAMES = {
10476
+ CIRCULAR: "dependency-cruiser",
10477
+ KNIP: "Knip",
10478
+ ESLINT: "ESLint",
10479
+ TYPESCRIPT: "TypeScript",
10480
+ MARKDOWN: "Markdown",
10481
+ LITERAL: "Literal",
10482
+ FORMATTING: "Formatting"
10483
+ };
10484
+ var VALIDATION_SKIP_LABELS = {
10485
+ VERB: "Skipping",
10486
+ CIRCULAR_REASON: "skip-circular",
10487
+ LITERAL_REASON: "skip-literal",
10488
+ DISABLED_BY_PREFIX: "disabled by",
10489
+ TYPESCRIPT_ABSENT_REASON: "TypeScript not detected in project",
10490
+ VALIDATION_PATHS_NO_TARGETS_REASON: "validation paths matched no files",
10491
+ MARKDOWN_NO_SCOPE_REASON: "no markdown files in --files scope",
10492
+ MARKDOWN_NO_DEFAULT_DIRECTORIES_REASON: "no spx/ or docs/ directories found"
10493
+ };
10494
+ var VALIDATION_COMMAND_OUTPUT = {
10495
+ CIRCULAR_FOUND: `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR}: circular dependencies found`,
10496
+ CIRCULAR_NONE_FOUND: `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR}: \u2713 No circular dependencies found`,
10497
+ KNIP_CONFIG_ERROR: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: \u2717 config error`,
10498
+ KNIP_DISABLED: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: skipped (${VALIDATION_SKIP_LABELS.DISABLED_BY_PREFIX} validation.knip.enabled)`,
10499
+ KNIP_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: \u2713 No unused code found`,
10500
+ KNIP_FAILURE: "Unused code found",
10501
+ ESLINT_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.ESLINT}: \u2713 No errors found`,
10502
+ ESLINT_FAILURE: `${VALIDATION_STAGE_DISPLAY_NAMES.ESLINT} validation failed`,
10503
+ ESLINT_MISSING_CONFIG: "ESLint config not found: project has tsconfig.json but no eslint.config.{ts,js,mjs,cjs}",
10504
+ TYPESCRIPT_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.TYPESCRIPT}: \u2713 No type errors`,
10505
+ TYPESCRIPT_FAILURE: `${VALIDATION_STAGE_DISPLAY_NAMES.TYPESCRIPT} validation failed`,
10506
+ MARKDOWN_NO_ISSUES: `${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: No issues found`,
10507
+ MARKDOWN_ERROR_SUMMARY_SUFFIX: "error(s) found",
10508
+ FORMATTING_NO_ISSUES: `${VALIDATION_STAGE_DISPLAY_NAMES.FORMATTING}: \u2713 No formatting issues found`,
10509
+ FORMATTING_FAILURE_SUMMARY: `${VALIDATION_STAGE_DISPLAY_NAMES.FORMATTING}: unformatted files found`
10510
+ };
10511
+ var CIRCULAR_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR}: skipped (--${VALIDATION_SKIP_LABELS.CIRCULAR_REASON})`;
10512
+ var CIRCULAR_SKIP_JSON_OUTPUT = JSON.stringify({
10513
+ skipped: true,
10514
+ reason: VALIDATION_SKIP_LABELS.CIRCULAR_REASON
10515
+ });
10516
+ var LITERAL_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.LITERAL}: skipped (--${VALIDATION_SKIP_LABELS.LITERAL_REASON})`;
10517
+ var LITERAL_SKIP_JSON_OUTPUT = JSON.stringify({
10518
+ skipped: true,
10519
+ reason: VALIDATION_SKIP_LABELS.LITERAL_REASON
10520
+ });
10521
+ function formatTypeScriptAbsentSkipMessage(stageName) {
10522
+ return `\u23ED ${VALIDATION_SKIP_LABELS.VERB} ${stageName} (${VALIDATION_SKIP_LABELS.TYPESCRIPT_ABSENT_REASON})`;
10523
+ }
10524
+ function formatValidationPathsNoTargetsSkipMessage(stageName) {
10525
+ return `\u23ED ${VALIDATION_SKIP_LABELS.VERB} ${stageName} (${VALIDATION_SKIP_LABELS.VALIDATION_PATHS_NO_TARGETS_REASON})`;
10526
+ }
10527
+
10528
+ // src/commands/validation/formatting.ts
10529
+ var FORMATTING_COMMAND_OUTPUT = {
10530
+ NO_ISSUES: VALIDATION_COMMAND_OUTPUT.FORMATTING_NO_ISSUES,
10531
+ FAILURE_SUMMARY: VALIDATION_COMMAND_OUTPUT.FORMATTING_FAILURE_SUMMARY,
10532
+ EMPTY_SCOPE_REASON: "no files in scope",
10533
+ NO_CONFIG_SKIP_REASON: `no ${DPRINT_CONFIG_FILENAME} at product root`
10534
+ };
10535
+ var FORMATTING_CONFIG_ERROR_MESSAGE = `${VALIDATION_STAGE_DISPLAY_NAMES.FORMATTING}: \u2717 config error`;
10536
+ async function formattingCommand(options) {
10537
+ const { cwd, files, quiet } = options;
10538
+ const startTime = Date.now();
10539
+ const loaded = await resolveConfig(cwd, [validationConfigDescriptor]);
10540
+ if (!loaded.ok) {
10541
+ return {
10542
+ exitCode: 1,
10543
+ output: `${FORMATTING_CONFIG_ERROR_MESSAGE} \u2014 ${loaded.error}`,
10544
+ durationMs: Date.now() - startTime
10545
+ };
10546
+ }
10547
+ const validationConfig = loaded.value[validationConfigDescriptor.section];
10548
+ if (!existsSync(join24(cwd, DPRINT_CONFIG_FILENAME))) {
10549
+ const output2 = quiet ? "" : `${VALIDATION_STAGE_DISPLAY_NAMES.FORMATTING}: skipped (${FORMATTING_COMMAND_OUTPUT.NO_CONFIG_SKIP_REASON})`;
10550
+ return { exitCode: 0, output: output2, durationMs: Date.now() - startTime };
10551
+ }
10552
+ const pathFilter = validationPathFilterForTool(
10553
+ validationConfig.paths,
10554
+ VALIDATION_PATH_TOOL_SUBSECTIONS.FORMATTING
10555
+ );
10556
+ const hasExplicitScope = files !== void 0 && files.length > 0;
10557
+ const scopedFiles = hasExplicitScope ? files.map((filePath) => isAbsolute3(filePath) ? relative3(cwd, filePath) : filePath).filter((relativePath) => pathPassesValidationFilter(relativePath, pathFilter)) : void 0;
10558
+ if (hasExplicitScope && (scopedFiles === void 0 || scopedFiles.length === 0)) {
10559
+ const output2 = quiet ? "" : `${VALIDATION_STAGE_DISPLAY_NAMES.FORMATTING}: skipped (${FORMATTING_COMMAND_OUTPUT.EMPTY_SCOPE_REASON})`;
10560
+ return { exitCode: 0, output: output2, durationMs: Date.now() - startTime };
10561
+ }
10562
+ const result = await validateFormatting({ projectRoot: cwd, files: scopedFiles });
10563
+ const durationMs = Date.now() - startTime;
10564
+ if (result.success) {
10565
+ return { exitCode: 0, output: quiet ? "" : FORMATTING_COMMAND_OUTPUT.NO_ISSUES, durationMs };
10566
+ }
10567
+ const detail = result.error ?? result.output;
10568
+ const output = [FORMATTING_COMMAND_OUTPUT.FAILURE_SUMMARY, detail].filter((line) => line.length > 0).join("\n");
10569
+ return { exitCode: 1, output, durationMs };
10570
+ }
10571
+
10572
+ // src/validation/languages/formatting.ts
10573
+ var FORMATTING_LANGUAGE_NAME = "formatting";
10574
+ var formattingValidationLanguage = {
10575
+ name: FORMATTING_LANGUAGE_NAME,
10576
+ stages: [
10577
+ {
10578
+ name: VALIDATION_STAGE_DISPLAY_NAMES.FORMATTING,
10579
+ failsPipeline: true,
10580
+ run: (context) => formattingCommand({ cwd: context.cwd, files: context.files, quiet: context.quiet })
10581
+ }
10582
+ ]
10583
+ };
10584
+
10585
+ // src/commands/validation/markdown.ts
10586
+ import { relative as relative4 } from "path";
10587
+
10055
10588
  // src/validation/steps/markdown.ts
10056
- import { existsSync, statSync } from "fs";
10057
- import { basename as basename4, dirname as dirname8, join as join24, relative as pathRelative } from "path";
10589
+ import { existsSync as existsSync2, statSync } from "fs";
10590
+ import { basename as basename4, dirname as dirname8, join as join25, relative as pathRelative } from "path";
10058
10591
  import { main as markdownlintMain } from "markdownlint-cli2";
10059
10592
  import relativeLinksRule from "markdownlint-rule-relative-links";
10060
10593
  var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
@@ -10093,7 +10626,7 @@ function buildMarkdownlintConfig(directoryName) {
10093
10626
  };
10094
10627
  }
10095
10628
  function getDefaultDirectories(projectRoot) {
10096
- return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join24(projectRoot, name)).filter((dir) => existsSync(dir));
10629
+ return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join25(projectRoot, name)).filter((dir) => existsSync2(dir));
10097
10630
  }
10098
10631
  function resolveMarkdownValidationTarget(path7, deps = defaultMarkdownValidationTargetDeps) {
10099
10632
  if (isExistingDirectory(path7, deps)) {
@@ -10176,7 +10709,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
10176
10709
  if (parsed) {
10177
10710
  errors.push({
10178
10711
  ...parsed,
10179
- file: join24(directory, parsed.file)
10712
+ file: join25(directory, parsed.file)
10180
10713
  });
10181
10714
  }
10182
10715
  }
@@ -10215,57 +10748,6 @@ function markdownlintConfigDirectoryName(directory, projectRoot) {
10215
10748
  return basename4(directory);
10216
10749
  }
10217
10750
 
10218
- // src/commands/validation/messages.ts
10219
- var VALIDATION_STAGE_DISPLAY_NAMES = {
10220
- CIRCULAR: "dependency-cruiser",
10221
- KNIP: "Knip",
10222
- ESLINT: "ESLint",
10223
- TYPESCRIPT: "TypeScript",
10224
- MARKDOWN: "Markdown",
10225
- LITERAL: "Literal"
10226
- };
10227
- var VALIDATION_SKIP_LABELS = {
10228
- VERB: "Skipping",
10229
- CIRCULAR_REASON: "skip-circular",
10230
- LITERAL_REASON: "skip-literal",
10231
- DISABLED_BY_PREFIX: "disabled by",
10232
- TYPESCRIPT_ABSENT_REASON: "TypeScript not detected in project",
10233
- VALIDATION_PATHS_NO_TARGETS_REASON: "validation paths matched no files",
10234
- MARKDOWN_NO_SCOPE_REASON: "no markdown files in --files scope",
10235
- MARKDOWN_NO_DEFAULT_DIRECTORIES_REASON: "no spx/ or docs/ directories found"
10236
- };
10237
- var VALIDATION_COMMAND_OUTPUT = {
10238
- CIRCULAR_FOUND: `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR}: circular dependencies found`,
10239
- CIRCULAR_NONE_FOUND: `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR}: \u2713 No circular dependencies found`,
10240
- KNIP_CONFIG_ERROR: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: \u2717 config error`,
10241
- KNIP_DISABLED: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: skipped (${VALIDATION_SKIP_LABELS.DISABLED_BY_PREFIX} validation.knip.enabled)`,
10242
- KNIP_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: \u2713 No unused code found`,
10243
- KNIP_FAILURE: "Unused code found",
10244
- ESLINT_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.ESLINT}: \u2713 No errors found`,
10245
- ESLINT_FAILURE: `${VALIDATION_STAGE_DISPLAY_NAMES.ESLINT} validation failed`,
10246
- ESLINT_MISSING_CONFIG: "ESLint config not found: project has tsconfig.json but no eslint.config.{ts,js,mjs,cjs}",
10247
- TYPESCRIPT_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.TYPESCRIPT}: \u2713 No type errors`,
10248
- TYPESCRIPT_FAILURE: `${VALIDATION_STAGE_DISPLAY_NAMES.TYPESCRIPT} validation failed`,
10249
- MARKDOWN_NO_ISSUES: `${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: No issues found`,
10250
- MARKDOWN_ERROR_SUMMARY_SUFFIX: "error(s) found"
10251
- };
10252
- var CIRCULAR_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR}: skipped (--${VALIDATION_SKIP_LABELS.CIRCULAR_REASON})`;
10253
- var CIRCULAR_SKIP_JSON_OUTPUT = JSON.stringify({
10254
- skipped: true,
10255
- reason: VALIDATION_SKIP_LABELS.CIRCULAR_REASON
10256
- });
10257
- var LITERAL_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.LITERAL}: skipped (--${VALIDATION_SKIP_LABELS.LITERAL_REASON})`;
10258
- var LITERAL_SKIP_JSON_OUTPUT = JSON.stringify({
10259
- skipped: true,
10260
- reason: VALIDATION_SKIP_LABELS.LITERAL_REASON
10261
- });
10262
- function formatTypeScriptAbsentSkipMessage(stageName) {
10263
- return `\u23ED ${VALIDATION_SKIP_LABELS.VERB} ${stageName} (${VALIDATION_SKIP_LABELS.TYPESCRIPT_ABSENT_REASON})`;
10264
- }
10265
- function formatValidationPathsNoTargetsSkipMessage(stageName) {
10266
- return `\u23ED ${VALIDATION_SKIP_LABELS.VERB} ${stageName} (${VALIDATION_SKIP_LABELS.VALIDATION_PATHS_NO_TARGETS_REASON})`;
10267
- }
10268
-
10269
10751
  // src/commands/validation/markdown.ts
10270
10752
  var MARKDOWN_COMMAND_OUTPUT = {
10271
10753
  ERROR_SUMMARY_SUFFIX: VALIDATION_COMMAND_OUTPUT.MARKDOWN_ERROR_SUMMARY_SUFFIX,
@@ -10295,7 +10777,7 @@ async function markdownCommand(options) {
10295
10777
  path: path7
10296
10778
  }));
10297
10779
  const targets = unfilteredTargets.filter(
10298
- (target) => pathPassesValidationFilter(relative3(cwd, target.path), pathFilter)
10780
+ (target) => pathPassesValidationFilter(relative4(cwd, target.path), pathFilter)
10299
10781
  );
10300
10782
  const skippedTargets = targetResolutions === void 0 ? [] : targetResolutions.map((resolution) => resolution.skipped).filter((skipped) => skipped !== void 0);
10301
10783
  const skippedOutput = quiet ? [] : skippedTargets.map(formatSkippedFileScope);
@@ -11204,8 +11686,8 @@ var ParseErrorCode;
11204
11686
  })(ParseErrorCode || (ParseErrorCode = {}));
11205
11687
 
11206
11688
  // src/validation/config/scope.ts
11207
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
11208
- import { isAbsolute as isAbsolute3, join as join25, relative as relative4, resolve as resolve6 } from "path";
11689
+ 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";
11209
11691
  var TSCONFIG_FILES = {
11210
11692
  full: "tsconfig.json",
11211
11693
  production: "tsconfig.production.json"
@@ -11234,7 +11716,7 @@ var TYPESCRIPT_FALLBACK_INCLUDE_PATTERNS = [
11234
11716
  var TEMPORARY_TSCONFIG_PARENT_SEGMENTS = ["node_modules", ".cache", "spx"];
11235
11717
  var defaultScopeDeps = {
11236
11718
  readFileSync: readFileSync3,
11237
- existsSync: existsSync2,
11719
+ existsSync: existsSync3,
11238
11720
  readdirSync
11239
11721
  };
11240
11722
  var EXPLICIT_TYPESCRIPT_SCOPE_TARGET_KIND = {
@@ -11242,7 +11724,7 @@ var EXPLICIT_TYPESCRIPT_SCOPE_TARGET_KIND = {
11242
11724
  FILE: "file"
11243
11725
  };
11244
11726
  function resolveProjectPath(projectRoot, path7) {
11245
- return isAbsolute3(path7) ? path7 : join25(projectRoot, path7);
11727
+ return isAbsolute4(path7) ? path7 : join26(projectRoot, path7);
11246
11728
  }
11247
11729
  function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
11248
11730
  try {
@@ -11281,12 +11763,12 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
11281
11763
  try {
11282
11764
  const items = deps.readdirSync(dirPath, { withFileTypes: true });
11283
11765
  const hasDirectTsFiles = items.some(
11284
- (item) => item.isFile() && pathHasTypeScriptSourceExtension(item.name) && pathPassesTypeScriptFileDiscoveryExcludes(join25(dirPath, item.name), options)
11766
+ (item) => item.isFile() && pathHasTypeScriptSourceExtension(item.name) && pathPassesTypeScriptFileDiscoveryExcludes(join26(dirPath, item.name), options)
11285
11767
  );
11286
11768
  if (hasDirectTsFiles) return true;
11287
11769
  const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
11288
11770
  for (const subdir of subdirs.slice(0, 5)) {
11289
- if (hasTypeScriptFilesRecursive(join25(dirPath, subdir.name), maxDepth - 1, deps, options)) {
11771
+ if (hasTypeScriptFilesRecursive(join26(dirPath, subdir.name), maxDepth - 1, deps, options)) {
11290
11772
  return true;
11291
11773
  }
11292
11774
  }
@@ -11323,7 +11805,7 @@ function directoryContributesTypeScriptScope(dir, config, projectRoot, deps) {
11323
11805
  return false;
11324
11806
  }
11325
11807
  try {
11326
- return hasTypeScriptFilesRecursive(join25(projectRoot, dir), 2, deps, {
11808
+ return hasTypeScriptFilesRecursive(join26(projectRoot, dir), 2, deps, {
11327
11809
  excludePatterns: config.exclude,
11328
11810
  projectRoot
11329
11811
  });
@@ -11552,13 +12034,13 @@ function normalizeActiveIncludePattern(pattern, projectRoot, deps) {
11552
12034
  function filterActiveIncludePatterns(patterns, excludePatterns, projectRoot, deps) {
11553
12035
  return patterns.map((pattern) => normalizeActiveIncludePattern(pattern, projectRoot, deps)).filter((pattern) => typeScriptScopePatternTargetsTypeScriptSource(pattern)).filter((pattern) => {
11554
12036
  const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
11555
- return topLevelDir === null || deps.existsSync(join25(projectRoot, topLevelDir));
12037
+ return topLevelDir === null || deps.existsSync(join26(projectRoot, topLevelDir));
11556
12038
  }).filter((pattern) => pathPassesValidationFilter(pattern, { exclude: excludePatterns }));
11557
12039
  }
11558
12040
  function getValidationDirectories(scope2, projectRoot, deps = defaultScopeDeps) {
11559
12041
  const config = resolveTypeScriptConfig(scope2, projectRoot, deps);
11560
12042
  const configDirectories = getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps);
11561
- const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join25(projectRoot, dir)));
12043
+ const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join26(projectRoot, dir)));
11562
12044
  return existingDirectories;
11563
12045
  }
11564
12046
  function getTypeScriptScope(scope2, projectRoot, deps = defaultScopeDeps) {
@@ -11579,14 +12061,14 @@ function pathPassesTypeScriptScope(path7, scopeConfig) {
11579
12061
  return included && !excluded;
11580
12062
  }
11581
12063
  function pathStaysInsideTypeScriptScopeRoot(projectRoot, path7) {
11582
- const resolvedPath = isAbsolute3(path7) ? resolve6(path7) : resolve6(projectRoot, path7);
11583
- const relativePath = relative4(projectRoot, resolvedPath);
12064
+ const resolvedPath = isAbsolute4(path7) ? resolve6(path7) : resolve6(projectRoot, path7);
12065
+ const relativePath = relative5(projectRoot, resolvedPath);
11584
12066
  const segments = normalizeTypeScriptScopePath(relativePath).split(PATH_SEGMENT_SEPARATOR3);
11585
- return relativePath.length === 0 || !segments.includes("..") && !isAbsolute3(relativePath);
12067
+ return relativePath.length === 0 || !segments.includes("..") && !isAbsolute4(relativePath);
11586
12068
  }
11587
12069
  function toProjectRelativeTypeScriptScopePath(projectRoot, path7) {
11588
- const resolvedPath = isAbsolute3(path7) ? resolve6(path7) : resolve6(projectRoot, path7);
11589
- const relativePath = relative4(projectRoot, resolvedPath);
12070
+ const resolvedPath = isAbsolute4(path7) ? resolve6(path7) : resolve6(projectRoot, path7);
12071
+ const relativePath = relative5(projectRoot, resolvedPath);
11590
12072
  return relativePath.length === 0 ? TYPESCRIPT_SCOPE_PROJECT_ROOT : normalizeTypeScriptScopePath(relativePath);
11591
12073
  }
11592
12074
  function toExplicitTypeScriptScopeTarget(projectRoot, originalPath, deps = defaultScopeDeps) {
@@ -11625,10 +12107,10 @@ function explicitTypeScriptScopeTargetPassesScope(target, scopeConfig) {
11625
12107
  if (typeScriptSourcePatterns.length > 0) {
11626
12108
  return typeScriptSourcePatterns.some((pattern) => typeScriptScopePatternIntersectsDirectory(pattern, target.path));
11627
12109
  }
11628
- return pathPassesTypeScriptScope(join25(target.path, TYPESCRIPT_SCOPE_DIRECTORY_PROBE_FILENAME), scopeConfig);
12110
+ return pathPassesTypeScriptScope(join26(target.path, TYPESCRIPT_SCOPE_DIRECTORY_PROBE_FILENAME), scopeConfig);
11629
12111
  }
11630
12112
  function directoryPassesTypeScriptExcludes(directory, scopeConfig) {
11631
- const probePath = join25(directory, TYPESCRIPT_SCOPE_DIRECTORY_PROBE_FILENAME);
12113
+ const probePath = join26(directory, TYPESCRIPT_SCOPE_DIRECTORY_PROBE_FILENAME);
11632
12114
  return !scopeConfig.excludePatterns.some(
11633
12115
  (pattern) => typeScriptScopePatternCoversDirectorySourceSet(pattern, directory) || pathMatchesTypeScriptPattern(probePath, pattern)
11634
12116
  );
@@ -11819,10 +12301,10 @@ function resolvedModulePath(resolvedPath) {
11819
12301
  return resolvedPath;
11820
12302
  }
11821
12303
  }
11822
- function nearestPackageRoot(filePath, existsSync7) {
12304
+ function nearestPackageRoot(filePath, existsSync8) {
11823
12305
  let currentDirectory = path6.dirname(filePath);
11824
12306
  while (true) {
11825
- if (existsSync7(path6.join(currentDirectory, PACKAGE_MANIFEST_FILENAME))) {
12307
+ if (existsSync8(path6.join(currentDirectory, PACKAGE_MANIFEST_FILENAME))) {
11826
12308
  return currentDirectory;
11827
12309
  }
11828
12310
  const parentDirectory = path6.dirname(currentDirectory);
@@ -11832,9 +12314,9 @@ function nearestPackageRoot(filePath, existsSync7) {
11832
12314
  currentDirectory = parentDirectory;
11833
12315
  }
11834
12316
  }
11835
- function bundledToolPath(resolvedPath, existsSync7) {
12317
+ function bundledToolPath(resolvedPath, existsSync8) {
11836
12318
  const bundledFilePath = resolvedModulePath(resolvedPath);
11837
- return nearestPackageRoot(bundledFilePath, existsSync7) ?? path6.dirname(bundledFilePath);
12319
+ return nearestPackageRoot(bundledFilePath, existsSync8) ?? path6.dirname(bundledFilePath);
11838
12320
  }
11839
12321
  async function discoverTool(tool, options = {}) {
11840
12322
  const { projectRoot = process.cwd(), deps = defaultToolDiscoveryDeps } = options;
@@ -11891,7 +12373,7 @@ import {
11891
12373
  cruise as dependencyCruiser
11892
12374
  } from "dependency-cruiser";
11893
12375
  import extractTypeScriptConfig from "dependency-cruiser/config-utl/extract-ts-config";
11894
- import { join as join26 } from "path";
12376
+ import { join as join27 } from "path";
11895
12377
  var DEPENDENCY_CRUISER_MODULE_SYSTEMS = ["es6", "cjs"];
11896
12378
  var DEPENDENCY_CRUISER_TYPESCRIPT_SOURCE_GLOB_SUFFIXES = [...TYPESCRIPT_FALLBACK_INCLUDE_PATTERNS];
11897
12379
  var DEPENDENCY_CRUISER_TYPESCRIPT_SOURCE_PATTERN = String.raw`(?<!\.d)\.(?:[cm]?ts|tsx)$`;
@@ -12131,7 +12613,7 @@ async function validateCircularDependencies(scope2, typescriptScope, projectRoot
12131
12613
  if (analyzeSourcePatterns.length === 0) {
12132
12614
  return { success: true };
12133
12615
  }
12134
- const tsConfigFile = join26(projectRoot, TSCONFIG_FILES[scope2]);
12616
+ const tsConfigFile = join27(projectRoot, TSCONFIG_FILES[scope2]);
12135
12617
  const result = await deps.dependencyCruiser(
12136
12618
  analyzeSourcePatterns,
12137
12619
  buildDependencyCruiserOptions(typescriptScope, projectRoot, tsConfigFile),
@@ -12251,9 +12733,9 @@ async function circularCommand(options, deps = defaultCircularCommandDeps) {
12251
12733
  }
12252
12734
 
12253
12735
  // src/validation/steps/knip.ts
12254
- import { existsSync as existsSync3 } from "fs";
12255
- import { mkdir as mkdir5, mkdtemp as mkdtemp2, rm, writeFile as writeFile3 } from "fs/promises";
12256
- import { isAbsolute as isAbsolute4, join as join27 } from "path";
12736
+ import { existsSync as existsSync4 } from "fs";
12737
+ import { mkdir as mkdir5, mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
12738
+ import { isAbsolute as isAbsolute5, join as join28 } from "path";
12257
12739
  var defaultKnipProcessRunner = lifecycleProcessRunner;
12258
12740
  var KNIP_COMMAND_TOKENS = {
12259
12741
  COMMAND: "knip",
@@ -12262,10 +12744,10 @@ var KNIP_COMMAND_TOKENS = {
12262
12744
  USE_TSCONFIG_FILES_FLAG: "--use-tsconfig-files"
12263
12745
  };
12264
12746
  var defaultKnipDeps = {
12265
- existsSync: existsSync3,
12747
+ existsSync: existsSync4,
12266
12748
  mkdir: mkdir5,
12267
12749
  mkdtemp: mkdtemp2,
12268
- rm,
12750
+ rm: rm2,
12269
12751
  writeFile: writeFile3
12270
12752
  };
12271
12753
  async function validateKnip2(context, runner = defaultKnipProcessRunner, deps = defaultKnipDeps) {
@@ -12286,7 +12768,7 @@ async function validateKnip2(context, runner = defaultKnipProcessRunner, deps =
12286
12768
  }
12287
12769
  async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
12288
12770
  const scopedTsconfig = typescriptScope.filteredByValidationPaths ? await createScopedKnipTsconfig(projectRoot, typescriptScope, deps) : void 0;
12289
- const localBin = join27(projectRoot, "node_modules", ".bin", "knip");
12771
+ const localBin = join28(projectRoot, "node_modules", ".bin", "knip");
12290
12772
  const binary = deps.existsSync(localBin) ? localBin : KNIP_COMMAND_TOKENS.NPX_COMMAND;
12291
12773
  const baseArgs = scopedTsconfig === void 0 ? [] : [
12292
12774
  KNIP_COMMAND_TOKENS.USE_TSCONFIG_FILES_FLAG,
@@ -12341,11 +12823,11 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
12341
12823
  });
12342
12824
  }
12343
12825
  async function createScopedKnipTsconfig(projectRoot, typescriptScope, deps) {
12344
- const tempParentDir = join27(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
12826
+ const tempParentDir = join28(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
12345
12827
  await deps.mkdir(tempParentDir, { recursive: true });
12346
- const tempDir = await deps.mkdtemp(join27(tempParentDir, "validate-knip-"));
12347
- const configPath = join27(tempDir, TSCONFIG_FILES.full);
12348
- const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join27(projectRoot, pattern);
12828
+ const tempDir = await deps.mkdtemp(join28(tempParentDir, "validate-knip-"));
12829
+ const configPath = join28(tempDir, TSCONFIG_FILES.full);
12830
+ const toProjectPathPattern = (pattern) => isAbsolute5(pattern) ? pattern : join28(projectRoot, pattern);
12349
12831
  const project = [
12350
12832
  ...typescriptScope.directories.flatMap(
12351
12833
  (directory) => TYPESCRIPT_FALLBACK_INCLUDE_PATTERNS.map((pattern) => `${directory}/${pattern}`)
@@ -12353,7 +12835,7 @@ async function createScopedKnipTsconfig(projectRoot, typescriptScope, deps) {
12353
12835
  ...typescriptScope.filePatterns
12354
12836
  ];
12355
12837
  const config = {
12356
- extends: join27(projectRoot, TSCONFIG_FILES.full),
12838
+ extends: join28(projectRoot, TSCONFIG_FILES.full),
12357
12839
  include: project.map(toProjectPathPattern),
12358
12840
  exclude: typescriptScope.excludePatterns.map(toProjectPathPattern)
12359
12841
  };
@@ -12419,13 +12901,13 @@ async function knipCommand(options, deps = defaultKnipCommandDeps) {
12419
12901
  }
12420
12902
 
12421
12903
  // src/validation/steps/eslint.ts
12422
- import { existsSync as existsSync5 } from "fs";
12423
- import { join as join29 } from "path";
12904
+ import { existsSync as existsSync6 } from "fs";
12905
+ import { join as join30 } from "path";
12424
12906
 
12425
12907
  // src/validation/lint-policy.ts
12426
12908
  import { execFileSync } from "child_process";
12427
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
12428
- import { join as join28 } from "path";
12909
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
12910
+ import { join as join29 } from "path";
12429
12911
 
12430
12912
  // src/validation/lint-policy-constants.ts
12431
12913
  var LINT_POLICY_MANIFESTS = {
@@ -12471,19 +12953,19 @@ var BASE_BRANCH_REFS = [LINT_POLICY_BASE_REFS.REMOTE_MAIN, LINT_POLICY_BASE_REFS
12471
12953
  function isSpecTreeNodePath(entry) {
12472
12954
  return NODE_SUFFIXES.some((suffix) => entry.endsWith(suffix));
12473
12955
  }
12474
- function readManifest(productDir, file, key) {
12956
+ function readManifest2(productDir, file, key) {
12475
12957
  return parseLintPolicyManifest(
12476
- readFileSync4(join28(productDir, file), "utf-8"),
12958
+ readFileSync4(join29(productDir, file), "utf-8"),
12477
12959
  file,
12478
12960
  key
12479
12961
  );
12480
12962
  }
12481
12963
  function manifestExists(productDir, file) {
12482
- return existsSync4(join28(productDir, file));
12964
+ return existsSync5(join29(productDir, file));
12483
12965
  }
12484
12966
  function findDeprecatedSpecNodePath(productDir) {
12485
12967
  function visit2(relativeDirectory) {
12486
- const absoluteDirectory = join28(productDir, relativeDirectory);
12968
+ const absoluteDirectory = join29(productDir, relativeDirectory);
12487
12969
  for (const entry of readdirSync2(absoluteDirectory, { withFileTypes: true })) {
12488
12970
  if (!entry.isDirectory()) {
12489
12971
  continue;
@@ -12499,8 +12981,8 @@ function findDeprecatedSpecNodePath(productDir) {
12499
12981
  }
12500
12982
  return void 0;
12501
12983
  }
12502
- const specTreeRootPath = join28(productDir, SPEC_TREE_ROOT);
12503
- if (!existsSync4(specTreeRootPath)) {
12984
+ const specTreeRootPath = join29(productDir, SPEC_TREE_ROOT);
12985
+ if (!existsSync5(specTreeRootPath)) {
12504
12986
  return void 0;
12505
12987
  }
12506
12988
  return visit2(SPEC_TREE_ROOT);
@@ -12525,8 +13007,8 @@ function assertManifestEntries(productDir, file, entries, suffixPredicate, suffi
12525
13007
  if (!suffixPredicate(entry)) {
12526
13008
  throw new Error(`${file} entry must ${suffixDescription}: ${entry}`);
12527
13009
  }
12528
- const absoluteEntry = join28(productDir, entry);
12529
- if (!existsSync4(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
13010
+ const absoluteEntry = join29(productDir, entry);
13011
+ if (!existsSync5(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
12530
13012
  throw new Error(`${file} entry does not exist as a directory: ${entry}`);
12531
13013
  }
12532
13014
  }
@@ -12658,11 +13140,11 @@ function validateLintPolicy(productDir) {
12658
13140
  }
12659
13141
  validateTestLintDebtNodeManifest(
12660
13142
  productDir,
12661
- readManifest(productDir, TEST_LINT_DEBT_NODE_MANIFEST_FILE, TEST_LINT_DEBT_NODE_MANIFEST_KEY)
13143
+ readManifest2(productDir, TEST_LINT_DEBT_NODE_MANIFEST_FILE, TEST_LINT_DEBT_NODE_MANIFEST_KEY)
12662
13144
  );
12663
13145
  validateTestOwnedConstantDebtNodeManifest(
12664
13146
  productDir,
12665
- readManifest(
13147
+ readManifest2(
12666
13148
  productDir,
12667
13149
  TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_FILE,
12668
13150
  TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_KEY
@@ -12674,36 +13156,6 @@ function validateLintPolicy(productDir) {
12674
13156
  }
12675
13157
  }
12676
13158
 
12677
- // src/validation/steps/subprocess-output.ts
12678
- var VALIDATION_SUBPROCESS_EVENTS = {
12679
- CLOSE: "close",
12680
- DATA: "data",
12681
- DRAIN: "drain",
12682
- ERROR: "error"
12683
- };
12684
- var defaultValidationSubprocessOutputStreams = {
12685
- stdout: process.stdout,
12686
- stderr: process.stderr
12687
- };
12688
- function forwardValidationSubprocessOutput(child, streams = defaultValidationSubprocessOutputStreams) {
12689
- child.stdout?.on(VALIDATION_SUBPROCESS_EVENTS.DATA, (chunk) => {
12690
- forwardChunkWithBackpressure(child.stdout, streams.stdout, chunk);
12691
- });
12692
- child.stderr?.on(VALIDATION_SUBPROCESS_EVENTS.DATA, (chunk) => {
12693
- forwardChunkWithBackpressure(child.stderr, streams.stderr, chunk);
12694
- });
12695
- }
12696
- function forwardChunkWithBackpressure(source, stream, chunk) {
12697
- const ready = stream.write(chunk);
12698
- if (ready || stream.once === void 0 || source === null || source === void 0) {
12699
- return;
12700
- }
12701
- source.pause();
12702
- stream.once(VALIDATION_SUBPROCESS_EVENTS.DRAIN, () => {
12703
- source.resume();
12704
- });
12705
- }
12706
-
12707
13159
  // src/validation/steps/eslint.ts
12708
13160
  var defaultEslintProcessRunner = lifecycleProcessRunner;
12709
13161
  var DEFAULT_ESLINT_CONFIG_FILE = "eslint.config.ts";
@@ -12771,8 +13223,8 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
12771
13223
  scopeConfig: context.scopeConfig
12772
13224
  });
12773
13225
  return new Promise((resolve8) => {
12774
- const localBin = join29(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
12775
- const binary = existsSync5(localBin) ? localBin : "npx";
13226
+ const localBin = join30(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
13227
+ const binary = existsSync6(localBin) ? localBin : "npx";
12776
13228
  const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
12777
13229
  const eslintProcess = spawnManagedSubprocess(runner, binary, spawnArgs, {
12778
13230
  cwd: projectRoot
@@ -12873,7 +13325,7 @@ async function lintCommand(options) {
12873
13325
 
12874
13326
  // src/validation/literal/index.ts
12875
13327
  import { readFile as readFile9 } from "fs/promises";
12876
- import { isAbsolute as isAbsolute5, relative as relative6, resolve as resolve7 } from "path";
13328
+ import { isAbsolute as isAbsolute6, relative as relative7, resolve as resolve7 } from "path";
12877
13329
 
12878
13330
  // src/lib/file-inclusion/predicates/ignore-source.ts
12879
13331
  var IGNORE_SOURCE_LAYER = "ignore-source";
@@ -12907,18 +13359,18 @@ var ignoreSourceLayer = makeLayer(
12907
13359
  );
12908
13360
 
12909
13361
  // src/lib/file-inclusion/pipeline.ts
12910
- import { readdir as readdir7 } from "fs/promises";
12911
- import { join as join30, relative as relative5, sep as sep2 } from "path";
13362
+ import { readdir as readdir8 } from "fs/promises";
13363
+ import { join as join31, relative as relative6, sep as sep2 } from "path";
12912
13364
  var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
12913
- function isNodeError3(err) {
13365
+ function isNodeError4(err) {
12914
13366
  return err instanceof Error && "code" in err;
12915
13367
  }
12916
13368
  async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
12917
13369
  let dirEntries;
12918
13370
  try {
12919
- dirEntries = await readdir7(absoluteDir, { withFileTypes: true });
13371
+ dirEntries = await readdir8(absoluteDir, { withFileTypes: true });
12920
13372
  } catch (err) {
12921
- if (isNodeError3(err) && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
13373
+ if (isNodeError4(err) && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
12922
13374
  return;
12923
13375
  }
12924
13376
  throw err;
@@ -12926,11 +13378,11 @@ async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
12926
13378
  for (const entry of dirEntries) {
12927
13379
  if (entry.isDirectory()) {
12928
13380
  if (artifactDirs.has(entry.name)) continue;
12929
- const absolutePath = join30(absoluteDir, entry.name);
13381
+ const absolutePath = join31(absoluteDir, entry.name);
12930
13382
  await collectPaths(absolutePath, projectRoot, result, artifactDirs);
12931
13383
  } else if (entry.isFile()) {
12932
- const absolutePath = join30(absoluteDir, entry.name);
12933
- const rel = relative5(projectRoot, absolutePath);
13384
+ const absolutePath = join31(absoluteDir, entry.name);
13385
+ const rel = relative6(projectRoot, absolutePath);
12934
13386
  result.push(sep2 === "/" ? rel : rel.split(sep2).join("/"));
12935
13387
  }
12936
13388
  }
@@ -13336,8 +13788,8 @@ async function validateLiteralReuse(input) {
13336
13788
  const config = input.config ?? literalConfigDescriptor.defaults;
13337
13789
  const request = input.files ? {
13338
13790
  explicit: input.files.map((f) => {
13339
- const abs = isAbsolute5(f) ? f : resolve7(input.productDir, f);
13340
- return relative6(input.productDir, abs).split(/[\\/]/g).join("/");
13791
+ const abs = isAbsolute6(f) ? f : resolve7(input.productDir, f);
13792
+ return relative7(input.productDir, abs).split(/[\\/]/g).join("/");
13341
13793
  })
13342
13794
  } : { walkRoot: input.productDir };
13343
13795
  const scope2 = await runPipeline(
@@ -13358,7 +13810,7 @@ async function validateLiteralReuse(input) {
13358
13810
  const testOccurrencesByFile = /* @__PURE__ */ new Map();
13359
13811
  const indexedOccurrencesByFile = /* @__PURE__ */ new Map();
13360
13812
  for (const abs of candidateFiles) {
13361
- const rel = relative6(input.productDir, abs).split(/[\\/]/g).join("/");
13813
+ const rel = relative7(input.productDir, abs).split(/[\\/]/g).join("/");
13362
13814
  const content = await readSafe(abs);
13363
13815
  if (content === null) continue;
13364
13816
  const occurrences = collectLiterals(content, rel, collectOptions);
@@ -13596,23 +14048,23 @@ function formatLoc(loc) {
13596
14048
  }
13597
14049
 
13598
14050
  // src/validation/steps/typescript.ts
13599
- import { existsSync as existsSync6, mkdirSync, rmSync, writeFileSync } from "fs";
14051
+ import { existsSync as existsSync7, mkdirSync, rmSync, writeFileSync } from "fs";
13600
14052
  import { mkdtemp as mkdtemp3 } from "fs/promises";
13601
- import { isAbsolute as isAbsolute6, join as join31 } from "path";
14053
+ import { isAbsolute as isAbsolute7, join as join32 } from "path";
13602
14054
  var defaultTypeScriptProcessRunner = lifecycleProcessRunner;
13603
14055
  var defaultTypeScriptDeps = {
13604
14056
  mkdtemp: mkdtemp3,
13605
14057
  mkdirSync,
13606
14058
  writeFileSync,
13607
14059
  rmSync,
13608
- existsSync: existsSync6
14060
+ existsSync: existsSync7
13609
14061
  };
13610
14062
  var TEMPORARY_TSCONFIG_COMPILER_OPTIONS = { noEmit: true };
13611
14063
  var TEMPORARY_TSCONFIG_DIR_PREFIX = "validate-ts-";
13612
14064
  async function createTemporaryTsconfigDir(projectRoot, deps) {
13613
- const parent = join31(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
14065
+ const parent = join32(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
13614
14066
  deps.mkdirSync(parent, { recursive: true });
13615
- return deps.mkdtemp(join31(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
14067
+ return deps.mkdtemp(join32(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
13616
14068
  }
13617
14069
  function buildTypeScriptArgs(context) {
13618
14070
  const { scope: scope2, configFile } = context;
@@ -13620,11 +14072,11 @@ function buildTypeScriptArgs(context) {
13620
14072
  }
13621
14073
  async function createFileSpecificTsconfig(scope2, files, projectRoot, deps = defaultTypeScriptDeps) {
13622
14074
  const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
13623
- const configPath = join31(tempDir, "tsconfig.json");
14075
+ const configPath = join32(tempDir, "tsconfig.json");
13624
14076
  const baseConfigFile = TSCONFIG_FILES[scope2];
13625
- const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join31(projectRoot, file));
14077
+ const absoluteFiles = files.map((file) => isAbsolute7(file) ? file : join32(projectRoot, file));
13626
14078
  const tempConfig = {
13627
- extends: join31(projectRoot, baseConfigFile),
14079
+ extends: join32(projectRoot, baseConfigFile),
13628
14080
  files: absoluteFiles,
13629
14081
  include: [],
13630
14082
  exclude: [],
@@ -13641,11 +14093,11 @@ async function createFileSpecificTsconfig(scope2, files, projectRoot, deps = def
13641
14093
  }
13642
14094
  async function createScopeFilteredTsconfig(scope2, projectRoot, scopeConfig, deps = defaultTypeScriptDeps) {
13643
14095
  const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
13644
- const configPath = join31(tempDir, "tsconfig.json");
14096
+ const configPath = join32(tempDir, "tsconfig.json");
13645
14097
  const baseConfigFile = TSCONFIG_FILES[scope2];
13646
- const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join31(projectRoot, pattern);
14098
+ const toProjectPathPattern = (pattern) => isAbsolute7(pattern) ? pattern : join32(projectRoot, pattern);
13647
14099
  const tempConfig = {
13648
- extends: join31(projectRoot, baseConfigFile),
14100
+ extends: join32(projectRoot, baseConfigFile),
13649
14101
  include: scopeConfig.filePatterns.map(toProjectPathPattern),
13650
14102
  exclude: scopeConfig.excludePatterns.map(toProjectPathPattern),
13651
14103
  compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
@@ -13673,7 +14125,7 @@ async function validateTypeScript(context, options = {}) {
13673
14125
  const { configPath, cleanup } = await createFileSpecificTsconfig(scope2, files, projectRoot, deps);
13674
14126
  try {
13675
14127
  return await new Promise((resolve8) => {
13676
- const tscBin = join31(projectRoot, "node_modules", ".bin", "tsc");
14128
+ const tscBin = join32(projectRoot, "node_modules", ".bin", "tsc");
13677
14129
  const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
13678
14130
  const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
13679
14131
  const tscProcess = spawnManagedSubprocess(runner, tscBinary, tscArgs2, {
@@ -13706,7 +14158,7 @@ async function validateTypeScript(context, options = {}) {
13706
14158
  return { success: true, skipped: true };
13707
14159
  }
13708
14160
  const { configPath, cleanup } = await createScopeFilteredTsconfig(scope2, projectRoot, scopeConfig, deps);
13709
- const tscBin = join31(projectRoot, "node_modules", ".bin", "tsc");
14161
+ const tscBin = join32(projectRoot, "node_modules", ".bin", "tsc");
13710
14162
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
13711
14163
  tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
13712
14164
  return new Promise((resolve8) => {
@@ -13728,7 +14180,7 @@ async function validateTypeScript(context, options = {}) {
13728
14180
  });
13729
14181
  });
13730
14182
  } else {
13731
- const tscBin = join31(projectRoot, "node_modules", ".bin", "tsc");
14183
+ const tscBin = join32(projectRoot, "node_modules", ".bin", "tsc");
13732
14184
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
13733
14185
  const rawArgs = buildTypeScriptArgs({ scope: scope2, configFile });
13734
14186
  tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
@@ -13905,11 +14357,12 @@ var typescriptValidationLanguage = {
13905
14357
 
13906
14358
  // src/validation/registry.ts
13907
14359
  var validationRegistry = {
13908
- languages: [typescriptValidationLanguage, markdownValidationLanguage]
14360
+ languages: [typescriptValidationLanguage, markdownValidationLanguage, formattingValidationLanguage]
13909
14361
  };
13910
- var validationPipelineStages = validationRegistry.languages.flatMap(
13911
- (language) => language.stages
13912
- );
14362
+ function composeValidationPipelineStages(languages) {
14363
+ return languages.flatMap((language) => language.stages);
14364
+ }
14365
+ var validationPipelineStages = composeValidationPipelineStages(validationRegistry.languages);
13913
14366
  var VALIDATION_PIPELINE_TOTAL_STEPS = validationPipelineStages.length;
13914
14367
 
13915
14368
  // src/commands/validation/format.ts
@@ -13972,7 +14425,7 @@ async function allCommand(options) {
13972
14425
  // src/validation/literal/allowlist-existing.ts
13973
14426
  import { randomBytes } from "crypto";
13974
14427
  import { rename as rename4, writeFile as writeFile4 } from "fs/promises";
13975
- import { dirname as dirname9, join as join32 } from "path";
14428
+ import { dirname as dirname9, join as join33 } from "path";
13976
14429
  var EXIT_OK = 0;
13977
14430
  var EXIT_ERROR = 1;
13978
14431
  var INCLUDE_FIELD = "include";
@@ -13992,7 +14445,7 @@ var productionWriter = {
13992
14445
  async write(filePath, content) {
13993
14446
  const dir = dirname9(filePath);
13994
14447
  const random = randomBytes(RANDOM_TOKEN_BYTES).toString("hex");
13995
- const tmpPath = join32(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
14448
+ const tmpPath = join33(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
13996
14449
  await writeFile4(tmpPath, content, "utf8");
13997
14450
  await rename4(tmpPath, filePath);
13998
14451
  }
@@ -14098,6 +14551,10 @@ var validationCliDefinition = {
14098
14551
  alias: "md",
14099
14552
  description: "Validate markdown link integrity and structure"
14100
14553
  },
14554
+ format: {
14555
+ commandName: "format",
14556
+ description: "Check code formatting with dprint"
14557
+ },
14101
14558
  all: {
14102
14559
  commandName: "all",
14103
14560
  description: "Run all validations"
@@ -14278,6 +14735,16 @@ function registerValidationCommands(validationCmd) {
14278
14735
  process.exit(result.exitCode);
14279
14736
  });
14280
14737
  addCommonOptions(markdownCmd);
14738
+ const formatCmd = addValidationSubcommand(validationCmd, subcommands.format).action(async (options) => {
14739
+ const result = await formattingCommand({
14740
+ cwd: process.cwd(),
14741
+ files: options.files,
14742
+ quiet: options.quiet
14743
+ });
14744
+ if (result.output) console.log(result.output);
14745
+ process.exit(result.exitCode);
14746
+ });
14747
+ addCommonOptions(formatCmd);
14281
14748
  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) => {
14282
14749
  const result = await allCommand({
14283
14750
  cwd: process.cwd(),
@@ -14339,6 +14806,10 @@ var WORKTREE_STATUS_FORMAT = {
14339
14806
  JSON: "json",
14340
14807
  TEXT: "text"
14341
14808
  };
14809
+ var WORKTREE_STATUS_RENDER = {
14810
+ FREE: "-",
14811
+ RUNNING_PID_PREFIX: "PID "
14812
+ };
14342
14813
  async function statusCommand2(options) {
14343
14814
  const multiTargetRequest = options.worktrees !== void 0 && options.worktrees.length > 1;
14344
14815
  const targets = await resolveStatusTargets(options);
@@ -14346,9 +14817,13 @@ async function statusCommand2(options) {
14346
14817
  const records = [];
14347
14818
  for (const target of targets.value) {
14348
14819
  const worktreesDir = await resolveWorktreesDir({ ...options, cwd: target.worktreeRoot });
14349
- const occupancy = await readOccupancy(worktreesDir, target.name, options.processTable, { fs: options.fs });
14350
- if (!occupancy.ok) return occupancy;
14351
- records.push({ worktree: target.name, status: occupancy.value });
14820
+ const claimResult = await readClaim(worktreesDir, target.name, { fs: options.fs });
14821
+ if (!claimResult.ok) return claimResult;
14822
+ const claim = claimResult.value;
14823
+ const status = classifyOccupancy(claim, options.processTable);
14824
+ records.push(
14825
+ status === OCCUPANCY_STATUS.RUNNING && claim !== void 0 ? { worktree: target.name, status, pid: claim.pid, session: claim.sessionId, host: claim.host } : { worktree: target.name, status }
14826
+ );
14352
14827
  }
14353
14828
  return { ok: true, value: renderStatus(records, options.format, multiTargetRequest) };
14354
14829
  }
@@ -14360,10 +14835,13 @@ async function resolveStatusTargets(options) {
14360
14835
  return { ok: true, value: [target.value] };
14361
14836
  }
14362
14837
  const targets = [];
14838
+ const seenRoots = /* @__PURE__ */ new Set();
14363
14839
  let firstError;
14364
14840
  for (const worktree of requested) {
14365
14841
  const target = await resolveTargetWorktree({ ...options, worktree });
14366
14842
  if (target.ok) {
14843
+ if (seenRoots.has(target.value.worktreeRoot)) continue;
14844
+ seenRoots.add(target.value.worktreeRoot);
14367
14845
  targets.push(target.value);
14368
14846
  } else {
14369
14847
  firstError ??= target.error;
@@ -14381,7 +14859,10 @@ function renderStatus(records, format2, multiTargetRequest) {
14381
14859
  return records.map(renderTextStatus).join("\n");
14382
14860
  }
14383
14861
  function renderTextStatus(record6) {
14384
- return `${record6.worktree} ${record6.status}`;
14862
+ if (record6.status === OCCUPANCY_STATUS.RUNNING) {
14863
+ return `${record6.worktree} ${WORKTREE_STATUS_RENDER.RUNNING_PID_PREFIX}${record6.pid} (${record6.session} @ ${record6.host})`;
14864
+ }
14865
+ return `${record6.worktree} ${WORKTREE_STATUS_RENDER.FREE}`;
14385
14866
  }
14386
14867
 
14387
14868
  // src/lib/worktree-path-info.ts
@@ -14429,7 +14910,7 @@ function registerWorktreeCommands(worktreeCmd) {
14429
14910
  });
14430
14911
  if (!result.ok) handleError3(result.error);
14431
14912
  });
14432
- worktreeCmd.command(`${WORKTREE_CLI.STATUS} ${WORKTREE_CLI.WORKTREE_ARGUMENT}`).description("Report a worktree's occupancy (occupied | unclaimed | stale)").option(`${WORKTREE_CLI.FORMAT_FLAG} <format>`, "Output format (text|json)", WORKTREE_STATUS_FORMAT.TEXT).option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (worktrees, options) => {
14913
+ 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) => {
14433
14914
  const result = await statusCommand2({
14434
14915
  cwd: process.cwd(),
14435
14916
  fs: defaultOccupancyFileSystem,