@pome-sh/cli 0.42.4 → 0.43.1

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 (30) hide show
  1. package/README.md +238 -117
  2. package/dist/build-info.json +3 -3
  3. package/dist/{checks-BKMTBJSO.js → checks-V442KL5O.js} +9 -14
  4. package/dist/{chunk-RPPF2KDQ.js → chunk-4DXTT3RV.js} +7 -7
  5. package/dist/{chunk-6KJC4BTO.js → chunk-5KFDRR53.js} +2 -2
  6. package/dist/{chunk-5NPGY73F.js → chunk-64AWQJ7R.js} +2 -2
  7. package/dist/{chunk-STMGQ7GU.js → chunk-A6AM6KS4.js} +1405 -172
  8. package/dist/{chunk-NNXVR46L.js → chunk-CS7O2ZXB.js} +114 -2
  9. package/dist/{chunk-VPXIGCZW.js → chunk-GQSNU3YP.js} +1 -1
  10. package/dist/{chunk-HRAD7MRX.js → chunk-TWURH7YM.js} +34 -10
  11. package/dist/{runTrialGroup-BP446HIE.js → runTrialGroup-HYGGBLMM.js} +20 -10
  12. package/dist/{server-GDK4TBTL.js → server-2WHSHTUD.js} +2 -2
  13. package/dist/src/cli/main.js +488 -226
  14. package/dist/{src-Z4C4SSDR.js → src-DBOHIQNB.js} +2 -2
  15. package/dist/{src-FUPOFZI4.js → src-FRPUTB2H.js} +2 -2
  16. package/dist/{src-UY4Y75BD.js → src-G6CMFUR6.js} +2 -2
  17. package/dist/{src-SNSK5ENY.js → src-SJDGRXC5.js} +2 -2
  18. package/dist/{src-V3RUKDMQ.js → src-VBXONWR3.js} +2 -2
  19. package/dist/twinHarness-33MAVAXV.js +6 -0
  20. package/dist/{twinSeed-X6VHBOLM.js → twinSeed-J7UAY3GM.js} +1 -1
  21. package/dist/{twinStart-QQU6OIO4.js → twinStart-3Q74IURZ.js} +21 -17
  22. package/package.json +1 -2
  23. package/dist/agent-Q2UTRJYL.js +0 -408
  24. package/dist/chunk-46POYRLY.js +0 -1240
  25. package/dist/chunk-DFOQGAKS.js +0 -116
  26. package/dist/chunk-JNXZBK3O.js +0 -17
  27. package/dist/chunk-WERG4AUH.js +0 -462
  28. package/dist/chunk-ZX4WNSZ5.js +0 -71
  29. package/dist/runDemo-DRDMCG25.js +0 -408
  30. package/dist/twinHarness-ATYQTDA5.js +0 -6
@@ -1,1240 +0,0 @@
1
- import { createHostedClient, perTwinReturnedByCloud, parseTaskFile, runAgentCommand, writeRunArtifactsCore, toTwinHttpEvent, redactJsonl, uploadRunBlobs, scoreFromFinalizeResponse, scoreStatus, evaluationCounts } from './chunk-STMGQ7GU.js';
2
- import { readSeedFileText, parseSeedFileText, soleTwinOf, twinsNamedBy, seedsForTwins } from './chunk-VPXIGCZW.js';
3
- import { readManifest, normalizeManifestTwins } from './chunk-DFOQGAKS.js';
4
- import { MOUNTED_TWINS, HostedAuthError, HostedDiscardRefusedError, HostedQuotaError, HostedOrchError, HostedTrialError, agentResponseSchema, isMultiTwinSeedEnvelope } from './chunk-NNXVR46L.js';
5
- import { redactSecrets, redactEvent } from './chunk-SG6ZTIMT.js';
6
- import { randomUUID, createHash } from 'node:crypto';
7
- import { rm, readFile, stat, mkdir, writeFile, chmod, mkdtemp, readdir, rename } from 'node:fs/promises';
8
- import { join, dirname } from 'node:path';
9
- import { tmpdir, homedir } from 'node:os';
10
- import { execFile } from 'node:child_process';
11
- import { promisify } from 'node:util';
12
- import { createInterface } from 'node:readline';
13
-
14
- // src/hosted/runSets.ts
15
- function groupRunSets(trials) {
16
- const byKey = /* @__PURE__ */ new Map();
17
- for (const trial of trials) {
18
- const key = trial.verdict.group_id ?? `solo:${trial.verdict.session_id}`;
19
- const bucket = byKey.get(key);
20
- if (bucket) bucket.push(trial);
21
- else byKey.set(key, [trial]);
22
- }
23
- const sets = [];
24
- for (const bucket of byKey.values()) {
25
- bucket.sort(
26
- (a, b) => a.verdict.finalized_at.localeCompare(b.verdict.finalized_at)
27
- );
28
- const last = bucket[bucket.length - 1];
29
- const hasFailed = bucket.some((t) => t.verdict.state === "fail");
30
- const allPassed = bucket.every((t) => t.verdict.state === "pass");
31
- sets.push({
32
- groupId: bucket[0].verdict.group_id,
33
- taskName: bucket[0].verdict.task_name,
34
- taskPath: bucket[0].verdict.task_path,
35
- trials: bucket,
36
- latestFinalizedAt: last.verdict.finalized_at,
37
- outcome: hasFailed ? "fail" : allPassed ? "pass" : "incomplete"
38
- });
39
- }
40
- sets.sort((a, b) => a.latestFinalizedAt.localeCompare(b.latestFinalizedAt));
41
- return sets;
42
- }
43
- function latestFailedRunSet(sets) {
44
- for (let i = sets.length - 1; i >= 0; i -= 1) {
45
- if (sets[i].outcome === "fail") return sets[i];
46
- }
47
- return null;
48
- }
49
- function latestIncompleteRunSet(sets) {
50
- for (let i = sets.length - 1; i >= 0; i -= 1) {
51
- if (sets[i].outcome === "incomplete") return sets[i];
52
- }
53
- return null;
54
- }
55
-
56
- // src/hosted/evalResultCache.ts
57
- var VERDICT_ARTIFACT_VERSION = 2;
58
- var VERDICT_FILENAME = "verdict.json";
59
- var VALID_STATES = /* @__PURE__ */ new Set([
60
- "pass",
61
- "fail",
62
- "incomplete"
63
- ]);
64
- async function writeVerdictArtifact(runDir, verdict) {
65
- const tmpPath = join(runDir, `.${VERDICT_FILENAME}.${process.pid}.${randomUUID()}.tmp`);
66
- try {
67
- await writeFile(tmpPath, `${JSON.stringify(verdict, null, 2)}
68
- `, "utf8");
69
- await rename(tmpPath, join(runDir, VERDICT_FILENAME));
70
- } catch (err) {
71
- await rm(tmpPath, { force: true }).catch(() => {
72
- });
73
- throw err;
74
- }
75
- }
76
- function isVerdictArtifact(parsed) {
77
- if (typeof parsed !== "object" || parsed === null) return false;
78
- const v = parsed;
79
- if (v.version !== VERDICT_ARTIFACT_VERSION) return false;
80
- if (v.source !== "cloud-finalize") return false;
81
- if (typeof v.session_id !== "string") return false;
82
- if (typeof v.task_name !== "string") return false;
83
- if (typeof v.task_path !== "string") return false;
84
- if (v.group_id !== null && typeof v.group_id !== "string") return false;
85
- if (typeof v.finalized_at !== "string") return false;
86
- if (typeof v.passed !== "boolean") return false;
87
- if (typeof v.score !== "number") return false;
88
- if (typeof v.state !== "string" || !VALID_STATES.has(v.state)) return false;
89
- if (typeof v.evaluated !== "number") return false;
90
- if (typeof v.not_evaluated !== "number") return false;
91
- if (typeof v.pre_satisfied !== "number") return false;
92
- if (typeof v.total !== "number") return false;
93
- if (!Array.isArray(v.criteria_results)) return false;
94
- return v.criteria_results.every((r) => {
95
- if (typeof r !== "object" || r === null) return false;
96
- const result = r;
97
- const criterion = result.criterion;
98
- return typeof criterion === "object" && criterion !== null && typeof criterion.text === "string" && typeof result.reason === "string" && typeof result.passed === "boolean" && typeof result.skipped === "boolean";
99
- });
100
- }
101
- var MISSING_ERROR_CODES = /* @__PURE__ */ new Set([
102
- "ENOENT",
103
- "ENOTDIR",
104
- "ELOOP",
105
- "ENAMETOOLONG"
106
- ]);
107
- function isMissingFileError(err) {
108
- const code = err?.code;
109
- return typeof code === "string" && MISSING_ERROR_CODES.has(code);
110
- }
111
- async function readVerdictArtifactDetailed(runDir) {
112
- const path = join(runDir, VERDICT_FILENAME);
113
- let raw;
114
- try {
115
- raw = await readFile(path, "utf8");
116
- } catch (err) {
117
- return isMissingFileError(err) ? { status: "missing" } : { status: "unreadable" };
118
- }
119
- let parsed;
120
- try {
121
- parsed = JSON.parse(raw);
122
- } catch {
123
- return { status: "unreadable" };
124
- }
125
- if (!isVerdictArtifact(parsed)) return { status: "unreadable" };
126
- return { status: "ok", trial: { runDir, verdict: parsed } };
127
- }
128
- async function scanVerdictArtifactsDetailed(artifactsRoot) {
129
- const trials = [];
130
- const unreadableDirs = [];
131
- let slugs;
132
- try {
133
- slugs = await readdir(artifactsRoot);
134
- } catch {
135
- return { trials, unreadableDirs };
136
- }
137
- for (const slug of slugs) {
138
- const slugDir = join(artifactsRoot, slug);
139
- let runIds;
140
- try {
141
- runIds = await readdir(slugDir);
142
- } catch {
143
- continue;
144
- }
145
- for (const runId of runIds) {
146
- const runDir = join(slugDir, runId);
147
- const result = await readVerdictArtifactDetailed(runDir);
148
- if (result.status === "ok") trials.push(result.trial);
149
- else if (result.status === "unreadable") unreadableDirs.push(runDir);
150
- }
151
- }
152
- unreadableDirs.sort();
153
- return { trials, unreadableDirs };
154
- }
155
- async function discoverRunSet(target) {
156
- const anchorResult = await readVerdictArtifactDetailed(target);
157
- if (anchorResult.status === "unreadable") {
158
- return {
159
- kind: "trial-dir",
160
- set: null,
161
- incompleteSet: null,
162
- totalSets: 0,
163
- unreadableCount: 1,
164
- unreadablePaths: [target]
165
- };
166
- }
167
- if (anchorResult.status === "ok") {
168
- const anchor = anchorResult.trial;
169
- const root = join(target, "..", "..");
170
- const { trials: trials2, unreadableDirs: unreadableDirs2 } = await scanVerdictArtifactsDetailed(root);
171
- const sets2 = groupRunSets(trials2);
172
- const own = sets2.find(
173
- (s) => anchor.verdict.group_id !== null && s.groupId === anchor.verdict.group_id || anchor.verdict.group_id === null && s.trials.length === 1 && s.trials[0].verdict.session_id === anchor.verdict.session_id
174
- ) ?? groupRunSets([anchor])[0];
175
- return {
176
- kind: "trial-dir",
177
- set: own,
178
- incompleteSet: null,
179
- totalSets: Math.max(sets2.length, 1),
180
- unreadableCount: unreadableDirs2.length,
181
- unreadablePaths: unreadableDirs2
182
- };
183
- }
184
- const { trials, unreadableDirs } = await scanVerdictArtifactsDetailed(target);
185
- const sets = groupRunSets(trials);
186
- const failedSet = latestFailedRunSet(sets);
187
- return {
188
- kind: "root",
189
- set: failedSet,
190
- incompleteSet: failedSet ? null : latestIncompleteRunSet(sets),
191
- totalSets: sets.length,
192
- unreadableCount: unreadableDirs.length,
193
- unreadablePaths: unreadableDirs
194
- };
195
- }
196
- var execFileAsync = promisify(execFile);
197
- var SERVICE = "sh.pome.cli";
198
- var ACCOUNT = "hosted";
199
- function keychainSupported() {
200
- if (process.env.POME_CLI_DISABLE_KEYCHAIN === "1") return false;
201
- return process.platform === "darwin";
202
- }
203
- async function writeKeychainCredentials(creds) {
204
- if (!keychainSupported()) return;
205
- const payload = JSON.stringify({
206
- api_key: creds.api_key,
207
- api_url: creds.api_url,
208
- dashboard_url: creds.dashboard_url,
209
- team_id: creds.team_id,
210
- created_at: (/* @__PURE__ */ new Date()).toISOString()
211
- });
212
- await execFileAsync("security", [
213
- "add-generic-password",
214
- "-U",
215
- "-s",
216
- SERVICE,
217
- "-a",
218
- ACCOUNT,
219
- "-w",
220
- payload
221
- ]);
222
- }
223
- async function readKeychainCredentials() {
224
- if (!keychainSupported()) return null;
225
- try {
226
- const { stdout } = await execFileAsync("security", [
227
- "find-generic-password",
228
- "-s",
229
- SERVICE,
230
- "-a",
231
- ACCOUNT,
232
- "-w"
233
- ]);
234
- const text = stdout.trim();
235
- if (!text) return null;
236
- const parsed = JSON.parse(text);
237
- if (typeof parsed.api_key !== "string" || !parsed.api_key.trim()) {
238
- return null;
239
- }
240
- return parsed;
241
- } catch {
242
- return null;
243
- }
244
- }
245
- async function deleteKeychainCredentials() {
246
- if (!keychainSupported()) return false;
247
- try {
248
- await execFileAsync("security", [
249
- "delete-generic-password",
250
- "-s",
251
- SERVICE,
252
- "-a",
253
- ACCOUNT
254
- ]);
255
- return true;
256
- } catch {
257
- return false;
258
- }
259
- }
260
-
261
- // src/cli/credentials.ts
262
- var DEFAULT_PATH = () => join(homedir(), ".pome", "credentials.json");
263
- async function persistCredentialsAfterLogin(credentials, path = DEFAULT_PATH()) {
264
- if (keychainSupported()) {
265
- try {
266
- await writeKeychainCredentials(credentials);
267
- try {
268
- await rm(path, { force: true });
269
- } catch {
270
- }
271
- return { stored: "keychain" };
272
- } catch {
273
- }
274
- }
275
- await writeCredentialsFile(credentials, path);
276
- return { stored: "file", path };
277
- }
278
- async function clearLocalCredentials(path = DEFAULT_PATH()) {
279
- await deleteKeychainCredentials();
280
- try {
281
- await rm(path, { force: true });
282
- } catch {
283
- }
284
- }
285
- async function resolveCredentials(input) {
286
- const envKey = process.env.POME_API_KEY;
287
- if (envKey && envKey.trim().length > 0) {
288
- const apiBaseUrl = resolveApiBaseUrl(input.apiBaseUrl, void 0);
289
- return { apiKey: envKey.trim(), apiBaseUrl };
290
- }
291
- const keychain = await readKeychainCredentials();
292
- if (keychain && typeof keychain.api_key === "string" && keychain.api_key.trim().length > 0) {
293
- return {
294
- apiKey: keychain.api_key.trim(),
295
- apiBaseUrl: resolveApiBaseUrl(input.apiBaseUrl, keychain.api_url),
296
- teamId: normalizeTeamId(keychain.team_id)
297
- };
298
- }
299
- const path = input.credentialsPath ?? DEFAULT_PATH();
300
- let raw;
301
- try {
302
- await assertSafeCredentialFileMode(path);
303
- raw = await readFile(path, "utf8");
304
- } catch (err) {
305
- if (err instanceof Error && err.code === "ENOENT") {
306
- throw new HostedAuthError(
307
- `Hosted mode requires authentication. Run \`pome login\`, set POME_API_KEY in your environment, or sign in on macOS to store credentials in Keychain. Expected file: ${path}.`
308
- );
309
- }
310
- throw err;
311
- }
312
- let parsed;
313
- try {
314
- parsed = JSON.parse(raw);
315
- } catch {
316
- throw new Error(`${path} is not valid JSON.`);
317
- }
318
- if (typeof parsed.api_key !== "string" || parsed.api_key.trim().length === 0) {
319
- throw new Error(`${path} is missing "api_key".`);
320
- }
321
- return {
322
- apiKey: parsed.api_key.trim(),
323
- apiBaseUrl: resolveApiBaseUrl(input.apiBaseUrl, parsed.api_url),
324
- teamId: normalizeTeamId(parsed.team_id)
325
- };
326
- }
327
- function normalizeTeamId(raw) {
328
- if (typeof raw !== "string") return void 0;
329
- const trimmed = raw.trim();
330
- return trimmed.length > 0 ? trimmed : void 0;
331
- }
332
- function resolveApiBaseUrl(callerInput, storedApiUrl) {
333
- const candidate = callerInput ?? storedApiUrl;
334
- if (typeof candidate !== "string" || candidate.trim().length === 0) {
335
- throw new Error(
336
- "Refusing to resolve hosted credentials: no apiBaseUrl supplied and no stored api_url found."
337
- );
338
- }
339
- return normalizeBaseUrl(candidate);
340
- }
341
- async function assertSafeCredentialFileMode(path) {
342
- const st = await stat(path);
343
- const perm = st.mode & 511;
344
- if ((perm & 63) !== 0) {
345
- throw new Error(
346
- `Refusing to read credentials at ${path}: mode 0${perm.toString(8)} is too permissive (group/other bits must be 0). Run: chmod 600 ${path}`
347
- );
348
- }
349
- }
350
- async function writeCredentialsFile(credentials, path = DEFAULT_PATH()) {
351
- await mkdir(dirname(path), { recursive: true, mode: 448 });
352
- await writeFile(
353
- path,
354
- `${JSON.stringify(
355
- {
356
- ...credentials,
357
- created_at: (/* @__PURE__ */ new Date()).toISOString()
358
- },
359
- null,
360
- 2
361
- )}
362
- `,
363
- { mode: 384 }
364
- );
365
- await chmod(path, 384);
366
- }
367
- function normalizeBaseUrl(raw) {
368
- return new URL(raw).toString().replace(/\/$/, "");
369
- }
370
- function resolveSeams(opts) {
371
- return {
372
- stdinIsTTY: opts.stdinIsTTY ?? Boolean(process.stdin.isTTY),
373
- confirm: opts.confirm ?? promptYesNo,
374
- ask: opts.ask ?? promptLine
375
- };
376
- }
377
- async function requestAgent(creds, body) {
378
- const res = await fetch(`${creds.apiBaseUrl}/v1/agents`, {
379
- method: "POST",
380
- headers: { "x-api-key": creds.apiKey, "content-type": "application/json" },
381
- body: JSON.stringify(prunePostBody(body))
382
- }).catch((err) => {
383
- throw new HostedOrchError(err instanceof Error ? err.message : "network error");
384
- });
385
- const text = await res.text();
386
- let json = {};
387
- try {
388
- json = text.length ? JSON.parse(text) : {};
389
- } catch {
390
- throw new HostedOrchError(`non-JSON response (status ${res.status})`);
391
- }
392
- return { ok: res.ok, status: res.status, json };
393
- }
394
- function prunePostBody(body) {
395
- const out = { name: body.name };
396
- if (body.slug !== void 0) out.slug = body.slug;
397
- if (body.description !== void 0) out.description = body.description;
398
- if (body.version !== void 0) out.version = body.version;
399
- if (body.framework !== void 0) out.framework = body.framework;
400
- if (body.twins && body.twins.length > 0) out.twins = body.twins;
401
- if (body.confirm !== void 0) out.confirm = body.confirm;
402
- return out;
403
- }
404
- function parseOkAgent(json) {
405
- const parsed = agentResponseSchema.safeParse(json);
406
- if (!parsed.success) {
407
- throw new HostedOrchError("POST /v1/agents returned an unexpected shape");
408
- }
409
- return parsed.data;
410
- }
411
- function mapHttpError(status, json) {
412
- const err = json.error;
413
- if (status === 401 || status === 403) {
414
- return new HostedAuthError(err?.message ?? `HTTP ${status}`);
415
- }
416
- if (status === 404) {
417
- return new HostedOrchError(
418
- err?.message ?? "POST /v1/agents returned 404 \u2014 check that --api-url/POME_API_URL points at a Pome control plane."
419
- );
420
- }
421
- return new HostedOrchError(err?.message ?? `POST /v1/agents \u2192 HTTP ${status}`);
422
- }
423
- function nearMissSuggestion(json) {
424
- const error = json.error;
425
- if (error?.type !== "conflict") return void 0;
426
- const details = error.details;
427
- if (!details) return void 0;
428
- const candidate = details.suggestion ?? details.slug ?? (Array.isArray(details.suggestions) ? details.suggestions[0] : void 0);
429
- return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
430
- }
431
- async function postAgentResolver(creds, body, seams) {
432
- const first = await requestAgent(creds, body);
433
- if (first.ok) return parseOkAgent(first.json);
434
- if (first.status === 409) {
435
- const suggestion = nearMissSuggestion(first.json);
436
- if (suggestion) {
437
- const interactive = seams.stdinIsTTY && !process.env.CI;
438
- let retry;
439
- if (interactive) {
440
- const yes = await seams.confirm(
441
- `An agent named "${suggestion}" already exists on your team. Did you mean "${suggestion}"? [y/N] `
442
- );
443
- retry = yes ? { ...body, slug: suggestion } : { ...body, confirm: true };
444
- } else {
445
- console.error(
446
- `Near-miss: this slug is close to your team's existing "${suggestion}". Registering the new agent anyway (run interactively to link the existing one instead).`
447
- );
448
- retry = { ...body, confirm: true };
449
- }
450
- const second = await requestAgent(creds, retry);
451
- if (second.ok) return parseOkAgent(second.json);
452
- throw mapHttpError(second.status, second.json);
453
- }
454
- }
455
- throw mapHttpError(first.status, first.json);
456
- }
457
- async function promptYesNo(question) {
458
- const rl = createInterface({ input: process.stdin, output: process.stderr });
459
- try {
460
- const answer = await new Promise((resolve) => rl.question(question, resolve));
461
- return /^y(es)?$/i.test(answer.trim());
462
- } finally {
463
- rl.close();
464
- }
465
- }
466
- async function promptLine(question) {
467
- const rl = createInterface({ input: process.stdin, output: process.stderr });
468
- try {
469
- return (await new Promise((resolve) => rl.question(question, resolve))).trim();
470
- } finally {
471
- rl.close();
472
- }
473
- }
474
- var POME_DIR = ".pome";
475
- var LINK_FILE = "link.json";
476
- var GITIGNORE_ENTRY = `${POME_DIR}/`;
477
- function linkPath(projectDir) {
478
- return join(projectDir, POME_DIR, LINK_FILE);
479
- }
480
- async function readLinkCache(projectDir) {
481
- let text;
482
- try {
483
- text = await readFile(linkPath(projectDir), "utf8");
484
- } catch (err) {
485
- if (err.code === "ENOENT") return null;
486
- return null;
487
- }
488
- let parsed;
489
- try {
490
- parsed = JSON.parse(text);
491
- } catch {
492
- return null;
493
- }
494
- if (typeof parsed !== "object" || parsed === null || typeof parsed.agent_id !== "string" || typeof parsed.team_id !== "string") {
495
- return null;
496
- }
497
- const { agent_id, team_id } = parsed;
498
- const resolved_at = typeof parsed.resolved_at === "string" ? parsed.resolved_at : "";
499
- return { agent_id, team_id, resolved_at };
500
- }
501
- async function writeLinkCache(projectDir, entry) {
502
- await mkdir(join(projectDir, POME_DIR), { recursive: true });
503
- const body = {
504
- agent_id: entry.agent_id,
505
- team_id: entry.team_id,
506
- resolved_at: (/* @__PURE__ */ new Date()).toISOString()
507
- };
508
- await writeFile(linkPath(projectDir), `${JSON.stringify(body, null, 2)}
509
- `);
510
- }
511
- function resolveCachedAgentId(cache, callerTeamId) {
512
- if (!cache || !callerTeamId) return void 0;
513
- return cache.team_id === callerTeamId ? cache.agent_id : void 0;
514
- }
515
- async function ensurePomeGitignored(projectDir) {
516
- const path = join(projectDir, ".gitignore");
517
- let existing;
518
- try {
519
- existing = await readFile(path, "utf8");
520
- } catch (err) {
521
- if (err.code !== "ENOENT") throw err;
522
- existing = null;
523
- }
524
- if (existing === null) {
525
- await writeFile(path, `${GITIGNORE_ENTRY}
526
- `);
527
- return;
528
- }
529
- const alreadyIgnored = existing.split(/\r?\n/).some((line) => line.trim() === GITIGNORE_ENTRY || line.trim() === POME_DIR);
530
- if (alreadyIgnored) return;
531
- const separator = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
532
- await writeFile(path, `${existing}${separator}${GITIGNORE_ENTRY}
533
- `);
534
- }
535
-
536
- // src/cli/agent-identity.ts
537
- async function resolveRunAgentIdentity(input) {
538
- const manifestRead = await readManifest(input.startDir);
539
- if (!manifestRead) {
540
- return input.agentVersionOverride ? { agentVersion: input.agentVersionOverride } : {};
541
- }
542
- const { agent } = manifestRead.manifest;
543
- const agentVersion = input.agentVersionOverride ?? agent.version;
544
- const base = {
545
- agentVersion,
546
- framework: agent.framework,
547
- agentSlug: agent.slug
548
- };
549
- const projectDir = dirname(manifestRead.path);
550
- try {
551
- const creds = await resolveCredentials({
552
- apiBaseUrl: input.apiBaseUrl,
553
- credentialsPath: input.credentialsPath
554
- });
555
- const cachedId = resolveCachedAgentId(await readLinkCache(projectDir), creds.teamId);
556
- if (cachedId) {
557
- return { ...base, agentId: cachedId };
558
- }
559
- const resolved = await postAgentResolver(
560
- creds,
561
- {
562
- name: agent.name ?? agent.slug,
563
- slug: agent.slug,
564
- description: agent.description,
565
- version: agent.version,
566
- framework: agent.framework,
567
- twins: normalizeManifestTwins(manifestRead.manifest.twins)
568
- },
569
- resolveSeams({ stdinIsTTY: false })
570
- );
571
- if (creds.teamId) {
572
- await writeLinkCache(projectDir, { agent_id: resolved.id, team_id: creds.teamId });
573
- await ensurePomeGitignored(projectDir);
574
- }
575
- return { ...base, agentId: resolved.id };
576
- } catch (err) {
577
- console.error(
578
- `pome: could not resolve agent identity (${err instanceof Error ? err.message : String(err)}); running unattributed.`
579
- );
580
- return base;
581
- }
582
- }
583
-
584
- // src/cli/defaults.ts
585
- var DEFAULT_CONTROL_PLANE_URL = "https://api.pome.sh";
586
- var DEFAULT_DASHBOARD_URL = "https://app.pome.sh";
587
- var DEFAULT_DOCS_SITE_ORIGIN = "https://docs.pome.sh";
588
- function ensureMcpSuffix(url) {
589
- return /\/mcp\/?$/.test(url) ? url : `${url.replace(/\/$/, "")}/mcp`;
590
- }
591
- var ALLOWED_TWINS = new Set(MOUNTED_TWINS);
592
- var SESSION_TWIN_NAMES = [...ALLOWED_TWINS];
593
- function redactSession(res) {
594
- const pcIn = res.provider_credentials;
595
- const pcOut = {};
596
- if (pcIn.github) {
597
- pcOut.github = { ...pcIn.github, token: "***redacted***" };
598
- }
599
- if (pcIn.stripe) {
600
- pcOut.stripe = { ...pcIn.stripe, api_key: "***redacted***" };
601
- }
602
- if (pcIn.slack) {
603
- pcOut.slack = { ...pcIn.slack, token: "***redacted***" };
604
- }
605
- return {
606
- session_id: res.session_id,
607
- session_token: res.session_token ? "***redacted***" : res.session_id,
608
- twin_url: res.twin_url,
609
- expires_at: res.expires_at,
610
- openapi_url: res.openapi_url,
611
- per_twin: res.per_twin,
612
- provider_credentials: pcOut,
613
- agent_token: "***redacted***"
614
- };
615
- }
616
- function formatEnvExport(res, twins) {
617
- const lines = [
618
- `# Pome hosted sandbox \u2014 treat as secret. Twins: ${twins.join(", ")}`,
619
- `export POME_AUTH_TOKEN=${JSON.stringify(res.agent_token)}`,
620
- `export POME_SESSION_ID=${JSON.stringify(res.session_id)}`,
621
- // Legacy single-endpoint URL (= the primary twin's api_url). Kept for
622
- // agents written against the pre-multi-twin contract.
623
- `export POME_TWIN_URL=${JSON.stringify(res.twin_url)}`,
624
- `export POME_TWIN_NAMES=${JSON.stringify(twins.join(","))}`
625
- ];
626
- const fromCloud = perTwinReturnedByCloud(res);
627
- for (const twin of twins) {
628
- const upper = twin.toUpperCase();
629
- const pt = res.per_twin?.[twin];
630
- if (pt?.api_url) {
631
- const mcpUrl = fromCloud ? ensureMcpSuffix(pt.mcp_url) : ensureMcpSuffix(pt.api_url);
632
- lines.push(`export POME_${upper}_REST_URL=${JSON.stringify(pt.api_url)}`);
633
- lines.push(
634
- `export POME_${upper}_MCP_URL=${JSON.stringify(mcpUrl)}`
635
- );
636
- }
637
- if (twin === "github") {
638
- const gh = res.provider_credentials.github;
639
- if (gh) {
640
- lines.push(`export POME_GITHUB_TOKEN=${JSON.stringify(gh.token)}`);
641
- }
642
- } else if (twin === "stripe") {
643
- const st = res.provider_credentials.stripe;
644
- if (st) {
645
- lines.push(`export POME_STRIPE_API_KEY=${JSON.stringify(st.api_key)}`);
646
- }
647
- if (pt?.api_url) {
648
- lines.push(`export POME_STRIPE_API_BASE=${JSON.stringify(pt.api_url)}`);
649
- }
650
- } else if (twin === "slack") {
651
- lines.push(`export POME_SLACK_TOKEN=${JSON.stringify(res.agent_token)}`);
652
- } else if (twin === "gmail") {
653
- lines.push(`export POME_GMAIL_TOKEN=${JSON.stringify(res.agent_token)}`);
654
- }
655
- }
656
- return `${lines.join("\n")}
657
- `;
658
- }
659
- function normalizeSessionTwins(raw) {
660
- const twins = raw.map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0);
661
- for (const twin of twins) {
662
- if (!ALLOWED_TWINS.has(twin)) {
663
- throw new Error(
664
- `Unknown twin "${twin}". Supported: ${[...ALLOWED_TWINS].join(", ")}.`
665
- );
666
- }
667
- }
668
- const deduped = [...new Set(twins)];
669
- if (deduped.length === 0) {
670
- throw new Error(
671
- `No twin specified. Pass --twin <name> (repeat for multi-twin). Supported: ${[...ALLOWED_TWINS].join(", ")}.`
672
- );
673
- }
674
- return deduped;
675
- }
676
- async function resolveSessionSeed(seedPath, requestedTwins) {
677
- const raw = readSeedFileText(seedPath, "pome sandbox create --seed");
678
- const origin = `--seed ${seedPath}`;
679
- const file = parseSeedFileText(raw, origin);
680
- const sole = soleTwinOf(file);
681
- if (requestedTwins.length === 0 && sole === void 0) {
682
- const named = twinsNamedBy(file);
683
- throw new Error(
684
- named.length === 0 ? `${origin} is a flat seed, so it does not name a twin. Pass the name: pome sandbox create --twin <${SESSION_TWIN_NAMES.join("|")}> --seed ${seedPath}` : `${origin} names ${named.length} twins (${named.join(", ")}), so it does not say which the sandbox is for. Pass the name: ${named.map((twin) => `--twin ${twin}`).join(" ")} --seed ${seedPath}`
685
- );
686
- }
687
- const twins = normalizeSessionTwins(
688
- requestedTwins.length > 0 ? requestedTwins : [sole]
689
- );
690
- const byTwin = await seedsForTwins(file, twins, origin);
691
- return {
692
- twins,
693
- seed: isMultiTwinSeedEnvelope(twins) ? byTwin : byTwin[twins[0]]
694
- };
695
- }
696
- async function runSessionCreate(opts) {
697
- const resolvedSeed = opts.seedPath === void 0 ? void 0 : await resolveSessionSeed(opts.seedPath, opts.twins);
698
- const twins = resolvedSeed?.twins ?? normalizeSessionTwins(opts.twins);
699
- const creds = await resolveCredentials({ apiBaseUrl: opts.apiBaseUrl });
700
- const client = createHostedClient({
701
- baseUrl: creds.apiBaseUrl,
702
- apiKey: creds.apiKey
703
- });
704
- const identity = await resolveRunAgentIdentity({
705
- startDir: process.cwd(),
706
- apiBaseUrl: creds.apiBaseUrl
707
- });
708
- const session = await client.createSession({
709
- twins,
710
- taskSource: "# ..\n",
711
- idempotencyKey: randomUUID(),
712
- agentId: identity.agentId,
713
- agentVersion: identity.agentVersion,
714
- ...resolvedSeed ? { seed: resolvedSeed.seed } : {}
715
- });
716
- if (opts.secretsFile) {
717
- await writeSecretsFile(opts.secretsFile, formatEnvExport(session, twins));
718
- console.error(`Wrote sandbox secrets to ${opts.secretsFile} (mode 0600).`);
719
- }
720
- if (opts.json) {
721
- const payload = redactSession(session);
722
- console.log(JSON.stringify(payload, null, 2));
723
- return;
724
- }
725
- console.error(`Sandbox: ${session.session_id}`);
726
- console.error(`Expires: ${session.expires_at}`);
727
- if (opts.seedPath !== void 0) {
728
- console.error(`Seed: ${opts.seedPath} (replaces the default for ${twins.join(", ")}).`);
729
- }
730
- let printedAny = false;
731
- const mcpFromCloud = perTwinReturnedByCloud(session);
732
- for (const twin of twins) {
733
- const pt = session.per_twin?.[twin];
734
- if (pt) {
735
- const label = twins.length > 1 ? `${twin} ` : "";
736
- const mcpUrl = mcpFromCloud ? ensureMcpSuffix(pt.mcp_url) : ensureMcpSuffix(pt.api_url);
737
- console.error(`${label}API: ${pt.api_url}`);
738
- console.error(`${label}MCP: ${mcpUrl}`);
739
- printedAny = true;
740
- }
741
- }
742
- if (!printedAny) {
743
- console.error(`Twin URL: ${session.twin_url}`);
744
- }
745
- console.error("Secrets redacted.");
746
- console.error(`Dashboard: ${DEFAULT_DASHBOARD_URL}/twins/${session.session_id}`);
747
- }
748
- async function writeSecretsFile(path, contents) {
749
- await mkdir(dirname(path), { recursive: true, mode: 448 });
750
- await writeFile(path, contents, { mode: 384 });
751
- await chmod(path, 384);
752
- }
753
- function displayState(serverState) {
754
- if (serverState === "ready") return "running";
755
- return serverState;
756
- }
757
- function matchesStateFilter(serverState, filter) {
758
- if (filter === "all") return true;
759
- if (filter === "running") return serverState === "ready" || serverState === "running";
760
- return serverState === filter;
761
- }
762
- async function runSessionList(opts) {
763
- const creds = await resolveCredentials({ apiBaseUrl: opts.apiBaseUrl });
764
- const client = createHostedClient({
765
- baseUrl: creds.apiBaseUrl,
766
- apiKey: creds.apiKey
767
- });
768
- const fetchLimit = opts.state === "all" ? opts.limit : Math.min(opts.limit * 4, 200);
769
- const all = await client.listSessions({ limit: fetchLimit });
770
- const filtered = all.filter((r) => matchesStateFilter(r.state, opts.state));
771
- const rows = filtered.slice(0, opts.limit);
772
- if (opts.json) {
773
- console.log(JSON.stringify(rows, null, 2));
774
- return;
775
- }
776
- if (rows.length === 0) {
777
- const scope = opts.state === "all" ? "any" : `state=${opts.state}`;
778
- console.error(`No sandboxes returned (${scope}).`);
779
- return;
780
- }
781
- for (const r of rows) {
782
- console.error(
783
- `${r.id} ${displayState(r.state)} ${r.twins.join(",")} expires ${r.expires_at}`
784
- );
785
- }
786
- }
787
- async function runSessionStop(opts) {
788
- const creds = await resolveCredentials({ apiBaseUrl: opts.apiBaseUrl });
789
- const client = createHostedClient({
790
- baseUrl: creds.apiBaseUrl,
791
- apiKey: creds.apiKey
792
- });
793
- try {
794
- await client.deleteSession(opts.sessionId, false, {
795
- discard: opts.discard === true
796
- });
797
- } catch (err) {
798
- if (err instanceof HostedDiscardRefusedError) {
799
- console.error(
800
- `Refused to stop ${err.sessionId}: it is still open (${err.state}), so its run has not been graded \u2014 Pome creates the run row at finalize, and stopping now discards it.`
801
- );
802
- if (err.taskName) console.error(` Task: ${err.taskName}`);
803
- console.error(` Open for ${err.openSeconds}s.`);
804
- console.error(
805
- ` To keep the run, finalize it instead. To discard it anyway: pome sandbox stop ${err.sessionId} --discard`
806
- );
807
- }
808
- throw err;
809
- }
810
- console.error(`Stopped sandbox ${opts.sessionId}.`);
811
- }
812
- function friendlyHostedError(err) {
813
- if (err instanceof HostedDiscardRefusedError) {
814
- return "";
815
- }
816
- if (err instanceof HostedAuthError) {
817
- return `${err.message} \xB7 Run \`pome login\` or set a valid POME_API_KEY.`;
818
- }
819
- if (err instanceof HostedQuotaError) {
820
- return `${err.message} \xB7 Quota or billing limit \u2014 check your team plan in the dashboard.`;
821
- }
822
- if (err instanceof HostedOrchError) {
823
- const m = err.message;
824
- if (/422|validation/i.test(m)) {
825
- return `${m} \xB7 Check twin name and request body.`;
826
- }
827
- if (/503|spawn/i.test(m)) {
828
- return `${m} \xB7 Control plane could not start the sandbox \u2014 retry later.`;
829
- }
830
- return m;
831
- }
832
- if (err instanceof Error) return err.message;
833
- return String(err);
834
- }
835
- function buildAgentEnv(params) {
836
- const {
837
- session,
838
- twins,
839
- perTwinFromCloud,
840
- prompt,
841
- otlpEndpoint,
842
- apiKey,
843
- agentId,
844
- runId,
845
- artifactsDir,
846
- slug,
847
- signalsPath
848
- } = params;
849
- const env = {
850
- POME_TASK: prompt,
851
- POME_TWIN_NAMES: twins.join(","),
852
- POME_OTEL_EXPORTER_OTLP_ENDPOINT: otlpEndpoint,
853
- POME_OTEL_EXPORTER_OTLP_HEADERS: `x-api-key=${apiKey}`,
854
- OTEL_SERVICE_NAME: agentId ?? "pome-agent",
855
- OTEL_RESOURCE_ATTRIBUTES: `pome.session_id=${session.session_id},pome.run_id=${runId}${agentId ? `,pome.agent_id=${agentId}` : ""}`,
856
- // The twin base URL, same value the self-host docs teach agents to fall
857
- // back on. Agent code written against the standalone docker twin
858
- // reads POME_TWIN_BASE_URL with a 127.0.0.1:3333 fallback; without this
859
- // injection that fallback fires on hosted runs and the agent probes a
860
- // loopback port nothing listens on. Legacy = the primary twin's bare url.
861
- POME_TWIN_BASE_URL: session.twin_url,
862
- // Legacy unconditional block (origin/main parity). Injected on EVERY hosted
863
- // run regardless of twin; the per-twin overlay below overwrites these in
864
- // place for github/stripe when those twins are in the session.
865
- POME_GITHUB_REST_URL: session.twin_url,
866
- POME_GITHUB_MCP_URL: `${session.twin_url}/mcp`,
867
- POME_GITHUB_TOKEN: session.provider_credentials.github?.token ?? session.agent_token,
868
- POME_STRIPE_API_BASE: session.twin_url,
869
- // Always use the JWT agent_token as the Stripe SDK's Bearer in hosted mode.
870
- // The cloud also returns `provider_credentials.stripe.api_key` (an
871
- // HMAC-signed `sk_test_pome_*`), but the pome-cloud proxy only accepts
872
- // JWTs — it routes by verifying the bearer against `agent_token`. A
873
- // stripe-style key short-circuits to the proxy's opaque 404 before the
874
- // request reaches the twin pod. The Stripe SDK sends any opaque string as
875
- // `Authorization: Bearer …`, and twin-stripe's dual-auth accepts JWTs
876
- // natively, so the JWT round-trips cleanly end-to-end.
877
- POME_STRIPE_API_KEY: session.agent_token,
878
- POME_AUTH_TOKEN: session.agent_token,
879
- POME_RUN_ID: runId,
880
- POME_ARTIFACTS_DIR: join(artifactsDir, slug, runId),
881
- POME_ADAPTER_SIGNALS_PATH: signalsPath
882
- };
883
- for (const twin of twins) {
884
- const upper = twin.toUpperCase();
885
- const pt = session.per_twin?.[twin];
886
- const apiUrl = pt?.api_url ?? session.twin_url;
887
- const mcpUrl = perTwinFromCloud && pt?.mcp_url ? ensureMcpSuffix(pt.mcp_url) : `${apiUrl.replace(/\/$/, "")}/mcp`;
888
- env[`POME_${upper}_REST_URL`] = apiUrl;
889
- env[`POME_${upper}_MCP_URL`] = mcpUrl;
890
- if (twin === "github") {
891
- env.POME_GITHUB_TOKEN = session.provider_credentials.github?.token ?? session.agent_token;
892
- } else if (twin === "stripe") {
893
- env.POME_STRIPE_API_BASE = apiUrl;
894
- env.POME_STRIPE_API_KEY = session.agent_token;
895
- } else if (twin === "slack") {
896
- env.POME_SLACK_TOKEN = session.agent_token;
897
- } else if (twin === "gmail") {
898
- env.POME_GMAIL_TOKEN = session.agent_token;
899
- }
900
- }
901
- return env;
902
- }
903
- async function runTaskHosted(options) {
904
- const scenario = await parseTaskFile(options.taskPath);
905
- const taskSource = await readFile(options.taskPath, "utf8");
906
- const taskHash = createHash("sha256").update(taskSource).digest("hex");
907
- const artifactsDir = options.artifactsDir ?? "runs";
908
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
909
- const client = options.client ?? createHostedClient({ baseUrl: options.hosted.baseUrl, apiKey: options.hosted.apiKey });
910
- const identity = await resolveRunAgentIdentity({
911
- startDir: dirname(options.taskPath),
912
- apiBaseUrl: options.hosted.baseUrl,
913
- agentVersionOverride: options.agentVersion
914
- });
915
- const agentId = identity.agentId;
916
- const agentVersion = identity.agentVersion;
917
- const agentSdk = identity.framework ?? null;
918
- const signalsDir = await mkdtemp(join(tmpdir(), "pome-signals-"));
919
- const signalsPath = join(signalsDir, `${randomUUID()}.jsonl`);
920
- await writeFile(signalsPath, "");
921
- const session = await createSessionOrExplain(
922
- client,
923
- options.premintedSession,
924
- {
925
- taskSource,
926
- twins: scenario.config.twins,
927
- agentId,
928
- agentVersion,
929
- seed: scenario.seedState
930
- }
931
- );
932
- const twins = scenario.config.twins;
933
- const primaryTwin = twins[0];
934
- const isMultiTwin = twins.length > 1;
935
- const twinApiUrl = (twin) => session.per_twin?.[twin]?.api_url ?? session.twin_url;
936
- const runId = session.session_id;
937
- try {
938
- const stateInitialByTwin = await fetchStateForTwins(
939
- client,
940
- twins,
941
- twinApiUrl,
942
- session.agent_token
943
- );
944
- const stateInitial = stateInitialByTwin[primaryTwin];
945
- const otlpEndpoint = process.env.POME_OTEL_COLLECTOR_URL?.trim() || new URL(
946
- `/v1/sessions/${session.session_id}/traces`,
947
- options.hosted.baseUrl
948
- ).toString();
949
- const env = buildAgentEnv({
950
- session,
951
- twins,
952
- perTwinFromCloud: perTwinReturnedByCloud(session),
953
- prompt: scenario.prompt,
954
- otlpEndpoint,
955
- apiKey: options.hosted.apiKey,
956
- agentId,
957
- runId,
958
- artifactsDir,
959
- slug: scenario.slug,
960
- signalsPath
961
- });
962
- const preflightTimeoutSeconds = Math.min(10, scenario.config.timeout);
963
- const preflight = await runAgentCommand({
964
- command: options.agentCommand,
965
- env,
966
- timeoutSeconds: preflightTimeoutSeconds,
967
- preflight: true
968
- });
969
- let agentResult = preflight;
970
- if (preflight.exitCode === 0) {
971
- await writeFile(signalsPath, "");
972
- agentResult = await runAgentCommand({
973
- command: options.agentCommand,
974
- env,
975
- timeoutSeconds: scenario.config.timeout
976
- });
977
- }
978
- if (options.abandonOnFailure && (agentResult.timedOut || agentResult.exitCode !== 0)) {
979
- const failed = preflight.exitCode !== 0 ? preflight : agentResult;
980
- const cause = stderrTail(failed.stderr);
981
- const failure = preflight.exitCode !== 0 ? {
982
- errorCode: "preflight_failed",
983
- reason: preflight.timedOut ? `agent preflight timed out after ${preflightTimeoutSeconds}s` : `agent preflight failed${cause ? ` \u2014 ${cause}` : ""}`
984
- } : agentResult.timedOut ? {
985
- errorCode: "agent_timeout",
986
- reason: `agent timed out after ${scenario.config.timeout}s`
987
- } : {
988
- errorCode: "agent_exit_nonzero",
989
- reason: `agent exited non-zero (exit ${agentResult.exitCode})${cause ? ` \u2014 ${cause}` : ""}`
990
- };
991
- try {
992
- const runDir = join(artifactsDir, scenario.slug, runId);
993
- await mkdir(runDir, { recursive: true });
994
- await writeFile(join(runDir, "stdout.txt"), redactSecrets(failed.stdout));
995
- await writeFile(join(runDir, "stderr.log"), redactSecrets(failed.stderr));
996
- } catch {
997
- }
998
- await abandonBestEffort(client, session.session_id, failure.errorCode);
999
- throw new HostedTrialError(failure.reason, failure.errorCode);
1000
- }
1001
- const stateFinalByTwin = {};
1002
- const eventsByTwin = {};
1003
- await Promise.all(
1004
- twins.map(async (twin) => {
1005
- const [sf, ev] = await Promise.all([
1006
- client.fetchState({ twinUrl: twinApiUrl(twin), agentToken: session.agent_token }),
1007
- client.fetchEvents({ twinUrl: twinApiUrl(twin), agentToken: session.agent_token })
1008
- ]);
1009
- stateFinalByTwin[twin] = sf;
1010
- eventsByTwin[twin] = isMultiTwin ? ev.map((e) => tagEventTwin(e, twin)) : ev;
1011
- })
1012
- );
1013
- const stateFinal = stateFinalByTwin[primaryTwin];
1014
- const events = isMultiTwin ? mergeEventsByTimestamp(twins.flatMap((t) => eventsByTwin[t] ?? [])) : eventsByTwin[primaryTwin] ?? [];
1015
- const legacyEvents = events;
1016
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
1017
- const durationMs = Date.parse(completedAt) - Date.parse(startedAt);
1018
- const artifacts = await writeRunArtifactsCore({
1019
- artifactsDir,
1020
- runId,
1021
- scenario,
1022
- startedAt,
1023
- completedAt,
1024
- stdout: agentResult.stdout,
1025
- stderr: agentResult.stderr,
1026
- exitCode: agentResult.exitCode,
1027
- events: legacyEvents,
1028
- stateInitial,
1029
- stateFinal
1030
- });
1031
- if (isMultiTwin) {
1032
- for (const twin of twins) {
1033
- if (twin === primaryTwin) continue;
1034
- try {
1035
- await writeFile(
1036
- join(artifacts.runDir, `state_final.${twin}.json`),
1037
- `${JSON.stringify(redactSecrets(stateFinalByTwin[twin]), null, 2)}
1038
- `
1039
- );
1040
- } catch (err) {
1041
- console.warn(
1042
- `[pome] state_final.${twin}.json not written (${err instanceof Error ? err.message : String(err)})`
1043
- );
1044
- }
1045
- }
1046
- }
1047
- const eventsJsonl = legacyEvents.map((e) => JSON.stringify(redactEvent(toTwinHttpEvent(e)))).join("\n") + "\n";
1048
- const stateInitialJson = JSON.stringify(redactSecrets(stateInitial));
1049
- const stateFinalJson = JSON.stringify(redactSecrets(stateFinal));
1050
- let signalsJsonl = "";
1051
- try {
1052
- signalsJsonl = redactJsonl(
1053
- await readFile(signalsPath, "utf8").catch(() => "")
1054
- );
1055
- } catch (err) {
1056
- console.warn(
1057
- `[pome] signals.jsonl upload skipped (${err instanceof Error ? err.message : String(err)}); continuing with signals_storage_key=null`
1058
- );
1059
- }
1060
- const metaJson = await readFile(join(artifacts.runDir, "meta.json"), "utf8").catch(
1061
- () => "{}"
1062
- );
1063
- const perTwinState = isMultiTwin ? Object.fromEntries(
1064
- twins.map((twin) => [
1065
- twin,
1066
- {
1067
- initialJson: JSON.stringify(redactSecrets(stateInitialByTwin[twin])),
1068
- finalJson: JSON.stringify(redactSecrets(stateFinalByTwin[twin]))
1069
- }
1070
- ])
1071
- ) : void 0;
1072
- const uploaded = await uploadRunBlobs(client, session.session_id, {
1073
- eventsJsonl,
1074
- stateInitialJson,
1075
- stateFinalJson,
1076
- signalsJsonl,
1077
- metaJson,
1078
- twins: isMultiTwin ? twins : void 0,
1079
- perTwinState
1080
- });
1081
- const eventsJsonlUrl = uploaded.eventsKey;
1082
- const stateKeys = {
1083
- initialKey: uploaded.stateInitialKey,
1084
- finalKey: uploaded.stateFinalKey
1085
- };
1086
- const signalsKey = uploaded.signalsKey;
1087
- const criteriaDefs = scenario.criteria.map((c, idx) => ({
1088
- id: `crit_${idx}`,
1089
- text: c.text,
1090
- kind: c.type,
1091
- ...c.twin ? { twin: c.twin } : {},
1092
- ...c.alwaysScored ? { always_scored: true } : {}
1093
- }));
1094
- const stopReason = agentResult.timedOut ? "agent_timeout" : agentResult.exitCode === 0 ? "agent_exit_0" : "agent_exit_nonzero";
1095
- const finalized = await client.finalize(session.session_id, {
1096
- stopReason,
1097
- exitCode: agentResult.exitCode ?? 0,
1098
- durationMs,
1099
- agentModel: options.agentModel ?? "unknown",
1100
- agentSdk,
1101
- criteria: criteriaDefs,
1102
- taskName: scenario.slug,
1103
- taskHash,
1104
- taskPrompt: scenario.prompt,
1105
- expectedBehavior: scenario.expectedBehavior,
1106
- // Explicit overrides when uploads succeeded; otherwise let cloud fall
1107
- // back. For events.jsonl the conventional `team-<>/session-<>/events.jsonl`
1108
- // path matches what /result-upload-url signs, so omitting traceStorageKey
1109
- // still resolves correctly. For state blobs there is no conventional
1110
- // fallback today — omitting them means cloud's judge sees
1111
- // "{}", which we accept as best-effort degradation.
1112
- traceStorageKey: eventsJsonlUrl ?? void 0,
1113
- stateInitialStorageKey: stateKeys.initialKey ?? void 0,
1114
- stateFinalStorageKey: stateKeys.finalKey ?? void 0,
1115
- signalsStorageKey: signalsKey ?? void 0,
1116
- // Multi-twin (M3): per-twin state storage keys (>=1 key per entry).
1117
- // Undefined for single-twin / an older cloud that returned no per_twin
1118
- // upload block — the cloud then scores the primary twin from the flat keys.
1119
- perTwinStateKeys: uploaded.perTwinStateKeys
1120
- });
1121
- const score = scoreFromFinalizeResponse(finalized);
1122
- const verdict = scoreStatus(score, scenario.config.passThreshold);
1123
- const exitCode = verdict === "pass" ? 0 : 1;
1124
- try {
1125
- const counts = evaluationCounts(score);
1126
- await writeVerdictArtifact(artifacts.runDir, {
1127
- version: VERDICT_ARTIFACT_VERSION,
1128
- source: "cloud-finalize",
1129
- task_name: scenario.slug,
1130
- task_path: options.taskPath,
1131
- group_id: options.groupId ?? null,
1132
- session_id: session.session_id,
1133
- cloud_run_id: finalized.run_id,
1134
- cloud_dashboard_url: finalized.dashboard_url,
1135
- judge_model: score.judge_model,
1136
- score: finalized.score,
1137
- pass_threshold: scenario.config.passThreshold,
1138
- state: verdict,
1139
- passed: exitCode === 0,
1140
- evaluated: counts.evaluated,
1141
- not_evaluated: counts.notEvaluated,
1142
- pre_satisfied: counts.preSatisfied,
1143
- total: counts.total,
1144
- criteria_results: score.results,
1145
- duration_ms: durationMs,
1146
- finalized_at: (/* @__PURE__ */ new Date()).toISOString()
1147
- });
1148
- } catch (err) {
1149
- console.warn(
1150
- `[pome] verdict.json not written (${err instanceof Error ? err.message : String(err)}); pome fix-prompt won't see this trial`
1151
- );
1152
- }
1153
- return {
1154
- scenario,
1155
- runId,
1156
- cloudRunId: finalized.run_id,
1157
- cloudDashboardUrl: finalized.dashboard_url,
1158
- artifacts,
1159
- score,
1160
- verdict,
1161
- exitCode,
1162
- durationMs
1163
- };
1164
- } catch (err) {
1165
- if (options.abandonOnFailure && !(err instanceof HostedTrialError)) {
1166
- await abandonBestEffort(client, session.session_id, "trial_crashed");
1167
- }
1168
- throw err;
1169
- } finally {
1170
- await client.deleteSession(session.session_id, true, { discard: true }).catch(() => void 0);
1171
- await rm(signalsDir, { recursive: true, force: true }).catch(() => void 0);
1172
- }
1173
- }
1174
- async function createSessionOrExplain(client, preminted, input) {
1175
- if (preminted) return preminted;
1176
- try {
1177
- return await client.createSession(input);
1178
- } catch (err) {
1179
- if (err instanceof HostedOrchError && err.status === 422 && (err.type === "multi_twin_unsupported" || /multi_twin_unsupported/i.test(err.message))) {
1180
- throw new HostedOrchError(
1181
- "this pome cloud does not support multi-twin sessions yet",
1182
- err.requestId,
1183
- err.status,
1184
- err.type
1185
- );
1186
- }
1187
- throw err;
1188
- }
1189
- }
1190
- async function fetchStateForTwins(client, twins, twinApiUrl, agentToken) {
1191
- const byTwin = {};
1192
- await Promise.all(
1193
- twins.map(async (twin) => {
1194
- byTwin[twin] = await client.fetchState({
1195
- twinUrl: twinApiUrl(twin),
1196
- agentToken
1197
- });
1198
- })
1199
- );
1200
- return byTwin;
1201
- }
1202
- function tagEventTwin(event, twin) {
1203
- if (event && typeof event === "object") {
1204
- const existing = event.twin;
1205
- if (existing === void 0 || existing === null || existing === "") {
1206
- return { ...event, twin };
1207
- }
1208
- }
1209
- return event;
1210
- }
1211
- function mergeEventsByTimestamp(events) {
1212
- const tsOf = (e) => {
1213
- const ts = e.ts;
1214
- return typeof ts === "string" ? ts : "";
1215
- };
1216
- return events.map((event, index) => ({ event, index })).sort((a, b) => {
1217
- const ta = tsOf(a.event);
1218
- const tb = tsOf(b.event);
1219
- if (ta < tb) return -1;
1220
- if (ta > tb) return 1;
1221
- return a.index - b.index;
1222
- }).map(({ event }) => event);
1223
- }
1224
- function stderrTail(stderr) {
1225
- const last = stderr.split("\n").map((line) => line.trim()).filter(Boolean).at(-1);
1226
- if (!last) return null;
1227
- const flat = redactSecrets(last.replace(/\s+/g, " ")).trim();
1228
- return flat.length > 160 ? `${flat.slice(0, 157)}\u2026` : flat;
1229
- }
1230
- async function abandonBestEffort(client, sessionId, errorCode) {
1231
- try {
1232
- await client.abandonSession(sessionId, { errorCode });
1233
- } catch (err) {
1234
- console.warn(
1235
- `[pome] session abandon skipped (${err instanceof Error ? err.message : String(err)}); the expiry sweeper will close ${sessionId}`
1236
- );
1237
- }
1238
- }
1239
-
1240
- export { DEFAULT_CONTROL_PLANE_URL, DEFAULT_DASHBOARD_URL, DEFAULT_DOCS_SITE_ORIGIN, SESSION_TWIN_NAMES, clearLocalCredentials, discoverRunSet, ensurePomeGitignored, friendlyHostedError, persistCredentialsAfterLogin, postAgentResolver, readLinkCache, resolveCachedAgentId, resolveCredentials, resolveRunAgentIdentity, resolveSeams, runSessionCreate, runSessionList, runSessionStop, runTaskHosted, writeLinkCache };