@outcomeeng/spx 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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 = {}) {
@@ -891,20 +633,870 @@ function registerClaudeCommands(claudeCmd) {
891
633
  process.exit(1);
892
634
  }
893
635
  }
894
- );
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
+ WORKTREES_SCOPE: "worktrees",
1050
+ RUNS_DIR: "runs",
1051
+ RUN_FILE_PREFIX: "run-",
1052
+ JSONL_EXTENSION: ".jsonl"
1053
+ };
1054
+ var STATE_STORE_DOMAIN = {
1055
+ AUDIT: "audit",
1056
+ COMPACT: "compact",
1057
+ REVIEW: "review",
1058
+ TEST: "test"
1059
+ };
1060
+ var STATE_STORE_ERROR = {
1061
+ INVALID_TOKEN: "state-store token must be a safe path segment",
1062
+ INVALID_BRANCH_SLUG: "state-store branch slug must be normalized before storage",
1063
+ RUN_FILE_CREATE_FAILED: "state-store run file create failed",
1064
+ RUN_FILE_COLLISION_LIMIT: "state-store run file collision limit exhausted",
1065
+ RECORD_ALREADY_EXISTS: "state-store record already exists",
1066
+ RECORD_WRITE_FAILED: "state-store record write failed",
1067
+ RECORD_READ_FAILED: "state-store record read failed"
1068
+ };
1069
+ var HEX_ENCODING = "hex";
1070
+ var RUN_ID_BYTES = 6;
1071
+ var RUN_FILE_CREATE_ATTEMPTS = 10;
1072
+ var RUN_TIMESTAMP_SEPARATOR = "_";
1073
+ var RUN_TIMESTAMP_MILLISECOND_DIGITS = 3;
1074
+ var SLUG_SEPARATOR = "-";
1075
+ var EMPTY_STRING = "";
1076
+ var RUN_TOKEN_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[a-f0-9]{12}$/;
1077
+ var TOKEN_PATTERN = /^[A-Za-z0-9_-]+$/;
1078
+ var EXCLUSIVE_CREATE_FLAG = "wx";
1079
+ var WRITE_EXISTING_FLAG = "r+";
1080
+ var ERROR_CODE_FILE_EXISTS = "EEXIST";
1081
+ var ERROR_CODE_NOT_FOUND = "ENOENT";
1082
+ var JSONL_LINE_SEPARATOR = "\n";
1083
+ var ERROR_DETAIL_SEPARATOR = ": ";
1084
+ var defaultFileSystem = {
1085
+ mkdir: async (path6, options) => {
1086
+ await nodeMkdir(path6, options);
1087
+ },
1088
+ writeFile: nodeWriteFile,
1089
+ appendFile: nodeAppendFile,
1090
+ readFile: nodeReadFile,
1091
+ readdir: nodeReaddir
1092
+ };
1093
+ function validateScopeToken(token) {
1094
+ return TOKEN_PATTERN.test(token) ? { ok: true, value: token } : { ok: false, error: STATE_STORE_ERROR.INVALID_TOKEN };
1095
+ }
1096
+ async function resolveWorktreeScopeDir(options = {}) {
1097
+ const gitResult = await detectWorktreeProductRoot(options.cwd, options.deps);
1098
+ return worktreeScopeDir(gitResult.productDir);
1099
+ }
1100
+ async function resolveSessionsScopeDir(options = {}) {
1101
+ const gitResult = await detectGitCommonDirProductRoot(options.cwd, options.deps);
1102
+ return {
1103
+ sessionsDir: sessionsScopeDir(gitResult.productDir),
1104
+ warning: gitResult.warning
1105
+ };
1106
+ }
1107
+ function worktreeScopeDir(productDir) {
1108
+ return { ok: true, value: join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.WORKTREE_SCOPE) };
1109
+ }
1110
+ function sessionsScopeDir(productDir) {
1111
+ return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
1112
+ }
1113
+ async function resolveWorktreesScopeDir(options = {}) {
1114
+ const gitResult = await detectGitCommonDirProductRoot(options.cwd, options.deps);
1115
+ return {
1116
+ worktreesDir: worktreesScopeDir(gitResult.productDir),
1117
+ warning: gitResult.warning
1118
+ };
1119
+ }
1120
+ function worktreesScopeDir(productDir) {
1121
+ return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.WORKTREES_SCOPE);
1122
+ }
1123
+ function composeScopeDir(baseScopeDir, ...tokens) {
1124
+ const segments = [];
1125
+ for (const token of tokens) {
1126
+ const validated = validateScopeToken(token);
1127
+ if (!validated.ok) return validated;
1128
+ segments.push(validated.value);
1129
+ }
1130
+ return { ok: true, value: join2(baseScopeDir, ...segments) };
1131
+ }
1132
+ function domainDir(scopeDir, domainName) {
1133
+ return composeScopeDir(scopeDir, domainName);
1134
+ }
1135
+ function runsDir(scopeDir, domainName) {
1136
+ const domain = domainDir(scopeDir, domainName);
1137
+ if (!domain.ok) return domain;
1138
+ return { ok: true, value: join2(domain.value, STATE_STORE_PATH.RUNS_DIR) };
1139
+ }
1140
+ function formatRunTimestamp(date) {
1141
+ const year = date.getUTCFullYear();
1142
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
1143
+ const day = String(date.getUTCDate()).padStart(2, "0");
1144
+ const hours = String(date.getUTCHours()).padStart(2, "0");
1145
+ const minutes = String(date.getUTCMinutes()).padStart(2, "0");
1146
+ const seconds = String(date.getUTCSeconds()).padStart(2, "0");
1147
+ const milliseconds = String(date.getUTCMilliseconds()).padStart(RUN_TIMESTAMP_MILLISECOND_DIGITS, "0");
1148
+ return `${year}-${month}-${day}${RUN_TIMESTAMP_SEPARATOR}${hours}-${minutes}-${seconds}-${milliseconds}`;
1149
+ }
1150
+ function generateRunId(randomBytes = nodeRandomBytes) {
1151
+ return randomBytes(RUN_ID_BYTES).toString(HEX_ENCODING);
1152
+ }
1153
+ function runFileName(runToken) {
1154
+ return `${STATE_STORE_PATH.RUN_FILE_PREFIX}${runToken}${STATE_STORE_PATH.JSONL_EXTENSION}`;
1155
+ }
1156
+ function isRunFileName(name) {
1157
+ return name.startsWith(STATE_STORE_PATH.RUN_FILE_PREFIX) && name.endsWith(STATE_STORE_PATH.JSONL_EXTENSION) && RUN_TOKEN_PATTERN.test(name.slice(
1158
+ STATE_STORE_PATH.RUN_FILE_PREFIX.length,
1159
+ -STATE_STORE_PATH.JSONL_EXTENSION.length
1160
+ ));
1161
+ }
1162
+ async function createJsonlRunFile(scopeDir, domainName, options = {}) {
1163
+ const fs7 = options.fs ?? defaultFileSystem;
1164
+ const domainRunsDir = runsDir(scopeDir, domainName);
1165
+ if (!domainRunsDir.ok) return domainRunsDir;
1166
+ const maxAttempts = options.maxAttempts ?? RUN_FILE_CREATE_ATTEMPTS;
1167
+ const startedDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
1168
+ const startedAt = formatRunTimestamp(startedDate);
1169
+ const randomBytes = options.randomBytes ?? nodeRandomBytes;
1170
+ try {
1171
+ await fs7.mkdir(domainRunsDir.value, { recursive: true });
1172
+ } catch (error) {
1173
+ return {
1174
+ ok: false,
1175
+ error: formatStateStoreError(STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED, toErrorMessage(error))
1176
+ };
1177
+ }
1178
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
1179
+ const runId = generateRunId(randomBytes);
1180
+ const runToken = `${startedAt}${SLUG_SEPARATOR}${runId}`;
1181
+ const name = runFileName(runToken);
1182
+ const path6 = join2(domainRunsDir.value, name);
1183
+ try {
1184
+ await fs7.writeFile(path6, EMPTY_STRING, { flag: EXCLUSIVE_CREATE_FLAG });
1185
+ return {
1186
+ ok: true,
1187
+ value: { runsDir: domainRunsDir.value, runFilePath: path6, runFileName: name, runToken, runId, startedAt }
1188
+ };
1189
+ } catch (error) {
1190
+ if (hasErrorCode(error, ERROR_CODE_FILE_EXISTS)) continue;
1191
+ return {
1192
+ ok: false,
1193
+ error: formatStateStoreError(STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED, toErrorMessage(error))
1194
+ };
1195
+ }
1196
+ }
1197
+ return { ok: false, error: STATE_STORE_ERROR.RUN_FILE_COLLISION_LIMIT };
1198
+ }
1199
+ async function writeJsonlRunRecord(runFilePath, record, options = {}) {
1200
+ const fs7 = options.fs ?? defaultFileSystem;
1201
+ try {
1202
+ const existing = await fs7.readFile(runFilePath, "utf8");
1203
+ if (existing.trim().length > 0) return { ok: false, error: STATE_STORE_ERROR.RECORD_ALREADY_EXISTS };
1204
+ } catch (error) {
1205
+ if (!hasErrorCode(error, ERROR_CODE_NOT_FOUND)) {
1206
+ return {
1207
+ ok: false,
1208
+ error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
1209
+ };
1210
+ }
1211
+ }
1212
+ try {
1213
+ await fs7.writeFile(runFilePath, serializeJsonlRecord(record), { flag: WRITE_EXISTING_FLAG });
1214
+ return { ok: true, value: runFilePath };
1215
+ } catch (error) {
1216
+ return {
1217
+ ok: false,
1218
+ error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
1219
+ };
1220
+ }
1221
+ }
1222
+ async function appendJsonlRecord(filePath, record, options = {}) {
1223
+ const fs7 = options.fs ?? defaultFileSystem;
1224
+ try {
1225
+ await fs7.mkdir(dirname2(filePath), { recursive: true });
1226
+ await fs7.appendFile(filePath, serializeJsonlRecord(record));
1227
+ return { ok: true, value: filePath };
1228
+ } catch (error) {
1229
+ return {
1230
+ ok: false,
1231
+ error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
1232
+ };
1233
+ }
1234
+ }
1235
+ async function readLatestJsonlRecord(filePath, options = {}) {
1236
+ const fs7 = options.fs ?? defaultFileSystem;
1237
+ let content;
1238
+ try {
1239
+ content = await fs7.readFile(filePath, "utf8");
1240
+ } catch (error) {
1241
+ if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return { ok: true, value: void 0 };
1242
+ return {
1243
+ ok: false,
1244
+ error: formatStateStoreError(STATE_STORE_ERROR.RECORD_READ_FAILED, toErrorMessage(error))
1245
+ };
1246
+ }
1247
+ for (const line of nonEmptyJsonlLinesNewestFirst(content)) {
1248
+ try {
1249
+ return { ok: true, value: JSON.parse(line) };
1250
+ } catch {
1251
+ continue;
1252
+ }
1253
+ }
1254
+ return { ok: true, value: void 0 };
1255
+ }
1256
+ function latestNonEmptyJsonlLine(content) {
1257
+ return nonEmptyJsonlLinesNewestFirst(content)[0];
1258
+ }
1259
+ function compareAsciiStrings(left, right) {
1260
+ if (left < right) return -1;
1261
+ if (left > right) return 1;
1262
+ return 0;
1263
+ }
1264
+ function formatStateStoreError(code, detail) {
1265
+ return detail === void 0 ? code : `${code}${ERROR_DETAIL_SEPARATOR}${detail}`;
1266
+ }
1267
+ function parseStateStoreError(error) {
1268
+ for (const code of Object.values(STATE_STORE_ERROR)) {
1269
+ if (error === code) return { code };
1270
+ const prefix = `${code}${ERROR_DETAIL_SEPARATOR}`;
1271
+ if (error.startsWith(prefix)) return { code, detail: error.slice(prefix.length) };
1272
+ }
1273
+ return void 0;
1274
+ }
1275
+ function serializeJsonlRecord(record) {
1276
+ return `${JSON.stringify(record)}${JSONL_LINE_SEPARATOR}`;
1277
+ }
1278
+ function nonEmptyJsonlLinesNewestFirst(content) {
1279
+ const lines = content.split(JSONL_LINE_SEPARATOR);
1280
+ const latestLines = [];
1281
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1282
+ const line = lines[index]?.trim() ?? EMPTY_STRING;
1283
+ if (line.length > 0) latestLines.push(line);
1284
+ }
1285
+ return latestLines;
1286
+ }
1287
+ function hasErrorCode(error, code) {
1288
+ return typeof error === "object" && error !== null && !Array.isArray(error) && "code" in error && error.code === code;
1289
+ }
1290
+ function toErrorMessage(error) {
1291
+ return error instanceof Error ? error.message : String(error);
1292
+ }
1293
+
1294
+ // src/domains/compact/index.ts
1295
+ var COMPACT_STORE_PATH = {
1296
+ STASH_FILE: "stash.jsonl"
1297
+ };
1298
+ var COMPACT_RECORD_FIELDS = {
1299
+ ACTIVE_NODE: "active_node",
1300
+ HAS_FOUNDATION: "has_foundation"
1301
+ };
1302
+ var COMPACT_MARKER = {
1303
+ FOUNDATION: "SPEC_TREE_FOUNDATION",
1304
+ CONTEXT: "SPEC_TREE_CONTEXT",
1305
+ TARGET_ATTRIBUTE: "target",
1306
+ ESCAPED_TARGET_QUOTE: '\\"',
1307
+ UNESCAPED_TARGET_QUOTE: '"'
1308
+ };
1309
+ var COMPACT_ERROR = {
1310
+ RECORD_SHAPE_INVALID: "compact record shape invalid"
1311
+ };
1312
+ var EMPTY_ACTIVE_NODE = "";
1313
+ var JSONL_LINE_SEPARATOR2 = "\n";
1314
+ var ATTRIBUTE_ASSIGNMENT = "=";
1315
+ var ESCAPE_CHARACTER = "\\";
1316
+ var CONTEXT_TARGET_PREFIX = `${COMPACT_MARKER.TARGET_ATTRIBUTE}${ATTRIBUTE_ASSIGNMENT}`;
1317
+ var NODE_PATH_PREFIX = "spx/";
1318
+ function resolveCompactSessionToken(sessionId, env) {
1319
+ if (sessionId !== void 0 && sessionId.length > 0) {
1320
+ return normalizeAgentSessionToken(sessionId);
1321
+ }
1322
+ return resolveAgentSessionId(env);
1323
+ }
1324
+ function extractCompactRecord(transcript) {
1325
+ let hasFoundation = false;
1326
+ let activeNode = EMPTY_ACTIVE_NODE;
1327
+ for (const value of transcriptStringValues(transcript)) {
1328
+ if (value.includes(COMPACT_MARKER.FOUNDATION)) hasFoundation = true;
1329
+ activeNode = extractLastContextTarget(value) ?? activeNode;
1330
+ }
1331
+ if (!hasFoundation) return void 0;
1332
+ return {
1333
+ [COMPACT_RECORD_FIELDS.ACTIVE_NODE]: activeNode,
1334
+ [COMPACT_RECORD_FIELDS.HAS_FOUNDATION]: true
1335
+ };
1336
+ }
1337
+ function compactStashPath(worktreeScopeDir2, sessionToken) {
1338
+ const compactScope = composeScopeDir(worktreeScopeDir2, sessionToken, STATE_STORE_DOMAIN.COMPACT);
1339
+ if (!compactScope.ok) return compactScope;
1340
+ return { ok: true, value: join3(compactScope.value, COMPACT_STORE_PATH.STASH_FILE) };
1341
+ }
1342
+ function parseCompactRecord(value) {
1343
+ 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) {
1344
+ return {
1345
+ ok: true,
1346
+ value: {
1347
+ active_node: value[COMPACT_RECORD_FIELDS.ACTIVE_NODE] ?? EMPTY_ACTIVE_NODE,
1348
+ has_foundation: true
1349
+ }
1350
+ };
1351
+ }
1352
+ return { ok: false, error: COMPACT_ERROR.RECORD_SHAPE_INVALID };
1353
+ }
1354
+ function transcriptStringValues(transcript) {
1355
+ const values = [];
1356
+ for (const rawLine of transcript.split(JSONL_LINE_SEPARATOR2)) {
1357
+ const line = rawLine.trim();
1358
+ if (line.length === 0) continue;
1359
+ try {
1360
+ collectStringValues(JSON.parse(line), values);
1361
+ } catch {
1362
+ continue;
1363
+ }
1364
+ }
1365
+ return values;
895
1366
  }
896
- var claudeDomain = {
897
- name: "claude",
898
- description: "Manage Claude Code settings and plugins",
1367
+ function collectStringValues(value, values) {
1368
+ if (typeof value === "string") {
1369
+ values.push(value);
1370
+ return;
1371
+ }
1372
+ if (Array.isArray(value)) {
1373
+ for (const entry of value) collectStringValues(entry, values);
1374
+ return;
1375
+ }
1376
+ if (typeof value === "object" && value !== null) {
1377
+ for (const entry of Object.values(value)) collectStringValues(entry, values);
1378
+ }
1379
+ }
1380
+ function extractLastContextTarget(value) {
1381
+ let activeNode;
1382
+ let searchFrom = 0;
1383
+ while (searchFrom < value.length) {
1384
+ const contextIndex = value.indexOf(COMPACT_MARKER.CONTEXT, searchFrom);
1385
+ if (contextIndex < 0) return activeNode;
1386
+ const target = extractContextTargetAt(value, contextIndex + COMPACT_MARKER.CONTEXT.length);
1387
+ if (target !== void 0) activeNode = target;
1388
+ searchFrom = contextIndex + COMPACT_MARKER.CONTEXT.length;
1389
+ }
1390
+ return activeNode;
1391
+ }
1392
+ function extractContextTargetAt(value, searchFrom) {
1393
+ const targetIndex = value.indexOf(CONTEXT_TARGET_PREFIX, searchFrom);
1394
+ if (targetIndex < 0) return void 0;
1395
+ const quoteIndex = findOpeningTargetQuote(value, targetIndex + CONTEXT_TARGET_PREFIX.length);
1396
+ if (quoteIndex === void 0) return void 0;
1397
+ const target = readTargetPath(value, quoteIndex + COMPACT_MARKER.UNESCAPED_TARGET_QUOTE.length);
1398
+ return target.startsWith(NODE_PATH_PREFIX) ? target : void 0;
1399
+ }
1400
+ function findOpeningTargetQuote(value, startIndex) {
1401
+ let index = startIndex;
1402
+ while (value[index] === ESCAPE_CHARACTER) index += 1;
1403
+ return value[index] === COMPACT_MARKER.UNESCAPED_TARGET_QUOTE ? index : void 0;
1404
+ }
1405
+ function readTargetPath(value, startIndex) {
1406
+ let endIndex = startIndex;
1407
+ while (endIndex < value.length && isTargetPathCharacter(value[endIndex] ?? EMPTY_ACTIVE_NODE)) {
1408
+ endIndex += 1;
1409
+ }
1410
+ return value.slice(startIndex, endIndex);
1411
+ }
1412
+ function isTargetPathCharacter(character) {
1413
+ const code = character.charCodeAt(0);
1414
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || character === "." || character === "_" || character === "-" || character === "/";
1415
+ }
1416
+
1417
+ // src/commands/compact/retrieve.ts
1418
+ var EMPTY_OUTPUT = "";
1419
+ async function compactRetrieveCommand(options) {
1420
+ const sessionToken = resolveCompactSessionToken(options.sessionId, options.env ?? process.env);
1421
+ if (sessionToken === void 0) return { exitCode: 1, output: EMPTY_OUTPUT };
1422
+ const worktreeScope = await resolveWorktreeScopeDir({ cwd: options.cwd });
1423
+ if (!worktreeScope.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
1424
+ const stashPath = compactStashPath(worktreeScope.value, sessionToken);
1425
+ if (!stashPath.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
1426
+ const latest = await readLatestJsonlRecord(stashPath.value);
1427
+ if (!latest.ok || latest.value === void 0) return { exitCode: 1, output: EMPTY_OUTPUT };
1428
+ const record = parseCompactRecord(latest.value);
1429
+ if (!record.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
1430
+ return { exitCode: 0, output: `${JSON.stringify(record.value)}
1431
+ ` };
1432
+ }
1433
+
1434
+ // src/commands/compact/store.ts
1435
+ import { readFile } from "fs/promises";
1436
+ var UTF8_ENCODING = "utf8";
1437
+ async function compactStoreCommand(options) {
1438
+ const sessionToken = resolveCompactSessionToken(options.sessionId, options.env ?? process.env);
1439
+ if (sessionToken === void 0) return 1;
1440
+ let transcript;
1441
+ try {
1442
+ transcript = await readFile(options.transcript, UTF8_ENCODING);
1443
+ } catch {
1444
+ return 1;
1445
+ }
1446
+ const record = extractCompactRecord(transcript);
1447
+ if (record === void 0) return 0;
1448
+ const worktreeScope = await resolveWorktreeScopeDir({ cwd: options.cwd });
1449
+ if (!worktreeScope.ok) return 1;
1450
+ const stashPath = compactStashPath(worktreeScope.value, sessionToken);
1451
+ if (!stashPath.ok) return 1;
1452
+ const written = await appendJsonlRecord(stashPath.value, record);
1453
+ return written.ok ? 0 : 1;
1454
+ }
1455
+
1456
+ // src/interfaces/cli/compact.ts
1457
+ var COMPACT_CLI = {
1458
+ commandName: "compact",
1459
+ storeCommandName: "store",
1460
+ retrieveCommandName: "retrieve",
1461
+ description: "Store and retrieve compact resume state",
1462
+ transcriptFlag: "--transcript",
1463
+ transcriptOption: "--transcript <path>",
1464
+ sessionIdFlag: "--session-id",
1465
+ sessionIdOption: "--session-id <id>",
1466
+ sessionIdDescription: "Agent session identity (overrides the agent-session environment)"
1467
+ };
1468
+ var compactDomain = {
1469
+ name: COMPACT_CLI.commandName,
1470
+ description: COMPACT_CLI.description,
899
1471
  register: (program2) => {
900
- const claudeCmd = program2.command("claude").description("Manage Claude Code settings and plugins");
901
- registerClaudeCommands(claudeCmd);
1472
+ const compactCmd = program2.command(COMPACT_CLI.commandName).description(COMPACT_CLI.description);
1473
+ 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) => {
1474
+ process.exit(
1475
+ await compactStoreCommand({
1476
+ transcript: options.transcript,
1477
+ sessionId: options.sessionId,
1478
+ cwd: process.cwd(),
1479
+ env: process.env
1480
+ })
1481
+ );
1482
+ });
1483
+ compactCmd.command(COMPACT_CLI.retrieveCommandName).description("Retrieve compact resume state").option(COMPACT_CLI.sessionIdOption, COMPACT_CLI.sessionIdDescription).action(async (options) => {
1484
+ const result = await compactRetrieveCommand({
1485
+ sessionId: options.sessionId,
1486
+ cwd: process.cwd(),
1487
+ env: process.env
1488
+ });
1489
+ if (result.output.length > 0) {
1490
+ process.stdout.write(result.output);
1491
+ }
1492
+ process.exitCode = result.exitCode;
1493
+ });
902
1494
  }
903
1495
  };
904
1496
 
905
1497
  // src/config/index.ts
906
1498
  import { readFile as readFile2 } from "fs/promises";
907
- import { join as join2 } from "path";
1499
+ import { join as join5 } from "path";
908
1500
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
909
1501
  import { parse as parseYaml, parseDocument as parseYamlDocument, stringify as stringifyYaml } from "yaml";
910
1502
 
@@ -1415,57 +2007,20 @@ function applyPathFilter(paths, config) {
1415
2007
  // src/domains/audit/config.ts
1416
2008
  var AUDIT_SECTION = "audit";
1417
2009
  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
2010
  BASE_REF: "baseRef",
1427
- BRANCH_SLUG: "branchSlug",
1428
- MAX_BYTES: "maxBytes",
1429
2011
  AUDITORS: "auditors",
1430
2012
  TARGETS: "targets"
1431
2013
  };
1432
- var AUDIT_BRANCH_SLUG_MIN_MAX_BYTES = 10;
1433
2014
  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
2015
  baseRef: "main",
1444
- branchSlug: {
1445
- maxBytes: 120
1446
- },
1447
2016
  auditors: [],
1448
2017
  targets: resolveDefaultTargets()
1449
2018
  };
1450
2019
  var AUDIT_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
1451
- AUDIT_CONFIG_FIELDS.STORAGE,
1452
2020
  AUDIT_CONFIG_FIELDS.BASE_REF,
1453
- AUDIT_CONFIG_FIELDS.BRANCH_SLUG,
1454
2021
  AUDIT_CONFIG_FIELDS.AUDITORS,
1455
2022
  AUDIT_CONFIG_FIELDS.TARGETS
1456
2023
  ]);
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
2024
  var auditConfigDescriptor = {
1470
2025
  section: AUDIT_SECTION,
1471
2026
  defaults: DEFAULT_AUDIT_CONFIG,
@@ -1485,15 +2040,11 @@ function validateAuditConfig(raw) {
1485
2040
  }
1486
2041
  const unknown = rejectUnknownFields2(AUDIT_SECTION, raw, AUDIT_ALLOWED_FIELDS);
1487
2042
  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
2043
  const baseRef = raw[AUDIT_CONFIG_FIELDS.BASE_REF] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.baseRef } : validateNonEmptyString2(
1491
2044
  `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BASE_REF}`,
1492
2045
  raw[AUDIT_CONFIG_FIELDS.BASE_REF]
1493
2046
  );
1494
2047
  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
2048
  const auditors = raw[AUDIT_CONFIG_FIELDS.AUDITORS] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.auditors } : validateArrayOfNonEmptyStrings(
1498
2049
  `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.AUDITORS}`,
1499
2050
  raw[AUDIT_CONFIG_FIELDS.AUDITORS]
@@ -1507,61 +2058,12 @@ function validateAuditConfig(raw) {
1507
2058
  return {
1508
2059
  ok: true,
1509
2060
  value: {
1510
- storage: storage.value,
1511
2061
  baseRef: baseRef.value,
1512
- branchSlug: branchSlug.value,
1513
2062
  auditors: auditors.value,
1514
2063
  targets: targets.value
1515
2064
  }
1516
2065
  };
1517
2066
  }
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
2067
  function validateNonEmptyString2(path6, value) {
1566
2068
  if (typeof value !== "string" || value.length === 0) {
1567
2069
  return { ok: false, error: `${path6} must be a non-empty string` };
@@ -1645,6 +2147,112 @@ var DECISION_KINDS = Object.keys(KIND_REGISTRY).filter(
1645
2147
  );
1646
2148
  var NODE_SUFFIXES = NODE_KINDS.map((k) => KIND_REGISTRY[k].suffix);
1647
2149
  var DECISION_SUFFIXES = DECISION_KINDS.map((k) => KIND_REGISTRY[k].suffix);
2150
+ var SPEC_TREE_GRAMMAR = {
2151
+ PRODUCT_SUFFIX: SPEC_TREE_CONFIG.PRODUCT.SUFFIX,
2152
+ EVIDENCE: {
2153
+ DIRECTORY_NAME: "tests",
2154
+ MODES: ["scenario", "mapping", "conformance", "property", "compliance"],
2155
+ LEVELS: ["l1", "l2", "l3"],
2156
+ TAILS: {
2157
+ TYPESCRIPT: ["test", "ts"],
2158
+ PYTHON: ["py"],
2159
+ RUST: ["rs"]
2160
+ },
2161
+ SEGMENT_SEPARATOR: "."
2162
+ },
2163
+ RUNNERS: ["vitest", "playwright", "subprocess"],
2164
+ ORDER: {
2165
+ SEPARATOR: "-",
2166
+ PATTERN: /^\d+$/
2167
+ },
2168
+ PATH_SEPARATOR: "/",
2169
+ COORDINATION_NOTES: ["PLAN.md", "ISSUES.md"],
2170
+ EVAL_LANE: ["eval.toml", "cases.jsonl", "prompt.md", "history.jsonl", "runs"],
2171
+ PRIOR_NODE_SUFFIXES: [".capability", ".feature", ".story"]
2172
+ };
2173
+ var SPEC_TREE_EVIDENCE_FILE = SPEC_TREE_GRAMMAR.EVIDENCE;
2174
+ var NAMING_SCHEMA_VERSION_ID = {
2175
+ PRIOR: "1.0.0",
2176
+ CANONICAL: "2.0.0"
2177
+ };
2178
+ function namingSchemaVersionFromNodeSuffixes(version2, nodeSuffixes) {
2179
+ return {
2180
+ version: version2,
2181
+ nodeSuffixes,
2182
+ decisionSuffixes: DECISION_SUFFIXES,
2183
+ productSuffix: SPEC_TREE_GRAMMAR.PRODUCT_SUFFIX,
2184
+ evidence: SPEC_TREE_GRAMMAR.EVIDENCE,
2185
+ runners: SPEC_TREE_GRAMMAR.RUNNERS,
2186
+ order: SPEC_TREE_GRAMMAR.ORDER,
2187
+ pathSeparator: SPEC_TREE_GRAMMAR.PATH_SEPARATOR,
2188
+ coordinationNotes: SPEC_TREE_GRAMMAR.COORDINATION_NOTES,
2189
+ evalLane: SPEC_TREE_GRAMMAR.EVAL_LANE
2190
+ };
2191
+ }
2192
+ var SPEC_TREE_NAMING_SCHEMA_VERSIONS = [
2193
+ namingSchemaVersionFromNodeSuffixes(NAMING_SCHEMA_VERSION_ID.PRIOR, SPEC_TREE_GRAMMAR.PRIOR_NODE_SUFFIXES),
2194
+ namingSchemaVersionFromNodeSuffixes(NAMING_SCHEMA_VERSION_ID.CANONICAL, NODE_SUFFIXES)
2195
+ ];
2196
+ var VERSION_COMPONENT_SEPARATOR = ".";
2197
+ var VERSION_COMPONENT_RADIX = 10;
2198
+ var VERSION_MISSING_COMPONENT = 0;
2199
+ var VERSION_ORDER_EQUAL = 0;
2200
+ var VERSION_NUMERIC_COMPONENT = /^\d+$/;
2201
+ function parseVersionComponents(version2) {
2202
+ return version2.split(VERSION_COMPONENT_SEPARATOR).map((part) => {
2203
+ if (!VERSION_NUMERIC_COMPONENT.test(part)) {
2204
+ throw new Error(
2205
+ `Naming-schema version "${version2}" must use numeric dotted components; "${part}" is not numeric`
2206
+ );
2207
+ }
2208
+ return Number.parseInt(part, VERSION_COMPONENT_RADIX);
2209
+ });
2210
+ }
2211
+ function compareNumericVersionIdentifiers(left, right) {
2212
+ const leftComponents = parseVersionComponents(left);
2213
+ const rightComponents = parseVersionComponents(right);
2214
+ const length = Math.max(leftComponents.length, rightComponents.length);
2215
+ for (let index = 0; index < length; index += 1) {
2216
+ const difference = (leftComponents[index] ?? VERSION_MISSING_COMPONENT) - (rightComponents[index] ?? VERSION_MISSING_COMPONENT);
2217
+ if (difference !== VERSION_ORDER_EQUAL) {
2218
+ return difference;
2219
+ }
2220
+ }
2221
+ return VERSION_ORDER_EQUAL;
2222
+ }
2223
+ function compareNamingSchemaVersions(left, right) {
2224
+ return compareNumericVersionIdentifiers(left.version, right.version);
2225
+ }
2226
+ function canonicalNamingSchemaVersion(versions) {
2227
+ const [first, ...rest] = versions;
2228
+ if (first === void 0) {
2229
+ throw new Error("Naming-schema version tuple must declare at least one version");
2230
+ }
2231
+ return rest.reduce(
2232
+ (max, version2) => compareNamingSchemaVersions(version2, max) > VERSION_ORDER_EQUAL ? version2 : max,
2233
+ first
2234
+ );
2235
+ }
2236
+ function supersededNodeSuffixes(versions) {
2237
+ const canonical = canonicalNamingSchemaVersion(versions);
2238
+ const canonicalSuffixes = new Set(canonical.nodeSuffixes);
2239
+ const superseded = /* @__PURE__ */ new Set();
2240
+ for (const version2 of versions) {
2241
+ if (version2 === canonical) {
2242
+ continue;
2243
+ }
2244
+ for (const suffix of version2.nodeSuffixes) {
2245
+ if (!canonicalSuffixes.has(suffix)) {
2246
+ superseded.add(suffix);
2247
+ }
2248
+ }
2249
+ }
2250
+ return [...superseded];
2251
+ }
2252
+ var SPEC_TREE_NAMING_VERSION = canonicalNamingSchemaVersion(SPEC_TREE_NAMING_SCHEMA_VERSIONS).version;
2253
+ var SPEC_TREE_SUPERSEDED_NODE_SUFFIXES = supersededNodeSuffixes(
2254
+ SPEC_TREE_NAMING_SCHEMA_VERSIONS
2255
+ );
1648
2256
  var SPEC_TREE_NODE_STATE = {
1649
2257
  DECLARED: "declared",
1650
2258
  SPECIFIED: "specified",
@@ -1822,7 +2430,7 @@ var REGISTERED_TOOL_NAMES = Object.keys(ADAPTER_MAP);
1822
2430
 
1823
2431
  // src/lib/file-inclusion/ignore-source.ts
1824
2432
  import { readFileSync } from "fs";
1825
- import { join } from "path";
2433
+ import { join as join4 } from "path";
1826
2434
  var IGNORE_SOURCE_FILENAME_DEFAULT = "EXCLUDE";
1827
2435
  var EMPTY_IGNORE_READER = {
1828
2436
  isUnderIgnoreSource() {
@@ -1856,7 +2464,7 @@ function validateEntry(entry, lineNumber) {
1856
2464
  }
1857
2465
  function createIgnoreSourceReader(projectRoot, config) {
1858
2466
  const { ignoreSourceFilename, specTreeRootSegment } = config;
1859
- const filePath = join(projectRoot, specTreeRootSegment, ignoreSourceFilename);
2467
+ const filePath = join4(projectRoot, specTreeRootSegment, ignoreSourceFilename);
1860
2468
  let content;
1861
2469
  try {
1862
2470
  content = readFileSync(filePath, "utf8");
@@ -1919,8 +2527,8 @@ var HIDDEN_PREFIX_LAYER = "hidden-prefix";
1919
2527
  var LAYER2 = HIDDEN_PREFIX_LAYER;
1920
2528
  function hiddenPrefixPredicate(path6, config) {
1921
2529
  const segments = path6.split("/");
1922
- const basename3 = segments[segments.length - 1] ?? "";
1923
- const matched = basename3.startsWith(config.hiddenPrefix);
2530
+ const basename5 = segments[segments.length - 1] ?? "";
2531
+ const matched = basename5.startsWith(config.hiddenPrefix);
1924
2532
  return { matched, layer: LAYER2 };
1925
2533
  }
1926
2534
 
@@ -2319,7 +2927,7 @@ var defaults3 = {
2319
2927
  enabled: false
2320
2928
  }
2321
2929
  };
2322
- function validatePaths2(raw) {
2930
+ function validatePaths(raw) {
2323
2931
  const basePath = `${VALIDATION_SECTION}.${VALIDATION_PATHS_SUBSECTION}`;
2324
2932
  const baseResult = validatePathFilterConfig(raw, basePath);
2325
2933
  if (!baseResult.ok) return baseResult;
@@ -2386,7 +2994,7 @@ function validate7(value) {
2386
2994
  }
2387
2995
  const candidate = value;
2388
2996
  const pathsRaw = candidate[VALIDATION_PATHS_SUBSECTION] ?? {};
2389
- const pathsResult = validatePaths2(pathsRaw);
2997
+ const pathsResult = validatePaths(pathsRaw);
2390
2998
  if (!pathsResult.ok) return pathsResult;
2391
2999
  const literalRaw = candidate[VALIDATION_LITERAL_SUBSECTION] ?? {};
2392
3000
  const literalResult = validateLiteral(literalRaw);
@@ -2414,11 +3022,11 @@ var productionRegistry = [
2414
3022
  ];
2415
3023
 
2416
3024
  // src/config/descriptor-digest.ts
2417
- import { createHash } from "crypto";
3025
+ import { createHash as createHash3 } from "crypto";
2418
3026
  var DEFAULT_DESCRIPTOR_PATH = "descriptor section";
2419
3027
  var SHA256_ALGORITHM = "sha256";
2420
- var UTF8_ENCODING = "utf8";
2421
- var HEX_ENCODING = "hex";
3028
+ var UTF8_ENCODING2 = "utf8";
3029
+ var HEX_ENCODING2 = "hex";
2422
3030
  function canonicalDescriptorJson(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
2423
3031
  const normalized = normalizeDescriptorJsonValue(value, path6, /* @__PURE__ */ new WeakSet());
2424
3032
  if (!normalized.ok) return normalized;
@@ -2427,7 +3035,7 @@ function canonicalDescriptorJson(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
2427
3035
  function digestDescriptorSection(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
2428
3036
  const canonical = canonicalDescriptorJson(value, path6);
2429
3037
  if (!canonical.ok) return canonical;
2430
- const sha256 = createHash(SHA256_ALGORITHM).update(Buffer.from(canonical.value, UTF8_ENCODING)).digest(HEX_ENCODING);
3038
+ const sha256 = createHash3(SHA256_ALGORITHM).update(Buffer.from(canonical.value, UTF8_ENCODING2)).digest(HEX_ENCODING2);
2431
3039
  return {
2432
3040
  ok: true,
2433
3041
  value: {
@@ -2569,7 +3177,7 @@ async function readProductConfigFile(productDir) {
2569
3177
  const detected = [];
2570
3178
  for (const format2 of CONFIG_FILE_FORMAT_ORDER) {
2571
3179
  const filename = CONFIG_FILE_DEFINITIONS[format2].filename;
2572
- const path6 = join2(productDir, filename);
3180
+ const path6 = join5(productDir, filename);
2573
3181
  let raw;
2574
3182
  try {
2575
3183
  raw = await readFile2(path6, "utf8");
@@ -2596,7 +3204,7 @@ function configFileForFormat(productDir, format2 = DEFAULT_CONFIG_FILE_FORMAT, r
2596
3204
  return {
2597
3205
  filename,
2598
3206
  format: format2,
2599
- path: join2(productDir, filename),
3207
+ path: join5(productDir, filename),
2600
3208
  raw
2601
3209
  };
2602
3210
  }
@@ -2893,7 +3501,7 @@ var configDomain = {
2893
3501
 
2894
3502
  // src/commands/session/archive.ts
2895
3503
  import { mkdir, rename, stat } from "fs/promises";
2896
- import { dirname as dirname2, join as join4 } from "path";
3504
+ import { dirname as dirname3, join as join7 } from "path";
2897
3505
 
2898
3506
  // src/domains/session/archive.ts
2899
3507
  var SESSION_FILE_EXTENSION = ".md";
@@ -2974,24 +3582,24 @@ var HANDOFF_BASE_PREREQUISITE_LABEL = {
2974
3582
  DETACHED_AT_DEFAULT_TIP: "HEAD is detached at the default-branch tip"
2975
3583
  };
2976
3584
  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"
3585
+ /** Unclean working tree: commit, or hand off from the main checkout. */
3586
+ COMMIT_OR_MAIN_CHECKOUT: "commit the changes, or run handoff from the main checkout",
3587
+ /** Off the default-branch tip: detach to it, or hand off from the main checkout. */
3588
+ DETACH_TO_TIP_OR_MAIN_CHECKOUT: "detach HEAD to the default-branch tip, or run handoff from the main checkout",
3589
+ /** Default branch unresolved: only the main checkout can anchor the base. */
3590
+ MAIN_CHECKOUT_ONLY: "run handoff from the main checkout"
2983
3591
  };
2984
3592
  var HANDOFF_BASE_FACT_LABEL = {
2985
3593
  DEFAULT_BRANCH: "default branch",
2986
3594
  DEFAULT_TIP: "origin tip",
2987
3595
  HEAD: "HEAD",
2988
3596
  CURRENT_WORKTREE: "current worktree",
2989
- ROOT_WORKTREE: "root worktree"
3597
+ MAIN_CHECKOUT: "main checkout"
2990
3598
  };
2991
3599
  var HANDOFF_BASE_UNRESOLVED = "unresolved";
2992
3600
  var CHECKLIST_INDENT = " ";
2993
3601
  var REMEDY_SEPARATOR = " \u2014 ";
2994
- var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this linked worktree.`;
3602
+ var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this worktree \u2014 it is not the main checkout.`;
2995
3603
  function renderFactLine(label, value) {
2996
3604
  return `${CHECKLIST_INDENT}${label}: ${value ?? HANDOFF_BASE_UNRESOLVED}`;
2997
3605
  }
@@ -3007,7 +3615,7 @@ function renderHandoffBaseChecklist(checklist) {
3007
3615
  renderFactLine(HANDOFF_BASE_FACT_LABEL.DEFAULT_TIP, checklist.defaultTipSha),
3008
3616
  renderFactLine(HANDOFF_BASE_FACT_LABEL.HEAD, checklist.headSha),
3009
3617
  renderFactLine(HANDOFF_BASE_FACT_LABEL.CURRENT_WORKTREE, checklist.currentWorktreePath),
3010
- renderFactLine(HANDOFF_BASE_FACT_LABEL.ROOT_WORKTREE, checklist.rootWorktreePath),
3618
+ renderFactLine(HANDOFF_BASE_FACT_LABEL.MAIN_CHECKOUT, checklist.mainCheckoutPath),
3011
3619
  ...checklist.prerequisites.map(renderPrerequisiteLine)
3012
3620
  ].join("\n");
3013
3621
  }
@@ -3049,6 +3657,18 @@ var SessionInvalidGoalError = class extends SessionError {
3049
3657
  this.name = "SessionInvalidGoalError";
3050
3658
  }
3051
3659
  };
3660
+ var SessionInvalidFieldError = class extends SessionError {
3661
+ /** The unknown field token the caller supplied. */
3662
+ field;
3663
+ /** The full set of valid field names. */
3664
+ validFields;
3665
+ constructor(field, validFields) {
3666
+ super(`Invalid field: "${field}". Valid fields: ${validFields.join(", ")}`);
3667
+ this.name = "SessionInvalidFieldError";
3668
+ this.field = field;
3669
+ this.validFields = validFields;
3670
+ }
3671
+ };
3052
3672
  var SessionInvalidNextStepError = class extends SessionError {
3053
3673
  constructor() {
3054
3674
  super("Invalid session next_step: next_step must be a non-empty string.");
@@ -3062,7 +3682,7 @@ var SessionHandoffBaseError = class extends SessionError {
3062
3682
  silent;
3063
3683
  constructor(options = {}) {
3064
3684
  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."
3685
+ "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
3686
  );
3067
3687
  this.name = SESSION_HANDOFF_BASE_ERROR_NAME;
3068
3688
  this.checklist = options.checklist ?? null;
@@ -3093,252 +3713,62 @@ var SessionLegacyFrontmatterInputError = class extends SessionError {
3093
3713
  );
3094
3714
  this.name = "SessionLegacyFrontmatterInputError";
3095
3715
  }
3096
- };
3097
- var SessionInvalidJsonHeaderError = class extends SessionError {
3098
- constructor(reason) {
3099
- super(`Invalid JSON header for handoff: ${reason}`);
3100
- this.name = "SessionInvalidJsonHeaderError";
3101
- }
3102
- };
3103
-
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";
3107
-
3108
- // src/config/defaults.ts
3109
- var DEFAULT_CONFIG = {
3110
- sessions: {
3111
- dir: ".spx/sessions",
3112
- statusDirs: {
3113
- todo: "todo",
3114
- doing: "doing",
3115
- archive: "archive"
3116
- }
3117
- }
3118
- };
3119
-
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
- };
3716
+ };
3717
+ var SessionWorkBranchNotOnOriginError = class extends SessionError {
3718
+ /** The work-branch ref the caller supplied that does not resolve on `origin`. */
3719
+ workBranch;
3720
+ constructor(workBranch) {
3721
+ super(
3722
+ `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.`
3723
+ );
3724
+ this.name = "SessionWorkBranchNotOnOriginError";
3725
+ this.workBranch = workBranch;
3726
+ }
3727
+ };
3728
+ var SessionInvalidJsonHeaderError = class extends SessionError {
3729
+ constructor(reason) {
3730
+ super(`Invalid JSON header for handoff: ${reason}`);
3731
+ this.name = "SessionInvalidJsonHeaderError";
3732
+ }
3733
+ };
3734
+
3735
+ // src/commands/session/resolve-config.ts
3736
+ import { join as join6 } from "path";
3737
+
3738
+ // src/config/defaults.ts
3739
+ var DEFAULT_CONFIG = {
3740
+ sessions: {
3741
+ statusDirs: {
3742
+ todo: "todo",
3743
+ doing: "doing",
3744
+ archive: "archive"
3247
3745
  }
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
3746
  }
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
- }
3747
+ };
3748
+
3749
+ // src/commands/session/resolve-config.ts
3317
3750
  async function resolveSessionConfig(options = {}) {
3318
3751
  const { sessionsDir, cwd, deps } = options;
3319
3752
  const { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
3320
3753
  if (sessionsDir) {
3321
3754
  return {
3322
3755
  config: {
3323
- todoDir: join3(sessionsDir, statusDirs2.todo),
3324
- doingDir: join3(sessionsDir, statusDirs2.doing),
3325
- archiveDir: join3(sessionsDir, statusDirs2.archive)
3756
+ todoDir: join6(sessionsDir, statusDirs2.todo),
3757
+ doingDir: join6(sessionsDir, statusDirs2.doing),
3758
+ archiveDir: join6(sessionsDir, statusDirs2.archive)
3326
3759
  }
3327
3760
  };
3328
3761
  }
3329
- const gitResult = await detectGitCommonDirProductRoot(cwd, deps);
3330
- const baseDir = join3(gitResult.productDir, DEFAULT_CONFIG.sessions.dir);
3762
+ const { sessionsDir: baseDir, warning } = await resolveSessionsScopeDir({ cwd, deps });
3331
3763
  return {
3332
3764
  config: {
3333
- todoDir: join3(baseDir, statusDirs2.todo),
3334
- doingDir: join3(baseDir, statusDirs2.doing),
3335
- archiveDir: join3(baseDir, statusDirs2.archive)
3765
+ todoDir: join6(baseDir, statusDirs2.todo),
3766
+ doingDir: join6(baseDir, statusDirs2.doing),
3767
+ archiveDir: join6(baseDir, statusDirs2.archive)
3336
3768
  },
3337
- warning: gitResult.warning
3769
+ warning
3338
3770
  };
3339
3771
  }
3340
-
3341
- // src/commands/session/resolve-config.ts
3342
3772
  async function resolveSessionConfigSurfacingWarning(sessionsDir, onWarning) {
3343
3773
  const { config, warning } = await resolveSessionConfig({ sessionsDir });
3344
3774
  if (warning !== void 0) {
@@ -3371,9 +3801,9 @@ async function fileExists(path6) {
3371
3801
  }
3372
3802
  async function probeSessionPaths(sessionId, config) {
3373
3803
  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);
3804
+ const todoPath = join7(config.todoDir, filename);
3805
+ const doingPath = join7(config.doingDir, filename);
3806
+ const archivePath = join7(config.archiveDir, filename);
3377
3807
  return {
3378
3808
  todo: await fileExists(todoPath) ? todoPath : null,
3379
3809
  doing: await fileExists(doingPath) ? doingPath : null,
@@ -3394,7 +3824,7 @@ async function resolveArchivePaths(sessionId, config) {
3394
3824
  }
3395
3825
  async function archiveSingle(sessionId, config) {
3396
3826
  const { source, target } = await resolveArchivePaths(sessionId, config);
3397
- await mkdir(dirname2(target), { recursive: true });
3827
+ await mkdir(dirname3(target), { recursive: true });
3398
3828
  await rename(source, target);
3399
3829
  return `${SESSION_ARCHIVE_OUTPUT.ARCHIVED}: ${sessionId}
3400
3830
  ${SESSION_ARCHIVE_OUTPUT.ARCHIVE_LOCATION}: ${target}`;
@@ -3417,7 +3847,7 @@ function resolveDeletePath(sessionId, existingPaths) {
3417
3847
  }
3418
3848
 
3419
3849
  // src/domains/session/show.ts
3420
- import { join as join5 } from "path";
3850
+ import { join as join8 } from "path";
3421
3851
 
3422
3852
  // src/domains/session/list.ts
3423
3853
  import { parse as parseYaml2 } from "yaml";
@@ -3428,12 +3858,12 @@ var SESSION_ID_SEPARATOR = "_";
3428
3858
  function generateSessionId(options = {}) {
3429
3859
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
3430
3860
  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");
3861
+ const year = date.getUTCFullYear();
3862
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
3863
+ const day = String(date.getUTCDate()).padStart(2, "0");
3864
+ const hours = String(date.getUTCHours()).padStart(2, "0");
3865
+ const minutes = String(date.getUTCMinutes()).padStart(2, "0");
3866
+ const seconds = String(date.getUTCSeconds()).padStart(2, "0");
3437
3867
  return `${year}-${month}-${day}${SESSION_ID_SEPARATOR}${hours}-${minutes}-${seconds}`;
3438
3868
  }
3439
3869
  function parseSessionId(id) {
@@ -3453,7 +3883,11 @@ function parseSessionId(id) {
3453
3883
  if (hours < 0 || hours > 23) return null;
3454
3884
  if (minutes < 0 || minutes > 59) return null;
3455
3885
  if (seconds < 0 || seconds > 59) return null;
3456
- return new Date(year, month, day, hours, minutes, seconds);
3886
+ const date = new Date(Date.UTC(year, month, day, hours, minutes, seconds));
3887
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day || date.getUTCHours() !== hours || date.getUTCMinutes() !== minutes || date.getUTCSeconds() !== seconds) {
3888
+ return null;
3889
+ }
3890
+ return date;
3457
3891
  }
3458
3892
 
3459
3893
  // src/domains/session/types.ts
@@ -3556,6 +3990,67 @@ function sortSessions(sessions) {
3556
3990
  return dateB.getTime() - dateA.getTime();
3557
3991
  });
3558
3992
  }
3993
+ var FIELD_SELECTION_SEPARATOR = ",";
3994
+ var SESSION_RECORD_FIELD = {
3995
+ ID: "id",
3996
+ STATUS: "status",
3997
+ PRIORITY: SESSION_FRONT_MATTER.PRIORITY,
3998
+ GIT_REF: SESSION_FRONT_MATTER.GIT_REF,
3999
+ GOAL: SESSION_FRONT_MATTER.GOAL,
4000
+ NEXT_STEP: SESSION_FRONT_MATTER.NEXT_STEP,
4001
+ SPECS: SESSION_FRONT_MATTER.SPECS,
4002
+ FILES: SESSION_FRONT_MATTER.FILES,
4003
+ CREATED_AT: SESSION_FRONT_MATTER.CREATED_AT,
4004
+ AGENT_SESSION_ID: SESSION_FRONT_MATTER.AGENT_SESSION_ID
4005
+ };
4006
+ var SESSION_RECORD_FIELDS = Object.values(SESSION_RECORD_FIELD);
4007
+ function toSessionRecord(session) {
4008
+ const { id, status, metadata } = session;
4009
+ const record = {
4010
+ id,
4011
+ status,
4012
+ priority: metadata.priority,
4013
+ git_ref: metadata.git_ref,
4014
+ goal: metadata.goal,
4015
+ next_step: metadata.next_step,
4016
+ specs: metadata.specs,
4017
+ files: metadata.files
4018
+ };
4019
+ if (metadata.created_at !== void 0) {
4020
+ record.created_at = metadata.created_at;
4021
+ }
4022
+ if (metadata.agent_session_id !== void 0) {
4023
+ record.agent_session_id = metadata.agent_session_id;
4024
+ }
4025
+ return record;
4026
+ }
4027
+ function projectSessionRecord(record, fields) {
4028
+ const projected = {};
4029
+ for (const field of fields) {
4030
+ const value = record[field];
4031
+ if (value !== void 0) {
4032
+ projected[field] = value;
4033
+ }
4034
+ }
4035
+ return projected;
4036
+ }
4037
+ function isSessionRecordField(value) {
4038
+ return SESSION_RECORD_FIELDS.includes(value);
4039
+ }
4040
+ function parseFieldSelection(input) {
4041
+ const tokens = input.split(FIELD_SELECTION_SEPARATOR).map((token) => token.trim()).filter((token) => token.length > 0);
4042
+ if (tokens.length === 0) {
4043
+ throw new SessionInvalidFieldError(input, SESSION_RECORD_FIELDS);
4044
+ }
4045
+ const selection = [];
4046
+ for (const token of tokens) {
4047
+ if (!isSessionRecordField(token)) {
4048
+ throw new SessionInvalidFieldError(token, SESSION_RECORD_FIELDS);
4049
+ }
4050
+ selection.push(token);
4051
+ }
4052
+ return selection;
4053
+ }
3559
4054
 
3560
4055
  // src/domains/session/show.ts
3561
4056
  var SESSION_SHOW_LABEL = {
@@ -3569,11 +4064,12 @@ var SESSION_SHOW_LABEL = {
3569
4064
  };
3570
4065
  var SESSION_SHOW_SEPARATOR_CHAR = "\u2500";
3571
4066
  var SESSION_SHOW_SEPARATOR_WIDTH = 40;
3572
- var { dir: sessionsBaseDir, statusDirs } = DEFAULT_CONFIG.sessions;
4067
+ var sessionsBaseDir = join8(STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
4068
+ var { statusDirs } = DEFAULT_CONFIG.sessions;
3573
4069
  var DEFAULT_SESSION_CONFIG = {
3574
- todoDir: join5(sessionsBaseDir, statusDirs.todo),
3575
- doingDir: join5(sessionsBaseDir, statusDirs.doing),
3576
- archiveDir: join5(sessionsBaseDir, statusDirs.archive)
4070
+ todoDir: join8(sessionsBaseDir, statusDirs.todo),
4071
+ doingDir: join8(sessionsBaseDir, statusDirs.doing),
4072
+ archiveDir: join8(sessionsBaseDir, statusDirs.archive)
3577
4073
  };
3578
4074
  var SEARCH_ORDER = [...SESSION_STATUSES];
3579
4075
  function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
@@ -3631,7 +4127,7 @@ async function deleteCommand(options) {
3631
4127
 
3632
4128
  // src/commands/session/handoff.ts
3633
4129
  import { mkdir as mkdir2, writeFile } from "fs/promises";
3634
- import { join as join6, resolve as resolve4 } from "path";
4130
+ import { join as join9, resolve as resolve3 } from "path";
3635
4131
  import { stringify as stringifyYaml3 } from "yaml";
3636
4132
 
3637
4133
  // src/domains/session/create.ts
@@ -3652,11 +4148,11 @@ function cleanPrerequisite(facts) {
3652
4148
  return {
3653
4149
  label: HANDOFF_BASE_PREREQUISITE_LABEL.CLEAN_WORKING_TREE,
3654
4150
  met: facts.isClean,
3655
- remedy: facts.isClean ? "" : HANDOFF_BASE_REMEDY.COMMIT_OR_ROOT
4151
+ remedy: facts.isClean ? "" : HANDOFF_BASE_REMEDY.COMMIT_OR_MAIN_CHECKOUT
3656
4152
  };
3657
4153
  }
3658
4154
  function detachedAtTipPrerequisite(facts, met) {
3659
- const remedy = facts.defaultTipSha === null ? HANDOFF_BASE_REMEDY.ROOT_ONLY : HANDOFF_BASE_REMEDY.DETACH_TO_TIP_OR_ROOT;
4155
+ const remedy = facts.defaultTipSha === null ? HANDOFF_BASE_REMEDY.MAIN_CHECKOUT_ONLY : HANDOFF_BASE_REMEDY.DETACH_TO_TIP_OR_MAIN_CHECKOUT;
3660
4156
  return {
3661
4157
  label: HANDOFF_BASE_PREREQUISITE_LABEL.DETACHED_AT_DEFAULT_TIP,
3662
4158
  met,
@@ -3669,7 +4165,7 @@ function buildChecklist(facts, prerequisites) {
3669
4165
  defaultTipSha: facts.defaultTipSha,
3670
4166
  headSha: facts.headSha,
3671
4167
  currentWorktreePath: facts.currentWorktreePath,
3672
- rootWorktreePath: facts.rootWorktreePath,
4168
+ mainCheckoutPath: facts.mainCheckoutPath,
3673
4169
  prerequisites
3674
4170
  };
3675
4171
  }
@@ -3677,7 +4173,7 @@ function resolveHandoffGitRef(facts) {
3677
4173
  if (!facts.isGitRepo) {
3678
4174
  throw new SessionHandoffBaseError({ silent: true });
3679
4175
  }
3680
- if (facts.isRootWorktree) {
4176
+ if (facts.isMainCheckout) {
3681
4177
  if (facts.branch !== null) return facts.branch;
3682
4178
  if (facts.headSha !== null) return facts.headSha;
3683
4179
  throw new SessionHandoffBaseError();
@@ -3692,6 +4188,12 @@ function resolveHandoffGitRef(facts) {
3692
4188
  }
3693
4189
  throw new SessionHandoffBaseError({ checklist: buildChecklist(facts, prerequisites) });
3694
4190
  }
4191
+ function resolveWorkBranchGitRef(workBranch, existsOnOrigin) {
4192
+ if (!existsOnOrigin) {
4193
+ throw new SessionWorkBranchNotOnOriginError(workBranch);
4194
+ }
4195
+ return workBranch;
4196
+ }
3695
4197
 
3696
4198
  // src/domains/session/parse-handoff-input.ts
3697
4199
  var LEGACY_FRONTMATTER_PREFIX = /^---\r?\n/;
@@ -3782,12 +4284,14 @@ function validateHandoffHeader(parsed) {
3782
4284
  const nextStep = ensureStringOrEmpty(obj[SESSION_FRONT_MATTER.NEXT_STEP], SESSION_FRONT_MATTER.NEXT_STEP);
3783
4285
  const specs = ensureStringArrayOrDefault(obj[SESSION_FRONT_MATTER.SPECS], SESSION_FRONT_MATTER.SPECS);
3784
4286
  const files = ensureStringArrayOrDefault(obj[SESSION_FRONT_MATTER.FILES], SESSION_FRONT_MATTER.FILES);
4287
+ const gitRef = ensureOptionalString(obj[SESSION_FRONT_MATTER.GIT_REF], SESSION_FRONT_MATTER.GIT_REF);
3785
4288
  return {
3786
4289
  priority,
3787
4290
  goal,
3788
4291
  next_step: nextStep,
3789
4292
  specs,
3790
- files
4293
+ files,
4294
+ ...gitRef === void 0 ? {} : { git_ref: gitRef }
3791
4295
  };
3792
4296
  }
3793
4297
  function ensurePriorityOrDefault(value) {
@@ -3806,6 +4310,13 @@ function ensureStringOrEmpty(value, fieldName) {
3806
4310
  }
3807
4311
  return value;
3808
4312
  }
4313
+ function ensureOptionalString(value, fieldName) {
4314
+ if (value === void 0) return void 0;
4315
+ if (typeof value !== "string") {
4316
+ throw new SessionInvalidJsonHeaderError(`${fieldName} must be a string`);
4317
+ }
4318
+ return value;
4319
+ }
3809
4320
  function ensureStringArrayOrDefault(value, fieldName) {
3810
4321
  if (value === void 0) return [];
3811
4322
  if (!Array.isArray(value)) {
@@ -3821,18 +4332,19 @@ function ensureStringArrayOrDefault(value, fieldName) {
3821
4332
 
3822
4333
  // src/commands/session/handoff.ts
3823
4334
  async function resolveSessionGitRef(cwd, deps) {
3824
- const [branch, headSha, gitRoots] = await Promise.all([
4335
+ const [branch, headSha, facts] = await Promise.all([
3825
4336
  getCurrentBranch(cwd, deps),
3826
4337
  getHeadSha(cwd, deps),
3827
- detectGitCommonDirProductRoot(cwd, deps)
4338
+ gatherGitFacts(cwd, deps)
3828
4339
  ]);
3829
- const currentWorktreePath = gitRoots.worktreeRoot;
3830
- const rootWorktreePath = gitRoots.productDir;
3831
- const isRoot = currentWorktreePath === rootWorktreePath;
4340
+ const isGitRepo = facts !== null;
4341
+ const currentWorktreePath = facts?.worktreeRoot ?? cwd ?? process.cwd();
4342
+ const isMain = facts !== null && isMainCheckout(facts);
4343
+ const designatedMainCheckout = facts !== null ? mainCheckoutPath(facts) : null;
3832
4344
  let isClean = false;
3833
4345
  let defaultBranch = null;
3834
4346
  let defaultTipSha = null;
3835
- if (gitRoots.isGitRepo && !isRoot) {
4347
+ if (isGitRepo && !isMain) {
3836
4348
  [isClean, defaultBranch] = await Promise.all([
3837
4349
  isWorkingTreeClean(cwd, deps),
3838
4350
  resolveDefaultBranch(cwd, deps)
@@ -3840,17 +4352,22 @@ async function resolveSessionGitRef(cwd, deps) {
3840
4352
  defaultTipSha = defaultBranch === null ? null : await resolveRefSha(`${ORIGIN_REF_PREFIX}${defaultBranch}`, cwd, deps);
3841
4353
  }
3842
4354
  return resolveHandoffGitRef({
3843
- isGitRepo: gitRoots.isGitRepo,
3844
- isRootWorktree: isRoot,
4355
+ isGitRepo,
4356
+ isMainCheckout: isMain,
3845
4357
  branch,
3846
4358
  headSha,
3847
4359
  isClean,
3848
4360
  defaultBranch,
3849
4361
  defaultTipSha,
3850
4362
  currentWorktreePath,
3851
- rootWorktreePath
4363
+ mainCheckoutPath: designatedMainCheckout
3852
4364
  });
3853
4365
  }
4366
+ async function resolveRecordedGitRef(suppliedRef, gateRef, cwd, deps) {
4367
+ if (suppliedRef === void 0 || suppliedRef.length === 0) return gateRef;
4368
+ const existsOnOrigin = await originBranchExists(suppliedRef, cwd, deps);
4369
+ return resolveWorkBranchGitRef(suppliedRef, existsOnOrigin);
4370
+ }
3854
4371
  async function handoffCommand(options) {
3855
4372
  const { config } = await resolveSessionConfig({
3856
4373
  sessionsDir: options.sessionsDir,
@@ -3868,9 +4385,10 @@ async function handoffCommand(options) {
3868
4385
  if (header.next_step.length === 0) {
3869
4386
  throw new SessionInvalidNextStepError();
3870
4387
  }
3871
- const gitRef = await resolveSessionGitRef(options.cwd, options.deps);
4388
+ const gateRef = await resolveSessionGitRef(options.cwd, options.deps);
4389
+ const gitRef = await resolveRecordedGitRef(header.git_ref, gateRef, options.cwd, options.deps);
3872
4390
  const sessionId = generateSessionId();
3873
- const agentSessionId = process.env.CLAUDE_SESSION_ID ?? process.env.CODEX_THREAD_ID;
4391
+ const agentSessionId = resolveAgentSessionId(options.env ?? process.env);
3874
4392
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
3875
4393
  const frontMatterObject = {
3876
4394
  [SESSION_FRONT_MATTER.PRIORITY]: header.priority,
@@ -3887,8 +4405,8 @@ async function handoffCommand(options) {
3887
4405
  const yaml = stringifyYaml3(frontMatterObject, { defaultStringType: "QUOTE_DOUBLE" }).trimEnd();
3888
4406
  const fullContent = `${SESSION_FRONT_MATTER_OPEN}${yaml}${SESSION_FRONT_MATTER_CLOSE}${body}`;
3889
4407
  const filename = `${sessionId}.md`;
3890
- const sessionPath = join6(config.todoDir, filename);
3891
- const absolutePath = resolve4(sessionPath);
4408
+ const sessionPath = join9(config.todoDir, filename);
4409
+ const absolutePath = resolve3(sessionPath);
3892
4410
  await mkdir2(config.todoDir, { recursive: true });
3893
4411
  await writeFile(sessionPath, fullContent, "utf-8");
3894
4412
  const output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
@@ -3898,7 +4416,7 @@ async function handoffCommand(options) {
3898
4416
 
3899
4417
  // src/commands/session/list.ts
3900
4418
  import { readdir, readFile as readFile3 } from "fs/promises";
3901
- import { join as join7 } from "path";
4419
+ import { join as join10 } from "path";
3902
4420
  var SESSION_LIST_FORMAT = {
3903
4421
  TEXT: "text",
3904
4422
  JSON: "json"
@@ -3912,7 +4430,7 @@ async function loadSessionsFromDir(dir, status) {
3912
4430
  for (const file of files) {
3913
4431
  if (!file.endsWith(".md")) continue;
3914
4432
  const id = file.replace(".md", "");
3915
- const filePath = join7(dir, file);
4433
+ const filePath = join10(dir, file);
3916
4434
  const content = await readFile3(filePath, "utf-8");
3917
4435
  const metadata = parseSessionMetadata(content);
3918
4436
  sessions.push({
@@ -3954,6 +4472,7 @@ function validateStatus(input) {
3954
4472
  );
3955
4473
  }
3956
4474
  async function listCommand(options) {
4475
+ const fieldSelection = options.fields !== void 0 ? parseFieldSelection(options.fields) : void 0;
3957
4476
  const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
3958
4477
  const statuses = options.status !== void 0 ? [validateStatus(options.status)] : DEFAULT_LIST_STATUSES;
3959
4478
  const sessionsByStatus = {};
@@ -3962,8 +4481,16 @@ async function listCommand(options) {
3962
4481
  const sessions = await loadSessionsFromDir(config[dirKey], status);
3963
4482
  sessionsByStatus[status] = sortSessions(sessions);
3964
4483
  }
3965
- if (options.format === SESSION_LIST_FORMAT.JSON) {
3966
- return JSON.stringify(sessionsByStatus, null, 2);
4484
+ const emitJson = fieldSelection !== void 0 || options.format === SESSION_LIST_FORMAT.JSON;
4485
+ if (emitJson) {
4486
+ const recordsByStatus = {};
4487
+ for (const status of statuses) {
4488
+ recordsByStatus[status] = (sessionsByStatus[status] ?? []).map((session) => {
4489
+ const record = toSessionRecord(session);
4490
+ return fieldSelection !== void 0 ? projectSessionRecord(record, fieldSelection) : record;
4491
+ });
4492
+ }
4493
+ return JSON.stringify(recordsByStatus, null, 2);
3967
4494
  }
3968
4495
  const lines = [];
3969
4496
  for (const status of statuses) {
@@ -3976,7 +4503,7 @@ async function listCommand(options) {
3976
4503
 
3977
4504
  // src/commands/session/pickup.ts
3978
4505
  import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rename as rename2 } from "fs/promises";
3979
- import { join as join8 } from "path";
4506
+ import { join as join11 } from "path";
3980
4507
 
3981
4508
  // src/domains/session/pickup.ts
3982
4509
  function buildClaimPaths(sessionId, config) {
@@ -4021,7 +4548,7 @@ async function loadTodoSessions(config) {
4021
4548
  for (const file of files) {
4022
4549
  if (!file.endsWith(".md")) continue;
4023
4550
  const id = file.replace(".md", "");
4024
- const filePath = join8(config.todoDir, file);
4551
+ const filePath = join11(config.todoDir, file);
4025
4552
  const content = await readFile4(filePath, "utf-8");
4026
4553
  const metadata = parseSessionMetadata(content);
4027
4554
  sessions.push({
@@ -4077,7 +4604,7 @@ async function pickupCommand(options) {
4077
4604
 
4078
4605
  // src/commands/session/prune.ts
4079
4606
  import { readdir as readdir3, readFile as readFile5, unlink as unlink2 } from "fs/promises";
4080
- import { join as join9 } from "path";
4607
+ import { join as join12 } from "path";
4081
4608
 
4082
4609
  // src/domains/session/prune.ts
4083
4610
  var DEFAULT_KEEP_COUNT = 5;
@@ -4126,7 +4653,7 @@ async function loadArchiveSessions(config) {
4126
4653
  for (const file of files) {
4127
4654
  if (!file.endsWith(".md")) continue;
4128
4655
  const id = file.replace(".md", "");
4129
- const filePath = join9(config.archiveDir, file);
4656
+ const filePath = join12(config.archiveDir, file);
4130
4657
  const content = await readFile5(filePath, "utf-8");
4131
4658
  const metadata = parseSessionMetadata(content);
4132
4659
  sessions.push({
@@ -4316,9 +4843,9 @@ Prefilled by the CLI:
4316
4843
  created_at, git_ref, and agent_session_id when an agent session ID is
4317
4844
  available.
4318
4845
 
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.
4846
+ git_ref records the branch name (main checkout on a branch), the HEAD SHA
4847
+ (detached), or the origin/<default> tip SHA (clean detached non-main checkout).
4848
+ Handoff is refused from any other non-main checkout state.
4322
4849
 
4323
4850
  Output Tags (for automation):
4324
4851
  <HANDOFF_ID>session-id</HANDOFF_ID> Session identifier
@@ -4368,11 +4895,12 @@ function handleError(error) {
4368
4895
  process.exit(1);
4369
4896
  }
4370
4897
  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) => {
4898
+ 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
4899
  try {
4373
4900
  const output = await listCommand({
4374
4901
  status: options.status,
4375
- format: options.json ? "json" : "text",
4902
+ format: options.json ? SESSION_LIST_FORMAT.JSON : SESSION_LIST_FORMAT.TEXT,
4903
+ fields: options.fields,
4376
4904
  sessionsDir: options.sessionsDir,
4377
4905
  onWarning: writeWarning
4378
4906
  });
@@ -4381,11 +4909,12 @@ function registerSessionCommands(sessionCmd) {
4381
4909
  handleError(error);
4382
4910
  }
4383
4911
  });
4384
- sessionCmd.command("todo").description("List todo sessions").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
4912
+ 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
4913
  try {
4386
4914
  const output = await listCommand({
4387
4915
  status: SESSION_STATUSES[0],
4388
- format: options.json ? "json" : "text",
4916
+ format: options.json ? SESSION_LIST_FORMAT.JSON : SESSION_LIST_FORMAT.TEXT,
4917
+ fields: options.fields,
4389
4918
  sessionsDir: options.sessionsDir,
4390
4919
  onWarning: writeWarning
4391
4920
  });
@@ -4440,7 +4969,8 @@ function registerSessionCommands(sessionCmd) {
4440
4969
  const content = await readStdin();
4441
4970
  const result = await handoffCommand({
4442
4971
  content,
4443
- sessionsDir: options.sessionsDir
4972
+ sessionsDir: options.sessionsDir,
4973
+ env: process.env
4444
4974
  });
4445
4975
  console.log(result.output);
4446
4976
  } catch (error) {
@@ -4513,7 +5043,7 @@ var sessionDomain = {
4513
5043
 
4514
5044
  // src/lib/spec-tree/index.ts
4515
5045
  import { readdir as readdir5, readFile as readFile7 } from "fs/promises";
4516
- import { join as join10 } from "path";
5046
+ import { join as join13 } from "path";
4517
5047
  var SPEC_TREE_FIELD_KEY = {
4518
5048
  VERSION: "version",
4519
5049
  PRODUCT: "product",
@@ -4531,7 +5061,9 @@ var SPEC_TREE_ENTRY_TYPE = {
4531
5061
  PRODUCT: SPEC_TREE_FIELD_KEY.PRODUCT,
4532
5062
  NODE: SPEC_TREE_KIND_CATEGORY.NODE,
4533
5063
  DECISION: SPEC_TREE_KIND_CATEGORY.DECISION,
4534
- EVIDENCE: "evidence"
5064
+ EVIDENCE: "evidence",
5065
+ SUPERSEDED: "superseded",
5066
+ INVALID: "invalid"
4535
5067
  };
4536
5068
  var SPEC_TREE_FILESYSTEM_RECORD_TYPE = {
4537
5069
  DIRECTORY: "directory",
@@ -4542,17 +5074,6 @@ var SPEC_TREE_EVIDENCE_STATUS = {
4542
5074
  FAILING: SPEC_TREE_NODE_STATE.FAILING,
4543
5075
  PASSING: SPEC_TREE_NODE_STATE.PASSING
4544
5076
  };
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
5077
  var SPEC_TREE_EVIDENCE_FILE_TAILS = Object.values(SPEC_TREE_EVIDENCE_FILE.TAILS);
4557
5078
  var SPEC_TREE_PROJECTION = {
4558
5079
  VERSION: 1,
@@ -4586,30 +5107,33 @@ var SPEC_TREE_NODE_RELATION_KEYS = {
4586
5107
  DECISIONS: SPEC_TREE_FIELD_KEY.DECISIONS
4587
5108
  };
4588
5109
  var ORDER_COMPARISON_EQUAL = 0;
4589
- var SPEC_TREE_PATH_SEPARATOR = "/";
4590
- var SPEC_TREE_ORDER_SEPARATOR = "-";
5110
+ var SPEC_TREE_PATH_SEPARATOR = SPEC_TREE_GRAMMAR.PATH_SEPARATOR;
5111
+ var SPEC_TREE_ORDER_SEPARATOR = SPEC_TREE_GRAMMAR.ORDER.SEPARATOR;
4591
5112
  var SPEC_TREE_ORDER_RADIX = 10;
4592
5113
  var SPEC_TREE_TEXT_ENCODING = "utf8";
4593
5114
  var SPEC_TREE_EMPTY_RELATIVE_PATH = "";
4594
- var SPEC_TREE_ORDER_PATTERN = /^\d+$/;
5115
+ var SPEC_TREE_ORDER_PATTERN = SPEC_TREE_GRAMMAR.ORDER.PATTERN;
4595
5116
  var SPEC_TREE_MIN_EVIDENCE_PATH_SEGMENTS = 2;
4596
5117
  var SPEC_TREE_PARENT_SEGMENT_OFFSET = 2;
4597
5118
  var SPEC_TREE_FIRST_EVIDENCE_MARKER_INDEX = 1;
4598
5119
  var SPEC_TREE_EXACTLY_ONE_EVIDENCE_MARKER = 1;
4599
5120
  function createFilesystemSpecTreeSource(options) {
4600
5121
  const registry = options.registry ?? KIND_REGISTRY;
5122
+ const schemaVersions = options.schemaVersions ?? SPEC_TREE_NAMING_SCHEMA_VERSIONS;
4601
5123
  const includePath = options.includePath ?? includeEverySpecTreePath;
4602
5124
  return {
4603
- entries: () => readFilesystemSourceEntries(options.productDir, registry, includePath),
5125
+ entries: () => readFilesystemSourceEntries(options.productDir, registry, schemaVersions, includePath),
4604
5126
  async readText(ref) {
4605
5127
  if (ref.path === void 0) {
4606
5128
  throw new Error("Filesystem source refs require a path");
4607
5129
  }
4608
- return readFile7(join10(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
5130
+ return readFile7(join13(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
4609
5131
  }
4610
5132
  };
4611
5133
  }
4612
- function recognizeSpecTreeFilesystemEntry(record, registry = KIND_REGISTRY) {
5134
+ function recognizeSpecTreeFilesystemEntry(record, options = {}) {
5135
+ const registry = options.registry ?? KIND_REGISTRY;
5136
+ const schemaVersions = options.schemaVersions ?? SPEC_TREE_NAMING_SCHEMA_VERSIONS;
4613
5137
  const name = readLastPathSegment(record.relativePath);
4614
5138
  if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE && isProductFile(record.relativePath)) {
4615
5139
  return {
@@ -4620,19 +5144,7 @@ function recognizeSpecTreeFilesystemEntry(record, registry = KIND_REGISTRY) {
4620
5144
  };
4621
5145
  }
4622
5146
  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));
4626
- if (parsed === null) return null;
4627
- return {
4628
- type: SPEC_TREE_ENTRY_TYPE.NODE,
4629
- kind: nodeMatch.kind,
4630
- id: record.relativePath,
4631
- order: parsed.order,
4632
- slug: parsed.slug,
4633
- parentId: record.parentId,
4634
- ref: sourceRefForNode(record.relativePath, parsed.slug)
4635
- };
5147
+ return recognizeDirectoryRecord(record, name, registry, schemaVersions);
4636
5148
  }
4637
5149
  if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE && record.parentId !== void 0 && isEvidenceFile(record.relativePath)) {
4638
5150
  return {
@@ -4660,9 +5172,79 @@ function recognizeSpecTreeFilesystemEntry(record, registry = KIND_REGISTRY) {
4660
5172
  }
4661
5173
  return null;
4662
5174
  }
5175
+ function recognizeDirectoryRecord(record, name, registry, schemaVersions) {
5176
+ const canonical = canonicalNamingSchemaVersion(schemaVersions);
5177
+ const canonicalMatch = matchNodeSuffixFromVersion(name, canonical);
5178
+ if (canonicalMatch !== null) {
5179
+ const kind = nodeKindForSuffix(canonicalMatch.suffix, registry);
5180
+ if (kind !== null) {
5181
+ return {
5182
+ type: SPEC_TREE_ENTRY_TYPE.NODE,
5183
+ kind,
5184
+ id: record.relativePath,
5185
+ order: canonicalMatch.parsed.order,
5186
+ slug: canonicalMatch.parsed.slug,
5187
+ parentId: record.parentId,
5188
+ ref: sourceRefForNode(record.relativePath, canonicalMatch.parsed.slug)
5189
+ };
5190
+ }
5191
+ }
5192
+ const supersededVersion = matchSupersededNodeVersion(name, schemaVersions, canonical);
5193
+ if (supersededVersion !== null) {
5194
+ return {
5195
+ type: SPEC_TREE_ENTRY_TYPE.SUPERSEDED,
5196
+ id: record.relativePath,
5197
+ version: supersededVersion,
5198
+ parentId: record.parentId,
5199
+ ref: sourceRefForRelativePath(record.relativePath)
5200
+ };
5201
+ }
5202
+ if (parseOrderedSlug(name) !== null) {
5203
+ return {
5204
+ type: SPEC_TREE_ENTRY_TYPE.INVALID,
5205
+ id: record.relativePath,
5206
+ parentId: record.parentId,
5207
+ ref: sourceRefForRelativePath(record.relativePath)
5208
+ };
5209
+ }
5210
+ return null;
5211
+ }
5212
+ function matchNodeSuffixFromVersion(name, version2) {
5213
+ for (const suffix of version2.nodeSuffixes) {
5214
+ if (!name.endsWith(suffix)) continue;
5215
+ const parsed = parseOrderedSlug(stripSuffix(name, suffix));
5216
+ if (parsed !== null) {
5217
+ return { suffix, parsed };
5218
+ }
5219
+ }
5220
+ return null;
5221
+ }
5222
+ function nodeKindForSuffix(suffix, registry) {
5223
+ for (const [kind, definition] of Object.entries(registry)) {
5224
+ if (definition.category === SPEC_TREE_KIND_CATEGORY.NODE && definition.suffix === suffix) {
5225
+ return kind;
5226
+ }
5227
+ }
5228
+ return null;
5229
+ }
5230
+ function matchSupersededNodeVersion(name, schemaVersions, canonical) {
5231
+ const canonicalSuffixes = new Set(canonical.nodeSuffixes);
5232
+ const priorVersions = schemaVersions.filter((version2) => version2 !== canonical).sort((left, right) => compareNamingSchemaVersions(right, left));
5233
+ for (const version2 of priorVersions) {
5234
+ for (const suffix of version2.nodeSuffixes) {
5235
+ if (canonicalSuffixes.has(suffix)) continue;
5236
+ if (name.endsWith(suffix) && parseOrderedSlug(stripSuffix(name, suffix)) !== null) {
5237
+ return version2.version;
5238
+ }
5239
+ }
5240
+ }
5241
+ return null;
5242
+ }
4663
5243
  async function readSpecTree(options) {
4664
5244
  const entries = await collectSourceEntries(options.source);
4665
5245
  const product = entries.find(isProductEntry) ?? null;
5246
+ const superseded = entries.filter(isSupersededEntry);
5247
+ const residual = entries.filter(isInvalidEntry);
4666
5248
  const evidenceByParent = groupEvidence(entries.filter(isEvidenceEntry));
4667
5249
  const decisions = entries.filter(isDecisionEntry).map(toDecision).sort(compareOrderedEntries);
4668
5250
  const decisionsByParent = groupDecisions(decisions);
@@ -4702,6 +5284,8 @@ async function readSpecTree(options) {
4702
5284
  nodes: roots,
4703
5285
  allNodes,
4704
5286
  decisions,
5287
+ superseded,
5288
+ residual,
4705
5289
  entries
4706
5290
  };
4707
5291
  }
@@ -4743,6 +5327,12 @@ function isDecisionEntry(entry) {
4743
5327
  function isEvidenceEntry(entry) {
4744
5328
  return entry.type === SPEC_TREE_ENTRY_TYPE.EVIDENCE;
4745
5329
  }
5330
+ function isSupersededEntry(entry) {
5331
+ return entry.type === SPEC_TREE_ENTRY_TYPE.SUPERSEDED;
5332
+ }
5333
+ function isInvalidEntry(entry) {
5334
+ return entry.type === SPEC_TREE_ENTRY_TYPE.INVALID;
5335
+ }
4746
5336
  function toDecision(entry) {
4747
5337
  return {
4748
5338
  id: entry.id,
@@ -4817,11 +5407,12 @@ function compareOrderedEntries(left, right) {
4817
5407
  if (orderComparison !== ORDER_COMPARISON_EQUAL) return orderComparison;
4818
5408
  return left.id.localeCompare(right.id);
4819
5409
  }
4820
- async function* readFilesystemSourceEntries(productDir, registry, includePath) {
5410
+ async function* readFilesystemSourceEntries(productDir, registry, schemaVersions, includePath) {
4821
5411
  yield* walkFilesystemDirectory({
4822
- absolutePath: join10(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY),
5412
+ absolutePath: join13(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY),
4823
5413
  relativePath: SPEC_TREE_EMPTY_RELATIVE_PATH,
4824
5414
  registry,
5415
+ schemaVersions,
4825
5416
  includePath
4826
5417
  });
4827
5418
  }
@@ -4842,14 +5433,15 @@ async function* walkFilesystemDirectory(context) {
4842
5433
  if (recordType === void 0) continue;
4843
5434
  const sourceEntry = recognizeSpecTreeFilesystemEntry(
4844
5435
  { type: recordType, relativePath, parentId: context.parentId },
4845
- context.registry
5436
+ { registry: context.registry, schemaVersions: context.schemaVersions }
4846
5437
  );
4847
5438
  if (sourceEntry !== null) yield sourceEntry;
4848
- if (entry.isDirectory() && shouldDescendIntoDirectory(entry.name, sourceEntry, context.registry)) {
5439
+ if (entry.isDirectory() && shouldDescendIntoDirectory(sourceEntry)) {
4849
5440
  yield* walkFilesystemDirectory({
4850
- absolutePath: join10(context.absolutePath, entry.name),
5441
+ absolutePath: join13(context.absolutePath, entry.name),
4851
5442
  relativePath,
4852
5443
  registry: context.registry,
5444
+ schemaVersions: context.schemaVersions,
4853
5445
  includePath: context.includePath,
4854
5446
  parentId: sourceEntry?.type === SPEC_TREE_ENTRY_TYPE.NODE ? sourceEntry.id : context.parentId
4855
5447
  });
@@ -4907,14 +5499,8 @@ function segmentsEndWith(segments, suffix) {
4907
5499
  const start = segments.length - suffix.length;
4908
5500
  return suffix.every((value, index) => segments[start + index] === value);
4909
5501
  }
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;
5502
+ function shouldDescendIntoDirectory(sourceEntry) {
5503
+ return sourceEntry === null || sourceEntry.type === SPEC_TREE_ENTRY_TYPE.NODE;
4918
5504
  }
4919
5505
  function sourceRefForRelativePath(relativePath) {
4920
5506
  const path6 = joinSpecTreePath(SPEC_TREE_CONFIG.ROOT_DIRECTORY, relativePath);
@@ -4998,14 +5584,14 @@ function formatNextNode(node) {
4998
5584
 
4999
5585
  // src/commands/testing/discovery.ts
5000
5586
  import { readdir as readdir6 } from "fs/promises";
5001
- import { join as join11, relative as relative3, sep } from "path";
5587
+ import { join as join14, relative, sep } from "path";
5002
5588
  var TESTS_DIRECTORY_NAME = SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME;
5003
5589
  var SPEC_ROOT_DIRECTORY = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
5004
5590
  var POSIX_SEPARATOR = "/";
5005
- var ERROR_CODE_NOT_FOUND = "ENOENT";
5591
+ var ERROR_CODE_NOT_FOUND2 = "ENOENT";
5006
5592
  async function discoverTestFiles(productDir) {
5007
5593
  const found = [];
5008
- await collectTestFiles(join11(productDir, SPEC_ROOT_DIRECTORY), false, found);
5594
+ await collectTestFiles(join14(productDir, SPEC_ROOT_DIRECTORY), false, found);
5009
5595
  return found.map((absolute) => toPosixRelative(productDir, absolute)).sort(compareAscii);
5010
5596
  }
5011
5597
  async function collectTestFiles(directory, insideTestsDir, found) {
@@ -5013,11 +5599,11 @@ async function collectTestFiles(directory, insideTestsDir, found) {
5013
5599
  try {
5014
5600
  entries = await readdir6(directory, { withFileTypes: true });
5015
5601
  } catch (error) {
5016
- if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return;
5602
+ if (hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) return;
5017
5603
  throw error;
5018
5604
  }
5019
5605
  for (const entry of entries) {
5020
- const childPath = join11(directory, entry.name);
5606
+ const childPath = join14(directory, entry.name);
5021
5607
  if (entry.isDirectory()) {
5022
5608
  await collectTestFiles(childPath, insideTestsDir || entry.name === TESTS_DIRECTORY_NAME, found);
5023
5609
  } else if (insideTestsDir && entry.isFile()) {
@@ -5026,14 +5612,14 @@ async function collectTestFiles(directory, insideTestsDir, found) {
5026
5612
  }
5027
5613
  }
5028
5614
  function toPosixRelative(productDir, absolute) {
5029
- return relative3(productDir, absolute).split(sep).join(POSIX_SEPARATOR);
5615
+ return relative(productDir, absolute).split(sep).join(POSIX_SEPARATOR);
5030
5616
  }
5031
5617
  function compareAscii(left, right) {
5032
5618
  if (left < right) return -1;
5033
5619
  if (left > right) return 1;
5034
5620
  return 0;
5035
5621
  }
5036
- function hasErrorCode(error, code) {
5622
+ function hasErrorCode2(error, code) {
5037
5623
  return typeof error === "object" && error !== null && error.code === code;
5038
5624
  }
5039
5625
 
@@ -5041,7 +5627,7 @@ function hasErrorCode(error, code) {
5041
5627
  var SUCCESS_EXIT_CODE = 0;
5042
5628
  function aggregateTestExitCode(invocations) {
5043
5629
  for (const invocation of invocations) {
5044
- if (invocation.invoked && invocation.exitCode !== void 0 && invocation.exitCode !== SUCCESS_EXIT_CODE) {
5630
+ if (invocation.invoked && invocation.exitCode !== SUCCESS_EXIT_CODE) {
5045
5631
  return invocation.exitCode;
5046
5632
  }
5047
5633
  }
@@ -5091,7 +5677,7 @@ async function runTests(options, deps) {
5091
5677
  outcomes.push({
5092
5678
  runnerId: group.language.name,
5093
5679
  testPaths: group.testPaths,
5094
- exitCode: invocation.exitCode ?? SUCCESS_EXIT_CODE
5680
+ exitCode: invocation.exitCode
5095
5681
  });
5096
5682
  }
5097
5683
  }
@@ -5099,55 +5685,11 @@ async function runTests(options, deps) {
5099
5685
  }
5100
5686
 
5101
5687
  // 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
- }
5688
+ import { join as join16 } from "path";
5149
5689
 
5150
5690
  // src/testing/run-state.ts
5691
+ import { createHash as createHash4 } from "crypto";
5692
+ import { join as join15 } from "path";
5151
5693
  var TEST_RUN_STATE_STATUS = {
5152
5694
  PASSED: "passed",
5153
5695
  FAILED: "failed",
@@ -5160,11 +5702,14 @@ var TESTING_RUN_STATE_INCOMPLETE_REASON = {
5160
5702
  SHAPE_INVALID_STATE: "shape-invalid-state"
5161
5703
  };
5162
5704
  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",
5705
+ RUN_FILE_COLLISION_LIMIT: "testing run file collision limit exhausted",
5706
+ RUN_FILE_CREATE_FAILED: "testing run file create failed",
5165
5707
  STATE_ALREADY_EXISTS: "testing run state already exists",
5166
5708
  STATE_WRITE_FAILED: "testing run state write failed"
5167
5709
  };
5710
+ var TESTING_RUN_STATE_ERROR_CODE = {
5711
+ NOT_FOUND: "ENOENT"
5712
+ };
5168
5713
  var TEST_RUN_STATE_FIELDS = {
5169
5714
  BRANCH_NAME: "branchName",
5170
5715
  HEAD_SHA: "headSha",
@@ -5186,119 +5731,64 @@ var PRODUCT_INPUT_DIGEST_FIELDS = {
5186
5731
  DESCRIPTOR_ID: "descriptorId",
5187
5732
  DIGEST: "digest"
5188
5733
  };
5189
- var DEFAULT_TESTING_STORAGE = {
5190
- spxDir: ".spx",
5191
- localDir: "local",
5192
- testingDir: "testing",
5193
- runsDir: "runs",
5194
- stateFile: "state.json"
5195
- };
5196
5734
  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;
5735
+ var HEX_ENCODING3 = "hex";
5205
5736
  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
- };
5737
+ var defaultFileSystem2 = defaultFileSystem;
5219
5738
  function formatTestRunTimestamp(date) {
5220
- return formatAuditRunTimestamp(date);
5739
+ return formatRunTimestamp(date);
5221
5740
  }
5222
- function testingRunsDir(productDir, storage = DEFAULT_TESTING_STORAGE) {
5223
- return join13(productDir, storage.spxDir, storage.localDir, storage.testingDir, storage.runsDir);
5741
+ function testingRunsDir(productDir) {
5742
+ const worktreeScope = worktreeScopeDir(productDir);
5743
+ if (!worktreeScope.ok) throw new Error(worktreeScope.error);
5744
+ const result = runsDir(worktreeScope.value, STATE_STORE_DOMAIN.TEST);
5745
+ if (!result.ok) throw new Error(result.error);
5746
+ return result.value;
5224
5747
  }
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 };
5748
+ async function createTestRunFile(productDir, options = {}) {
5749
+ const worktreeScope = worktreeScopeDir(productDir);
5750
+ if (!worktreeScope.ok) return worktreeScope;
5751
+ const created = await createJsonlRunFile(worktreeScope.value, STATE_STORE_DOMAIN.TEST, options);
5752
+ if (!created.ok) return {
5753
+ ok: false,
5754
+ error: testingRunFileError(created.error)
5755
+ };
5756
+ return { ok: true, value: created.value };
5251
5757
  }
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);
5758
+ async function writeTerminalTestRunState(runFilePath, state, options = {}) {
5759
+ const written = await writeJsonlRunRecord(runFilePath, testRunStateRecord(state), options);
5760
+ if (written.ok) return written;
5761
+ if (written.error === STATE_STORE_ERROR.RECORD_ALREADY_EXISTS) {
5258
5762
  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
5763
  }
5764
+ return {
5765
+ ok: false,
5766
+ error: testingWriteError(written.error)
5767
+ };
5275
5768
  }
5276
5769
  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);
5770
+ const fs7 = options.fs ?? defaultFileSystem2;
5771
+ const runsDir2 = testingRunsDir(productDir);
5280
5772
  let entries;
5281
5773
  try {
5282
- entries = await fs7.readdir(runsDir, { withFileTypes: true });
5774
+ entries = await fs7.readdir(runsDir2, { withFileTypes: true });
5283
5775
  } catch (error) {
5284
- if (hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) {
5776
+ if (hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) {
5285
5777
  return { ok: true, value: { terminalRuns: [], incompleteRuns: [] } };
5286
5778
  }
5287
- return { ok: false, error: toErrorMessage(error) };
5779
+ return { ok: false, error: toErrorMessage2(error) };
5288
5780
  }
5289
5781
  const terminalRuns = [];
5290
5782
  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);
5783
+ for (const entry of entries.filter(isTestRunFileEntry)) {
5784
+ const runFilePath = join15(runsDir2, entry.name);
5785
+ const stateResult = await readTestRunStatePath(runFilePath, fs7);
5295
5786
  if (stateResult.ok) {
5296
- terminalRuns.push({ runDirectoryName: entry.name, runDir, statePath, state: stateResult.value });
5787
+ terminalRuns.push({ runFileName: entry.name, runFilePath, state: stateResult.value });
5297
5788
  } else {
5298
5789
  incompleteRuns.push({
5299
- runDirectoryName: entry.name,
5300
- runDir,
5301
- statePath,
5790
+ runFileName: entry.name,
5791
+ runFilePath,
5302
5792
  reason: stateResult.reason,
5303
5793
  ...stateResult.error === void 0 ? {} : { error: stateResult.error }
5304
5794
  });
@@ -5331,29 +5821,55 @@ function digestTestContents(entries) {
5331
5821
  function isStalenessMatch(recorded, current) {
5332
5822
  return recorded.testingConfigDigest === current.testingConfigDigest && recorded.discoveredTestPathsDigest === current.discoveredTestPathsDigest && recorded.discoveredTestContentDigest === current.discoveredTestContentDigest && productInputDigestsEqual(recorded.productInputDigests, current.productInputDigests);
5333
5823
  }
5334
- function extractStalenessInputs(state) {
5824
+ function extractStalenessInputs(state) {
5825
+ return {
5826
+ testingConfigDigest: state.testingConfigDigest,
5827
+ discoveredTestPathsDigest: state.discoveredTestPathsDigest,
5828
+ discoveredTestContentDigest: state.discoveredTestContentDigest,
5829
+ productInputDigests: state.productInputDigests
5830
+ };
5831
+ }
5832
+ function testRunStateRecord(state) {
5335
5833
  return {
5834
+ branchName: state.branchName,
5835
+ headSha: state.headSha,
5336
5836
  testingConfigDigest: state.testingConfigDigest,
5837
+ runnerOutcomes: state.runnerOutcomes.map((outcome) => ({
5838
+ runnerId: outcome.runnerId,
5839
+ testPaths: outcome.testPaths,
5840
+ exitCode: outcome.exitCode
5841
+ })),
5337
5842
  discoveredTestPathsDigest: state.discoveredTestPathsDigest,
5338
5843
  discoveredTestContentDigest: state.discoveredTestContentDigest,
5339
- productInputDigests: state.productInputDigests
5844
+ productInputDigests: state.productInputDigests.map((digest) => ({
5845
+ descriptorId: digest.descriptorId,
5846
+ digest: digest.digest
5847
+ })),
5848
+ startedAt: state.startedAt,
5849
+ completedAt: state.completedAt,
5850
+ status: state.status
5340
5851
  };
5341
5852
  }
5342
- async function readTestRunStatePath(statePath, fs7) {
5343
- let raw;
5853
+ async function readTestRunStatePath(runFilePath, fs7) {
5854
+ let content;
5344
5855
  try {
5345
- raw = await fs7.readFile(statePath, UTF8_ENCODING2);
5856
+ content = await fs7.readFile(runFilePath, "utf8");
5346
5857
  } 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) };
5858
+ return {
5859
+ ok: false,
5860
+ reason: hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND) ? TESTING_RUN_STATE_INCOMPLETE_REASON.MISSING_STATE : TESTING_RUN_STATE_INCOMPLETE_REASON.IO_ERROR,
5861
+ error: toErrorMessage2(error)
5862
+ };
5863
+ }
5864
+ const latest = latestNonEmptyJsonlLine(content);
5865
+ if (latest === void 0) {
5866
+ return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE };
5351
5867
  }
5352
5868
  let parsed;
5353
5869
  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) };
5870
+ parsed = JSON.parse(latest);
5871
+ } catch {
5872
+ return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE };
5357
5873
  }
5358
5874
  const validated = validateTestRunState(parsed);
5359
5875
  if (!validated.ok) {
@@ -5375,8 +5891,8 @@ function validateTestRunState(value) {
5375
5891
  if (!discoveredTestPathsDigest.ok) return discoveredTestPathsDigest;
5376
5892
  const discoveredTestContentDigest = readString(value, TEST_RUN_STATE_FIELDS.DISCOVERED_TEST_CONTENT_DIGEST);
5377
5893
  if (!discoveredTestContentDigest.ok) return discoveredTestContentDigest;
5378
- const productInputDigests = readProductInputDigests(value[TEST_RUN_STATE_FIELDS.PRODUCT_INPUT_DIGESTS]);
5379
- if (!productInputDigests.ok) return productInputDigests;
5894
+ const productInputDigests2 = readProductInputDigests(value[TEST_RUN_STATE_FIELDS.PRODUCT_INPUT_DIGESTS]);
5895
+ if (!productInputDigests2.ok) return productInputDigests2;
5380
5896
  const startedAt = readString(value, TEST_RUN_STATE_FIELDS.STARTED_AT);
5381
5897
  if (!startedAt.ok) return startedAt;
5382
5898
  const completedAt = readString(value, TEST_RUN_STATE_FIELDS.COMPLETED_AT);
@@ -5392,7 +5908,7 @@ function validateTestRunState(value) {
5392
5908
  runnerOutcomes: runnerOutcomes.value,
5393
5909
  discoveredTestPathsDigest: discoveredTestPathsDigest.value,
5394
5910
  discoveredTestContentDigest: discoveredTestContentDigest.value,
5395
- productInputDigests: productInputDigests.value,
5911
+ productInputDigests: productInputDigests2.value,
5396
5912
  startedAt: startedAt.value,
5397
5913
  completedAt: completedAt.value,
5398
5914
  status: status.value
@@ -5456,7 +5972,7 @@ function compareTerminalRuns(left, right) {
5456
5972
  if (completed !== 0) return completed;
5457
5973
  const started = compareAsciiStrings(left.state.startedAt, right.state.startedAt);
5458
5974
  if (started !== 0) return started;
5459
- return compareAsciiStrings(left.runDirectoryName, right.runDirectoryName);
5975
+ return compareAsciiStrings(left.runFileName, right.runFileName);
5460
5976
  }
5461
5977
  function productInputDigestsEqual(left, right) {
5462
5978
  return canonicalProductInputDigests(left) === canonicalProductInputDigests(right);
@@ -5465,37 +5981,50 @@ function canonicalProductInputDigests(digests) {
5465
5981
  const normalized = [...digests].map((digest) => [digest.descriptorId, digest.digest]).sort((left, right) => compareAsciiStrings(left.join(SEGMENT_SEPARATOR), right.join(SEGMENT_SEPARATOR)));
5466
5982
  return JSON.stringify(normalized);
5467
5983
  }
5468
- function compareAsciiStrings(left, right) {
5469
- if (left < right) return -1;
5470
- if (left > right) return 1;
5471
- return 0;
5984
+ function isTestRunFileEntry(entry) {
5985
+ return entry.isFile() && isRunFileName(entry.name);
5986
+ }
5987
+ function testingRunFileError(error) {
5988
+ const stateStoreError = parseStateStoreError(error);
5989
+ if (stateStoreError?.code === STATE_STORE_ERROR.RUN_FILE_COLLISION_LIMIT) {
5990
+ return TESTING_RUN_STATE_ERROR.RUN_FILE_COLLISION_LIMIT;
5991
+ }
5992
+ if (stateStoreError?.code === STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED) {
5993
+ return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.RUN_FILE_CREATE_FAILED, stateStoreError.detail);
5994
+ }
5995
+ return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.RUN_FILE_CREATE_FAILED, error);
5996
+ }
5997
+ function testingWriteError(error) {
5998
+ const stateStoreError = parseStateStoreError(error);
5999
+ if (stateStoreError?.code === STATE_STORE_ERROR.RECORD_WRITE_FAILED) {
6000
+ return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED, stateStoreError.detail);
6001
+ }
6002
+ return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED, error);
5472
6003
  }
5473
- function isTestRunDirectoryEntry(entry) {
5474
- return entry.isDirectory() && RUN_DIRECTORY_NAME_PATTERN.test(entry.name);
6004
+ function withDomainErrorDetail(domainError, detail) {
6005
+ return detail === void 0 ? domainError : `${domainError}: ${detail}`;
5475
6006
  }
5476
6007
  function isRecord4(value) {
5477
6008
  return typeof value === "object" && value !== null && !Array.isArray(value);
5478
6009
  }
5479
6010
  function sha256Hex(value) {
5480
- return createHash3(SHA256_ALGORITHM2).update(value).digest(HEX_ENCODING2);
5481
- }
5482
- function generateHexId(size, randomBytes) {
5483
- return randomBytes(size).toString(HEX_ENCODING2);
6011
+ return createHash4(SHA256_ALGORITHM2).update(value).digest(HEX_ENCODING3);
5484
6012
  }
5485
- function hasErrorCode2(error, code) {
5486
- return isRecord4(error) && error.code === code;
5487
- }
5488
- function toErrorMessage(error) {
6013
+ function toErrorMessage2(error) {
5489
6014
  return error instanceof Error ? error.message : String(error);
5490
6015
  }
5491
6016
 
5492
6017
  // src/commands/testing/run-command.ts
5493
6018
  var NO_GIT_IDENTITY = "(unknown)";
5494
6019
  var TEXT_ENCODING = "utf8";
5495
- var NO_PRODUCT_INPUT_DIGESTS = [];
6020
+ var PRODUCT_INPUT_FIELDS = {
6021
+ PATH: "path",
6022
+ PRESENT: "present",
6023
+ CONTENT: "content"
6024
+ };
5496
6025
  function resolveRecordingDependencies(deps) {
5497
6026
  return {
5498
- fs: deps.fs ?? defaultFileSystem,
6027
+ fs: deps.fs ?? defaultFileSystem2,
5499
6028
  now: deps.now ?? (() => /* @__PURE__ */ new Date()),
5500
6029
  git: deps.git
5501
6030
  };
@@ -5518,28 +6047,74 @@ function coveredTestPaths(dispatch) {
5518
6047
  async function readCoveredContents(productDir, paths, fs7) {
5519
6048
  const entries = [];
5520
6049
  for (const path6 of paths) {
5521
- entries.push({ path: path6, content: await fs7.readFile(join14(productDir, path6), TEXT_ENCODING) });
6050
+ entries.push({ path: path6, content: await fs7.readFile(join16(productDir, path6), TEXT_ENCODING) });
6051
+ }
6052
+ return entries;
6053
+ }
6054
+ async function readProductInputEntries(productDir, paths, fs7) {
6055
+ const entries = [];
6056
+ for (const path6 of [...paths].sort(compareAsciiStrings)) {
6057
+ try {
6058
+ entries.push({
6059
+ [PRODUCT_INPUT_FIELDS.PATH]: path6,
6060
+ [PRODUCT_INPUT_FIELDS.PRESENT]: true,
6061
+ [PRODUCT_INPUT_FIELDS.CONTENT]: await fs7.readFile(join16(productDir, path6), TEXT_ENCODING)
6062
+ });
6063
+ } catch (error) {
6064
+ if (!hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) throw error;
6065
+ entries.push({
6066
+ [PRODUCT_INPUT_FIELDS.PATH]: path6,
6067
+ [PRODUCT_INPUT_FIELDS.PRESENT]: false
6068
+ });
6069
+ }
5522
6070
  }
5523
6071
  return entries;
5524
6072
  }
6073
+ async function digestLanguageProductInputs(productDir, language, coveredPaths, fs7) {
6074
+ const entries = await readProductInputEntries(productDir, languageProductInputPaths(language, coveredPaths), fs7);
6075
+ const digest = digestDescriptorSection(entries, `${language.name} product inputs`);
6076
+ if (!digest.ok) {
6077
+ throw new Error(`failed to digest ${language.name} product inputs: ${digest.error}`);
6078
+ }
6079
+ return digest.value.sha256;
6080
+ }
6081
+ async function productInputDigests(productDir, registry, coveredPaths, fs7) {
6082
+ const digests = [];
6083
+ for (const language of registry.languages) {
6084
+ digests.push({
6085
+ descriptorId: language.name,
6086
+ digest: await digestLanguageProductInputs(productDir, language, coveredPaths, fs7)
6087
+ });
6088
+ }
6089
+ return digests;
6090
+ }
5525
6091
  function deriveStatus(outcomes) {
5526
6092
  const allPassed = outcomes.every((outcome) => outcome.exitCode === SUCCESS_EXIT_CODE);
5527
6093
  return allPassed ? TEST_RUN_STATE_STATUS.PASSED : TEST_RUN_STATE_STATUS.FAILED;
5528
6094
  }
5529
- async function currentStalenessInputs(productDir, coveredPaths, deps = {}) {
5530
- const fs7 = deps.fs ?? defaultFileSystem;
6095
+ async function currentStalenessInputs(productDir, coveredPaths, deps) {
6096
+ const fs7 = deps.fs ?? defaultFileSystem2;
5531
6097
  const testingConfigDigest = deps.testingConfigDigest ?? (await resolveTestingConfig(productDir)).digest;
5532
6098
  const contents = await readCoveredContents(productDir, coveredPaths, fs7);
5533
6099
  return {
5534
6100
  testingConfigDigest,
5535
6101
  discoveredTestPathsDigest: digestTestPaths(coveredPaths),
5536
6102
  discoveredTestContentDigest: digestTestContents(contents),
5537
- productInputDigests: NO_PRODUCT_INPUT_DIGESTS
6103
+ productInputDigests: await productInputDigests(productDir, deps.registry, coveredPaths, fs7)
5538
6104
  };
5539
6105
  }
5540
- async function recordRun(directory, productDir, dispatch, recording, testingConfigDigest) {
6106
+ function languageProductInputPaths(language, coveredPaths) {
6107
+ return [
6108
+ .../* @__PURE__ */ new Set([
6109
+ ...language.productInputPaths,
6110
+ ...language.coveredProductInputPaths?.(coveredPaths) ?? []
6111
+ ])
6112
+ ].sort(compareAsciiStrings);
6113
+ }
6114
+ async function recordRun(runFile, productDir, dispatch, recording, registry, testingConfigDigest) {
5541
6115
  const staleness = await currentStalenessInputs(productDir, coveredTestPaths(dispatch), {
5542
6116
  fs: recording.fs,
6117
+ registry,
5543
6118
  testingConfigDigest
5544
6119
  });
5545
6120
  const branchName = await getCurrentBranch(productDir, recording.git) ?? NO_GIT_IDENTITY;
@@ -5552,43 +6127,43 @@ async function recordRun(directory, productDir, dispatch, recording, testingConf
5552
6127
  discoveredTestPathsDigest: staleness.discoveredTestPathsDigest,
5553
6128
  discoveredTestContentDigest: staleness.discoveredTestContentDigest,
5554
6129
  productInputDigests: staleness.productInputDigests,
5555
- startedAt: directory.startedAt,
6130
+ startedAt: runFile.startedAt,
5556
6131
  completedAt: formatTestRunTimestamp(recording.now()),
5557
6132
  status: deriveStatus(dispatch.outcomes)
5558
6133
  };
5559
- const written = await writeTerminalTestRunState(directory.runDir, state, { fs: recording.fs });
6134
+ const written = await writeTerminalTestRunState(runFile.runFilePath, state, { fs: recording.fs });
5560
6135
  if (!written.ok) {
5561
6136
  throw new Error(`failed to record test run: ${written.error}`);
5562
6137
  }
5563
6138
  return state;
5564
6139
  }
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}`);
6140
+ async function reserveRunFile(productDir, recording) {
6141
+ const runFile = await createTestRunFile(productDir, { fs: recording.fs, now: recording.now });
6142
+ if (!runFile.ok) {
6143
+ throw new Error(`failed to create test run file: ${runFile.error}`);
5569
6144
  }
5570
- return directory.value;
6145
+ return runFile.value;
5571
6146
  }
5572
6147
  async function runTestsCommand(options, deps) {
5573
6148
  const { config, digest } = await resolveTestingConfig(options.productDir);
5574
6149
  const passingScope = options.passing ? config.passingScope : void 0;
5575
6150
  const recording = resolveRecordingDependencies(deps);
5576
- const directory = await reserveRunDirectory(options.productDir, recording);
6151
+ const runFile = await reserveRunFile(options.productDir, recording);
5577
6152
  const dispatch = await runTests(
5578
6153
  { productDir: options.productDir, registry: deps.registry, passingScope },
5579
6154
  { runnerDepsFor: deps.runnerDepsFor }
5580
6155
  );
5581
- const recorded = await recordRun(directory, options.productDir, dispatch, recording, digest);
6156
+ const recorded = await recordRun(runFile, options.productDir, dispatch, recording, deps.registry, digest);
5582
6157
  return { dispatch, recorded };
5583
6158
  }
5584
6159
  async function runNodeCommand(options, deps) {
5585
6160
  const recording = resolveRecordingDependencies(deps);
5586
- const directory = await reserveRunDirectory(options.productDir, recording);
6161
+ const runFile = await reserveRunFile(options.productDir, recording);
5587
6162
  const dispatch = await runTests(
5588
6163
  { productDir: options.productDir, registry: deps.registry, passingScope: { include: [options.nodePath] } },
5589
6164
  { runnerDepsFor: deps.runnerDepsFor }
5590
6165
  );
5591
- const recorded = await recordRun(directory, options.productDir, dispatch, recording);
6166
+ const recorded = await recordRun(runFile, options.productDir, dispatch, recording, deps.registry);
5592
6167
  return { dispatch, recorded };
5593
6168
  }
5594
6169
 
@@ -5596,19 +6171,37 @@ async function runNodeCommand(options, deps) {
5596
6171
  var PATH_SEPARATOR = "/";
5597
6172
  function createNodeOutcomeResolver(deps) {
5598
6173
  let evidence;
6174
+ const currentInputsByCoveredPaths = /* @__PURE__ */ new Map();
5599
6175
  const sharedEvidence = () => evidence ??= loadResolverEvidence(deps);
6176
+ const refreshEvidence = () => {
6177
+ evidence = loadResolverEvidence(deps);
6178
+ };
6179
+ const currentInputsFor = (coveredPaths) => {
6180
+ const cacheKey = coveredPathCollectionKey(coveredPaths);
6181
+ const cached = currentInputsByCoveredPaths.get(cacheKey);
6182
+ if (cached !== void 0) {
6183
+ return cached;
6184
+ }
6185
+ const current = currentStalenessInputs(deps.productDir, coveredPaths, deps);
6186
+ currentInputsByCoveredPaths.set(cacheKey, current);
6187
+ return current;
6188
+ };
5600
6189
  return async (nodeId) => {
5601
6190
  const { discoveredTestPaths, terminalRuns } = await sharedEvidence();
5602
6191
  const nodePath = `${SPEC_TREE_CONFIG.ROOT_DIRECTORY}${PATH_SEPARATOR}${nodeId}`;
5603
6192
  const nodeTestPaths = filterNodeTestPaths(discoveredTestPaths, nodePath);
5604
- const usable = await usableRecordedOutcome(deps, discoveredTestPaths, terminalRuns, nodeTestPaths);
6193
+ const usable = await usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor);
5605
6194
  if (usable !== void 0) {
5606
6195
  return usable;
5607
6196
  }
5608
6197
  const { dispatch, recorded } = await runNodeCommand({ productDir: deps.productDir, nodePath }, deps);
6198
+ refreshEvidence();
5609
6199
  return outcomesCoverPaths(dispatch.outcomes, nodeTestPaths) && recorded.status === TEST_RUN_STATE_STATUS.PASSED;
5610
6200
  };
5611
6201
  }
6202
+ function coveredPathCollectionKey(coveredPaths) {
6203
+ return JSON.stringify([...coveredPaths].sort());
6204
+ }
5612
6205
  async function loadResolverEvidence(deps) {
5613
6206
  const discoveredTestPaths = await discoverTestFiles(deps.productDir);
5614
6207
  const runs = await readTestingRuns(deps.productDir, deps);
@@ -5618,7 +6211,7 @@ function filterNodeTestPaths(discoveredTestPaths, nodePath) {
5618
6211
  const prefix = `${nodePath}${PATH_SEPARATOR}`;
5619
6212
  return discoveredTestPaths.filter((path6) => path6.startsWith(prefix));
5620
6213
  }
5621
- async function usableRecordedOutcome(deps, discoveredTestPaths, terminalRuns, nodeTestPaths) {
6214
+ async function usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor) {
5622
6215
  const latest = selectLatestTerminalTestRunForNode(terminalRuns, nodeTestPaths);
5623
6216
  if (latest === void 0) {
5624
6217
  return void 0;
@@ -5628,7 +6221,7 @@ async function usableRecordedOutcome(deps, discoveredTestPaths, terminalRuns, no
5628
6221
  if (!runCoveredPaths.every((path6) => presentTestPaths.has(path6))) {
5629
6222
  return void 0;
5630
6223
  }
5631
- const current = await currentStalenessInputs(deps.productDir, runCoveredPaths, deps);
6224
+ const current = await currentInputsFor(runCoveredPaths);
5632
6225
  const fresh = isStalenessMatch(extractStalenessInputs(latest.state), current);
5633
6226
  return fresh && latest.state.status === TEST_RUN_STATE_STATUS.PASSED ? true : void 0;
5634
6227
  }
@@ -5648,11 +6241,11 @@ function serializeNodeStatus(state) {
5648
6241
  }
5649
6242
 
5650
6243
  // src/lib/node-status/provider.ts
5651
- import { join as join16 } from "path";
6244
+ import { join as join18 } from "path";
5652
6245
 
5653
6246
  // src/lib/node-status/read.ts
5654
6247
  import { readFileSync as readFileSync2 } from "fs";
5655
- import { join as join15 } from "path";
6248
+ import { join as join17 } from "path";
5656
6249
  var NODE_STATUS_FILENAME = "spx.status.json";
5657
6250
  var NODE_STATUS_VALUES = new Set(Object.values(SPEC_TREE_NODE_STATE));
5658
6251
  function isNodeError2(error) {
@@ -5662,7 +6255,7 @@ function isSpecTreeNodeState(value) {
5662
6255
  return typeof value === "string" && NODE_STATUS_VALUES.has(value);
5663
6256
  }
5664
6257
  function readNodeStatus(nodeDir) {
5665
- const filePath = join15(nodeDir, NODE_STATUS_FILENAME);
6258
+ const filePath = join17(nodeDir, NODE_STATUS_FILENAME);
5666
6259
  let content;
5667
6260
  try {
5668
6261
  content = readFileSync2(filePath, "utf8");
@@ -5684,7 +6277,7 @@ function readNodeStatus(nodeDir) {
5684
6277
 
5685
6278
  // src/lib/node-status/provider.ts
5686
6279
  function nodeDirectory(productDir, node) {
5687
- return join16(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
6280
+ return join18(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
5688
6281
  }
5689
6282
  function createNodeStatusProvider(productDir) {
5690
6283
  return {
@@ -5696,7 +6289,7 @@ function createNodeStatusProvider(productDir) {
5696
6289
 
5697
6290
  // src/lib/node-status/update.ts
5698
6291
  import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
5699
- import { dirname as dirname3, join as join17 } from "path";
6292
+ import { dirname as dirname4, join as join19 } from "path";
5700
6293
  var NODE_STATUS_TEXT_ENCODING = "utf8";
5701
6294
  async function updateNodeStatus(options) {
5702
6295
  const { productDir, resolveOutcome } = options;
@@ -5734,8 +6327,8 @@ function isNodeExcluded(ignoreReader, node) {
5734
6327
  return ignoreReader.isUnderIgnoreSource(reference);
5735
6328
  }
5736
6329
  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 });
6330
+ const filePath = join19(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
6331
+ await mkdir4(dirname4(filePath), { recursive: true });
5739
6332
  await writeFile2(filePath, serializeNodeStatus(state), NODE_STATUS_TEXT_ENCODING);
5740
6333
  }
5741
6334
 
@@ -5849,26 +6442,95 @@ function formatNodeLabel(node) {
5849
6442
  }
5850
6443
 
5851
6444
  // src/testing/languages/python.ts
5852
- import { basename } from "path";
6445
+ import { basename as basename2, dirname as dirname5, join as join20 } from "path/posix";
6446
+
6447
+ // src/validation/discovery/language-finder.ts
6448
+ import fs5 from "fs";
6449
+ import path4 from "path";
6450
+ var TYPESCRIPT_MARKER = "tsconfig.json";
6451
+ var PYTHON_MARKER = "pyproject.toml";
6452
+ var ESLINT_CONFIG_FILES = [
6453
+ "eslint.config.ts",
6454
+ "eslint.config.js",
6455
+ "eslint.config.mjs",
6456
+ "eslint.config.cjs"
6457
+ ];
6458
+ var ESLINT_PRODUCTION_CONFIG_FILES = [
6459
+ "eslint.config.production.ts",
6460
+ "eslint.config.production.js",
6461
+ "eslint.config.production.mjs",
6462
+ "eslint.config.production.cjs"
6463
+ ];
6464
+ var defaultLanguageDetectionDeps = {
6465
+ existsSync: fs5.existsSync
6466
+ };
6467
+ function detectTypeScript(projectRoot, deps = defaultLanguageDetectionDeps) {
6468
+ const present = deps.existsSync(path4.join(projectRoot, TYPESCRIPT_MARKER));
6469
+ if (!present) {
6470
+ return { present: false };
6471
+ }
6472
+ const eslintConfigFile = ESLINT_CONFIG_FILES.find(
6473
+ (configFile) => deps.existsSync(path4.join(projectRoot, configFile))
6474
+ );
6475
+ const productionEslintConfigFile = ESLINT_PRODUCTION_CONFIG_FILES.find(
6476
+ (configFile) => deps.existsSync(path4.join(projectRoot, configFile))
6477
+ );
6478
+ return { present: true, eslintConfigFile, productionEslintConfigFile };
6479
+ }
6480
+ function detectPython(projectRoot, deps = defaultLanguageDetectionDeps) {
6481
+ const present = deps.existsSync(path4.join(projectRoot, PYTHON_MARKER));
6482
+ return { present };
6483
+ }
6484
+
6485
+ // src/testing/languages/python-pytest-contract.ts
6486
+ var PYTHON_PYTEST_IGNORE_FLAG_PREFIX = "--ignore=spx/";
6487
+ var PYTHON_PYTEST_IGNORE_FLAG_SUFFIX = "/";
6488
+ var UV_COMMAND = "uv";
6489
+ var PYTEST_INVOKE_ARGS = ["run", "--active", "pytest"];
6490
+
6491
+ // src/testing/languages/python.ts
5853
6492
  var PYTHON_TESTING_LANGUAGE_NAME = "python";
6493
+ var PYTHON_PRODUCT_INPUT_PATH = {
6494
+ CONFTEST: "conftest.py",
6495
+ HIDDEN_PYTEST_INI: ".pytest.ini",
6496
+ HIDDEN_PYTEST_TOML: ".pytest.toml",
6497
+ PYPROJECT: "pyproject.toml",
6498
+ PYTEST_TOML: "pytest.toml",
6499
+ PYTEST_INI: "pytest.ini",
6500
+ SETUP_CFG: "setup.cfg",
6501
+ SETUP_PY: "setup.py",
6502
+ TOX_INI: "tox.ini",
6503
+ UV_LOCK: "uv.lock"
6504
+ };
6505
+ var PYTHON_PRODUCT_INPUT_PATHS = Object.values(PYTHON_PRODUCT_INPUT_PATH);
5854
6506
  var PYTHON_TEST_FILE_PREFIX = "test_";
5855
6507
  var PYTHON_TEST_FILE_EXTENSION = ".py";
5856
6508
  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
6509
  function matchesTestFile(filePath) {
5862
- return basename(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
6510
+ return basename2(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
6511
+ }
6512
+ function coveredProductInputPaths(coveredTestPaths2) {
6513
+ const paths = /* @__PURE__ */ new Set();
6514
+ for (const testPath of coveredTestPaths2) {
6515
+ if (!matchesTestFile(testPath)) continue;
6516
+ let directory = dirname5(testPath);
6517
+ while (directory !== "." && directory.length > 0) {
6518
+ paths.add(join20(directory, PYTHON_PRODUCT_INPUT_PATH.CONFTEST));
6519
+ const parent = dirname5(directory);
6520
+ if (parent === directory) break;
6521
+ directory = parent;
6522
+ }
6523
+ }
6524
+ return [...paths].sort(compareAsciiStrings);
5863
6525
  }
5864
6526
  function excludeFlag(nodePath) {
5865
6527
  return `${PYTHON_PYTEST_IGNORE_FLAG_PREFIX}${nodePath}${PYTHON_PYTEST_IGNORE_FLAG_SUFFIX}`;
5866
6528
  }
5867
6529
  function detect(projectRoot, deps) {
5868
- return deps.isLanguagePresent(projectRoot);
6530
+ return deps?.isLanguagePresent?.(projectRoot) ?? detectPython(projectRoot).present;
5869
6531
  }
5870
6532
  async function runTests2(request, deps) {
5871
- if (!deps.isLanguagePresent(request.projectRoot)) {
6533
+ if (!detect(request.projectRoot, deps)) {
5872
6534
  return { invoked: false };
5873
6535
  }
5874
6536
  const args = [
@@ -5882,6 +6544,8 @@ async function runTests2(request, deps) {
5882
6544
  var pythonTestingLanguage = {
5883
6545
  name: PYTHON_TESTING_LANGUAGE_NAME,
5884
6546
  testFilePatterns: PYTHON_TEST_FILE_PATTERNS,
6547
+ productInputPaths: PYTHON_PRODUCT_INPUT_PATHS,
6548
+ coveredProductInputPaths,
5885
6549
  matchesTestFile,
5886
6550
  excludeFlag,
5887
6551
  detect,
@@ -5892,6 +6556,15 @@ var pythonTestingLanguage = {
5892
6556
  var TYPESCRIPT_TESTING_LANGUAGE_NAME = "typescript";
5893
6557
  var TYPESCRIPT_TEST_FILE_PATTERNS = ["*.test.ts", "*.test.tsx"];
5894
6558
  var TYPESCRIPT_TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx"];
6559
+ var TYPESCRIPT_PRODUCT_INPUT_PATHS = [
6560
+ "package.json",
6561
+ "pnpm-lock.yaml",
6562
+ "tsconfig.json",
6563
+ "vitest.config.js",
6564
+ "vitest.config.mjs",
6565
+ "vitest.config.ts",
6566
+ "vitest.config.mts"
6567
+ ];
5895
6568
  var TYPESCRIPT_VITEST_EXCLUDE_FLAG_PREFIX = "--exclude=spx/";
5896
6569
  var TYPESCRIPT_VITEST_EXCLUDE_FLAG_SUFFIX = "/**";
5897
6570
  var PACKAGE_MANAGER_COMMAND = "pnpm";
@@ -5904,10 +6577,10 @@ function excludeFlag2(nodePath) {
5904
6577
  return `${TYPESCRIPT_VITEST_EXCLUDE_FLAG_PREFIX}${nodePath}${TYPESCRIPT_VITEST_EXCLUDE_FLAG_SUFFIX}`;
5905
6578
  }
5906
6579
  function detect2(projectRoot, deps) {
5907
- return deps.isLanguagePresent(projectRoot);
6580
+ return deps?.isLanguagePresent?.(projectRoot) ?? detectTypeScript(projectRoot).present;
5908
6581
  }
5909
6582
  async function runTests3(request, deps) {
5910
- if (!deps.isLanguagePresent(request.projectRoot)) {
6583
+ if (!detect2(request.projectRoot, deps)) {
5911
6584
  return { invoked: false };
5912
6585
  }
5913
6586
  const args = [
@@ -5923,6 +6596,7 @@ async function runTests3(request, deps) {
5923
6596
  var typescriptTestingLanguage = {
5924
6597
  name: TYPESCRIPT_TESTING_LANGUAGE_NAME,
5925
6598
  testFilePatterns: TYPESCRIPT_TEST_FILE_PATTERNS,
6599
+ productInputPaths: TYPESCRIPT_PRODUCT_INPUT_PATHS,
5926
6600
  matchesTestFile: matchesTestFile2,
5927
6601
  excludeFlag: excludeFlag2,
5928
6602
  detect: detect2,
@@ -6078,51 +6752,8 @@ function spawnManagedSubprocess(runner, command, args, options) {
6078
6752
  });
6079
6753
  }
6080
6754
 
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
6755
  // src/interfaces/cli/testing-runner-deps.ts
6120
6756
  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
6757
  function createCommandRunner(productDir, outStream) {
6127
6758
  return (command, args) => new Promise((resolveResult) => {
6128
6759
  const child = spawnManagedSubprocess(lifecycleProcessRunner, command, args, {
@@ -6136,13 +6767,7 @@ function createCommandRunner(productDir, outStream) {
6136
6767
  }
6137
6768
  function createRunnerDepsFor(productDir, outStream = process.stdout) {
6138
6769
  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
- };
6770
+ return () => ({ runCommand });
6146
6771
  }
6147
6772
 
6148
6773
  // src/interfaces/cli/spec.ts
@@ -6273,10 +6898,10 @@ var testingDomain = {
6273
6898
  };
6274
6899
 
6275
6900
  // src/commands/validation/markdown.ts
6276
- import { relative as relative5 } from "path";
6901
+ import { relative as relative3 } from "path";
6277
6902
 
6278
6903
  // src/validation/config/path-filter.ts
6279
- import { isAbsolute as isAbsolute2, relative as relative4 } from "path";
6904
+ import { isAbsolute as isAbsolute2, relative as relative2 } from "path";
6280
6905
  var PATH_PREFIX_SEPARATOR = "/";
6281
6906
  function hasEffectiveValidationPathMetadata(filter) {
6282
6907
  return "hasIncludeFilter" in filter && typeof filter.hasIncludeFilter === "boolean" && "noMatchingIncludes" in filter && typeof filter.noMatchingIncludes === "boolean";
@@ -6296,7 +6921,7 @@ function nonEmpty(values) {
6296
6921
  return values?.filter((value) => value.length > 0) ?? [];
6297
6922
  }
6298
6923
  function toProjectRelativeValidationPath(projectRoot, path6) {
6299
- return isAbsolute2(path6) ? relative4(projectRoot, path6) : path6;
6924
+ return isAbsolute2(path6) ? relative2(projectRoot, path6) : path6;
6300
6925
  }
6301
6926
  function intersectIncludes(baseInclude, toolInclude) {
6302
6927
  const base = nonEmpty(baseInclude);
@@ -6371,8 +6996,8 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
6371
6996
  }
6372
6997
 
6373
6998
  // 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";
6999
+ import { existsSync, statSync } from "fs";
7000
+ import { basename as basename3, dirname as dirname6, join as join21, relative as pathRelative } from "path";
6376
7001
  import { main as markdownlintMain } from "markdownlint-cli2";
6377
7002
  import relativeLinksRule from "markdownlint-rule-relative-links";
6378
7003
  var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
@@ -6411,7 +7036,7 @@ function buildMarkdownlintConfig(directoryName) {
6411
7036
  };
6412
7037
  }
6413
7038
  function getDefaultDirectories(projectRoot) {
6414
- return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join18(projectRoot, name)).filter((dir) => existsSync2(dir));
7039
+ return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join21(projectRoot, name)).filter((dir) => existsSync(dir));
6415
7040
  }
6416
7041
  function resolveMarkdownValidationTarget(path6, deps = defaultMarkdownValidationTargetDeps) {
6417
7042
  if (isExistingDirectory(path6, deps)) {
@@ -6470,7 +7095,7 @@ async function validateMarkdown(options) {
6470
7095
  async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
6471
7096
  const errors = [];
6472
7097
  const directory = targetDirectory(target);
6473
- const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename2(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
7098
+ const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename3(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
6474
7099
  const { customRules, ...markdownlintConfig } = config;
6475
7100
  const optionsOverride = {
6476
7101
  config: {
@@ -6494,7 +7119,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
6494
7119
  if (parsed) {
6495
7120
  errors.push({
6496
7121
  ...parsed,
6497
- file: join18(directory, parsed.file)
7122
+ file: join21(directory, parsed.file)
6498
7123
  });
6499
7124
  }
6500
7125
  }
@@ -6502,7 +7127,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
6502
7127
  return errors;
6503
7128
  }
6504
7129
  function targetDirectory(target) {
6505
- return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname4(target.path) : target.path;
7130
+ return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname6(target.path) : target.path;
6506
7131
  }
6507
7132
  function hasMarkdownExtension(path6) {
6508
7133
  const lastDot = path6.lastIndexOf(".");
@@ -6530,7 +7155,7 @@ function markdownlintConfigDirectoryName(directory, projectRoot) {
6530
7155
  return rootSegment;
6531
7156
  }
6532
7157
  }
6533
- return basename2(directory);
7158
+ return basename3(directory);
6534
7159
  }
6535
7160
 
6536
7161
  // src/commands/validation/messages.ts
@@ -6606,7 +7231,7 @@ async function markdownCommand(options) {
6606
7231
  path: path6
6607
7232
  }));
6608
7233
  const targets = unfilteredTargets.filter(
6609
- (target) => pathPassesValidationFilter(relative5(cwd, target.path), pathFilter)
7234
+ (target) => pathPassesValidationFilter(relative3(cwd, target.path), pathFilter)
6610
7235
  );
6611
7236
  const skippedTargets = targetResolutions === void 0 ? [] : targetResolutions.map((resolution) => resolution.skipped).filter((skipped) => skipped !== void 0);
6612
7237
  const skippedOutput = quiet ? [] : skippedTargets.map(formatSkippedFileScope);
@@ -7199,39 +7824,39 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7199
7824
  const token = _scanner.scan();
7200
7825
  switch (_scanner.getTokenError()) {
7201
7826
  case 4:
7202
- handleError2(
7827
+ handleError3(
7203
7828
  14
7204
7829
  /* ParseErrorCode.InvalidUnicode */
7205
7830
  );
7206
7831
  break;
7207
7832
  case 5:
7208
- handleError2(
7833
+ handleError3(
7209
7834
  15
7210
7835
  /* ParseErrorCode.InvalidEscapeCharacter */
7211
7836
  );
7212
7837
  break;
7213
7838
  case 3:
7214
- handleError2(
7839
+ handleError3(
7215
7840
  13
7216
7841
  /* ParseErrorCode.UnexpectedEndOfNumber */
7217
7842
  );
7218
7843
  break;
7219
7844
  case 1:
7220
7845
  if (!disallowComments) {
7221
- handleError2(
7846
+ handleError3(
7222
7847
  11
7223
7848
  /* ParseErrorCode.UnexpectedEndOfComment */
7224
7849
  );
7225
7850
  }
7226
7851
  break;
7227
7852
  case 2:
7228
- handleError2(
7853
+ handleError3(
7229
7854
  12
7230
7855
  /* ParseErrorCode.UnexpectedEndOfString */
7231
7856
  );
7232
7857
  break;
7233
7858
  case 6:
7234
- handleError2(
7859
+ handleError3(
7235
7860
  16
7236
7861
  /* ParseErrorCode.InvalidCharacter */
7237
7862
  );
@@ -7241,7 +7866,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7241
7866
  case 12:
7242
7867
  case 13:
7243
7868
  if (disallowComments) {
7244
- handleError2(
7869
+ handleError3(
7245
7870
  10
7246
7871
  /* ParseErrorCode.InvalidCommentToken */
7247
7872
  );
@@ -7250,7 +7875,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7250
7875
  }
7251
7876
  break;
7252
7877
  case 16:
7253
- handleError2(
7878
+ handleError3(
7254
7879
  1
7255
7880
  /* ParseErrorCode.InvalidSymbol */
7256
7881
  );
@@ -7263,7 +7888,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7263
7888
  }
7264
7889
  }
7265
7890
  }
7266
- function handleError2(error, skipUntilAfter = [], skipUntil = []) {
7891
+ function handleError3(error, skipUntilAfter = [], skipUntil = []) {
7267
7892
  onError(error);
7268
7893
  if (skipUntilAfter.length + skipUntil.length > 0) {
7269
7894
  let token = _scanner.getToken();
@@ -7295,7 +7920,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7295
7920
  const tokenValue = _scanner.getTokenValue();
7296
7921
  let value = Number(tokenValue);
7297
7922
  if (isNaN(value)) {
7298
- handleError2(
7923
+ handleError3(
7299
7924
  2
7300
7925
  /* ParseErrorCode.InvalidNumberFormat */
7301
7926
  );
@@ -7320,7 +7945,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7320
7945
  }
7321
7946
  function parseProperty() {
7322
7947
  if (_scanner.getToken() !== 10) {
7323
- handleError2(3, [], [
7948
+ handleError3(3, [], [
7324
7949
  2,
7325
7950
  5
7326
7951
  /* SyntaxKind.CommaToken */
@@ -7332,14 +7957,14 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7332
7957
  onSeparator(":");
7333
7958
  scanNext();
7334
7959
  if (!parseValue()) {
7335
- handleError2(4, [], [
7960
+ handleError3(4, [], [
7336
7961
  2,
7337
7962
  5
7338
7963
  /* SyntaxKind.CommaToken */
7339
7964
  ]);
7340
7965
  }
7341
7966
  } else {
7342
- handleError2(5, [], [
7967
+ handleError3(5, [], [
7343
7968
  2,
7344
7969
  5
7345
7970
  /* SyntaxKind.CommaToken */
@@ -7355,7 +7980,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7355
7980
  while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
7356
7981
  if (_scanner.getToken() === 5) {
7357
7982
  if (!needsComma) {
7358
- handleError2(4, [], []);
7983
+ handleError3(4, [], []);
7359
7984
  }
7360
7985
  onSeparator(",");
7361
7986
  scanNext();
@@ -7363,10 +7988,10 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7363
7988
  break;
7364
7989
  }
7365
7990
  } else if (needsComma) {
7366
- handleError2(6, [], []);
7991
+ handleError3(6, [], []);
7367
7992
  }
7368
7993
  if (!parseProperty()) {
7369
- handleError2(4, [], [
7994
+ handleError3(4, [], [
7370
7995
  2,
7371
7996
  5
7372
7997
  /* SyntaxKind.CommaToken */
@@ -7376,7 +8001,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7376
8001
  }
7377
8002
  onObjectEnd();
7378
8003
  if (_scanner.getToken() !== 2) {
7379
- handleError2(7, [
8004
+ handleError3(7, [
7380
8005
  2
7381
8006
  /* SyntaxKind.CloseBraceToken */
7382
8007
  ], []);
@@ -7393,7 +8018,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7393
8018
  while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
7394
8019
  if (_scanner.getToken() === 5) {
7395
8020
  if (!needsComma) {
7396
- handleError2(4, [], []);
8021
+ handleError3(4, [], []);
7397
8022
  }
7398
8023
  onSeparator(",");
7399
8024
  scanNext();
@@ -7401,7 +8026,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7401
8026
  break;
7402
8027
  }
7403
8028
  } else if (needsComma) {
7404
- handleError2(6, [], []);
8029
+ handleError3(6, [], []);
7405
8030
  }
7406
8031
  if (isFirstElement) {
7407
8032
  _jsonPath.push(0);
@@ -7410,7 +8035,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7410
8035
  _jsonPath[_jsonPath.length - 1]++;
7411
8036
  }
7412
8037
  if (!parseValue()) {
7413
- handleError2(4, [], [
8038
+ handleError3(4, [], [
7414
8039
  4,
7415
8040
  5
7416
8041
  /* SyntaxKind.CommaToken */
@@ -7423,7 +8048,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7423
8048
  _jsonPath.pop();
7424
8049
  }
7425
8050
  if (_scanner.getToken() !== 4) {
7426
- handleError2(8, [
8051
+ handleError3(8, [
7427
8052
  4
7428
8053
  /* SyntaxKind.CloseBracketToken */
7429
8054
  ], []);
@@ -7449,15 +8074,15 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
7449
8074
  if (options.allowEmptyContent) {
7450
8075
  return true;
7451
8076
  }
7452
- handleError2(4, [], []);
8077
+ handleError3(4, [], []);
7453
8078
  return false;
7454
8079
  }
7455
8080
  if (!parseValue()) {
7456
- handleError2(4, [], []);
8081
+ handleError3(4, [], []);
7457
8082
  return false;
7458
8083
  }
7459
8084
  if (_scanner.getToken() !== 17) {
7460
- handleError2(9, [], []);
8085
+ handleError3(9, [], []);
7461
8086
  }
7462
8087
  return true;
7463
8088
  }
@@ -7515,8 +8140,8 @@ var ParseErrorCode;
7515
8140
  })(ParseErrorCode || (ParseErrorCode = {}));
7516
8141
 
7517
8142
  // 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";
8143
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
8144
+ import { isAbsolute as isAbsolute3, join as join22 } from "path";
7520
8145
  var TSCONFIG_FILES = {
7521
8146
  full: "tsconfig.json",
7522
8147
  production: "tsconfig.production.json"
@@ -7526,11 +8151,11 @@ var GLOB_MARKER = "*";
7526
8151
  var HIDDEN_PATH_PREFIX = ".";
7527
8152
  var defaultScopeDeps = {
7528
8153
  readFileSync: readFileSync3,
7529
- existsSync: existsSync3,
8154
+ existsSync: existsSync2,
7530
8155
  readdirSync
7531
8156
  };
7532
8157
  function resolveProjectPath(projectRoot, path6) {
7533
- return isAbsolute3(path6) ? path6 : join19(projectRoot, path6);
8158
+ return isAbsolute3(path6) ? path6 : join22(projectRoot, path6);
7534
8159
  }
7535
8160
  function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
7536
8161
  try {
@@ -7574,7 +8199,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
7574
8199
  if (hasDirectTsFiles) return true;
7575
8200
  const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
7576
8201
  for (const subdir of subdirs.slice(0, 5)) {
7577
- if (hasTypeScriptFilesRecursive(join19(dirPath, subdir.name), maxDepth - 1, deps)) {
8202
+ if (hasTypeScriptFilesRecursive(join22(dirPath, subdir.name), maxDepth - 1, deps)) {
7578
8203
  return true;
7579
8204
  }
7580
8205
  }
@@ -7597,7 +8222,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
7597
8222
  });
7598
8223
  if (!isExcluded) {
7599
8224
  try {
7600
- const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join19(projectRoot, dir), 2, deps);
8225
+ const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join22(projectRoot, dir), 2, deps);
7601
8226
  if (hasTypeScriptFiles) {
7602
8227
  directories.add(dir);
7603
8228
  }
@@ -7628,13 +8253,13 @@ function getLiteralTopLevelPatternDirectory(pattern) {
7628
8253
  function filterActiveIncludePatterns(patterns, excludePatterns, projectRoot, deps) {
7629
8254
  return patterns.filter((pattern) => {
7630
8255
  const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
7631
- return topLevelDir === null || deps.existsSync(join19(projectRoot, topLevelDir));
8256
+ return topLevelDir === null || deps.existsSync(join22(projectRoot, topLevelDir));
7632
8257
  }).filter((pattern) => pathPassesValidationFilter(pattern, { exclude: excludePatterns }));
7633
8258
  }
7634
8259
  function getValidationDirectories(scope, projectRoot, deps = defaultScopeDeps) {
7635
8260
  const config = resolveTypeScriptConfig(scope, projectRoot, deps);
7636
8261
  const configDirectories = getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps);
7637
- const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join19(projectRoot, dir)));
8262
+ const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join22(projectRoot, dir)));
7638
8263
  return existingDirectories;
7639
8264
  }
7640
8265
  function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
@@ -7758,7 +8383,7 @@ function formatSkipMessage(stepName, result) {
7758
8383
 
7759
8384
  // src/validation/steps/circular.ts
7760
8385
  import madge from "madge";
7761
- import { join as join20 } from "path";
8386
+ import { join as join23 } from "path";
7762
8387
  var CIRCULAR_DEPS_KEYS = {
7763
8388
  MADGE: "madge"
7764
8389
  };
@@ -7767,11 +8392,11 @@ var defaultCircularDeps = {
7767
8392
  };
7768
8393
  async function validateCircularDependencies(scope, typescriptScope, projectRoot, deps = defaultCircularDeps) {
7769
8394
  try {
7770
- const analyzeDirectories = typescriptScope.directories.map((directory) => join20(projectRoot, directory));
8395
+ const analyzeDirectories = typescriptScope.directories.map((directory) => join23(projectRoot, directory));
7771
8396
  if (analyzeDirectories.length === 0) {
7772
8397
  return { success: true };
7773
8398
  }
7774
- const tsConfigFile = join20(projectRoot, TSCONFIG_FILES[scope]);
8399
+ const tsConfigFile = join23(projectRoot, TSCONFIG_FILES[scope]);
7775
8400
  const excludeRegExps = typescriptScope.excludePatterns.map((pattern) => {
7776
8401
  const cleanPattern = pattern.replace(/\/\*\*?\/\*$/, "");
7777
8402
  const escaped = cleanPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -7851,10 +8476,10 @@ ${cycles}`;
7851
8476
  }
7852
8477
 
7853
8478
  // src/validation/steps/knip.ts
7854
- import { existsSync as existsSync4 } from "fs";
8479
+ import { existsSync as existsSync3 } from "fs";
7855
8480
  import { mkdtemp, rm, writeFile as writeFile3 } from "fs/promises";
7856
8481
  import { tmpdir } from "os";
7857
- import { isAbsolute as isAbsolute4, join as join21 } from "path";
8482
+ import { isAbsolute as isAbsolute4, join as join24 } from "path";
7858
8483
  var defaultKnipProcessRunner = lifecycleProcessRunner;
7859
8484
  var KNIP_COMMAND_TOKENS = {
7860
8485
  COMMAND: "knip",
@@ -7862,7 +8487,7 @@ var KNIP_COMMAND_TOKENS = {
7862
8487
  USE_TSCONFIG_FILES_FLAG: "--use-tsconfig-files"
7863
8488
  };
7864
8489
  var defaultKnipDeps = {
7865
- existsSync: existsSync4,
8490
+ existsSync: existsSync3,
7866
8491
  mkdtemp,
7867
8492
  rm,
7868
8493
  writeFile: writeFile3
@@ -7882,7 +8507,7 @@ async function validateKnip2(context, runner = defaultKnipProcessRunner, deps =
7882
8507
  }
7883
8508
  async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
7884
8509
  const scopedTsconfig = typescriptScope.filteredByValidationPaths ? await createScopedKnipTsconfig(projectRoot, typescriptScope, deps) : void 0;
7885
- const localBin = join21(projectRoot, "node_modules", ".bin", "knip");
8510
+ const localBin = join24(projectRoot, "node_modules", ".bin", "knip");
7886
8511
  const binary = deps.existsSync(localBin) ? localBin : "npx";
7887
8512
  const baseArgs = scopedTsconfig === void 0 ? [] : [
7888
8513
  KNIP_COMMAND_TOKENS.USE_TSCONFIG_FILES_FLAG,
@@ -7937,12 +8562,12 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
7937
8562
  });
7938
8563
  }
7939
8564
  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);
8565
+ const tempDir = await deps.mkdtemp(join24(tmpdir(), "validate-knip-"));
8566
+ const configPath = join24(tempDir, TSCONFIG_FILES.full);
8567
+ const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join24(projectRoot, pattern);
7943
8568
  const project = typescriptScope.filePatterns.length > 0 ? typescriptScope.filePatterns : typescriptScope.directories.map((directory) => `${directory}/**/*.{js,ts,tsx}`);
7944
8569
  const config = {
7945
- extends: join21(projectRoot, TSCONFIG_FILES.full),
8570
+ extends: join24(projectRoot, TSCONFIG_FILES.full),
7946
8571
  include: project.map(toProjectPathPattern),
7947
8572
  exclude: typescriptScope.excludePatterns.map(toProjectPathPattern)
7948
8573
  };
@@ -7991,13 +8616,13 @@ async function knipCommand(options) {
7991
8616
  }
7992
8617
 
7993
8618
  // src/validation/steps/eslint.ts
7994
- import { existsSync as existsSync6 } from "fs";
7995
- import { join as join23 } from "path";
8619
+ import { existsSync as existsSync5 } from "fs";
8620
+ import { join as join26 } from "path";
7996
8621
 
7997
8622
  // src/validation/lint-policy.ts
7998
8623
  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";
8624
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
8625
+ import { join as join25 } from "path";
8001
8626
 
8002
8627
  // src/validation/lint-policy-constants.ts
8003
8628
  var LINT_POLICY_MANIFESTS = {
@@ -8038,29 +8663,30 @@ var TEST_LINT_DEBT_NODE_MANIFEST_FILE = LINT_POLICY_MANIFESTS.TEST_LINT_DEBT_NOD
8038
8663
  var TEST_LINT_DEBT_NODE_MANIFEST_KEY = LINT_POLICY_MANIFESTS.TEST_LINT_DEBT_NODES.key;
8039
8664
  var TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_FILE = LINT_POLICY_MANIFESTS.TEST_OWNED_CONSTANT_DEBT_NODES.file;
8040
8665
  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)$/;
8666
+ var SPEC_TREE_ROOT = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
8044
8667
  var BASE_BRANCH_REFS = [LINT_POLICY_BASE_REFS.REMOTE_MAIN, LINT_POLICY_BASE_REFS.LOCAL_MAIN];
8668
+ function isSpecTreeNodePath(entry) {
8669
+ return NODE_SUFFIXES.some((suffix) => entry.endsWith(suffix));
8670
+ }
8045
8671
  function readManifest(productDir, file, key) {
8046
8672
  return parseLintPolicyManifest(
8047
- readFileSync4(join22(productDir, file), "utf-8"),
8673
+ readFileSync4(join25(productDir, file), "utf-8"),
8048
8674
  file,
8049
8675
  key
8050
8676
  );
8051
8677
  }
8052
8678
  function manifestExists(productDir, file) {
8053
- return existsSync5(join22(productDir, file));
8679
+ return existsSync4(join25(productDir, file));
8054
8680
  }
8055
8681
  function findDeprecatedSpecNodePath(productDir) {
8056
8682
  function visit2(relativeDirectory) {
8057
- const absoluteDirectory = join22(productDir, relativeDirectory);
8683
+ const absoluteDirectory = join25(productDir, relativeDirectory);
8058
8684
  for (const entry of readdirSync2(absoluteDirectory, { withFileTypes: true })) {
8059
8685
  if (!entry.isDirectory()) {
8060
8686
  continue;
8061
8687
  }
8062
8688
  const childPath = `${relativeDirectory}/${entry.name}`;
8063
- if (DEPRECATED_SPEC_NODE_SUFFIX_PATTERN.test(entry.name)) {
8689
+ if (SPEC_TREE_SUPERSEDED_NODE_SUFFIXES.some((suffix) => entry.name.endsWith(suffix))) {
8064
8690
  return childPath;
8065
8691
  }
8066
8692
  const nestedDeprecatedPath = visit2(childPath);
@@ -8070,13 +8696,13 @@ function findDeprecatedSpecNodePath(productDir) {
8070
8696
  }
8071
8697
  return void 0;
8072
8698
  }
8073
- const specTreeRootPath = join22(productDir, SPEC_TREE_ROOT);
8074
- if (!existsSync5(specTreeRootPath)) {
8699
+ const specTreeRootPath = join25(productDir, SPEC_TREE_ROOT);
8700
+ if (!existsSync4(specTreeRootPath)) {
8075
8701
  return void 0;
8076
8702
  }
8077
8703
  return visit2(SPEC_TREE_ROOT);
8078
8704
  }
8079
- function assertManifestEntries(productDir, file, entries, suffixPattern, suffixDescription) {
8705
+ function assertManifestEntries(productDir, file, entries, suffixPredicate, suffixDescription) {
8080
8706
  const duplicates = entries.filter((entry, index) => entries.indexOf(entry) !== index);
8081
8707
  if (duplicates.length > 0) {
8082
8708
  throw new Error(
@@ -8093,11 +8719,11 @@ function assertManifestEntries(productDir, file, entries, suffixPattern, suffixD
8093
8719
  if (entry.includes("..")) {
8094
8720
  throw new Error(`${file} entry must not contain '..': ${entry}`);
8095
8721
  }
8096
- if (!suffixPattern.test(entry)) {
8722
+ if (!suffixPredicate(entry)) {
8097
8723
  throw new Error(`${file} entry must ${suffixDescription}: ${entry}`);
8098
8724
  }
8099
- const absoluteEntry = join22(productDir, entry);
8100
- if (!existsSync5(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
8725
+ const absoluteEntry = join25(productDir, entry);
8726
+ if (!existsSync4(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
8101
8727
  throw new Error(`${file} entry does not exist as a directory: ${entry}`);
8102
8728
  }
8103
8729
  }
@@ -8186,7 +8812,7 @@ function validateTestLintDebtNodeManifest(productDir, entries) {
8186
8812
  productDir,
8187
8813
  TEST_LINT_DEBT_NODE_MANIFEST_FILE,
8188
8814
  entries,
8189
- SPEC_TREE_NODE_SUFFIX_PATTERN,
8815
+ isSpecTreeNodePath,
8190
8816
  "be a Spec Tree node path"
8191
8817
  );
8192
8818
  assertManifestDoesNotGrow(
@@ -8201,7 +8827,7 @@ function validateTestOwnedConstantDebtNodeManifest(productDir, entries) {
8201
8827
  productDir,
8202
8828
  TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_FILE,
8203
8829
  entries,
8204
- SPEC_TREE_NODE_SUFFIX_PATTERN,
8830
+ isSpecTreeNodePath,
8205
8831
  "be a Spec Tree node path"
8206
8832
  );
8207
8833
  assertManifestDoesNotGrow(
@@ -8356,8 +8982,8 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
8356
8982
  scopeConfig: context.scopeConfig
8357
8983
  });
8358
8984
  return new Promise((resolve6) => {
8359
- const localBin = join23(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
8360
- const binary = existsSync6(localBin) ? localBin : "npx";
8985
+ const localBin = join26(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
8986
+ const binary = existsSync5(localBin) ? localBin : "npx";
8361
8987
  const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
8362
8988
  const eslintProcess = spawnManagedSubprocess(runner, binary, spawnArgs, {
8363
8989
  cwd: projectRoot
@@ -8458,7 +9084,7 @@ async function lintCommand(options) {
8458
9084
 
8459
9085
  // src/validation/literal/index.ts
8460
9086
  import { readFile as readFile8 } from "fs/promises";
8461
- import { isAbsolute as isAbsolute5, relative as relative7, resolve as resolve5 } from "path";
9087
+ import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve4 } from "path";
8462
9088
 
8463
9089
  // src/lib/file-inclusion/predicates/ignore-source.ts
8464
9090
  var IGNORE_SOURCE_LAYER = "ignore-source";
@@ -8493,7 +9119,7 @@ var ignoreSourceLayer = makeLayer(
8493
9119
 
8494
9120
  // src/lib/file-inclusion/pipeline.ts
8495
9121
  import { readdir as readdir7 } from "fs/promises";
8496
- import { join as join24, relative as relative6, sep as sep2 } from "path";
9122
+ import { join as join27, relative as relative4, sep as sep2 } from "path";
8497
9123
  var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
8498
9124
  function isNodeError3(err) {
8499
9125
  return err instanceof Error && "code" in err;
@@ -8511,11 +9137,11 @@ async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
8511
9137
  for (const entry of dirEntries) {
8512
9138
  if (entry.isDirectory()) {
8513
9139
  if (artifactDirs.has(entry.name)) continue;
8514
- const absolutePath = join24(absoluteDir, entry.name);
9140
+ const absolutePath = join27(absoluteDir, entry.name);
8515
9141
  await collectPaths(absolutePath, projectRoot, result, artifactDirs);
8516
9142
  } else if (entry.isFile()) {
8517
- const absolutePath = join24(absoluteDir, entry.name);
8518
- const rel = relative6(projectRoot, absolutePath);
9143
+ const absolutePath = join27(absoluteDir, entry.name);
9144
+ const rel = relative4(projectRoot, absolutePath);
8519
9145
  result.push(sep2 === "/" ? rel : rel.split(sep2).join("/"));
8520
9146
  }
8521
9147
  }
@@ -8921,8 +9547,8 @@ async function validateLiteralReuse(input) {
8921
9547
  const config = input.config ?? literalConfigDescriptor.defaults;
8922
9548
  const request = input.files ? {
8923
9549
  explicit: input.files.map((f) => {
8924
- const abs = isAbsolute5(f) ? f : resolve5(input.productDir, f);
8925
- return relative7(input.productDir, abs).split(/[\\/]/g).join("/");
9550
+ const abs = isAbsolute5(f) ? f : resolve4(input.productDir, f);
9551
+ return relative5(input.productDir, abs).split(/[\\/]/g).join("/");
8926
9552
  })
8927
9553
  } : { walkRoot: input.productDir };
8928
9554
  const scope = await runPipeline(
@@ -8933,7 +9559,7 @@ async function validateLiteralReuse(input) {
8933
9559
  EMPTY_IGNORE_READER
8934
9560
  );
8935
9561
  const filtered = applyPathFilter2(scope.included, input.pathConfig);
8936
- const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve5(input.productDir, entry.path));
9562
+ const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve4(input.productDir, entry.path));
8937
9563
  const collectOptions = {
8938
9564
  visitorKeys: defaultVisitorKeys,
8939
9565
  minStringLength: config.minStringLength,
@@ -8943,7 +9569,7 @@ async function validateLiteralReuse(input) {
8943
9569
  const testOccurrencesByFile = /* @__PURE__ */ new Map();
8944
9570
  const indexedOccurrencesByFile = /* @__PURE__ */ new Map();
8945
9571
  for (const abs of candidateFiles) {
8946
- const rel = relative7(input.productDir, abs).split(/[\\/]/g).join("/");
9572
+ const rel = relative5(input.productDir, abs).split(/[\\/]/g).join("/");
8947
9573
  const content = await readSafe(abs);
8948
9574
  if (content === null) continue;
8949
9575
  const occurrences = collectLiterals(content, rel, collectOptions);
@@ -9172,24 +9798,24 @@ function formatLoc(loc) {
9172
9798
  }
9173
9799
 
9174
9800
  // src/validation/steps/typescript.ts
9175
- import { existsSync as existsSync7, mkdirSync, rmSync, writeFileSync } from "fs";
9801
+ import { existsSync as existsSync6, mkdirSync, rmSync, writeFileSync } from "fs";
9176
9802
  import { mkdtemp as mkdtemp2 } from "fs/promises";
9177
- import { isAbsolute as isAbsolute6, join as join25 } from "path";
9803
+ import { isAbsolute as isAbsolute6, join as join28 } from "path";
9178
9804
  var defaultTypeScriptProcessRunner = lifecycleProcessRunner;
9179
9805
  var defaultTypeScriptDeps = {
9180
9806
  mkdtemp: mkdtemp2,
9181
9807
  mkdirSync,
9182
9808
  writeFileSync,
9183
9809
  rmSync,
9184
- existsSync: existsSync7
9810
+ existsSync: existsSync6
9185
9811
  };
9186
9812
  var TEMPORARY_TSCONFIG_COMPILER_OPTIONS = { noEmit: true };
9187
9813
  var TEMPORARY_TSCONFIG_PARENT_SEGMENTS = ["node_modules", ".cache", "spx"];
9188
9814
  var TEMPORARY_TSCONFIG_DIR_PREFIX = "validate-ts-";
9189
9815
  async function createTemporaryTsconfigDir(projectRoot, deps) {
9190
- const parent = join25(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
9816
+ const parent = join28(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
9191
9817
  deps.mkdirSync(parent, { recursive: true });
9192
- return deps.mkdtemp(join25(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
9818
+ return deps.mkdtemp(join28(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
9193
9819
  }
9194
9820
  function buildTypeScriptArgs(context) {
9195
9821
  const { scope, configFile } = context;
@@ -9197,11 +9823,11 @@ function buildTypeScriptArgs(context) {
9197
9823
  }
9198
9824
  async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defaultTypeScriptDeps) {
9199
9825
  const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
9200
- const configPath = join25(tempDir, "tsconfig.json");
9826
+ const configPath = join28(tempDir, "tsconfig.json");
9201
9827
  const baseConfigFile = TSCONFIG_FILES[scope];
9202
- const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join25(projectRoot, file));
9828
+ const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join28(projectRoot, file));
9203
9829
  const tempConfig = {
9204
- extends: join25(projectRoot, baseConfigFile),
9830
+ extends: join28(projectRoot, baseConfigFile),
9205
9831
  files: absoluteFiles,
9206
9832
  include: [],
9207
9833
  exclude: [],
@@ -9218,11 +9844,11 @@ async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defa
9218
9844
  }
9219
9845
  async function createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps = defaultTypeScriptDeps) {
9220
9846
  const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
9221
- const configPath = join25(tempDir, "tsconfig.json");
9847
+ const configPath = join28(tempDir, "tsconfig.json");
9222
9848
  const baseConfigFile = TSCONFIG_FILES[scope];
9223
- const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join25(projectRoot, pattern);
9849
+ const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join28(projectRoot, pattern);
9224
9850
  const tempConfig = {
9225
- extends: join25(projectRoot, baseConfigFile),
9851
+ extends: join28(projectRoot, baseConfigFile),
9226
9852
  include: scopeConfig.filePatterns.map(toProjectPathPattern),
9227
9853
  exclude: scopeConfig.excludePatterns.map(toProjectPathPattern),
9228
9854
  compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
@@ -9250,7 +9876,7 @@ async function validateTypeScript(context, options = {}) {
9250
9876
  const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, projectRoot, deps);
9251
9877
  try {
9252
9878
  return await new Promise((resolve6) => {
9253
- const tscBin = join25(projectRoot, "node_modules", ".bin", "tsc");
9879
+ const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
9254
9880
  const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
9255
9881
  const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
9256
9882
  const tscProcess = spawnManagedSubprocess(runner, tscBinary, tscArgs2, {
@@ -9283,7 +9909,7 @@ async function validateTypeScript(context, options = {}) {
9283
9909
  return { success: true, skipped: true };
9284
9910
  }
9285
9911
  const { configPath, cleanup } = await createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps);
9286
- const tscBin = join25(projectRoot, "node_modules", ".bin", "tsc");
9912
+ const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
9287
9913
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
9288
9914
  tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
9289
9915
  return new Promise((resolve6) => {
@@ -9305,7 +9931,7 @@ async function validateTypeScript(context, options = {}) {
9305
9931
  });
9306
9932
  });
9307
9933
  } else {
9308
- const tscBin = join25(projectRoot, "node_modules", ".bin", "tsc");
9934
+ const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
9309
9935
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
9310
9936
  const rawArgs = buildTypeScriptArgs({ scope, configFile });
9311
9937
  tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
@@ -9565,7 +10191,7 @@ function truncate(value) {
9565
10191
 
9566
10192
  // src/validation/literal/allowlist-existing.ts
9567
10193
  import { rename as rename4, writeFile as writeFile4 } from "fs/promises";
9568
- import { dirname as dirname5, join as join26 } from "path";
10194
+ import { dirname as dirname7, join as join29 } from "path";
9569
10195
  var EXIT_OK = 0;
9570
10196
  var EXIT_ERROR = 1;
9571
10197
  var INCLUDE_FIELD = "include";
@@ -9585,9 +10211,9 @@ var productionReader = {
9585
10211
  };
9586
10212
  var productionWriter = {
9587
10213
  async write(filePath, content) {
9588
- const dir = dirname5(filePath);
10214
+ const dir = dirname7(filePath);
9589
10215
  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}`);
10216
+ const tmpPath = join29(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
9591
10217
  await writeFile4(tmpPath, content, "utf8");
9592
10218
  await rename4(tmpPath, filePath);
9593
10219
  }
@@ -9908,15 +10534,403 @@ var validationDomain = {
9908
10534
  }
9909
10535
  };
9910
10536
 
10537
+ // src/domains/worktree/controlling-process.ts
10538
+ var CONTROLLING_PID_ENV = "SPX_WORKTREE_CONTROLLING_PID";
10539
+ var AGENT_RUNTIME_NAMES = ["claude", "codex"];
10540
+ var AGENT_COMMAND_PATTERN = new RegExp(`\\b(?:${AGENT_RUNTIME_NAMES.join("|")})\\b`, "i");
10541
+ var CONTROLLING_PROCESS_ERROR = {
10542
+ UNRESOLVED: "worktree controlling process could not be resolved"
10543
+ };
10544
+ var MAX_ANCESTRY_DEPTH = 64;
10545
+ var PID_RADIX = 10;
10546
+ function resolveControllingProcess(selfPid, table, env) {
10547
+ const host = table.currentHost();
10548
+ for (const pid of controllingPidCandidates(selfPid, table, env)) {
10549
+ const startedAt = table.startTimeOf(pid);
10550
+ if (startedAt !== void 0) return { ok: true, value: { pid, startedAt, host } };
10551
+ }
10552
+ return { ok: false, error: CONTROLLING_PROCESS_ERROR.UNRESOLVED };
10553
+ }
10554
+ function* controllingPidCandidates(selfPid, table, env) {
10555
+ const override = parsePid(env[CONTROLLING_PID_ENV]);
10556
+ if (override !== void 0) yield override;
10557
+ const agent = findAgentAncestor(selfPid, table);
10558
+ if (agent !== void 0) yield agent;
10559
+ const parent = table.parentOf(selfPid);
10560
+ if (parent !== void 0) yield parent;
10561
+ }
10562
+ function findAgentAncestor(selfPid, table) {
10563
+ let pid = table.parentOf(selfPid);
10564
+ for (let depth = 0; pid !== void 0 && depth < MAX_ANCESTRY_DEPTH; depth += 1) {
10565
+ const command = table.commandOf(pid);
10566
+ if (command !== void 0 && AGENT_COMMAND_PATTERN.test(command)) return pid;
10567
+ pid = table.parentOf(pid);
10568
+ }
10569
+ return void 0;
10570
+ }
10571
+ function parsePid(value) {
10572
+ if (value === void 0 || value.length === 0) return void 0;
10573
+ const parsed = Number.parseInt(value, PID_RADIX);
10574
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
10575
+ }
10576
+
10577
+ // src/domains/worktree/occupancy-store.ts
10578
+ import {
10579
+ mkdir as nodeMkdir2,
10580
+ readFile as nodeReadFile2,
10581
+ rename as nodeRename,
10582
+ rm as nodeRm,
10583
+ writeFile as nodeWriteFile2
10584
+ } from "fs/promises";
10585
+ import { join as join30 } from "path";
10586
+ var OCCUPANCY_STATUS = {
10587
+ UNCLAIMED: "unclaimed",
10588
+ OCCUPIED: "occupied",
10589
+ STALE: "stale"
10590
+ };
10591
+ var OCCUPANCY_CLAIM = {
10592
+ FILE_EXTENSION: ".claim",
10593
+ TEMP_EXTENSION: ".tmp"
10594
+ };
10595
+ var OCCUPANCY_ERROR = {
10596
+ INVALID_NAME: "worktree occupancy claim name must be a safe path segment",
10597
+ CLAIM_WRITE_FAILED: "worktree occupancy claim write failed",
10598
+ CLAIM_READ_FAILED: "worktree occupancy claim read failed",
10599
+ CLAIM_REMOVE_FAILED: "worktree occupancy claim remove failed",
10600
+ CLAIM_MALFORMED: "worktree occupancy claim record is malformed"
10601
+ };
10602
+ var ERROR_DETAIL_SEPARATOR2 = ": ";
10603
+ var defaultOccupancyFileSystem = {
10604
+ mkdir: async (path6, options) => {
10605
+ await nodeMkdir2(path6, options);
10606
+ },
10607
+ writeFile: async (path6, data) => {
10608
+ await nodeWriteFile2(path6, data);
10609
+ },
10610
+ rename: nodeRename,
10611
+ readFile: nodeReadFile2,
10612
+ rm: async (path6, options) => {
10613
+ await nodeRm(path6, options);
10614
+ }
10615
+ };
10616
+ function claimFileName(name) {
10617
+ return `${name}${OCCUPANCY_CLAIM.FILE_EXTENSION}`;
10618
+ }
10619
+ function claimFilePath(worktreesDir, name) {
10620
+ const validated = validateScopeToken(name);
10621
+ if (!validated.ok) return { ok: false, error: OCCUPANCY_ERROR.INVALID_NAME };
10622
+ return { ok: true, value: join30(worktreesDir, claimFileName(validated.value)) };
10623
+ }
10624
+ function classifyOccupancy(claim, probe) {
10625
+ if (claim === void 0) return OCCUPANCY_STATUS.UNCLAIMED;
10626
+ if (claim.host !== probe.currentHost()) return OCCUPANCY_STATUS.STALE;
10627
+ if (!probe.isAlive(claim.pid)) return OCCUPANCY_STATUS.STALE;
10628
+ const liveStartTime = probe.startTimeOf(claim.pid);
10629
+ if (liveStartTime !== void 0 && liveStartTime !== claim.startedAt) return OCCUPANCY_STATUS.STALE;
10630
+ return OCCUPANCY_STATUS.OCCUPIED;
10631
+ }
10632
+ async function writeClaim(worktreesDir, name, record, options = {}) {
10633
+ const fs7 = options.fs ?? defaultOccupancyFileSystem;
10634
+ const pathResult = claimFilePath(worktreesDir, name);
10635
+ if (!pathResult.ok) return pathResult;
10636
+ const claimPath = pathResult.value;
10637
+ const tempPath = `${claimPath}${OCCUPANCY_CLAIM.TEMP_EXTENSION}`;
10638
+ try {
10639
+ await fs7.mkdir(worktreesDir, { recursive: true });
10640
+ await fs7.writeFile(tempPath, serializeClaim(record));
10641
+ await fs7.rename(tempPath, claimPath);
10642
+ return { ok: true, value: claimPath };
10643
+ } catch (error) {
10644
+ return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_WRITE_FAILED, toErrorMessage3(error)) };
10645
+ }
10646
+ }
10647
+ async function readClaim(worktreesDir, name, options = {}) {
10648
+ const fs7 = options.fs ?? defaultOccupancyFileSystem;
10649
+ const pathResult = claimFilePath(worktreesDir, name);
10650
+ if (!pathResult.ok) return pathResult;
10651
+ let content;
10652
+ try {
10653
+ content = await fs7.readFile(pathResult.value, "utf8");
10654
+ } catch (error) {
10655
+ if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return { ok: true, value: void 0 };
10656
+ return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_READ_FAILED, toErrorMessage3(error)) };
10657
+ }
10658
+ return parseClaim(content);
10659
+ }
10660
+ async function removeClaim(worktreesDir, name, options = {}) {
10661
+ const fs7 = options.fs ?? defaultOccupancyFileSystem;
10662
+ const pathResult = claimFilePath(worktreesDir, name);
10663
+ if (!pathResult.ok) return pathResult;
10664
+ try {
10665
+ await fs7.rm(pathResult.value, { force: true });
10666
+ return { ok: true, value: void 0 };
10667
+ } catch (error) {
10668
+ return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_REMOVE_FAILED, toErrorMessage3(error)) };
10669
+ }
10670
+ }
10671
+ async function readOccupancy(worktreesDir, name, probe, options = {}) {
10672
+ const claimResult = await readClaim(worktreesDir, name, options);
10673
+ if (!claimResult.ok) return claimResult;
10674
+ return { ok: true, value: classifyOccupancy(claimResult.value, probe) };
10675
+ }
10676
+ function serializeClaim(record) {
10677
+ return JSON.stringify({
10678
+ sessionId: record.sessionId,
10679
+ host: record.host,
10680
+ pid: record.pid,
10681
+ startedAt: record.startedAt
10682
+ });
10683
+ }
10684
+ function parseClaim(content) {
10685
+ let parsed;
10686
+ try {
10687
+ parsed = JSON.parse(content);
10688
+ } catch {
10689
+ return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
10690
+ }
10691
+ if (!isWorktreeClaimRecord(parsed)) return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
10692
+ return { ok: true, value: parsed };
10693
+ }
10694
+ function isWorktreeClaimRecord(value) {
10695
+ if (typeof value !== "object" || value === null) return false;
10696
+ const candidate = value;
10697
+ return typeof candidate.sessionId === "string" && typeof candidate.host === "string" && typeof candidate.pid === "number" && typeof candidate.startedAt === "string";
10698
+ }
10699
+ function formatOccupancyError(code, detail) {
10700
+ return `${code}${ERROR_DETAIL_SEPARATOR2}${detail}`;
10701
+ }
10702
+ function toErrorMessage3(error) {
10703
+ return error instanceof Error ? error.message : String(error);
10704
+ }
10705
+
10706
+ // src/domains/worktree/process-table.ts
10707
+ import { spawnSync } from "child_process";
10708
+ import { hostname } from "os";
10709
+ var PS_COMMAND = "/bin/ps";
10710
+ var PS_FIELD = {
10711
+ START_TIME: "lstart",
10712
+ PARENT_PID: "ppid",
10713
+ // The full command line, not `comm` — `comm` reports only the executable
10714
+ // basename, so a shebang-installed agent run through an interpreter reads as
10715
+ // the interpreter (e.g. `node`) and the agent-runtime match misses it. `args`
10716
+ // carries the script path, so both native and interpreted agents are matched.
10717
+ COMMAND: "args"
10718
+ };
10719
+ var SIGNAL_LIVENESS_PROBE = 0;
10720
+ var PROCESS_EXISTS_NO_PERMISSION = "EPERM";
10721
+ var PID_RADIX2 = 10;
10722
+ var STABLE_PS_ENV = { ...process.env, TZ: "UTC", LC_ALL: "C" };
10723
+ function psField(pid, field) {
10724
+ const result = spawnSync(PS_COMMAND, ["-o", `${field}=`, "-p", String(pid)], {
10725
+ encoding: "utf8",
10726
+ env: STABLE_PS_ENV
10727
+ });
10728
+ if (result.status !== 0 || typeof result.stdout !== "string") return void 0;
10729
+ const value = result.stdout.trim();
10730
+ return value.length > 0 ? value : void 0;
10731
+ }
10732
+ var defaultProcessTable = {
10733
+ currentHost: () => hostname(),
10734
+ isAlive: (pid) => {
10735
+ try {
10736
+ process.kill(pid, SIGNAL_LIVENESS_PROBE);
10737
+ return true;
10738
+ } catch (error) {
10739
+ return hasErrorCode(error, PROCESS_EXISTS_NO_PERMISSION);
10740
+ }
10741
+ },
10742
+ startTimeOf: (pid) => psField(pid, PS_FIELD.START_TIME),
10743
+ parentOf: (pid) => {
10744
+ const value = psField(pid, PS_FIELD.PARENT_PID);
10745
+ if (value === void 0) return void 0;
10746
+ const parsed = Number.parseInt(value, PID_RADIX2);
10747
+ return Number.isInteger(parsed) ? parsed : void 0;
10748
+ },
10749
+ commandOf: (pid) => psField(pid, PS_FIELD.COMMAND)
10750
+ };
10751
+
10752
+ // src/commands/worktree/resolve.ts
10753
+ import { stat as stat4 } from "fs/promises";
10754
+ import { dirname as dirname8, resolve as resolve5 } from "path";
10755
+
10756
+ // src/domains/worktree/worktree-name.ts
10757
+ import { basename as basename4 } from "path";
10758
+ var SEPARATOR_CHARACTER = /[^a-z0-9_]/;
10759
+ var NAME_SEPARATOR = "-";
10760
+ function worktreeClaimName(worktreeRoot) {
10761
+ return basename4(worktreeRoot).toLowerCase().split(SEPARATOR_CHARACTER).filter((segment) => segment.length > 0).join(NAME_SEPARATOR);
10762
+ }
10763
+
10764
+ // src/commands/worktree/resolve.ts
10765
+ var WORKTREE_RESOLVE_ERROR = {
10766
+ NOT_A_WORKTREE: "path resolves to no worktree"
10767
+ };
10768
+ async function resolveWorktreesDir(options) {
10769
+ if (options.worktreesDir !== void 0) return options.worktreesDir;
10770
+ const resolved = await resolveWorktreesScopeDir({ cwd: options.cwd, deps: options.gitDeps });
10771
+ options.onWarning?.(resolved.warning);
10772
+ return resolved.worktreesDir;
10773
+ }
10774
+ async function resolveCurrentWorktreeName(options) {
10775
+ const worktree = await detectWorktreeProductRoot(options.cwd, options.gitDeps);
10776
+ return worktreeClaimName(worktree.productDir);
10777
+ }
10778
+ async function resolveTargetWorktree(options) {
10779
+ const base = options.cwd ?? process.cwd();
10780
+ const targetPath = options.worktree === void 0 ? base : resolve5(base, options.worktree);
10781
+ const targetGitPath = await isExistingNonDirectory(targetPath) ? dirname8(targetPath) : targetPath;
10782
+ const worktree = await detectWorktreeProductRoot(targetGitPath, options.gitDeps);
10783
+ if (!worktree.isGitRepo) {
10784
+ return { ok: false, error: `${WORKTREE_RESOLVE_ERROR.NOT_A_WORKTREE}: ${options.worktree ?? base}` };
10785
+ }
10786
+ return { ok: true, value: { name: worktreeClaimName(worktree.productDir), worktreeRoot: worktree.productDir } };
10787
+ }
10788
+ async function isExistingNonDirectory(path6) {
10789
+ try {
10790
+ const pathStats = await stat4(path6);
10791
+ return !pathStats.isDirectory();
10792
+ } catch {
10793
+ return false;
10794
+ }
10795
+ }
10796
+
10797
+ // src/commands/worktree/claim.ts
10798
+ async function claimCommand(options) {
10799
+ const table = options.processTable ?? defaultProcessTable;
10800
+ const controlling = resolveControllingProcess(options.selfPid ?? process.pid, table, options.env ?? process.env);
10801
+ if (!controlling.ok) return controlling;
10802
+ const worktreesDir = await resolveWorktreesDir(options);
10803
+ const name = await resolveCurrentWorktreeName(options);
10804
+ return writeClaim(
10805
+ worktreesDir,
10806
+ name,
10807
+ {
10808
+ sessionId: options.sessionId,
10809
+ host: controlling.value.host,
10810
+ pid: controlling.value.pid,
10811
+ startedAt: controlling.value.startedAt
10812
+ },
10813
+ { fs: options.fs }
10814
+ );
10815
+ }
10816
+
10817
+ // src/commands/worktree/release.ts
10818
+ async function releaseCommand2(options) {
10819
+ const worktreesDir = await resolveWorktreesDir(options);
10820
+ const name = await resolveCurrentWorktreeName(options);
10821
+ return removeClaim(worktreesDir, name, { fs: options.fs });
10822
+ }
10823
+
10824
+ // src/commands/worktree/status.ts
10825
+ var WORKTREE_STATUS_FORMAT = {
10826
+ JSON: "json",
10827
+ TEXT: "text"
10828
+ };
10829
+ async function statusCommand2(options) {
10830
+ const table = options.processTable ?? defaultProcessTable;
10831
+ const multiTargetRequest = options.worktrees !== void 0 && options.worktrees.length > 1;
10832
+ const targets = await resolveStatusTargets(options);
10833
+ if (!targets.ok) return targets;
10834
+ const records = [];
10835
+ for (const target of targets.value) {
10836
+ const worktreesDir = await resolveWorktreesDir({ ...options, cwd: target.worktreeRoot });
10837
+ const occupancy = await readOccupancy(worktreesDir, target.name, table, { fs: options.fs });
10838
+ if (!occupancy.ok) return occupancy;
10839
+ records.push({ worktree: target.name, status: occupancy.value });
10840
+ }
10841
+ return { ok: true, value: renderStatus(records, options.format, multiTargetRequest) };
10842
+ }
10843
+ async function resolveStatusTargets(options) {
10844
+ const requested = options.worktrees;
10845
+ if (requested === void 0 || requested.length === 0) {
10846
+ const target = await resolveTargetWorktree(options);
10847
+ if (!target.ok) return target;
10848
+ return { ok: true, value: [target.value] };
10849
+ }
10850
+ const targets = [];
10851
+ let firstError;
10852
+ for (const worktree of requested) {
10853
+ const target = await resolveTargetWorktree({ ...options, worktree });
10854
+ if (target.ok) {
10855
+ targets.push(target.value);
10856
+ } else {
10857
+ firstError ??= target.error;
10858
+ }
10859
+ }
10860
+ if (targets.length === 0) {
10861
+ return { ok: false, error: firstError ?? "no worktree status targets resolved" };
10862
+ }
10863
+ return { ok: true, value: targets };
10864
+ }
10865
+ function renderStatus(records, format2, multiTargetRequest) {
10866
+ if (format2 === WORKTREE_STATUS_FORMAT.JSON) {
10867
+ return JSON.stringify(multiTargetRequest ? records : records[0]);
10868
+ }
10869
+ return records.map(renderTextStatus).join("\n");
10870
+ }
10871
+ function renderTextStatus(record) {
10872
+ return `${record.worktree} ${record.status}`;
10873
+ }
10874
+
10875
+ // src/interfaces/cli/worktree.ts
10876
+ var WORKTREE_CLI = {
10877
+ COMMAND: "worktree",
10878
+ CLAIM: "claim",
10879
+ STATUS: "status",
10880
+ RELEASE: "release",
10881
+ WORKTREE_ARGUMENT: "[worktrees...]",
10882
+ SESSION_ID_FLAG: "--session-id",
10883
+ FORMAT_FLAG: "--format",
10884
+ WORKTREES_DIR_FLAG: "--worktrees-dir"
10885
+ };
10886
+ var WORKTREE_DOMAIN_DESCRIPTION = "Coordinate worktree occupancy across a bare-repository pool";
10887
+ function handleError2(error) {
10888
+ console.error("Error:", error);
10889
+ process.exit(1);
10890
+ }
10891
+ function registerWorktreeCommands(worktreeCmd) {
10892
+ worktreeCmd.command(WORKTREE_CLI.CLAIM).description("Record a worktree-occupancy claim for the running worktree").requiredOption(`${WORKTREE_CLI.SESSION_ID_FLAG} <id>`, "Claiming agent session id").option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (options) => {
10893
+ const result = await claimCommand({
10894
+ sessionId: options.sessionId,
10895
+ worktreesDir: options.worktreesDir,
10896
+ onWarning: writeWarning
10897
+ });
10898
+ if (!result.ok) handleError2(result.error);
10899
+ });
10900
+ worktreeCmd.command(`${WORKTREE_CLI.STATUS} ${WORKTREE_CLI.WORKTREE_ARGUMENT}`).description("Report a worktree's occupancy (occupied | unclaimed | stale)").option(`${WORKTREE_CLI.FORMAT_FLAG} <format>`, "Output format (text|json)", WORKTREE_STATUS_FORMAT.TEXT).option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (worktrees, options) => {
10901
+ const result = await statusCommand2({
10902
+ worktrees,
10903
+ format: options.format,
10904
+ worktreesDir: options.worktreesDir,
10905
+ onWarning: writeWarning
10906
+ });
10907
+ if (!result.ok) handleError2(result.error);
10908
+ console.log(result.value);
10909
+ });
10910
+ worktreeCmd.command(WORKTREE_CLI.RELEASE).description("Release the running worktree's occupancy claim").option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (options) => {
10911
+ const result = await releaseCommand2({ worktreesDir: options.worktreesDir, onWarning: writeWarning });
10912
+ if (!result.ok) handleError2(result.error);
10913
+ });
10914
+ }
10915
+ var worktreeDomain = {
10916
+ name: WORKTREE_CLI.COMMAND,
10917
+ description: WORKTREE_DOMAIN_DESCRIPTION,
10918
+ register: (program2) => {
10919
+ const worktreeCmd = program2.command(WORKTREE_CLI.COMMAND).description(WORKTREE_DOMAIN_DESCRIPTION);
10920
+ registerWorktreeCommands(worktreeCmd);
10921
+ }
10922
+ };
10923
+
9911
10924
  // src/interfaces/cli/registry.ts
9912
10925
  var CLI_DOMAINS = [
9913
- auditDomain,
9914
10926
  claudeDomain,
10927
+ compactDomain,
9915
10928
  configDomain,
9916
10929
  sessionDomain,
9917
10930
  specDomain,
9918
10931
  testingDomain,
9919
- validationDomain
10932
+ validationDomain,
10933
+ worktreeDomain
9920
10934
  ];
9921
10935
 
9922
10936
  // src/cli.ts