@xera-ai/core 0.1.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/dist/bin/internal.js +2039 -725
  2. package/dist/{adapter → core/src/adapter}/types.d.ts +1 -1
  3. package/dist/core/src/adapter/types.d.ts.map +1 -0
  4. package/dist/core/src/artifact/hash.d.ts.map +1 -0
  5. package/dist/core/src/artifact/meta.d.ts.map +1 -0
  6. package/dist/core/src/artifact/paths.d.ts.map +1 -0
  7. package/dist/core/src/artifact/status.d.ts.map +1 -0
  8. package/dist/core/src/auth/encrypt.d.ts.map +1 -0
  9. package/dist/core/src/auth/key.d.ts.map +1 -0
  10. package/dist/core/src/auth/refresh.d.ts.map +1 -0
  11. package/dist/core/src/auth/state.d.ts.map +1 -0
  12. package/dist/core/src/bin-internal/doctor.d.ts +5 -0
  13. package/dist/core/src/bin-internal/doctor.d.ts.map +1 -0
  14. package/dist/core/src/bin-internal/eval-deterministic.d.ts +5 -0
  15. package/dist/core/src/bin-internal/eval-deterministic.d.ts.map +1 -0
  16. package/dist/core/src/bin-internal/eval-prepare.d.ts +7 -0
  17. package/dist/core/src/bin-internal/eval-prepare.d.ts.map +1 -0
  18. package/dist/core/src/bin-internal/eval-report.d.ts +5 -0
  19. package/dist/core/src/bin-internal/eval-report.d.ts.map +1 -0
  20. package/dist/core/src/bin-internal/exec.d.ts.map +1 -0
  21. package/dist/core/src/bin-internal/fetch.d.ts.map +1 -0
  22. package/dist/core/src/bin-internal/heal-prepare.d.ts +19 -0
  23. package/dist/core/src/bin-internal/heal-prepare.d.ts.map +1 -0
  24. package/dist/core/src/bin-internal/index.d.ts.map +1 -0
  25. package/dist/core/src/bin-internal/lint.d.ts.map +1 -0
  26. package/dist/core/src/bin-internal/normalize.d.ts.map +1 -0
  27. package/dist/core/src/bin-internal/post.d.ts.map +1 -0
  28. package/dist/core/src/bin-internal/promote.d.ts.map +1 -0
  29. package/dist/core/src/bin-internal/report.d.ts.map +1 -0
  30. package/dist/core/src/bin-internal/status-cmd.d.ts.map +1 -0
  31. package/dist/core/src/bin-internal/typecheck.d.ts.map +1 -0
  32. package/dist/core/src/bin-internal/unlock.d.ts.map +1 -0
  33. package/dist/core/src/bin-internal/validate-feature.d.ts.map +1 -0
  34. package/dist/core/src/bin-internal/verify-prompts.d.ts +7 -0
  35. package/dist/core/src/bin-internal/verify-prompts.d.ts.map +1 -0
  36. package/dist/core/src/classifier/aggregate.d.ts.map +1 -0
  37. package/dist/core/src/classifier/history.d.ts.map +1 -0
  38. package/dist/core/src/classifier/types.d.ts.map +1 -0
  39. package/dist/core/src/config/define.d.ts.map +1 -0
  40. package/dist/core/src/config/load.d.ts.map +1 -0
  41. package/dist/{config → core/src/config}/schema.d.ts.map +1 -1
  42. package/dist/core/src/eval/paths.d.ts +15 -0
  43. package/dist/core/src/eval/paths.d.ts.map +1 -0
  44. package/dist/core/src/eval/run-id.d.ts +6 -0
  45. package/dist/core/src/eval/run-id.d.ts.map +1 -0
  46. package/dist/core/src/eval/types.d.ts +551 -0
  47. package/dist/core/src/eval/types.d.ts.map +1 -0
  48. package/dist/core/src/index.d.ts.map +1 -0
  49. package/dist/core/src/jira/client.d.ts.map +1 -0
  50. package/dist/core/src/jira/fields.d.ts.map +1 -0
  51. package/dist/core/src/jira/mcp-backend.d.ts.map +1 -0
  52. package/dist/core/src/jira/rest-backend.d.ts.map +1 -0
  53. package/dist/core/src/jira/retry.d.ts.map +1 -0
  54. package/dist/core/src/jira/types.d.ts.map +1 -0
  55. package/dist/core/src/lock/file-lock.d.ts.map +1 -0
  56. package/dist/core/src/logging/ndjson-logger.d.ts.map +1 -0
  57. package/dist/core/src/reporter/jira-comment.d.ts.map +1 -0
  58. package/dist/core/src/reporter/status-writer.d.ts.map +1 -0
  59. package/dist/src/index.js +19 -12
  60. package/dist/web/src/adapter.d.ts +3 -0
  61. package/dist/web/src/adapter.d.ts.map +1 -0
  62. package/dist/web/src/auth-setup/define.d.ts +16 -0
  63. package/dist/web/src/auth-setup/define.d.ts.map +1 -0
  64. package/dist/web/src/auth-setup/playwright-state.d.ts +2 -0
  65. package/dist/web/src/auth-setup/playwright-state.d.ts.map +1 -0
  66. package/dist/web/src/auth-setup/runner.d.ts +12 -0
  67. package/dist/web/src/auth-setup/runner.d.ts.map +1 -0
  68. package/dist/web/src/executor/index.d.ts +18 -0
  69. package/dist/web/src/executor/index.d.ts.map +1 -0
  70. package/dist/web/src/executor/playwright-args.d.ts +7 -0
  71. package/dist/web/src/executor/playwright-args.d.ts.map +1 -0
  72. package/dist/web/src/generator/gherkin-validate.d.ts +9 -0
  73. package/dist/web/src/generator/gherkin-validate.d.ts.map +1 -0
  74. package/dist/web/src/generator/lint.d.ts +9 -0
  75. package/dist/web/src/generator/lint.d.ts.map +1 -0
  76. package/dist/web/src/generator/pom-scan.d.ts +6 -0
  77. package/dist/web/src/generator/pom-scan.d.ts.map +1 -0
  78. package/dist/web/src/generator/promote.d.ts +7 -0
  79. package/dist/web/src/generator/promote.d.ts.map +1 -0
  80. package/dist/web/src/generator/selector-rules.d.ts +10 -0
  81. package/dist/web/src/generator/selector-rules.d.ts.map +1 -0
  82. package/dist/web/src/generator/typecheck.d.ts +11 -0
  83. package/dist/web/src/generator/typecheck.d.ts.map +1 -0
  84. package/dist/web/src/index.d.ts +18 -0
  85. package/dist/web/src/index.d.ts.map +1 -0
  86. package/dist/web/src/trace-normalizer/normalize.d.ts +7 -0
  87. package/dist/web/src/trace-normalizer/normalize.d.ts.map +1 -0
  88. package/dist/web/src/trace-normalizer/parse.d.ts +37 -0
  89. package/dist/web/src/trace-normalizer/parse.d.ts.map +1 -0
  90. package/dist/web/src/trace-normalizer/scrub-rules.d.ts +12 -0
  91. package/dist/web/src/trace-normalizer/scrub-rules.d.ts.map +1 -0
  92. package/dist/web/src/trace-normalizer/scrub.d.ts +29 -0
  93. package/dist/web/src/trace-normalizer/scrub.d.ts.map +1 -0
  94. package/dist/web/src/trace-normalizer/unzip.d.ts +6 -0
  95. package/dist/web/src/trace-normalizer/unzip.d.ts.map +1 -0
  96. package/package.json +3 -2
  97. package/src/adapter/types.ts +5 -2
  98. package/src/artifact/meta.ts +1 -1
  99. package/src/artifact/status.ts +1 -1
  100. package/src/auth/encrypt.ts +2 -2
  101. package/src/auth/key.ts +1 -2
  102. package/src/auth/refresh.ts +4 -1
  103. package/src/auth/state.ts +2 -2
  104. package/src/bin-internal/doctor.ts +133 -0
  105. package/src/bin-internal/eval-deterministic.ts +149 -0
  106. package/src/bin-internal/eval-prepare.ts +214 -0
  107. package/src/bin-internal/eval-report.ts +177 -0
  108. package/src/bin-internal/exec.ts +38 -16
  109. package/src/bin-internal/fetch.ts +21 -10
  110. package/src/bin-internal/heal-prepare.ts +230 -0
  111. package/src/bin-internal/index.ts +25 -11
  112. package/src/bin-internal/lint.ts +11 -4
  113. package/src/bin-internal/normalize.ts +23 -9
  114. package/src/bin-internal/post.ts +10 -4
  115. package/src/bin-internal/report.ts +3 -3
  116. package/src/bin-internal/status-cmd.ts +11 -3
  117. package/src/bin-internal/typecheck.ts +9 -3
  118. package/src/bin-internal/unlock.ts +12 -4
  119. package/src/bin-internal/validate-feature.ts +14 -5
  120. package/src/bin-internal/verify-prompts.ts +59 -0
  121. package/src/classifier/aggregate.ts +13 -6
  122. package/src/config/define.ts +3 -1
  123. package/src/config/load.ts +1 -1
  124. package/src/config/schema.ts +43 -37
  125. package/src/eval/paths.ts +32 -0
  126. package/src/eval/run-id.ts +30 -0
  127. package/src/eval/types.ts +101 -0
  128. package/src/jira/client.ts +4 -2
  129. package/src/jira/fields.ts +4 -2
  130. package/src/jira/mcp-backend.ts +1 -1
  131. package/src/jira/rest-backend.ts +17 -5
  132. package/src/jira/retry.ts +2 -2
  133. package/src/lock/file-lock.ts +2 -2
  134. package/src/logging/ndjson-logger.ts +2 -2
  135. package/src/reporter/jira-comment.ts +13 -7
  136. package/src/reporter/status-writer.ts +2 -2
  137. package/dist/adapter/types.d.ts.map +0 -1
  138. package/dist/artifact/hash.d.ts.map +0 -1
  139. package/dist/artifact/meta.d.ts.map +0 -1
  140. package/dist/artifact/paths.d.ts.map +0 -1
  141. package/dist/artifact/status.d.ts.map +0 -1
  142. package/dist/auth/encrypt.d.ts.map +0 -1
  143. package/dist/auth/key.d.ts.map +0 -1
  144. package/dist/auth/refresh.d.ts.map +0 -1
  145. package/dist/auth/state.d.ts.map +0 -1
  146. package/dist/bin-internal/exec.d.ts.map +0 -1
  147. package/dist/bin-internal/fetch.d.ts.map +0 -1
  148. package/dist/bin-internal/index.d.ts.map +0 -1
  149. package/dist/bin-internal/lint.d.ts.map +0 -1
  150. package/dist/bin-internal/normalize.d.ts.map +0 -1
  151. package/dist/bin-internal/post.d.ts.map +0 -1
  152. package/dist/bin-internal/promote.d.ts.map +0 -1
  153. package/dist/bin-internal/report.d.ts.map +0 -1
  154. package/dist/bin-internal/status-cmd.d.ts.map +0 -1
  155. package/dist/bin-internal/typecheck.d.ts.map +0 -1
  156. package/dist/bin-internal/unlock.d.ts.map +0 -1
  157. package/dist/bin-internal/validate-feature.d.ts.map +0 -1
  158. package/dist/classifier/aggregate.d.ts.map +0 -1
  159. package/dist/classifier/history.d.ts.map +0 -1
  160. package/dist/classifier/types.d.ts.map +0 -1
  161. package/dist/config/define.d.ts.map +0 -1
  162. package/dist/config/load.d.ts.map +0 -1
  163. package/dist/index.d.ts.map +0 -1
  164. package/dist/jira/client.d.ts.map +0 -1
  165. package/dist/jira/fields.d.ts.map +0 -1
  166. package/dist/jira/mcp-backend.d.ts.map +0 -1
  167. package/dist/jira/rest-backend.d.ts.map +0 -1
  168. package/dist/jira/retry.d.ts.map +0 -1
  169. package/dist/jira/types.d.ts.map +0 -1
  170. package/dist/lock/file-lock.d.ts.map +0 -1
  171. package/dist/logging/ndjson-logger.d.ts.map +0 -1
  172. package/dist/reporter/jira-comment.d.ts.map +0 -1
  173. package/dist/reporter/status-writer.d.ts.map +0 -1
  174. /package/dist/{artifact → core/src/artifact}/hash.d.ts +0 -0
  175. /package/dist/{artifact → core/src/artifact}/meta.d.ts +0 -0
  176. /package/dist/{artifact → core/src/artifact}/paths.d.ts +0 -0
  177. /package/dist/{artifact → core/src/artifact}/status.d.ts +0 -0
  178. /package/dist/{auth → core/src/auth}/encrypt.d.ts +0 -0
  179. /package/dist/{auth → core/src/auth}/key.d.ts +0 -0
  180. /package/dist/{auth → core/src/auth}/refresh.d.ts +0 -0
  181. /package/dist/{auth → core/src/auth}/state.d.ts +0 -0
  182. /package/dist/{bin-internal → core/src/bin-internal}/exec.d.ts +0 -0
  183. /package/dist/{bin-internal → core/src/bin-internal}/fetch.d.ts +0 -0
  184. /package/dist/{bin-internal → core/src/bin-internal}/index.d.ts +0 -0
  185. /package/dist/{bin-internal → core/src/bin-internal}/lint.d.ts +0 -0
  186. /package/dist/{bin-internal → core/src/bin-internal}/normalize.d.ts +0 -0
  187. /package/dist/{bin-internal → core/src/bin-internal}/post.d.ts +0 -0
  188. /package/dist/{bin-internal → core/src/bin-internal}/promote.d.ts +0 -0
  189. /package/dist/{bin-internal → core/src/bin-internal}/report.d.ts +0 -0
  190. /package/dist/{bin-internal → core/src/bin-internal}/status-cmd.d.ts +0 -0
  191. /package/dist/{bin-internal → core/src/bin-internal}/typecheck.d.ts +0 -0
  192. /package/dist/{bin-internal → core/src/bin-internal}/unlock.d.ts +0 -0
  193. /package/dist/{bin-internal → core/src/bin-internal}/validate-feature.d.ts +0 -0
  194. /package/dist/{classifier → core/src/classifier}/aggregate.d.ts +0 -0
  195. /package/dist/{classifier → core/src/classifier}/history.d.ts +0 -0
  196. /package/dist/{classifier → core/src/classifier}/types.d.ts +0 -0
  197. /package/dist/{config → core/src/config}/define.d.ts +0 -0
  198. /package/dist/{config → core/src/config}/load.d.ts +0 -0
  199. /package/dist/{config → core/src/config}/schema.d.ts +0 -0
  200. /package/dist/{index.d.ts → core/src/index.d.ts} +0 -0
  201. /package/dist/{jira → core/src/jira}/client.d.ts +0 -0
  202. /package/dist/{jira → core/src/jira}/fields.d.ts +0 -0
  203. /package/dist/{jira → core/src/jira}/mcp-backend.d.ts +0 -0
  204. /package/dist/{jira → core/src/jira}/rest-backend.d.ts +0 -0
  205. /package/dist/{jira → core/src/jira}/retry.d.ts +0 -0
  206. /package/dist/{jira → core/src/jira}/types.d.ts +0 -0
  207. /package/dist/{lock → core/src/lock}/file-lock.d.ts +0 -0
  208. /package/dist/{logging → core/src/logging}/ndjson-logger.d.ts +0 -0
  209. /package/dist/{reporter → core/src/reporter}/jira-comment.d.ts +0 -0
  210. /package/dist/{reporter → core/src/reporter}/status-writer.d.ts +0 -0
@@ -1,241 +1,1254 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
 
4
- // src/bin-internal/fetch.ts
5
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
6
- import { dirname as dirname2 } from "path";
4
+ // src/bin-internal/doctor.ts
5
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
6
+ import { join as join2 } from "path";
7
7
 
8
- // src/config/load.ts
9
- import { existsSync } from "fs";
8
+ // src/bin-internal/verify-prompts.ts
9
+ import { existsSync, readFileSync } from "fs";
10
10
  import { join } from "path";
11
- import { pathToFileURL } from "url";
11
+ var IN_SCOPE_PROMPTS = [
12
+ "feature-from-story.md",
13
+ "script-from-feature.md",
14
+ "heal-locator.md"
15
+ ];
16
+ var REQUIRED_SECTION_HEADING = "## Handling untrusted input";
17
+ var REQUIRED_KEYWORDS = ["UNTRUSTED", "injection-follow", "<XR_"];
18
+ function verifyPrompts(repoRoot) {
19
+ const promptsDir = join(repoRoot, "packages/prompts");
20
+ const results = [];
21
+ for (const filename of IN_SCOPE_PROMPTS) {
22
+ const path = join(promptsDir, filename);
23
+ if (!existsSync(path)) {
24
+ results.push({
25
+ ok: false,
26
+ message: `${filename}: file missing at packages/prompts/${filename}`
27
+ });
28
+ continue;
29
+ }
30
+ const text = readFileSync(path, "utf8");
31
+ if (!text.includes(REQUIRED_SECTION_HEADING)) {
32
+ results.push({
33
+ ok: false,
34
+ message: `${filename}: missing required section "${REQUIRED_SECTION_HEADING}"`
35
+ });
36
+ continue;
37
+ }
38
+ for (const keyword of REQUIRED_KEYWORDS) {
39
+ if (!text.includes(keyword)) {
40
+ results.push({
41
+ ok: false,
42
+ message: `${filename}: missing required keyword "${keyword}" (expected in "${REQUIRED_SECTION_HEADING}" section)`
43
+ });
44
+ }
45
+ }
46
+ }
47
+ return results;
48
+ }
49
+ async function verifyPromptsCmd(_argv) {
50
+ const results = verifyPrompts(process.cwd());
51
+ if (results.length === 0) {
52
+ console.log("[xera:verify-prompts] ok");
53
+ return 0;
54
+ }
55
+ for (const r of results)
56
+ console.error(`[xera:verify-prompts] ${r.message}`);
57
+ return 1;
58
+ }
12
59
 
13
- // src/config/schema.ts
60
+ // src/bin-internal/doctor.ts
61
+ var REQUIRED_FILES_PER_STAGE = {
62
+ "feature-from-story": ["golden/test.feature"],
63
+ "script-from-feature": ["golden/spec-requirements.md"],
64
+ "diagnose-failure": []
65
+ };
66
+ var REQUIRED_SCRIPTS = [
67
+ "xera:eval-prepare",
68
+ "xera:eval-deterministic",
69
+ "xera:eval-report",
70
+ "xera:verify-prompts",
71
+ "xera:doctor"
72
+ ];
73
+ function frontmatterField(content, field) {
74
+ const m = content.match(new RegExp(`^${field}:\\s*(\\S+)\\s*$`, "m"));
75
+ return m?.[1] ?? null;
76
+ }
77
+ function checkGoldenEvalDir(repoRoot) {
78
+ const root = join2(repoRoot, "fixtures/golden-eval");
79
+ if (!existsSync2(root))
80
+ return [{ ok: false, message: "fixtures/golden-eval/ does not exist" }];
81
+ const dirs = readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
82
+ const results = [];
83
+ if (dirs.length < 3) {
84
+ results.push({
85
+ ok: false,
86
+ message: `fixtures/golden-eval/ has ${dirs.length} ticket dir(s); need \u2265 3`
87
+ });
88
+ }
89
+ for (const entry of dirs) {
90
+ const dir = join2(root, entry.name);
91
+ const metaPath = join2(dir, "meta.json");
92
+ if (!existsSync2(metaPath)) {
93
+ results.push({ ok: false, message: `${entry.name}: meta.json missing` });
94
+ continue;
95
+ }
96
+ let meta;
97
+ try {
98
+ meta = JSON.parse(readFileSync2(metaPath, "utf8"));
99
+ } catch (err) {
100
+ results.push({
101
+ ok: false,
102
+ message: `${entry.name}: meta.json parse error: ${err.message}`
103
+ });
104
+ continue;
105
+ }
106
+ const stages = Array.isArray(meta.stages) ? meta.stages : [];
107
+ if (stages.length === 0)
108
+ results.push({ ok: false, message: `${entry.name}: meta.stages is empty` });
109
+ if (!existsSync2(join2(dir, "story.md")))
110
+ results.push({ ok: false, message: `${entry.name}: story.md missing` });
111
+ for (const stage of stages) {
112
+ const required = REQUIRED_FILES_PER_STAGE[stage] ?? [];
113
+ for (const rel of required) {
114
+ if (!existsSync2(join2(dir, rel))) {
115
+ results.push({
116
+ ok: false,
117
+ message: `${meta.id ?? entry.name}: stage "${stage}" declared but ${rel} missing`
118
+ });
119
+ }
120
+ }
121
+ }
122
+ }
123
+ return results;
124
+ }
125
+ function checkRubricPrompt(repoRoot) {
126
+ const path = join2(repoRoot, "packages/prompts/eval-rubric.md");
127
+ if (!existsSync2(path))
128
+ return [{ ok: false, message: "packages/prompts/eval-rubric.md missing" }];
129
+ const text = readFileSync2(path, "utf8");
130
+ const id = frontmatterField(text, "id");
131
+ const version = frontmatterField(text, "version");
132
+ if (id !== "eval-rubric")
133
+ return [{ ok: false, message: 'eval-rubric.md frontmatter "id" must be "eval-rubric"' }];
134
+ if (!version)
135
+ return [{ ok: false, message: 'eval-rubric.md frontmatter "version" missing' }];
136
+ return [];
137
+ }
138
+ function checkEvalSkill(repoRoot) {
139
+ const path = join2(repoRoot, "packages/skills/xera-eval.md");
140
+ if (!existsSync2(path))
141
+ return [{ ok: false, message: "packages/skills/xera-eval.md missing" }];
142
+ const text = readFileSync2(path, "utf8");
143
+ if (!frontmatterField(text, "name"))
144
+ return [{ ok: false, message: 'xera-eval.md frontmatter "name" missing' }];
145
+ return [];
146
+ }
147
+ function checkPromptInjectionPreamble(repoRoot) {
148
+ return verifyPrompts(repoRoot);
149
+ }
150
+ function checkRootScripts(repoRoot) {
151
+ const path = join2(repoRoot, "package.json");
152
+ if (!existsSync2(path))
153
+ return [{ ok: false, message: "root package.json missing" }];
154
+ const pkg = JSON.parse(readFileSync2(path, "utf8"));
155
+ const scripts = pkg.scripts ?? {};
156
+ const missing = REQUIRED_SCRIPTS.filter((s) => typeof scripts[s] !== "string");
157
+ return missing.map((s) => ({ ok: false, message: `root package.json missing script: ${s}` }));
158
+ }
159
+ async function doctorCmd(_argv, opts = {}) {
160
+ const repoRoot = opts.cwd ?? process.cwd();
161
+ const results = [
162
+ ...checkGoldenEvalDir(repoRoot),
163
+ ...checkRubricPrompt(repoRoot),
164
+ ...checkEvalSkill(repoRoot),
165
+ ...checkPromptInjectionPreamble(repoRoot),
166
+ ...checkRootScripts(repoRoot)
167
+ ];
168
+ if (results.length === 0) {
169
+ console.log("[xera:doctor] ok");
170
+ return 0;
171
+ }
172
+ for (const r of results)
173
+ console.error(`[xera:doctor] ${r.message}`);
174
+ return 1;
175
+ }
176
+
177
+ // src/bin-internal/eval-deterministic.ts
178
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
179
+ import { join as join4 } from "path";
180
+ import { validateGherkin } from "@xera-ai/web";
181
+
182
+ // src/eval/paths.ts
183
+ import { join as join3 } from "path";
184
+ function resolveEvalPaths(cwd, runId) {
185
+ const root = join3(cwd, ".xera", "eval", runId);
186
+ return {
187
+ root,
188
+ manifest: join3(root, "manifest.json"),
189
+ lock: join3(root, ".lock"),
190
+ deterministicScores: join3(root, "deterministic-scores.json"),
191
+ judgeScores: join3(root, "judge-scores.json"),
192
+ report: join3(root, "report.md"),
193
+ summary: join3(root, "summary.json"),
194
+ inputsDir: join3(root, "inputs"),
195
+ actualDir: join3(root, "actual"),
196
+ ticketInputsDir: (ticket) => join3(root, "inputs", ticket),
197
+ ticketActualDir: (ticket) => join3(root, "actual", ticket)
198
+ };
199
+ }
200
+
201
+ // src/eval/types.ts
14
202
  import { z } from "zod";
15
- var AuthRoleSchema = z.object({
16
- envEmail: z.string().min(1),
17
- envPassword: z.string().min(1)
203
+ var STAGES = ["feature-from-story", "script-from-feature", "diagnose-failure"];
204
+ var StageSchema = z.enum(STAGES);
205
+ var VerdictSchema = z.enum(["PASS", "FAIL", "NA"]);
206
+ var PromptVersionsSchema = z.object({
207
+ "feature-from-story": z.string(),
208
+ "script-from-feature": z.string(),
209
+ "diagnose-failure": z.string(),
210
+ "eval-rubric": z.string()
18
211
  });
19
- var AuthSchema = z.object({
20
- strategy: z.enum(["storageState", "apiToken", "none"]).default("none"),
21
- ttl: z.string().default("8h"),
22
- refreshBuffer: z.string().default("30m"),
23
- setupScript: z.string().optional(),
24
- roles: z.record(z.string(), AuthRoleSchema).default({})
212
+ var ManifestSchema = z.object({
213
+ run_id: z.string(),
214
+ started_at: z.string(),
215
+ git_sha: z.string(),
216
+ tickets: z.array(z.string()).min(1),
217
+ stages: z.array(StageSchema).min(1),
218
+ ticket_stages: z.record(z.string(), z.array(StageSchema).min(1)),
219
+ prompt_versions: PromptVersionsSchema,
220
+ flags: z.object({
221
+ force: z.boolean(),
222
+ only_prompt: StageSchema.nullable(),
223
+ only_ticket: z.string().nullable(),
224
+ judge_only: z.boolean()
225
+ })
25
226
  });
26
- var WebSchema = z.object({
27
- baseUrl: z.record(z.string(), z.string().url()).refine((m) => Object.keys(m).length > 0, {
28
- message: "baseUrl must have at least one environment"
227
+ var DimensionSchema = z.object({
228
+ name: z.string(),
229
+ verdict: VerdictSchema,
230
+ notes: z.string()
231
+ });
232
+ var JudgmentSchema = z.object({
233
+ stage: StageSchema,
234
+ ticket: z.string(),
235
+ dimensions: z.array(DimensionSchema).min(1)
236
+ });
237
+ var JudgeScoresSchema = z.object({
238
+ run_id: z.string(),
239
+ judgments: z.array(JudgmentSchema)
240
+ });
241
+ var DeterministicEntrySchema = z.object({
242
+ ticket: z.string(),
243
+ stage: StageSchema,
244
+ passed: z.boolean(),
245
+ checks: z.array(z.string()),
246
+ error: z.string().optional()
247
+ });
248
+ var DeterministicScoresSchema = z.object({
249
+ run_id: z.string(),
250
+ entries: z.array(DeterministicEntrySchema)
251
+ });
252
+ var ResultSchema = z.object({
253
+ ticket: z.string(),
254
+ stage: StageSchema,
255
+ deterministic: z.object({
256
+ passed: z.boolean(),
257
+ checks: z.array(z.string()),
258
+ error: z.string().optional()
29
259
  }),
30
- defaultEnv: z.string(),
31
- auth: AuthSchema.default({}),
32
- testData: z.object({
33
- users: z.record(z.string(), z.object({ fromAuth: z.string() })).default({})
34
- }).default({ users: {} })
35
- }).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
36
- message: "defaultEnv must exist in baseUrl map",
37
- path: ["defaultEnv"]
260
+ judge: z.object({
261
+ passed: z.boolean(),
262
+ dimensions: z.array(DimensionSchema),
263
+ score: z.number().min(0).max(1)
264
+ }).nullable(),
265
+ skipped: z.boolean().optional()
38
266
  });
39
- var JiraSchema = z.object({
40
- baseUrl: z.string().url(),
41
- projectKeys: z.array(z.string().min(1)).min(1),
42
- fields: z.object({
43
- story: z.string().min(1),
44
- acceptanceCriteria: z.string().optional(),
45
- attachments: z.string().default("attachment")
267
+ var SummarySchema = z.object({
268
+ run_id: z.string(),
269
+ git_sha: z.string(),
270
+ prompt_versions: PromptVersionsSchema,
271
+ results: z.array(ResultSchema),
272
+ overall: z.object({
273
+ passed: z.number().int().nonnegative(),
274
+ failed: z.number().int().nonnegative(),
275
+ total: z.number().int().nonnegative(),
276
+ score: z.number().min(0).max(1)
46
277
  })
47
278
  });
48
- var AISchema = z.object({
49
- livePageSnapshot: z.boolean().default(true),
50
- confidenceThreshold: z.enum(["low", "medium", "high"]).default("medium"),
51
- maxRetries: z.object({
52
- typecheck: z.number().int().min(0).max(5).default(2),
53
- lint: z.number().int().min(0).max(5).default(2),
54
- validateFeature: z.number().int().min(0).max(5).default(2)
55
- }).default({})
56
- }).default({});
57
- var ReportingSchema = z.object({
58
- language: z.enum(["en", "vi"]).default("en"),
59
- postToJira: z.boolean().default(true),
60
- transition: z.object({
61
- onPass: z.string().nullable().default(null),
62
- onFail: z.string().nullable().default(null)
63
- }).default({}),
64
- artifactLinks: z.enum(["git", "local"]).default("git")
65
- }).default({});
66
- var XeraConfigSchema = z.object({
67
- jira: JiraSchema,
68
- web: WebSchema,
69
- ai: AISchema,
70
- reporting: ReportingSchema,
71
- adapters: z.array(z.string().min(1)).min(1).default(["web"])
72
- });
73
279
 
74
- // src/config/load.ts
75
- async function loadConfig(cwd) {
76
- const path = join(cwd, "xera.config.ts");
77
- if (!existsSync(path)) {
78
- throw new Error(`xera.config.ts not found in ${cwd}`);
280
+ // src/bin-internal/eval-deterministic.ts
281
+ function checkFeatureFromStory(actualFeaturePath) {
282
+ if (!existsSync3(actualFeaturePath)) {
283
+ return { passed: false, checks: ["validate-feature"], error: "actual missing: test.feature" };
284
+ }
285
+ try {
286
+ const r = validateGherkin(readFileSync3(actualFeaturePath, "utf8"));
287
+ if (r.ok)
288
+ return { passed: true, checks: ["validate-feature"] };
289
+ return {
290
+ passed: false,
291
+ checks: ["validate-feature"],
292
+ error: r.errors.map((e) => `line ${e.line}: ${e.message}`).join("; ")
293
+ };
294
+ } catch (err) {
295
+ return { passed: false, checks: ["validate-feature"], error: err.message };
79
296
  }
80
- const mod = await import(pathToFileURL(path).href);
81
- const raw = mod.default ?? mod;
82
- return XeraConfigSchema.parse(raw);
83
297
  }
84
-
85
- // src/artifact/paths.ts
86
- import { join as join2 } from "path";
87
- var TICKET_RE = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
88
- function resolveArtifactPaths(repoRoot, ticket) {
89
- if (!TICKET_RE.test(ticket)) {
90
- throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
298
+ function checkScriptFromFeature(actualTicketDir) {
299
+ const specPath = join4(actualTicketDir, "spec.ts");
300
+ if (!existsSync3(specPath)) {
301
+ return { passed: false, checks: ["file-presence"], error: "actual missing: spec.ts" };
91
302
  }
92
- const ticketDir = join2(repoRoot, ".xera", ticket);
93
- return {
94
- ticketDir,
95
- storyPath: join2(ticketDir, "story.md"),
96
- featurePath: join2(ticketDir, "test.feature"),
97
- specPath: join2(ticketDir, "spec.ts"),
98
- pageObjectsDir: join2(ticketDir, "page-objects"),
99
- runsDir: join2(ticketDir, "runs"),
100
- metaPath: join2(ticketDir, "meta.json"),
101
- statusPath: join2(ticketDir, "status.json"),
102
- logPath: join2(ticketDir, "xera.log"),
103
- lockPath: join2(ticketDir, ".lock"),
104
- authDir: join2(repoRoot, ".xera", ".auth"),
105
- runPath: (runId) => {
106
- const runDir = join2(ticketDir, "runs", runId);
107
- return {
108
- runDir,
109
- reportJsonPath: join2(runDir, "report.json"),
110
- tracePath: join2(runDir, "trace.zip"),
111
- normalizedPath: join2(runDir, "normalized.json"),
112
- screenshotsDir: join2(runDir, "screenshots"),
113
- videoDir: join2(runDir, "videos")
114
- };
303
+ return { passed: true, checks: ["file-presence"] };
304
+ }
305
+ function checkDiagnoseFailure(inputsTicketDir, actualTicketDir) {
306
+ const inputPath = join4(inputsTicketDir, "classifier-input.json");
307
+ const actualPath = join4(actualTicketDir, "classification.json");
308
+ if (!existsSync3(actualPath)) {
309
+ return {
310
+ passed: false,
311
+ checks: ["bucket-match"],
312
+ error: "actual missing: classification.json"
313
+ };
314
+ }
315
+ if (!existsSync3(inputPath)) {
316
+ return {
317
+ passed: false,
318
+ checks: ["bucket-match"],
319
+ error: "inputs missing: classifier-input.json"
320
+ };
321
+ }
322
+ const golden = JSON.parse(readFileSync3(inputPath, "utf8"));
323
+ const actual = JSON.parse(readFileSync3(actualPath, "utf8"));
324
+ const goldScens = golden.scenarios ?? [];
325
+ const actScens = actual.scenarios ?? [];
326
+ const mismatches = [];
327
+ for (const g of goldScens) {
328
+ const a = actScens.find((s) => s.name === g.name);
329
+ if (!a) {
330
+ mismatches.push(`missing scenario "${g.name}"`);
331
+ continue;
115
332
  }
116
- };
333
+ if (a.class !== g.class)
334
+ mismatches.push(`scenario "${g.name}": expected class ${g.class}, got ${a.class}`);
335
+ }
336
+ if (mismatches.length > 0) {
337
+ return {
338
+ passed: false,
339
+ checks: ["bucket-match"],
340
+ error: `bucket mismatch \u2014 ${mismatches.join("; ")}`
341
+ };
342
+ }
343
+ return { passed: true, checks: ["bucket-match"] };
117
344
  }
118
- function generateRunId(now = new Date) {
119
- return now.toISOString().replace(/[:.]/g, "-").replace("Z", "");
345
+ async function evalDeterministicCmd(argv, opts = {}) {
346
+ const cwd = opts.cwd ?? process.cwd();
347
+ const runId = argv[0];
348
+ if (!runId) {
349
+ console.error("[xera:eval-deterministic] usage: eval-deterministic <run-id>");
350
+ return 1;
351
+ }
352
+ const paths = resolveEvalPaths(cwd, runId);
353
+ if (!existsSync3(paths.manifest)) {
354
+ console.error(`[xera:eval-deterministic] missing manifest.json at ${paths.manifest}`);
355
+ return 1;
356
+ }
357
+ const manifest = ManifestSchema.parse(JSON.parse(readFileSync3(paths.manifest, "utf8")));
358
+ const entries = [];
359
+ for (const [ticket, ticketStages] of Object.entries(manifest.ticket_stages)) {
360
+ for (const stage of ticketStages) {
361
+ const inputsDir = paths.ticketInputsDir(ticket);
362
+ const actualDir = paths.ticketActualDir(ticket);
363
+ let result;
364
+ if (stage === "feature-from-story") {
365
+ result = checkFeatureFromStory(join4(actualDir, "test.feature"));
366
+ } else if (stage === "script-from-feature") {
367
+ result = checkScriptFromFeature(actualDir);
368
+ } else {
369
+ result = checkDiagnoseFailure(inputsDir, actualDir);
370
+ }
371
+ const entry = {
372
+ ticket,
373
+ stage,
374
+ passed: result.passed,
375
+ checks: result.checks
376
+ };
377
+ if (result.error !== undefined)
378
+ entry.error = result.error;
379
+ entries.push(entry);
380
+ }
381
+ }
382
+ const scores = { run_id: runId, entries };
383
+ DeterministicScoresSchema.parse(scores);
384
+ writeFileSync(paths.deterministicScores, JSON.stringify(scores, null, 2));
385
+ console.log(`[xera:eval-deterministic] wrote ${entries.length} entries`);
386
+ return 0;
120
387
  }
121
388
 
122
- // src/artifact/hash.ts
123
- import { createHash } from "crypto";
124
- import { existsSync as existsSync2, readFileSync } from "fs";
125
- function hashString(s) {
126
- return `sha256:${createHash("sha256").update(s).digest("hex")}`;
389
+ // src/bin-internal/eval-prepare.ts
390
+ import {
391
+ copyFileSync,
392
+ existsSync as existsSync5,
393
+ mkdirSync as mkdirSync2,
394
+ readFileSync as readFileSync5,
395
+ readdirSync as readdirSync2,
396
+ writeFileSync as writeFileSync3
397
+ } from "fs";
398
+ import { join as join5 } from "path";
399
+
400
+ // src/eval/run-id.ts
401
+ import { execSync } from "child_process";
402
+ function defaultGetGitSha() {
403
+ try {
404
+ return execSync("git rev-parse HEAD", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
405
+ } catch {
406
+ return null;
407
+ }
127
408
  }
128
- function hashFile(path) {
129
- return hashString(readFileSync(path, "utf8"));
409
+ function pad(n) {
410
+ return n.toString().padStart(2, "0");
130
411
  }
131
- function hashFileIfExists(path) {
132
- if (!existsSync2(path))
133
- return null;
134
- return hashFile(path);
412
+ function generateRunId(opts = {}) {
413
+ const getGitSha = opts.getGitSha ?? defaultGetGitSha;
414
+ const now = (opts.now ?? (() => new Date))();
415
+ const date = `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}`;
416
+ const time = `${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}`;
417
+ const sha = getGitSha();
418
+ const short = sha ? sha.slice(0, 7) : "nogit";
419
+ return `${date}-${time}-${short}`;
135
420
  }
136
421
 
137
- // src/artifact/meta.ts
138
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
422
+ // src/lock/file-lock.ts
423
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
424
+ import { hostname } from "os";
139
425
  import { dirname } from "path";
140
- import { z as z2 } from "zod";
141
- var MetaJsonSchema = z2.object({
142
- ticket: z2.string(),
143
- adapter: z2.string(),
144
- xera_version: z2.string(),
145
- prompts_version: z2.string(),
146
- fetched_at: z2.string().optional(),
147
- story_hash: z2.string().optional(),
148
- feature_generated_at: z2.string().optional(),
149
- feature_generated_from_story_hash: z2.string().optional(),
150
- feature_hash: z2.string().optional(),
151
- script_generated_at: z2.string().optional(),
152
- script_generated_from_feature_hash: z2.string().optional(),
153
- script_warnings: z2.array(z2.string()).optional()
154
- });
155
- function readMeta(path) {
156
- if (!existsSync3(path))
157
- return null;
158
- return MetaJsonSchema.parse(JSON.parse(readFileSync2(path, "utf8")));
159
- }
160
- function writeMeta(path, meta) {
426
+ function acquireLock(path, runId) {
427
+ if (existsSync4(path))
428
+ return false;
161
429
  mkdirSync(dirname(path), { recursive: true });
162
- writeFileSync(path, JSON.stringify(meta, null, 2));
430
+ const data = {
431
+ pid: process.pid,
432
+ hostname: hostname(),
433
+ started_at: new Date().toISOString(),
434
+ run_id: runId
435
+ };
436
+ try {
437
+ writeFileSync2(path, JSON.stringify(data), { flag: "wx" });
438
+ return true;
439
+ } catch {
440
+ return false;
441
+ }
163
442
  }
164
- function updateMeta(path, patch) {
165
- const existing = readMeta(path);
166
- if (!existing) {
167
- throw new Error(`meta.json not found at ${path}; cannot update`);
443
+ function releaseLock(path) {
444
+ if (existsSync4(path))
445
+ unlinkSync(path);
446
+ }
447
+ function readLock(path) {
448
+ if (!existsSync4(path))
449
+ return null;
450
+ return JSON.parse(readFileSync4(path, "utf8"));
451
+ }
452
+ function isLockStale(path) {
453
+ const lock = readLock(path);
454
+ if (!lock)
455
+ return true;
456
+ if (lock.hostname !== hostname()) {
457
+ return false;
168
458
  }
169
- const next = { ...existing, ...patch };
170
- writeMeta(path, next);
171
- return next;
459
+ try {
460
+ process.kill(lock.pid, 0);
461
+ return false;
462
+ } catch {
463
+ return true;
464
+ }
465
+ }
466
+ function forceUnlock(path) {
467
+ releaseLock(path);
172
468
  }
173
469
 
174
- // src/jira/mcp-backend.ts
175
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
176
- import { tmpdir } from "os";
177
- import { join as join3 } from "path";
178
- var MCP_ENV = "XERA_MCP_JIRA";
179
- async function createMcpBackend(_baseUrl) {
180
- if (process.env[MCP_ENV] !== "1")
181
- return null;
182
- const tmpDir = join3(tmpdir(), "xera-mcp");
183
- mkdirSync2(tmpDir, { recursive: true });
184
- return {
185
- backend: "mcp",
186
- async fetchTicket(key, _fields) {
187
- const cachePath = join3(tmpDir, `${key}.json`);
188
- if (!existsSync4(cachePath)) {
189
- throw new Error(`MCP-mode fetch requires the skill to first call mcp__atlassian__getJiraIssue and write ${cachePath}. ` + `If you are running this directly, unset ${MCP_ENV} to use REST.`);
470
+ // src/bin-internal/eval-prepare.ts
471
+ function parseFlags(argv) {
472
+ const flags = { force: false, only_prompt: null, only_ticket: null };
473
+ for (const arg of argv) {
474
+ if (arg === "--force")
475
+ flags.force = true;
476
+ else if (arg.startsWith("--prompt=")) {
477
+ const v = arg.slice("--prompt=".length);
478
+ if (!STAGES.includes(v)) {
479
+ return { error: `Unknown stage: ${v}. Valid: ${STAGES.join(", ")}.` };
190
480
  }
191
- const parsed = JSON.parse(readFileSync3(cachePath, "utf8"));
192
- return parsed;
193
- },
194
- async postComment(key, body) {
195
- const outPath = join3(tmpDir, `${key}.comment.json`);
196
- writeFileSync2(outPath, JSON.stringify({ key, body }));
197
- return { id: "mcp-pending" };
198
- },
199
- async transitionStatus(key, statusName) {
200
- const outPath = join3(tmpDir, `${key}.transition.json`);
201
- writeFileSync2(outPath, JSON.stringify({ key, statusName }));
481
+ flags.only_prompt = v;
482
+ } else if (arg.startsWith("--ticket=")) {
483
+ flags.only_ticket = arg.slice("--ticket=".length);
484
+ } else {
485
+ return { error: `Unknown argument: ${arg}` };
486
+ }
487
+ }
488
+ return flags;
489
+ }
490
+ function readPromptVersion(repoRoot, name) {
491
+ const path = join5(repoRoot, "packages/prompts", `${name}.md`);
492
+ if (!existsSync5(path))
493
+ return "0.0.0";
494
+ const text = readFileSync5(path, "utf8");
495
+ const m = /^version:\s*(\S+)\s*$/m.exec(text);
496
+ return m?.[1] ?? "0.0.0";
497
+ }
498
+ function discoverEvalTickets(repoRoot) {
499
+ const root = join5(repoRoot, "fixtures/golden-eval");
500
+ if (!existsSync5(root))
501
+ return [];
502
+ const out = [];
503
+ for (const entry of readdirSync2(root, { withFileTypes: true })) {
504
+ if (!entry.isDirectory())
505
+ continue;
506
+ if (entry.name === "README.md" || entry.name.startsWith("."))
507
+ continue;
508
+ const dir = join5(root, entry.name);
509
+ const metaPath = join5(dir, "meta.json");
510
+ if (!existsSync5(metaPath))
511
+ continue;
512
+ const meta = JSON.parse(readFileSync5(metaPath, "utf8"));
513
+ out.push({ id: meta.id, dir, stages: meta.stages });
514
+ }
515
+ return out.sort((a, b) => a.id.localeCompare(b.id));
516
+ }
517
+ function discoverClassifierTickets(repoRoot) {
518
+ const root = join5(repoRoot, "fixtures/golden-tickets");
519
+ if (!existsSync5(root))
520
+ return [];
521
+ const out = [];
522
+ for (const entry of readdirSync2(root, { withFileTypes: true })) {
523
+ if (!entry.isFile() || !entry.name.endsWith(".json"))
524
+ continue;
525
+ const path = join5(root, entry.name);
526
+ const data = JSON.parse(readFileSync5(path, "utf8"));
527
+ if (typeof data.ticket === "string")
528
+ out.push({ id: data.ticket, path });
529
+ }
530
+ return out.sort((a, b) => a.id.localeCompare(b.id));
531
+ }
532
+ async function evalPrepareCmd(argv, opts = {}) {
533
+ const repoRoot = opts.cwd ?? process.cwd();
534
+ const flags = parseFlags(argv);
535
+ if ("error" in flags) {
536
+ console.error(`[xera:eval-prepare] ${flags.error}`);
537
+ return 1;
538
+ }
539
+ const evalTickets = discoverEvalTickets(repoRoot);
540
+ const classifierTickets = discoverClassifierTickets(repoRoot);
541
+ const stages = flags.only_prompt ? [flags.only_prompt] : [...STAGES];
542
+ const wantsEval = stages.some((s) => s !== "diagnose-failure");
543
+ const wantsClassifier = stages.includes("diagnose-failure");
544
+ let selectedTickets = [];
545
+ if (wantsEval)
546
+ selectedTickets.push(...evalTickets.map((t) => t.id));
547
+ if (wantsClassifier)
548
+ selectedTickets.push(...classifierTickets.map((t) => t.id));
549
+ selectedTickets = [...new Set(selectedTickets)].sort();
550
+ if (flags.only_ticket) {
551
+ if (!selectedTickets.includes(flags.only_ticket)) {
552
+ console.error(`[xera:eval-prepare] No golden fixture for ${flags.only_ticket}`);
553
+ return 1;
554
+ }
555
+ selectedTickets = [flags.only_ticket];
556
+ }
557
+ if (selectedTickets.length === 0) {
558
+ console.error("[xera:eval-prepare] No tickets selected (after filters).");
559
+ return 1;
560
+ }
561
+ const ticket_stages = {};
562
+ for (const ticket of selectedTickets) {
563
+ const evalT = evalTickets.find((t) => t.id === ticket);
564
+ let ticketDeclared;
565
+ if (evalT) {
566
+ ticketDeclared = evalT.stages;
567
+ } else {
568
+ ticketDeclared = ["diagnose-failure"];
569
+ }
570
+ const intersection = ticketDeclared.filter((s) => stages.includes(s));
571
+ if (intersection.length > 0) {
572
+ ticket_stages[ticket] = intersection;
573
+ }
574
+ }
575
+ selectedTickets = selectedTickets.filter((t) => ticket_stages[t] !== undefined);
576
+ if (selectedTickets.length === 0) {
577
+ console.error("[xera:eval-prepare] No tickets applicable to requested stages.");
578
+ return 1;
579
+ }
580
+ const runId = generateRunId({
581
+ ...opts.now ? { now: opts.now } : {},
582
+ ...opts.getGitSha ? { getGitSha: opts.getGitSha } : {}
583
+ });
584
+ const paths = resolveEvalPaths(repoRoot, runId);
585
+ if (existsSync5(paths.root) && !flags.force) {
586
+ console.error(`[xera:eval-prepare] run dir already exists: ${paths.root}. Pass --force to re-run.`);
587
+ return 1;
588
+ }
589
+ mkdirSync2(paths.inputsDir, { recursive: true });
590
+ mkdirSync2(paths.actualDir, { recursive: true });
591
+ for (const ticket of selectedTickets) {
592
+ const ticketInputs = paths.ticketInputsDir(ticket);
593
+ mkdirSync2(ticketInputs, { recursive: true });
594
+ const evalT = evalTickets.find((t) => t.id === ticket);
595
+ const classT = classifierTickets.find((t) => t.id === ticket);
596
+ if (evalT) {
597
+ copyFileSync(join5(evalT.dir, "story.md"), join5(ticketInputs, "story.md"));
598
+ const featurePath = join5(evalT.dir, "golden/test.feature");
599
+ if (existsSync5(featurePath))
600
+ copyFileSync(featurePath, join5(ticketInputs, "test.feature"));
601
+ }
602
+ if (classT) {
603
+ copyFileSync(classT.path, join5(ticketInputs, "classifier-input.json"));
604
+ }
605
+ }
606
+ const now = (opts.now ?? (() => new Date))();
607
+ const manifest = {
608
+ run_id: runId,
609
+ started_at: now.toISOString(),
610
+ git_sha: runId.split("-")[2] ?? "nogit",
611
+ tickets: selectedTickets,
612
+ stages,
613
+ ticket_stages,
614
+ prompt_versions: {
615
+ "feature-from-story": readPromptVersion(repoRoot, "feature-from-story"),
616
+ "script-from-feature": readPromptVersion(repoRoot, "script-from-feature"),
617
+ "diagnose-failure": readPromptVersion(repoRoot, "diagnose-failure"),
618
+ "eval-rubric": readPromptVersion(repoRoot, "eval-rubric")
202
619
  },
203
- async listFields(_sampleKey) {
204
- throw new Error("listFields is REST-only; init flow uses REST for field discovery.");
620
+ flags: {
621
+ force: flags.force,
622
+ only_prompt: flags.only_prompt,
623
+ only_ticket: flags.only_ticket,
624
+ judge_only: false
205
625
  }
206
626
  };
627
+ ManifestSchema.parse(manifest);
628
+ writeFileSync3(paths.manifest, JSON.stringify(manifest, null, 2));
629
+ if (!acquireLock(paths.lock, runId)) {
630
+ console.error(`[xera:eval-prepare] failed to acquire lock at ${paths.lock}`);
631
+ return 4;
632
+ }
633
+ console.log(`[xera:eval-prepare] prepared ${selectedTickets.length} ticket(s) for stages: ${stages.join(", ")}`);
634
+ console.log(`RUN_ID=${runId}`);
635
+ return 0;
207
636
  }
208
637
 
209
- // src/jira/rest-backend.ts
210
- function createRestBackend(baseUrl, creds) {
211
- const authHeader = `Basic ${Buffer.from(`${creds.email}:${creds.apiToken}`).toString("base64")}`;
212
- const base = baseUrl.replace(/\/$/, "");
213
- async function req(path, init) {
214
- const r = await fetch(`${base}${path}`, {
215
- ...init,
216
- headers: {
217
- Authorization: authHeader,
218
- Accept: "application/json",
219
- "Content-Type": "application/json",
220
- ...init?.headers ?? {}
221
- }
222
- });
223
- if (!r.ok && r.status !== 201) {
224
- throw new Error(`Jira REST ${init?.method ?? "GET"} ${path} failed: ${r.status} ${await r.text()}`);
225
- }
226
- return r;
638
+ // src/bin-internal/eval-report.ts
639
+ import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
640
+ function scoreJudgment(j) {
641
+ const nonNa = j.dimensions.filter((d) => d.verdict !== "NA");
642
+ if (nonNa.length === 0)
643
+ return { passed: true, score: 1 };
644
+ const passes = nonNa.filter((d) => d.verdict === "PASS").length;
645
+ const score = passes / nonNa.length;
646
+ const passed = nonNa.every((d) => d.verdict === "PASS");
647
+ return { passed, score };
648
+ }
649
+ function renderReport(summary) {
650
+ const lines = [];
651
+ lines.push(`# xera eval report ${summary.run_id}`);
652
+ lines.push("");
653
+ lines.push(`**Git SHA:** \`${summary.git_sha}\``);
654
+ lines.push("");
655
+ lines.push("**Prompt versions:**");
656
+ for (const [k, v] of Object.entries(summary.prompt_versions))
657
+ lines.push(`- \`${k}\`: ${v}`);
658
+ lines.push("");
659
+ lines.push(`**Overall:** ${summary.overall.passed}/${summary.overall.total} PASS (score ${(summary.overall.score * 100).toFixed(0)}%)`);
660
+ lines.push("");
661
+ lines.push("## Results");
662
+ lines.push("");
663
+ lines.push("| Ticket | Stage | Deterministic | Judge | Score |");
664
+ lines.push("|---|---|---|---|---|");
665
+ for (const r of summary.results) {
666
+ const det = r.deterministic.passed ? "PASS" : `FAIL (${r.deterministic.error ?? ""})`;
667
+ const judge = r.skipped ? "SKIPPED" : r.judge ? r.judge.passed ? "PASS" : "FAIL" : "SKIPPED";
668
+ const score = r.judge ? `${(r.judge.score * 100).toFixed(0)}%` : "\u2014";
669
+ lines.push(`| ${r.ticket} | ${r.stage} | ${det} | ${judge} | ${score} |`);
227
670
  }
228
- return {
229
- backend: "rest",
230
- async fetchTicket(key, fields) {
231
- const want = ["summary", fields.story];
232
- if (fields.acceptanceCriteria)
233
- want.push(fields.acceptanceCriteria);
234
- want.push("attachment");
235
- const r = await req(`/rest/api/3/issue/${encodeURIComponent(key)}?fields=${want.join(",")}`);
236
- const json = await r.json();
237
- const f = json.fields;
238
- const attachments = Array.isArray(f.attachment) ? f.attachment.map((a) => ({ filename: a.filename, url: a.content })) : [];
671
+ lines.push("");
672
+ lines.push("## Dimension breakdown");
673
+ lines.push("");
674
+ for (const r of summary.results) {
675
+ if (!r.judge || r.judge.dimensions.length === 0)
676
+ continue;
677
+ lines.push(`### ${r.ticket} \u2014 ${r.stage}`);
678
+ lines.push("");
679
+ for (const d of r.judge.dimensions)
680
+ lines.push(`- **${d.name}** \u2014 ${d.verdict}: ${d.notes}`);
681
+ lines.push("");
682
+ }
683
+ return lines.join(`
684
+ `);
685
+ }
686
+ async function evalReportCmd(argv, opts = {}) {
687
+ const cwd = opts.cwd ?? process.cwd();
688
+ const runId = argv[0];
689
+ if (!runId) {
690
+ console.error("[xera:eval-report] usage: eval-report <run-id>");
691
+ return 1;
692
+ }
693
+ const paths = resolveEvalPaths(cwd, runId);
694
+ if (!existsSync6(paths.manifest)) {
695
+ console.error(`[xera:eval-report] missing manifest.json at ${paths.manifest}`);
696
+ return 1;
697
+ }
698
+ const manifest = ManifestSchema.parse(JSON.parse(readFileSync6(paths.manifest, "utf8")));
699
+ try {
700
+ let det;
701
+ let judge;
702
+ try {
703
+ det = DeterministicScoresSchema.parse(JSON.parse(readFileSync6(paths.deterministicScores, "utf8")));
704
+ } catch (err) {
705
+ console.error(`[xera:eval-report] invalid deterministic-scores.json: ${err.message}`);
706
+ return 2;
707
+ }
708
+ try {
709
+ judge = JudgeScoresSchema.parse(JSON.parse(readFileSync6(paths.judgeScores, "utf8")));
710
+ } catch (err) {
711
+ console.error(`[xera:eval-report] invalid judge-scores.json: ${err.message}`);
712
+ return 2;
713
+ }
714
+ const results = [];
715
+ for (const detEntry of det.entries) {
716
+ const judgment = judge.judgments.find((j) => j.ticket === detEntry.ticket && j.stage === detEntry.stage);
717
+ if (!judgment && detEntry.error?.startsWith("actual missing")) {
718
+ const r2 = {
719
+ ticket: detEntry.ticket,
720
+ stage: detEntry.stage,
721
+ deterministic: {
722
+ passed: detEntry.passed,
723
+ checks: detEntry.checks,
724
+ ...detEntry.error !== undefined ? { error: detEntry.error } : {}
725
+ },
726
+ judge: null,
727
+ skipped: true
728
+ };
729
+ results.push(r2);
730
+ continue;
731
+ }
732
+ if (!judgment) {
733
+ const r2 = {
734
+ ticket: detEntry.ticket,
735
+ stage: detEntry.stage,
736
+ deterministic: {
737
+ passed: detEntry.passed,
738
+ checks: detEntry.checks,
739
+ ...detEntry.error !== undefined ? { error: detEntry.error } : {}
740
+ },
741
+ judge: { passed: false, dimensions: [], score: 0 }
742
+ };
743
+ results.push(r2);
744
+ continue;
745
+ }
746
+ const { passed, score } = scoreJudgment(judgment);
747
+ const r = {
748
+ ticket: detEntry.ticket,
749
+ stage: detEntry.stage,
750
+ deterministic: {
751
+ passed: detEntry.passed,
752
+ checks: detEntry.checks,
753
+ ...detEntry.error !== undefined ? { error: detEntry.error } : {}
754
+ },
755
+ judge: { passed, dimensions: judgment.dimensions, score }
756
+ };
757
+ results.push(r);
758
+ }
759
+ const counted = results.filter((r) => !r.skipped);
760
+ const passedCount = counted.filter((r) => r.deterministic.passed && r.judge?.passed).length;
761
+ const failedCount = counted.length - passedCount;
762
+ const avgScore = counted.length === 0 ? 0 : counted.reduce((acc, r) => acc + (r.deterministic.passed && r.judge ? r.judge.score : 0), 0) / counted.length;
763
+ const summary = {
764
+ run_id: runId,
765
+ git_sha: manifest.git_sha,
766
+ prompt_versions: manifest.prompt_versions,
767
+ results,
768
+ overall: { passed: passedCount, failed: failedCount, total: counted.length, score: avgScore }
769
+ };
770
+ SummarySchema.parse(summary);
771
+ writeFileSync4(paths.summary, JSON.stringify(summary, null, 2));
772
+ writeFileSync4(paths.report, renderReport(summary));
773
+ console.log(`[xera:eval-report] ${passedCount}/${counted.length} PASS (avg ${(avgScore * 100).toFixed(0)}%)`);
774
+ return 0;
775
+ } finally {
776
+ releaseLock(paths.lock);
777
+ }
778
+ }
779
+
780
+ // src/bin-internal/exec.ts
781
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5 } from "fs";
782
+ import { join as join9 } from "path";
783
+ import { chromium } from "@playwright/test";
784
+ import { runAuthSetup, runPlaywright, stagePlaywrightState } from "@xera-ai/web";
785
+
786
+ // src/artifact/paths.ts
787
+ import { join as join6 } from "path";
788
+ var TICKET_RE = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
789
+ function resolveArtifactPaths(repoRoot, ticket) {
790
+ if (!TICKET_RE.test(ticket)) {
791
+ throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
792
+ }
793
+ const ticketDir = join6(repoRoot, ".xera", ticket);
794
+ return {
795
+ ticketDir,
796
+ storyPath: join6(ticketDir, "story.md"),
797
+ featurePath: join6(ticketDir, "test.feature"),
798
+ specPath: join6(ticketDir, "spec.ts"),
799
+ pageObjectsDir: join6(ticketDir, "page-objects"),
800
+ runsDir: join6(ticketDir, "runs"),
801
+ metaPath: join6(ticketDir, "meta.json"),
802
+ statusPath: join6(ticketDir, "status.json"),
803
+ logPath: join6(ticketDir, "xera.log"),
804
+ lockPath: join6(ticketDir, ".lock"),
805
+ authDir: join6(repoRoot, ".xera", ".auth"),
806
+ runPath: (runId) => {
807
+ const runDir = join6(ticketDir, "runs", runId);
808
+ return {
809
+ runDir,
810
+ reportJsonPath: join6(runDir, "report.json"),
811
+ tracePath: join6(runDir, "trace.zip"),
812
+ normalizedPath: join6(runDir, "normalized.json"),
813
+ screenshotsDir: join6(runDir, "screenshots"),
814
+ videoDir: join6(runDir, "videos")
815
+ };
816
+ }
817
+ };
818
+ }
819
+ function generateRunId2(now = new Date) {
820
+ return now.toISOString().replace(/[:.]/g, "-").replace("Z", "");
821
+ }
822
+
823
+ // src/auth/refresh.ts
824
+ var RE = /^(\d+)([hms])$/;
825
+ function parseDuration(d) {
826
+ const m = RE.exec(d);
827
+ if (!m)
828
+ throw new Error(`Bad duration "${d}" \u2014 expected e.g. "8h", "30m", "45s"`);
829
+ const n = Number(m[1]);
830
+ const unit = m[2];
831
+ if (unit === "h")
832
+ return n * 3600 * 1000;
833
+ if (unit === "m")
834
+ return n * 60 * 1000;
835
+ return n * 1000;
836
+ }
837
+ function needsRefresh(entry, policy, now = new Date) {
838
+ if (!entry)
839
+ return true;
840
+ const ttlMs = parseDuration(policy.ttl);
841
+ const bufMs = parseDuration(policy.refreshBuffer);
842
+ const createdAt = new Date(entry.created_at).getTime();
843
+ if (now.getTime() - createdAt > ttlMs)
844
+ return true;
845
+ const expiresAt = new Date(entry.expires_at).getTime();
846
+ if (expiresAt - now.getTime() < bufMs)
847
+ return true;
848
+ return false;
849
+ }
850
+
851
+ // src/auth/state.ts
852
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
853
+ import { join as join7 } from "path";
854
+ import { z as z2 } from "zod";
855
+
856
+ // src/auth/encrypt.ts
857
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
858
+ var ALGO = "aes-256-gcm";
859
+ var KEY_LEN = 32;
860
+ var IV_LEN = 12;
861
+ var TAG_LEN = 16;
862
+ var VERSION = "v1";
863
+ function generateKey() {
864
+ return randomBytes(KEY_LEN).toString("hex");
865
+ }
866
+ function keyToBuf(key) {
867
+ const buf = Buffer.from(key, "hex");
868
+ if (buf.length !== KEY_LEN)
869
+ throw new Error(`Key must be ${KEY_LEN} bytes (got ${buf.length})`);
870
+ return buf;
871
+ }
872
+ function encrypt(plaintext, keyHex) {
873
+ const key = keyToBuf(keyHex);
874
+ const iv = randomBytes(IV_LEN);
875
+ const cipher = createCipheriv(ALGO, key, iv);
876
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
877
+ const tag = cipher.getAuthTag();
878
+ return `${VERSION}:${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
879
+ }
880
+ function decrypt(ciphertext, keyHex) {
881
+ const [version, ivB64, tagB64, ctB64] = ciphertext.split(":");
882
+ if (version !== VERSION)
883
+ throw new Error(`Unsupported ciphertext version: ${version}`);
884
+ if (!ivB64 || !tagB64 || !ctB64)
885
+ throw new Error("Malformed ciphertext");
886
+ const key = keyToBuf(keyHex);
887
+ const iv = Buffer.from(ivB64, "base64");
888
+ const tag = Buffer.from(tagB64, "base64");
889
+ const ct = Buffer.from(ctB64, "base64");
890
+ if (tag.length !== TAG_LEN)
891
+ throw new Error("Bad auth tag length");
892
+ const decipher = createDecipheriv(ALGO, key, iv);
893
+ decipher.setAuthTag(tag);
894
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
895
+ }
896
+
897
+ // src/auth/key.ts
898
+ var AUTH_KEY_ENV = "XERA_AUTH_KEY";
899
+ function resolveAuthKey() {
900
+ const key = process.env[AUTH_KEY_ENV];
901
+ if (!key) {
902
+ throw new Error(`${AUTH_KEY_ENV} not set. It is auto-generated by \`xera init\` and saved to .env. If you deleted .env, regenerate it by running \`xera init --update\` \u2014 note that any cached auth state will be invalidated.`);
903
+ }
904
+ if (!/^[0-9a-f]{64}$/i.test(key)) {
905
+ throw new Error(`${AUTH_KEY_ENV} must be a 64-character hex string (32 bytes).`);
906
+ }
907
+ return key;
908
+ }
909
+
910
+ // src/auth/state.ts
911
+ var AuthStateEntrySchema = z2.object({
912
+ role: z2.string(),
913
+ strategy: z2.enum(["storageState", "apiToken"]),
914
+ created_at: z2.string(),
915
+ expires_at: z2.string(),
916
+ payload: z2.record(z2.string(), z2.unknown())
917
+ });
918
+ function pathFor(authDir, role) {
919
+ return join7(authDir, `${role}.json`);
920
+ }
921
+ function writeAuthState(authDir, entry) {
922
+ mkdirSync3(authDir, { recursive: true });
923
+ const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
924
+ writeFileSync5(pathFor(authDir, entry.role), ct);
925
+ }
926
+ function readAuthState(authDir, role) {
927
+ const p = pathFor(authDir, role);
928
+ if (!existsSync7(p))
929
+ return null;
930
+ const txt = readFileSync7(p, "utf8");
931
+ const plain = decrypt(txt, resolveAuthKey());
932
+ return AuthStateEntrySchema.parse(JSON.parse(plain));
933
+ }
934
+
935
+ // src/config/load.ts
936
+ import { existsSync as existsSync8 } from "fs";
937
+ import { join as join8 } from "path";
938
+ import { pathToFileURL } from "url";
939
+
940
+ // src/config/schema.ts
941
+ import { z as z3 } from "zod";
942
+ var AuthRoleSchema = z3.object({
943
+ envEmail: z3.string().min(1),
944
+ envPassword: z3.string().min(1)
945
+ });
946
+ var AuthSchema = z3.object({
947
+ strategy: z3.enum(["storageState", "apiToken", "none"]).default("none"),
948
+ ttl: z3.string().default("8h"),
949
+ refreshBuffer: z3.string().default("30m"),
950
+ setupScript: z3.string().optional(),
951
+ roles: z3.record(z3.string(), AuthRoleSchema).default({})
952
+ });
953
+ var WebSchema = z3.object({
954
+ baseUrl: z3.record(z3.string(), z3.string().url()).refine((m) => Object.keys(m).length > 0, {
955
+ message: "baseUrl must have at least one environment"
956
+ }),
957
+ defaultEnv: z3.string(),
958
+ auth: AuthSchema.default({}),
959
+ testData: z3.object({
960
+ users: z3.record(z3.string(), z3.object({ fromAuth: z3.string() })).default({})
961
+ }).default({ users: {} })
962
+ }).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
963
+ message: "defaultEnv must exist in baseUrl map",
964
+ path: ["defaultEnv"]
965
+ });
966
+ var JiraSchema = z3.object({
967
+ baseUrl: z3.string().url(),
968
+ projectKeys: z3.array(z3.string().min(1)).min(1),
969
+ fields: z3.object({
970
+ story: z3.string().min(1),
971
+ acceptanceCriteria: z3.string().optional(),
972
+ attachments: z3.string().default("attachment")
973
+ })
974
+ });
975
+ var AISchema = z3.object({
976
+ livePageSnapshot: z3.boolean().default(true),
977
+ confidenceThreshold: z3.enum(["low", "medium", "high"]).default("medium"),
978
+ maxRetries: z3.object({
979
+ typecheck: z3.number().int().min(0).max(5).default(2),
980
+ lint: z3.number().int().min(0).max(5).default(2),
981
+ validateFeature: z3.number().int().min(0).max(5).default(2)
982
+ }).default({})
983
+ }).default({});
984
+ var ReportingSchema = z3.object({
985
+ language: z3.enum(["en", "vi"]).default("en"),
986
+ postToJira: z3.boolean().default(true),
987
+ transition: z3.object({
988
+ onPass: z3.string().nullable().default(null),
989
+ onFail: z3.string().nullable().default(null)
990
+ }).default({}),
991
+ artifactLinks: z3.enum(["git", "local"]).default("git")
992
+ }).default({});
993
+ var XeraConfigSchema = z3.object({
994
+ jira: JiraSchema,
995
+ web: WebSchema,
996
+ ai: AISchema,
997
+ reporting: ReportingSchema,
998
+ adapters: z3.array(z3.string().min(1)).min(1).default(["web"])
999
+ });
1000
+
1001
+ // src/config/load.ts
1002
+ async function loadConfig(cwd) {
1003
+ const path = join8(cwd, "xera.config.ts");
1004
+ if (!existsSync8(path)) {
1005
+ throw new Error(`xera.config.ts not found in ${cwd}`);
1006
+ }
1007
+ const mod = await import(pathToFileURL(path).href);
1008
+ const raw = mod.default ?? mod;
1009
+ return XeraConfigSchema.parse(raw);
1010
+ }
1011
+
1012
+ // src/logging/ndjson-logger.ts
1013
+ import { appendFileSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync8 } from "fs";
1014
+ import { dirname as dirname2 } from "path";
1015
+
1016
+ class NdjsonLogger {
1017
+ path;
1018
+ constructor(path) {
1019
+ this.path = path;
1020
+ mkdirSync4(dirname2(path), { recursive: true });
1021
+ }
1022
+ log(payload) {
1023
+ const entry = { ts: new Date().toISOString(), ...payload };
1024
+ appendFileSync(this.path, `${JSON.stringify(entry)}
1025
+ `);
1026
+ }
1027
+ static readAll(path) {
1028
+ if (!existsSync9(path))
1029
+ return [];
1030
+ const txt = readFileSync8(path, "utf8").trim();
1031
+ if (!txt)
1032
+ return [];
1033
+ return txt.split(`
1034
+ `).map((line) => JSON.parse(line));
1035
+ }
1036
+ }
1037
+
1038
+ // src/bin-internal/exec.ts
1039
+ async function execCmd(argv) {
1040
+ const ticket = argv[0];
1041
+ if (!ticket) {
1042
+ console.error("[xera:exec] usage: exec <TICKET>");
1043
+ return 1;
1044
+ }
1045
+ const cwd = process.cwd();
1046
+ const config = await loadConfig(cwd);
1047
+ const paths = resolveArtifactPaths(cwd, ticket);
1048
+ const runId = generateRunId2();
1049
+ const log = new NdjsonLogger(paths.logPath);
1050
+ if (!acquireLock(paths.lockPath, runId)) {
1051
+ if (isLockStale(paths.lockPath)) {
1052
+ console.error(`[xera:exec] stale lock detected; force unlocking. Run \`xera-internal unlock ${ticket}\` to clear manually.`);
1053
+ forceUnlock(paths.lockPath);
1054
+ acquireLock(paths.lockPath, runId);
1055
+ } else {
1056
+ const existing = readLock(paths.lockPath);
1057
+ console.error(`[xera:exec] another run in progress (PID ${existing?.pid} on ${existing?.hostname}, started ${existing?.started_at}). Wait or run \`xera-internal unlock ${ticket}\`.`);
1058
+ return 1;
1059
+ }
1060
+ }
1061
+ const t0 = Date.now();
1062
+ try {
1063
+ if (config.web.auth.strategy === "storageState" && config.web.auth.setupScript) {
1064
+ const browser = await chromium.launch();
1065
+ try {
1066
+ for (const [roleName, roleCreds] of Object.entries(config.web.auth.roles)) {
1067
+ const entry = readAuthState(paths.authDir, roleName);
1068
+ if (needsRefresh(entry, {
1069
+ ttl: config.web.auth.ttl,
1070
+ refreshBuffer: config.web.auth.refreshBuffer
1071
+ })) {
1072
+ const email = process.env[roleCreds.envEmail];
1073
+ const password = process.env[roleCreds.envPassword];
1074
+ if (!email || !password) {
1075
+ console.error(`[xera:exec] missing env ${roleCreds.envEmail} or ${roleCreds.envPassword} for role "${roleName}"`);
1076
+ return 1;
1077
+ }
1078
+ await runAuthSetup({
1079
+ role: roleName,
1080
+ creds: { email, password },
1081
+ setupScriptPath: join9(cwd, config.web.auth.setupScript),
1082
+ authDir: paths.authDir,
1083
+ browser
1084
+ });
1085
+ log.log({ step: "auth-refresh", role: roleName });
1086
+ }
1087
+ }
1088
+ } finally {
1089
+ await browser.close();
1090
+ }
1091
+ }
1092
+ if (config.web.auth.strategy === "storageState") {
1093
+ for (const roleName of Object.keys(config.web.auth.roles)) {
1094
+ if (readAuthState(paths.authDir, roleName)) {
1095
+ stagePlaywrightState(paths.authDir, roleName);
1096
+ }
1097
+ }
1098
+ }
1099
+ const cfgPath = join9(cwd, "playwright.config.ts");
1100
+ if (!existsSync10(cfgPath)) {
1101
+ console.error(`[xera:exec] missing ${cfgPath}. Run \`xera init\` to scaffold it, then re-run.`);
1102
+ return 1;
1103
+ }
1104
+ const runDir = paths.runPath(runId).runDir;
1105
+ mkdirSync5(runDir, { recursive: true });
1106
+ const envName = process.env.XERA_ENV ?? config.web.defaultEnv;
1107
+ const baseURL = config.web.baseUrl[envName] ?? config.web.baseUrl[config.web.defaultEnv];
1108
+ const reportJsonPath = join9(runDir, "report.json");
1109
+ log.log({ step: "exec.start", runId, env: envName, baseURL });
1110
+ const r = await runPlaywright({
1111
+ specPath: paths.specPath,
1112
+ configPath: cfgPath,
1113
+ outputDir: runDir,
1114
+ env: {
1115
+ XERA_BASE_URL: baseURL,
1116
+ XERA_ENV: envName,
1117
+ PLAYWRIGHT_JSON_OUTPUT_NAME: reportJsonPath
1118
+ }
1119
+ });
1120
+ log.log({ step: "exec.done", runId, exit: r.exitCode, ms: Date.now() - t0 });
1121
+ console.log(`[xera:exec] runId=${runId} outcome=${r.outcome}`);
1122
+ return r.outcome === "PASS" ? 0 : 3;
1123
+ } finally {
1124
+ releaseLock(paths.lockPath);
1125
+ }
1126
+ }
1127
+
1128
+ // src/bin-internal/fetch.ts
1129
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
1130
+ import { dirname as dirname4 } from "path";
1131
+
1132
+ // src/artifact/hash.ts
1133
+ import { createHash } from "crypto";
1134
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
1135
+ function hashString(s) {
1136
+ return `sha256:${createHash("sha256").update(s).digest("hex")}`;
1137
+ }
1138
+ function hashFile(path) {
1139
+ return hashString(readFileSync9(path, "utf8"));
1140
+ }
1141
+ function hashFileIfExists(path) {
1142
+ if (!existsSync11(path))
1143
+ return null;
1144
+ return hashFile(path);
1145
+ }
1146
+
1147
+ // src/artifact/meta.ts
1148
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
1149
+ import { dirname as dirname3 } from "path";
1150
+ import { z as z4 } from "zod";
1151
+ var MetaJsonSchema = z4.object({
1152
+ ticket: z4.string(),
1153
+ adapter: z4.string(),
1154
+ xera_version: z4.string(),
1155
+ prompts_version: z4.string(),
1156
+ fetched_at: z4.string().optional(),
1157
+ story_hash: z4.string().optional(),
1158
+ feature_generated_at: z4.string().optional(),
1159
+ feature_generated_from_story_hash: z4.string().optional(),
1160
+ feature_hash: z4.string().optional(),
1161
+ script_generated_at: z4.string().optional(),
1162
+ script_generated_from_feature_hash: z4.string().optional(),
1163
+ script_warnings: z4.array(z4.string()).optional()
1164
+ });
1165
+ function readMeta(path) {
1166
+ if (!existsSync12(path))
1167
+ return null;
1168
+ return MetaJsonSchema.parse(JSON.parse(readFileSync10(path, "utf8")));
1169
+ }
1170
+ function writeMeta(path, meta) {
1171
+ mkdirSync6(dirname3(path), { recursive: true });
1172
+ writeFileSync6(path, JSON.stringify(meta, null, 2));
1173
+ }
1174
+ function updateMeta(path, patch) {
1175
+ const existing = readMeta(path);
1176
+ if (!existing) {
1177
+ throw new Error(`meta.json not found at ${path}; cannot update`);
1178
+ }
1179
+ const next = { ...existing, ...patch };
1180
+ writeMeta(path, next);
1181
+ return next;
1182
+ }
1183
+
1184
+ // src/jira/mcp-backend.ts
1185
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
1186
+ import { tmpdir } from "os";
1187
+ import { join as join10 } from "path";
1188
+ var MCP_ENV = "XERA_MCP_JIRA";
1189
+ async function createMcpBackend(_baseUrl) {
1190
+ if (process.env[MCP_ENV] !== "1")
1191
+ return null;
1192
+ const tmpDir = join10(tmpdir(), "xera-mcp");
1193
+ mkdirSync7(tmpDir, { recursive: true });
1194
+ return {
1195
+ backend: "mcp",
1196
+ async fetchTicket(key, _fields) {
1197
+ const cachePath = join10(tmpDir, `${key}.json`);
1198
+ if (!existsSync13(cachePath)) {
1199
+ throw new Error(`MCP-mode fetch requires the skill to first call mcp__atlassian__getJiraIssue and write ${cachePath}. ` + `If you are running this directly, unset ${MCP_ENV} to use REST.`);
1200
+ }
1201
+ const parsed = JSON.parse(readFileSync11(cachePath, "utf8"));
1202
+ return parsed;
1203
+ },
1204
+ async postComment(key, body) {
1205
+ const outPath = join10(tmpDir, `${key}.comment.json`);
1206
+ writeFileSync7(outPath, JSON.stringify({ key, body }));
1207
+ return { id: "mcp-pending" };
1208
+ },
1209
+ async transitionStatus(key, statusName) {
1210
+ const outPath = join10(tmpDir, `${key}.transition.json`);
1211
+ writeFileSync7(outPath, JSON.stringify({ key, statusName }));
1212
+ },
1213
+ async listFields(_sampleKey) {
1214
+ throw new Error("listFields is REST-only; init flow uses REST for field discovery.");
1215
+ }
1216
+ };
1217
+ }
1218
+
1219
+ // src/jira/rest-backend.ts
1220
+ function createRestBackend(baseUrl, creds) {
1221
+ const authHeader = `Basic ${Buffer.from(`${creds.email}:${creds.apiToken}`).toString("base64")}`;
1222
+ const base = baseUrl.replace(/\/$/, "");
1223
+ async function req(path, init) {
1224
+ const r = await fetch(`${base}${path}`, {
1225
+ ...init,
1226
+ headers: {
1227
+ Authorization: authHeader,
1228
+ Accept: "application/json",
1229
+ "Content-Type": "application/json",
1230
+ ...init?.headers ?? {}
1231
+ }
1232
+ });
1233
+ if (!r.ok && r.status !== 201) {
1234
+ throw new Error(`Jira REST ${init?.method ?? "GET"} ${path} failed: ${r.status} ${await r.text()}`);
1235
+ }
1236
+ return r;
1237
+ }
1238
+ return {
1239
+ backend: "rest",
1240
+ async fetchTicket(key, fields) {
1241
+ const want = ["summary", fields.story];
1242
+ if (fields.acceptanceCriteria)
1243
+ want.push(fields.acceptanceCriteria);
1244
+ want.push("attachment");
1245
+ const r = await req(`/rest/api/3/issue/${encodeURIComponent(key)}?fields=${want.join(",")}`);
1246
+ const json = await r.json();
1247
+ const f = json.fields;
1248
+ const attachments = Array.isArray(f.attachment) ? f.attachment.map((a) => ({
1249
+ filename: a.filename,
1250
+ url: a.content
1251
+ })) : [];
239
1252
  const ticket = {
240
1253
  key: json.key,
241
1254
  summary: String(f.summary ?? ""),
@@ -252,7 +1265,11 @@ function createRestBackend(baseUrl, creds) {
252
1265
  const r = await req(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`, {
253
1266
  method: "POST",
254
1267
  body: JSON.stringify({
255
- body: { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text: body }] }] }
1268
+ body: {
1269
+ type: "doc",
1270
+ version: 1,
1271
+ content: [{ type: "paragraph", content: [{ type: "text", text: body }] }]
1272
+ }
256
1273
  })
257
1274
  });
258
1275
  const json = await r.json();
@@ -278,422 +1295,670 @@ function createRestBackend(baseUrl, creds) {
278
1295
  hasContent: value !== null && value !== undefined && value !== ""
279
1296
  }));
280
1297
  }
281
- };
282
- }
283
-
284
- // src/jira/client.ts
285
- async function createJiraClient(opts) {
286
- if (opts.preferMcp !== false) {
287
- const mcp = await createMcpBackend(opts.baseUrl);
288
- if (mcp)
289
- return mcp;
290
- }
291
- if (!opts.rest) {
292
- throw new Error("Atlassian MCP not connected and no REST credentials provided (JIRA_EMAIL + JIRA_API_TOKEN).");
293
- }
294
- return createRestBackend(opts.baseUrl, opts.rest);
295
- }
296
-
297
- // src/bin-internal/fetch.ts
298
- async function fetchCmd(argv, opts = {}) {
299
- const cwd = opts.cwd ?? process.cwd();
300
- const ticket = argv[0];
301
- if (!ticket) {
302
- console.error("[xera:fetch] usage: xera-internal fetch <TICKET>");
303
- return 1;
304
- }
305
- const config = await loadConfig(cwd);
306
- const paths = resolveArtifactPaths(cwd, ticket);
307
- let t;
308
- if (process.env.XERA_TEST_JIRA) {
309
- t = JSON.parse(process.env.XERA_TEST_JIRA);
310
- } else {
311
- const client = await createJiraClient({
312
- baseUrl: config.jira.baseUrl,
313
- preferMcp: true,
314
- ...process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN ? { rest: { email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN } } : {}
315
- });
316
- const fieldMap = config.jira.fields.acceptanceCriteria !== undefined ? { story: config.jira.fields.story, acceptanceCriteria: config.jira.fields.acceptanceCriteria } : { story: config.jira.fields.story };
317
- t = await client.fetchTicket(ticket, fieldMap);
318
- }
319
- const story = renderStory(t);
320
- mkdirSync3(dirname2(paths.storyPath), { recursive: true });
321
- writeFileSync3(paths.storyPath, story);
322
- const existing = readMeta(paths.metaPath);
323
- writeMeta(paths.metaPath, {
324
- ticket,
325
- adapter: "web",
326
- xera_version: "0.1.0",
327
- prompts_version: "1.0.0",
328
- ...existing ?? {},
329
- story_hash: hashString(story),
330
- fetched_at: new Date().toISOString()
331
- });
332
- console.log(`[xera:fetch] wrote ${paths.storyPath}`);
333
- return 0;
334
- }
335
- function renderStory(t) {
336
- const lines = [];
337
- lines.push(`# ${t.key}: ${t.summary}`, "");
338
- const story = t.story.trim();
339
- if (/^##\s+story\b/i.test(story)) {
340
- lines.push(story, "");
341
- } else {
342
- lines.push("## Story", "", story, "");
343
- }
344
- if (t.acceptanceCriteria && t.acceptanceCriteria.trim()) {
345
- const ac = t.acceptanceCriteria.trim();
346
- if (/^##\s+acceptance\s+criteria\b/i.test(ac)) {
347
- lines.push(ac, "");
348
- } else {
349
- lines.push("## Acceptance Criteria", "", ac, "");
350
- }
351
- }
352
- if (t.attachments.length > 0) {
353
- lines.push("## Attachments", "", ...t.attachments.map((a) => `- [${a.filename}](${a.url})`), "");
354
- }
355
- return lines.join(`
356
- `);
357
- }
358
-
359
- // src/bin-internal/validate-feature.ts
360
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
361
- import { validateGherkin } from "@xera-ai/web";
362
- async function validateFeatureCmd(argv) {
363
- const ticket = argv[0];
364
- if (!ticket) {
365
- console.error("[xera:validate-feature] usage: validate-feature <TICKET>");
366
- return 1;
367
- }
368
- const paths = resolveArtifactPaths(process.cwd(), ticket);
369
- if (!existsSync5(paths.featurePath)) {
370
- console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
371
- return 1;
372
- }
373
- const r = validateGherkin(readFileSync4(paths.featurePath, "utf8"));
374
- if (r.ok) {
375
- console.log("[xera:validate-feature] ok");
376
- return 0;
377
- }
378
- for (const e of r.errors)
379
- console.error(`[xera:validate-feature] line ${e.line}: ${e.message}`);
380
- return 2;
381
- }
382
-
383
- // src/bin-internal/typecheck.ts
384
- import { typecheckTicket } from "@xera-ai/web";
385
- async function typecheckCmd(argv) {
386
- const ticket = argv[0];
387
- if (!ticket) {
388
- console.error("[xera:typecheck] usage: typecheck <TICKET>");
389
- return 1;
390
- }
391
- const paths = resolveArtifactPaths(process.cwd(), ticket);
392
- const r = await typecheckTicket(paths.ticketDir);
393
- if (r.ok) {
394
- console.log("[xera:typecheck] ok");
395
- return 0;
396
- }
397
- for (const e of r.errors)
398
- console.error(`[xera:typecheck] ${e}`);
399
- return 2;
400
- }
401
-
402
- // src/bin-internal/lint.ts
403
- import { lintTicket } from "@xera-ai/web";
404
- async function lintCmd(argv) {
405
- const ticket = argv[0];
406
- if (!ticket) {
407
- console.error("[xera:lint] usage: lint <TICKET>");
408
- return 1;
409
- }
410
- const paths = resolveArtifactPaths(process.cwd(), ticket);
411
- const r = await lintTicket(paths.ticketDir);
412
- if (r.ok) {
413
- console.log("[xera:lint] ok");
414
- return 0;
415
- }
416
- for (const w of r.warnings)
417
- console.error(`[xera:lint] ${w.file}:${w.line} [${w.rule}] ${w.message}`);
418
- return 2;
419
- }
420
-
421
- // src/lock/file-lock.ts
422
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync4 } from "fs";
423
- import { dirname as dirname3 } from "path";
424
- import { hostname } from "os";
425
- function acquireLock(path, runId) {
426
- if (existsSync6(path))
427
- return false;
428
- mkdirSync4(dirname3(path), { recursive: true });
429
- const data = {
430
- pid: process.pid,
431
- hostname: hostname(),
432
- started_at: new Date().toISOString(),
433
- run_id: runId
434
- };
435
- try {
436
- writeFileSync4(path, JSON.stringify(data), { flag: "wx" });
437
- return true;
438
- } catch {
439
- return false;
440
- }
441
- }
442
- function releaseLock(path) {
443
- if (existsSync6(path))
444
- unlinkSync(path);
445
- }
446
- function readLock(path) {
447
- if (!existsSync6(path))
448
- return null;
449
- return JSON.parse(readFileSync5(path, "utf8"));
450
- }
451
- function isLockStale(path) {
452
- const lock = readLock(path);
453
- if (!lock)
454
- return true;
455
- if (lock.hostname !== hostname()) {
456
- return false;
457
- }
458
- try {
459
- process.kill(lock.pid, 0);
460
- return false;
461
- } catch {
462
- return true;
463
- }
464
- }
465
- function forceUnlock(path) {
466
- releaseLock(path);
467
- }
468
-
469
- // src/logging/ndjson-logger.ts
470
- import { appendFileSync, mkdirSync as mkdirSync5, existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
471
- import { dirname as dirname4 } from "path";
472
-
473
- class NdjsonLogger {
474
- path;
475
- constructor(path) {
476
- this.path = path;
477
- mkdirSync5(dirname4(path), { recursive: true });
478
- }
479
- log(payload) {
480
- const entry = { ts: new Date().toISOString(), ...payload };
481
- appendFileSync(this.path, `${JSON.stringify(entry)}
482
- `);
483
- }
484
- static readAll(path) {
485
- if (!existsSync7(path))
486
- return [];
487
- const txt = readFileSync6(path, "utf8").trim();
488
- if (!txt)
489
- return [];
490
- return txt.split(`
491
- `).map((line) => JSON.parse(line));
492
- }
493
- }
494
-
495
- // src/auth/state.ts
496
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
497
- import { join as join4 } from "path";
498
- import { z as z3 } from "zod";
499
-
500
- // src/auth/encrypt.ts
501
- import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
502
- var ALGO = "aes-256-gcm";
503
- var KEY_LEN = 32;
504
- var IV_LEN = 12;
505
- var TAG_LEN = 16;
506
- var VERSION = "v1";
507
- function generateKey() {
508
- return randomBytes(KEY_LEN).toString("hex");
509
- }
510
- function keyToBuf(key) {
511
- const buf = Buffer.from(key, "hex");
512
- if (buf.length !== KEY_LEN)
513
- throw new Error(`Key must be ${KEY_LEN} bytes (got ${buf.length})`);
514
- return buf;
515
- }
516
- function encrypt(plaintext, keyHex) {
517
- const key = keyToBuf(keyHex);
518
- const iv = randomBytes(IV_LEN);
519
- const cipher = createCipheriv(ALGO, key, iv);
520
- const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
521
- const tag = cipher.getAuthTag();
522
- return `${VERSION}:${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
523
- }
524
- function decrypt(ciphertext, keyHex) {
525
- const [version, ivB64, tagB64, ctB64] = ciphertext.split(":");
526
- if (version !== VERSION)
527
- throw new Error(`Unsupported ciphertext version: ${version}`);
528
- if (!ivB64 || !tagB64 || !ctB64)
529
- throw new Error("Malformed ciphertext");
530
- const key = keyToBuf(keyHex);
531
- const iv = Buffer.from(ivB64, "base64");
532
- const tag = Buffer.from(tagB64, "base64");
533
- const ct = Buffer.from(ctB64, "base64");
534
- if (tag.length !== TAG_LEN)
535
- throw new Error("Bad auth tag length");
536
- const decipher = createDecipheriv(ALGO, key, iv);
537
- decipher.setAuthTag(tag);
538
- return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
1298
+ };
539
1299
  }
540
1300
 
541
- // src/auth/key.ts
542
- var AUTH_KEY_ENV = "XERA_AUTH_KEY";
543
- function resolveAuthKey() {
544
- const key = process.env[AUTH_KEY_ENV];
545
- if (!key) {
546
- throw new Error(`${AUTH_KEY_ENV} not set. It is auto-generated by \`xera init\` and saved to .env. ` + `If you deleted .env, regenerate it by running \`xera init --update\` \u2014 note that any cached auth state will be invalidated.`);
1301
+ // src/jira/client.ts
1302
+ async function createJiraClient(opts) {
1303
+ if (opts.preferMcp !== false) {
1304
+ const mcp = await createMcpBackend(opts.baseUrl);
1305
+ if (mcp)
1306
+ return mcp;
547
1307
  }
548
- if (!/^[0-9a-f]{64}$/i.test(key)) {
549
- throw new Error(`${AUTH_KEY_ENV} must be a 64-character hex string (32 bytes).`);
1308
+ if (!opts.rest) {
1309
+ throw new Error("Atlassian MCP not connected and no REST credentials provided (JIRA_EMAIL + JIRA_API_TOKEN).");
550
1310
  }
551
- return key;
552
- }
553
-
554
- // src/auth/state.ts
555
- var AuthStateEntrySchema = z3.object({
556
- role: z3.string(),
557
- strategy: z3.enum(["storageState", "apiToken"]),
558
- created_at: z3.string(),
559
- expires_at: z3.string(),
560
- payload: z3.record(z3.string(), z3.unknown())
561
- });
562
- function pathFor(authDir, role) {
563
- return join4(authDir, `${role}.json`);
564
- }
565
- function writeAuthState(authDir, entry) {
566
- mkdirSync6(authDir, { recursive: true });
567
- const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
568
- writeFileSync5(pathFor(authDir, entry.role), ct);
569
- }
570
- function readAuthState(authDir, role) {
571
- const p = pathFor(authDir, role);
572
- if (!existsSync8(p))
573
- return null;
574
- const txt = readFileSync7(p, "utf8");
575
- const plain = decrypt(txt, resolveAuthKey());
576
- return AuthStateEntrySchema.parse(JSON.parse(plain));
577
- }
578
-
579
- // src/auth/refresh.ts
580
- var RE = /^(\d+)([hms])$/;
581
- function parseDuration(d) {
582
- const m = RE.exec(d);
583
- if (!m)
584
- throw new Error(`Bad duration "${d}" \u2014 expected e.g. "8h", "30m", "45s"`);
585
- const n = Number(m[1]);
586
- const unit = m[2];
587
- if (unit === "h")
588
- return n * 3600 * 1000;
589
- if (unit === "m")
590
- return n * 60 * 1000;
591
- return n * 1000;
592
- }
593
- function needsRefresh(entry, policy, now = new Date) {
594
- if (!entry)
595
- return true;
596
- const ttlMs = parseDuration(policy.ttl);
597
- const bufMs = parseDuration(policy.refreshBuffer);
598
- const createdAt = new Date(entry.created_at).getTime();
599
- if (now.getTime() - createdAt > ttlMs)
600
- return true;
601
- const expiresAt = new Date(entry.expires_at).getTime();
602
- if (expiresAt - now.getTime() < bufMs)
603
- return true;
604
- return false;
1311
+ return createRestBackend(opts.baseUrl, opts.rest);
605
1312
  }
606
1313
 
607
- // src/bin-internal/exec.ts
608
- import { stagePlaywrightState, runAuthSetup, runPlaywright } from "@xera-ai/web";
609
- import { chromium } from "@playwright/test";
610
- import { mkdirSync as mkdirSync7, existsSync as existsSync9 } from "fs";
611
- import { join as join5 } from "path";
612
- async function execCmd(argv) {
1314
+ // src/bin-internal/fetch.ts
1315
+ async function fetchCmd(argv, opts = {}) {
1316
+ const cwd = opts.cwd ?? process.cwd();
613
1317
  const ticket = argv[0];
614
1318
  if (!ticket) {
615
- console.error("[xera:exec] usage: exec <TICKET>");
1319
+ console.error("[xera:fetch] usage: xera-internal fetch <TICKET>");
616
1320
  return 1;
617
1321
  }
618
- const cwd = process.cwd();
619
1322
  const config = await loadConfig(cwd);
620
1323
  const paths = resolveArtifactPaths(cwd, ticket);
621
- const runId = generateRunId();
622
- const log = new NdjsonLogger(paths.logPath);
623
- if (!acquireLock(paths.lockPath, runId)) {
624
- if (isLockStale(paths.lockPath)) {
625
- console.error(`[xera:exec] stale lock detected; force unlocking. Run \`xera-internal unlock ${ticket}\` to clear manually.`);
626
- forceUnlock(paths.lockPath);
627
- acquireLock(paths.lockPath, runId);
1324
+ let t;
1325
+ if (process.env.XERA_TEST_JIRA) {
1326
+ t = JSON.parse(process.env.XERA_TEST_JIRA);
1327
+ } else {
1328
+ const client = await createJiraClient({
1329
+ baseUrl: config.jira.baseUrl,
1330
+ preferMcp: true,
1331
+ ...process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN ? { rest: { email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN } } : {}
1332
+ });
1333
+ const fieldMap = config.jira.fields.acceptanceCriteria !== undefined ? {
1334
+ story: config.jira.fields.story,
1335
+ acceptanceCriteria: config.jira.fields.acceptanceCriteria
1336
+ } : { story: config.jira.fields.story };
1337
+ t = await client.fetchTicket(ticket, fieldMap);
1338
+ }
1339
+ const story = renderStory(t);
1340
+ mkdirSync8(dirname4(paths.storyPath), { recursive: true });
1341
+ writeFileSync8(paths.storyPath, story);
1342
+ const existing = readMeta(paths.metaPath);
1343
+ writeMeta(paths.metaPath, {
1344
+ ticket,
1345
+ adapter: "web",
1346
+ xera_version: "0.1.0",
1347
+ prompts_version: "1.0.0",
1348
+ ...existing ?? {},
1349
+ story_hash: hashString(story),
1350
+ fetched_at: new Date().toISOString()
1351
+ });
1352
+ console.log(`[xera:fetch] wrote ${paths.storyPath}`);
1353
+ return 0;
1354
+ }
1355
+ function renderStory(t) {
1356
+ const lines = [];
1357
+ lines.push(`# ${t.key}: ${t.summary}`, "");
1358
+ const story = t.story.trim();
1359
+ if (/^##\s+story\b/i.test(story)) {
1360
+ lines.push(story, "");
1361
+ } else {
1362
+ lines.push("## Story", "", story, "");
1363
+ }
1364
+ if (t.acceptanceCriteria?.trim()) {
1365
+ const ac = t.acceptanceCriteria.trim();
1366
+ if (/^##\s+acceptance\s+criteria\b/i.test(ac)) {
1367
+ lines.push(ac, "");
628
1368
  } else {
629
- const existing = readLock(paths.lockPath);
630
- console.error(`[xera:exec] another run in progress (PID ${existing?.pid} on ${existing?.hostname}, started ${existing?.started_at}). Wait or run \`xera-internal unlock ${ticket}\`.`);
631
- return 1;
1369
+ lines.push("## Acceptance Criteria", "", ac, "");
632
1370
  }
633
1371
  }
634
- const t0 = Date.now();
635
- try {
636
- if (config.web.auth.strategy === "storageState" && config.web.auth.setupScript) {
637
- const browser = await chromium.launch();
638
- try {
639
- for (const [roleName, roleCreds] of Object.entries(config.web.auth.roles)) {
640
- const entry = readAuthState(paths.authDir, roleName);
641
- if (needsRefresh(entry, { ttl: config.web.auth.ttl, refreshBuffer: config.web.auth.refreshBuffer })) {
642
- const email = process.env[roleCreds.envEmail];
643
- const password = process.env[roleCreds.envPassword];
644
- if (!email || !password) {
645
- console.error(`[xera:exec] missing env ${roleCreds.envEmail} or ${roleCreds.envPassword} for role "${roleName}"`);
646
- return 1;
647
- }
648
- await runAuthSetup({
649
- role: roleName,
650
- creds: { email, password },
651
- setupScriptPath: join5(cwd, config.web.auth.setupScript),
652
- authDir: paths.authDir,
653
- browser
654
- });
655
- log.log({ step: "auth-refresh", role: roleName });
1372
+ if (t.attachments.length > 0) {
1373
+ lines.push("## Attachments", "", ...t.attachments.map((a) => `- [${a.filename}](${a.url})`), "");
1374
+ }
1375
+ return lines.join(`
1376
+ `);
1377
+ }
1378
+
1379
+ // src/bin-internal/heal-prepare.ts
1380
+ import { existsSync as existsSync14, readFileSync as readFileSync12, readdirSync as readdirSync3, writeFileSync as writeFileSync9 } from "fs";
1381
+ import { join as join11 } from "path";
1382
+ import { scrubFreeText } from "@xera-ai/web";
1383
+
1384
+ // ../../node_modules/.bun/fflate@0.8.2/node_modules/fflate/esm/index.mjs
1385
+ import { createRequire } from "module";
1386
+ var require2 = createRequire("/");
1387
+ var Worker;
1388
+ try {
1389
+ Worker = require2("worker_threads").Worker;
1390
+ } catch (e) {}
1391
+ var u8 = Uint8Array;
1392
+ var u16 = Uint16Array;
1393
+ var i32 = Int32Array;
1394
+ var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0]);
1395
+ var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0]);
1396
+ var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
1397
+ var freb = function(eb, start) {
1398
+ var b = new u16(31);
1399
+ for (var i = 0;i < 31; ++i) {
1400
+ b[i] = start += 1 << eb[i - 1];
1401
+ }
1402
+ var r = new i32(b[30]);
1403
+ for (var i = 1;i < 30; ++i) {
1404
+ for (var j = b[i];j < b[i + 1]; ++j) {
1405
+ r[j] = j - b[i] << 5 | i;
1406
+ }
1407
+ }
1408
+ return { b, r };
1409
+ };
1410
+ var _a = freb(fleb, 2);
1411
+ var fl = _a.b;
1412
+ var revfl = _a.r;
1413
+ fl[28] = 258, revfl[258] = 28;
1414
+ var _b = freb(fdeb, 0);
1415
+ var fd = _b.b;
1416
+ var revfd = _b.r;
1417
+ var rev = new u16(32768);
1418
+ for (i = 0;i < 32768; ++i) {
1419
+ x = (i & 43690) >> 1 | (i & 21845) << 1;
1420
+ x = (x & 52428) >> 2 | (x & 13107) << 2;
1421
+ x = (x & 61680) >> 4 | (x & 3855) << 4;
1422
+ rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
1423
+ }
1424
+ var x;
1425
+ var i;
1426
+ var hMap = function(cd, mb, r) {
1427
+ var s = cd.length;
1428
+ var i2 = 0;
1429
+ var l = new u16(mb);
1430
+ for (;i2 < s; ++i2) {
1431
+ if (cd[i2])
1432
+ ++l[cd[i2] - 1];
1433
+ }
1434
+ var le = new u16(mb);
1435
+ for (i2 = 1;i2 < mb; ++i2) {
1436
+ le[i2] = le[i2 - 1] + l[i2 - 1] << 1;
1437
+ }
1438
+ var co;
1439
+ if (r) {
1440
+ co = new u16(1 << mb);
1441
+ var rvb = 15 - mb;
1442
+ for (i2 = 0;i2 < s; ++i2) {
1443
+ if (cd[i2]) {
1444
+ var sv = i2 << 4 | cd[i2];
1445
+ var r_1 = mb - cd[i2];
1446
+ var v = le[cd[i2] - 1]++ << r_1;
1447
+ for (var m = v | (1 << r_1) - 1;v <= m; ++v) {
1448
+ co[rev[v] >> rvb] = sv;
1449
+ }
1450
+ }
1451
+ }
1452
+ } else {
1453
+ co = new u16(s);
1454
+ for (i2 = 0;i2 < s; ++i2) {
1455
+ if (cd[i2]) {
1456
+ co[i2] = rev[le[cd[i2] - 1]++] >> 15 - cd[i2];
1457
+ }
1458
+ }
1459
+ }
1460
+ return co;
1461
+ };
1462
+ var flt = new u8(288);
1463
+ for (i = 0;i < 144; ++i)
1464
+ flt[i] = 8;
1465
+ var i;
1466
+ for (i = 144;i < 256; ++i)
1467
+ flt[i] = 9;
1468
+ var i;
1469
+ for (i = 256;i < 280; ++i)
1470
+ flt[i] = 7;
1471
+ var i;
1472
+ for (i = 280;i < 288; ++i)
1473
+ flt[i] = 8;
1474
+ var i;
1475
+ var fdt = new u8(32);
1476
+ for (i = 0;i < 32; ++i)
1477
+ fdt[i] = 5;
1478
+ var i;
1479
+ var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
1480
+ var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
1481
+ var max = function(a) {
1482
+ var m = a[0];
1483
+ for (var i2 = 1;i2 < a.length; ++i2) {
1484
+ if (a[i2] > m)
1485
+ m = a[i2];
1486
+ }
1487
+ return m;
1488
+ };
1489
+ var bits = function(d, p, m) {
1490
+ var o = p / 8 | 0;
1491
+ return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
1492
+ };
1493
+ var bits16 = function(d, p) {
1494
+ var o = p / 8 | 0;
1495
+ return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
1496
+ };
1497
+ var shft = function(p) {
1498
+ return (p + 7) / 8 | 0;
1499
+ };
1500
+ var slc = function(v, s, e) {
1501
+ if (s == null || s < 0)
1502
+ s = 0;
1503
+ if (e == null || e > v.length)
1504
+ e = v.length;
1505
+ return new u8(v.subarray(s, e));
1506
+ };
1507
+ var ec = [
1508
+ "unexpected EOF",
1509
+ "invalid block type",
1510
+ "invalid length/literal",
1511
+ "invalid distance",
1512
+ "stream finished",
1513
+ "no stream handler",
1514
+ ,
1515
+ "no callback",
1516
+ "invalid UTF-8 data",
1517
+ "extra field too long",
1518
+ "date not in range 1980-2099",
1519
+ "filename too long",
1520
+ "stream finishing",
1521
+ "invalid zip data"
1522
+ ];
1523
+ var err = function(ind, msg, nt) {
1524
+ var e = new Error(msg || ec[ind]);
1525
+ e.code = ind;
1526
+ if (Error.captureStackTrace)
1527
+ Error.captureStackTrace(e, err);
1528
+ if (!nt)
1529
+ throw e;
1530
+ return e;
1531
+ };
1532
+ var inflt = function(dat, st, buf, dict) {
1533
+ var sl = dat.length, dl = dict ? dict.length : 0;
1534
+ if (!sl || st.f && !st.l)
1535
+ return buf || new u8(0);
1536
+ var noBuf = !buf;
1537
+ var resize = noBuf || st.i != 2;
1538
+ var noSt = st.i;
1539
+ if (noBuf)
1540
+ buf = new u8(sl * 3);
1541
+ var cbuf = function(l2) {
1542
+ var bl = buf.length;
1543
+ if (l2 > bl) {
1544
+ var nbuf = new u8(Math.max(bl * 2, l2));
1545
+ nbuf.set(buf);
1546
+ buf = nbuf;
1547
+ }
1548
+ };
1549
+ var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
1550
+ var tbts = sl * 8;
1551
+ do {
1552
+ if (!lm) {
1553
+ final = bits(dat, pos, 1);
1554
+ var type = bits(dat, pos + 1, 3);
1555
+ pos += 3;
1556
+ if (!type) {
1557
+ var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
1558
+ if (t > sl) {
1559
+ if (noSt)
1560
+ err(0);
1561
+ break;
1562
+ }
1563
+ if (resize)
1564
+ cbuf(bt + l);
1565
+ buf.set(dat.subarray(s, t), bt);
1566
+ st.b = bt += l, st.p = pos = t * 8, st.f = final;
1567
+ continue;
1568
+ } else if (type == 1)
1569
+ lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
1570
+ else if (type == 2) {
1571
+ var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
1572
+ var tl = hLit + bits(dat, pos + 5, 31) + 1;
1573
+ pos += 14;
1574
+ var ldt = new u8(tl);
1575
+ var clt = new u8(19);
1576
+ for (var i2 = 0;i2 < hcLen; ++i2) {
1577
+ clt[clim[i2]] = bits(dat, pos + i2 * 3, 7);
1578
+ }
1579
+ pos += hcLen * 3;
1580
+ var clb = max(clt), clbmsk = (1 << clb) - 1;
1581
+ var clm = hMap(clt, clb, 1);
1582
+ for (var i2 = 0;i2 < tl; ) {
1583
+ var r = clm[bits(dat, pos, clbmsk)];
1584
+ pos += r & 15;
1585
+ var s = r >> 4;
1586
+ if (s < 16) {
1587
+ ldt[i2++] = s;
1588
+ } else {
1589
+ var c = 0, n = 0;
1590
+ if (s == 16)
1591
+ n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i2 - 1];
1592
+ else if (s == 17)
1593
+ n = 3 + bits(dat, pos, 7), pos += 3;
1594
+ else if (s == 18)
1595
+ n = 11 + bits(dat, pos, 127), pos += 7;
1596
+ while (n--)
1597
+ ldt[i2++] = c;
656
1598
  }
657
1599
  }
658
- } finally {
659
- await browser.close();
1600
+ var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
1601
+ lbt = max(lt);
1602
+ dbt = max(dt);
1603
+ lm = hMap(lt, lbt, 1);
1604
+ dm = hMap(dt, dbt, 1);
1605
+ } else
1606
+ err(1);
1607
+ if (pos > tbts) {
1608
+ if (noSt)
1609
+ err(0);
1610
+ break;
660
1611
  }
661
1612
  }
662
- if (config.web.auth.strategy === "storageState") {
663
- for (const roleName of Object.keys(config.web.auth.roles)) {
664
- if (readAuthState(paths.authDir, roleName)) {
665
- stagePlaywrightState(paths.authDir, roleName);
1613
+ if (resize)
1614
+ cbuf(bt + 131072);
1615
+ var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
1616
+ var lpos = pos;
1617
+ for (;; lpos = pos) {
1618
+ var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
1619
+ pos += c & 15;
1620
+ if (pos > tbts) {
1621
+ if (noSt)
1622
+ err(0);
1623
+ break;
1624
+ }
1625
+ if (!c)
1626
+ err(2);
1627
+ if (sym < 256)
1628
+ buf[bt++] = sym;
1629
+ else if (sym == 256) {
1630
+ lpos = pos, lm = null;
1631
+ break;
1632
+ } else {
1633
+ var add = sym - 254;
1634
+ if (sym > 264) {
1635
+ var i2 = sym - 257, b = fleb[i2];
1636
+ add = bits(dat, pos, (1 << b) - 1) + fl[i2];
1637
+ pos += b;
1638
+ }
1639
+ var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
1640
+ if (!d)
1641
+ err(3);
1642
+ pos += d & 15;
1643
+ var dt = fd[dsym];
1644
+ if (dsym > 3) {
1645
+ var b = fdeb[dsym];
1646
+ dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
1647
+ }
1648
+ if (pos > tbts) {
1649
+ if (noSt)
1650
+ err(0);
1651
+ break;
1652
+ }
1653
+ if (resize)
1654
+ cbuf(bt + 131072);
1655
+ var end = bt + add;
1656
+ if (bt < dt) {
1657
+ var shift = dl - dt, dend = Math.min(dt, end);
1658
+ if (shift + bt < 0)
1659
+ err(3);
1660
+ for (;bt < dend; ++bt)
1661
+ buf[bt] = dict[shift + bt];
666
1662
  }
1663
+ for (;bt < end; ++bt)
1664
+ buf[bt] = buf[bt - dt];
667
1665
  }
668
1666
  }
669
- const cfgPath = join5(cwd, "playwright.config.ts");
670
- if (!existsSync9(cfgPath)) {
671
- console.error(`[xera:exec] missing ${cfgPath}. Run \`xera init\` to scaffold it, then re-run.`);
672
- return 1;
1667
+ st.l = lm, st.p = lpos, st.b = bt, st.f = final;
1668
+ if (lm)
1669
+ final = 1, st.m = lbt, st.d = dm, st.n = dbt;
1670
+ } while (!final);
1671
+ return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
1672
+ };
1673
+ var et = /* @__PURE__ */ new u8(0);
1674
+ var b2 = function(d, b) {
1675
+ return d[b] | d[b + 1] << 8;
1676
+ };
1677
+ var b4 = function(d, b) {
1678
+ return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
1679
+ };
1680
+ var b8 = function(d, b) {
1681
+ return b4(d, b) + b4(d, b + 4) * 4294967296;
1682
+ };
1683
+ function inflateSync(data, opts) {
1684
+ return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary);
1685
+ }
1686
+ var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder;
1687
+ var tds = 0;
1688
+ try {
1689
+ td.decode(et, { stream: true });
1690
+ tds = 1;
1691
+ } catch (e) {}
1692
+ var dutf8 = function(d) {
1693
+ for (var r = "", i2 = 0;; ) {
1694
+ var c = d[i2++];
1695
+ var eb = (c > 127) + (c > 223) + (c > 239);
1696
+ if (i2 + eb > d.length)
1697
+ return { s: r, r: slc(d, i2 - 1) };
1698
+ if (!eb)
1699
+ r += String.fromCharCode(c);
1700
+ else if (eb == 3) {
1701
+ c = ((c & 15) << 18 | (d[i2++] & 63) << 12 | (d[i2++] & 63) << 6 | d[i2++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023);
1702
+ } else if (eb & 1)
1703
+ r += String.fromCharCode((c & 31) << 6 | d[i2++] & 63);
1704
+ else
1705
+ r += String.fromCharCode((c & 15) << 12 | (d[i2++] & 63) << 6 | d[i2++] & 63);
1706
+ }
1707
+ };
1708
+ function strFromU8(dat, latin1) {
1709
+ if (latin1) {
1710
+ var r = "";
1711
+ for (var i2 = 0;i2 < dat.length; i2 += 16384)
1712
+ r += String.fromCharCode.apply(null, dat.subarray(i2, i2 + 16384));
1713
+ return r;
1714
+ } else if (td) {
1715
+ return td.decode(dat);
1716
+ } else {
1717
+ var _a2 = dutf8(dat), s = _a2.s, r = _a2.r;
1718
+ if (r.length)
1719
+ err(8);
1720
+ return s;
1721
+ }
1722
+ }
1723
+ var slzh = function(d, b) {
1724
+ return b + 30 + b2(d, b + 26) + b2(d, b + 28);
1725
+ };
1726
+ var zh = function(d, b, z5) {
1727
+ var fnl = b2(d, b + 28), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl, bs = b4(d, b + 20);
1728
+ var _a2 = z5 && bs == 4294967295 ? z64e(d, es) : [bs, b4(d, b + 24), b4(d, b + 42)], sc = _a2[0], su = _a2[1], off = _a2[2];
1729
+ return [b2(d, b + 10), sc, su, fn, es + b2(d, b + 30) + b2(d, b + 32), off];
1730
+ };
1731
+ var z64e = function(d, b) {
1732
+ for (;b2(d, b) != 1; b += 4 + b2(d, b + 2))
1733
+ ;
1734
+ return [b8(d, b + 12), b8(d, b + 4), b8(d, b + 20)];
1735
+ };
1736
+ function unzipSync(data, opts) {
1737
+ var files = {};
1738
+ var e = data.length - 22;
1739
+ for (;b4(data, e) != 101010256; --e) {
1740
+ if (!e || data.length - e > 65558)
1741
+ err(13);
1742
+ }
1743
+ var c = b2(data, e + 8);
1744
+ if (!c)
1745
+ return {};
1746
+ var o = b4(data, e + 16);
1747
+ var z5 = o == 4294967295 || c == 65535;
1748
+ if (z5) {
1749
+ var ze = b4(data, e - 12);
1750
+ z5 = b4(data, ze) == 101075792;
1751
+ if (z5) {
1752
+ c = b4(data, ze + 32);
1753
+ o = b4(data, ze + 48);
673
1754
  }
674
- const runDir = paths.runPath(runId).runDir;
675
- mkdirSync7(runDir, { recursive: true });
676
- const envName = process.env.XERA_ENV ?? config.web.defaultEnv;
677
- const baseURL = config.web.baseUrl[envName] ?? config.web.baseUrl[config.web.defaultEnv];
678
- log.log({ step: "exec.start", runId, env: envName, baseURL });
679
- const r = await runPlaywright({
680
- specPath: paths.specPath,
681
- configPath: cfgPath,
682
- outputDir: runDir,
683
- env: { XERA_BASE_URL: baseURL, XERA_ENV: envName }
684
- });
685
- log.log({ step: "exec.done", runId, exit: r.exitCode, ms: Date.now() - t0 });
686
- console.log(`[xera:exec] runId=${runId} outcome=${r.outcome}`);
687
- return r.outcome === "PASS" ? 0 : 3;
688
- } finally {
689
- releaseLock(paths.lockPath);
690
1755
  }
1756
+ var fltr = opts && opts.filter;
1757
+ for (var i2 = 0;i2 < c; ++i2) {
1758
+ var _a2 = zh(data, o, z5), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
1759
+ o = no;
1760
+ if (!fltr || fltr({
1761
+ name: fn,
1762
+ size: sc,
1763
+ originalSize: su,
1764
+ compression: c_2
1765
+ })) {
1766
+ if (!c_2)
1767
+ files[fn] = slc(data, b, b + sc);
1768
+ else if (c_2 == 8)
1769
+ files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
1770
+ else
1771
+ err(14, "unknown compression type " + c_2);
1772
+ }
1773
+ }
1774
+ return files;
1775
+ }
1776
+
1777
+ // src/bin-internal/heal-prepare.ts
1778
+ var LOCATOR_LINE_RE = /^Locator:\s*(.+)$/m;
1779
+ function classifyKind(raw) {
1780
+ if (/^getByRole\b/.test(raw))
1781
+ return "role";
1782
+ if (/^getByTestId\b/.test(raw))
1783
+ return "test-id";
1784
+ if (/^getByLabel\b/.test(raw))
1785
+ return "label";
1786
+ if (/^getByText\b/.test(raw))
1787
+ return "text";
1788
+ if (/^locator\(\s*['"`]\s*\.[A-Za-z_-]/.test(raw))
1789
+ return "css-class";
1790
+ return "other";
1791
+ }
1792
+ function extractDomSnapshot(tracePath) {
1793
+ if (!existsSync14(tracePath))
1794
+ return "";
1795
+ const buf = readFileSync12(tracePath);
1796
+ const entries = unzipSync(buf);
1797
+ const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
1798
+ let chosenKey = null;
1799
+ if (traceKey) {
1800
+ const traceText = new TextDecoder().decode(entries[traceKey]);
1801
+ const lines = traceText.split(`
1802
+ `).filter(Boolean);
1803
+ for (let i2 = lines.length - 1;i2 >= 0; i2--) {
1804
+ try {
1805
+ const evt = JSON.parse(lines[i2]);
1806
+ const isSnapshot = evt.type === "frame-snapshot" || evt.type === "snapshot";
1807
+ if (!isSnapshot)
1808
+ continue;
1809
+ const snap = evt.snapshot ?? {};
1810
+ const resourceName = snap.resourceName;
1811
+ if (typeof resourceName === "string") {
1812
+ if (entries[resourceName]) {
1813
+ chosenKey = resourceName;
1814
+ break;
1815
+ }
1816
+ const guessed = `resources/${resourceName.replace(/^resources\//, "")}`;
1817
+ if (entries[guessed]) {
1818
+ chosenKey = guessed;
1819
+ break;
1820
+ }
1821
+ }
1822
+ const snapshotName = snap.snapshotName;
1823
+ if (typeof snapshotName === "string") {
1824
+ const directGuess = Object.keys(entries).find((k) => k.endsWith(".html") && k.includes(snapshotName));
1825
+ if (directGuess) {
1826
+ chosenKey = directGuess;
1827
+ break;
1828
+ }
1829
+ }
1830
+ } catch {}
1831
+ }
1832
+ }
1833
+ if (!chosenKey) {
1834
+ const htmlKeys = Object.keys(entries).filter((name) => name.endsWith(".html")).sort();
1835
+ chosenKey = htmlKeys[htmlKeys.length - 1] ?? null;
1836
+ }
1837
+ if (!chosenKey)
1838
+ return "";
1839
+ const html = new TextDecoder().decode(entries[chosenKey]);
1840
+ return scrubFreeText(html);
1841
+ }
1842
+ function findPomLine(ticketDir, rawLocator) {
1843
+ const pomDir = join11(ticketDir, "page-objects");
1844
+ const candidates = [];
1845
+ if (existsSync14(pomDir)) {
1846
+ for (const name of readdirSync3(pomDir)) {
1847
+ if (name.endsWith(".ts"))
1848
+ candidates.push(join11(pomDir, name));
1849
+ }
1850
+ }
1851
+ for (const file of candidates) {
1852
+ const text = readFileSync12(file, "utf8");
1853
+ const lines = text.split(`
1854
+ `);
1855
+ for (let i2 = 0;i2 < lines.length; i2++) {
1856
+ const line = lines[i2];
1857
+ if (line.includes(rawLocator)) {
1858
+ const methodMatch = /^\s*(\w+)\s*=/.exec(line);
1859
+ return {
1860
+ pomFile: file,
1861
+ pomLine: i2 + 1,
1862
+ pomLineContent: line,
1863
+ pomMethodName: methodMatch?.[1] ?? "<anonymous>"
1864
+ };
1865
+ }
1866
+ }
1867
+ }
1868
+ throw new Error(`POM line not found for locator: ${rawLocator}`);
1869
+ }
1870
+ function findGherkinStep(featureText, rawLocator) {
1871
+ const quoteMatch = /['"`]([^'"`]{2,})['"`]/.exec(rawLocator);
1872
+ if (quoteMatch) {
1873
+ const needle = quoteMatch[1];
1874
+ for (const line of featureText.split(`
1875
+ `)) {
1876
+ if (line.includes(needle) && /^\s*(When|Then|And|Given)\b/.test(line)) {
1877
+ return line.trim();
1878
+ }
1879
+ }
1880
+ }
1881
+ for (const line of featureText.split(`
1882
+ `)) {
1883
+ if (/^\s*(When|Then)\b/.test(line))
1884
+ return line.trim();
1885
+ }
1886
+ return "";
1887
+ }
1888
+ function healPrepare(repoRoot, ticket, runId, scenarioName) {
1889
+ const paths = resolveArtifactPaths(repoRoot, ticket);
1890
+ const classifierPath = join11(paths.ticketDir, "classifier-input.json");
1891
+ const classifier = JSON.parse(readFileSync12(classifierPath, "utf8"));
1892
+ const cls = classifier.scenarios.find((s) => s.name === scenarioName);
1893
+ if (!cls)
1894
+ throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
1895
+ const runDir = join11(paths.runsDir, runId);
1896
+ const normalized = JSON.parse(readFileSync12(join11(runDir, "normalized.json"), "utf8"));
1897
+ const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
1898
+ if (!normSc?.failure)
1899
+ throw new Error(`no failure recorded for scenario "${scenarioName}"`);
1900
+ const errorMessage = normSc.failure.errorMessage ?? "";
1901
+ const m = LOCATOR_LINE_RE.exec(errorMessage);
1902
+ if (!m)
1903
+ throw new Error(`cannot extract locator from errorMessage: ${errorMessage.slice(0, 80)}`);
1904
+ const raw = m[1].trim();
1905
+ const kind = classifyKind(raw);
1906
+ const pomLoc = findPomLine(paths.ticketDir, raw);
1907
+ const featureText = readFileSync12(paths.featurePath, "utf8");
1908
+ const gherkinStep = findGherkinStep(featureText, raw);
1909
+ const domSnapshotAtFailure = extractDomSnapshot(join11(runDir, "trace.zip"));
1910
+ return {
1911
+ ticket,
1912
+ runId,
1913
+ scenarioName,
1914
+ failedLocator: { raw, kind, ...pomLoc },
1915
+ gherkinStep,
1916
+ domSnapshotAtFailure
1917
+ };
1918
+ }
1919
+ async function healPrepareCmd(argv) {
1920
+ const [ticket, runId, ...scenarioParts] = argv;
1921
+ if (!ticket || !runId || scenarioParts.length === 0) {
1922
+ console.error("[xera:heal-prepare] usage: heal-prepare <TICKET> <RUN_ID> <SCENARIO_NAME>");
1923
+ return 1;
1924
+ }
1925
+ const scenarioName = scenarioParts.join(" ");
1926
+ try {
1927
+ const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
1928
+ const paths = resolveArtifactPaths(process.cwd(), ticket);
1929
+ const outPath = join11(paths.runsDir, runId, "heal-input.json");
1930
+ writeFileSync9(outPath, JSON.stringify(result, null, 2));
1931
+ console.log(`[xera:heal-prepare] wrote ${outPath}`);
1932
+ return 0;
1933
+ } catch (err2) {
1934
+ console.error(`[xera:heal-prepare] ${err2.message}`);
1935
+ return 1;
1936
+ }
1937
+ }
1938
+
1939
+ // src/bin-internal/lint.ts
1940
+ import { lintTicket } from "@xera-ai/web";
1941
+ async function lintCmd(argv) {
1942
+ const ticket = argv[0];
1943
+ if (!ticket) {
1944
+ console.error("[xera:lint] usage: lint <TICKET>");
1945
+ return 1;
1946
+ }
1947
+ const paths = resolveArtifactPaths(process.cwd(), ticket);
1948
+ const r = await lintTicket(paths.ticketDir);
1949
+ if (r.ok) {
1950
+ console.log("[xera:lint] ok");
1951
+ return 0;
1952
+ }
1953
+ for (const w of r.warnings)
1954
+ console.error(`[xera:lint] ${w.file}:${w.line} [${w.rule}] ${w.message}`);
1955
+ return 2;
691
1956
  }
692
1957
 
693
1958
  // src/bin-internal/normalize.ts
1959
+ import { existsSync as existsSync15, readdirSync as readdirSync4 } from "fs";
1960
+ import { join as join12 } from "path";
694
1961
  import { normalizeRun } from "@xera-ai/web";
695
- import { readdirSync, existsSync as existsSync10 } from "fs";
696
- import { join as join6 } from "path";
697
1962
  async function normalizeCmd(argv) {
698
1963
  const ticket = argv[0];
699
1964
  if (!ticket) {
@@ -702,13 +1967,13 @@ async function normalizeCmd(argv) {
702
1967
  }
703
1968
  const paths = resolveArtifactPaths(process.cwd(), ticket);
704
1969
  const runArg = argv.find((a) => a.startsWith("--run="));
705
- const runId = runArg ? runArg.split("=")[1] : readdirSync(paths.runsDir).filter((n) => !n.startsWith(".")).sort().pop();
1970
+ const runId = runArg ? runArg.split("=")[1] : readdirSync4(paths.runsDir).filter((n) => !n.startsWith(".")).sort().pop();
706
1971
  if (!runId) {
707
1972
  console.error("[xera:normalize] no run found");
708
1973
  return 1;
709
1974
  }
710
- const runDir = join6(paths.runsDir, runId);
711
- if (!existsSync10(runDir)) {
1975
+ const runDir = join12(paths.runsDir, runId);
1976
+ if (!existsSync15(runDir)) {
712
1977
  console.error(`[xera:normalize] runs/${runId} missing`);
713
1978
  return 1;
714
1979
  }
@@ -717,77 +1982,46 @@ async function normalizeCmd(argv) {
717
1982
  return 0;
718
1983
  }
719
1984
 
720
- // src/bin-internal/report.ts
721
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
722
- import { join as join7 } from "path";
723
-
724
- // src/classifier/aggregate.ts
725
- var CLASS_PRIORITY = [
726
- "REAL_BUG",
727
- "TEST_BUG",
728
- "SELECTOR_DRIFT",
729
- "FLAKY",
730
- "PASS"
731
- ];
732
- var CONF_RANK = { low: 1, medium: 2, high: 3 };
733
- function aggregateScenarios(scenarios) {
734
- if (scenarios.length === 0) {
735
- return { overall: "PASS", overallConfidence: "low", scenarios: [] };
736
- }
737
- if (scenarios.every((s) => s.outcome === "PASS")) {
738
- return { overall: "PASS", overallConfidence: "high", scenarios };
739
- }
740
- let chosen = "PASS";
741
- for (const cls of CLASS_PRIORITY) {
742
- if (scenarios.some((s) => s.class === cls)) {
743
- chosen = cls;
744
- break;
745
- }
746
- }
747
- const matching = scenarios.filter((s) => s.class === chosen);
748
- const minConf = matching.reduce((acc, s) => CONF_RANK[s.confidence] < CONF_RANK[acc] ? s.confidence : acc, "high");
749
- return { overall: chosen, overallConfidence: minConf, scenarios };
750
- }
751
-
752
- // src/reporter/status-writer.ts
753
- import { existsSync as existsSync12 } from "fs";
1985
+ // src/bin-internal/post.ts
1986
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
1987
+ import { join as join13 } from "path";
754
1988
 
755
1989
  // src/artifact/status.ts
756
- import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "fs";
1990
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
757
1991
  import { dirname as dirname5 } from "path";
758
- import { z as z4 } from "zod";
759
- var ClassificationEnum = z4.enum(["PASS", "REAL_BUG", "SELECTOR_DRIFT", "FLAKY", "TEST_BUG"]);
760
- var ResultEnum = z4.enum(["PASS", "FAIL"]);
761
- var ConfidenceEnum = z4.enum(["low", "medium", "high"]);
762
- var HistoryEntrySchema = z4.object({
763
- ts: z4.string(),
1992
+ import { z as z5 } from "zod";
1993
+ var ClassificationEnum = z5.enum(["PASS", "REAL_BUG", "SELECTOR_DRIFT", "FLAKY", "TEST_BUG"]);
1994
+ var ResultEnum = z5.enum(["PASS", "FAIL"]);
1995
+ var ConfidenceEnum = z5.enum(["low", "medium", "high"]);
1996
+ var HistoryEntrySchema = z5.object({
1997
+ ts: z5.string(),
764
1998
  result: ResultEnum,
765
1999
  class: ClassificationEnum
766
2000
  });
767
- var StatusJsonSchema = z4.object({
768
- ticket: z4.string(),
769
- lastRun: z4.string(),
2001
+ var StatusJsonSchema = z5.object({
2002
+ ticket: z5.string(),
2003
+ lastRun: z5.string(),
770
2004
  result: ResultEnum,
771
2005
  classification: ClassificationEnum,
772
2006
  confidence: ConfidenceEnum,
773
- scenarios: z4.object({
774
- total: z4.number().int().nonnegative(),
775
- passed: z4.number().int().nonnegative(),
776
- failed: z4.number().int().nonnegative(),
777
- skipped: z4.number().int().nonnegative()
2007
+ scenarios: z5.object({
2008
+ total: z5.number().int().nonnegative(),
2009
+ passed: z5.number().int().nonnegative(),
2010
+ failed: z5.number().int().nonnegative(),
2011
+ skipped: z5.number().int().nonnegative()
778
2012
  }),
779
- history: z4.array(HistoryEntrySchema).default([]),
780
- last_jira_comment_id: z4.string().optional()
2013
+ history: z5.array(HistoryEntrySchema).default([]),
2014
+ last_jira_comment_id: z5.string().optional()
781
2015
  });
782
2016
  var HISTORY_CAP = 20;
783
2017
  function readStatus(path) {
784
- if (!existsSync11(path))
2018
+ if (!existsSync16(path))
785
2019
  return null;
786
- return StatusJsonSchema.parse(JSON.parse(readFileSync8(path, "utf8")));
2020
+ return StatusJsonSchema.parse(JSON.parse(readFileSync13(path, "utf8")));
787
2021
  }
788
2022
  function writeStatus(path, status) {
789
- mkdirSync8(dirname5(path), { recursive: true });
790
- writeFileSync6(path, JSON.stringify(status, null, 2));
2023
+ mkdirSync9(dirname5(path), { recursive: true });
2024
+ writeFileSync10(path, JSON.stringify(status, null, 2));
791
2025
  }
792
2026
  function appendHistory(path, entry) {
793
2027
  const s = readStatus(path);
@@ -799,32 +2033,82 @@ function appendHistory(path, entry) {
799
2033
  return s;
800
2034
  }
801
2035
 
802
- // src/reporter/status-writer.ts
803
- function writeStatusFromClassification(path, input) {
804
- const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
805
- const entry = { ts: input.runTs, result, class: input.classification.overall };
806
- if (!existsSync12(path)) {
807
- writeStatus(path, {
808
- ticket: input.ticket,
809
- lastRun: input.runTs,
810
- result,
811
- classification: input.classification.overall,
812
- confidence: input.classification.overallConfidence,
813
- scenarios: input.scenarioCounts,
814
- history: [entry]
815
- });
816
- return;
2036
+ // src/bin-internal/post.ts
2037
+ async function postCmd(argv) {
2038
+ const ticket = argv[0];
2039
+ if (!ticket) {
2040
+ console.error("[xera:post] usage: post <TICKET>");
2041
+ return 1;
817
2042
  }
818
- const cur = readStatus(path);
819
- writeStatus(path, {
820
- ...cur,
821
- lastRun: input.runTs,
822
- result,
823
- classification: input.classification.overall,
824
- confidence: input.classification.overallConfidence,
825
- scenarios: input.scenarioCounts
2043
+ const cwd = process.cwd();
2044
+ const config = await loadConfig(cwd);
2045
+ if (!config.reporting.postToJira) {
2046
+ console.log("[xera:post] postToJira disabled in config; skipping");
2047
+ return 0;
2048
+ }
2049
+ const paths = resolveArtifactPaths(cwd, ticket);
2050
+ const draftPath = join13(paths.ticketDir, "jira-comment.draft.md");
2051
+ if (!existsSync17(draftPath)) {
2052
+ console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
2053
+ return 1;
2054
+ }
2055
+ const body = readFileSync14(draftPath, "utf8");
2056
+ const client = await createJiraClient({
2057
+ baseUrl: config.jira.baseUrl,
2058
+ preferMcp: true,
2059
+ ...process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN ? { rest: { email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN } } : {}
826
2060
  });
827
- appendHistory(path, entry);
2061
+ const r = await client.postComment(ticket, body);
2062
+ console.log(`[xera:post] posted comment id=${r.id}`);
2063
+ const s = readStatus(paths.statusPath);
2064
+ if (s)
2065
+ writeStatus(paths.statusPath, { ...s, last_jira_comment_id: r.id });
2066
+ return 0;
2067
+ }
2068
+
2069
+ // src/bin-internal/promote.ts
2070
+ import { promotePom } from "@xera-ai/web";
2071
+ async function promoteCmd(argv) {
2072
+ const [ticket, className] = argv;
2073
+ if (!ticket || !className) {
2074
+ console.error("[xera:promote] usage: promote <TICKET> <PomClassName>");
2075
+ return 1;
2076
+ }
2077
+ await promotePom({ repoRoot: process.cwd(), ticket, className });
2078
+ console.log(`[xera:promote] moved ${className} \u2192 shared/page-objects/`);
2079
+ return 0;
2080
+ }
2081
+
2082
+ // src/bin-internal/report.ts
2083
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "fs";
2084
+ import { join as join14 } from "path";
2085
+
2086
+ // src/classifier/aggregate.ts
2087
+ var CLASS_PRIORITY = [
2088
+ "REAL_BUG",
2089
+ "TEST_BUG",
2090
+ "SELECTOR_DRIFT",
2091
+ "FLAKY",
2092
+ "PASS"
2093
+ ];
2094
+ var CONF_RANK = { low: 1, medium: 2, high: 3 };
2095
+ function aggregateScenarios(scenarios) {
2096
+ if (scenarios.length === 0) {
2097
+ return { overall: "PASS", overallConfidence: "low", scenarios: [] };
2098
+ }
2099
+ if (scenarios.every((s) => s.outcome === "PASS")) {
2100
+ return { overall: "PASS", overallConfidence: "high", scenarios };
2101
+ }
2102
+ let chosen = "PASS";
2103
+ for (const cls of CLASS_PRIORITY) {
2104
+ if (scenarios.some((s) => s.class === cls)) {
2105
+ chosen = cls;
2106
+ break;
2107
+ }
2108
+ }
2109
+ const matching = scenarios.filter((s) => s.class === chosen);
2110
+ const minConf = matching.reduce((acc, s) => CONF_RANK[s.confidence] < CONF_RANK[acc] ? s.confidence : acc, "high");
2111
+ return { overall: chosen, overallConfidence: minConf, scenarios };
828
2112
  }
829
2113
 
830
2114
  // src/reporter/jira-comment.ts
@@ -856,6 +2140,35 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
856
2140
  `);
857
2141
  }
858
2142
 
2143
+ // src/reporter/status-writer.ts
2144
+ import { existsSync as existsSync18 } from "fs";
2145
+ function writeStatusFromClassification(path, input) {
2146
+ const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
2147
+ const entry = { ts: input.runTs, result, class: input.classification.overall };
2148
+ if (!existsSync18(path)) {
2149
+ writeStatus(path, {
2150
+ ticket: input.ticket,
2151
+ lastRun: input.runTs,
2152
+ result,
2153
+ classification: input.classification.overall,
2154
+ confidence: input.classification.overallConfidence,
2155
+ scenarios: input.scenarioCounts,
2156
+ history: [entry]
2157
+ });
2158
+ return;
2159
+ }
2160
+ const cur = readStatus(path);
2161
+ writeStatus(path, {
2162
+ ...cur,
2163
+ lastRun: input.runTs,
2164
+ result,
2165
+ classification: input.classification.overall,
2166
+ confidence: input.classification.overallConfidence,
2167
+ scenarios: input.scenarioCounts
2168
+ });
2169
+ appendHistory(path, entry);
2170
+ }
2171
+
859
2172
  // src/bin-internal/report.ts
860
2173
  async function reportCmd(argv) {
861
2174
  const ticket = argv[0];
@@ -865,7 +2178,7 @@ async function reportCmd(argv) {
865
2178
  return 1;
866
2179
  }
867
2180
  const paths = resolveArtifactPaths(process.cwd(), ticket);
868
- const input = JSON.parse(readFileSync9(inputArg.slice("--input=".length), "utf8"));
2181
+ const input = JSON.parse(readFileSync15(inputArg.slice("--input=".length), "utf8"));
869
2182
  const aggregated = aggregateScenarios(input.scenarios);
870
2183
  const ts = new Date().toISOString();
871
2184
  writeStatusFromClassification(paths.statusPath, {
@@ -883,47 +2196,12 @@ async function reportCmd(argv) {
883
2196
  xeraVersion: "0.1.0",
884
2197
  promptsVersion: "1.0.0"
885
2198
  });
886
- const draftPath = join7(paths.ticketDir, "jira-comment.draft.md");
887
- writeFileSync7(draftPath, md);
2199
+ const draftPath = join14(paths.ticketDir, "jira-comment.draft.md");
2200
+ writeFileSync11(draftPath, md);
888
2201
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
889
2202
  return 0;
890
2203
  }
891
2204
 
892
- // src/bin-internal/post.ts
893
- import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
894
- import { join as join8 } from "path";
895
- async function postCmd(argv) {
896
- const ticket = argv[0];
897
- if (!ticket) {
898
- console.error("[xera:post] usage: post <TICKET>");
899
- return 1;
900
- }
901
- const cwd = process.cwd();
902
- const config = await loadConfig(cwd);
903
- if (!config.reporting.postToJira) {
904
- console.log("[xera:post] postToJira disabled in config; skipping");
905
- return 0;
906
- }
907
- const paths = resolveArtifactPaths(cwd, ticket);
908
- const draftPath = join8(paths.ticketDir, "jira-comment.draft.md");
909
- if (!existsSync13(draftPath)) {
910
- console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
911
- return 1;
912
- }
913
- const body = readFileSync10(draftPath, "utf8");
914
- const client = await createJiraClient({
915
- baseUrl: config.jira.baseUrl,
916
- preferMcp: true,
917
- ...process.env.JIRA_EMAIL && process.env.JIRA_API_TOKEN ? { rest: { email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN } } : {}
918
- });
919
- const r = await client.postComment(ticket, body);
920
- console.log(`[xera:post] posted comment id=${r.id}`);
921
- const s = readStatus(paths.statusPath);
922
- if (s)
923
- writeStatus(paths.statusPath, { ...s, last_jira_comment_id: r.id });
924
- return 0;
925
- }
926
-
927
2205
  // src/bin-internal/status-cmd.ts
928
2206
  async function statusCmd(argv) {
929
2207
  const ticket = argv[0];
@@ -943,6 +2221,25 @@ async function statusCmd(argv) {
943
2221
  return 0;
944
2222
  }
945
2223
 
2224
+ // src/bin-internal/typecheck.ts
2225
+ import { typecheckTicket } from "@xera-ai/web";
2226
+ async function typecheckCmd(argv) {
2227
+ const ticket = argv[0];
2228
+ if (!ticket) {
2229
+ console.error("[xera:typecheck] usage: typecheck <TICKET>");
2230
+ return 1;
2231
+ }
2232
+ const paths = resolveArtifactPaths(process.cwd(), ticket);
2233
+ const r = await typecheckTicket(paths.ticketDir);
2234
+ if (r.ok) {
2235
+ console.log("[xera:typecheck] ok");
2236
+ return 0;
2237
+ }
2238
+ for (const e of r.errors)
2239
+ console.error(`[xera:typecheck] ${e}`);
2240
+ return 2;
2241
+ }
2242
+
946
2243
  // src/bin-internal/unlock.ts
947
2244
  async function unlockCmd(argv) {
948
2245
  const ticket = argv[0];
@@ -966,32 +2263,49 @@ async function unlockCmd(argv) {
966
2263
  return 0;
967
2264
  }
968
2265
 
969
- // src/bin-internal/promote.ts
970
- import { promotePom } from "@xera-ai/web";
971
- async function promoteCmd(argv) {
972
- const [ticket, className] = argv;
973
- if (!ticket || !className) {
974
- console.error("[xera:promote] usage: promote <TICKET> <PomClassName>");
2266
+ // src/bin-internal/validate-feature.ts
2267
+ import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
2268
+ import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
2269
+ async function validateFeatureCmd(argv) {
2270
+ const ticket = argv[0];
2271
+ if (!ticket) {
2272
+ console.error("[xera:validate-feature] usage: validate-feature <TICKET>");
975
2273
  return 1;
976
2274
  }
977
- await promotePom({ repoRoot: process.cwd(), ticket, className });
978
- console.log(`[xera:promote] moved ${className} \u2192 shared/page-objects/`);
979
- return 0;
2275
+ const paths = resolveArtifactPaths(process.cwd(), ticket);
2276
+ if (!existsSync19(paths.featurePath)) {
2277
+ console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
2278
+ return 1;
2279
+ }
2280
+ const r = validateGherkin2(readFileSync16(paths.featurePath, "utf8"));
2281
+ if (r.ok) {
2282
+ console.log("[xera:validate-feature] ok");
2283
+ return 0;
2284
+ }
2285
+ for (const e of r.errors)
2286
+ console.error(`[xera:validate-feature] line ${e.line}: ${e.message}`);
2287
+ return 2;
980
2288
  }
981
2289
 
982
2290
  // src/bin-internal/index.ts
983
2291
  var COMMANDS = {
2292
+ doctor: doctorCmd,
2293
+ "eval-deterministic": evalDeterministicCmd,
2294
+ "eval-prepare": evalPrepareCmd,
2295
+ "eval-report": evalReportCmd,
2296
+ exec: execCmd,
984
2297
  fetch: fetchCmd,
985
- "validate-feature": validateFeatureCmd,
986
- typecheck: typecheckCmd,
2298
+ "heal-prepare": healPrepareCmd,
987
2299
  lint: lintCmd,
988
- exec: execCmd,
989
2300
  normalize: normalizeCmd,
990
- report: reportCmd,
991
2301
  post: postCmd,
2302
+ promote: promoteCmd,
2303
+ report: reportCmd,
992
2304
  status: statusCmd,
2305
+ typecheck: typecheckCmd,
993
2306
  unlock: unlockCmd,
994
- promote: promoteCmd
2307
+ "validate-feature": validateFeatureCmd,
2308
+ "verify-prompts": verifyPromptsCmd
995
2309
  };
996
2310
  async function run(argv) {
997
2311
  const [cmd, ...rest] = argv;
@@ -1002,8 +2316,8 @@ Commands: ${Object.keys(COMMANDS).join(", ")}`);
1002
2316
  }
1003
2317
  try {
1004
2318
  return await COMMANDS[cmd](rest);
1005
- } catch (err) {
1006
- console.error(`[xera:${cmd}] failed: ${err.message}`);
2319
+ } catch (err2) {
2320
+ console.error(`[xera:${cmd}] failed: ${err2.message}`);
1007
2321
  return 4;
1008
2322
  }
1009
2323
  }