ubume 0.1.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 (182) hide show
  1. package/README.md +88 -0
  2. package/bin/codexa-local-harness-bridge.js +3 -0
  3. package/bin/codexa.js +28 -0
  4. package/bin/ubume-local-harness-bridge.js +325 -0
  5. package/bin/ubume.js +398 -0
  6. package/package.json +66 -0
  7. package/src/app.tsx +5759 -0
  8. package/src/commands/handler.ts +889 -0
  9. package/src/config/appVersion.ts +69 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.ts +196 -0
  12. package/src/config/layeredConfig.ts +853 -0
  13. package/src/config/legacyEnv.ts +16 -0
  14. package/src/config/persistence.ts +377 -0
  15. package/src/config/runtimeConfig.ts +558 -0
  16. package/src/config/settings.ts +405 -0
  17. package/src/config/toml-serialize.ts +109 -0
  18. package/src/config/trustStore.ts +84 -0
  19. package/src/config/updateCheckCache.ts +85 -0
  20. package/src/core/README.md +55 -0
  21. package/src/core/agent/loop.ts +464 -0
  22. package/src/core/agent/protocol.ts +345 -0
  23. package/src/core/agent/tools.ts +423 -0
  24. package/src/core/auth/codexAuth.ts +359 -0
  25. package/src/core/codex/codexExecArgs.ts +166 -0
  26. package/src/core/codex/codexLaunch.ts +163 -0
  27. package/src/core/codex/codexPrompt.ts +429 -0
  28. package/src/core/debug/inputDebug.ts +51 -0
  29. package/src/core/debug/localStreamDebug.ts +50 -0
  30. package/src/core/debug/modelStateDebug.ts +35 -0
  31. package/src/core/executables/antigravityExecutable.ts +48 -0
  32. package/src/core/executables/claudeExecutable.ts +63 -0
  33. package/src/core/executables/codexExecutable.ts +160 -0
  34. package/src/core/executables/executableResolver.ts +164 -0
  35. package/src/core/executables/geminiExecutable.ts +78 -0
  36. package/src/core/models/codexCapabilities.ts +97 -0
  37. package/src/core/models/codexModelCapabilities.ts +624 -0
  38. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  39. package/src/core/models/modelSpecs.ts +303 -0
  40. package/src/core/models/providerModelCache.ts +94 -0
  41. package/src/core/perf/profiler.ts +125 -0
  42. package/src/core/perf/renderDebug.ts +398 -0
  43. package/src/core/process/CommandRunner.ts +280 -0
  44. package/src/core/process/processValidation.ts +111 -0
  45. package/src/core/providerLauncher/launcher.ts +220 -0
  46. package/src/core/providerLauncher/registry.ts +354 -0
  47. package/src/core/providerLauncher/types.ts +95 -0
  48. package/src/core/providerLauncher/workspaceConfig.ts +487 -0
  49. package/src/core/providerRuntime/anthropic.ts +580 -0
  50. package/src/core/providerRuntime/antigravity.ts +500 -0
  51. package/src/core/providerRuntime/capabilityProfile.ts +383 -0
  52. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
  53. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  54. package/src/core/providerRuntime/codexaCupy.ts +97 -0
  55. package/src/core/providerRuntime/codexaNative.ts +425 -0
  56. package/src/core/providerRuntime/contextMetadata.ts +397 -0
  57. package/src/core/providerRuntime/gemini.ts +789 -0
  58. package/src/core/providerRuntime/lmstudio.ts +118 -0
  59. package/src/core/providerRuntime/local.ts +770 -0
  60. package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
  61. package/src/core/providerRuntime/localOutputBudget.ts +17 -0
  62. package/src/core/providerRuntime/mistralVibe.ts +667 -0
  63. package/src/core/providerRuntime/models.ts +175 -0
  64. package/src/core/providerRuntime/reasoning.ts +20 -0
  65. package/src/core/providerRuntime/registry.ts +286 -0
  66. package/src/core/providerRuntime/types.ts +142 -0
  67. package/src/core/providerRuntime/unsloth.ts +216 -0
  68. package/src/core/providers/codexJsonStream.ts +305 -0
  69. package/src/core/providers/codexSubprocess.ts +378 -0
  70. package/src/core/providers/codexTranscript.ts +695 -0
  71. package/src/core/providers/openaiNative.ts +13 -0
  72. package/src/core/providers/registry.ts +21 -0
  73. package/src/core/providers/types.ts +91 -0
  74. package/src/core/shared/attachments.ts +101 -0
  75. package/src/core/shared/cleanupFastFail.ts +67 -0
  76. package/src/core/shared/clipboard.ts +24 -0
  77. package/src/core/shared/clipboardImage.ts +111 -0
  78. package/src/core/shared/githubDiagnostics.ts +222 -0
  79. package/src/core/shared/hollowResponseFormat.ts +39 -0
  80. package/src/core/terminal/clearFrameBoundary.ts +852 -0
  81. package/src/core/terminal/frameLock.ts +110 -0
  82. package/src/core/terminal/inkRenderReset.ts +123 -0
  83. package/src/core/terminal/startupClear.ts +20 -0
  84. package/src/core/terminal/terminalCapabilities.ts +100 -0
  85. package/src/core/terminal/terminalControl.ts +169 -0
  86. package/src/core/terminal/terminalSanitize.ts +147 -0
  87. package/src/core/terminal/terminalTitle.ts +400 -0
  88. package/src/core/version/channel.ts +27 -0
  89. package/src/core/version/packageManager.ts +119 -0
  90. package/src/core/version/updateCheck.ts +203 -0
  91. package/src/core/workspace/appData.ts +107 -0
  92. package/src/core/workspace/conversationStore.ts +335 -0
  93. package/src/core/workspace/launchContext.ts +259 -0
  94. package/src/core/workspace/planStorage.ts +135 -0
  95. package/src/core/workspace/projectInstructions.ts +54 -0
  96. package/src/core/workspace/scratchDir.ts +64 -0
  97. package/src/core/workspace/workspaceActivity.ts +384 -0
  98. package/src/core/workspace/workspaceGuard.ts +377 -0
  99. package/src/core/workspace/workspaceRoot.ts +47 -0
  100. package/src/exec.ts +73 -0
  101. package/src/headless/execArgs.ts +296 -0
  102. package/src/headless/execRunner.ts +304 -0
  103. package/src/index.tsx +270 -0
  104. package/src/legacyEnvBootstrap.ts +5 -0
  105. package/src/session/appSession.ts +771 -0
  106. package/src/session/chatLifecycle.ts +994 -0
  107. package/src/session/conversation.ts +107 -0
  108. package/src/session/liveRenderScheduler.ts +214 -0
  109. package/src/session/persistedResponse.ts +93 -0
  110. package/src/session/planFlow.ts +159 -0
  111. package/src/session/planTranscript.ts +19 -0
  112. package/src/session/promptRunSchedule.ts +26 -0
  113. package/src/session/types.ts +234 -0
  114. package/src/test/runtimeTestUtils.ts +14 -0
  115. package/src/types/react-dom.d.ts +3 -0
  116. package/src/ui/chrome/ActivityBars.tsx +68 -0
  117. package/src/ui/chrome/ActivityIndicator.tsx +58 -0
  118. package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
  119. package/src/ui/chrome/AppShell.tsx +474 -0
  120. package/src/ui/chrome/BottomComposer.tsx +1135 -0
  121. package/src/ui/chrome/DashCard.tsx +82 -0
  122. package/src/ui/chrome/RunFooter.tsx +65 -0
  123. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  124. package/src/ui/chrome/Spinner.tsx +25 -0
  125. package/src/ui/chrome/TopHeader.tsx +439 -0
  126. package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
  127. package/src/ui/chrome/busyStatusAnimation.ts +11 -0
  128. package/src/ui/input/commandNormalize.ts +66 -0
  129. package/src/ui/input/focus.ts +73 -0
  130. package/src/ui/input/imageAttachments.ts +18 -0
  131. package/src/ui/input/inputBuffer.ts +203 -0
  132. package/src/ui/input/pastedContent.ts +75 -0
  133. package/src/ui/input/rawArrowKeys.ts +28 -0
  134. package/src/ui/input/slashCommands.ts +43 -0
  135. package/src/ui/input/useStdinRawModeLease.ts +23 -0
  136. package/src/ui/layout.ts +560 -0
  137. package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
  138. package/src/ui/panels/AuthPanel.tsx +149 -0
  139. package/src/ui/panels/BackendPicker.tsx +28 -0
  140. package/src/ui/panels/ModePicker.tsx +31 -0
  141. package/src/ui/panels/ModelPicker.tsx +31 -0
  142. package/src/ui/panels/ModelPickerScreen.tsx +761 -0
  143. package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
  144. package/src/ui/panels/Panel.tsx +51 -0
  145. package/src/ui/panels/PermissionsPanel.tsx +78 -0
  146. package/src/ui/panels/PlanActionPicker.tsx +187 -0
  147. package/src/ui/panels/ProviderPicker.tsx +753 -0
  148. package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
  149. package/src/ui/panels/ReasoningPicker.tsx +46 -0
  150. package/src/ui/panels/ResumePicker.tsx +90 -0
  151. package/src/ui/panels/SelectionPanel.tsx +138 -0
  152. package/src/ui/panels/SettingsPanel.tsx +156 -0
  153. package/src/ui/panels/TextEntryPanel.tsx +139 -0
  154. package/src/ui/panels/ThemePicker.tsx +32 -0
  155. package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
  156. package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
  157. package/src/ui/panels/responsivePickerViewport.ts +64 -0
  158. package/src/ui/render/Markdown.tsx +331 -0
  159. package/src/ui/render/diffRenderer.ts +116 -0
  160. package/src/ui/render/logoVariants.ts +113 -0
  161. package/src/ui/render/modeDisplay.ts +52 -0
  162. package/src/ui/render/outputPipeline.ts +64 -0
  163. package/src/ui/render/runtimeDisplay.ts +128 -0
  164. package/src/ui/render/terminalAnswerFormat.ts +128 -0
  165. package/src/ui/render/textLayout.ts +392 -0
  166. package/src/ui/theme.tsx +274 -0
  167. package/src/ui/themeFlow.ts +41 -0
  168. package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
  169. package/src/ui/timeline/AgentBlock.tsx +130 -0
  170. package/src/ui/timeline/StaticIntroItem.tsx +54 -0
  171. package/src/ui/timeline/ThinkingBlock.tsx +100 -0
  172. package/src/ui/timeline/Timeline.tsx +1410 -0
  173. package/src/ui/timeline/TranscriptShell.tsx +302 -0
  174. package/src/ui/timeline/TurnGroup.tsx +673 -0
  175. package/src/ui/timeline/layoutListWindow.ts +145 -0
  176. package/src/ui/timeline/liveViewportWindow.ts +68 -0
  177. package/src/ui/timeline/progressEntries.ts +156 -0
  178. package/src/ui/timeline/runActivityView.ts +37 -0
  179. package/src/ui/timeline/staticTranscriptCache.ts +174 -0
  180. package/src/ui/timeline/streamCoalesce.ts +53 -0
  181. package/src/ui/timeline/timelineMeasure.ts +3273 -0
  182. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,296 @@
1
+ import type { LaunchArgs } from "../config/launchArgs.js";
2
+
3
+ // ─── Types ───────────────────────────────────────────────────────────────────
4
+
5
+ export interface HeadlessExecArgs {
6
+ help: boolean;
7
+ benchmarkDiagnostics: boolean;
8
+ timing: boolean;
9
+ promptPolicy: "raw" | "wrapped";
10
+ prompt: string;
11
+ launchArgs: LaunchArgs;
12
+ }
13
+
14
+ export type HeadlessExecArgsParseResult =
15
+ | { ok: true; value: HeadlessExecArgs }
16
+ | { ok: false; error: string };
17
+
18
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
19
+
20
+ function normalizeNonEmpty(value: string | undefined): string | null {
21
+ const trimmed = value?.trim();
22
+ return trimmed ? trimmed : null;
23
+ }
24
+
25
+ function parseConfigFlagValue(raw: string | undefined): string | null {
26
+ const trimmed = raw?.trim();
27
+ if (!trimmed) {
28
+ return null;
29
+ }
30
+
31
+ const separatorIndex = trimmed.indexOf("=");
32
+ if (separatorIndex <= 0 || separatorIndex === trimmed.length - 1) {
33
+ return null;
34
+ }
35
+
36
+ return trimmed;
37
+ }
38
+
39
+ function parseModelFlagValue(raw: string | undefined): string | null {
40
+ const trimmed = raw?.trim();
41
+ return trimmed ? trimmed : null;
42
+ }
43
+
44
+ function quoteTomlString(value: string): string {
45
+ return JSON.stringify(value);
46
+ }
47
+
48
+ function buildLaunchArgs(params: {
49
+ prompt: string | null;
50
+ profile: string | null;
51
+ configOverrides: string[];
52
+ passthroughArgs: string[];
53
+ modelOverride: string | null;
54
+ }): LaunchArgs {
55
+ return {
56
+ help: false,
57
+ version: false,
58
+ initialPrompt: params.prompt,
59
+ profile: params.profile,
60
+ configOverrides: params.configOverrides,
61
+ passthroughArgs: params.passthroughArgs,
62
+ modelOverride: params.modelOverride,
63
+ noClear: false,
64
+ };
65
+ }
66
+
67
+ // ─── Parser ──────────────────────────────────────────────────────────────────
68
+
69
+ export function parseHeadlessExecArgs(argv: readonly string[]): HeadlessExecArgsParseResult {
70
+ const configOverrides: string[] = [];
71
+ const passthroughArgs: string[] = [];
72
+ const positionalPromptParts: string[] = [];
73
+ let explicitPrompt: string | null = null;
74
+ let profile: string | null = null;
75
+ let modelOverride: string | null = null;
76
+ let help = false;
77
+ let benchmarkDiagnostics = false;
78
+ let timing = false;
79
+ let promptPolicy: "raw" | "wrapped" = "raw";
80
+
81
+ for (let index = 0; index < argv.length; index += 1) {
82
+ const arg = argv[index];
83
+ if (!arg) {
84
+ continue;
85
+ }
86
+
87
+ if (arg === "--help" || arg === "-h") {
88
+ help = true;
89
+ continue;
90
+ }
91
+
92
+ if (arg === "--benchmark-diagnostics") {
93
+ benchmarkDiagnostics = true;
94
+ timing = true;
95
+ continue;
96
+ }
97
+
98
+ if (arg === "--timing") {
99
+ timing = true;
100
+ continue;
101
+ }
102
+
103
+ if (arg === "--skip-git-repo-check") {
104
+ passthroughArgs.push(arg);
105
+ continue;
106
+ }
107
+
108
+ // --codexa-prompt-policy is the pre-rename spelling; keep accepting it.
109
+ if (arg === "--ubume-prompt-policy" || arg === "--codexa-prompt-policy") {
110
+ const value = normalizeNonEmpty(argv[index + 1]);
111
+ if (value !== "raw" && value !== "wrapped") {
112
+ return { ok: false, error: "Missing or invalid value for --ubume-prompt-policy. Use raw or wrapped." };
113
+ }
114
+ promptPolicy = value;
115
+ index += 1;
116
+ continue;
117
+ }
118
+
119
+ if (arg.startsWith("--ubume-prompt-policy=") || arg.startsWith("--codexa-prompt-policy=")) {
120
+ const value = normalizeNonEmpty(arg.slice(arg.indexOf("=") + 1));
121
+ if (value !== "raw" && value !== "wrapped") {
122
+ return { ok: false, error: "Invalid value for --ubume-prompt-policy. Use raw or wrapped." };
123
+ }
124
+ promptPolicy = value;
125
+ continue;
126
+ }
127
+
128
+ if (arg === "--") {
129
+ positionalPromptParts.push(...argv.slice(index + 1));
130
+ break;
131
+ }
132
+
133
+ if (arg === "--prompt") {
134
+ const value = normalizeNonEmpty(argv[index + 1]);
135
+ if (!value) {
136
+ return { ok: false, error: "Missing value for --prompt." };
137
+ }
138
+ explicitPrompt = value;
139
+ index += 1;
140
+ continue;
141
+ }
142
+
143
+ if (arg.startsWith("--prompt=")) {
144
+ const value = normalizeNonEmpty(arg.slice("--prompt=".length));
145
+ if (!value) {
146
+ return { ok: false, error: "Missing value for --prompt." };
147
+ }
148
+ explicitPrompt = value;
149
+ continue;
150
+ }
151
+
152
+ if (arg === "--profile") {
153
+ const value = normalizeNonEmpty(argv[index + 1]);
154
+ if (!value) {
155
+ return { ok: false, error: "Missing value for --profile." };
156
+ }
157
+ profile = value;
158
+ passthroughArgs.push(arg, value);
159
+ index += 1;
160
+ continue;
161
+ }
162
+
163
+ if (arg.startsWith("--profile=")) {
164
+ const value = normalizeNonEmpty(arg.slice("--profile=".length));
165
+ if (!value) {
166
+ return { ok: false, error: "Missing value for --profile." };
167
+ }
168
+ profile = value;
169
+ passthroughArgs.push(`--profile=${value}`);
170
+ continue;
171
+ }
172
+
173
+ if (arg === "-c" || arg === "--config") {
174
+ const value = parseConfigFlagValue(argv[index + 1]);
175
+ if (!value) {
176
+ return { ok: false, error: `Missing key=value payload for ${arg}.` };
177
+ }
178
+ configOverrides.push(value);
179
+ passthroughArgs.push(arg, value);
180
+ index += 1;
181
+ continue;
182
+ }
183
+
184
+ if (arg.startsWith("--config=")) {
185
+ const value = parseConfigFlagValue(arg.slice("--config=".length));
186
+ if (!value) {
187
+ return { ok: false, error: "Missing key=value payload for --config." };
188
+ }
189
+ configOverrides.push(value);
190
+ passthroughArgs.push(`--config=${value}`);
191
+ continue;
192
+ }
193
+
194
+ if (arg.startsWith("-c=")) {
195
+ const value = parseConfigFlagValue(arg.slice(3));
196
+ if (!value) {
197
+ return { ok: false, error: "Missing key=value payload for -c." };
198
+ }
199
+ configOverrides.push(value);
200
+ passthroughArgs.push(`-c=${value}`);
201
+ continue;
202
+ }
203
+
204
+ if (arg === "--model" || arg === "-m") {
205
+ const value = parseModelFlagValue(argv[index + 1]);
206
+ if (!value) {
207
+ return { ok: false, error: `Missing value for ${arg}.` };
208
+ }
209
+ configOverrides.push(`model=${quoteTomlString(value)}`);
210
+ passthroughArgs.push(arg, value);
211
+ modelOverride = value;
212
+ index += 1;
213
+ continue;
214
+ }
215
+
216
+ if (arg.startsWith("--model=")) {
217
+ const value = parseModelFlagValue(arg.slice("--model=".length));
218
+ if (!value) {
219
+ return { ok: false, error: "Missing value for --model." };
220
+ }
221
+ configOverrides.push(`model=${quoteTomlString(value)}`);
222
+ passthroughArgs.push(`--model=${value}`);
223
+ modelOverride = value;
224
+ continue;
225
+ }
226
+
227
+ if (arg === "--reasoning") {
228
+ const value = parseModelFlagValue(argv[index + 1]);
229
+ if (!value) {
230
+ return { ok: false, error: `Missing value for ${arg}.` };
231
+ }
232
+ configOverrides.push(`model_reasoning_effort=${value}`);
233
+ passthroughArgs.push(arg, value);
234
+ index += 1;
235
+ continue;
236
+ }
237
+
238
+ if (arg.startsWith("--reasoning=")) {
239
+ const value = parseModelFlagValue(arg.slice("--reasoning=".length));
240
+ if (!value) {
241
+ return { ok: false, error: "Missing value for --reasoning." };
242
+ }
243
+ configOverrides.push(`model_reasoning_effort=${value}`);
244
+ passthroughArgs.push(`--reasoning=${value}`);
245
+ continue;
246
+ }
247
+
248
+ if (arg.startsWith("-")) {
249
+ return { ok: false, error: `Unknown option for ubume exec: ${arg}` };
250
+ }
251
+
252
+ positionalPromptParts.push(arg, ...argv.slice(index + 1));
253
+ break;
254
+ }
255
+
256
+ const positionalPrompt = positionalPromptParts
257
+ .map((part) => part.trim())
258
+ .filter(Boolean)
259
+ .join(" ")
260
+ .trim() || null;
261
+
262
+ if (explicitPrompt && positionalPrompt) {
263
+ return { ok: false, error: "Provide a prompt either positionally or with --prompt, not both." };
264
+ }
265
+
266
+ const prompt = explicitPrompt ?? positionalPrompt;
267
+ if (help) {
268
+ return {
269
+ ok: true,
270
+ value: {
271
+ help,
272
+ benchmarkDiagnostics,
273
+ timing,
274
+ promptPolicy,
275
+ prompt: prompt ?? "",
276
+ launchArgs: buildLaunchArgs({ prompt, profile, configOverrides, passthroughArgs, modelOverride }),
277
+ },
278
+ };
279
+ }
280
+
281
+ if (!prompt) {
282
+ return { ok: false, error: "Missing prompt. Use ubume exec \"prompt\" or ubume exec --prompt \"prompt\"." };
283
+ }
284
+
285
+ return {
286
+ ok: true,
287
+ value: {
288
+ help,
289
+ benchmarkDiagnostics,
290
+ timing,
291
+ promptPolicy,
292
+ prompt,
293
+ launchArgs: buildLaunchArgs({ prompt, profile, configOverrides, passthroughArgs, modelOverride }),
294
+ },
295
+ };
296
+ }
@@ -0,0 +1,304 @@
1
+ import {
2
+ resolveLayeredConfig,
3
+ type LayeredConfigResult,
4
+ } from "../config/layeredConfig.js";
5
+ import type { LaunchArgs } from "../config/launchArgs.js";
6
+ import {
7
+ mergeRuntimeConfig,
8
+ resolveRuntimeConfig,
9
+ type ResolvedRuntimeConfig,
10
+ } from "../config/runtimeConfig.js";
11
+ import { loadProjectInstructions, type ProjectInstructionsLoadResult } from "../core/workspace/projectInstructions.js";
12
+ import { getBackendProvider } from "../core/providers/registry.js";
13
+ import type { BackendProvider } from "../core/providers/types.js";
14
+ import { isNoiseLine } from "../core/providers/codexTranscript.js";
15
+ import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
16
+ import { resolveWorkspaceRoot } from "../core/workspace/workspaceRoot.js";
17
+ import type { RunToolActivity } from "../session/types.js";
18
+
19
+ // ─── Types & constants ────────────────────────────────────────────────────────
20
+
21
+ export const HEADLESS_EXEC_PARSE_ERROR = 2;
22
+ export const HEADLESS_EXEC_PROVIDER_UNAVAILABLE = 3;
23
+ export const HEADLESS_EXEC_RUN_FAILED = 1;
24
+
25
+ export interface HeadlessExecIo {
26
+ stdout: Pick<NodeJS.WriteStream, "write">;
27
+ stderr: Pick<NodeJS.WriteStream, "write">;
28
+ }
29
+
30
+ export interface HeadlessExecOptions {
31
+ prompt: string;
32
+ launchArgs: LaunchArgs;
33
+ workspaceRoot?: string;
34
+ benchmarkDiagnostics?: HeadlessExecTiming;
35
+ promptPolicy?: "raw" | "wrapped";
36
+ }
37
+
38
+ export interface HeadlessExecResult {
39
+ exitCode: number;
40
+ }
41
+
42
+ export interface HeadlessExecDependencies {
43
+ resolveWorkspaceRoot: () => string;
44
+ resolveLayeredConfig: (options: { workspaceRoot: string; launchArgs: LaunchArgs }) => LayeredConfigResult;
45
+ resolveRuntimeConfig: typeof resolveRuntimeConfig;
46
+ getBackendProvider: (id: string) => BackendProvider;
47
+ loadProjectInstructions: (workspaceRoot: string) => ProjectInstructionsLoadResult;
48
+ }
49
+
50
+ export type HeadlessExecTimingValue = string | number | boolean | null | readonly string[];
51
+
52
+ export interface HeadlessExecTiming {
53
+ enabled: boolean;
54
+ mark: (phase: string, fields?: Record<string, HeadlessExecTimingValue>) => void;
55
+ }
56
+
57
+ export function createHeadlessExecTiming(options: {
58
+ enabled: boolean;
59
+ stderr?: Pick<NodeJS.WriteStream, "write">;
60
+ startTimeMs?: number;
61
+ }): HeadlessExecTiming {
62
+ const startTimeMs = options.startTimeMs ?? Date.now();
63
+ const stderr = options.stderr ?? process.stderr;
64
+ let previousElapsedMs = 0;
65
+
66
+ return {
67
+ enabled: options.enabled,
68
+ mark: (phase, fields = {}) => {
69
+ if (!options.enabled) return;
70
+ const elapsedMs = Date.now() - startTimeMs;
71
+ const deltaMs = elapsedMs - previousElapsedMs;
72
+ previousElapsedMs = elapsedMs;
73
+ const formattedFields = Object.entries(fields)
74
+ .map(([key, value]) => {
75
+ const serialized = Array.isArray(value) || typeof value === "string"
76
+ ? JSON.stringify(value)
77
+ : String(value);
78
+ return `${key}=${serialized}`;
79
+ })
80
+ .join(" ");
81
+ writeLine(stderr, `[ubume exec timing] phase=${phase} elapsed_ms=${elapsedMs} delta_ms=${deltaMs}${formattedFields ? ` ${formattedFields}` : ""}`);
82
+ },
83
+ };
84
+ }
85
+
86
+ export const createHeadlessBenchmarkDiagnostics = createHeadlessExecTiming;
87
+
88
+ const DEFAULT_DEPENDENCIES: HeadlessExecDependencies = {
89
+ resolveWorkspaceRoot,
90
+ resolveLayeredConfig,
91
+ resolveRuntimeConfig,
92
+ getBackendProvider,
93
+ loadProjectInstructions,
94
+ };
95
+
96
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
97
+
98
+ function writeLine(stream: Pick<NodeJS.WriteStream, "write">, line: string): void {
99
+ stream.write(`${line}\n`);
100
+ }
101
+
102
+ function formatDiagnosticText(value: string): string {
103
+ return sanitizeTerminalOutput(value)
104
+ .replace(/\r\n/g, "\n")
105
+ .replace(/\r/g, "\n")
106
+ .trim();
107
+ }
108
+
109
+ function writeDiagnostic(stderr: Pick<NodeJS.WriteStream, "write">, kind: string, message: string): void {
110
+ const safeMessage = formatDiagnosticText(message);
111
+ if (!safeMessage) return;
112
+ writeLine(stderr, `[ubume exec] ${kind}: ${safeMessage.replace(/\n/g, "\n ")}`);
113
+ }
114
+
115
+ function formatToolActivity(activity: RunToolActivity): string {
116
+ const summary = activity.summary?.trim();
117
+ const safeSummary = summary && isProcessTerminationNoise(summary) ? "" : summary;
118
+ return summary
119
+ ? `${activity.status}: ${activity.command}${safeSummary ? `\n${safeSummary}` : ""}`
120
+ : `${activity.status}: ${activity.command}`;
121
+ }
122
+
123
+ function isStructuredCodexEventLine(line: string): boolean {
124
+ const trimmed = line.trim();
125
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
126
+ return false;
127
+ }
128
+
129
+ try {
130
+ const parsed = JSON.parse(trimmed) as { type?: unknown };
131
+ return typeof parsed.type === "string"
132
+ && /^(?:thread|turn|item)\./.test(parsed.type);
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ function isProcessTerminationNoise(line: string): boolean {
139
+ return /^SUCCESS: The process with PID \d+ .* has been terminated\.$/.test(line.trim());
140
+ }
141
+
142
+ function shouldSuppressAssistantChunk(chunk: string): boolean {
143
+ const lines = chunk
144
+ .replace(/\r\n/g, "\n")
145
+ .replace(/\r/g, "\n")
146
+ .split("\n")
147
+ .map((line) => line.trim())
148
+ .filter(Boolean);
149
+
150
+ return lines.length > 0
151
+ && lines.every((line) => isStructuredCodexEventLine(line) || isProcessTerminationNoise(line));
152
+ }
153
+
154
+ function formatRuntimeStartup(runtime: ResolvedRuntimeConfig, workspaceRoot: string, provider: BackendProvider): string {
155
+ return [
156
+ `workspace ${workspaceRoot}`,
157
+ `provider ${provider.label} (${provider.id})`,
158
+ `model ${runtime.model}`,
159
+ `mode ${runtime.mode}`,
160
+ `planMode ${runtime.planMode ? "enabled" : "disabled"}`,
161
+ `sandbox ${runtime.policy.sandboxMode}`,
162
+ `approval ${runtime.policy.approvalPolicy}`,
163
+ `network ${runtime.policy.networkAccess ? "enabled" : "disabled"}`,
164
+ ].join("; ");
165
+ }
166
+
167
+ // ─── Runner ───────────────────────────────────────────────────────────────────
168
+
169
+ export async function runHeadlessExec(
170
+ options: HeadlessExecOptions,
171
+ io: HeadlessExecIo = { stdout: process.stdout, stderr: process.stderr },
172
+ dependencies: Partial<HeadlessExecDependencies> = {},
173
+ ): Promise<HeadlessExecResult> {
174
+ const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
175
+ const diagnostics = options.benchmarkDiagnostics;
176
+ const promptPolicy = options.promptPolicy ?? "raw";
177
+ diagnostics?.mark("run_headless_start");
178
+ const workspaceRoot = options.workspaceRoot ?? deps.resolveWorkspaceRoot();
179
+ diagnostics?.mark("workspace_resolved", { workspace_root: workspaceRoot });
180
+ const layeredConfig = deps.resolveLayeredConfig({
181
+ workspaceRoot,
182
+ launchArgs: options.launchArgs,
183
+ });
184
+ diagnostics?.mark("layered_config_loaded");
185
+ const runtimeConfig = mergeRuntimeConfig(layeredConfig.runtime, { planMode: false });
186
+ const runtime = deps.resolveRuntimeConfig(runtimeConfig);
187
+ diagnostics?.mark("runtime_config_resolved", {
188
+ effective_model: runtime.model,
189
+ effective_reasoning_effort: runtime.reasoningLevel,
190
+ prompt_policy: promptPolicy,
191
+ });
192
+
193
+ const projectInstructionsLoad = promptPolicy === "wrapped"
194
+ ? deps.loadProjectInstructions(workspaceRoot)
195
+ : ({ status: "missing" } as ProjectInstructionsLoadResult);
196
+ const projectInstructions = projectInstructionsLoad.status === "loaded"
197
+ ? projectInstructionsLoad.instructions
198
+ : null;
199
+ diagnostics?.mark("project_instructions_resolved", {
200
+ whether_project_instructions_loaded: projectInstructionsLoad.status === "loaded",
201
+ project_instructions_path: projectInstructions?.path ?? ("path" in projectInstructionsLoad ? projectInstructionsLoad.path : null),
202
+ project_instructions_character_count: projectInstructions?.content.length ?? 0,
203
+ });
204
+
205
+ const provider = deps.getBackendProvider(runtime.provider);
206
+ diagnostics?.mark("provider_created", {
207
+ provider_id: provider.id,
208
+ provider_label: provider.label,
209
+ });
210
+
211
+ writeDiagnostic(io.stderr, "startup", formatRuntimeStartup(runtime, workspaceRoot, provider));
212
+
213
+ if (layeredConfig.diagnostics.ignoredEntries.length > 0) {
214
+ writeDiagnostic(io.stderr, "config", `ignored ${layeredConfig.diagnostics.ignoredEntries.join("; ")}`);
215
+ }
216
+
217
+ if (projectInstructionsLoad.status === "error") {
218
+ writeDiagnostic(io.stderr, "config", `could not load project instructions at ${projectInstructionsLoad.path}: ${projectInstructionsLoad.message}`);
219
+ }
220
+
221
+ if (!provider.run) {
222
+ writeDiagnostic(io.stderr, "error", `${provider.label} is unavailable for headless execution.`);
223
+ return { exitCode: HEADLESS_EXEC_PROVIDER_UNAVAILABLE };
224
+ }
225
+
226
+ return await new Promise<HeadlessExecResult>((resolve) => {
227
+ let settled = false;
228
+ const settle = (exitCode: number) => {
229
+ if (settled) return;
230
+ settled = true;
231
+ resolve({ exitCode });
232
+ };
233
+
234
+ try {
235
+ let streamedAssistantChars = 0;
236
+ const toolActivityIds = new Set<string>();
237
+
238
+ provider.run!(
239
+ options.prompt,
240
+ { runtime, workspaceRoot, projectInstructions, promptPolicy },
241
+ {
242
+ onAssistantDelta: (chunk) => {
243
+ const safeChunk = sanitizeTerminalOutput(chunk, { preserveTabs: false, tabSize: 2 });
244
+ if (shouldSuppressAssistantChunk(safeChunk)) return;
245
+ if (!safeChunk) return;
246
+ streamedAssistantChars += safeChunk.length;
247
+ io.stdout.write(safeChunk);
248
+ },
249
+ onProgress: (update) => {
250
+ const safeText = formatDiagnosticText(update.text);
251
+ if (!safeText || isNoiseLine(safeText) || isProcessTerminationNoise(safeText)) return;
252
+ if (update.source === "tool" && toolActivityIds.has(update.id)) return;
253
+ writeDiagnostic(io.stderr, update.source, safeText);
254
+ },
255
+ onToolActivity: (activity) => {
256
+ toolActivityIds.add(activity.id);
257
+ writeDiagnostic(io.stderr, "tool", formatToolActivity(activity));
258
+ },
259
+ onFinalAnswerObserved: (response) => {
260
+ diagnostics?.mark("final_answer_observed", {
261
+ final_answer_character_count: response.length,
262
+ });
263
+ },
264
+ onResponse: (response) => {
265
+ const safeResponse = sanitizeTerminalOutput(response, { preserveTabs: false, tabSize: 2 });
266
+ if (streamedAssistantChars === 0 && safeResponse) {
267
+ io.stdout.write(safeResponse);
268
+ }
269
+ settle(0);
270
+ },
271
+ onError: (message, rawOutput) => {
272
+ const details = [message, rawOutput].filter((value) => value?.trim()).join("\n");
273
+ writeDiagnostic(io.stderr, "error", details || "Provider run failed.");
274
+ settle(HEADLESS_EXEC_RUN_FAILED);
275
+ },
276
+ benchmarkHooks: diagnostics?.enabled
277
+ ? {
278
+ onProviderPrepStart: () => diagnostics.mark("provider_prep_start"),
279
+ onProviderPrepComplete: () => diagnostics.mark("provider_prep_complete"),
280
+ onProviderPromptPrepared: ({ policy, characterCount }) => diagnostics.mark("provider_prompt_prepared", {
281
+ prompt_policy: policy,
282
+ prompt_character_count_before_wrapping: options.prompt.length,
283
+ prompt_character_count_after_wrapping: characterCount,
284
+ }),
285
+ onCodexProcessSpawned: ({ executable, argv }) => diagnostics.mark("codex_process_spawned", {
286
+ codex_argv_preview: [executable, ...argv].join(" "),
287
+ }),
288
+ onFirstStdout: (observed = true) => diagnostics.mark("first_stdout", { observed }),
289
+ onFirstStderr: (observed = true) => diagnostics.mark("first_stderr", { observed }),
290
+ onCodexProcessExit: (exitCode) => diagnostics.mark("codex_process_exit", {
291
+ exit_code: exitCode,
292
+ }),
293
+ onCleanupStart: () => diagnostics.mark("cleanup_start"),
294
+ onCleanupComplete: ({ skipped }) => diagnostics.mark("cleanup_complete", { skipped }),
295
+ }
296
+ : undefined,
297
+ },
298
+ );
299
+ } catch (error) {
300
+ writeDiagnostic(io.stderr, "error", error instanceof Error ? error.message : String(error));
301
+ settle(HEADLESS_EXEC_RUN_FAILED);
302
+ }
303
+ });
304
+ }