@outcomeeng/spx 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,264 +2,6 @@
2
2
  import { Command } from "commander";
3
3
  import { createRequire as createRequire2 } from "module";
4
4
 
5
- // src/domains/audit/paths.ts
6
- import { existsSync } from "fs";
7
- import { relative, resolve } from "path";
8
- var AUDIT_PATH_DEFECT = {
9
- ESCAPES_ROOT: "path escapes product directory",
10
- MISSING_FILE: "missing file"
11
- };
12
- function validatePaths(verdict, productDir) {
13
- const defects = [];
14
- const root = resolve(productDir);
15
- for (const gate of verdict.gates) {
16
- for (const finding of gate.findings) {
17
- checkPath(finding.spec_file, root, defects);
18
- checkPath(finding.test_file, root, defects);
19
- }
20
- }
21
- return defects;
22
- }
23
- function checkPath(filePath, root, defects) {
24
- if (!filePath) return;
25
- const rel = relative(root, resolve(root, filePath));
26
- if (rel.startsWith("..")) {
27
- defects.push(`${AUDIT_PATH_DEFECT.ESCAPES_ROOT}: ${filePath}`);
28
- return;
29
- }
30
- if (!existsSync(resolve(root, filePath))) {
31
- defects.push(`${AUDIT_PATH_DEFECT.MISSING_FILE}: ${filePath}`);
32
- }
33
- }
34
-
35
- // src/domains/audit/reader.ts
36
- import { readFile } from "fs/promises";
37
- import { XMLParser, XMLValidator } from "fast-xml-parser";
38
- var AUDIT_VERDICT_VALUE = {
39
- APPROVED: "APPROVED",
40
- REJECT: "REJECT"
41
- };
42
- var AUDIT_GATE_STATUS = {
43
- FAIL: "FAIL",
44
- PASS: "PASS",
45
- SKIPPED: "SKIPPED"
46
- };
47
- var AUDIT_VERDICT_XML = {
48
- ROOT: "audit_verdict",
49
- HEADER: "header",
50
- SPEC_NODE: "spec_node",
51
- VERDICT: "verdict",
52
- TIMESTAMP: "timestamp",
53
- GATES: "gates",
54
- GATE: "gate",
55
- NAME: "name",
56
- STATUS: "status",
57
- SKIPPED_REASON: "skipped_reason",
58
- FINDINGS: "findings",
59
- FINDING: "finding",
60
- COUNT: "count",
61
- PARSED_COUNT: "@_count",
62
- SPEC_FILE: "spec_file",
63
- TEST_FILE: "test_file"
64
- };
65
- var ARRAY_TAGS = /* @__PURE__ */ new Set([AUDIT_VERDICT_XML.GATE, AUDIT_VERDICT_XML.FINDING]);
66
- var PARSER_OPTIONS = {
67
- ignoreAttributes: false,
68
- attributeNamePrefix: "@_",
69
- parseTagValue: false,
70
- parseAttributeValue: false,
71
- isArray: (name) => ARRAY_TAGS.has(name)
72
- };
73
- async function readVerdictFile(filePath) {
74
- let xml;
75
- try {
76
- xml = await readFile(filePath, "utf-8");
77
- } catch (cause) {
78
- throw new Error(`Failed to read verdict file: ${filePath}`, { cause });
79
- }
80
- const validation = XMLValidator.validate(xml);
81
- if (validation !== true) {
82
- throw new Error(
83
- `Malformed XML in verdict file: ${filePath}: ${validation.err.msg}`
84
- );
85
- }
86
- const parser = new XMLParser(PARSER_OPTIONS);
87
- const parsed = parser.parse(xml);
88
- return buildVerdict(parsed);
89
- }
90
- function isObject(value) {
91
- return typeof value === "object" && value !== null;
92
- }
93
- function asString(value) {
94
- return typeof value === "string" ? value : void 0;
95
- }
96
- function buildVerdict(parsed) {
97
- const rootUnknown = parsed[AUDIT_VERDICT_XML.ROOT];
98
- if (!isObject(rootUnknown)) {
99
- return { gates: [] };
100
- }
101
- const headerUnknown = rootUnknown[AUDIT_VERDICT_XML.HEADER];
102
- const gatesUnknown = rootUnknown[AUDIT_VERDICT_XML.GATES];
103
- return {
104
- header: isObject(headerUnknown) ? buildHeader(headerUnknown) : void 0,
105
- gates: isObject(gatesUnknown) ? buildGates(gatesUnknown) : []
106
- };
107
- }
108
- function buildHeader(raw) {
109
- return {
110
- spec_node: asString(raw[AUDIT_VERDICT_XML.SPEC_NODE]),
111
- verdict: asString(raw[AUDIT_VERDICT_XML.VERDICT]),
112
- timestamp: asString(raw[AUDIT_VERDICT_XML.TIMESTAMP])
113
- };
114
- }
115
- function buildGates(raw) {
116
- const gatesUnknown = raw[AUDIT_VERDICT_XML.GATE];
117
- if (!Array.isArray(gatesUnknown)) return [];
118
- return gatesUnknown.filter(isObject).map(buildGate);
119
- }
120
- function buildGate(raw) {
121
- const findingsUnknown = raw[AUDIT_VERDICT_XML.FINDINGS];
122
- return {
123
- name: asString(raw[AUDIT_VERDICT_XML.NAME]),
124
- status: asString(raw[AUDIT_VERDICT_XML.STATUS]),
125
- skipped_reason: asString(raw[AUDIT_VERDICT_XML.SKIPPED_REASON]),
126
- count: isObject(findingsUnknown) ? asString(findingsUnknown[AUDIT_VERDICT_XML.PARSED_COUNT]) : void 0,
127
- findings: isObject(findingsUnknown) ? buildFindings(findingsUnknown) : []
128
- };
129
- }
130
- function buildFindings(raw) {
131
- const findingsUnknown = raw[AUDIT_VERDICT_XML.FINDING];
132
- if (!Array.isArray(findingsUnknown)) return [];
133
- return findingsUnknown.filter(isObject).map(buildFinding);
134
- }
135
- function buildFinding(raw) {
136
- return {
137
- spec_file: asString(raw[AUDIT_VERDICT_XML.SPEC_FILE]),
138
- test_file: asString(raw[AUDIT_VERDICT_XML.TEST_FILE])
139
- };
140
- }
141
-
142
- // src/domains/audit/semantic.ts
143
- function validateSemantics(verdict) {
144
- const defects = [];
145
- const hasFail = verdict.gates.some((g) => g.status === AUDIT_GATE_STATUS.FAIL);
146
- const hasSkipped = verdict.gates.some((g) => g.status === AUDIT_GATE_STATUS.SKIPPED);
147
- if (verdict.header?.verdict === AUDIT_VERDICT_VALUE.APPROVED && (hasFail || hasSkipped)) {
148
- defects.push("incoherent verdict: APPROVED but at least one gate is not PASS");
149
- } else if (verdict.header?.verdict === AUDIT_VERDICT_VALUE.REJECT && !hasFail && !hasSkipped) {
150
- defects.push("incoherent verdict: REJECT but all gates are PASS");
151
- }
152
- for (const gate of verdict.gates) {
153
- const label = gate.name ? `gate "${gate.name}"` : "gate";
154
- if (gate.status === AUDIT_GATE_STATUS.FAIL && gate.findings.length === 0) {
155
- defects.push(`failed gate has no findings: ${label}`);
156
- }
157
- if (gate.status === AUDIT_GATE_STATUS.SKIPPED && !gate.skipped_reason) {
158
- defects.push(`skipped gate missing reason: ${label}`);
159
- }
160
- }
161
- return defects;
162
- }
163
-
164
- // src/domains/audit/structural.ts
165
- var VALID_VERDICTS = new Set(Object.values(AUDIT_VERDICT_VALUE));
166
- var VALID_GATE_STATUSES = new Set(Object.values(AUDIT_GATE_STATUS));
167
- var STRUCTURAL_DEFECT_TEXT = {
168
- MISSING_REQUIRED_ELEMENT: "missing required element",
169
- INVALID_ENUM_VALUE: "invalid enum value"
170
- };
171
- function validateStructure(verdict) {
172
- const defects = [];
173
- if (!verdict.header) {
174
- defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <header>`);
175
- return defects;
176
- }
177
- if (!verdict.header.spec_node) {
178
- defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <spec_node>`);
179
- }
180
- if (!verdict.header.verdict) {
181
- defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <verdict> in <header>`);
182
- } else if (!VALID_VERDICTS.has(verdict.header.verdict)) {
183
- defects.push(
184
- `${STRUCTURAL_DEFECT_TEXT.INVALID_ENUM_VALUE}: verdict "${verdict.header.verdict}" is not one of APPROVED, REJECT`
185
- );
186
- }
187
- if (!verdict.header.timestamp) {
188
- defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <timestamp>`);
189
- }
190
- if (verdict.gates.length === 0) {
191
- defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: at least one <gate> in <gates>`);
192
- }
193
- for (const gate of verdict.gates) {
194
- const label = gate.name ? `gate "${gate.name}"` : "gate";
195
- if (gate.status !== void 0 && !VALID_GATE_STATUSES.has(gate.status)) {
196
- defects.push(
197
- `${STRUCTURAL_DEFECT_TEXT.INVALID_ENUM_VALUE}: ${label} status "${gate.status}" is not one of PASS, FAIL, SKIPPED`
198
- );
199
- }
200
- if (gate.count !== void 0) {
201
- const declared = parseInt(gate.count, 10);
202
- const actual = gate.findings.length;
203
- if (isNaN(declared) || declared !== actual) {
204
- defects.push(
205
- `count mismatch: ${label} declares count="${gate.count}" but has ${actual} finding(s)`
206
- );
207
- }
208
- }
209
- }
210
- return defects;
211
- }
212
-
213
- // src/domains/audit/verify.ts
214
- async function runVerifyPipeline(filePath, productDir) {
215
- let verdict;
216
- try {
217
- verdict = await readVerdictFile(filePath);
218
- } catch (error) {
219
- const message = error instanceof Error ? error.message : String(error);
220
- return { lines: [`reader: ${message}`], exitCode: 1 };
221
- }
222
- const structuralDefects = validateStructure(verdict);
223
- if (structuralDefects.length > 0) {
224
- return { lines: structuralDefects.map((d) => `structural: ${d}`), exitCode: 1 };
225
- }
226
- const semanticDefects = validateSemantics(verdict);
227
- if (semanticDefects.length > 0) {
228
- return { lines: semanticDefects.map((d) => `semantic: ${d}`), exitCode: 1 };
229
- }
230
- const pathDefects = validatePaths(verdict, productDir);
231
- if (pathDefects.length > 0) {
232
- return { lines: pathDefects.map((d) => `paths: ${d}`), exitCode: 1 };
233
- }
234
- return { lines: [], exitCode: 0, verdict: verdict.header?.verdict };
235
- }
236
-
237
- // src/commands/audit/verify.ts
238
- async function runVerifyCommand(filePath, productDir, writeLine) {
239
- const result = await runVerifyPipeline(filePath, productDir);
240
- if (result.exitCode === 0) {
241
- writeLine(result.verdict ?? "");
242
- } else {
243
- for (const line of result.lines) {
244
- writeLine(line);
245
- }
246
- }
247
- return result.exitCode;
248
- }
249
-
250
- // src/interfaces/cli/audit.ts
251
- var auditDomain = {
252
- name: "audit",
253
- description: "Audit verdict verification",
254
- register: (program2) => {
255
- const auditCmd = program2.command("audit").description("Audit verdict verification");
256
- auditCmd.command("verify <file>").description("Verify an audit verdict XML file").action(async (file) => {
257
- const exitCode = await runVerifyCommand(file, process.cwd(), console.log);
258
- process.exit(exitCode);
259
- });
260
- }
261
- };
262
-
263
5
  // src/commands/claude/init.ts
264
6
  import { execa } from "execa";
265
7
  async function initCommand(options = {}) {
@@ -762,149 +504,988 @@ async function writeSettings(filePath, settings, deps = { fs: realFs }) {
762
504
  `settings-${Date.now()}-${Math.random().toString(36).substring(7)}.json`
763
505
  );
764
506
  try {
765
- const content = JSON.stringify(settings, null, 2) + "\n";
766
- await deps.fs.writeFile(tempPath, content);
767
- await deps.fs.rename(tempPath, filePath);
507
+ const content = JSON.stringify(settings, null, 2) + "\n";
508
+ await deps.fs.writeFile(tempPath, content);
509
+ await deps.fs.rename(tempPath, filePath);
510
+ } catch (error) {
511
+ try {
512
+ await deps.fs.unlink(tempPath);
513
+ } catch {
514
+ }
515
+ if (error instanceof Error) {
516
+ throw new Error(`Failed to write settings: ${error.message}`);
517
+ }
518
+ throw error;
519
+ }
520
+ }
521
+
522
+ // src/commands/claude/settings/consolidate.ts
523
+ import os2 from "os";
524
+ import path3 from "path";
525
+ async function consolidateCommand(options = {}) {
526
+ const root = options.root ? path3.resolve(options.root.replace(/^~/, os2.homedir())) : path3.join(os2.homedir(), "Code");
527
+ const globalSettingsPath = options.globalSettings || path3.join(os2.homedir(), ".claude", "settings.json");
528
+ const shouldWrite = options.write || false;
529
+ const outputFile = options.outputFile;
530
+ const previewOnly = !shouldWrite && !outputFile;
531
+ const settingsFiles = await findSettingsFiles(root);
532
+ if (settingsFiles.length === 0) {
533
+ return `No settings files found in ${root}
534
+
535
+ Searched for: **/.claude/settings.local.json`;
536
+ }
537
+ const localPermissions = await parseAllSettings(settingsFiles);
538
+ let globalSettings = await parseSettingsFile(globalSettingsPath);
539
+ if (!globalSettings) {
540
+ globalSettings = {
541
+ permissions: {
542
+ allow: [],
543
+ deny: [],
544
+ ask: []
545
+ }
546
+ };
547
+ }
548
+ if (!globalSettings.permissions) {
549
+ globalSettings.permissions = {
550
+ allow: [],
551
+ deny: [],
552
+ ask: []
553
+ };
554
+ }
555
+ const { merged, result } = mergePermissions(
556
+ globalSettings.permissions,
557
+ localPermissions
558
+ );
559
+ if (shouldWrite) {
560
+ try {
561
+ result.backupPath = await createBackup(globalSettingsPath);
562
+ } catch (error) {
563
+ if (error instanceof Error && !error.message.includes("not found")) {
564
+ throw error;
565
+ }
566
+ }
567
+ }
568
+ if (shouldWrite) {
569
+ const updatedSettings = {
570
+ ...globalSettings,
571
+ permissions: merged
572
+ };
573
+ await writeSettings(globalSettingsPath, updatedSettings);
574
+ } else if (outputFile) {
575
+ const updatedSettings = {
576
+ ...globalSettings,
577
+ permissions: merged
578
+ };
579
+ const resolvedOutputPath = path3.resolve(outputFile.replace(/^~/, os2.homedir()));
580
+ await writeSettings(resolvedOutputPath, updatedSettings);
581
+ result.outputPath = resolvedOutputPath;
582
+ }
583
+ return formatReport(result, previewOnly, globalSettingsPath, outputFile);
584
+ }
585
+
586
+ // src/interfaces/cli/claude.ts
587
+ function registerClaudeCommands(claudeCmd) {
588
+ claudeCmd.command("init").description("Initialize or update outcomeeng marketplace plugin").action(async () => {
589
+ try {
590
+ const output = await initCommand({ cwd: process.cwd() });
591
+ console.log(output);
592
+ } catch (error) {
593
+ console.error(
594
+ "Error:",
595
+ error instanceof Error ? error.message : String(error)
596
+ );
597
+ process.exit(1);
598
+ }
599
+ });
600
+ const settingsCmd = claudeCmd.command("settings").description("Manage Claude Code settings");
601
+ settingsCmd.command("consolidate").description(
602
+ "Consolidate permissions from project-specific settings into global settings"
603
+ ).option("--write", "Write changes to global settings file (default: preview only)").option(
604
+ "--output-file <path>",
605
+ "Write merged settings to specified file instead of global settings"
606
+ ).option(
607
+ "--root <path>",
608
+ "Root directory to scan for settings files (default: ~/Code)"
609
+ ).option(
610
+ "--global-settings <path>",
611
+ "Path to global settings file (default: ~/.claude/settings.json)"
612
+ ).action(
613
+ async (options) => {
614
+ try {
615
+ if (options.write && options.outputFile) {
616
+ console.error(
617
+ "Error: --write and --output-file are mutually exclusive\nUse --write to modify global settings, or --output-file to write to a different location"
618
+ );
619
+ process.exit(1);
620
+ }
621
+ const output = await consolidateCommand({
622
+ write: options.write,
623
+ outputFile: options.outputFile,
624
+ root: options.root,
625
+ globalSettings: options.globalSettings
626
+ });
627
+ console.log(output);
628
+ } catch (error) {
629
+ console.error(
630
+ "Error:",
631
+ error instanceof Error ? error.message : String(error)
632
+ );
633
+ process.exit(1);
634
+ }
635
+ }
636
+ );
637
+ }
638
+ var claudeDomain = {
639
+ name: "claude",
640
+ description: "Manage Claude Code settings and plugins",
641
+ register: (program2) => {
642
+ const claudeCmd = program2.command("claude").description("Manage Claude Code settings and plugins");
643
+ registerClaudeCommands(claudeCmd);
644
+ }
645
+ };
646
+
647
+ // src/domains/compact/index.ts
648
+ import { join as join3 } from "path";
649
+
650
+ // src/domains/session/agent-session.ts
651
+ import { createHash } from "crypto";
652
+ var AGENT_SESSION_ENV = {
653
+ CLAUDE_SESSION_ID: "CLAUDE_SESSION_ID",
654
+ CODEX_THREAD_ID: "CODEX_THREAD_ID"
655
+ };
656
+ var AGENT_SESSION_TOKEN_PATTERN = /^[A-Za-z0-9_-]+$/;
657
+ var AGENT_SESSION_TOKEN_UNSAFE_PATTERN = /[^A-Za-z0-9_-]+/g;
658
+ var AGENT_SESSION_TOKEN_EDGE_SEPARATOR_PATTERN = /^-+|-+$/g;
659
+ var AGENT_SESSION_TOKEN_SEPARATOR = "-";
660
+ var AGENT_SESSION_TOKEN_HASH_ALGORITHM = "sha256";
661
+ var AGENT_SESSION_TOKEN_HASH_ENCODING = "hex";
662
+ var AGENT_SESSION_TOKEN_HASH_LENGTH = 12;
663
+ var EMPTY_TOKEN = "";
664
+ function nonEmptyEnvValue(value) {
665
+ return value === void 0 || value.length === 0 ? void 0 : value;
666
+ }
667
+ function hashAgentSessionToken(value) {
668
+ return createHash(AGENT_SESSION_TOKEN_HASH_ALGORITHM).update(value).digest(AGENT_SESSION_TOKEN_HASH_ENCODING).slice(0, AGENT_SESSION_TOKEN_HASH_LENGTH);
669
+ }
670
+ function normalizeAgentSessionToken(value) {
671
+ if (AGENT_SESSION_TOKEN_PATTERN.test(value)) return value;
672
+ const normalized = value.replace(AGENT_SESSION_TOKEN_UNSAFE_PATTERN, AGENT_SESSION_TOKEN_SEPARATOR).replace(AGENT_SESSION_TOKEN_EDGE_SEPARATOR_PATTERN, EMPTY_TOKEN);
673
+ const hash = hashAgentSessionToken(value);
674
+ return normalized.length === 0 ? hash : `${normalized}${AGENT_SESSION_TOKEN_SEPARATOR}${hash}`;
675
+ }
676
+ function resolveAgentSessionId(env) {
677
+ const raw = nonEmptyEnvValue(env[AGENT_SESSION_ENV.CLAUDE_SESSION_ID]) ?? nonEmptyEnvValue(env[AGENT_SESSION_ENV.CODEX_THREAD_ID]);
678
+ return raw === void 0 ? void 0 : normalizeAgentSessionToken(raw);
679
+ }
680
+
681
+ // src/lib/state-store/index.ts
682
+ import { createHash as createHash2, randomBytes as nodeRandomBytes } from "crypto";
683
+ import {
684
+ appendFile as nodeAppendFile,
685
+ mkdir as nodeMkdir,
686
+ readdir as nodeReaddir,
687
+ readFile as nodeReadFile,
688
+ writeFile as nodeWriteFile
689
+ } from "fs/promises";
690
+ import { dirname as dirname2, join as join2 } from "path";
691
+
692
+ // src/git/root.ts
693
+ import { execa as execa2 } from "execa";
694
+ import { basename, dirname, isAbsolute, join, resolve } from "path";
695
+
696
+ // src/git/environment.ts
697
+ function withoutGitEnvironment(env) {
698
+ const cleaned = { ...env };
699
+ for (const key of Object.keys(cleaned)) {
700
+ if (key.startsWith("GIT_")) {
701
+ delete cleaned[key];
702
+ }
703
+ }
704
+ return cleaned;
705
+ }
706
+
707
+ // src/git/root.ts
708
+ var defaultGitDependencies = {
709
+ execa: async (command, args, options) => {
710
+ const result = await execa2(command, args, {
711
+ ...options,
712
+ env: withoutGitEnvironment(process.env),
713
+ extendEnv: false
714
+ });
715
+ return {
716
+ exitCode: result.exitCode ?? 0,
717
+ stdout: typeof result.stdout === "string" ? result.stdout : String(result.stdout),
718
+ stderr: typeof result.stderr === "string" ? result.stderr : String(result.stderr)
719
+ };
720
+ }
721
+ };
722
+ var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository; resolving session storage relative to the current directory.";
723
+ var GIT_ROOT_COMMAND = {
724
+ EXECUTABLE: "git",
725
+ REV_PARSE: "rev-parse",
726
+ ABBREV_REF: "--abbrev-ref",
727
+ GIT_COMMON_DIR: "--git-common-dir",
728
+ HEAD: "HEAD",
729
+ SHOW_TOPLEVEL: "--show-toplevel",
730
+ SYMBOLIC_REF: "symbolic-ref",
731
+ SHOW_REF: "show-ref",
732
+ VERIFY: "--verify",
733
+ QUIET: "--quiet",
734
+ SHORT: "--short",
735
+ ORIGIN_HEAD_REF: "refs/remotes/origin/HEAD",
736
+ STATUS: "status",
737
+ WORKTREE: "worktree",
738
+ LIST: "list",
739
+ PORCELAIN: "--porcelain",
740
+ PATH_FORMAT_ABSOLUTE: "--path-format=absolute",
741
+ CONFIG: "config",
742
+ CONFIG_GET: "--get",
743
+ CONFIG_TYPE_BOOL: "--type=bool",
744
+ CORE_BARE_KEY: "core.bare",
745
+ REMOTE: "remote",
746
+ GET_URL: "get-url",
747
+ ORIGIN: "origin"
748
+ };
749
+ var GIT_URL_SUFFIX = ".git";
750
+ var GIT_DIR_BASENAME = ".git";
751
+ var ORIGIN_REF_PREFIX = "origin/";
752
+ var REMOTE_ORIGIN_REF_PREFIX = "refs/remotes/origin/";
753
+ var GIT_CORE_BARE_TRUE = "true";
754
+ var GIT_SHOW_TOPLEVEL_ARGS = [
755
+ GIT_ROOT_COMMAND.REV_PARSE,
756
+ GIT_ROOT_COMMAND.SHOW_TOPLEVEL
757
+ ];
758
+ var GIT_COMMON_DIR_ARGS = [
759
+ GIT_ROOT_COMMAND.REV_PARSE,
760
+ GIT_ROOT_COMMAND.PATH_FORMAT_ABSOLUTE,
761
+ GIT_ROOT_COMMAND.GIT_COMMON_DIR
762
+ ];
763
+ var GIT_CORE_BARE_ARGS = [
764
+ GIT_ROOT_COMMAND.CONFIG,
765
+ GIT_ROOT_COMMAND.CONFIG_GET,
766
+ GIT_ROOT_COMMAND.CONFIG_TYPE_BOOL,
767
+ GIT_ROOT_COMMAND.CORE_BARE_KEY
768
+ ];
769
+ var GIT_REMOTE_GET_URL_ORIGIN_ARGS = [
770
+ GIT_ROOT_COMMAND.REMOTE,
771
+ GIT_ROOT_COMMAND.GET_URL,
772
+ GIT_ROOT_COMMAND.ORIGIN
773
+ ];
774
+ var GIT_CURRENT_BRANCH_ARGS = [
775
+ GIT_ROOT_COMMAND.REV_PARSE,
776
+ GIT_ROOT_COMMAND.ABBREV_REF,
777
+ GIT_ROOT_COMMAND.HEAD
778
+ ];
779
+ var GIT_HEAD_SHA_ARGS = [
780
+ GIT_ROOT_COMMAND.REV_PARSE,
781
+ GIT_ROOT_COMMAND.HEAD
782
+ ];
783
+ var GIT_ORIGIN_HEAD_REF_ARGS = [
784
+ GIT_ROOT_COMMAND.SYMBOLIC_REF,
785
+ GIT_ROOT_COMMAND.SHORT,
786
+ GIT_ROOT_COMMAND.ORIGIN_HEAD_REF
787
+ ];
788
+ var GIT_STATUS_PORCELAIN_ARGS = [
789
+ GIT_ROOT_COMMAND.STATUS,
790
+ GIT_ROOT_COMMAND.PORCELAIN
791
+ ];
792
+ var GIT_WORKTREE_LIST_PORCELAIN_ARGS = [
793
+ GIT_ROOT_COMMAND.WORKTREE,
794
+ GIT_ROOT_COMMAND.LIST,
795
+ GIT_ROOT_COMMAND.PORCELAIN
796
+ ];
797
+ var GIT_WORKTREE_PORCELAIN_ROOT_PREFIX = "worktree ";
798
+ var GIT_WORKTREE_PORCELAIN_BARE_LINE = "bare";
799
+ var GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE = "prunable";
800
+ var GIT_WORKTREE_PORCELAIN_PRUNABLE_PREFIX = `${GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE} `;
801
+ var TRAILING_PATH_SEPARATORS_PATTERN = /[\\/]+$/;
802
+ async function detectWorktreeProductRoot(cwd = process.cwd(), deps = defaultGitDependencies) {
803
+ try {
804
+ const result = await deps.execa(
805
+ GIT_ROOT_COMMAND.EXECUTABLE,
806
+ [...GIT_SHOW_TOPLEVEL_ARGS],
807
+ { cwd, reject: false }
808
+ );
809
+ if (result.exitCode === 0 && result.stdout) {
810
+ return {
811
+ productDir: extractStdout(result.stdout),
812
+ isGitRepo: true
813
+ };
814
+ }
815
+ return {
816
+ productDir: cwd,
817
+ isGitRepo: false,
818
+ warning: NOT_GIT_REPO_WARNING
819
+ };
820
+ } catch {
821
+ return {
822
+ productDir: cwd,
823
+ isGitRepo: false,
824
+ warning: NOT_GIT_REPO_WARNING
825
+ };
826
+ }
827
+ }
828
+ function extractStdout(stdout) {
829
+ if (!stdout) return "";
830
+ const str = typeof stdout === "string" ? stdout : String(stdout);
831
+ return str.trim().replace(TRAILING_PATH_SEPARATORS_PATTERN, "");
832
+ }
833
+ function trimStdout(stdout) {
834
+ if (!stdout) return "";
835
+ const str = typeof stdout === "string" ? stdout : String(stdout);
836
+ return str.trim();
837
+ }
838
+ async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = defaultGitDependencies) {
839
+ try {
840
+ const toplevelResult = await deps.execa(
841
+ GIT_ROOT_COMMAND.EXECUTABLE,
842
+ [...GIT_SHOW_TOPLEVEL_ARGS],
843
+ { cwd, reject: false }
844
+ );
845
+ if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) {
846
+ return {
847
+ productDir: cwd,
848
+ isGitRepo: false,
849
+ warning: NOT_GIT_REPO_WARNING,
850
+ worktreeRoot: cwd
851
+ };
852
+ }
853
+ const toplevel = extractStdout(toplevelResult.stdout);
854
+ const commonDirResult = await deps.execa(
855
+ GIT_ROOT_COMMAND.EXECUTABLE,
856
+ [...GIT_COMMON_DIR_ARGS],
857
+ { cwd, reject: false }
858
+ );
859
+ if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
860
+ return {
861
+ productDir: toplevel,
862
+ isGitRepo: true,
863
+ worktreeRoot: toplevel
864
+ };
865
+ }
866
+ const commonDir = extractStdout(commonDirResult.stdout);
867
+ const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve(toplevel, commonDir);
868
+ const gitCommonDirProductRoot = dirname(absoluteCommonDir);
869
+ return {
870
+ productDir: gitCommonDirProductRoot,
871
+ isGitRepo: true,
872
+ worktreeRoot: toplevel
873
+ };
874
+ } catch {
875
+ return {
876
+ productDir: cwd,
877
+ isGitRepo: false,
878
+ warning: NOT_GIT_REPO_WARNING,
879
+ worktreeRoot: cwd
880
+ };
881
+ }
882
+ }
883
+ async function resolveDefaultBranch(cwd = process.cwd(), deps = defaultGitDependencies) {
884
+ const result = await deps.execa(
885
+ GIT_ROOT_COMMAND.EXECUTABLE,
886
+ [...GIT_ORIGIN_HEAD_REF_ARGS],
887
+ { cwd, reject: false }
888
+ );
889
+ if (result.exitCode !== 0) return null;
890
+ const ref = extractStdout(result.stdout);
891
+ if (!ref.startsWith(ORIGIN_REF_PREFIX)) return null;
892
+ const branch = ref.slice(ORIGIN_REF_PREFIX.length);
893
+ return branch.length === 0 ? null : branch;
894
+ }
895
+ async function getCurrentBranch(cwd = process.cwd(), deps = defaultGitDependencies) {
896
+ const result = await deps.execa(
897
+ GIT_ROOT_COMMAND.EXECUTABLE,
898
+ [...GIT_CURRENT_BRANCH_ARGS],
899
+ { cwd, reject: false }
900
+ );
901
+ if (result.exitCode !== 0) return null;
902
+ const branch = extractStdout(result.stdout);
903
+ if (branch.length === 0 || branch === GIT_ROOT_COMMAND.HEAD) return null;
904
+ return branch;
905
+ }
906
+ async function getHeadSha(cwd = process.cwd(), deps = defaultGitDependencies) {
907
+ const result = await deps.execa(
908
+ GIT_ROOT_COMMAND.EXECUTABLE,
909
+ [...GIT_HEAD_SHA_ARGS],
910
+ { cwd, reject: false }
911
+ );
912
+ if (result.exitCode !== 0) return null;
913
+ const sha = extractStdout(result.stdout);
914
+ return sha.length === 0 ? null : sha;
915
+ }
916
+ async function resolveRefSha(ref, cwd = process.cwd(), deps = defaultGitDependencies) {
917
+ const result = await deps.execa(
918
+ GIT_ROOT_COMMAND.EXECUTABLE,
919
+ [GIT_ROOT_COMMAND.REV_PARSE, ref],
920
+ { cwd, reject: false }
921
+ );
922
+ if (result.exitCode !== 0) return null;
923
+ const sha = extractStdout(result.stdout);
924
+ return sha.length === 0 ? null : sha;
925
+ }
926
+ async function originBranchExists(branch, cwd = process.cwd(), deps = defaultGitDependencies) {
927
+ if (branch === GIT_ROOT_COMMAND.HEAD) return false;
928
+ const result = await deps.execa(
929
+ GIT_ROOT_COMMAND.EXECUTABLE,
930
+ [
931
+ GIT_ROOT_COMMAND.SHOW_REF,
932
+ GIT_ROOT_COMMAND.VERIFY,
933
+ GIT_ROOT_COMMAND.QUIET,
934
+ `${REMOTE_ORIGIN_REF_PREFIX}${branch}`
935
+ ],
936
+ { cwd, reject: false }
937
+ );
938
+ return result.exitCode === 0;
939
+ }
940
+ async function isWorkingTreeClean(cwd = process.cwd(), deps = defaultGitDependencies) {
941
+ const result = await deps.execa(
942
+ GIT_ROOT_COMMAND.EXECUTABLE,
943
+ [...GIT_STATUS_PORCELAIN_ARGS],
944
+ { cwd, reject: false }
945
+ );
946
+ if (result.exitCode !== 0) return false;
947
+ return extractStdout(result.stdout).length === 0;
948
+ }
949
+ function repositoryName(originUrl) {
950
+ if (originUrl === null) return null;
951
+ const trimmed = originUrl.trim().replace(TRAILING_PATH_SEPARATORS_PATTERN, "");
952
+ const lastSeparator = Math.max(
953
+ trimmed.lastIndexOf("/"),
954
+ trimmed.lastIndexOf("\\"),
955
+ trimmed.lastIndexOf(":")
956
+ );
957
+ const segment = trimmed.slice(lastSeparator + 1);
958
+ const name = segment.endsWith(GIT_URL_SUFFIX) ? segment.slice(0, -GIT_URL_SUFFIX.length) : segment;
959
+ return name.length === 0 ? null : name;
960
+ }
961
+ function normalizeGitPath(path6) {
962
+ return path6.trim().replace(TRAILING_PATH_SEPARATORS_PATTERN, "");
963
+ }
964
+ function normalizedGitPathKey(path6) {
965
+ return normalizeGitPath(path6).replace(/\\/g, "/");
966
+ }
967
+ function isObservedWorktreeRoot(worktreeRoots, candidate) {
968
+ const candidateKey = normalizedGitPathKey(candidate);
969
+ return worktreeRoots.some((root) => normalizedGitPathKey(root) === candidateKey);
970
+ }
971
+ function isPrunableWorktreeRecordLine(line) {
972
+ return line === GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE || line.startsWith(GIT_WORKTREE_PORCELAIN_PRUNABLE_PREFIX);
973
+ }
974
+ function parseWorktreeRoots(stdout) {
975
+ const roots = [];
976
+ for (const record of stdout.split(/\n\n+/)) {
977
+ const lines = record.split("\n");
978
+ if (lines.includes(GIT_WORKTREE_PORCELAIN_BARE_LINE) || lines.some(isPrunableWorktreeRecordLine)) continue;
979
+ const rootLine = lines.find((line) => line.startsWith(GIT_WORKTREE_PORCELAIN_ROOT_PREFIX));
980
+ if (rootLine === void 0) continue;
981
+ const root = normalizeGitPath(rootLine.slice(GIT_WORKTREE_PORCELAIN_ROOT_PREFIX.length));
982
+ if (root.length > 0) roots.push(root);
983
+ }
984
+ return roots;
985
+ }
986
+ function observedWorktreeRoots(worktreeListResult) {
987
+ if (worktreeListResult.exitCode !== 0) return [];
988
+ return [...new Set(parseWorktreeRoots(worktreeListResult.stdout))];
989
+ }
990
+ function isMainCheckout(facts) {
991
+ const commonDirParent = dirname(facts.commonDir);
992
+ if (!facts.commonDirIsBare) return commonDirParent === facts.worktreeRoot;
993
+ if (commonDirParent !== dirname(facts.worktreeRoot)) return false;
994
+ const name = repositoryName(facts.originUrl);
995
+ return name !== null && basename(facts.worktreeRoot) === name && facts.worktreeListRead && isObservedWorktreeRoot(facts.worktreeRoots, facts.worktreeRoot);
996
+ }
997
+ function mainCheckoutPath(facts) {
998
+ const commonDirParent = dirname(facts.commonDir);
999
+ if (!facts.commonDirIsBare) return commonDirParent;
1000
+ const name = repositoryName(facts.originUrl);
1001
+ if (name === null) return null;
1002
+ if (!facts.worktreeListRead) return null;
1003
+ const candidate = join(commonDirParent, name);
1004
+ return isObservedWorktreeRoot(facts.worktreeRoots, candidate) ? candidate : null;
1005
+ }
1006
+ async function gatherGitFacts(cwd = process.cwd(), deps = defaultGitDependencies) {
1007
+ try {
1008
+ const [toplevelResult, commonDirResult, originResult, worktreeListResult] = await Promise.all([
1009
+ deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_SHOW_TOPLEVEL_ARGS], { cwd, reject: false }),
1010
+ deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_COMMON_DIR_ARGS], { cwd, reject: false }),
1011
+ deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_REMOTE_GET_URL_ORIGIN_ARGS], { cwd, reject: false }),
1012
+ deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_WORKTREE_LIST_PORCELAIN_ARGS], { cwd, reject: false })
1013
+ ]);
1014
+ if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) return null;
1015
+ const worktreeRoot = extractStdout(toplevelResult.stdout);
1016
+ const worktreeListRead = worktreeListResult.exitCode === 0;
1017
+ const worktreeRoots = observedWorktreeRoots(worktreeListResult);
1018
+ const originUrl = originResult.exitCode === 0 && originResult.stdout ? trimStdout(originResult.stdout) : null;
1019
+ if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
1020
+ return {
1021
+ worktreeRoot,
1022
+ worktreeRoots,
1023
+ worktreeListRead,
1024
+ commonDir: join(worktreeRoot, GIT_DIR_BASENAME),
1025
+ commonDirIsBare: false,
1026
+ originUrl
1027
+ };
1028
+ }
1029
+ const rawCommonDir = extractStdout(commonDirResult.stdout);
1030
+ const commonDir = isAbsolute(rawCommonDir) ? rawCommonDir : resolve(worktreeRoot, rawCommonDir);
1031
+ const bareResult = await deps.execa(
1032
+ GIT_ROOT_COMMAND.EXECUTABLE,
1033
+ [...GIT_CORE_BARE_ARGS],
1034
+ { cwd, reject: false }
1035
+ );
1036
+ const commonDirIsBare = bareResult.exitCode === 0 && extractStdout(bareResult.stdout) === GIT_CORE_BARE_TRUE;
1037
+ return { worktreeRoot, worktreeRoots, worktreeListRead, commonDir, commonDirIsBare, originUrl };
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ }
1042
+
1043
+ // src/lib/state-store/index.ts
1044
+ var STATE_STORE_PATH = {
1045
+ SPX_DIR: ".spx",
1046
+ BRANCH_SCOPE: "branch",
1047
+ WORKTREE_SCOPE: "worktree",
1048
+ SESSIONS_SCOPE: "sessions",
1049
+ RUNS_DIR: "runs",
1050
+ RUN_FILE_PREFIX: "run-",
1051
+ JSONL_EXTENSION: ".jsonl"
1052
+ };
1053
+ var STATE_STORE_DOMAIN = {
1054
+ AUDIT: "audit",
1055
+ COMPACT: "compact",
1056
+ REVIEW: "review",
1057
+ TEST: "test"
1058
+ };
1059
+ var STATE_STORE_ERROR = {
1060
+ INVALID_TOKEN: "state-store token must be a safe path segment",
1061
+ INVALID_BRANCH_SLUG: "state-store branch slug must be normalized before storage",
1062
+ RUN_FILE_CREATE_FAILED: "state-store run file create failed",
1063
+ RUN_FILE_COLLISION_LIMIT: "state-store run file collision limit exhausted",
1064
+ RECORD_ALREADY_EXISTS: "state-store record already exists",
1065
+ RECORD_WRITE_FAILED: "state-store record write failed",
1066
+ RECORD_READ_FAILED: "state-store record read failed"
1067
+ };
1068
+ var HEX_ENCODING = "hex";
1069
+ var RUN_ID_BYTES = 6;
1070
+ var RUN_FILE_CREATE_ATTEMPTS = 10;
1071
+ var RUN_TIMESTAMP_SEPARATOR = "_";
1072
+ var RUN_TIMESTAMP_MILLISECOND_DIGITS = 3;
1073
+ var SLUG_SEPARATOR = "-";
1074
+ var EMPTY_STRING = "";
1075
+ var RUN_TOKEN_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[a-f0-9]{12}$/;
1076
+ var TOKEN_PATTERN = /^[A-Za-z0-9_-]+$/;
1077
+ var EXCLUSIVE_CREATE_FLAG = "wx";
1078
+ var WRITE_EXISTING_FLAG = "r+";
1079
+ var ERROR_CODE_FILE_EXISTS = "EEXIST";
1080
+ var ERROR_CODE_NOT_FOUND = "ENOENT";
1081
+ var JSONL_LINE_SEPARATOR = "\n";
1082
+ var ERROR_DETAIL_SEPARATOR = ": ";
1083
+ var defaultFileSystem = {
1084
+ mkdir: async (path6, options) => {
1085
+ await nodeMkdir(path6, options);
1086
+ },
1087
+ writeFile: nodeWriteFile,
1088
+ appendFile: nodeAppendFile,
1089
+ readFile: nodeReadFile,
1090
+ readdir: nodeReaddir
1091
+ };
1092
+ function validateScopeToken(token) {
1093
+ return TOKEN_PATTERN.test(token) ? { ok: true, value: token } : { ok: false, error: STATE_STORE_ERROR.INVALID_TOKEN };
1094
+ }
1095
+ async function resolveWorktreeScopeDir(options = {}) {
1096
+ const gitResult = await detectWorktreeProductRoot(options.cwd, options.deps);
1097
+ return worktreeScopeDir(gitResult.productDir);
1098
+ }
1099
+ async function resolveSessionsScopeDir(options = {}) {
1100
+ const gitResult = await detectGitCommonDirProductRoot(options.cwd, options.deps);
1101
+ return {
1102
+ sessionsDir: sessionsScopeDir(gitResult.productDir),
1103
+ warning: gitResult.warning
1104
+ };
1105
+ }
1106
+ function worktreeScopeDir(productDir) {
1107
+ return { ok: true, value: join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.WORKTREE_SCOPE) };
1108
+ }
1109
+ function sessionsScopeDir(productDir) {
1110
+ return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
1111
+ }
1112
+ function composeScopeDir(baseScopeDir, ...tokens) {
1113
+ const segments = [];
1114
+ for (const token of tokens) {
1115
+ const validated = validateScopeToken(token);
1116
+ if (!validated.ok) return validated;
1117
+ segments.push(validated.value);
1118
+ }
1119
+ return { ok: true, value: join2(baseScopeDir, ...segments) };
1120
+ }
1121
+ function domainDir(scopeDir, domainName) {
1122
+ return composeScopeDir(scopeDir, domainName);
1123
+ }
1124
+ function runsDir(scopeDir, domainName) {
1125
+ const domain = domainDir(scopeDir, domainName);
1126
+ if (!domain.ok) return domain;
1127
+ return { ok: true, value: join2(domain.value, STATE_STORE_PATH.RUNS_DIR) };
1128
+ }
1129
+ function formatRunTimestamp(date) {
1130
+ const year = date.getUTCFullYear();
1131
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
1132
+ const day = String(date.getUTCDate()).padStart(2, "0");
1133
+ const hours = String(date.getUTCHours()).padStart(2, "0");
1134
+ const minutes = String(date.getUTCMinutes()).padStart(2, "0");
1135
+ const seconds = String(date.getUTCSeconds()).padStart(2, "0");
1136
+ const milliseconds = String(date.getUTCMilliseconds()).padStart(RUN_TIMESTAMP_MILLISECOND_DIGITS, "0");
1137
+ return `${year}-${month}-${day}${RUN_TIMESTAMP_SEPARATOR}${hours}-${minutes}-${seconds}-${milliseconds}`;
1138
+ }
1139
+ function generateRunId(randomBytes = nodeRandomBytes) {
1140
+ return randomBytes(RUN_ID_BYTES).toString(HEX_ENCODING);
1141
+ }
1142
+ function runFileName(runToken) {
1143
+ return `${STATE_STORE_PATH.RUN_FILE_PREFIX}${runToken}${STATE_STORE_PATH.JSONL_EXTENSION}`;
1144
+ }
1145
+ function isRunFileName(name) {
1146
+ return name.startsWith(STATE_STORE_PATH.RUN_FILE_PREFIX) && name.endsWith(STATE_STORE_PATH.JSONL_EXTENSION) && RUN_TOKEN_PATTERN.test(name.slice(
1147
+ STATE_STORE_PATH.RUN_FILE_PREFIX.length,
1148
+ -STATE_STORE_PATH.JSONL_EXTENSION.length
1149
+ ));
1150
+ }
1151
+ async function createJsonlRunFile(scopeDir, domainName, options = {}) {
1152
+ const fs7 = options.fs ?? defaultFileSystem;
1153
+ const domainRunsDir = runsDir(scopeDir, domainName);
1154
+ if (!domainRunsDir.ok) return domainRunsDir;
1155
+ const maxAttempts = options.maxAttempts ?? RUN_FILE_CREATE_ATTEMPTS;
1156
+ const startedDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
1157
+ const startedAt = formatRunTimestamp(startedDate);
1158
+ const randomBytes = options.randomBytes ?? nodeRandomBytes;
1159
+ try {
1160
+ await fs7.mkdir(domainRunsDir.value, { recursive: true });
1161
+ } catch (error) {
1162
+ return {
1163
+ ok: false,
1164
+ error: formatStateStoreError(STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED, toErrorMessage(error))
1165
+ };
1166
+ }
1167
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
1168
+ const runId = generateRunId(randomBytes);
1169
+ const runToken = `${startedAt}${SLUG_SEPARATOR}${runId}`;
1170
+ const name = runFileName(runToken);
1171
+ const path6 = join2(domainRunsDir.value, name);
1172
+ try {
1173
+ await fs7.writeFile(path6, EMPTY_STRING, { flag: EXCLUSIVE_CREATE_FLAG });
1174
+ return {
1175
+ ok: true,
1176
+ value: { runsDir: domainRunsDir.value, runFilePath: path6, runFileName: name, runToken, runId, startedAt }
1177
+ };
1178
+ } catch (error) {
1179
+ if (hasErrorCode(error, ERROR_CODE_FILE_EXISTS)) continue;
1180
+ return {
1181
+ ok: false,
1182
+ error: formatStateStoreError(STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED, toErrorMessage(error))
1183
+ };
1184
+ }
1185
+ }
1186
+ return { ok: false, error: STATE_STORE_ERROR.RUN_FILE_COLLISION_LIMIT };
1187
+ }
1188
+ async function writeJsonlRunRecord(runFilePath, record, options = {}) {
1189
+ const fs7 = options.fs ?? defaultFileSystem;
1190
+ try {
1191
+ const existing = await fs7.readFile(runFilePath, "utf8");
1192
+ if (existing.trim().length > 0) return { ok: false, error: STATE_STORE_ERROR.RECORD_ALREADY_EXISTS };
768
1193
  } catch (error) {
769
- try {
770
- await deps.fs.unlink(tempPath);
771
- } catch {
772
- }
773
- if (error instanceof Error) {
774
- throw new Error(`Failed to write settings: ${error.message}`);
1194
+ if (!hasErrorCode(error, ERROR_CODE_NOT_FOUND)) {
1195
+ return {
1196
+ ok: false,
1197
+ error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
1198
+ };
775
1199
  }
776
- throw error;
777
1200
  }
778
- }
779
-
780
- // src/commands/claude/settings/consolidate.ts
781
- import os2 from "os";
782
- import path3 from "path";
783
- async function consolidateCommand(options = {}) {
784
- const root = options.root ? path3.resolve(options.root.replace(/^~/, os2.homedir())) : path3.join(os2.homedir(), "Code");
785
- const globalSettingsPath = options.globalSettings || path3.join(os2.homedir(), ".claude", "settings.json");
786
- const shouldWrite = options.write || false;
787
- const outputFile = options.outputFile;
788
- const previewOnly = !shouldWrite && !outputFile;
789
- const settingsFiles = await findSettingsFiles(root);
790
- if (settingsFiles.length === 0) {
791
- return `No settings files found in ${root}
792
-
793
- Searched for: **/.claude/settings.local.json`;
1201
+ try {
1202
+ await fs7.writeFile(runFilePath, serializeJsonlRecord(record), { flag: WRITE_EXISTING_FLAG });
1203
+ return { ok: true, value: runFilePath };
1204
+ } catch (error) {
1205
+ return {
1206
+ ok: false,
1207
+ error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
1208
+ };
794
1209
  }
795
- const localPermissions = await parseAllSettings(settingsFiles);
796
- let globalSettings = await parseSettingsFile(globalSettingsPath);
797
- if (!globalSettings) {
798
- globalSettings = {
799
- permissions: {
800
- allow: [],
801
- deny: [],
802
- ask: []
803
- }
1210
+ }
1211
+ async function appendJsonlRecord(filePath, record, options = {}) {
1212
+ const fs7 = options.fs ?? defaultFileSystem;
1213
+ try {
1214
+ await fs7.mkdir(dirname2(filePath), { recursive: true });
1215
+ await fs7.appendFile(filePath, serializeJsonlRecord(record));
1216
+ return { ok: true, value: filePath };
1217
+ } catch (error) {
1218
+ return {
1219
+ ok: false,
1220
+ error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
804
1221
  };
805
1222
  }
806
- if (!globalSettings.permissions) {
807
- globalSettings.permissions = {
808
- allow: [],
809
- deny: [],
810
- ask: []
1223
+ }
1224
+ async function readLatestJsonlRecord(filePath, options = {}) {
1225
+ const fs7 = options.fs ?? defaultFileSystem;
1226
+ let content;
1227
+ try {
1228
+ content = await fs7.readFile(filePath, "utf8");
1229
+ } catch (error) {
1230
+ if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return { ok: true, value: void 0 };
1231
+ return {
1232
+ ok: false,
1233
+ error: formatStateStoreError(STATE_STORE_ERROR.RECORD_READ_FAILED, toErrorMessage(error))
811
1234
  };
812
1235
  }
813
- const { merged, result } = mergePermissions(
814
- globalSettings.permissions,
815
- localPermissions
816
- );
817
- if (shouldWrite) {
1236
+ for (const line of nonEmptyJsonlLinesNewestFirst(content)) {
818
1237
  try {
819
- result.backupPath = await createBackup(globalSettingsPath);
820
- } catch (error) {
821
- if (error instanceof Error && !error.message.includes("not found")) {
822
- throw error;
823
- }
1238
+ return { ok: true, value: JSON.parse(line) };
1239
+ } catch {
1240
+ continue;
824
1241
  }
825
1242
  }
826
- if (shouldWrite) {
827
- const updatedSettings = {
828
- ...globalSettings,
829
- permissions: merged
830
- };
831
- await writeSettings(globalSettingsPath, updatedSettings);
832
- } else if (outputFile) {
833
- const updatedSettings = {
834
- ...globalSettings,
835
- permissions: merged
836
- };
837
- const resolvedOutputPath = path3.resolve(outputFile.replace(/^~/, os2.homedir()));
838
- await writeSettings(resolvedOutputPath, updatedSettings);
839
- result.outputPath = resolvedOutputPath;
1243
+ return { ok: true, value: void 0 };
1244
+ }
1245
+ function latestNonEmptyJsonlLine(content) {
1246
+ return nonEmptyJsonlLinesNewestFirst(content)[0];
1247
+ }
1248
+ function compareAsciiStrings(left, right) {
1249
+ if (left < right) return -1;
1250
+ if (left > right) return 1;
1251
+ return 0;
1252
+ }
1253
+ function formatStateStoreError(code, detail) {
1254
+ return detail === void 0 ? code : `${code}${ERROR_DETAIL_SEPARATOR}${detail}`;
1255
+ }
1256
+ function parseStateStoreError(error) {
1257
+ for (const code of Object.values(STATE_STORE_ERROR)) {
1258
+ if (error === code) return { code };
1259
+ const prefix = `${code}${ERROR_DETAIL_SEPARATOR}`;
1260
+ if (error.startsWith(prefix)) return { code, detail: error.slice(prefix.length) };
840
1261
  }
841
- return formatReport(result, previewOnly, globalSettingsPath, outputFile);
1262
+ return void 0;
1263
+ }
1264
+ function serializeJsonlRecord(record) {
1265
+ return `${JSON.stringify(record)}${JSONL_LINE_SEPARATOR}`;
1266
+ }
1267
+ function nonEmptyJsonlLinesNewestFirst(content) {
1268
+ const lines = content.split(JSONL_LINE_SEPARATOR);
1269
+ const latestLines = [];
1270
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1271
+ const line = lines[index]?.trim() ?? EMPTY_STRING;
1272
+ if (line.length > 0) latestLines.push(line);
1273
+ }
1274
+ return latestLines;
1275
+ }
1276
+ function hasErrorCode(error, code) {
1277
+ return typeof error === "object" && error !== null && !Array.isArray(error) && "code" in error && error.code === code;
1278
+ }
1279
+ function toErrorMessage(error) {
1280
+ return error instanceof Error ? error.message : String(error);
842
1281
  }
843
1282
 
844
- // src/interfaces/cli/claude.ts
845
- function registerClaudeCommands(claudeCmd) {
846
- claudeCmd.command("init").description("Initialize or update outcomeeng marketplace plugin").action(async () => {
847
- try {
848
- const output = await initCommand({ cwd: process.cwd() });
849
- console.log(output);
850
- } catch (error) {
851
- console.error(
852
- "Error:",
853
- error instanceof Error ? error.message : String(error)
854
- );
855
- process.exit(1);
856
- }
857
- });
858
- const settingsCmd = claudeCmd.command("settings").description("Manage Claude Code settings");
859
- settingsCmd.command("consolidate").description(
860
- "Consolidate permissions from project-specific settings into global settings"
861
- ).option("--write", "Write changes to global settings file (default: preview only)").option(
862
- "--output-file <path>",
863
- "Write merged settings to specified file instead of global settings"
864
- ).option(
865
- "--root <path>",
866
- "Root directory to scan for settings files (default: ~/Code)"
867
- ).option(
868
- "--global-settings <path>",
869
- "Path to global settings file (default: ~/.claude/settings.json)"
870
- ).action(
871
- async (options) => {
872
- try {
873
- if (options.write && options.outputFile) {
874
- console.error(
875
- "Error: --write and --output-file are mutually exclusive\nUse --write to modify global settings, or --output-file to write to a different location"
876
- );
877
- process.exit(1);
878
- }
879
- const output = await consolidateCommand({
880
- write: options.write,
881
- outputFile: options.outputFile,
882
- root: options.root,
883
- globalSettings: options.globalSettings
884
- });
885
- console.log(output);
886
- } catch (error) {
887
- console.error(
888
- "Error:",
889
- error instanceof Error ? error.message : String(error)
890
- );
891
- process.exit(1);
1283
+ // src/domains/compact/index.ts
1284
+ var COMPACT_STORE_PATH = {
1285
+ STASH_FILE: "stash.jsonl"
1286
+ };
1287
+ var COMPACT_RECORD_FIELDS = {
1288
+ ACTIVE_NODE: "active_node",
1289
+ HAS_FOUNDATION: "has_foundation"
1290
+ };
1291
+ var COMPACT_MARKER = {
1292
+ FOUNDATION: "SPEC_TREE_FOUNDATION",
1293
+ CONTEXT: "SPEC_TREE_CONTEXT",
1294
+ TARGET_ATTRIBUTE: "target",
1295
+ ESCAPED_TARGET_QUOTE: '\\"',
1296
+ UNESCAPED_TARGET_QUOTE: '"'
1297
+ };
1298
+ var COMPACT_ERROR = {
1299
+ RECORD_SHAPE_INVALID: "compact record shape invalid"
1300
+ };
1301
+ var EMPTY_ACTIVE_NODE = "";
1302
+ var JSONL_LINE_SEPARATOR2 = "\n";
1303
+ var ATTRIBUTE_ASSIGNMENT = "=";
1304
+ var ESCAPE_CHARACTER = "\\";
1305
+ var CONTEXT_TARGET_PREFIX = `${COMPACT_MARKER.TARGET_ATTRIBUTE}${ATTRIBUTE_ASSIGNMENT}`;
1306
+ var NODE_PATH_PREFIX = "spx/";
1307
+ function resolveCompactSessionToken(sessionId, env) {
1308
+ if (sessionId !== void 0 && sessionId.length > 0) {
1309
+ return normalizeAgentSessionToken(sessionId);
1310
+ }
1311
+ return resolveAgentSessionId(env);
1312
+ }
1313
+ function extractCompactRecord(transcript) {
1314
+ let hasFoundation = false;
1315
+ let activeNode = EMPTY_ACTIVE_NODE;
1316
+ for (const value of transcriptStringValues(transcript)) {
1317
+ if (value.includes(COMPACT_MARKER.FOUNDATION)) hasFoundation = true;
1318
+ activeNode = extractLastContextTarget(value) ?? activeNode;
1319
+ }
1320
+ if (!hasFoundation) return void 0;
1321
+ return {
1322
+ [COMPACT_RECORD_FIELDS.ACTIVE_NODE]: activeNode,
1323
+ [COMPACT_RECORD_FIELDS.HAS_FOUNDATION]: true
1324
+ };
1325
+ }
1326
+ function compactStashPath(worktreeScopeDir2, sessionToken) {
1327
+ const compactScope = composeScopeDir(worktreeScopeDir2, sessionToken, STATE_STORE_DOMAIN.COMPACT);
1328
+ if (!compactScope.ok) return compactScope;
1329
+ return { ok: true, value: join3(compactScope.value, COMPACT_STORE_PATH.STASH_FILE) };
1330
+ }
1331
+ function parseCompactRecord(value) {
1332
+ if (typeof value === "object" && value !== null && COMPACT_RECORD_FIELDS.ACTIVE_NODE in value && COMPACT_RECORD_FIELDS.HAS_FOUNDATION in value && typeof value[COMPACT_RECORD_FIELDS.ACTIVE_NODE] === "string" && value[COMPACT_RECORD_FIELDS.HAS_FOUNDATION] === true) {
1333
+ return {
1334
+ ok: true,
1335
+ value: {
1336
+ active_node: value[COMPACT_RECORD_FIELDS.ACTIVE_NODE] ?? EMPTY_ACTIVE_NODE,
1337
+ has_foundation: true
892
1338
  }
1339
+ };
1340
+ }
1341
+ return { ok: false, error: COMPACT_ERROR.RECORD_SHAPE_INVALID };
1342
+ }
1343
+ function transcriptStringValues(transcript) {
1344
+ const values = [];
1345
+ for (const rawLine of transcript.split(JSONL_LINE_SEPARATOR2)) {
1346
+ const line = rawLine.trim();
1347
+ if (line.length === 0) continue;
1348
+ try {
1349
+ collectStringValues(JSON.parse(line), values);
1350
+ } catch {
1351
+ continue;
893
1352
  }
894
- );
1353
+ }
1354
+ return values;
895
1355
  }
896
- var claudeDomain = {
897
- name: "claude",
898
- description: "Manage Claude Code settings and plugins",
1356
+ function collectStringValues(value, values) {
1357
+ if (typeof value === "string") {
1358
+ values.push(value);
1359
+ return;
1360
+ }
1361
+ if (Array.isArray(value)) {
1362
+ for (const entry of value) collectStringValues(entry, values);
1363
+ return;
1364
+ }
1365
+ if (typeof value === "object" && value !== null) {
1366
+ for (const entry of Object.values(value)) collectStringValues(entry, values);
1367
+ }
1368
+ }
1369
+ function extractLastContextTarget(value) {
1370
+ let activeNode;
1371
+ let searchFrom = 0;
1372
+ while (searchFrom < value.length) {
1373
+ const contextIndex = value.indexOf(COMPACT_MARKER.CONTEXT, searchFrom);
1374
+ if (contextIndex < 0) return activeNode;
1375
+ const target = extractContextTargetAt(value, contextIndex + COMPACT_MARKER.CONTEXT.length);
1376
+ if (target !== void 0) activeNode = target;
1377
+ searchFrom = contextIndex + COMPACT_MARKER.CONTEXT.length;
1378
+ }
1379
+ return activeNode;
1380
+ }
1381
+ function extractContextTargetAt(value, searchFrom) {
1382
+ const targetIndex = value.indexOf(CONTEXT_TARGET_PREFIX, searchFrom);
1383
+ if (targetIndex < 0) return void 0;
1384
+ const quoteIndex = findOpeningTargetQuote(value, targetIndex + CONTEXT_TARGET_PREFIX.length);
1385
+ if (quoteIndex === void 0) return void 0;
1386
+ const target = readTargetPath(value, quoteIndex + COMPACT_MARKER.UNESCAPED_TARGET_QUOTE.length);
1387
+ return target.startsWith(NODE_PATH_PREFIX) ? target : void 0;
1388
+ }
1389
+ function findOpeningTargetQuote(value, startIndex) {
1390
+ let index = startIndex;
1391
+ while (value[index] === ESCAPE_CHARACTER) index += 1;
1392
+ return value[index] === COMPACT_MARKER.UNESCAPED_TARGET_QUOTE ? index : void 0;
1393
+ }
1394
+ function readTargetPath(value, startIndex) {
1395
+ let endIndex = startIndex;
1396
+ while (endIndex < value.length && isTargetPathCharacter(value[endIndex] ?? EMPTY_ACTIVE_NODE)) {
1397
+ endIndex += 1;
1398
+ }
1399
+ return value.slice(startIndex, endIndex);
1400
+ }
1401
+ function isTargetPathCharacter(character) {
1402
+ const code = character.charCodeAt(0);
1403
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || character === "." || character === "_" || character === "-" || character === "/";
1404
+ }
1405
+
1406
+ // src/commands/compact/retrieve.ts
1407
+ var EMPTY_OUTPUT = "";
1408
+ async function compactRetrieveCommand(options) {
1409
+ const sessionToken = resolveCompactSessionToken(options.sessionId, options.env ?? process.env);
1410
+ if (sessionToken === void 0) return { exitCode: 1, output: EMPTY_OUTPUT };
1411
+ const worktreeScope = await resolveWorktreeScopeDir({ cwd: options.cwd });
1412
+ if (!worktreeScope.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
1413
+ const stashPath = compactStashPath(worktreeScope.value, sessionToken);
1414
+ if (!stashPath.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
1415
+ const latest = await readLatestJsonlRecord(stashPath.value);
1416
+ if (!latest.ok || latest.value === void 0) return { exitCode: 1, output: EMPTY_OUTPUT };
1417
+ const record = parseCompactRecord(latest.value);
1418
+ if (!record.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
1419
+ return { exitCode: 0, output: `${JSON.stringify(record.value)}
1420
+ ` };
1421
+ }
1422
+
1423
+ // src/commands/compact/store.ts
1424
+ import { readFile } from "fs/promises";
1425
+ var UTF8_ENCODING = "utf8";
1426
+ async function compactStoreCommand(options) {
1427
+ const sessionToken = resolveCompactSessionToken(options.sessionId, options.env ?? process.env);
1428
+ if (sessionToken === void 0) return 1;
1429
+ let transcript;
1430
+ try {
1431
+ transcript = await readFile(options.transcript, UTF8_ENCODING);
1432
+ } catch {
1433
+ return 1;
1434
+ }
1435
+ const record = extractCompactRecord(transcript);
1436
+ if (record === void 0) return 0;
1437
+ const worktreeScope = await resolveWorktreeScopeDir({ cwd: options.cwd });
1438
+ if (!worktreeScope.ok) return 1;
1439
+ const stashPath = compactStashPath(worktreeScope.value, sessionToken);
1440
+ if (!stashPath.ok) return 1;
1441
+ const written = await appendJsonlRecord(stashPath.value, record);
1442
+ return written.ok ? 0 : 1;
1443
+ }
1444
+
1445
+ // src/interfaces/cli/compact.ts
1446
+ var COMPACT_CLI = {
1447
+ commandName: "compact",
1448
+ storeCommandName: "store",
1449
+ retrieveCommandName: "retrieve",
1450
+ description: "Store and retrieve compact resume state",
1451
+ transcriptFlag: "--transcript",
1452
+ transcriptOption: "--transcript <path>",
1453
+ sessionIdFlag: "--session-id",
1454
+ sessionIdOption: "--session-id <id>",
1455
+ sessionIdDescription: "Agent session identity (overrides the agent-session environment)"
1456
+ };
1457
+ var compactDomain = {
1458
+ name: COMPACT_CLI.commandName,
1459
+ description: COMPACT_CLI.description,
899
1460
  register: (program2) => {
900
- const claudeCmd = program2.command("claude").description("Manage Claude Code settings and plugins");
901
- registerClaudeCommands(claudeCmd);
1461
+ const compactCmd = program2.command(COMPACT_CLI.commandName).description(COMPACT_CLI.description);
1462
+ compactCmd.command(COMPACT_CLI.storeCommandName).description("Store compact resume state").requiredOption(COMPACT_CLI.transcriptOption, "Transcript JSONL path").option(COMPACT_CLI.sessionIdOption, COMPACT_CLI.sessionIdDescription).action(async (options) => {
1463
+ process.exit(
1464
+ await compactStoreCommand({
1465
+ transcript: options.transcript,
1466
+ sessionId: options.sessionId,
1467
+ cwd: process.cwd(),
1468
+ env: process.env
1469
+ })
1470
+ );
1471
+ });
1472
+ compactCmd.command(COMPACT_CLI.retrieveCommandName).description("Retrieve compact resume state").option(COMPACT_CLI.sessionIdOption, COMPACT_CLI.sessionIdDescription).action(async (options) => {
1473
+ const result = await compactRetrieveCommand({
1474
+ sessionId: options.sessionId,
1475
+ cwd: process.cwd(),
1476
+ env: process.env
1477
+ });
1478
+ if (result.output.length > 0) {
1479
+ process.stdout.write(result.output);
1480
+ }
1481
+ process.exitCode = result.exitCode;
1482
+ });
902
1483
  }
903
1484
  };
904
1485
 
905
1486
  // src/config/index.ts
906
1487
  import { readFile as readFile2 } from "fs/promises";
907
- import { join as join2 } from "path";
1488
+ import { join as join5 } from "path";
908
1489
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
909
1490
  import { parse as parseYaml, parseDocument as parseYamlDocument, stringify as stringifyYaml } from "yaml";
910
1491
 
@@ -1415,57 +1996,20 @@ function applyPathFilter(paths, config) {
1415
1996
  // src/domains/audit/config.ts
1416
1997
  var AUDIT_SECTION = "audit";
1417
1998
  var AUDIT_CONFIG_FIELDS = {
1418
- STORAGE: "storage",
1419
- SPX_DIR: "spxDir",
1420
- NODES_DIR: "nodesDir",
1421
- AUDIT_DIR: "auditDir",
1422
- RUNS_DIR: "runsDir",
1423
- VERDICT_FILE: "verdictFile",
1424
- VERDICT_FILE_SUFFIX: "verdictFileSuffix",
1425
- STATE_FILE: "stateFile",
1426
1999
  BASE_REF: "baseRef",
1427
- BRANCH_SLUG: "branchSlug",
1428
- MAX_BYTES: "maxBytes",
1429
2000
  AUDITORS: "auditors",
1430
2001
  TARGETS: "targets"
1431
2002
  };
1432
- var AUDIT_BRANCH_SLUG_MIN_MAX_BYTES = 10;
1433
2003
  var DEFAULT_AUDIT_CONFIG = {
1434
- storage: {
1435
- spxDir: ".spx",
1436
- nodesDir: "nodes",
1437
- auditDir: "audit",
1438
- runsDir: "runs",
1439
- verdictFile: "verdict.audit.xml",
1440
- verdictFileSuffix: ".audit.xml",
1441
- stateFile: "state.json"
1442
- },
1443
2004
  baseRef: "main",
1444
- branchSlug: {
1445
- maxBytes: 120
1446
- },
1447
2005
  auditors: [],
1448
2006
  targets: resolveDefaultTargets()
1449
2007
  };
1450
2008
  var AUDIT_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
1451
- AUDIT_CONFIG_FIELDS.STORAGE,
1452
2009
  AUDIT_CONFIG_FIELDS.BASE_REF,
1453
- AUDIT_CONFIG_FIELDS.BRANCH_SLUG,
1454
2010
  AUDIT_CONFIG_FIELDS.AUDITORS,
1455
2011
  AUDIT_CONFIG_FIELDS.TARGETS
1456
2012
  ]);
1457
- var AUDIT_STORAGE_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
1458
- AUDIT_CONFIG_FIELDS.SPX_DIR,
1459
- AUDIT_CONFIG_FIELDS.NODES_DIR,
1460
- AUDIT_CONFIG_FIELDS.AUDIT_DIR,
1461
- AUDIT_CONFIG_FIELDS.RUNS_DIR,
1462
- AUDIT_CONFIG_FIELDS.VERDICT_FILE,
1463
- AUDIT_CONFIG_FIELDS.VERDICT_FILE_SUFFIX,
1464
- AUDIT_CONFIG_FIELDS.STATE_FILE
1465
- ]);
1466
- var AUDIT_BRANCH_SLUG_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
1467
- AUDIT_CONFIG_FIELDS.MAX_BYTES
1468
- ]);
1469
2013
  var auditConfigDescriptor = {
1470
2014
  section: AUDIT_SECTION,
1471
2015
  defaults: DEFAULT_AUDIT_CONFIG,
@@ -1485,15 +2029,11 @@ function validateAuditConfig(raw) {
1485
2029
  }
1486
2030
  const unknown = rejectUnknownFields2(AUDIT_SECTION, raw, AUDIT_ALLOWED_FIELDS);
1487
2031
  if (!unknown.ok) return unknown;
1488
- const storage = raw[AUDIT_CONFIG_FIELDS.STORAGE] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.storage } : validateStorage(raw[AUDIT_CONFIG_FIELDS.STORAGE]);
1489
- if (!storage.ok) return storage;
1490
2032
  const baseRef = raw[AUDIT_CONFIG_FIELDS.BASE_REF] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.baseRef } : validateNonEmptyString2(
1491
2033
  `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BASE_REF}`,
1492
2034
  raw[AUDIT_CONFIG_FIELDS.BASE_REF]
1493
2035
  );
1494
2036
  if (!baseRef.ok) return baseRef;
1495
- const branchSlug = raw[AUDIT_CONFIG_FIELDS.BRANCH_SLUG] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.branchSlug } : validateBranchSlug(raw[AUDIT_CONFIG_FIELDS.BRANCH_SLUG]);
1496
- if (!branchSlug.ok) return branchSlug;
1497
2037
  const auditors = raw[AUDIT_CONFIG_FIELDS.AUDITORS] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.auditors } : validateArrayOfNonEmptyStrings(
1498
2038
  `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.AUDITORS}`,
1499
2039
  raw[AUDIT_CONFIG_FIELDS.AUDITORS]
@@ -1507,61 +2047,12 @@ function validateAuditConfig(raw) {
1507
2047
  return {
1508
2048
  ok: true,
1509
2049
  value: {
1510
- storage: storage.value,
1511
2050
  baseRef: baseRef.value,
1512
- branchSlug: branchSlug.value,
1513
2051
  auditors: auditors.value,
1514
2052
  targets: targets.value
1515
2053
  }
1516
2054
  };
1517
2055
  }
1518
- function validateStorage(raw) {
1519
- if (!isRecord2(raw)) {
1520
- return { ok: false, error: `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.STORAGE} must be an object` };
1521
- }
1522
- const unknown = rejectUnknownFields2(
1523
- `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.STORAGE}`,
1524
- raw,
1525
- AUDIT_STORAGE_ALLOWED_FIELDS
1526
- );
1527
- if (!unknown.ok) return unknown;
1528
- return validateStringRecord(
1529
- `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.STORAGE}`,
1530
- DEFAULT_AUDIT_CONFIG.storage,
1531
- raw
1532
- );
1533
- }
1534
- function validateBranchSlug(raw) {
1535
- if (!isRecord2(raw)) {
1536
- return { ok: false, error: `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BRANCH_SLUG} must be an object` };
1537
- }
1538
- const unknown = rejectUnknownFields2(
1539
- `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BRANCH_SLUG}`,
1540
- raw,
1541
- AUDIT_BRANCH_SLUG_ALLOWED_FIELDS
1542
- );
1543
- if (!unknown.ok) return unknown;
1544
- const maxBytesRaw = raw[AUDIT_CONFIG_FIELDS.MAX_BYTES];
1545
- if (maxBytesRaw === void 0) return { ok: true, value: DEFAULT_AUDIT_CONFIG.branchSlug };
1546
- if (typeof maxBytesRaw !== "number" || !Number.isInteger(maxBytesRaw) || maxBytesRaw < AUDIT_BRANCH_SLUG_MIN_MAX_BYTES) {
1547
- return {
1548
- ok: false,
1549
- error: `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BRANCH_SLUG}.${AUDIT_CONFIG_FIELDS.MAX_BYTES} must be an integer greater than or equal to ${AUDIT_BRANCH_SLUG_MIN_MAX_BYTES}`
1550
- };
1551
- }
1552
- return { ok: true, value: { maxBytes: maxBytesRaw } };
1553
- }
1554
- function validateStringRecord(path6, defaults4, raw) {
1555
- const next = { ...defaults4 };
1556
- for (const field of Object.keys(defaults4)) {
1557
- const value = raw[field];
1558
- if (value === void 0) continue;
1559
- const validated = validateNonEmptyString2(`${path6}.${field}`, value);
1560
- if (!validated.ok) return validated;
1561
- next[field] = validated.value;
1562
- }
1563
- return { ok: true, value: next };
1564
- }
1565
2056
  function validateNonEmptyString2(path6, value) {
1566
2057
  if (typeof value !== "string" || value.length === 0) {
1567
2058
  return { ok: false, error: `${path6} must be a non-empty string` };
@@ -1645,6 +2136,112 @@ var DECISION_KINDS = Object.keys(KIND_REGISTRY).filter(
1645
2136
  );
1646
2137
  var NODE_SUFFIXES = NODE_KINDS.map((k) => KIND_REGISTRY[k].suffix);
1647
2138
  var DECISION_SUFFIXES = DECISION_KINDS.map((k) => KIND_REGISTRY[k].suffix);
2139
+ var SPEC_TREE_GRAMMAR = {
2140
+ PRODUCT_SUFFIX: SPEC_TREE_CONFIG.PRODUCT.SUFFIX,
2141
+ EVIDENCE: {
2142
+ DIRECTORY_NAME: "tests",
2143
+ MODES: ["scenario", "mapping", "conformance", "property", "compliance"],
2144
+ LEVELS: ["l1", "l2", "l3"],
2145
+ TAILS: {
2146
+ TYPESCRIPT: ["test", "ts"],
2147
+ PYTHON: ["py"],
2148
+ RUST: ["rs"]
2149
+ },
2150
+ SEGMENT_SEPARATOR: "."
2151
+ },
2152
+ RUNNERS: ["vitest", "playwright", "subprocess"],
2153
+ ORDER: {
2154
+ SEPARATOR: "-",
2155
+ PATTERN: /^\d+$/
2156
+ },
2157
+ PATH_SEPARATOR: "/",
2158
+ COORDINATION_NOTES: ["PLAN.md", "ISSUES.md"],
2159
+ EVAL_LANE: ["eval.toml", "cases.jsonl", "prompt.md", "history.jsonl", "runs"],
2160
+ PRIOR_NODE_SUFFIXES: [".capability", ".feature", ".story"]
2161
+ };
2162
+ var SPEC_TREE_EVIDENCE_FILE = SPEC_TREE_GRAMMAR.EVIDENCE;
2163
+ var NAMING_SCHEMA_VERSION_ID = {
2164
+ PRIOR: "1.0.0",
2165
+ CANONICAL: "2.0.0"
2166
+ };
2167
+ function namingSchemaVersionFromNodeSuffixes(version2, nodeSuffixes) {
2168
+ return {
2169
+ version: version2,
2170
+ nodeSuffixes,
2171
+ decisionSuffixes: DECISION_SUFFIXES,
2172
+ productSuffix: SPEC_TREE_GRAMMAR.PRODUCT_SUFFIX,
2173
+ evidence: SPEC_TREE_GRAMMAR.EVIDENCE,
2174
+ runners: SPEC_TREE_GRAMMAR.RUNNERS,
2175
+ order: SPEC_TREE_GRAMMAR.ORDER,
2176
+ pathSeparator: SPEC_TREE_GRAMMAR.PATH_SEPARATOR,
2177
+ coordinationNotes: SPEC_TREE_GRAMMAR.COORDINATION_NOTES,
2178
+ evalLane: SPEC_TREE_GRAMMAR.EVAL_LANE
2179
+ };
2180
+ }
2181
+ var SPEC_TREE_NAMING_SCHEMA_VERSIONS = [
2182
+ namingSchemaVersionFromNodeSuffixes(NAMING_SCHEMA_VERSION_ID.PRIOR, SPEC_TREE_GRAMMAR.PRIOR_NODE_SUFFIXES),
2183
+ namingSchemaVersionFromNodeSuffixes(NAMING_SCHEMA_VERSION_ID.CANONICAL, NODE_SUFFIXES)
2184
+ ];
2185
+ var VERSION_COMPONENT_SEPARATOR = ".";
2186
+ var VERSION_COMPONENT_RADIX = 10;
2187
+ var VERSION_MISSING_COMPONENT = 0;
2188
+ var VERSION_ORDER_EQUAL = 0;
2189
+ var VERSION_NUMERIC_COMPONENT = /^\d+$/;
2190
+ function parseVersionComponents(version2) {
2191
+ return version2.split(VERSION_COMPONENT_SEPARATOR).map((part) => {
2192
+ if (!VERSION_NUMERIC_COMPONENT.test(part)) {
2193
+ throw new Error(
2194
+ `Naming-schema version "${version2}" must use numeric dotted components; "${part}" is not numeric`
2195
+ );
2196
+ }
2197
+ return Number.parseInt(part, VERSION_COMPONENT_RADIX);
2198
+ });
2199
+ }
2200
+ function compareNumericVersionIdentifiers(left, right) {
2201
+ const leftComponents = parseVersionComponents(left);
2202
+ const rightComponents = parseVersionComponents(right);
2203
+ const length = Math.max(leftComponents.length, rightComponents.length);
2204
+ for (let index = 0; index < length; index += 1) {
2205
+ const difference = (leftComponents[index] ?? VERSION_MISSING_COMPONENT) - (rightComponents[index] ?? VERSION_MISSING_COMPONENT);
2206
+ if (difference !== VERSION_ORDER_EQUAL) {
2207
+ return difference;
2208
+ }
2209
+ }
2210
+ return VERSION_ORDER_EQUAL;
2211
+ }
2212
+ function compareNamingSchemaVersions(left, right) {
2213
+ return compareNumericVersionIdentifiers(left.version, right.version);
2214
+ }
2215
+ function canonicalNamingSchemaVersion(versions) {
2216
+ const [first, ...rest] = versions;
2217
+ if (first === void 0) {
2218
+ throw new Error("Naming-schema version tuple must declare at least one version");
2219
+ }
2220
+ return rest.reduce(
2221
+ (max, version2) => compareNamingSchemaVersions(version2, max) > VERSION_ORDER_EQUAL ? version2 : max,
2222
+ first
2223
+ );
2224
+ }
2225
+ function supersededNodeSuffixes(versions) {
2226
+ const canonical = canonicalNamingSchemaVersion(versions);
2227
+ const canonicalSuffixes = new Set(canonical.nodeSuffixes);
2228
+ const superseded = /* @__PURE__ */ new Set();
2229
+ for (const version2 of versions) {
2230
+ if (version2 === canonical) {
2231
+ continue;
2232
+ }
2233
+ for (const suffix of version2.nodeSuffixes) {
2234
+ if (!canonicalSuffixes.has(suffix)) {
2235
+ superseded.add(suffix);
2236
+ }
2237
+ }
2238
+ }
2239
+ return [...superseded];
2240
+ }
2241
+ var SPEC_TREE_NAMING_VERSION = canonicalNamingSchemaVersion(SPEC_TREE_NAMING_SCHEMA_VERSIONS).version;
2242
+ var SPEC_TREE_SUPERSEDED_NODE_SUFFIXES = supersededNodeSuffixes(
2243
+ SPEC_TREE_NAMING_SCHEMA_VERSIONS
2244
+ );
1648
2245
  var SPEC_TREE_NODE_STATE = {
1649
2246
  DECLARED: "declared",
1650
2247
  SPECIFIED: "specified",
@@ -1822,7 +2419,7 @@ var REGISTERED_TOOL_NAMES = Object.keys(ADAPTER_MAP);
1822
2419
 
1823
2420
  // src/lib/file-inclusion/ignore-source.ts
1824
2421
  import { readFileSync } from "fs";
1825
- import { join } from "path";
2422
+ import { join as join4 } from "path";
1826
2423
  var IGNORE_SOURCE_FILENAME_DEFAULT = "EXCLUDE";
1827
2424
  var EMPTY_IGNORE_READER = {
1828
2425
  isUnderIgnoreSource() {
@@ -1856,7 +2453,7 @@ function validateEntry(entry, lineNumber) {
1856
2453
  }
1857
2454
  function createIgnoreSourceReader(projectRoot, config) {
1858
2455
  const { ignoreSourceFilename, specTreeRootSegment } = config;
1859
- const filePath = join(projectRoot, specTreeRootSegment, ignoreSourceFilename);
2456
+ const filePath = join4(projectRoot, specTreeRootSegment, ignoreSourceFilename);
1860
2457
  let content;
1861
2458
  try {
1862
2459
  content = readFileSync(filePath, "utf8");
@@ -1919,8 +2516,8 @@ var HIDDEN_PREFIX_LAYER = "hidden-prefix";
1919
2516
  var LAYER2 = HIDDEN_PREFIX_LAYER;
1920
2517
  function hiddenPrefixPredicate(path6, config) {
1921
2518
  const segments = path6.split("/");
1922
- const basename3 = segments[segments.length - 1] ?? "";
1923
- const matched = basename3.startsWith(config.hiddenPrefix);
2519
+ const basename4 = segments[segments.length - 1] ?? "";
2520
+ const matched = basename4.startsWith(config.hiddenPrefix);
1924
2521
  return { matched, layer: LAYER2 };
1925
2522
  }
1926
2523
 
@@ -2319,7 +2916,7 @@ var defaults3 = {
2319
2916
  enabled: false
2320
2917
  }
2321
2918
  };
2322
- function validatePaths2(raw) {
2919
+ function validatePaths(raw) {
2323
2920
  const basePath = `${VALIDATION_SECTION}.${VALIDATION_PATHS_SUBSECTION}`;
2324
2921
  const baseResult = validatePathFilterConfig(raw, basePath);
2325
2922
  if (!baseResult.ok) return baseResult;
@@ -2386,7 +2983,7 @@ function validate7(value) {
2386
2983
  }
2387
2984
  const candidate = value;
2388
2985
  const pathsRaw = candidate[VALIDATION_PATHS_SUBSECTION] ?? {};
2389
- const pathsResult = validatePaths2(pathsRaw);
2986
+ const pathsResult = validatePaths(pathsRaw);
2390
2987
  if (!pathsResult.ok) return pathsResult;
2391
2988
  const literalRaw = candidate[VALIDATION_LITERAL_SUBSECTION] ?? {};
2392
2989
  const literalResult = validateLiteral(literalRaw);
@@ -2414,11 +3011,11 @@ var productionRegistry = [
2414
3011
  ];
2415
3012
 
2416
3013
  // src/config/descriptor-digest.ts
2417
- import { createHash } from "crypto";
3014
+ import { createHash as createHash3 } from "crypto";
2418
3015
  var DEFAULT_DESCRIPTOR_PATH = "descriptor section";
2419
3016
  var SHA256_ALGORITHM = "sha256";
2420
- var UTF8_ENCODING = "utf8";
2421
- var HEX_ENCODING = "hex";
3017
+ var UTF8_ENCODING2 = "utf8";
3018
+ var HEX_ENCODING2 = "hex";
2422
3019
  function canonicalDescriptorJson(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
2423
3020
  const normalized = normalizeDescriptorJsonValue(value, path6, /* @__PURE__ */ new WeakSet());
2424
3021
  if (!normalized.ok) return normalized;
@@ -2427,7 +3024,7 @@ function canonicalDescriptorJson(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
2427
3024
  function digestDescriptorSection(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
2428
3025
  const canonical = canonicalDescriptorJson(value, path6);
2429
3026
  if (!canonical.ok) return canonical;
2430
- const sha256 = createHash(SHA256_ALGORITHM).update(Buffer.from(canonical.value, UTF8_ENCODING)).digest(HEX_ENCODING);
3027
+ const sha256 = createHash3(SHA256_ALGORITHM).update(Buffer.from(canonical.value, UTF8_ENCODING2)).digest(HEX_ENCODING2);
2431
3028
  return {
2432
3029
  ok: true,
2433
3030
  value: {
@@ -2569,7 +3166,7 @@ async function readProductConfigFile(productDir) {
2569
3166
  const detected = [];
2570
3167
  for (const format2 of CONFIG_FILE_FORMAT_ORDER) {
2571
3168
  const filename = CONFIG_FILE_DEFINITIONS[format2].filename;
2572
- const path6 = join2(productDir, filename);
3169
+ const path6 = join5(productDir, filename);
2573
3170
  let raw;
2574
3171
  try {
2575
3172
  raw = await readFile2(path6, "utf8");
@@ -2596,7 +3193,7 @@ function configFileForFormat(productDir, format2 = DEFAULT_CONFIG_FILE_FORMAT, r
2596
3193
  return {
2597
3194
  filename,
2598
3195
  format: format2,
2599
- path: join2(productDir, filename),
3196
+ path: join5(productDir, filename),
2600
3197
  raw
2601
3198
  };
2602
3199
  }
@@ -2893,7 +3490,7 @@ var configDomain = {
2893
3490
 
2894
3491
  // src/commands/session/archive.ts
2895
3492
  import { mkdir, rename, stat } from "fs/promises";
2896
- import { dirname as dirname2, join as join4 } from "path";
3493
+ import { dirname as dirname3, join as join7 } from "path";
2897
3494
 
2898
3495
  // src/domains/session/archive.ts
2899
3496
  var SESSION_FILE_EXTENSION = ".md";
@@ -2974,24 +3571,24 @@ var HANDOFF_BASE_PREREQUISITE_LABEL = {
2974
3571
  DETACHED_AT_DEFAULT_TIP: "HEAD is detached at the default-branch tip"
2975
3572
  };
2976
3573
  var HANDOFF_BASE_REMEDY = {
2977
- /** Unclean working tree: commit, or hand off from the root worktree. */
2978
- COMMIT_OR_ROOT: "commit the changes, or run handoff from the root worktree",
2979
- /** Off the default-branch tip: detach to it, or hand off from the root worktree. */
2980
- DETACH_TO_TIP_OR_ROOT: "detach HEAD to the default-branch tip, or run handoff from the root worktree",
2981
- /** Default branch unresolved: only the root worktree can anchor the base. */
2982
- ROOT_ONLY: "run handoff from the root worktree"
3574
+ /** Unclean working tree: commit, or hand off from the main checkout. */
3575
+ COMMIT_OR_MAIN_CHECKOUT: "commit the changes, or run handoff from the main checkout",
3576
+ /** Off the default-branch tip: detach to it, or hand off from the main checkout. */
3577
+ DETACH_TO_TIP_OR_MAIN_CHECKOUT: "detach HEAD to the default-branch tip, or run handoff from the main checkout",
3578
+ /** Default branch unresolved: only the main checkout can anchor the base. */
3579
+ MAIN_CHECKOUT_ONLY: "run handoff from the main checkout"
2983
3580
  };
2984
3581
  var HANDOFF_BASE_FACT_LABEL = {
2985
3582
  DEFAULT_BRANCH: "default branch",
2986
3583
  DEFAULT_TIP: "origin tip",
2987
3584
  HEAD: "HEAD",
2988
3585
  CURRENT_WORKTREE: "current worktree",
2989
- ROOT_WORKTREE: "root worktree"
3586
+ MAIN_CHECKOUT: "main checkout"
2990
3587
  };
2991
3588
  var HANDOFF_BASE_UNRESOLVED = "unresolved";
2992
3589
  var CHECKLIST_INDENT = " ";
2993
3590
  var REMEDY_SEPARATOR = " \u2014 ";
2994
- var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this linked worktree.`;
3591
+ var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this worktree \u2014 it is not the main checkout.`;
2995
3592
  function renderFactLine(label, value) {
2996
3593
  return `${CHECKLIST_INDENT}${label}: ${value ?? HANDOFF_BASE_UNRESOLVED}`;
2997
3594
  }
@@ -3007,7 +3604,7 @@ function renderHandoffBaseChecklist(checklist) {
3007
3604
  renderFactLine(HANDOFF_BASE_FACT_LABEL.DEFAULT_TIP, checklist.defaultTipSha),
3008
3605
  renderFactLine(HANDOFF_BASE_FACT_LABEL.HEAD, checklist.headSha),
3009
3606
  renderFactLine(HANDOFF_BASE_FACT_LABEL.CURRENT_WORKTREE, checklist.currentWorktreePath),
3010
- renderFactLine(HANDOFF_BASE_FACT_LABEL.ROOT_WORKTREE, checklist.rootWorktreePath),
3607
+ renderFactLine(HANDOFF_BASE_FACT_LABEL.MAIN_CHECKOUT, checklist.mainCheckoutPath),
3011
3608
  ...checklist.prerequisites.map(renderPrerequisiteLine)
3012
3609
  ].join("\n");
3013
3610
  }
@@ -3049,6 +3646,18 @@ var SessionInvalidGoalError = class extends SessionError {
3049
3646
  this.name = "SessionInvalidGoalError";
3050
3647
  }
3051
3648
  };
3649
+ var SessionInvalidFieldError = class extends SessionError {
3650
+ /** The unknown field token the caller supplied. */
3651
+ field;
3652
+ /** The full set of valid field names. */
3653
+ validFields;
3654
+ constructor(field, validFields) {
3655
+ super(`Invalid field: "${field}". Valid fields: ${validFields.join(", ")}`);
3656
+ this.name = "SessionInvalidFieldError";
3657
+ this.field = field;
3658
+ this.validFields = validFields;
3659
+ }
3660
+ };
3052
3661
  var SessionInvalidNextStepError = class extends SessionError {
3053
3662
  constructor() {
3054
3663
  super("Invalid session next_step: next_step must be a non-empty string.");
@@ -3062,7 +3671,7 @@ var SessionHandoffBaseError = class extends SessionError {
3062
3671
  silent;
3063
3672
  constructor(options = {}) {
3064
3673
  super(
3065
- "Cannot create a handoff session from this git work context. Run handoff from the root worktree, or from a linked worktree with a clean working tree detached at the tip of the default branch."
3674
+ "Cannot create a handoff session from this git work context. Run handoff from the main checkout, or from a non-main checkout with a clean working tree detached at the tip of the default branch."
3066
3675
  );
3067
3676
  this.name = SESSION_HANDOFF_BASE_ERROR_NAME;
3068
3677
  this.checklist = options.checklist ?? null;
@@ -3091,7 +3700,18 @@ var SessionLegacyFrontmatterInputError = class extends SessionError {
3091
3700
 
3092
3701
  printf '%s\\n' '{"priority":"high","goal":"...","next_step":"..."}' '# Body' | spx session handoff`
3093
3702
  );
3094
- this.name = "SessionLegacyFrontmatterInputError";
3703
+ this.name = "SessionLegacyFrontmatterInputError";
3704
+ }
3705
+ };
3706
+ var SessionWorkBranchNotOnOriginError = class extends SessionError {
3707
+ /** The work-branch ref the caller supplied that does not resolve on `origin`. */
3708
+ workBranch;
3709
+ constructor(workBranch) {
3710
+ super(
3711
+ `Work-branch ref not found on origin: ${workBranch}. Push the branch to origin before recording it as the handoff base, or omit git_ref to record the git context.`
3712
+ );
3713
+ this.name = "SessionWorkBranchNotOnOriginError";
3714
+ this.workBranch = workBranch;
3095
3715
  }
3096
3716
  };
3097
3717
  var SessionInvalidJsonHeaderError = class extends SessionError {
@@ -3101,14 +3721,12 @@ var SessionInvalidJsonHeaderError = class extends SessionError {
3101
3721
  }
3102
3722
  };
3103
3723
 
3104
- // src/git/root.ts
3105
- import { execa as execa2 } from "execa";
3106
- import { dirname, isAbsolute, join as join3, relative as relative2, resolve as resolve3 } from "path";
3724
+ // src/commands/session/resolve-config.ts
3725
+ import { join as join6 } from "path";
3107
3726
 
3108
3727
  // src/config/defaults.ts
3109
3728
  var DEFAULT_CONFIG = {
3110
3729
  sessions: {
3111
- dir: ".spx/sessions",
3112
3730
  statusDirs: {
3113
3731
  todo: "todo",
3114
3732
  doing: "doing",
@@ -3117,228 +3735,29 @@ var DEFAULT_CONFIG = {
3117
3735
  }
3118
3736
  };
3119
3737
 
3120
- // src/git/environment.ts
3121
- function withoutGitEnvironment(env) {
3122
- const cleaned = { ...env };
3123
- for (const key of Object.keys(cleaned)) {
3124
- if (key.startsWith("GIT_")) {
3125
- delete cleaned[key];
3126
- }
3127
- }
3128
- return cleaned;
3129
- }
3130
-
3131
- // src/git/root.ts
3132
- var defaultDeps = {
3133
- execa: async (command, args, options) => {
3134
- const result = await execa2(command, args, {
3135
- ...options,
3136
- env: withoutGitEnvironment(process.env),
3137
- extendEnv: false
3138
- });
3139
- return {
3140
- exitCode: result.exitCode ?? 0,
3141
- stdout: typeof result.stdout === "string" ? result.stdout : String(result.stdout),
3142
- stderr: typeof result.stderr === "string" ? result.stderr : String(result.stderr)
3143
- };
3144
- }
3145
- };
3146
- var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository; resolving session storage relative to the current directory.";
3147
- var GIT_ROOT_COMMAND = {
3148
- EXECUTABLE: "git",
3149
- REV_PARSE: "rev-parse",
3150
- ABBREV_REF: "--abbrev-ref",
3151
- GIT_COMMON_DIR: "--git-common-dir",
3152
- HEAD: "HEAD",
3153
- SHOW_TOPLEVEL: "--show-toplevel",
3154
- SYMBOLIC_REF: "symbolic-ref",
3155
- SHORT: "--short",
3156
- ORIGIN_HEAD_REF: "refs/remotes/origin/HEAD",
3157
- STATUS: "status",
3158
- PORCELAIN: "--porcelain",
3159
- PATH_FORMAT_ABSOLUTE: "--path-format=absolute"
3160
- };
3161
- var ORIGIN_REF_PREFIX = "origin/";
3162
- var GIT_SHOW_TOPLEVEL_ARGS = [
3163
- GIT_ROOT_COMMAND.REV_PARSE,
3164
- GIT_ROOT_COMMAND.SHOW_TOPLEVEL
3165
- ];
3166
- var GIT_COMMON_DIR_ARGS = [
3167
- GIT_ROOT_COMMAND.REV_PARSE,
3168
- GIT_ROOT_COMMAND.PATH_FORMAT_ABSOLUTE,
3169
- GIT_ROOT_COMMAND.GIT_COMMON_DIR
3170
- ];
3171
- var GIT_CURRENT_BRANCH_ARGS = [
3172
- GIT_ROOT_COMMAND.REV_PARSE,
3173
- GIT_ROOT_COMMAND.ABBREV_REF,
3174
- GIT_ROOT_COMMAND.HEAD
3175
- ];
3176
- var GIT_HEAD_SHA_ARGS = [
3177
- GIT_ROOT_COMMAND.REV_PARSE,
3178
- GIT_ROOT_COMMAND.HEAD
3179
- ];
3180
- var GIT_ORIGIN_HEAD_REF_ARGS = [
3181
- GIT_ROOT_COMMAND.SYMBOLIC_REF,
3182
- GIT_ROOT_COMMAND.SHORT,
3183
- GIT_ROOT_COMMAND.ORIGIN_HEAD_REF
3184
- ];
3185
- var GIT_STATUS_PORCELAIN_ARGS = [
3186
- GIT_ROOT_COMMAND.STATUS,
3187
- GIT_ROOT_COMMAND.PORCELAIN
3188
- ];
3189
- async function detectWorktreeProductRoot(cwd = process.cwd(), deps = defaultDeps) {
3190
- try {
3191
- const result = await deps.execa(
3192
- GIT_ROOT_COMMAND.EXECUTABLE,
3193
- [...GIT_SHOW_TOPLEVEL_ARGS],
3194
- { cwd, reject: false }
3195
- );
3196
- if (result.exitCode === 0 && result.stdout) {
3197
- return {
3198
- productDir: extractStdout(result.stdout),
3199
- isGitRepo: true
3200
- };
3201
- }
3202
- return {
3203
- productDir: cwd,
3204
- isGitRepo: false,
3205
- warning: NOT_GIT_REPO_WARNING
3206
- };
3207
- } catch {
3208
- return {
3209
- productDir: cwd,
3210
- isGitRepo: false,
3211
- warning: NOT_GIT_REPO_WARNING
3212
- };
3213
- }
3214
- }
3215
- function extractStdout(stdout) {
3216
- if (!stdout) return "";
3217
- const str = typeof stdout === "string" ? stdout : String(stdout);
3218
- return str.trim().replace(/\/+$/, "");
3219
- }
3220
- async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = defaultDeps) {
3221
- try {
3222
- const toplevelResult = await deps.execa(
3223
- GIT_ROOT_COMMAND.EXECUTABLE,
3224
- [...GIT_SHOW_TOPLEVEL_ARGS],
3225
- { cwd, reject: false }
3226
- );
3227
- if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) {
3228
- return {
3229
- productDir: cwd,
3230
- isGitRepo: false,
3231
- warning: NOT_GIT_REPO_WARNING,
3232
- worktreeRoot: cwd
3233
- };
3234
- }
3235
- const toplevel = extractStdout(toplevelResult.stdout);
3236
- const commonDirResult = await deps.execa(
3237
- GIT_ROOT_COMMAND.EXECUTABLE,
3238
- [...GIT_COMMON_DIR_ARGS],
3239
- { cwd, reject: false }
3240
- );
3241
- if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
3242
- return {
3243
- productDir: toplevel,
3244
- isGitRepo: true,
3245
- worktreeRoot: toplevel
3246
- };
3247
- }
3248
- const commonDir = extractStdout(commonDirResult.stdout);
3249
- const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve3(toplevel, commonDir);
3250
- const gitCommonDirProductRoot = dirname(absoluteCommonDir);
3251
- return {
3252
- productDir: gitCommonDirProductRoot,
3253
- isGitRepo: true,
3254
- worktreeRoot: toplevel
3255
- };
3256
- } catch {
3257
- return {
3258
- productDir: cwd,
3259
- isGitRepo: false,
3260
- warning: NOT_GIT_REPO_WARNING,
3261
- worktreeRoot: cwd
3262
- };
3263
- }
3264
- }
3265
- async function resolveDefaultBranch(cwd = process.cwd(), deps = defaultDeps) {
3266
- const result = await deps.execa(
3267
- GIT_ROOT_COMMAND.EXECUTABLE,
3268
- [...GIT_ORIGIN_HEAD_REF_ARGS],
3269
- { cwd, reject: false }
3270
- );
3271
- if (result.exitCode !== 0) return null;
3272
- const ref = extractStdout(result.stdout);
3273
- if (!ref.startsWith(ORIGIN_REF_PREFIX)) return null;
3274
- const branch = ref.slice(ORIGIN_REF_PREFIX.length);
3275
- return branch.length === 0 ? null : branch;
3276
- }
3277
- async function getCurrentBranch(cwd = process.cwd(), deps = defaultDeps) {
3278
- const result = await deps.execa(
3279
- GIT_ROOT_COMMAND.EXECUTABLE,
3280
- [...GIT_CURRENT_BRANCH_ARGS],
3281
- { cwd, reject: false }
3282
- );
3283
- if (result.exitCode !== 0) return null;
3284
- const branch = extractStdout(result.stdout);
3285
- if (branch.length === 0 || branch === GIT_ROOT_COMMAND.HEAD) return null;
3286
- return branch;
3287
- }
3288
- async function getHeadSha(cwd = process.cwd(), deps = defaultDeps) {
3289
- const result = await deps.execa(
3290
- GIT_ROOT_COMMAND.EXECUTABLE,
3291
- [...GIT_HEAD_SHA_ARGS],
3292
- { cwd, reject: false }
3293
- );
3294
- if (result.exitCode !== 0) return null;
3295
- const sha = extractStdout(result.stdout);
3296
- return sha.length === 0 ? null : sha;
3297
- }
3298
- async function resolveRefSha(ref, cwd = process.cwd(), deps = defaultDeps) {
3299
- const result = await deps.execa(
3300
- GIT_ROOT_COMMAND.EXECUTABLE,
3301
- [GIT_ROOT_COMMAND.REV_PARSE, ref],
3302
- { cwd, reject: false }
3303
- );
3304
- if (result.exitCode !== 0) return null;
3305
- const sha = extractStdout(result.stdout);
3306
- return sha.length === 0 ? null : sha;
3307
- }
3308
- async function isWorkingTreeClean(cwd = process.cwd(), deps = defaultDeps) {
3309
- const result = await deps.execa(
3310
- GIT_ROOT_COMMAND.EXECUTABLE,
3311
- [...GIT_STATUS_PORCELAIN_ARGS],
3312
- { cwd, reject: false }
3313
- );
3314
- if (result.exitCode !== 0) return false;
3315
- return extractStdout(result.stdout).length === 0;
3316
- }
3738
+ // src/commands/session/resolve-config.ts
3317
3739
  async function resolveSessionConfig(options = {}) {
3318
3740
  const { sessionsDir, cwd, deps } = options;
3319
3741
  const { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
3320
3742
  if (sessionsDir) {
3321
3743
  return {
3322
3744
  config: {
3323
- todoDir: join3(sessionsDir, statusDirs2.todo),
3324
- doingDir: join3(sessionsDir, statusDirs2.doing),
3325
- archiveDir: join3(sessionsDir, statusDirs2.archive)
3745
+ todoDir: join6(sessionsDir, statusDirs2.todo),
3746
+ doingDir: join6(sessionsDir, statusDirs2.doing),
3747
+ archiveDir: join6(sessionsDir, statusDirs2.archive)
3326
3748
  }
3327
3749
  };
3328
3750
  }
3329
- const gitResult = await detectGitCommonDirProductRoot(cwd, deps);
3330
- const baseDir = join3(gitResult.productDir, DEFAULT_CONFIG.sessions.dir);
3751
+ const { sessionsDir: baseDir, warning } = await resolveSessionsScopeDir({ cwd, deps });
3331
3752
  return {
3332
3753
  config: {
3333
- todoDir: join3(baseDir, statusDirs2.todo),
3334
- doingDir: join3(baseDir, statusDirs2.doing),
3335
- archiveDir: join3(baseDir, statusDirs2.archive)
3754
+ todoDir: join6(baseDir, statusDirs2.todo),
3755
+ doingDir: join6(baseDir, statusDirs2.doing),
3756
+ archiveDir: join6(baseDir, statusDirs2.archive)
3336
3757
  },
3337
- warning: gitResult.warning
3758
+ warning
3338
3759
  };
3339
3760
  }
3340
-
3341
- // src/commands/session/resolve-config.ts
3342
3761
  async function resolveSessionConfigSurfacingWarning(sessionsDir, onWarning) {
3343
3762
  const { config, warning } = await resolveSessionConfig({ sessionsDir });
3344
3763
  if (warning !== void 0) {
@@ -3371,9 +3790,9 @@ async function fileExists(path6) {
3371
3790
  }
3372
3791
  async function probeSessionPaths(sessionId, config) {
3373
3792
  const filename = `${sessionId}${SESSION_FILE_EXTENSION}`;
3374
- const todoPath = join4(config.todoDir, filename);
3375
- const doingPath = join4(config.doingDir, filename);
3376
- const archivePath = join4(config.archiveDir, filename);
3793
+ const todoPath = join7(config.todoDir, filename);
3794
+ const doingPath = join7(config.doingDir, filename);
3795
+ const archivePath = join7(config.archiveDir, filename);
3377
3796
  return {
3378
3797
  todo: await fileExists(todoPath) ? todoPath : null,
3379
3798
  doing: await fileExists(doingPath) ? doingPath : null,
@@ -3394,7 +3813,7 @@ async function resolveArchivePaths(sessionId, config) {
3394
3813
  }
3395
3814
  async function archiveSingle(sessionId, config) {
3396
3815
  const { source, target } = await resolveArchivePaths(sessionId, config);
3397
- await mkdir(dirname2(target), { recursive: true });
3816
+ await mkdir(dirname3(target), { recursive: true });
3398
3817
  await rename(source, target);
3399
3818
  return `${SESSION_ARCHIVE_OUTPUT.ARCHIVED}: ${sessionId}
3400
3819
  ${SESSION_ARCHIVE_OUTPUT.ARCHIVE_LOCATION}: ${target}`;
@@ -3417,7 +3836,7 @@ function resolveDeletePath(sessionId, existingPaths) {
3417
3836
  }
3418
3837
 
3419
3838
  // src/domains/session/show.ts
3420
- import { join as join5 } from "path";
3839
+ import { join as join8 } from "path";
3421
3840
 
3422
3841
  // src/domains/session/list.ts
3423
3842
  import { parse as parseYaml2 } from "yaml";
@@ -3428,12 +3847,12 @@ var SESSION_ID_SEPARATOR = "_";
3428
3847
  function generateSessionId(options = {}) {
3429
3848
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
3430
3849
  const date = now();
3431
- const year = date.getFullYear();
3432
- const month = String(date.getMonth() + 1).padStart(2, "0");
3433
- const day = String(date.getDate()).padStart(2, "0");
3434
- const hours = String(date.getHours()).padStart(2, "0");
3435
- const minutes = String(date.getMinutes()).padStart(2, "0");
3436
- const seconds = String(date.getSeconds()).padStart(2, "0");
3850
+ const year = date.getUTCFullYear();
3851
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
3852
+ const day = String(date.getUTCDate()).padStart(2, "0");
3853
+ const hours = String(date.getUTCHours()).padStart(2, "0");
3854
+ const minutes = String(date.getUTCMinutes()).padStart(2, "0");
3855
+ const seconds = String(date.getUTCSeconds()).padStart(2, "0");
3437
3856
  return `${year}-${month}-${day}${SESSION_ID_SEPARATOR}${hours}-${minutes}-${seconds}`;
3438
3857
  }
3439
3858
  function parseSessionId(id) {
@@ -3453,7 +3872,11 @@ function parseSessionId(id) {
3453
3872
  if (hours < 0 || hours > 23) return null;
3454
3873
  if (minutes < 0 || minutes > 59) return null;
3455
3874
  if (seconds < 0 || seconds > 59) return null;
3456
- return new Date(year, month, day, hours, minutes, seconds);
3875
+ const date = new Date(Date.UTC(year, month, day, hours, minutes, seconds));
3876
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day || date.getUTCHours() !== hours || date.getUTCMinutes() !== minutes || date.getUTCSeconds() !== seconds) {
3877
+ return null;
3878
+ }
3879
+ return date;
3457
3880
  }
3458
3881
 
3459
3882
  // src/domains/session/types.ts
@@ -3556,6 +3979,67 @@ function sortSessions(sessions) {
3556
3979
  return dateB.getTime() - dateA.getTime();
3557
3980
  });
3558
3981
  }
3982
+ var FIELD_SELECTION_SEPARATOR = ",";
3983
+ var SESSION_RECORD_FIELD = {
3984
+ ID: "id",
3985
+ STATUS: "status",
3986
+ PRIORITY: SESSION_FRONT_MATTER.PRIORITY,
3987
+ GIT_REF: SESSION_FRONT_MATTER.GIT_REF,
3988
+ GOAL: SESSION_FRONT_MATTER.GOAL,
3989
+ NEXT_STEP: SESSION_FRONT_MATTER.NEXT_STEP,
3990
+ SPECS: SESSION_FRONT_MATTER.SPECS,
3991
+ FILES: SESSION_FRONT_MATTER.FILES,
3992
+ CREATED_AT: SESSION_FRONT_MATTER.CREATED_AT,
3993
+ AGENT_SESSION_ID: SESSION_FRONT_MATTER.AGENT_SESSION_ID
3994
+ };
3995
+ var SESSION_RECORD_FIELDS = Object.values(SESSION_RECORD_FIELD);
3996
+ function toSessionRecord(session) {
3997
+ const { id, status, metadata } = session;
3998
+ const record = {
3999
+ id,
4000
+ status,
4001
+ priority: metadata.priority,
4002
+ git_ref: metadata.git_ref,
4003
+ goal: metadata.goal,
4004
+ next_step: metadata.next_step,
4005
+ specs: metadata.specs,
4006
+ files: metadata.files
4007
+ };
4008
+ if (metadata.created_at !== void 0) {
4009
+ record.created_at = metadata.created_at;
4010
+ }
4011
+ if (metadata.agent_session_id !== void 0) {
4012
+ record.agent_session_id = metadata.agent_session_id;
4013
+ }
4014
+ return record;
4015
+ }
4016
+ function projectSessionRecord(record, fields) {
4017
+ const projected = {};
4018
+ for (const field of fields) {
4019
+ const value = record[field];
4020
+ if (value !== void 0) {
4021
+ projected[field] = value;
4022
+ }
4023
+ }
4024
+ return projected;
4025
+ }
4026
+ function isSessionRecordField(value) {
4027
+ return SESSION_RECORD_FIELDS.includes(value);
4028
+ }
4029
+ function parseFieldSelection(input) {
4030
+ const tokens = input.split(FIELD_SELECTION_SEPARATOR).map((token) => token.trim()).filter((token) => token.length > 0);
4031
+ if (tokens.length === 0) {
4032
+ throw new SessionInvalidFieldError(input, SESSION_RECORD_FIELDS);
4033
+ }
4034
+ const selection = [];
4035
+ for (const token of tokens) {
4036
+ if (!isSessionRecordField(token)) {
4037
+ throw new SessionInvalidFieldError(token, SESSION_RECORD_FIELDS);
4038
+ }
4039
+ selection.push(token);
4040
+ }
4041
+ return selection;
4042
+ }
3559
4043
 
3560
4044
  // src/domains/session/show.ts
3561
4045
  var SESSION_SHOW_LABEL = {
@@ -3569,11 +4053,12 @@ var SESSION_SHOW_LABEL = {
3569
4053
  };
3570
4054
  var SESSION_SHOW_SEPARATOR_CHAR = "\u2500";
3571
4055
  var SESSION_SHOW_SEPARATOR_WIDTH = 40;
3572
- var { dir: sessionsBaseDir, statusDirs } = DEFAULT_CONFIG.sessions;
4056
+ var sessionsBaseDir = join8(STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
4057
+ var { statusDirs } = DEFAULT_CONFIG.sessions;
3573
4058
  var DEFAULT_SESSION_CONFIG = {
3574
- todoDir: join5(sessionsBaseDir, statusDirs.todo),
3575
- doingDir: join5(sessionsBaseDir, statusDirs.doing),
3576
- archiveDir: join5(sessionsBaseDir, statusDirs.archive)
4059
+ todoDir: join8(sessionsBaseDir, statusDirs.todo),
4060
+ doingDir: join8(sessionsBaseDir, statusDirs.doing),
4061
+ archiveDir: join8(sessionsBaseDir, statusDirs.archive)
3577
4062
  };
3578
4063
  var SEARCH_ORDER = [...SESSION_STATUSES];
3579
4064
  function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
@@ -3631,7 +4116,7 @@ async function deleteCommand(options) {
3631
4116
 
3632
4117
  // src/commands/session/handoff.ts
3633
4118
  import { mkdir as mkdir2, writeFile } from "fs/promises";
3634
- import { join as join6, resolve as resolve4 } from "path";
4119
+ import { join as join9, resolve as resolve3 } from "path";
3635
4120
  import { stringify as stringifyYaml3 } from "yaml";
3636
4121
 
3637
4122
  // src/domains/session/create.ts
@@ -3652,11 +4137,11 @@ function cleanPrerequisite(facts) {
3652
4137
  return {
3653
4138
  label: HANDOFF_BASE_PREREQUISITE_LABEL.CLEAN_WORKING_TREE,
3654
4139
  met: facts.isClean,
3655
- remedy: facts.isClean ? "" : HANDOFF_BASE_REMEDY.COMMIT_OR_ROOT
4140
+ remedy: facts.isClean ? "" : HANDOFF_BASE_REMEDY.COMMIT_OR_MAIN_CHECKOUT
3656
4141
  };
3657
4142
  }
3658
4143
  function detachedAtTipPrerequisite(facts, met) {
3659
- const remedy = facts.defaultTipSha === null ? HANDOFF_BASE_REMEDY.ROOT_ONLY : HANDOFF_BASE_REMEDY.DETACH_TO_TIP_OR_ROOT;
4144
+ const remedy = facts.defaultTipSha === null ? HANDOFF_BASE_REMEDY.MAIN_CHECKOUT_ONLY : HANDOFF_BASE_REMEDY.DETACH_TO_TIP_OR_MAIN_CHECKOUT;
3660
4145
  return {
3661
4146
  label: HANDOFF_BASE_PREREQUISITE_LABEL.DETACHED_AT_DEFAULT_TIP,
3662
4147
  met,
@@ -3669,7 +4154,7 @@ function buildChecklist(facts, prerequisites) {
3669
4154
  defaultTipSha: facts.defaultTipSha,
3670
4155
  headSha: facts.headSha,
3671
4156
  currentWorktreePath: facts.currentWorktreePath,
3672
- rootWorktreePath: facts.rootWorktreePath,
4157
+ mainCheckoutPath: facts.mainCheckoutPath,
3673
4158
  prerequisites
3674
4159
  };
3675
4160
  }
@@ -3677,7 +4162,7 @@ function resolveHandoffGitRef(facts) {
3677
4162
  if (!facts.isGitRepo) {
3678
4163
  throw new SessionHandoffBaseError({ silent: true });
3679
4164
  }
3680
- if (facts.isRootWorktree) {
4165
+ if (facts.isMainCheckout) {
3681
4166
  if (facts.branch !== null) return facts.branch;
3682
4167
  if (facts.headSha !== null) return facts.headSha;
3683
4168
  throw new SessionHandoffBaseError();
@@ -3692,6 +4177,12 @@ function resolveHandoffGitRef(facts) {
3692
4177
  }
3693
4178
  throw new SessionHandoffBaseError({ checklist: buildChecklist(facts, prerequisites) });
3694
4179
  }
4180
+ function resolveWorkBranchGitRef(workBranch, existsOnOrigin) {
4181
+ if (!existsOnOrigin) {
4182
+ throw new SessionWorkBranchNotOnOriginError(workBranch);
4183
+ }
4184
+ return workBranch;
4185
+ }
3695
4186
 
3696
4187
  // src/domains/session/parse-handoff-input.ts
3697
4188
  var LEGACY_FRONTMATTER_PREFIX = /^---\r?\n/;
@@ -3782,12 +4273,14 @@ function validateHandoffHeader(parsed) {
3782
4273
  const nextStep = ensureStringOrEmpty(obj[SESSION_FRONT_MATTER.NEXT_STEP], SESSION_FRONT_MATTER.NEXT_STEP);
3783
4274
  const specs = ensureStringArrayOrDefault(obj[SESSION_FRONT_MATTER.SPECS], SESSION_FRONT_MATTER.SPECS);
3784
4275
  const files = ensureStringArrayOrDefault(obj[SESSION_FRONT_MATTER.FILES], SESSION_FRONT_MATTER.FILES);
4276
+ const gitRef = ensureOptionalString(obj[SESSION_FRONT_MATTER.GIT_REF], SESSION_FRONT_MATTER.GIT_REF);
3785
4277
  return {
3786
4278
  priority,
3787
4279
  goal,
3788
4280
  next_step: nextStep,
3789
4281
  specs,
3790
- files
4282
+ files,
4283
+ ...gitRef === void 0 ? {} : { git_ref: gitRef }
3791
4284
  };
3792
4285
  }
3793
4286
  function ensurePriorityOrDefault(value) {
@@ -3806,6 +4299,13 @@ function ensureStringOrEmpty(value, fieldName) {
3806
4299
  }
3807
4300
  return value;
3808
4301
  }
4302
+ function ensureOptionalString(value, fieldName) {
4303
+ if (value === void 0) return void 0;
4304
+ if (typeof value !== "string") {
4305
+ throw new SessionInvalidJsonHeaderError(`${fieldName} must be a string`);
4306
+ }
4307
+ return value;
4308
+ }
3809
4309
  function ensureStringArrayOrDefault(value, fieldName) {
3810
4310
  if (value === void 0) return [];
3811
4311
  if (!Array.isArray(value)) {
@@ -3821,18 +4321,19 @@ function ensureStringArrayOrDefault(value, fieldName) {
3821
4321
 
3822
4322
  // src/commands/session/handoff.ts
3823
4323
  async function resolveSessionGitRef(cwd, deps) {
3824
- const [branch, headSha, gitRoots] = await Promise.all([
4324
+ const [branch, headSha, facts] = await Promise.all([
3825
4325
  getCurrentBranch(cwd, deps),
3826
4326
  getHeadSha(cwd, deps),
3827
- detectGitCommonDirProductRoot(cwd, deps)
4327
+ gatherGitFacts(cwd, deps)
3828
4328
  ]);
3829
- const currentWorktreePath = gitRoots.worktreeRoot;
3830
- const rootWorktreePath = gitRoots.productDir;
3831
- const isRoot = currentWorktreePath === rootWorktreePath;
4329
+ const isGitRepo = facts !== null;
4330
+ const currentWorktreePath = facts?.worktreeRoot ?? cwd ?? process.cwd();
4331
+ const isMain = facts !== null && isMainCheckout(facts);
4332
+ const designatedMainCheckout = facts !== null ? mainCheckoutPath(facts) : null;
3832
4333
  let isClean = false;
3833
4334
  let defaultBranch = null;
3834
4335
  let defaultTipSha = null;
3835
- if (gitRoots.isGitRepo && !isRoot) {
4336
+ if (isGitRepo && !isMain) {
3836
4337
  [isClean, defaultBranch] = await Promise.all([
3837
4338
  isWorkingTreeClean(cwd, deps),
3838
4339
  resolveDefaultBranch(cwd, deps)
@@ -3840,17 +4341,22 @@ async function resolveSessionGitRef(cwd, deps) {
3840
4341
  defaultTipSha = defaultBranch === null ? null : await resolveRefSha(`${ORIGIN_REF_PREFIX}${defaultBranch}`, cwd, deps);
3841
4342
  }
3842
4343
  return resolveHandoffGitRef({
3843
- isGitRepo: gitRoots.isGitRepo,
3844
- isRootWorktree: isRoot,
4344
+ isGitRepo,
4345
+ isMainCheckout: isMain,
3845
4346
  branch,
3846
4347
  headSha,
3847
4348
  isClean,
3848
4349
  defaultBranch,
3849
4350
  defaultTipSha,
3850
4351
  currentWorktreePath,
3851
- rootWorktreePath
4352
+ mainCheckoutPath: designatedMainCheckout
3852
4353
  });
3853
4354
  }
4355
+ async function resolveRecordedGitRef(suppliedRef, gateRef, cwd, deps) {
4356
+ if (suppliedRef === void 0 || suppliedRef.length === 0) return gateRef;
4357
+ const existsOnOrigin = await originBranchExists(suppliedRef, cwd, deps);
4358
+ return resolveWorkBranchGitRef(suppliedRef, existsOnOrigin);
4359
+ }
3854
4360
  async function handoffCommand(options) {
3855
4361
  const { config } = await resolveSessionConfig({
3856
4362
  sessionsDir: options.sessionsDir,
@@ -3868,9 +4374,10 @@ async function handoffCommand(options) {
3868
4374
  if (header.next_step.length === 0) {
3869
4375
  throw new SessionInvalidNextStepError();
3870
4376
  }
3871
- const gitRef = await resolveSessionGitRef(options.cwd, options.deps);
4377
+ const gateRef = await resolveSessionGitRef(options.cwd, options.deps);
4378
+ const gitRef = await resolveRecordedGitRef(header.git_ref, gateRef, options.cwd, options.deps);
3872
4379
  const sessionId = generateSessionId();
3873
- const agentSessionId = process.env.CLAUDE_SESSION_ID ?? process.env.CODEX_THREAD_ID;
4380
+ const agentSessionId = resolveAgentSessionId(options.env ?? process.env);
3874
4381
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
3875
4382
  const frontMatterObject = {
3876
4383
  [SESSION_FRONT_MATTER.PRIORITY]: header.priority,
@@ -3887,8 +4394,8 @@ async function handoffCommand(options) {
3887
4394
  const yaml = stringifyYaml3(frontMatterObject, { defaultStringType: "QUOTE_DOUBLE" }).trimEnd();
3888
4395
  const fullContent = `${SESSION_FRONT_MATTER_OPEN}${yaml}${SESSION_FRONT_MATTER_CLOSE}${body}`;
3889
4396
  const filename = `${sessionId}.md`;
3890
- const sessionPath = join6(config.todoDir, filename);
3891
- const absolutePath = resolve4(sessionPath);
4397
+ const sessionPath = join9(config.todoDir, filename);
4398
+ const absolutePath = resolve3(sessionPath);
3892
4399
  await mkdir2(config.todoDir, { recursive: true });
3893
4400
  await writeFile(sessionPath, fullContent, "utf-8");
3894
4401
  const output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
@@ -3898,7 +4405,7 @@ async function handoffCommand(options) {
3898
4405
 
3899
4406
  // src/commands/session/list.ts
3900
4407
  import { readdir, readFile as readFile3 } from "fs/promises";
3901
- import { join as join7 } from "path";
4408
+ import { join as join10 } from "path";
3902
4409
  var SESSION_LIST_FORMAT = {
3903
4410
  TEXT: "text",
3904
4411
  JSON: "json"
@@ -3912,7 +4419,7 @@ async function loadSessionsFromDir(dir, status) {
3912
4419
  for (const file of files) {
3913
4420
  if (!file.endsWith(".md")) continue;
3914
4421
  const id = file.replace(".md", "");
3915
- const filePath = join7(dir, file);
4422
+ const filePath = join10(dir, file);
3916
4423
  const content = await readFile3(filePath, "utf-8");
3917
4424
  const metadata = parseSessionMetadata(content);
3918
4425
  sessions.push({
@@ -3954,6 +4461,7 @@ function validateStatus(input) {
3954
4461
  );
3955
4462
  }
3956
4463
  async function listCommand(options) {
4464
+ const fieldSelection = options.fields !== void 0 ? parseFieldSelection(options.fields) : void 0;
3957
4465
  const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
3958
4466
  const statuses = options.status !== void 0 ? [validateStatus(options.status)] : DEFAULT_LIST_STATUSES;
3959
4467
  const sessionsByStatus = {};
@@ -3962,8 +4470,16 @@ async function listCommand(options) {
3962
4470
  const sessions = await loadSessionsFromDir(config[dirKey], status);
3963
4471
  sessionsByStatus[status] = sortSessions(sessions);
3964
4472
  }
3965
- if (options.format === SESSION_LIST_FORMAT.JSON) {
3966
- return JSON.stringify(sessionsByStatus, null, 2);
4473
+ const emitJson = fieldSelection !== void 0 || options.format === SESSION_LIST_FORMAT.JSON;
4474
+ if (emitJson) {
4475
+ const recordsByStatus = {};
4476
+ for (const status of statuses) {
4477
+ recordsByStatus[status] = (sessionsByStatus[status] ?? []).map((session) => {
4478
+ const record = toSessionRecord(session);
4479
+ return fieldSelection !== void 0 ? projectSessionRecord(record, fieldSelection) : record;
4480
+ });
4481
+ }
4482
+ return JSON.stringify(recordsByStatus, null, 2);
3967
4483
  }
3968
4484
  const lines = [];
3969
4485
  for (const status of statuses) {
@@ -3976,7 +4492,7 @@ async function listCommand(options) {
3976
4492
 
3977
4493
  // src/commands/session/pickup.ts
3978
4494
  import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rename as rename2 } from "fs/promises";
3979
- import { join as join8 } from "path";
4495
+ import { join as join11 } from "path";
3980
4496
 
3981
4497
  // src/domains/session/pickup.ts
3982
4498
  function buildClaimPaths(sessionId, config) {
@@ -4021,7 +4537,7 @@ async function loadTodoSessions(config) {
4021
4537
  for (const file of files) {
4022
4538
  if (!file.endsWith(".md")) continue;
4023
4539
  const id = file.replace(".md", "");
4024
- const filePath = join8(config.todoDir, file);
4540
+ const filePath = join11(config.todoDir, file);
4025
4541
  const content = await readFile4(filePath, "utf-8");
4026
4542
  const metadata = parseSessionMetadata(content);
4027
4543
  sessions.push({
@@ -4077,7 +4593,7 @@ async function pickupCommand(options) {
4077
4593
 
4078
4594
  // src/commands/session/prune.ts
4079
4595
  import { readdir as readdir3, readFile as readFile5, unlink as unlink2 } from "fs/promises";
4080
- import { join as join9 } from "path";
4596
+ import { join as join12 } from "path";
4081
4597
 
4082
4598
  // src/domains/session/prune.ts
4083
4599
  var DEFAULT_KEEP_COUNT = 5;
@@ -4126,7 +4642,7 @@ async function loadArchiveSessions(config) {
4126
4642
  for (const file of files) {
4127
4643
  if (!file.endsWith(".md")) continue;
4128
4644
  const id = file.replace(".md", "");
4129
- const filePath = join9(config.archiveDir, file);
4645
+ const filePath = join12(config.archiveDir, file);
4130
4646
  const content = await readFile5(filePath, "utf-8");
4131
4647
  const metadata = parseSessionMetadata(content);
4132
4648
  sessions.push({
@@ -4316,9 +4832,9 @@ Prefilled by the CLI:
4316
4832
  created_at, git_ref, and agent_session_id when an agent session ID is
4317
4833
  available.
4318
4834
 
4319
- git_ref records the branch name (root worktree on a branch), the HEAD SHA
4320
- (detached), or the origin/<default> tip SHA (clean detached linked worktree).
4321
- Handoff is refused from any other linked-worktree state.
4835
+ git_ref records the branch name (main checkout on a branch), the HEAD SHA
4836
+ (detached), or the origin/<default> tip SHA (clean detached non-main checkout).
4837
+ Handoff is refused from any other non-main checkout state.
4322
4838
 
4323
4839
  Output Tags (for automation):
4324
4840
  <HANDOFF_ID>session-id</HANDOFF_ID> Session identifier
@@ -4349,17 +4865,17 @@ async function readStdin() {
4349
4865
  if (process.stdin.isTTY) {
4350
4866
  return void 0;
4351
4867
  }
4352
- return new Promise((resolve6) => {
4868
+ return new Promise((resolve5) => {
4353
4869
  let data = "";
4354
4870
  process.stdin.setEncoding("utf-8");
4355
4871
  process.stdin.on("data", (chunk) => {
4356
4872
  data += chunk;
4357
4873
  });
4358
4874
  process.stdin.on("end", () => {
4359
- resolve6(data.length === 0 ? void 0 : data);
4875
+ resolve5(data.length === 0 ? void 0 : data);
4360
4876
  });
4361
4877
  process.stdin.on("error", () => {
4362
- resolve6(void 0);
4878
+ resolve5(void 0);
4363
4879
  });
4364
4880
  });
4365
4881
  }
@@ -4368,11 +4884,12 @@ function handleError(error) {
4368
4884
  process.exit(1);
4369
4885
  }
4370
4886
  function registerSessionCommands(sessionCmd) {
4371
- sessionCmd.command("list").description("List active sessions (doing + todo by default)").option("--status <status>", "Filter by status (todo|doing|archive); defaults to doing + todo").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
4887
+ sessionCmd.command("list").description("List active sessions (doing + todo by default)").option("--status <status>", "Filter by status (todo|doing|archive); defaults to doing + todo").option("--json", "Output as JSON").option("--fields <fields>", "Comma-separated fields to emit as JSON (implies --json)").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
4372
4888
  try {
4373
4889
  const output = await listCommand({
4374
4890
  status: options.status,
4375
- format: options.json ? "json" : "text",
4891
+ format: options.json ? SESSION_LIST_FORMAT.JSON : SESSION_LIST_FORMAT.TEXT,
4892
+ fields: options.fields,
4376
4893
  sessionsDir: options.sessionsDir,
4377
4894
  onWarning: writeWarning
4378
4895
  });
@@ -4381,11 +4898,12 @@ function registerSessionCommands(sessionCmd) {
4381
4898
  handleError(error);
4382
4899
  }
4383
4900
  });
4384
- sessionCmd.command("todo").description("List todo sessions").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
4901
+ sessionCmd.command("todo").description("List todo sessions").option("--json", "Output as JSON").option("--fields <fields>", "Comma-separated fields to emit as JSON (implies --json)").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
4385
4902
  try {
4386
4903
  const output = await listCommand({
4387
4904
  status: SESSION_STATUSES[0],
4388
- format: options.json ? "json" : "text",
4905
+ format: options.json ? SESSION_LIST_FORMAT.JSON : SESSION_LIST_FORMAT.TEXT,
4906
+ fields: options.fields,
4389
4907
  sessionsDir: options.sessionsDir,
4390
4908
  onWarning: writeWarning
4391
4909
  });
@@ -4440,7 +4958,8 @@ function registerSessionCommands(sessionCmd) {
4440
4958
  const content = await readStdin();
4441
4959
  const result = await handoffCommand({
4442
4960
  content,
4443
- sessionsDir: options.sessionsDir
4961
+ sessionsDir: options.sessionsDir,
4962
+ env: process.env
4444
4963
  });
4445
4964
  console.log(result.output);
4446
4965
  } catch (error) {
@@ -4513,7 +5032,7 @@ var sessionDomain = {
4513
5032
 
4514
5033
  // src/lib/spec-tree/index.ts
4515
5034
  import { readdir as readdir5, readFile as readFile7 } from "fs/promises";
4516
- import { join as join10 } from "path";
5035
+ import { join as join13 } from "path";
4517
5036
  var SPEC_TREE_FIELD_KEY = {
4518
5037
  VERSION: "version",
4519
5038
  PRODUCT: "product",
@@ -4531,7 +5050,9 @@ var SPEC_TREE_ENTRY_TYPE = {
4531
5050
  PRODUCT: SPEC_TREE_FIELD_KEY.PRODUCT,
4532
5051
  NODE: SPEC_TREE_KIND_CATEGORY.NODE,
4533
5052
  DECISION: SPEC_TREE_KIND_CATEGORY.DECISION,
4534
- EVIDENCE: "evidence"
5053
+ EVIDENCE: "evidence",
5054
+ SUPERSEDED: "superseded",
5055
+ INVALID: "invalid"
4535
5056
  };
4536
5057
  var SPEC_TREE_FILESYSTEM_RECORD_TYPE = {
4537
5058
  DIRECTORY: "directory",
@@ -4542,17 +5063,6 @@ var SPEC_TREE_EVIDENCE_STATUS = {
4542
5063
  FAILING: SPEC_TREE_NODE_STATE.FAILING,
4543
5064
  PASSING: SPEC_TREE_NODE_STATE.PASSING
4544
5065
  };
4545
- var SPEC_TREE_EVIDENCE_FILE = {
4546
- DIRECTORY_NAME: "tests",
4547
- MODES: ["scenario", "mapping", "conformance", "property", "compliance"],
4548
- LEVELS: ["l1", "l2", "l3"],
4549
- TAILS: {
4550
- TYPESCRIPT: ["test", "ts"],
4551
- PYTHON: ["py"],
4552
- RUST: ["rs"]
4553
- },
4554
- SEGMENT_SEPARATOR: "."
4555
- };
4556
5066
  var SPEC_TREE_EVIDENCE_FILE_TAILS = Object.values(SPEC_TREE_EVIDENCE_FILE.TAILS);
4557
5067
  var SPEC_TREE_PROJECTION = {
4558
5068
  VERSION: 1,
@@ -4586,30 +5096,33 @@ var SPEC_TREE_NODE_RELATION_KEYS = {
4586
5096
  DECISIONS: SPEC_TREE_FIELD_KEY.DECISIONS
4587
5097
  };
4588
5098
  var ORDER_COMPARISON_EQUAL = 0;
4589
- var SPEC_TREE_PATH_SEPARATOR = "/";
4590
- var SPEC_TREE_ORDER_SEPARATOR = "-";
5099
+ var SPEC_TREE_PATH_SEPARATOR = SPEC_TREE_GRAMMAR.PATH_SEPARATOR;
5100
+ var SPEC_TREE_ORDER_SEPARATOR = SPEC_TREE_GRAMMAR.ORDER.SEPARATOR;
4591
5101
  var SPEC_TREE_ORDER_RADIX = 10;
4592
5102
  var SPEC_TREE_TEXT_ENCODING = "utf8";
4593
5103
  var SPEC_TREE_EMPTY_RELATIVE_PATH = "";
4594
- var SPEC_TREE_ORDER_PATTERN = /^\d+$/;
5104
+ var SPEC_TREE_ORDER_PATTERN = SPEC_TREE_GRAMMAR.ORDER.PATTERN;
4595
5105
  var SPEC_TREE_MIN_EVIDENCE_PATH_SEGMENTS = 2;
4596
5106
  var SPEC_TREE_PARENT_SEGMENT_OFFSET = 2;
4597
5107
  var SPEC_TREE_FIRST_EVIDENCE_MARKER_INDEX = 1;
4598
5108
  var SPEC_TREE_EXACTLY_ONE_EVIDENCE_MARKER = 1;
4599
5109
  function createFilesystemSpecTreeSource(options) {
4600
5110
  const registry = options.registry ?? KIND_REGISTRY;
5111
+ const schemaVersions = options.schemaVersions ?? SPEC_TREE_NAMING_SCHEMA_VERSIONS;
4601
5112
  const includePath = options.includePath ?? includeEverySpecTreePath;
4602
5113
  return {
4603
- entries: () => readFilesystemSourceEntries(options.productDir, registry, includePath),
5114
+ entries: () => readFilesystemSourceEntries(options.productDir, registry, schemaVersions, includePath),
4604
5115
  async readText(ref) {
4605
5116
  if (ref.path === void 0) {
4606
5117
  throw new Error("Filesystem source refs require a path");
4607
5118
  }
4608
- return readFile7(join10(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
5119
+ return readFile7(join13(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
4609
5120
  }
4610
5121
  };
4611
5122
  }
4612
- function recognizeSpecTreeFilesystemEntry(record, registry = KIND_REGISTRY) {
5123
+ function recognizeSpecTreeFilesystemEntry(record, options = {}) {
5124
+ const registry = options.registry ?? KIND_REGISTRY;
5125
+ const schemaVersions = options.schemaVersions ?? SPEC_TREE_NAMING_SCHEMA_VERSIONS;
4613
5126
  const name = readLastPathSegment(record.relativePath);
4614
5127
  if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE && isProductFile(record.relativePath)) {
4615
5128
  return {
@@ -4620,49 +5133,107 @@ function recognizeSpecTreeFilesystemEntry(record, registry = KIND_REGISTRY) {
4620
5133
  };
4621
5134
  }
4622
5135
  if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.DIRECTORY) {
4623
- const nodeMatch = matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.NODE);
4624
- if (nodeMatch === null) return null;
4625
- const parsed = parseOrderedSlug(stripSuffix(name, nodeMatch.definition.suffix));
5136
+ return recognizeDirectoryRecord(record, name, registry, schemaVersions);
5137
+ }
5138
+ if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE && record.parentId !== void 0 && isEvidenceFile(record.relativePath)) {
5139
+ return {
5140
+ type: SPEC_TREE_ENTRY_TYPE.EVIDENCE,
5141
+ id: record.relativePath,
5142
+ parentId: record.parentId,
5143
+ status: SPEC_TREE_EVIDENCE_STATUS.LINKED,
5144
+ ref: sourceRefForRelativePath(record.relativePath)
5145
+ };
5146
+ }
5147
+ if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE) {
5148
+ const decisionMatch = matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.DECISION);
5149
+ if (decisionMatch === null) return null;
5150
+ const parsed = parseOrderedSlug(stripSuffix(name, decisionMatch.definition.suffix));
4626
5151
  if (parsed === null) return null;
4627
5152
  return {
4628
- type: SPEC_TREE_ENTRY_TYPE.NODE,
4629
- kind: nodeMatch.kind,
5153
+ type: SPEC_TREE_ENTRY_TYPE.DECISION,
5154
+ kind: decisionMatch.kind,
4630
5155
  id: record.relativePath,
4631
5156
  order: parsed.order,
4632
5157
  slug: parsed.slug,
4633
5158
  parentId: record.parentId,
4634
- ref: sourceRefForNode(record.relativePath, parsed.slug)
5159
+ ref: sourceRefForRelativePath(record.relativePath)
4635
5160
  };
4636
5161
  }
4637
- if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE && record.parentId !== void 0 && isEvidenceFile(record.relativePath)) {
5162
+ return null;
5163
+ }
5164
+ function recognizeDirectoryRecord(record, name, registry, schemaVersions) {
5165
+ const canonical = canonicalNamingSchemaVersion(schemaVersions);
5166
+ const canonicalMatch = matchNodeSuffixFromVersion(name, canonical);
5167
+ if (canonicalMatch !== null) {
5168
+ const kind = nodeKindForSuffix(canonicalMatch.suffix, registry);
5169
+ if (kind !== null) {
5170
+ return {
5171
+ type: SPEC_TREE_ENTRY_TYPE.NODE,
5172
+ kind,
5173
+ id: record.relativePath,
5174
+ order: canonicalMatch.parsed.order,
5175
+ slug: canonicalMatch.parsed.slug,
5176
+ parentId: record.parentId,
5177
+ ref: sourceRefForNode(record.relativePath, canonicalMatch.parsed.slug)
5178
+ };
5179
+ }
5180
+ }
5181
+ const supersededVersion = matchSupersededNodeVersion(name, schemaVersions, canonical);
5182
+ if (supersededVersion !== null) {
4638
5183
  return {
4639
- type: SPEC_TREE_ENTRY_TYPE.EVIDENCE,
5184
+ type: SPEC_TREE_ENTRY_TYPE.SUPERSEDED,
4640
5185
  id: record.relativePath,
5186
+ version: supersededVersion,
4641
5187
  parentId: record.parentId,
4642
- status: SPEC_TREE_EVIDENCE_STATUS.LINKED,
4643
5188
  ref: sourceRefForRelativePath(record.relativePath)
4644
5189
  };
4645
5190
  }
4646
- if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE) {
4647
- const decisionMatch = matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.DECISION);
4648
- if (decisionMatch === null) return null;
4649
- const parsed = parseOrderedSlug(stripSuffix(name, decisionMatch.definition.suffix));
4650
- if (parsed === null) return null;
5191
+ if (parseOrderedSlug(name) !== null) {
4651
5192
  return {
4652
- type: SPEC_TREE_ENTRY_TYPE.DECISION,
4653
- kind: decisionMatch.kind,
5193
+ type: SPEC_TREE_ENTRY_TYPE.INVALID,
4654
5194
  id: record.relativePath,
4655
- order: parsed.order,
4656
- slug: parsed.slug,
4657
5195
  parentId: record.parentId,
4658
5196
  ref: sourceRefForRelativePath(record.relativePath)
4659
5197
  };
4660
5198
  }
4661
5199
  return null;
4662
5200
  }
5201
+ function matchNodeSuffixFromVersion(name, version2) {
5202
+ for (const suffix of version2.nodeSuffixes) {
5203
+ if (!name.endsWith(suffix)) continue;
5204
+ const parsed = parseOrderedSlug(stripSuffix(name, suffix));
5205
+ if (parsed !== null) {
5206
+ return { suffix, parsed };
5207
+ }
5208
+ }
5209
+ return null;
5210
+ }
5211
+ function nodeKindForSuffix(suffix, registry) {
5212
+ for (const [kind, definition] of Object.entries(registry)) {
5213
+ if (definition.category === SPEC_TREE_KIND_CATEGORY.NODE && definition.suffix === suffix) {
5214
+ return kind;
5215
+ }
5216
+ }
5217
+ return null;
5218
+ }
5219
+ function matchSupersededNodeVersion(name, schemaVersions, canonical) {
5220
+ const canonicalSuffixes = new Set(canonical.nodeSuffixes);
5221
+ const priorVersions = schemaVersions.filter((version2) => version2 !== canonical).sort((left, right) => compareNamingSchemaVersions(right, left));
5222
+ for (const version2 of priorVersions) {
5223
+ for (const suffix of version2.nodeSuffixes) {
5224
+ if (canonicalSuffixes.has(suffix)) continue;
5225
+ if (name.endsWith(suffix) && parseOrderedSlug(stripSuffix(name, suffix)) !== null) {
5226
+ return version2.version;
5227
+ }
5228
+ }
5229
+ }
5230
+ return null;
5231
+ }
4663
5232
  async function readSpecTree(options) {
4664
5233
  const entries = await collectSourceEntries(options.source);
4665
5234
  const product = entries.find(isProductEntry) ?? null;
5235
+ const superseded = entries.filter(isSupersededEntry);
5236
+ const residual = entries.filter(isInvalidEntry);
4666
5237
  const evidenceByParent = groupEvidence(entries.filter(isEvidenceEntry));
4667
5238
  const decisions = entries.filter(isDecisionEntry).map(toDecision).sort(compareOrderedEntries);
4668
5239
  const decisionsByParent = groupDecisions(decisions);
@@ -4702,6 +5273,8 @@ async function readSpecTree(options) {
4702
5273
  nodes: roots,
4703
5274
  allNodes,
4704
5275
  decisions,
5276
+ superseded,
5277
+ residual,
4705
5278
  entries
4706
5279
  };
4707
5280
  }
@@ -4743,6 +5316,12 @@ function isDecisionEntry(entry) {
4743
5316
  function isEvidenceEntry(entry) {
4744
5317
  return entry.type === SPEC_TREE_ENTRY_TYPE.EVIDENCE;
4745
5318
  }
5319
+ function isSupersededEntry(entry) {
5320
+ return entry.type === SPEC_TREE_ENTRY_TYPE.SUPERSEDED;
5321
+ }
5322
+ function isInvalidEntry(entry) {
5323
+ return entry.type === SPEC_TREE_ENTRY_TYPE.INVALID;
5324
+ }
4746
5325
  function toDecision(entry) {
4747
5326
  return {
4748
5327
  id: entry.id,
@@ -4817,11 +5396,12 @@ function compareOrderedEntries(left, right) {
4817
5396
  if (orderComparison !== ORDER_COMPARISON_EQUAL) return orderComparison;
4818
5397
  return left.id.localeCompare(right.id);
4819
5398
  }
4820
- async function* readFilesystemSourceEntries(productDir, registry, includePath) {
5399
+ async function* readFilesystemSourceEntries(productDir, registry, schemaVersions, includePath) {
4821
5400
  yield* walkFilesystemDirectory({
4822
- absolutePath: join10(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY),
5401
+ absolutePath: join13(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY),
4823
5402
  relativePath: SPEC_TREE_EMPTY_RELATIVE_PATH,
4824
5403
  registry,
5404
+ schemaVersions,
4825
5405
  includePath
4826
5406
  });
4827
5407
  }
@@ -4842,14 +5422,15 @@ async function* walkFilesystemDirectory(context) {
4842
5422
  if (recordType === void 0) continue;
4843
5423
  const sourceEntry = recognizeSpecTreeFilesystemEntry(
4844
5424
  { type: recordType, relativePath, parentId: context.parentId },
4845
- context.registry
5425
+ { registry: context.registry, schemaVersions: context.schemaVersions }
4846
5426
  );
4847
5427
  if (sourceEntry !== null) yield sourceEntry;
4848
- if (entry.isDirectory() && shouldDescendIntoDirectory(entry.name, sourceEntry, context.registry)) {
5428
+ if (entry.isDirectory() && shouldDescendIntoDirectory(sourceEntry)) {
4849
5429
  yield* walkFilesystemDirectory({
4850
- absolutePath: join10(context.absolutePath, entry.name),
5430
+ absolutePath: join13(context.absolutePath, entry.name),
4851
5431
  relativePath,
4852
5432
  registry: context.registry,
5433
+ schemaVersions: context.schemaVersions,
4853
5434
  includePath: context.includePath,
4854
5435
  parentId: sourceEntry?.type === SPEC_TREE_ENTRY_TYPE.NODE ? sourceEntry.id : context.parentId
4855
5436
  });
@@ -4907,14 +5488,8 @@ function segmentsEndWith(segments, suffix) {
4907
5488
  const start = segments.length - suffix.length;
4908
5489
  return suffix.every((value, index) => segments[start + index] === value);
4909
5490
  }
4910
- function shouldDescendIntoDirectory(name, sourceEntry, registry) {
4911
- if (sourceEntry !== null) return true;
4912
- if (name === SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME) return true;
4913
- return !isUnregisteredOrderedDirectory(name, registry);
4914
- }
4915
- function isUnregisteredOrderedDirectory(name, registry) {
4916
- if (matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.NODE) !== null) return false;
4917
- return parseOrderedSlug(name) !== null;
5491
+ function shouldDescendIntoDirectory(sourceEntry) {
5492
+ return sourceEntry === null || sourceEntry.type === SPEC_TREE_ENTRY_TYPE.NODE;
4918
5493
  }
4919
5494
  function sourceRefForRelativePath(relativePath) {
4920
5495
  const path6 = joinSpecTreePath(SPEC_TREE_CONFIG.ROOT_DIRECTORY, relativePath);
@@ -4998,14 +5573,14 @@ function formatNextNode(node) {
4998
5573
 
4999
5574
  // src/commands/testing/discovery.ts
5000
5575
  import { readdir as readdir6 } from "fs/promises";
5001
- import { join as join11, relative as relative3, sep } from "path";
5576
+ import { join as join14, relative, sep } from "path";
5002
5577
  var TESTS_DIRECTORY_NAME = SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME;
5003
5578
  var SPEC_ROOT_DIRECTORY = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
5004
5579
  var POSIX_SEPARATOR = "/";
5005
- var ERROR_CODE_NOT_FOUND = "ENOENT";
5580
+ var ERROR_CODE_NOT_FOUND2 = "ENOENT";
5006
5581
  async function discoverTestFiles(productDir) {
5007
5582
  const found = [];
5008
- await collectTestFiles(join11(productDir, SPEC_ROOT_DIRECTORY), false, found);
5583
+ await collectTestFiles(join14(productDir, SPEC_ROOT_DIRECTORY), false, found);
5009
5584
  return found.map((absolute) => toPosixRelative(productDir, absolute)).sort(compareAscii);
5010
5585
  }
5011
5586
  async function collectTestFiles(directory, insideTestsDir, found) {
@@ -5013,11 +5588,11 @@ async function collectTestFiles(directory, insideTestsDir, found) {
5013
5588
  try {
5014
5589
  entries = await readdir6(directory, { withFileTypes: true });
5015
5590
  } catch (error) {
5016
- if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return;
5591
+ if (hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) return;
5017
5592
  throw error;
5018
5593
  }
5019
5594
  for (const entry of entries) {
5020
- const childPath = join11(directory, entry.name);
5595
+ const childPath = join14(directory, entry.name);
5021
5596
  if (entry.isDirectory()) {
5022
5597
  await collectTestFiles(childPath, insideTestsDir || entry.name === TESTS_DIRECTORY_NAME, found);
5023
5598
  } else if (insideTestsDir && entry.isFile()) {
@@ -5026,14 +5601,14 @@ async function collectTestFiles(directory, insideTestsDir, found) {
5026
5601
  }
5027
5602
  }
5028
5603
  function toPosixRelative(productDir, absolute) {
5029
- return relative3(productDir, absolute).split(sep).join(POSIX_SEPARATOR);
5604
+ return relative(productDir, absolute).split(sep).join(POSIX_SEPARATOR);
5030
5605
  }
5031
5606
  function compareAscii(left, right) {
5032
5607
  if (left < right) return -1;
5033
5608
  if (left > right) return 1;
5034
5609
  return 0;
5035
5610
  }
5036
- function hasErrorCode(error, code) {
5611
+ function hasErrorCode2(error, code) {
5037
5612
  return typeof error === "object" && error !== null && error.code === code;
5038
5613
  }
5039
5614
 
@@ -5041,7 +5616,7 @@ function hasErrorCode(error, code) {
5041
5616
  var SUCCESS_EXIT_CODE = 0;
5042
5617
  function aggregateTestExitCode(invocations) {
5043
5618
  for (const invocation of invocations) {
5044
- if (invocation.invoked && invocation.exitCode !== void 0 && invocation.exitCode !== SUCCESS_EXIT_CODE) {
5619
+ if (invocation.invoked && invocation.exitCode !== SUCCESS_EXIT_CODE) {
5045
5620
  return invocation.exitCode;
5046
5621
  }
5047
5622
  }
@@ -5091,7 +5666,7 @@ async function runTests(options, deps) {
5091
5666
  outcomes.push({
5092
5667
  runnerId: group.language.name,
5093
5668
  testPaths: group.testPaths,
5094
- exitCode: invocation.exitCode ?? SUCCESS_EXIT_CODE
5669
+ exitCode: invocation.exitCode
5095
5670
  });
5096
5671
  }
5097
5672
  }
@@ -5099,55 +5674,11 @@ async function runTests(options, deps) {
5099
5674
  }
5100
5675
 
5101
5676
  // src/commands/testing/run-command.ts
5102
- import { join as join14 } from "path";
5103
-
5104
- // src/testing/run-state.ts
5105
- import { createHash as createHash3, randomBytes as nodeRandomBytes2 } from "crypto";
5106
- import {
5107
- mkdir as nodeMkdir2,
5108
- readdir as nodeReaddir2,
5109
- readFile as nodeReadFile2,
5110
- rename as nodeRename2,
5111
- writeFile as nodeWriteFile2
5112
- } from "fs/promises";
5113
- import { join as join13 } from "path";
5114
-
5115
- // src/domains/audit/run-state.ts
5116
- import { createHash as createHash2, randomBytes as nodeRandomBytes } from "crypto";
5117
- import {
5118
- mkdir as nodeMkdir,
5119
- readdir as nodeReaddir,
5120
- readFile as nodeReadFile,
5121
- rename as nodeRename,
5122
- writeFile as nodeWriteFile
5123
- } from "fs/promises";
5124
- import { join as join12 } from "path";
5125
- var AUDIT_RUN_STATE_STATUS = {
5126
- APPROVED: "approved",
5127
- REJECTED: "rejected",
5128
- FAILED: "failed",
5129
- INTERRUPTED: "interrupted"
5130
- };
5131
- var AUDIT_RUN_STATE_DISPLAY = {
5132
- [AUDIT_RUN_STATE_STATUS.APPROVED]: "APPROVED",
5133
- [AUDIT_RUN_STATE_STATUS.REJECTED]: AUDIT_VERDICT_VALUE.REJECT,
5134
- [AUDIT_RUN_STATE_STATUS.FAILED]: "FAILED",
5135
- [AUDIT_RUN_STATE_STATUS.INTERRUPTED]: "INTERRUPTED"
5136
- };
5137
- var AUDIT_RUN_TIMESTAMP_SEPARATOR = "_";
5138
- var AUDIT_RUN_TIMESTAMP_MILLISECOND_DIGITS = 3;
5139
- function formatAuditRunTimestamp(date) {
5140
- const year = date.getUTCFullYear();
5141
- const month = String(date.getUTCMonth() + 1).padStart(2, "0");
5142
- const day = String(date.getUTCDate()).padStart(2, "0");
5143
- const hours = String(date.getUTCHours()).padStart(2, "0");
5144
- const minutes = String(date.getUTCMinutes()).padStart(2, "0");
5145
- const seconds = String(date.getUTCSeconds()).padStart(2, "0");
5146
- const milliseconds = String(date.getUTCMilliseconds()).padStart(AUDIT_RUN_TIMESTAMP_MILLISECOND_DIGITS, "0");
5147
- return `${year}-${month}-${day}${AUDIT_RUN_TIMESTAMP_SEPARATOR}${hours}-${minutes}-${seconds}-${milliseconds}`;
5148
- }
5677
+ import { join as join16 } from "path";
5149
5678
 
5150
5679
  // src/testing/run-state.ts
5680
+ import { createHash as createHash4 } from "crypto";
5681
+ import { join as join15 } from "path";
5151
5682
  var TEST_RUN_STATE_STATUS = {
5152
5683
  PASSED: "passed",
5153
5684
  FAILED: "failed",
@@ -5160,11 +5691,14 @@ var TESTING_RUN_STATE_INCOMPLETE_REASON = {
5160
5691
  SHAPE_INVALID_STATE: "shape-invalid-state"
5161
5692
  };
5162
5693
  var TESTING_RUN_STATE_ERROR = {
5163
- RUN_DIRECTORY_COLLISION_LIMIT: "testing run directory collision limit exhausted",
5164
- RUN_DIRECTORY_CREATE_FAILED: "testing run directory create failed",
5694
+ RUN_FILE_COLLISION_LIMIT: "testing run file collision limit exhausted",
5695
+ RUN_FILE_CREATE_FAILED: "testing run file create failed",
5165
5696
  STATE_ALREADY_EXISTS: "testing run state already exists",
5166
5697
  STATE_WRITE_FAILED: "testing run state write failed"
5167
5698
  };
5699
+ var TESTING_RUN_STATE_ERROR_CODE = {
5700
+ NOT_FOUND: "ENOENT"
5701
+ };
5168
5702
  var TEST_RUN_STATE_FIELDS = {
5169
5703
  BRANCH_NAME: "branchName",
5170
5704
  HEAD_SHA: "headSha",
@@ -5186,119 +5720,64 @@ var PRODUCT_INPUT_DIGEST_FIELDS = {
5186
5720
  DESCRIPTOR_ID: "descriptorId",
5187
5721
  DIGEST: "digest"
5188
5722
  };
5189
- var DEFAULT_TESTING_STORAGE = {
5190
- spxDir: ".spx",
5191
- localDir: "local",
5192
- testingDir: "testing",
5193
- runsDir: "runs",
5194
- stateFile: "state.json"
5195
- };
5196
5723
  var SHA256_ALGORITHM2 = "sha256";
5197
- var HEX_ENCODING2 = "hex";
5198
- var UTF8_ENCODING2 = "utf8";
5199
- var RUN_ID_BYTES = 6;
5200
- var TEMP_STATE_ID_BYTES = 6;
5201
- var RUN_DIRECTORY_CREATE_ATTEMPTS = 10;
5202
- var TEMP_STATE_FILE_PREFIX = ".state";
5203
- var TEMP_STATE_FILE_SUFFIX = ".tmp";
5204
- var JSON_INDENT_SPACES = 2;
5724
+ var HEX_ENCODING3 = "hex";
5205
5725
  var SEGMENT_SEPARATOR = "-";
5206
- var EXCLUSIVE_CREATE_FLAG = "wx";
5207
- var ERROR_CODE_FILE_EXISTS = "EEXIST";
5208
- var ERROR_CODE_NOT_FOUND2 = "ENOENT";
5209
- var RUN_DIRECTORY_NAME_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[a-f0-9]{12}$/;
5210
- var defaultFileSystem = {
5211
- mkdir: async (path6, options) => {
5212
- await nodeMkdir2(path6, options);
5213
- },
5214
- writeFile: nodeWriteFile2,
5215
- rename: nodeRename2,
5216
- readFile: nodeReadFile2,
5217
- readdir: nodeReaddir2
5218
- };
5726
+ var defaultFileSystem2 = defaultFileSystem;
5219
5727
  function formatTestRunTimestamp(date) {
5220
- return formatAuditRunTimestamp(date);
5728
+ return formatRunTimestamp(date);
5221
5729
  }
5222
- function testingRunsDir(productDir, storage = DEFAULT_TESTING_STORAGE) {
5223
- return join13(productDir, storage.spxDir, storage.localDir, storage.testingDir, storage.runsDir);
5730
+ function testingRunsDir(productDir) {
5731
+ const worktreeScope = worktreeScopeDir(productDir);
5732
+ if (!worktreeScope.ok) throw new Error(worktreeScope.error);
5733
+ const result = runsDir(worktreeScope.value, STATE_STORE_DOMAIN.TEST);
5734
+ if (!result.ok) throw new Error(result.error);
5735
+ return result.value;
5224
5736
  }
5225
- async function createTestRunDirectory(productDir, options = {}) {
5226
- const fs7 = options.fs ?? defaultFileSystem;
5227
- const storage = options.storage ?? DEFAULT_TESTING_STORAGE;
5228
- const maxAttempts = options.maxAttempts ?? RUN_DIRECTORY_CREATE_ATTEMPTS;
5229
- const startedDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
5230
- const startedAt = formatTestRunTimestamp(startedDate);
5231
- const runsDir = testingRunsDir(productDir, storage);
5232
- const randomBytes = options.randomBytes ?? nodeRandomBytes2;
5233
- try {
5234
- await fs7.mkdir(runsDir, { recursive: true });
5235
- } catch (error) {
5236
- return { ok: false, error: `${TESTING_RUN_STATE_ERROR.RUN_DIRECTORY_CREATE_FAILED}: ${toErrorMessage(error)}` };
5237
- }
5238
- for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
5239
- const runId = generateHexId(RUN_ID_BYTES, randomBytes);
5240
- const runDirectoryName = `${startedAt}${SEGMENT_SEPARATOR}${runId}`;
5241
- const runDir = join13(runsDir, runDirectoryName);
5242
- try {
5243
- await fs7.mkdir(runDir);
5244
- return { ok: true, value: { runsDir, runDir, runDirectoryName, runId, startedAt } };
5245
- } catch (error) {
5246
- if (hasErrorCode2(error, ERROR_CODE_FILE_EXISTS)) continue;
5247
- return { ok: false, error: `${TESTING_RUN_STATE_ERROR.RUN_DIRECTORY_CREATE_FAILED}: ${toErrorMessage(error)}` };
5248
- }
5249
- }
5250
- return { ok: false, error: TESTING_RUN_STATE_ERROR.RUN_DIRECTORY_COLLISION_LIMIT };
5737
+ async function createTestRunFile(productDir, options = {}) {
5738
+ const worktreeScope = worktreeScopeDir(productDir);
5739
+ if (!worktreeScope.ok) return worktreeScope;
5740
+ const created = await createJsonlRunFile(worktreeScope.value, STATE_STORE_DOMAIN.TEST, options);
5741
+ if (!created.ok) return {
5742
+ ok: false,
5743
+ error: testingRunFileError(created.error)
5744
+ };
5745
+ return { ok: true, value: created.value };
5251
5746
  }
5252
- async function writeTerminalTestRunState(runDir, state, options = {}) {
5253
- const fs7 = options.fs ?? defaultFileSystem;
5254
- const storage = options.storage ?? DEFAULT_TESTING_STORAGE;
5255
- const statePath = join13(runDir, storage.stateFile);
5256
- try {
5257
- await fs7.readFile(statePath, UTF8_ENCODING2);
5747
+ async function writeTerminalTestRunState(runFilePath, state, options = {}) {
5748
+ const written = await writeJsonlRunRecord(runFilePath, testRunStateRecord(state), options);
5749
+ if (written.ok) return written;
5750
+ if (written.error === STATE_STORE_ERROR.RECORD_ALREADY_EXISTS) {
5258
5751
  return { ok: false, error: TESTING_RUN_STATE_ERROR.STATE_ALREADY_EXISTS };
5259
- } catch (error) {
5260
- if (!hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) {
5261
- return { ok: false, error: `${TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED}: ${toErrorMessage(error)}` };
5262
- }
5263
- }
5264
- const tempId = generateHexId(TEMP_STATE_ID_BYTES, options.randomBytes ?? nodeRandomBytes2);
5265
- const tempPath = join13(runDir, `${TEMP_STATE_FILE_PREFIX}-${tempId}${TEMP_STATE_FILE_SUFFIX}`);
5266
- const serialized = `${JSON.stringify(state, null, JSON_INDENT_SPACES)}
5267
- `;
5268
- try {
5269
- await fs7.writeFile(tempPath, serialized, { flag: EXCLUSIVE_CREATE_FLAG });
5270
- await fs7.rename(tempPath, statePath);
5271
- return { ok: true, value: statePath };
5272
- } catch (error) {
5273
- return { ok: false, error: `${TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED}: ${toErrorMessage(error)}` };
5274
5752
  }
5753
+ return {
5754
+ ok: false,
5755
+ error: testingWriteError(written.error)
5756
+ };
5275
5757
  }
5276
5758
  async function readTestingRuns(productDir, options = {}) {
5277
- const fs7 = options.fs ?? defaultFileSystem;
5278
- const storage = options.storage ?? DEFAULT_TESTING_STORAGE;
5279
- const runsDir = testingRunsDir(productDir, storage);
5759
+ const fs7 = options.fs ?? defaultFileSystem2;
5760
+ const runsDir2 = testingRunsDir(productDir);
5280
5761
  let entries;
5281
5762
  try {
5282
- entries = await fs7.readdir(runsDir, { withFileTypes: true });
5763
+ entries = await fs7.readdir(runsDir2, { withFileTypes: true });
5283
5764
  } catch (error) {
5284
- if (hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) {
5765
+ if (hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) {
5285
5766
  return { ok: true, value: { terminalRuns: [], incompleteRuns: [] } };
5286
5767
  }
5287
- return { ok: false, error: toErrorMessage(error) };
5768
+ return { ok: false, error: toErrorMessage2(error) };
5288
5769
  }
5289
5770
  const terminalRuns = [];
5290
5771
  const incompleteRuns = [];
5291
- for (const entry of entries.filter(isTestRunDirectoryEntry)) {
5292
- const runDir = join13(runsDir, entry.name);
5293
- const statePath = join13(runDir, storage.stateFile);
5294
- const stateResult = await readTestRunStatePath(statePath, fs7);
5772
+ for (const entry of entries.filter(isTestRunFileEntry)) {
5773
+ const runFilePath = join15(runsDir2, entry.name);
5774
+ const stateResult = await readTestRunStatePath(runFilePath, fs7);
5295
5775
  if (stateResult.ok) {
5296
- terminalRuns.push({ runDirectoryName: entry.name, runDir, statePath, state: stateResult.value });
5776
+ terminalRuns.push({ runFileName: entry.name, runFilePath, state: stateResult.value });
5297
5777
  } else {
5298
5778
  incompleteRuns.push({
5299
- runDirectoryName: entry.name,
5300
- runDir,
5301
- statePath,
5779
+ runFileName: entry.name,
5780
+ runFilePath,
5302
5781
  reason: stateResult.reason,
5303
5782
  ...stateResult.error === void 0 ? {} : { error: stateResult.error }
5304
5783
  });
@@ -5339,21 +5818,47 @@ function extractStalenessInputs(state) {
5339
5818
  productInputDigests: state.productInputDigests
5340
5819
  };
5341
5820
  }
5342
- async function readTestRunStatePath(statePath, fs7) {
5343
- let raw;
5821
+ function testRunStateRecord(state) {
5822
+ return {
5823
+ branchName: state.branchName,
5824
+ headSha: state.headSha,
5825
+ testingConfigDigest: state.testingConfigDigest,
5826
+ runnerOutcomes: state.runnerOutcomes.map((outcome) => ({
5827
+ runnerId: outcome.runnerId,
5828
+ testPaths: outcome.testPaths,
5829
+ exitCode: outcome.exitCode
5830
+ })),
5831
+ discoveredTestPathsDigest: state.discoveredTestPathsDigest,
5832
+ discoveredTestContentDigest: state.discoveredTestContentDigest,
5833
+ productInputDigests: state.productInputDigests.map((digest) => ({
5834
+ descriptorId: digest.descriptorId,
5835
+ digest: digest.digest
5836
+ })),
5837
+ startedAt: state.startedAt,
5838
+ completedAt: state.completedAt,
5839
+ status: state.status
5840
+ };
5841
+ }
5842
+ async function readTestRunStatePath(runFilePath, fs7) {
5843
+ let content;
5344
5844
  try {
5345
- raw = await fs7.readFile(statePath, UTF8_ENCODING2);
5845
+ content = await fs7.readFile(runFilePath, "utf8");
5346
5846
  } catch (error) {
5347
- if (hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) {
5348
- return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.MISSING_STATE };
5349
- }
5350
- return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.IO_ERROR, error: toErrorMessage(error) };
5847
+ return {
5848
+ ok: false,
5849
+ reason: hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND) ? TESTING_RUN_STATE_INCOMPLETE_REASON.MISSING_STATE : TESTING_RUN_STATE_INCOMPLETE_REASON.IO_ERROR,
5850
+ error: toErrorMessage2(error)
5851
+ };
5852
+ }
5853
+ const latest = latestNonEmptyJsonlLine(content);
5854
+ if (latest === void 0) {
5855
+ return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE };
5351
5856
  }
5352
5857
  let parsed;
5353
5858
  try {
5354
- parsed = JSON.parse(raw);
5355
- } catch (error) {
5356
- return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE, error: toErrorMessage(error) };
5859
+ parsed = JSON.parse(latest);
5860
+ } catch {
5861
+ return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE };
5357
5862
  }
5358
5863
  const validated = validateTestRunState(parsed);
5359
5864
  if (!validated.ok) {
@@ -5375,8 +5880,8 @@ function validateTestRunState(value) {
5375
5880
  if (!discoveredTestPathsDigest.ok) return discoveredTestPathsDigest;
5376
5881
  const discoveredTestContentDigest = readString(value, TEST_RUN_STATE_FIELDS.DISCOVERED_TEST_CONTENT_DIGEST);
5377
5882
  if (!discoveredTestContentDigest.ok) return discoveredTestContentDigest;
5378
- const productInputDigests = readProductInputDigests(value[TEST_RUN_STATE_FIELDS.PRODUCT_INPUT_DIGESTS]);
5379
- if (!productInputDigests.ok) return productInputDigests;
5883
+ const productInputDigests2 = readProductInputDigests(value[TEST_RUN_STATE_FIELDS.PRODUCT_INPUT_DIGESTS]);
5884
+ if (!productInputDigests2.ok) return productInputDigests2;
5380
5885
  const startedAt = readString(value, TEST_RUN_STATE_FIELDS.STARTED_AT);
5381
5886
  if (!startedAt.ok) return startedAt;
5382
5887
  const completedAt = readString(value, TEST_RUN_STATE_FIELDS.COMPLETED_AT);
@@ -5392,7 +5897,7 @@ function validateTestRunState(value) {
5392
5897
  runnerOutcomes: runnerOutcomes.value,
5393
5898
  discoveredTestPathsDigest: discoveredTestPathsDigest.value,
5394
5899
  discoveredTestContentDigest: discoveredTestContentDigest.value,
5395
- productInputDigests: productInputDigests.value,
5900
+ productInputDigests: productInputDigests2.value,
5396
5901
  startedAt: startedAt.value,
5397
5902
  completedAt: completedAt.value,
5398
5903
  status: status.value
@@ -5456,7 +5961,7 @@ function compareTerminalRuns(left, right) {
5456
5961
  if (completed !== 0) return completed;
5457
5962
  const started = compareAsciiStrings(left.state.startedAt, right.state.startedAt);
5458
5963
  if (started !== 0) return started;
5459
- return compareAsciiStrings(left.runDirectoryName, right.runDirectoryName);
5964
+ return compareAsciiStrings(left.runFileName, right.runFileName);
5460
5965
  }
5461
5966
  function productInputDigestsEqual(left, right) {
5462
5967
  return canonicalProductInputDigests(left) === canonicalProductInputDigests(right);
@@ -5465,37 +5970,50 @@ function canonicalProductInputDigests(digests) {
5465
5970
  const normalized = [...digests].map((digest) => [digest.descriptorId, digest.digest]).sort((left, right) => compareAsciiStrings(left.join(SEGMENT_SEPARATOR), right.join(SEGMENT_SEPARATOR)));
5466
5971
  return JSON.stringify(normalized);
5467
5972
  }
5468
- function compareAsciiStrings(left, right) {
5469
- if (left < right) return -1;
5470
- if (left > right) return 1;
5471
- return 0;
5973
+ function isTestRunFileEntry(entry) {
5974
+ return entry.isFile() && isRunFileName(entry.name);
5975
+ }
5976
+ function testingRunFileError(error) {
5977
+ const stateStoreError = parseStateStoreError(error);
5978
+ if (stateStoreError?.code === STATE_STORE_ERROR.RUN_FILE_COLLISION_LIMIT) {
5979
+ return TESTING_RUN_STATE_ERROR.RUN_FILE_COLLISION_LIMIT;
5980
+ }
5981
+ if (stateStoreError?.code === STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED) {
5982
+ return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.RUN_FILE_CREATE_FAILED, stateStoreError.detail);
5983
+ }
5984
+ return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.RUN_FILE_CREATE_FAILED, error);
5985
+ }
5986
+ function testingWriteError(error) {
5987
+ const stateStoreError = parseStateStoreError(error);
5988
+ if (stateStoreError?.code === STATE_STORE_ERROR.RECORD_WRITE_FAILED) {
5989
+ return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED, stateStoreError.detail);
5990
+ }
5991
+ return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED, error);
5472
5992
  }
5473
- function isTestRunDirectoryEntry(entry) {
5474
- return entry.isDirectory() && RUN_DIRECTORY_NAME_PATTERN.test(entry.name);
5993
+ function withDomainErrorDetail(domainError, detail) {
5994
+ return detail === void 0 ? domainError : `${domainError}: ${detail}`;
5475
5995
  }
5476
5996
  function isRecord4(value) {
5477
5997
  return typeof value === "object" && value !== null && !Array.isArray(value);
5478
5998
  }
5479
5999
  function sha256Hex(value) {
5480
- return createHash3(SHA256_ALGORITHM2).update(value).digest(HEX_ENCODING2);
6000
+ return createHash4(SHA256_ALGORITHM2).update(value).digest(HEX_ENCODING3);
5481
6001
  }
5482
- function generateHexId(size, randomBytes) {
5483
- return randomBytes(size).toString(HEX_ENCODING2);
5484
- }
5485
- function hasErrorCode2(error, code) {
5486
- return isRecord4(error) && error.code === code;
5487
- }
5488
- function toErrorMessage(error) {
6002
+ function toErrorMessage2(error) {
5489
6003
  return error instanceof Error ? error.message : String(error);
5490
6004
  }
5491
6005
 
5492
6006
  // src/commands/testing/run-command.ts
5493
6007
  var NO_GIT_IDENTITY = "(unknown)";
5494
6008
  var TEXT_ENCODING = "utf8";
5495
- var NO_PRODUCT_INPUT_DIGESTS = [];
6009
+ var PRODUCT_INPUT_FIELDS = {
6010
+ PATH: "path",
6011
+ PRESENT: "present",
6012
+ CONTENT: "content"
6013
+ };
5496
6014
  function resolveRecordingDependencies(deps) {
5497
6015
  return {
5498
- fs: deps.fs ?? defaultFileSystem,
6016
+ fs: deps.fs ?? defaultFileSystem2,
5499
6017
  now: deps.now ?? (() => /* @__PURE__ */ new Date()),
5500
6018
  git: deps.git
5501
6019
  };
@@ -5518,28 +6036,74 @@ function coveredTestPaths(dispatch) {
5518
6036
  async function readCoveredContents(productDir, paths, fs7) {
5519
6037
  const entries = [];
5520
6038
  for (const path6 of paths) {
5521
- entries.push({ path: path6, content: await fs7.readFile(join14(productDir, path6), TEXT_ENCODING) });
6039
+ entries.push({ path: path6, content: await fs7.readFile(join16(productDir, path6), TEXT_ENCODING) });
6040
+ }
6041
+ return entries;
6042
+ }
6043
+ async function readProductInputEntries(productDir, paths, fs7) {
6044
+ const entries = [];
6045
+ for (const path6 of [...paths].sort(compareAsciiStrings)) {
6046
+ try {
6047
+ entries.push({
6048
+ [PRODUCT_INPUT_FIELDS.PATH]: path6,
6049
+ [PRODUCT_INPUT_FIELDS.PRESENT]: true,
6050
+ [PRODUCT_INPUT_FIELDS.CONTENT]: await fs7.readFile(join16(productDir, path6), TEXT_ENCODING)
6051
+ });
6052
+ } catch (error) {
6053
+ if (!hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) throw error;
6054
+ entries.push({
6055
+ [PRODUCT_INPUT_FIELDS.PATH]: path6,
6056
+ [PRODUCT_INPUT_FIELDS.PRESENT]: false
6057
+ });
6058
+ }
5522
6059
  }
5523
6060
  return entries;
5524
6061
  }
6062
+ async function digestLanguageProductInputs(productDir, language, coveredPaths, fs7) {
6063
+ const entries = await readProductInputEntries(productDir, languageProductInputPaths(language, coveredPaths), fs7);
6064
+ const digest = digestDescriptorSection(entries, `${language.name} product inputs`);
6065
+ if (!digest.ok) {
6066
+ throw new Error(`failed to digest ${language.name} product inputs: ${digest.error}`);
6067
+ }
6068
+ return digest.value.sha256;
6069
+ }
6070
+ async function productInputDigests(productDir, registry, coveredPaths, fs7) {
6071
+ const digests = [];
6072
+ for (const language of registry.languages) {
6073
+ digests.push({
6074
+ descriptorId: language.name,
6075
+ digest: await digestLanguageProductInputs(productDir, language, coveredPaths, fs7)
6076
+ });
6077
+ }
6078
+ return digests;
6079
+ }
5525
6080
  function deriveStatus(outcomes) {
5526
6081
  const allPassed = outcomes.every((outcome) => outcome.exitCode === SUCCESS_EXIT_CODE);
5527
6082
  return allPassed ? TEST_RUN_STATE_STATUS.PASSED : TEST_RUN_STATE_STATUS.FAILED;
5528
6083
  }
5529
- async function currentStalenessInputs(productDir, coveredPaths, deps = {}) {
5530
- const fs7 = deps.fs ?? defaultFileSystem;
6084
+ async function currentStalenessInputs(productDir, coveredPaths, deps) {
6085
+ const fs7 = deps.fs ?? defaultFileSystem2;
5531
6086
  const testingConfigDigest = deps.testingConfigDigest ?? (await resolveTestingConfig(productDir)).digest;
5532
6087
  const contents = await readCoveredContents(productDir, coveredPaths, fs7);
5533
6088
  return {
5534
6089
  testingConfigDigest,
5535
6090
  discoveredTestPathsDigest: digestTestPaths(coveredPaths),
5536
6091
  discoveredTestContentDigest: digestTestContents(contents),
5537
- productInputDigests: NO_PRODUCT_INPUT_DIGESTS
6092
+ productInputDigests: await productInputDigests(productDir, deps.registry, coveredPaths, fs7)
5538
6093
  };
5539
6094
  }
5540
- async function recordRun(directory, productDir, dispatch, recording, testingConfigDigest) {
6095
+ function languageProductInputPaths(language, coveredPaths) {
6096
+ return [
6097
+ .../* @__PURE__ */ new Set([
6098
+ ...language.productInputPaths,
6099
+ ...language.coveredProductInputPaths?.(coveredPaths) ?? []
6100
+ ])
6101
+ ].sort(compareAsciiStrings);
6102
+ }
6103
+ async function recordRun(runFile, productDir, dispatch, recording, registry, testingConfigDigest) {
5541
6104
  const staleness = await currentStalenessInputs(productDir, coveredTestPaths(dispatch), {
5542
6105
  fs: recording.fs,
6106
+ registry,
5543
6107
  testingConfigDigest
5544
6108
  });
5545
6109
  const branchName = await getCurrentBranch(productDir, recording.git) ?? NO_GIT_IDENTITY;
@@ -5552,43 +6116,43 @@ async function recordRun(directory, productDir, dispatch, recording, testingConf
5552
6116
  discoveredTestPathsDigest: staleness.discoveredTestPathsDigest,
5553
6117
  discoveredTestContentDigest: staleness.discoveredTestContentDigest,
5554
6118
  productInputDigests: staleness.productInputDigests,
5555
- startedAt: directory.startedAt,
6119
+ startedAt: runFile.startedAt,
5556
6120
  completedAt: formatTestRunTimestamp(recording.now()),
5557
6121
  status: deriveStatus(dispatch.outcomes)
5558
6122
  };
5559
- const written = await writeTerminalTestRunState(directory.runDir, state, { fs: recording.fs });
6123
+ const written = await writeTerminalTestRunState(runFile.runFilePath, state, { fs: recording.fs });
5560
6124
  if (!written.ok) {
5561
6125
  throw new Error(`failed to record test run: ${written.error}`);
5562
6126
  }
5563
6127
  return state;
5564
6128
  }
5565
- async function reserveRunDirectory(productDir, recording) {
5566
- const directory = await createTestRunDirectory(productDir, { fs: recording.fs, now: recording.now });
5567
- if (!directory.ok) {
5568
- throw new Error(`failed to create test run directory: ${directory.error}`);
6129
+ async function reserveRunFile(productDir, recording) {
6130
+ const runFile = await createTestRunFile(productDir, { fs: recording.fs, now: recording.now });
6131
+ if (!runFile.ok) {
6132
+ throw new Error(`failed to create test run file: ${runFile.error}`);
5569
6133
  }
5570
- return directory.value;
6134
+ return runFile.value;
5571
6135
  }
5572
6136
  async function runTestsCommand(options, deps) {
5573
6137
  const { config, digest } = await resolveTestingConfig(options.productDir);
5574
6138
  const passingScope = options.passing ? config.passingScope : void 0;
5575
6139
  const recording = resolveRecordingDependencies(deps);
5576
- const directory = await reserveRunDirectory(options.productDir, recording);
6140
+ const runFile = await reserveRunFile(options.productDir, recording);
5577
6141
  const dispatch = await runTests(
5578
6142
  { productDir: options.productDir, registry: deps.registry, passingScope },
5579
6143
  { runnerDepsFor: deps.runnerDepsFor }
5580
6144
  );
5581
- const recorded = await recordRun(directory, options.productDir, dispatch, recording, digest);
6145
+ const recorded = await recordRun(runFile, options.productDir, dispatch, recording, deps.registry, digest);
5582
6146
  return { dispatch, recorded };
5583
6147
  }
5584
6148
  async function runNodeCommand(options, deps) {
5585
6149
  const recording = resolveRecordingDependencies(deps);
5586
- const directory = await reserveRunDirectory(options.productDir, recording);
6150
+ const runFile = await reserveRunFile(options.productDir, recording);
5587
6151
  const dispatch = await runTests(
5588
6152
  { productDir: options.productDir, registry: deps.registry, passingScope: { include: [options.nodePath] } },
5589
6153
  { runnerDepsFor: deps.runnerDepsFor }
5590
6154
  );
5591
- const recorded = await recordRun(directory, options.productDir, dispatch, recording);
6155
+ const recorded = await recordRun(runFile, options.productDir, dispatch, recording, deps.registry);
5592
6156
  return { dispatch, recorded };
5593
6157
  }
5594
6158
 
@@ -5596,19 +6160,37 @@ async function runNodeCommand(options, deps) {
5596
6160
  var PATH_SEPARATOR = "/";
5597
6161
  function createNodeOutcomeResolver(deps) {
5598
6162
  let evidence;
6163
+ const currentInputsByCoveredPaths = /* @__PURE__ */ new Map();
5599
6164
  const sharedEvidence = () => evidence ??= loadResolverEvidence(deps);
6165
+ const refreshEvidence = () => {
6166
+ evidence = loadResolverEvidence(deps);
6167
+ };
6168
+ const currentInputsFor = (coveredPaths) => {
6169
+ const cacheKey = coveredPathCollectionKey(coveredPaths);
6170
+ const cached = currentInputsByCoveredPaths.get(cacheKey);
6171
+ if (cached !== void 0) {
6172
+ return cached;
6173
+ }
6174
+ const current = currentStalenessInputs(deps.productDir, coveredPaths, deps);
6175
+ currentInputsByCoveredPaths.set(cacheKey, current);
6176
+ return current;
6177
+ };
5600
6178
  return async (nodeId) => {
5601
6179
  const { discoveredTestPaths, terminalRuns } = await sharedEvidence();
5602
6180
  const nodePath = `${SPEC_TREE_CONFIG.ROOT_DIRECTORY}${PATH_SEPARATOR}${nodeId}`;
5603
6181
  const nodeTestPaths = filterNodeTestPaths(discoveredTestPaths, nodePath);
5604
- const usable = await usableRecordedOutcome(deps, discoveredTestPaths, terminalRuns, nodeTestPaths);
6182
+ const usable = await usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor);
5605
6183
  if (usable !== void 0) {
5606
6184
  return usable;
5607
6185
  }
5608
6186
  const { dispatch, recorded } = await runNodeCommand({ productDir: deps.productDir, nodePath }, deps);
6187
+ refreshEvidence();
5609
6188
  return outcomesCoverPaths(dispatch.outcomes, nodeTestPaths) && recorded.status === TEST_RUN_STATE_STATUS.PASSED;
5610
6189
  };
5611
6190
  }
6191
+ function coveredPathCollectionKey(coveredPaths) {
6192
+ return JSON.stringify([...coveredPaths].sort());
6193
+ }
5612
6194
  async function loadResolverEvidence(deps) {
5613
6195
  const discoveredTestPaths = await discoverTestFiles(deps.productDir);
5614
6196
  const runs = await readTestingRuns(deps.productDir, deps);
@@ -5618,7 +6200,7 @@ function filterNodeTestPaths(discoveredTestPaths, nodePath) {
5618
6200
  const prefix = `${nodePath}${PATH_SEPARATOR}`;
5619
6201
  return discoveredTestPaths.filter((path6) => path6.startsWith(prefix));
5620
6202
  }
5621
- async function usableRecordedOutcome(deps, discoveredTestPaths, terminalRuns, nodeTestPaths) {
6203
+ async function usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor) {
5622
6204
  const latest = selectLatestTerminalTestRunForNode(terminalRuns, nodeTestPaths);
5623
6205
  if (latest === void 0) {
5624
6206
  return void 0;
@@ -5628,7 +6210,7 @@ async function usableRecordedOutcome(deps, discoveredTestPaths, terminalRuns, no
5628
6210
  if (!runCoveredPaths.every((path6) => presentTestPaths.has(path6))) {
5629
6211
  return void 0;
5630
6212
  }
5631
- const current = await currentStalenessInputs(deps.productDir, runCoveredPaths, deps);
6213
+ const current = await currentInputsFor(runCoveredPaths);
5632
6214
  const fresh = isStalenessMatch(extractStalenessInputs(latest.state), current);
5633
6215
  return fresh && latest.state.status === TEST_RUN_STATE_STATUS.PASSED ? true : void 0;
5634
6216
  }
@@ -5648,11 +6230,11 @@ function serializeNodeStatus(state) {
5648
6230
  }
5649
6231
 
5650
6232
  // src/lib/node-status/provider.ts
5651
- import { join as join16 } from "path";
6233
+ import { join as join18 } from "path";
5652
6234
 
5653
6235
  // src/lib/node-status/read.ts
5654
6236
  import { readFileSync as readFileSync2 } from "fs";
5655
- import { join as join15 } from "path";
6237
+ import { join as join17 } from "path";
5656
6238
  var NODE_STATUS_FILENAME = "spx.status.json";
5657
6239
  var NODE_STATUS_VALUES = new Set(Object.values(SPEC_TREE_NODE_STATE));
5658
6240
  function isNodeError2(error) {
@@ -5662,7 +6244,7 @@ function isSpecTreeNodeState(value) {
5662
6244
  return typeof value === "string" && NODE_STATUS_VALUES.has(value);
5663
6245
  }
5664
6246
  function readNodeStatus(nodeDir) {
5665
- const filePath = join15(nodeDir, NODE_STATUS_FILENAME);
6247
+ const filePath = join17(nodeDir, NODE_STATUS_FILENAME);
5666
6248
  let content;
5667
6249
  try {
5668
6250
  content = readFileSync2(filePath, "utf8");
@@ -5684,7 +6266,7 @@ function readNodeStatus(nodeDir) {
5684
6266
 
5685
6267
  // src/lib/node-status/provider.ts
5686
6268
  function nodeDirectory(productDir, node) {
5687
- return join16(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
6269
+ return join18(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
5688
6270
  }
5689
6271
  function createNodeStatusProvider(productDir) {
5690
6272
  return {
@@ -5696,7 +6278,7 @@ function createNodeStatusProvider(productDir) {
5696
6278
 
5697
6279
  // src/lib/node-status/update.ts
5698
6280
  import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
5699
- import { dirname as dirname3, join as join17 } from "path";
6281
+ import { dirname as dirname4, join as join19 } from "path";
5700
6282
  var NODE_STATUS_TEXT_ENCODING = "utf8";
5701
6283
  async function updateNodeStatus(options) {
5702
6284
  const { productDir, resolveOutcome } = options;
@@ -5734,8 +6316,8 @@ function isNodeExcluded(ignoreReader, node) {
5734
6316
  return ignoreReader.isUnderIgnoreSource(reference);
5735
6317
  }
5736
6318
  async function writeNodeStatus(productDir, nodeId, state) {
5737
- const filePath = join17(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
5738
- await mkdir4(dirname3(filePath), { recursive: true });
6319
+ const filePath = join19(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
6320
+ await mkdir4(dirname4(filePath), { recursive: true });
5739
6321
  await writeFile2(filePath, serializeNodeStatus(state), NODE_STATUS_TEXT_ENCODING);
5740
6322
  }
5741
6323
 
@@ -5849,26 +6431,95 @@ function formatNodeLabel(node) {
5849
6431
  }
5850
6432
 
5851
6433
  // src/testing/languages/python.ts
5852
- import { basename } from "path";
6434
+ import { basename as basename2, dirname as dirname5, join as join20 } from "path/posix";
6435
+
6436
+ // src/validation/discovery/language-finder.ts
6437
+ import fs5 from "fs";
6438
+ import path4 from "path";
6439
+ var TYPESCRIPT_MARKER = "tsconfig.json";
6440
+ var PYTHON_MARKER = "pyproject.toml";
6441
+ var ESLINT_CONFIG_FILES = [
6442
+ "eslint.config.ts",
6443
+ "eslint.config.js",
6444
+ "eslint.config.mjs",
6445
+ "eslint.config.cjs"
6446
+ ];
6447
+ var ESLINT_PRODUCTION_CONFIG_FILES = [
6448
+ "eslint.config.production.ts",
6449
+ "eslint.config.production.js",
6450
+ "eslint.config.production.mjs",
6451
+ "eslint.config.production.cjs"
6452
+ ];
6453
+ var defaultLanguageDetectionDeps = {
6454
+ existsSync: fs5.existsSync
6455
+ };
6456
+ function detectTypeScript(projectRoot, deps = defaultLanguageDetectionDeps) {
6457
+ const present = deps.existsSync(path4.join(projectRoot, TYPESCRIPT_MARKER));
6458
+ if (!present) {
6459
+ return { present: false };
6460
+ }
6461
+ const eslintConfigFile = ESLINT_CONFIG_FILES.find(
6462
+ (configFile) => deps.existsSync(path4.join(projectRoot, configFile))
6463
+ );
6464
+ const productionEslintConfigFile = ESLINT_PRODUCTION_CONFIG_FILES.find(
6465
+ (configFile) => deps.existsSync(path4.join(projectRoot, configFile))
6466
+ );
6467
+ return { present: true, eslintConfigFile, productionEslintConfigFile };
6468
+ }
6469
+ function detectPython(projectRoot, deps = defaultLanguageDetectionDeps) {
6470
+ const present = deps.existsSync(path4.join(projectRoot, PYTHON_MARKER));
6471
+ return { present };
6472
+ }
6473
+
6474
+ // src/testing/languages/python-pytest-contract.ts
6475
+ var PYTHON_PYTEST_IGNORE_FLAG_PREFIX = "--ignore=spx/";
6476
+ var PYTHON_PYTEST_IGNORE_FLAG_SUFFIX = "/";
6477
+ var UV_COMMAND = "uv";
6478
+ var PYTEST_INVOKE_ARGS = ["run", "--active", "pytest"];
6479
+
6480
+ // src/testing/languages/python.ts
5853
6481
  var PYTHON_TESTING_LANGUAGE_NAME = "python";
6482
+ var PYTHON_PRODUCT_INPUT_PATH = {
6483
+ CONFTEST: "conftest.py",
6484
+ HIDDEN_PYTEST_INI: ".pytest.ini",
6485
+ HIDDEN_PYTEST_TOML: ".pytest.toml",
6486
+ PYPROJECT: "pyproject.toml",
6487
+ PYTEST_TOML: "pytest.toml",
6488
+ PYTEST_INI: "pytest.ini",
6489
+ SETUP_CFG: "setup.cfg",
6490
+ SETUP_PY: "setup.py",
6491
+ TOX_INI: "tox.ini",
6492
+ UV_LOCK: "uv.lock"
6493
+ };
6494
+ var PYTHON_PRODUCT_INPUT_PATHS = Object.values(PYTHON_PRODUCT_INPUT_PATH);
5854
6495
  var PYTHON_TEST_FILE_PREFIX = "test_";
5855
6496
  var PYTHON_TEST_FILE_EXTENSION = ".py";
5856
6497
  var PYTHON_TEST_FILE_PATTERNS = [`${PYTHON_TEST_FILE_PREFIX}*${PYTHON_TEST_FILE_EXTENSION}`];
5857
- var PYTHON_PYTEST_IGNORE_FLAG_PREFIX = "--ignore=spx/";
5858
- var PYTHON_PYTEST_IGNORE_FLAG_SUFFIX = "/";
5859
- var UV_COMMAND = "uv";
5860
- var PYTEST_INVOKE_ARGS = ["run", "pytest"];
5861
6498
  function matchesTestFile(filePath) {
5862
- return basename(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
6499
+ return basename2(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
6500
+ }
6501
+ function coveredProductInputPaths(coveredTestPaths2) {
6502
+ const paths = /* @__PURE__ */ new Set();
6503
+ for (const testPath of coveredTestPaths2) {
6504
+ if (!matchesTestFile(testPath)) continue;
6505
+ let directory = dirname5(testPath);
6506
+ while (directory !== "." && directory.length > 0) {
6507
+ paths.add(join20(directory, PYTHON_PRODUCT_INPUT_PATH.CONFTEST));
6508
+ const parent = dirname5(directory);
6509
+ if (parent === directory) break;
6510
+ directory = parent;
6511
+ }
6512
+ }
6513
+ return [...paths].sort(compareAsciiStrings);
5863
6514
  }
5864
6515
  function excludeFlag(nodePath) {
5865
6516
  return `${PYTHON_PYTEST_IGNORE_FLAG_PREFIX}${nodePath}${PYTHON_PYTEST_IGNORE_FLAG_SUFFIX}`;
5866
6517
  }
5867
6518
  function detect(projectRoot, deps) {
5868
- return deps.isLanguagePresent(projectRoot);
6519
+ return deps?.isLanguagePresent?.(projectRoot) ?? detectPython(projectRoot).present;
5869
6520
  }
5870
6521
  async function runTests2(request, deps) {
5871
- if (!deps.isLanguagePresent(request.projectRoot)) {
6522
+ if (!detect(request.projectRoot, deps)) {
5872
6523
  return { invoked: false };
5873
6524
  }
5874
6525
  const args = [
@@ -5882,6 +6533,8 @@ async function runTests2(request, deps) {
5882
6533
  var pythonTestingLanguage = {
5883
6534
  name: PYTHON_TESTING_LANGUAGE_NAME,
5884
6535
  testFilePatterns: PYTHON_TEST_FILE_PATTERNS,
6536
+ productInputPaths: PYTHON_PRODUCT_INPUT_PATHS,
6537
+ coveredProductInputPaths,
5885
6538
  matchesTestFile,
5886
6539
  excludeFlag,
5887
6540
  detect,
@@ -5892,6 +6545,15 @@ var pythonTestingLanguage = {
5892
6545
  var TYPESCRIPT_TESTING_LANGUAGE_NAME = "typescript";
5893
6546
  var TYPESCRIPT_TEST_FILE_PATTERNS = ["*.test.ts", "*.test.tsx"];
5894
6547
  var TYPESCRIPT_TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx"];
6548
+ var TYPESCRIPT_PRODUCT_INPUT_PATHS = [
6549
+ "package.json",
6550
+ "pnpm-lock.yaml",
6551
+ "tsconfig.json",
6552
+ "vitest.config.js",
6553
+ "vitest.config.mjs",
6554
+ "vitest.config.ts",
6555
+ "vitest.config.mts"
6556
+ ];
5895
6557
  var TYPESCRIPT_VITEST_EXCLUDE_FLAG_PREFIX = "--exclude=spx/";
5896
6558
  var TYPESCRIPT_VITEST_EXCLUDE_FLAG_SUFFIX = "/**";
5897
6559
  var PACKAGE_MANAGER_COMMAND = "pnpm";
@@ -5904,10 +6566,10 @@ function excludeFlag2(nodePath) {
5904
6566
  return `${TYPESCRIPT_VITEST_EXCLUDE_FLAG_PREFIX}${nodePath}${TYPESCRIPT_VITEST_EXCLUDE_FLAG_SUFFIX}`;
5905
6567
  }
5906
6568
  function detect2(projectRoot, deps) {
5907
- return deps.isLanguagePresent(projectRoot);
6569
+ return deps?.isLanguagePresent?.(projectRoot) ?? detectTypeScript(projectRoot).present;
5908
6570
  }
5909
6571
  async function runTests3(request, deps) {
5910
- if (!deps.isLanguagePresent(request.projectRoot)) {
6572
+ if (!detect2(request.projectRoot, deps)) {
5911
6573
  return { invoked: false };
5912
6574
  }
5913
6575
  const args = [
@@ -5923,6 +6585,7 @@ async function runTests3(request, deps) {
5923
6585
  var typescriptTestingLanguage = {
5924
6586
  name: TYPESCRIPT_TESTING_LANGUAGE_NAME,
5925
6587
  testFilePatterns: TYPESCRIPT_TEST_FILE_PATTERNS,
6588
+ productInputPaths: TYPESCRIPT_PRODUCT_INPUT_PATHS,
5926
6589
  matchesTestFile: matchesTestFile2,
5927
6590
  excludeFlag: excludeFlag2,
5928
6591
  detect: detect2,
@@ -6078,51 +6741,8 @@ function spawnManagedSubprocess(runner, command, args, options) {
6078
6741
  });
6079
6742
  }
6080
6743
 
6081
- // src/validation/discovery/language-finder.ts
6082
- import fs5 from "fs";
6083
- import path4 from "path";
6084
- var TYPESCRIPT_MARKER = "tsconfig.json";
6085
- var PYTHON_MARKER = "pyproject.toml";
6086
- var ESLINT_CONFIG_FILES = [
6087
- "eslint.config.ts",
6088
- "eslint.config.js",
6089
- "eslint.config.mjs",
6090
- "eslint.config.cjs"
6091
- ];
6092
- var ESLINT_PRODUCTION_CONFIG_FILES = [
6093
- "eslint.config.production.ts",
6094
- "eslint.config.production.js",
6095
- "eslint.config.production.mjs",
6096
- "eslint.config.production.cjs"
6097
- ];
6098
- var defaultLanguageDetectionDeps = {
6099
- existsSync: fs5.existsSync
6100
- };
6101
- function detectTypeScript(projectRoot, deps = defaultLanguageDetectionDeps) {
6102
- const present = deps.existsSync(path4.join(projectRoot, TYPESCRIPT_MARKER));
6103
- if (!present) {
6104
- return { present: false };
6105
- }
6106
- const eslintConfigFile = ESLINT_CONFIG_FILES.find(
6107
- (configFile) => deps.existsSync(path4.join(projectRoot, configFile))
6108
- );
6109
- const productionEslintConfigFile = ESLINT_PRODUCTION_CONFIG_FILES.find(
6110
- (configFile) => deps.existsSync(path4.join(projectRoot, configFile))
6111
- );
6112
- return { present: true, eslintConfigFile, productionEslintConfigFile };
6113
- }
6114
- function detectPython(projectRoot, deps = defaultLanguageDetectionDeps) {
6115
- const present = deps.existsSync(path4.join(projectRoot, PYTHON_MARKER));
6116
- return { present };
6117
- }
6118
-
6119
6744
  // src/interfaces/cli/testing-runner-deps.ts
6120
6745
  var PROCESS_FAILURE_EXIT_CODE = 1;
6121
- var NO_PRESENCE_DETECTOR_ERROR = "no presence detector configured for testing language";
6122
- var PRESENCE_BY_LANGUAGE_NAME = {
6123
- [typescriptTestingLanguage.name]: (productDir) => detectTypeScript(productDir).present,
6124
- [pythonTestingLanguage.name]: (productDir) => detectPython(productDir).present
6125
- };
6126
6746
  function createCommandRunner(productDir, outStream) {
6127
6747
  return (command, args) => new Promise((resolveResult) => {
6128
6748
  const child = spawnManagedSubprocess(lifecycleProcessRunner, command, args, {
@@ -6136,13 +6756,7 @@ function createCommandRunner(productDir, outStream) {
6136
6756
  }
6137
6757
  function createRunnerDepsFor(productDir, outStream = process.stdout) {
6138
6758
  const runCommand = createCommandRunner(productDir, outStream);
6139
- return (language) => {
6140
- const isLanguagePresent = PRESENCE_BY_LANGUAGE_NAME[language.name];
6141
- if (isLanguagePresent === void 0) {
6142
- throw new Error(`${NO_PRESENCE_DETECTOR_ERROR}: ${language.name}`);
6143
- }
6144
- return { isLanguagePresent, runCommand };
6145
- };
6759
+ return () => ({ runCommand });
6146
6760
  }
6147
6761
 
6148
6762
  // src/interfaces/cli/spec.ts
@@ -6273,10 +6887,10 @@ var testingDomain = {
6273
6887
  };
6274
6888
 
6275
6889
  // src/commands/validation/markdown.ts
6276
- import { relative as relative5 } from "path";
6890
+ import { relative as relative3 } from "path";
6277
6891
 
6278
6892
  // src/validation/config/path-filter.ts
6279
- import { isAbsolute as isAbsolute2, relative as relative4 } from "path";
6893
+ import { isAbsolute as isAbsolute2, relative as relative2 } from "path";
6280
6894
  var PATH_PREFIX_SEPARATOR = "/";
6281
6895
  function hasEffectiveValidationPathMetadata(filter) {
6282
6896
  return "hasIncludeFilter" in filter && typeof filter.hasIncludeFilter === "boolean" && "noMatchingIncludes" in filter && typeof filter.noMatchingIncludes === "boolean";
@@ -6296,7 +6910,7 @@ function nonEmpty(values) {
6296
6910
  return values?.filter((value) => value.length > 0) ?? [];
6297
6911
  }
6298
6912
  function toProjectRelativeValidationPath(projectRoot, path6) {
6299
- return isAbsolute2(path6) ? relative4(projectRoot, path6) : path6;
6913
+ return isAbsolute2(path6) ? relative2(projectRoot, path6) : path6;
6300
6914
  }
6301
6915
  function intersectIncludes(baseInclude, toolInclude) {
6302
6916
  const base = nonEmpty(baseInclude);
@@ -6371,8 +6985,8 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
6371
6985
  }
6372
6986
 
6373
6987
  // src/validation/steps/markdown.ts
6374
- import { existsSync as existsSync2, statSync } from "fs";
6375
- import { basename as basename2, dirname as dirname4, join as join18, relative as pathRelative } from "path";
6988
+ import { existsSync, statSync } from "fs";
6989
+ import { basename as basename3, dirname as dirname6, join as join21, relative as pathRelative } from "path";
6376
6990
  import { main as markdownlintMain } from "markdownlint-cli2";
6377
6991
  import relativeLinksRule from "markdownlint-rule-relative-links";
6378
6992
  var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
@@ -6411,7 +7025,7 @@ function buildMarkdownlintConfig(directoryName) {
6411
7025
  };
6412
7026
  }
6413
7027
  function getDefaultDirectories(projectRoot) {
6414
- return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join18(projectRoot, name)).filter((dir) => existsSync2(dir));
7028
+ return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join21(projectRoot, name)).filter((dir) => existsSync(dir));
6415
7029
  }
6416
7030
  function resolveMarkdownValidationTarget(path6, deps = defaultMarkdownValidationTargetDeps) {
6417
7031
  if (isExistingDirectory(path6, deps)) {
@@ -6470,7 +7084,7 @@ async function validateMarkdown(options) {
6470
7084
  async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
6471
7085
  const errors = [];
6472
7086
  const directory = targetDirectory(target);
6473
- const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename2(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
7087
+ const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename3(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
6474
7088
  const { customRules, ...markdownlintConfig } = config;
6475
7089
  const optionsOverride = {
6476
7090
  config: {
@@ -6494,7 +7108,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
6494
7108
  if (parsed) {
6495
7109
  errors.push({
6496
7110
  ...parsed,
6497
- file: join18(directory, parsed.file)
7111
+ file: join21(directory, parsed.file)
6498
7112
  });
6499
7113
  }
6500
7114
  }
@@ -6502,7 +7116,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
6502
7116
  return errors;
6503
7117
  }
6504
7118
  function targetDirectory(target) {
6505
- return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname4(target.path) : target.path;
7119
+ return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname6(target.path) : target.path;
6506
7120
  }
6507
7121
  function hasMarkdownExtension(path6) {
6508
7122
  const lastDot = path6.lastIndexOf(".");
@@ -6530,7 +7144,7 @@ function markdownlintConfigDirectoryName(directory, projectRoot) {
6530
7144
  return rootSegment;
6531
7145
  }
6532
7146
  }
6533
- return basename2(directory);
7147
+ return basename3(directory);
6534
7148
  }
6535
7149
 
6536
7150
  // src/commands/validation/messages.ts
@@ -6606,7 +7220,7 @@ async function markdownCommand(options) {
6606
7220
  path: path6
6607
7221
  }));
6608
7222
  const targets = unfilteredTargets.filter(
6609
- (target) => pathPassesValidationFilter(relative5(cwd, target.path), pathFilter)
7223
+ (target) => pathPassesValidationFilter(relative3(cwd, target.path), pathFilter)
6610
7224
  );
6611
7225
  const skippedTargets = targetResolutions === void 0 ? [] : targetResolutions.map((resolution) => resolution.skipped).filter((skipped) => skipped !== void 0);
6612
7226
  const skippedOutput = quiet ? [] : skippedTargets.map(formatSkippedFileScope);
@@ -7515,8 +8129,8 @@ var ParseErrorCode;
7515
8129
  })(ParseErrorCode || (ParseErrorCode = {}));
7516
8130
 
7517
8131
  // src/validation/config/scope.ts
7518
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
7519
- import { isAbsolute as isAbsolute3, join as join19 } from "path";
8132
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
8133
+ import { isAbsolute as isAbsolute3, join as join22 } from "path";
7520
8134
  var TSCONFIG_FILES = {
7521
8135
  full: "tsconfig.json",
7522
8136
  production: "tsconfig.production.json"
@@ -7526,11 +8140,11 @@ var GLOB_MARKER = "*";
7526
8140
  var HIDDEN_PATH_PREFIX = ".";
7527
8141
  var defaultScopeDeps = {
7528
8142
  readFileSync: readFileSync3,
7529
- existsSync: existsSync3,
8143
+ existsSync: existsSync2,
7530
8144
  readdirSync
7531
8145
  };
7532
8146
  function resolveProjectPath(projectRoot, path6) {
7533
- return isAbsolute3(path6) ? path6 : join19(projectRoot, path6);
8147
+ return isAbsolute3(path6) ? path6 : join22(projectRoot, path6);
7534
8148
  }
7535
8149
  function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
7536
8150
  try {
@@ -7574,7 +8188,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
7574
8188
  if (hasDirectTsFiles) return true;
7575
8189
  const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
7576
8190
  for (const subdir of subdirs.slice(0, 5)) {
7577
- if (hasTypeScriptFilesRecursive(join19(dirPath, subdir.name), maxDepth - 1, deps)) {
8191
+ if (hasTypeScriptFilesRecursive(join22(dirPath, subdir.name), maxDepth - 1, deps)) {
7578
8192
  return true;
7579
8193
  }
7580
8194
  }
@@ -7597,7 +8211,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
7597
8211
  });
7598
8212
  if (!isExcluded) {
7599
8213
  try {
7600
- const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join19(projectRoot, dir), 2, deps);
8214
+ const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join22(projectRoot, dir), 2, deps);
7601
8215
  if (hasTypeScriptFiles) {
7602
8216
  directories.add(dir);
7603
8217
  }
@@ -7628,13 +8242,13 @@ function getLiteralTopLevelPatternDirectory(pattern) {
7628
8242
  function filterActiveIncludePatterns(patterns, excludePatterns, projectRoot, deps) {
7629
8243
  return patterns.filter((pattern) => {
7630
8244
  const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
7631
- return topLevelDir === null || deps.existsSync(join19(projectRoot, topLevelDir));
8245
+ return topLevelDir === null || deps.existsSync(join22(projectRoot, topLevelDir));
7632
8246
  }).filter((pattern) => pathPassesValidationFilter(pattern, { exclude: excludePatterns }));
7633
8247
  }
7634
8248
  function getValidationDirectories(scope, projectRoot, deps = defaultScopeDeps) {
7635
8249
  const config = resolveTypeScriptConfig(scope, projectRoot, deps);
7636
8250
  const configDirectories = getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps);
7637
- const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join19(projectRoot, dir)));
8251
+ const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join22(projectRoot, dir)));
7638
8252
  return existingDirectories;
7639
8253
  }
7640
8254
  function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
@@ -7758,7 +8372,7 @@ function formatSkipMessage(stepName, result) {
7758
8372
 
7759
8373
  // src/validation/steps/circular.ts
7760
8374
  import madge from "madge";
7761
- import { join as join20 } from "path";
8375
+ import { join as join23 } from "path";
7762
8376
  var CIRCULAR_DEPS_KEYS = {
7763
8377
  MADGE: "madge"
7764
8378
  };
@@ -7767,11 +8381,11 @@ var defaultCircularDeps = {
7767
8381
  };
7768
8382
  async function validateCircularDependencies(scope, typescriptScope, projectRoot, deps = defaultCircularDeps) {
7769
8383
  try {
7770
- const analyzeDirectories = typescriptScope.directories.map((directory) => join20(projectRoot, directory));
8384
+ const analyzeDirectories = typescriptScope.directories.map((directory) => join23(projectRoot, directory));
7771
8385
  if (analyzeDirectories.length === 0) {
7772
8386
  return { success: true };
7773
8387
  }
7774
- const tsConfigFile = join20(projectRoot, TSCONFIG_FILES[scope]);
8388
+ const tsConfigFile = join23(projectRoot, TSCONFIG_FILES[scope]);
7775
8389
  const excludeRegExps = typescriptScope.excludePatterns.map((pattern) => {
7776
8390
  const cleanPattern = pattern.replace(/\/\*\*?\/\*$/, "");
7777
8391
  const escaped = cleanPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -7851,10 +8465,10 @@ ${cycles}`;
7851
8465
  }
7852
8466
 
7853
8467
  // src/validation/steps/knip.ts
7854
- import { existsSync as existsSync4 } from "fs";
8468
+ import { existsSync as existsSync3 } from "fs";
7855
8469
  import { mkdtemp, rm, writeFile as writeFile3 } from "fs/promises";
7856
8470
  import { tmpdir } from "os";
7857
- import { isAbsolute as isAbsolute4, join as join21 } from "path";
8471
+ import { isAbsolute as isAbsolute4, join as join24 } from "path";
7858
8472
  var defaultKnipProcessRunner = lifecycleProcessRunner;
7859
8473
  var KNIP_COMMAND_TOKENS = {
7860
8474
  COMMAND: "knip",
@@ -7862,7 +8476,7 @@ var KNIP_COMMAND_TOKENS = {
7862
8476
  USE_TSCONFIG_FILES_FLAG: "--use-tsconfig-files"
7863
8477
  };
7864
8478
  var defaultKnipDeps = {
7865
- existsSync: existsSync4,
8479
+ existsSync: existsSync3,
7866
8480
  mkdtemp,
7867
8481
  rm,
7868
8482
  writeFile: writeFile3
@@ -7882,7 +8496,7 @@ async function validateKnip2(context, runner = defaultKnipProcessRunner, deps =
7882
8496
  }
7883
8497
  async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
7884
8498
  const scopedTsconfig = typescriptScope.filteredByValidationPaths ? await createScopedKnipTsconfig(projectRoot, typescriptScope, deps) : void 0;
7885
- const localBin = join21(projectRoot, "node_modules", ".bin", "knip");
8499
+ const localBin = join24(projectRoot, "node_modules", ".bin", "knip");
7886
8500
  const binary = deps.existsSync(localBin) ? localBin : "npx";
7887
8501
  const baseArgs = scopedTsconfig === void 0 ? [] : [
7888
8502
  KNIP_COMMAND_TOKENS.USE_TSCONFIG_FILES_FLAG,
@@ -7912,13 +8526,13 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
7912
8526
  knipProcess.stderr?.on("data", (data) => {
7913
8527
  knipError += data.toString();
7914
8528
  });
7915
- return new Promise((resolve6) => {
8529
+ return new Promise((resolve5) => {
7916
8530
  const resolveAfterCleanup = (result) => {
7917
8531
  if (resultResolved) {
7918
8532
  return;
7919
8533
  }
7920
8534
  resultResolved = true;
7921
- void cleanupOnce().finally(() => resolve6(result));
8535
+ void cleanupOnce().finally(() => resolve5(result));
7922
8536
  };
7923
8537
  knipProcess.on("close", (code) => {
7924
8538
  if (code === 0) {
@@ -7937,12 +8551,12 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
7937
8551
  });
7938
8552
  }
7939
8553
  async function createScopedKnipTsconfig(projectRoot, typescriptScope, deps) {
7940
- const tempDir = await deps.mkdtemp(join21(tmpdir(), "validate-knip-"));
7941
- const configPath = join21(tempDir, TSCONFIG_FILES.full);
7942
- const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join21(projectRoot, pattern);
8554
+ const tempDir = await deps.mkdtemp(join24(tmpdir(), "validate-knip-"));
8555
+ const configPath = join24(tempDir, TSCONFIG_FILES.full);
8556
+ const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join24(projectRoot, pattern);
7943
8557
  const project = typescriptScope.filePatterns.length > 0 ? typescriptScope.filePatterns : typescriptScope.directories.map((directory) => `${directory}/**/*.{js,ts,tsx}`);
7944
8558
  const config = {
7945
- extends: join21(projectRoot, TSCONFIG_FILES.full),
8559
+ extends: join24(projectRoot, TSCONFIG_FILES.full),
7946
8560
  include: project.map(toProjectPathPattern),
7947
8561
  exclude: typescriptScope.excludePatterns.map(toProjectPathPattern)
7948
8562
  };
@@ -7991,13 +8605,13 @@ async function knipCommand(options) {
7991
8605
  }
7992
8606
 
7993
8607
  // src/validation/steps/eslint.ts
7994
- import { existsSync as existsSync6 } from "fs";
7995
- import { join as join23 } from "path";
8608
+ import { existsSync as existsSync5 } from "fs";
8609
+ import { join as join26 } from "path";
7996
8610
 
7997
8611
  // src/validation/lint-policy.ts
7998
8612
  import { execFileSync } from "child_process";
7999
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
8000
- import { join as join22 } from "path";
8613
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
8614
+ import { join as join25 } from "path";
8001
8615
 
8002
8616
  // src/validation/lint-policy-constants.ts
8003
8617
  var LINT_POLICY_MANIFESTS = {
@@ -8038,29 +8652,30 @@ var TEST_LINT_DEBT_NODE_MANIFEST_FILE = LINT_POLICY_MANIFESTS.TEST_LINT_DEBT_NOD
8038
8652
  var TEST_LINT_DEBT_NODE_MANIFEST_KEY = LINT_POLICY_MANIFESTS.TEST_LINT_DEBT_NODES.key;
8039
8653
  var TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_FILE = LINT_POLICY_MANIFESTS.TEST_OWNED_CONSTANT_DEBT_NODES.file;
8040
8654
  var TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_KEY = LINT_POLICY_MANIFESTS.TEST_OWNED_CONSTANT_DEBT_NODES.key;
8041
- var SPEC_TREE_ROOT = "spx";
8042
- var SPEC_TREE_NODE_SUFFIX_PATTERN = /\.(enabler|outcome)$/;
8043
- var DEPRECATED_SPEC_NODE_SUFFIX_PATTERN = /\.(capability|feature|story)$/;
8655
+ var SPEC_TREE_ROOT = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
8044
8656
  var BASE_BRANCH_REFS = [LINT_POLICY_BASE_REFS.REMOTE_MAIN, LINT_POLICY_BASE_REFS.LOCAL_MAIN];
8657
+ function isSpecTreeNodePath(entry) {
8658
+ return NODE_SUFFIXES.some((suffix) => entry.endsWith(suffix));
8659
+ }
8045
8660
  function readManifest(productDir, file, key) {
8046
8661
  return parseLintPolicyManifest(
8047
- readFileSync4(join22(productDir, file), "utf-8"),
8662
+ readFileSync4(join25(productDir, file), "utf-8"),
8048
8663
  file,
8049
8664
  key
8050
8665
  );
8051
8666
  }
8052
8667
  function manifestExists(productDir, file) {
8053
- return existsSync5(join22(productDir, file));
8668
+ return existsSync4(join25(productDir, file));
8054
8669
  }
8055
8670
  function findDeprecatedSpecNodePath(productDir) {
8056
8671
  function visit2(relativeDirectory) {
8057
- const absoluteDirectory = join22(productDir, relativeDirectory);
8672
+ const absoluteDirectory = join25(productDir, relativeDirectory);
8058
8673
  for (const entry of readdirSync2(absoluteDirectory, { withFileTypes: true })) {
8059
8674
  if (!entry.isDirectory()) {
8060
8675
  continue;
8061
8676
  }
8062
8677
  const childPath = `${relativeDirectory}/${entry.name}`;
8063
- if (DEPRECATED_SPEC_NODE_SUFFIX_PATTERN.test(entry.name)) {
8678
+ if (SPEC_TREE_SUPERSEDED_NODE_SUFFIXES.some((suffix) => entry.name.endsWith(suffix))) {
8064
8679
  return childPath;
8065
8680
  }
8066
8681
  const nestedDeprecatedPath = visit2(childPath);
@@ -8070,13 +8685,13 @@ function findDeprecatedSpecNodePath(productDir) {
8070
8685
  }
8071
8686
  return void 0;
8072
8687
  }
8073
- const specTreeRootPath = join22(productDir, SPEC_TREE_ROOT);
8074
- if (!existsSync5(specTreeRootPath)) {
8688
+ const specTreeRootPath = join25(productDir, SPEC_TREE_ROOT);
8689
+ if (!existsSync4(specTreeRootPath)) {
8075
8690
  return void 0;
8076
8691
  }
8077
8692
  return visit2(SPEC_TREE_ROOT);
8078
8693
  }
8079
- function assertManifestEntries(productDir, file, entries, suffixPattern, suffixDescription) {
8694
+ function assertManifestEntries(productDir, file, entries, suffixPredicate, suffixDescription) {
8080
8695
  const duplicates = entries.filter((entry, index) => entries.indexOf(entry) !== index);
8081
8696
  if (duplicates.length > 0) {
8082
8697
  throw new Error(
@@ -8093,11 +8708,11 @@ function assertManifestEntries(productDir, file, entries, suffixPattern, suffixD
8093
8708
  if (entry.includes("..")) {
8094
8709
  throw new Error(`${file} entry must not contain '..': ${entry}`);
8095
8710
  }
8096
- if (!suffixPattern.test(entry)) {
8711
+ if (!suffixPredicate(entry)) {
8097
8712
  throw new Error(`${file} entry must ${suffixDescription}: ${entry}`);
8098
8713
  }
8099
- const absoluteEntry = join22(productDir, entry);
8100
- if (!existsSync5(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
8714
+ const absoluteEntry = join25(productDir, entry);
8715
+ if (!existsSync4(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
8101
8716
  throw new Error(`${file} entry does not exist as a directory: ${entry}`);
8102
8717
  }
8103
8718
  }
@@ -8186,7 +8801,7 @@ function validateTestLintDebtNodeManifest(productDir, entries) {
8186
8801
  productDir,
8187
8802
  TEST_LINT_DEBT_NODE_MANIFEST_FILE,
8188
8803
  entries,
8189
- SPEC_TREE_NODE_SUFFIX_PATTERN,
8804
+ isSpecTreeNodePath,
8190
8805
  "be a Spec Tree node path"
8191
8806
  );
8192
8807
  assertManifestDoesNotGrow(
@@ -8201,7 +8816,7 @@ function validateTestOwnedConstantDebtNodeManifest(productDir, entries) {
8201
8816
  productDir,
8202
8817
  TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_FILE,
8203
8818
  entries,
8204
- SPEC_TREE_NODE_SUFFIX_PATTERN,
8819
+ isSpecTreeNodePath,
8205
8820
  "be a Spec Tree node path"
8206
8821
  );
8207
8822
  assertManifestDoesNotGrow(
@@ -8355,9 +8970,9 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
8355
8970
  scope,
8356
8971
  scopeConfig: context.scopeConfig
8357
8972
  });
8358
- return new Promise((resolve6) => {
8359
- const localBin = join23(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
8360
- const binary = existsSync6(localBin) ? localBin : "npx";
8973
+ return new Promise((resolve5) => {
8974
+ const localBin = join26(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
8975
+ const binary = existsSync5(localBin) ? localBin : "npx";
8361
8976
  const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
8362
8977
  const eslintProcess = spawnManagedSubprocess(runner, binary, spawnArgs, {
8363
8978
  cwd: projectRoot
@@ -8365,13 +8980,13 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
8365
8980
  forwardValidationSubprocessOutput(eslintProcess, outputStreams);
8366
8981
  eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
8367
8982
  if (code === 0) {
8368
- resolve6({ success: true });
8983
+ resolve5({ success: true });
8369
8984
  } else {
8370
- resolve6({ success: false, error: `ESLint exited with code ${code}` });
8985
+ resolve5({ success: false, error: `ESLint exited with code ${code}` });
8371
8986
  }
8372
8987
  });
8373
8988
  eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
8374
- resolve6({ success: false, error: error.message });
8989
+ resolve5({ success: false, error: error.message });
8375
8990
  });
8376
8991
  });
8377
8992
  }
@@ -8458,7 +9073,7 @@ async function lintCommand(options) {
8458
9073
 
8459
9074
  // src/validation/literal/index.ts
8460
9075
  import { readFile as readFile8 } from "fs/promises";
8461
- import { isAbsolute as isAbsolute5, relative as relative7, resolve as resolve5 } from "path";
9076
+ import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve4 } from "path";
8462
9077
 
8463
9078
  // src/lib/file-inclusion/predicates/ignore-source.ts
8464
9079
  var IGNORE_SOURCE_LAYER = "ignore-source";
@@ -8493,7 +9108,7 @@ var ignoreSourceLayer = makeLayer(
8493
9108
 
8494
9109
  // src/lib/file-inclusion/pipeline.ts
8495
9110
  import { readdir as readdir7 } from "fs/promises";
8496
- import { join as join24, relative as relative6, sep as sep2 } from "path";
9111
+ import { join as join27, relative as relative4, sep as sep2 } from "path";
8497
9112
  var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
8498
9113
  function isNodeError3(err) {
8499
9114
  return err instanceof Error && "code" in err;
@@ -8511,11 +9126,11 @@ async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
8511
9126
  for (const entry of dirEntries) {
8512
9127
  if (entry.isDirectory()) {
8513
9128
  if (artifactDirs.has(entry.name)) continue;
8514
- const absolutePath = join24(absoluteDir, entry.name);
9129
+ const absolutePath = join27(absoluteDir, entry.name);
8515
9130
  await collectPaths(absolutePath, projectRoot, result, artifactDirs);
8516
9131
  } else if (entry.isFile()) {
8517
- const absolutePath = join24(absoluteDir, entry.name);
8518
- const rel = relative6(projectRoot, absolutePath);
9132
+ const absolutePath = join27(absoluteDir, entry.name);
9133
+ const rel = relative4(projectRoot, absolutePath);
8519
9134
  result.push(sep2 === "/" ? rel : rel.split(sep2).join("/"));
8520
9135
  }
8521
9136
  }
@@ -8921,8 +9536,8 @@ async function validateLiteralReuse(input) {
8921
9536
  const config = input.config ?? literalConfigDescriptor.defaults;
8922
9537
  const request = input.files ? {
8923
9538
  explicit: input.files.map((f) => {
8924
- const abs = isAbsolute5(f) ? f : resolve5(input.productDir, f);
8925
- return relative7(input.productDir, abs).split(/[\\/]/g).join("/");
9539
+ const abs = isAbsolute5(f) ? f : resolve4(input.productDir, f);
9540
+ return relative5(input.productDir, abs).split(/[\\/]/g).join("/");
8926
9541
  })
8927
9542
  } : { walkRoot: input.productDir };
8928
9543
  const scope = await runPipeline(
@@ -8933,7 +9548,7 @@ async function validateLiteralReuse(input) {
8933
9548
  EMPTY_IGNORE_READER
8934
9549
  );
8935
9550
  const filtered = applyPathFilter2(scope.included, input.pathConfig);
8936
- const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve5(input.productDir, entry.path));
9551
+ const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve4(input.productDir, entry.path));
8937
9552
  const collectOptions = {
8938
9553
  visitorKeys: defaultVisitorKeys,
8939
9554
  minStringLength: config.minStringLength,
@@ -8943,7 +9558,7 @@ async function validateLiteralReuse(input) {
8943
9558
  const testOccurrencesByFile = /* @__PURE__ */ new Map();
8944
9559
  const indexedOccurrencesByFile = /* @__PURE__ */ new Map();
8945
9560
  for (const abs of candidateFiles) {
8946
- const rel = relative7(input.productDir, abs).split(/[\\/]/g).join("/");
9561
+ const rel = relative5(input.productDir, abs).split(/[\\/]/g).join("/");
8947
9562
  const content = await readSafe(abs);
8948
9563
  if (content === null) continue;
8949
9564
  const occurrences = collectLiterals(content, rel, collectOptions);
@@ -9172,24 +9787,24 @@ function formatLoc(loc) {
9172
9787
  }
9173
9788
 
9174
9789
  // src/validation/steps/typescript.ts
9175
- import { existsSync as existsSync7, mkdirSync, rmSync, writeFileSync } from "fs";
9790
+ import { existsSync as existsSync6, mkdirSync, rmSync, writeFileSync } from "fs";
9176
9791
  import { mkdtemp as mkdtemp2 } from "fs/promises";
9177
- import { isAbsolute as isAbsolute6, join as join25 } from "path";
9792
+ import { isAbsolute as isAbsolute6, join as join28 } from "path";
9178
9793
  var defaultTypeScriptProcessRunner = lifecycleProcessRunner;
9179
9794
  var defaultTypeScriptDeps = {
9180
9795
  mkdtemp: mkdtemp2,
9181
9796
  mkdirSync,
9182
9797
  writeFileSync,
9183
9798
  rmSync,
9184
- existsSync: existsSync7
9799
+ existsSync: existsSync6
9185
9800
  };
9186
9801
  var TEMPORARY_TSCONFIG_COMPILER_OPTIONS = { noEmit: true };
9187
9802
  var TEMPORARY_TSCONFIG_PARENT_SEGMENTS = ["node_modules", ".cache", "spx"];
9188
9803
  var TEMPORARY_TSCONFIG_DIR_PREFIX = "validate-ts-";
9189
9804
  async function createTemporaryTsconfigDir(projectRoot, deps) {
9190
- const parent = join25(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
9805
+ const parent = join28(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
9191
9806
  deps.mkdirSync(parent, { recursive: true });
9192
- return deps.mkdtemp(join25(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
9807
+ return deps.mkdtemp(join28(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
9193
9808
  }
9194
9809
  function buildTypeScriptArgs(context) {
9195
9810
  const { scope, configFile } = context;
@@ -9197,11 +9812,11 @@ function buildTypeScriptArgs(context) {
9197
9812
  }
9198
9813
  async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defaultTypeScriptDeps) {
9199
9814
  const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
9200
- const configPath = join25(tempDir, "tsconfig.json");
9815
+ const configPath = join28(tempDir, "tsconfig.json");
9201
9816
  const baseConfigFile = TSCONFIG_FILES[scope];
9202
- const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join25(projectRoot, file));
9817
+ const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join28(projectRoot, file));
9203
9818
  const tempConfig = {
9204
- extends: join25(projectRoot, baseConfigFile),
9819
+ extends: join28(projectRoot, baseConfigFile),
9205
9820
  files: absoluteFiles,
9206
9821
  include: [],
9207
9822
  exclude: [],
@@ -9218,11 +9833,11 @@ async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defa
9218
9833
  }
9219
9834
  async function createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps = defaultTypeScriptDeps) {
9220
9835
  const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
9221
- const configPath = join25(tempDir, "tsconfig.json");
9836
+ const configPath = join28(tempDir, "tsconfig.json");
9222
9837
  const baseConfigFile = TSCONFIG_FILES[scope];
9223
- const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join25(projectRoot, pattern);
9838
+ const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join28(projectRoot, pattern);
9224
9839
  const tempConfig = {
9225
- extends: join25(projectRoot, baseConfigFile),
9840
+ extends: join28(projectRoot, baseConfigFile),
9226
9841
  include: scopeConfig.filePatterns.map(toProjectPathPattern),
9227
9842
  exclude: scopeConfig.excludePatterns.map(toProjectPathPattern),
9228
9843
  compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
@@ -9249,8 +9864,8 @@ async function validateTypeScript(context, options = {}) {
9249
9864
  if (files && files.length > 0) {
9250
9865
  const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, projectRoot, deps);
9251
9866
  try {
9252
- return await new Promise((resolve6) => {
9253
- const tscBin = join25(projectRoot, "node_modules", ".bin", "tsc");
9867
+ return await new Promise((resolve5) => {
9868
+ const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
9254
9869
  const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
9255
9870
  const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
9256
9871
  const tscProcess = spawnManagedSubprocess(runner, tscBinary, tscArgs2, {
@@ -9260,14 +9875,14 @@ async function validateTypeScript(context, options = {}) {
9260
9875
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
9261
9876
  cleanup();
9262
9877
  if (code === 0) {
9263
- resolve6({ success: true, skipped: false });
9878
+ resolve5({ success: true, skipped: false });
9264
9879
  } else {
9265
- resolve6({ success: false, error: `TypeScript exited with code ${code}` });
9880
+ resolve5({ success: false, error: `TypeScript exited with code ${code}` });
9266
9881
  }
9267
9882
  });
9268
9883
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
9269
9884
  cleanup();
9270
- resolve6({ success: false, error: error.message });
9885
+ resolve5({ success: false, error: error.message });
9271
9886
  });
9272
9887
  });
9273
9888
  } catch (error) {
@@ -9283,10 +9898,10 @@ async function validateTypeScript(context, options = {}) {
9283
9898
  return { success: true, skipped: true };
9284
9899
  }
9285
9900
  const { configPath, cleanup } = await createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps);
9286
- const tscBin = join25(projectRoot, "node_modules", ".bin", "tsc");
9901
+ const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
9287
9902
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
9288
9903
  tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
9289
- return new Promise((resolve6) => {
9904
+ return new Promise((resolve5) => {
9290
9905
  const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
9291
9906
  cwd: projectRoot
9292
9907
  });
@@ -9294,36 +9909,36 @@ async function validateTypeScript(context, options = {}) {
9294
9909
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
9295
9910
  cleanup();
9296
9911
  if (code === 0) {
9297
- resolve6({ success: true, skipped: false });
9912
+ resolve5({ success: true, skipped: false });
9298
9913
  } else {
9299
- resolve6({ success: false, error: `TypeScript exited with code ${code}` });
9914
+ resolve5({ success: false, error: `TypeScript exited with code ${code}` });
9300
9915
  }
9301
9916
  });
9302
9917
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
9303
9918
  cleanup();
9304
- resolve6({ success: false, error: error.message });
9919
+ resolve5({ success: false, error: error.message });
9305
9920
  });
9306
9921
  });
9307
9922
  } else {
9308
- const tscBin = join25(projectRoot, "node_modules", ".bin", "tsc");
9923
+ const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
9309
9924
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
9310
9925
  const rawArgs = buildTypeScriptArgs({ scope, configFile });
9311
9926
  tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
9312
9927
  }
9313
- return new Promise((resolve6) => {
9928
+ return new Promise((resolve5) => {
9314
9929
  const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
9315
9930
  cwd: projectRoot
9316
9931
  });
9317
9932
  forwardValidationSubprocessOutput(tscProcess, outputStreams);
9318
9933
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
9319
9934
  if (code === 0) {
9320
- resolve6({ success: true, skipped: false });
9935
+ resolve5({ success: true, skipped: false });
9321
9936
  } else {
9322
- resolve6({ success: false, error: `TypeScript exited with code ${code}` });
9937
+ resolve5({ success: false, error: `TypeScript exited with code ${code}` });
9323
9938
  }
9324
9939
  });
9325
9940
  tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
9326
- resolve6({ success: false, error: error.message });
9941
+ resolve5({ success: false, error: error.message });
9327
9942
  });
9328
9943
  });
9329
9944
  }
@@ -9565,7 +10180,7 @@ function truncate(value) {
9565
10180
 
9566
10181
  // src/validation/literal/allowlist-existing.ts
9567
10182
  import { rename as rename4, writeFile as writeFile4 } from "fs/promises";
9568
- import { dirname as dirname5, join as join26 } from "path";
10183
+ import { dirname as dirname7, join as join29 } from "path";
9569
10184
  var EXIT_OK = 0;
9570
10185
  var EXIT_ERROR = 1;
9571
10186
  var INCLUDE_FIELD = "include";
@@ -9585,9 +10200,9 @@ var productionReader = {
9585
10200
  };
9586
10201
  var productionWriter = {
9587
10202
  async write(filePath, content) {
9588
- const dir = dirname5(filePath);
10203
+ const dir = dirname7(filePath);
9589
10204
  const random = Math.random().toString(RANDOM_BASE).slice(RANDOM_PREFIX_SLICE, RANDOM_PREFIX_SLICE + RANDOM_TOKEN_LENGTH);
9590
- const tmpPath = join26(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
10205
+ const tmpPath = join29(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
9591
10206
  await writeFile4(tmpPath, content, "utf8");
9592
10207
  await rename4(tmpPath, filePath);
9593
10208
  }
@@ -9910,8 +10525,8 @@ var validationDomain = {
9910
10525
 
9911
10526
  // src/interfaces/cli/registry.ts
9912
10527
  var CLI_DOMAINS = [
9913
- auditDomain,
9914
10528
  claudeDomain,
10529
+ compactDomain,
9915
10530
  configDomain,
9916
10531
  sessionDomain,
9917
10532
  specDomain,