impel-cli 0.20.14 → 0.20.16

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.
@@ -0,0 +1,584 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from "node:crypto";
4
+ import { spawn, spawnSync } from "node:child_process";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ import { nativeSpawnInvocation } from "../src/nativeProcess.js";
11
+ import { summarizeRawAttempt } from "./analyze-native-codex.mjs";
12
+
13
+ const PROFILE_SCHEMA = "impel.native-codex-profile.v1";
14
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
15
+ const DEFAULT_GRACE_MS = 5_000;
16
+ const MAX_CAPTURE_BYTES = 256 * 1024 * 1024;
17
+
18
+ function privateDirectory(directory) {
19
+ if (fs.existsSync(directory) && fs.lstatSync(directory).isSymbolicLink()) {
20
+ throw new Error(`refusing to use symlinked output directory ${directory}`);
21
+ }
22
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
23
+ try { fs.chmodSync(directory, 0o700); } catch { /* Best effort on Windows. */ }
24
+ }
25
+
26
+ function privateWrite(filePath, contents) {
27
+ fs.writeFileSync(filePath, contents, { encoding: "utf8", mode: 0o600, flag: "wx" });
28
+ try { fs.chmodSync(filePath, 0o600); } catch { /* Best effort on Windows. */ }
29
+ }
30
+
31
+ function openPrivate(filePath) {
32
+ const descriptor = fs.openSync(filePath, "wx", 0o600);
33
+ try { fs.chmodSync(filePath, 0o600); } catch { /* Best effort on Windows. */ }
34
+ return descriptor;
35
+ }
36
+
37
+ function delay(milliseconds) {
38
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
39
+ }
40
+
41
+ function posixGroupAlive(pid) {
42
+ try {
43
+ process.kill(-pid, 0);
44
+ return true;
45
+ } catch (error) {
46
+ return error?.code === "EPERM";
47
+ }
48
+ }
49
+
50
+ function windowsProcessAlive(pid, spawnSyncImpl = spawnSync) {
51
+ const result = spawnSyncImpl(
52
+ "tasklist.exe",
53
+ ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"],
54
+ { encoding: "utf8", windowsHide: true },
55
+ );
56
+ return result.status === 0 && new RegExp(`"${pid}"`, "u").test(result.stdout || "");
57
+ }
58
+
59
+ async function waitUntil(predicate, deadlineMs) {
60
+ const deadline = Date.now() + deadlineMs;
61
+ while (Date.now() < deadline) {
62
+ if (!predicate()) return true;
63
+ await delay(25);
64
+ }
65
+ return !predicate();
66
+ }
67
+
68
+ export async function terminateProcessTree({
69
+ child,
70
+ platform = process.platform,
71
+ graceMs = DEFAULT_GRACE_MS,
72
+ spawnSyncImpl = spawnSync,
73
+ } = {}) {
74
+ if (!Number.isSafeInteger(child?.pid) || child.pid <= 0) {
75
+ return { descendantsRemaining: false };
76
+ }
77
+ if (platform === "win32") {
78
+ spawnSyncImpl("taskkill.exe", ["/PID", String(child.pid), "/T"], {
79
+ encoding: "utf8",
80
+ windowsHide: true,
81
+ });
82
+ const exited = await waitUntil(() => windowsProcessAlive(child.pid, spawnSyncImpl), graceMs);
83
+ if (!exited) {
84
+ spawnSyncImpl("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], {
85
+ encoding: "utf8",
86
+ windowsHide: true,
87
+ });
88
+ await waitUntil(() => windowsProcessAlive(child.pid, spawnSyncImpl), 500);
89
+ }
90
+ return { descendantsRemaining: windowsProcessAlive(child.pid, spawnSyncImpl) };
91
+ }
92
+
93
+ try { process.kill(-child.pid, "SIGTERM"); } catch (error) {
94
+ if (error?.code !== "ESRCH") throw error;
95
+ }
96
+ const exited = await waitUntil(() => posixGroupAlive(child.pid), graceMs);
97
+ if (!exited) {
98
+ try { process.kill(-child.pid, "SIGKILL"); } catch (error) {
99
+ if (error?.code !== "ESRCH") throw error;
100
+ }
101
+ await waitUntil(() => posixGroupAlive(child.pid), 500);
102
+ }
103
+ return { descendantsRemaining: posixGroupAlive(child.pid) };
104
+ }
105
+
106
+ export async function runProfiledProcess({
107
+ command,
108
+ args = [],
109
+ environment = process.env,
110
+ cwd = process.cwd(),
111
+ input = "",
112
+ stdoutPath,
113
+ stderrPath,
114
+ timeoutMs = DEFAULT_TIMEOUT_MS,
115
+ graceMs = DEFAULT_GRACE_MS,
116
+ platform = process.platform,
117
+ } = {}) {
118
+ const stdoutFd = openPrivate(stdoutPath);
119
+ const stderrFd = openPrivate(stderrPath);
120
+ const startedAtMs = Date.now();
121
+ let capturedBytes = 0;
122
+ let spawnError = null;
123
+ let exitCode = null;
124
+ let signal = null;
125
+ const child = spawn(command, args, {
126
+ cwd,
127
+ env: environment,
128
+ detached: platform !== "win32",
129
+ stdio: ["pipe", "pipe", "pipe"],
130
+ windowsHide: true,
131
+ });
132
+ child.stdout.on("data", (chunk) => {
133
+ capturedBytes += chunk.length;
134
+ if (capturedBytes <= MAX_CAPTURE_BYTES) fs.writeSync(stdoutFd, chunk);
135
+ });
136
+ child.stderr.on("data", (chunk) => {
137
+ capturedBytes += chunk.length;
138
+ if (capturedBytes <= MAX_CAPTURE_BYTES) fs.writeSync(stderrFd, chunk);
139
+ });
140
+ child.once("error", (error) => { spawnError = error; });
141
+ const closed = new Promise((resolve) => child.once("close", (code, childSignal) => {
142
+ exitCode = code;
143
+ signal = childSignal;
144
+ resolve();
145
+ }));
146
+ child.stdin.on("error", () => {});
147
+ child.stdin.end(input);
148
+
149
+ let timedOut = false;
150
+ let cancellationLatencyMs = null;
151
+ let descendantsRemaining = null;
152
+ let timeoutHandle;
153
+ const timeout = new Promise((resolve) => {
154
+ timeoutHandle = setTimeout(() => resolve("timeout"), timeoutMs);
155
+ });
156
+ const winner = await Promise.race([closed.then(() => "closed"), timeout]);
157
+ clearTimeout(timeoutHandle);
158
+ if (winner === "timeout") {
159
+ timedOut = true;
160
+ const cancellationStarted = Date.now();
161
+ const termination = await terminateProcessTree({ child, platform, graceMs });
162
+ descendantsRemaining = termination.descendantsRemaining;
163
+ await Promise.race([closed, delay(1_000)]);
164
+ cancellationLatencyMs = Date.now() - cancellationStarted;
165
+ }
166
+ try { fs.closeSync(stdoutFd); } catch {}
167
+ try { fs.closeSync(stderrFd); } catch {}
168
+ if (capturedBytes > MAX_CAPTURE_BYTES) {
169
+ throw new Error("profile output exceeded the private capture limit");
170
+ }
171
+ return {
172
+ startedAtMs,
173
+ endedAtMs: Date.now(),
174
+ exitCode,
175
+ signal,
176
+ timedOut,
177
+ cancellationLatencyMs,
178
+ descendantsRemaining,
179
+ spawnError: spawnError?.message || null,
180
+ };
181
+ }
182
+
183
+ function impelInvocation(binary, args, environment = process.env) {
184
+ if (binary.endsWith(".js")) {
185
+ return { command: process.execPath, args: [path.resolve(binary), ...args] };
186
+ }
187
+ const invocation = nativeSpawnInvocation(binary, args, environment);
188
+ return { command: invocation.command, args: invocation.args };
189
+ }
190
+
191
+ function runCaptured(command, args, { environment = process.env, timeoutMs = 60_000 } = {}) {
192
+ return new Promise((resolve, reject) => {
193
+ const child = spawn(command, args, {
194
+ env: environment,
195
+ stdio: ["ignore", "pipe", "pipe"],
196
+ windowsHide: true,
197
+ });
198
+ let stdout = "";
199
+ let stderr = "";
200
+ const timer = setTimeout(() => child.kill(), timeoutMs);
201
+ child.stdout.setEncoding("utf8");
202
+ child.stderr.setEncoding("utf8");
203
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
204
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
205
+ child.once("error", reject);
206
+ child.once("close", (code) => {
207
+ clearTimeout(timer);
208
+ resolve({ code, stdout, stderr });
209
+ });
210
+ });
211
+ }
212
+
213
+ async function currentTenant(impelBinary, environment) {
214
+ for (let attempt = 0; attempt < 2; attempt += 1) {
215
+ const invocation = impelInvocation(impelBinary, ["tenant", "current"], environment);
216
+ const result = await runCaptured(invocation.command, invocation.args, { environment });
217
+ const lines = result.stdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
218
+ if (result.code === 0 && lines.length === 1 && /^[A-Za-z0-9_.:-]{1,160}$/u.test(lines[0])) {
219
+ return lines[0];
220
+ }
221
+ if (attempt === 0) await delay(250);
222
+ }
223
+ throw new Error("could not establish one exact current Impel tenant after two bounded attempts");
224
+ }
225
+
226
+ export async function assertExpectedTenant({ expectedTenant, impelBinary = "impel", environment = process.env }) {
227
+ const actual = await currentTenant(impelBinary, environment);
228
+ if (actual !== expectedTenant) {
229
+ throw new Error(`selected tenant ${JSON.stringify(actual)} does not match expected tenant ${JSON.stringify(expectedTenant)}`);
230
+ }
231
+ return actual;
232
+ }
233
+
234
+ function listRollouts(root, startedAtMs, endedAtMs) {
235
+ if (!fs.existsSync(root)) return [];
236
+ const results = [];
237
+ const days = new Set();
238
+ for (let timestamp = startedAtMs - 24 * 60 * 60 * 1000;
239
+ timestamp <= endedAtMs + 24 * 60 * 60 * 1000;
240
+ timestamp += 24 * 60 * 60 * 1000) {
241
+ const date = new Date(timestamp);
242
+ for (const values of [
243
+ [date.getFullYear(), date.getMonth() + 1, date.getDate()],
244
+ [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()],
245
+ ]) {
246
+ days.add(values.map((value, index) => index === 0 ? String(value) : String(value).padStart(2, "0")).join("/"));
247
+ }
248
+ }
249
+ for (const day of days) {
250
+ const directory = path.join(root, ...day.split("/"));
251
+ if (!fs.existsSync(directory) || fs.lstatSync(directory).isSymbolicLink()) continue;
252
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
253
+ if (!entry.isFile() || !/^rollout-.*\.jsonl$/u.test(entry.name)) continue;
254
+ const entryPath = path.join(directory, entry.name);
255
+ const modified = fs.statSync(entryPath).mtimeMs;
256
+ if (modified >= startedAtMs - 2_000 && modified <= endedAtMs + 2_000) results.push(entryPath);
257
+ }
258
+ }
259
+ return results;
260
+ }
261
+
262
+ function rolloutMetadata(filePath) {
263
+ const descriptor = fs.openSync(filePath, "r");
264
+ try {
265
+ const chunks = [];
266
+ let bytes = 0;
267
+ let position = 0;
268
+ for (;;) {
269
+ const buffer = Buffer.alloc(64 * 1024);
270
+ const length = fs.readSync(descriptor, buffer, 0, buffer.length, position);
271
+ if (length === 0) break;
272
+ const chunk = buffer.subarray(0, length);
273
+ const newline = chunk.indexOf(0x0a);
274
+ chunks.push(newline === -1 ? chunk : chunk.subarray(0, newline));
275
+ bytes += newline === -1 ? length : newline;
276
+ if (bytes > 4 * 1024 * 1024) throw new Error("rollout metadata exceeds the bounded parser limit");
277
+ if (newline !== -1) break;
278
+ position += length;
279
+ }
280
+ const firstLine = Buffer.concat(chunks).toString("utf8").replace(/\r$/u, "");
281
+ const record = JSON.parse(firstLine);
282
+ const payload = record.type === "session_meta" ? record.payload : null;
283
+ const threadId = payload?.session_id || payload?.id;
284
+ return {
285
+ threadId: typeof threadId === "string" ? threadId : null,
286
+ parentThreadId: typeof payload?.parent_thread_id === "string" ? payload.parent_thread_id : null,
287
+ };
288
+ } finally {
289
+ fs.closeSync(descriptor);
290
+ }
291
+ }
292
+
293
+ function rootThreadIds(stdoutText) {
294
+ const ids = new Set();
295
+ for (const line of stdoutText.split(/\r?\n/u)) {
296
+ if (!line.trim()) continue;
297
+ try {
298
+ const event = JSON.parse(line);
299
+ const value = event.thread_id || event.threadId;
300
+ if (typeof value === "string") ids.add(value);
301
+ } catch {
302
+ // Strict parsing reports this later. Rollout collection stays best effort.
303
+ }
304
+ }
305
+ return ids;
306
+ }
307
+
308
+ function collectAttemptRollouts({ rolloutRoot, startedAtMs, endedAtMs, stdoutText, destination }) {
309
+ const candidates = listRollouts(rolloutRoot, startedAtMs, endedAtMs).flatMap((filePath) => {
310
+ try { return [{ filePath, ...rolloutMetadata(filePath) }]; } catch { return []; }
311
+ });
312
+ const selectedIds = rootThreadIds(stdoutText);
313
+ let changed = true;
314
+ while (changed) {
315
+ changed = false;
316
+ for (const candidate of candidates) {
317
+ if (candidate.parentThreadId && selectedIds.has(candidate.parentThreadId)
318
+ && !selectedIds.has(candidate.threadId)) {
319
+ selectedIds.add(candidate.threadId);
320
+ changed = true;
321
+ }
322
+ }
323
+ }
324
+ const selected = candidates.filter((candidate) => selectedIds.has(candidate.threadId));
325
+ if (selected.length === 0) return [];
326
+ privateDirectory(destination);
327
+ return selected.map((candidate, index) => {
328
+ const target = path.join(destination, `rollout-${String(index + 1).padStart(2, "0")}.jsonl`);
329
+ privateWrite(target, fs.readFileSync(candidate.filePath, "utf8"));
330
+ return target;
331
+ });
332
+ }
333
+
334
+ function numericFlag(value, name, { minimum = 1, maximum = Number.MAX_SAFE_INTEGER } = {}) {
335
+ const parsed = Number(value);
336
+ if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
337
+ throw new Error(`${name} must be an integer from ${minimum} to ${maximum}`);
338
+ }
339
+ return parsed;
340
+ }
341
+
342
+ function parseArguments(argv) {
343
+ const options = { mode: "direct", cohort: "original", attempts: 1, concurrency: 1, timeoutMs: DEFAULT_TIMEOUT_MS, attemptOffset: 0, extraArgs: [] };
344
+ for (let index = 0; index < argv.length; index += 1) {
345
+ const argument = argv[index];
346
+ if (["--help", "-h"].includes(argument)) return { help: true };
347
+ const [name, inline] = argument.split(/=(.*)/su, 2);
348
+ const take = () => inline === undefined ? argv[index += 1] : inline;
349
+ if (name === "--expected-tenant") options.expectedTenant = take();
350
+ else if (name === "--agent") options.agent = take();
351
+ else if (name === "--prompt-file") options.promptFile = take();
352
+ else if (name === "--output-dir") options.outputDir = take();
353
+ else if (name === "--impel-bin") options.impelBinary = take();
354
+ else if (name === "--mode") options.mode = take();
355
+ else if (name === "--cohort") options.cohort = take();
356
+ else if (name === "--slo-class") options.sloClass = take();
357
+ else if (name === "--attempts") options.attempts = numericFlag(take(), "--attempts", { maximum: 1_000 });
358
+ else if (name === "--concurrency") options.concurrency = numericFlag(take(), "--concurrency", { maximum: 16 });
359
+ else if (name === "--timeout-ms") options.timeoutMs = numericFlag(take(), "--timeout-ms", { minimum: 1_000 });
360
+ else if (name === "--attempt-offset") options.attemptOffset = numericFlag(take(), "--attempt-offset", { minimum: 0 });
361
+ else if (name === "--extra-codex-arg") options.extraArgs.push(take());
362
+ else throw new Error(`unknown argument ${argument}`);
363
+ }
364
+ for (const required of ["expectedTenant", "agent", "promptFile", "outputDir", "sloClass"]) {
365
+ if (typeof options[required] !== "string" || !options[required].trim()) throw new Error(`--${required.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`)} is required`);
366
+ }
367
+ if (!["direct", "compatible"].includes(options.mode)) throw new Error("--mode must be direct or compatible");
368
+ if (!["original", "replacement"].includes(options.cohort)) throw new Error("--cohort must be original or replacement");
369
+ if (!["cibi", "ctos"].includes(options.sloClass)) throw new Error("--slo-class must be cibi or ctos");
370
+ if (options.concurrency > options.attempts) options.concurrency = options.attempts;
371
+ options.impelBinary ||= "impel";
372
+ return { ...options, help: false };
373
+ }
374
+
375
+ function safeAttemptRecord({ options, index, processResult, summary, jsonValid, parseError }) {
376
+ const codex = summary?.codex || {};
377
+ const telemetry = summary?.telemetry || {};
378
+ const durationMs = processResult.endedAtMs - processResult.startedAtMs;
379
+ const telemetryCompleted = telemetry.eventCounts?.local_tool_completed || 0;
380
+ const mcpDurationMs = telemetryCompleted > 0
381
+ ? telemetry.mcpDurationMs || 0
382
+ : codex.mcpDurationMs || 0;
383
+ const startCalls = (codex.toolCounts?.answer_native_agent || 0)
384
+ + (codex.toolCounts?.run_native_agent || 0);
385
+ const success = processResult.exitCode === 0
386
+ && !processResult.timedOut
387
+ && processResult.descendantsRemaining !== true
388
+ && jsonValid
389
+ && codex.turnCompleted === true
390
+ && (codex.errorEvents || 0) === 0
391
+ && (telemetry.localFailures || 0) === 0
392
+ && startCalls === 1;
393
+ return {
394
+ attemptId: `${options.cohort}-${String(index + 1 + options.attemptOffset).padStart(3, "0")}`,
395
+ cohort: options.cohort,
396
+ ...(options.cohort === "replacement" ? { replacementFor: `original-${String(index + 1 + options.attemptOffset).padStart(3, "0")}` } : {}),
397
+ tenantId: options.expectedTenant,
398
+ sloClass: options.sloClass,
399
+ mode: options.mode,
400
+ promptSha256: options.promptSha256,
401
+ startedAt: new Date(processResult.startedAtMs).toISOString(),
402
+ endedAt: new Date(processResult.endedAtMs).toISOString(),
403
+ durationMs,
404
+ exitCode: processResult.exitCode,
405
+ signal: processResult.signal,
406
+ timedOut: processResult.timedOut,
407
+ cancellationLatencyMs: processResult.cancellationLatencyMs,
408
+ descendantsRemaining: processResult.descendantsRemaining,
409
+ jsonValid,
410
+ parseFailureClass: parseError ? "invalid-jsonl" : null,
411
+ success,
412
+ eventCount: codex.eventCount || 0,
413
+ threadCount: codex.threadCount || 0,
414
+ toolCounts: codex.toolCounts || {},
415
+ parentToolCounts: codex.parentToolCounts || {},
416
+ childToolCounts: codex.childToolCounts || {},
417
+ codeCells: codex.codeCells || 0,
418
+ codeWaits: codex.codeWaits || 0,
419
+ inputTokens: codex.inputTokens || 0,
420
+ cachedInputTokens: codex.cachedInputTokens || 0,
421
+ outputTokens: codex.outputTokens || 0,
422
+ mcpDurationMs,
423
+ mcpDurationSource: telemetryCompleted > 0
424
+ ? "telemetry"
425
+ : (codex.mcpCompletedCalls || 0) > 0 ? "rollout" : "unavailable",
426
+ nonMcpOverheadMs: Math.max(0, durationMs - mcpDurationMs),
427
+ telemetryEventCount: telemetry.eventCount || 0,
428
+ telemetryCorrelationCount: telemetry.correlationCount || 0,
429
+ telemetryGatewayRequestCount: telemetry.gatewayRequestCount || 0,
430
+ };
431
+ }
432
+
433
+ async function profileAttempt(options, sessionDir, index) {
434
+ await assertExpectedTenant({
435
+ expectedTenant: options.expectedTenant,
436
+ impelBinary: options.impelBinary,
437
+ environment: process.env,
438
+ });
439
+ const attemptName = `${options.cohort}-${String(index + 1 + options.attemptOffset).padStart(3, "0")}`;
440
+ const attemptDir = path.join(sessionDir, attemptName);
441
+ privateDirectory(attemptDir);
442
+ const stdoutPath = path.join(attemptDir, "stdout.jsonl");
443
+ const stderrPath = path.join(attemptDir, "stderr.log");
444
+ const telemetryPath = path.join(attemptDir, "telemetry.jsonl");
445
+ const codexArgs = [
446
+ "codex",
447
+ ...(options.mode === "direct" ? ["--agent", options.agent] : []),
448
+ "exec",
449
+ "--json",
450
+ ...options.extraArgs,
451
+ "-",
452
+ ];
453
+ const invocation = impelInvocation(options.impelBinary, codexArgs, process.env);
454
+ const processResult = await runProfiledProcess({
455
+ ...invocation,
456
+ environment: {
457
+ ...process.env,
458
+ IMPEL_NATIVE_AGENT_TELEMETRY_PATH: telemetryPath,
459
+ IMPEL_NATIVE_HOST: options.mode === "direct" ? "codex-direct-profile" : "codex-compatible-agent",
460
+ IMPEL_NATIVE_HOST_BUILD: options.hostBuild,
461
+ },
462
+ input: options.prompt,
463
+ stdoutPath,
464
+ stderrPath,
465
+ timeoutMs: options.timeoutMs,
466
+ });
467
+ const stdoutText = fs.readFileSync(stdoutPath, "utf8");
468
+ const rolloutPaths = collectAttemptRollouts({
469
+ rolloutRoot: options.rolloutRoot,
470
+ startedAtMs: processResult.startedAtMs,
471
+ endedAtMs: processResult.endedAtMs,
472
+ stdoutText,
473
+ destination: path.join(attemptDir, "rollouts"),
474
+ });
475
+ let summary = null;
476
+ let parseError = null;
477
+ try {
478
+ summary = summarizeRawAttempt({
479
+ stdoutText,
480
+ telemetryText: fs.existsSync(telemetryPath) ? fs.readFileSync(telemetryPath, "utf8") : "",
481
+ rolloutTexts: rolloutPaths.map((rolloutPath) => fs.readFileSync(rolloutPath, "utf8")),
482
+ });
483
+ } catch (error) {
484
+ parseError = error;
485
+ }
486
+ const record = safeAttemptRecord({
487
+ options,
488
+ index,
489
+ processResult,
490
+ summary,
491
+ jsonValid: parseError === null,
492
+ parseError,
493
+ });
494
+ privateWrite(path.join(attemptDir, "record.json"), `${JSON.stringify(record, null, 2)}\n`);
495
+ return record;
496
+ }
497
+
498
+ async function runWorkers(options, sessionDir) {
499
+ const results = new Array(options.attempts);
500
+ let next = 0;
501
+ const worker = async () => {
502
+ for (;;) {
503
+ const index = next;
504
+ next += 1;
505
+ if (index >= options.attempts) return;
506
+ results[index] = await profileAttempt(options, sessionDir, index);
507
+ }
508
+ };
509
+ await Promise.all(Array.from({ length: options.concurrency }, () => worker()));
510
+ return results;
511
+ }
512
+
513
+ async function restoreTenant(originalTenant, options) {
514
+ let actual;
515
+ try { actual = await currentTenant(options.impelBinary, process.env); } catch { return false; }
516
+ if (actual === originalTenant) return true;
517
+ const invocation = impelInvocation(options.impelBinary, ["tenant", "use", originalTenant], process.env);
518
+ const result = await runCaptured(invocation.command, invocation.args, { environment: process.env });
519
+ if (result.code !== 0) return false;
520
+ return await currentTenant(options.impelBinary, process.env) === originalTenant;
521
+ }
522
+
523
+ async function main(argv) {
524
+ const options = parseArguments(argv);
525
+ if (options.help) {
526
+ process.stdout.write(
527
+ "Usage: node scripts/profile-native-codex.mjs --expected-tenant <id> --slo-class <cibi|ctos> --agent <id|title> --prompt-file <private-file> --output-dir <private-dir> [--mode direct|compatible] [--attempts 30] [--concurrency 2] [--cohort original|replacement]\n",
528
+ );
529
+ return;
530
+ }
531
+ const promptPath = path.resolve(options.promptFile);
532
+ if (fs.lstatSync(promptPath).isSymbolicLink() || !fs.lstatSync(promptPath).isFile()) {
533
+ throw new Error("prompt file must be a regular non-symlink file");
534
+ }
535
+ options.prompt = fs.readFileSync(promptPath, "utf8");
536
+ options.promptSha256 = crypto.createHash("sha256").update(options.prompt, "utf8").digest("hex");
537
+ const originalTenant = await currentTenant(options.impelBinary, process.env);
538
+ if (originalTenant !== options.expectedTenant) {
539
+ throw new Error(`selected tenant ${JSON.stringify(originalTenant)} does not match expected tenant ${JSON.stringify(options.expectedTenant)}`);
540
+ }
541
+ const versionInvocation = impelInvocation(options.impelBinary, ["--version"], process.env);
542
+ const version = await runCaptured(versionInvocation.command, versionInvocation.args, { environment: process.env });
543
+ if (version.code !== 0 || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?\s*$/u.test(version.stdout)) {
544
+ throw new Error("could not establish the Impel CLI build under test");
545
+ }
546
+ options.hostBuild = version.stdout.trim();
547
+ const configRoot = process.env.IMPEL_CONFIG_DIR || path.join(os.homedir(), ".config", "impel");
548
+ options.rolloutRoot = path.join(configRoot, "cli", "tenants", options.expectedTenant, "codex", "sessions");
549
+ const outputRoot = path.resolve(options.outputDir);
550
+ privateDirectory(outputRoot);
551
+ const sessionId = `codex-${new Date().toISOString().replace(/[:.]/gu, "-")}-${crypto.randomBytes(4).toString("hex")}`;
552
+ const sessionDir = path.join(outputRoot, sessionId);
553
+ privateDirectory(sessionDir);
554
+ let restored = false;
555
+ try {
556
+ const attempts = await runWorkers(options, sessionDir);
557
+ const aggregate = {
558
+ schema: PROFILE_SCHEMA,
559
+ sessionId,
560
+ generatedAt: new Date().toISOString(),
561
+ tenantId: options.expectedTenant,
562
+ sloClass: options.sloClass,
563
+ mode: options.mode,
564
+ cohort: options.cohort,
565
+ cliVersion: options.hostBuild,
566
+ promptSha256: options.promptSha256,
567
+ attempts,
568
+ };
569
+ const aggregatePath = path.join(sessionDir, "aggregate.json");
570
+ privateWrite(aggregatePath, `${JSON.stringify(aggregate, null, 2)}\n`);
571
+ process.stderr.write(`Private Codex profile written under ${sessionDir}\n`);
572
+ process.stdout.write(`${JSON.stringify(aggregate)}\n`);
573
+ } finally {
574
+ restored = await restoreTenant(originalTenant, options);
575
+ if (!restored) process.stderr.write("profile-native-codex: could not verify restoration of the original tenant\n");
576
+ }
577
+ if (!restored) process.exitCode = 1;
578
+ }
579
+
580
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
581
+ if (isMain) main(process.argv.slice(2)).catch((error) => {
582
+ process.stderr.write(`profile-native-codex: ${error.message}\n`);
583
+ process.exitCode = 1;
584
+ });