@pome-sh/cli 0.42.4 → 0.43.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.
@@ -1,19 +1,24 @@
1
+ import { readSeedFileText, parseSeedFileText, soleTwinOf, twinsNamedBy, seedsForTwins } from './chunk-3YCX3KUL.js';
1
2
  import { gmailSeedSchema, defaultSeedState } from './chunk-PASFBRK4.js';
2
3
  import { linearSeedSchema, defaultSeedState as defaultSeedState$1 } from './chunk-3FZY376K.js';
3
- import { isTwinName, TWIN_REGISTRY } from './chunk-RPPF2KDQ.js';
4
- import { criterionSchema, normalizeTaskConfigKeys, finalizeResponseSchema, HostedDiscardRefusedError, HostedOrchError, HostedAuthError, HostedQuotaError, submitResultResponseSchema, createEvalSessionResponseSchema, createSessionResponseSchema, sessionPublicSchema } from './chunk-NNXVR46L.js';
4
+ import { isTwinName, TWIN_REGISTRY } from './chunk-ELIEDNF3.js';
5
+ import { criterionSchema, normalizeTaskConfigKeys, finalizeResponseSchema, MOUNTED_TWINS, HostedDiscardRefusedError, HostedOrchError, HostedAuthError, readManifest, normalizeManifestTwins, HostedQuotaError, HostedTrialError, submitResultResponseSchema, createEvalSessionResponseSchema, createSessionResponseSchema, agentResponseSchema, isMultiTwinSeedEnvelope, sessionPublicSchema } from './chunk-CS7O2ZXB.js';
5
6
  import { seedSchema as seedSchema$1, parseSeed, defaultSeedState as defaultSeedState$2 } from './chunk-NBOQN5VX.js';
6
7
  import { seedSchema } from './chunk-YBWG5JK2.js';
7
8
  import { toTwinHttpEventRow } from './chunk-HRAD7MRX.js';
8
9
  import { redactEvent, redactSecrets } from './chunk-SG6ZTIMT.js';
9
10
  import { seedSchema as seedSchema$2 } from './chunk-2K6BJ3PI.js';
10
- import { mkdir, appendFile, writeFile, readFile } from 'node:fs/promises';
11
+ import { mkdir, appendFile, writeFile, readFile, rm, stat, chmod, mkdtemp, readdir, rename } from 'node:fs/promises';
11
12
  import { existsSync } from 'node:fs';
12
- import { join, basename, extname } from 'node:path';
13
+ import { join, basename, extname, dirname } from 'node:path';
13
14
  import { parse } from 'yaml';
14
15
  import { z } from 'zod';
16
+ import { randomUUID, createHash } from 'node:crypto';
17
+ import { tmpdir, homedir } from 'node:os';
18
+ import { execFile, spawn } from 'node:child_process';
19
+ import { promisify } from 'node:util';
20
+ import { createInterface } from 'node:readline';
15
21
  import { gzipSync } from 'node:zlib';
16
- import { spawn } from 'node:child_process';
17
22
 
18
23
  // src/recorder/specMeta.ts
19
24
  var META_SPEC_VERSION = 1;
@@ -497,6 +502,189 @@ function stripFence(input) {
497
502
  function slugFromPath(path) {
498
503
  return basename(path, extname(path)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
499
504
  }
505
+
506
+ // src/hosted/runSets.ts
507
+ function groupRunSets(trials) {
508
+ const byKey = /* @__PURE__ */ new Map();
509
+ for (const trial of trials) {
510
+ const key = trial.verdict.group_id ?? `solo:${trial.verdict.session_id}`;
511
+ const bucket = byKey.get(key);
512
+ if (bucket) bucket.push(trial);
513
+ else byKey.set(key, [trial]);
514
+ }
515
+ const sets = [];
516
+ for (const bucket of byKey.values()) {
517
+ bucket.sort(
518
+ (a, b) => a.verdict.finalized_at.localeCompare(b.verdict.finalized_at)
519
+ );
520
+ const last = bucket[bucket.length - 1];
521
+ const hasFailed = bucket.some((t) => t.verdict.state === "fail");
522
+ const allPassed = bucket.every((t) => t.verdict.state === "pass");
523
+ sets.push({
524
+ groupId: bucket[0].verdict.group_id,
525
+ taskName: bucket[0].verdict.task_name,
526
+ taskPath: bucket[0].verdict.task_path,
527
+ trials: bucket,
528
+ latestFinalizedAt: last.verdict.finalized_at,
529
+ outcome: hasFailed ? "fail" : allPassed ? "pass" : "incomplete"
530
+ });
531
+ }
532
+ sets.sort((a, b) => a.latestFinalizedAt.localeCompare(b.latestFinalizedAt));
533
+ return sets;
534
+ }
535
+ function latestFailedRunSet(sets) {
536
+ for (let i = sets.length - 1; i >= 0; i -= 1) {
537
+ if (sets[i].outcome === "fail") return sets[i];
538
+ }
539
+ return null;
540
+ }
541
+ function latestIncompleteRunSet(sets) {
542
+ for (let i = sets.length - 1; i >= 0; i -= 1) {
543
+ if (sets[i].outcome === "incomplete") return sets[i];
544
+ }
545
+ return null;
546
+ }
547
+
548
+ // src/hosted/evalResultCache.ts
549
+ var VERDICT_ARTIFACT_VERSION = 2;
550
+ var VERDICT_FILENAME = "verdict.json";
551
+ var VALID_STATES = /* @__PURE__ */ new Set([
552
+ "pass",
553
+ "fail",
554
+ "incomplete"
555
+ ]);
556
+ async function writeVerdictArtifact(runDir, verdict) {
557
+ const tmpPath = join(runDir, `.${VERDICT_FILENAME}.${process.pid}.${randomUUID()}.tmp`);
558
+ try {
559
+ await writeFile(tmpPath, `${JSON.stringify(verdict, null, 2)}
560
+ `, "utf8");
561
+ await rename(tmpPath, join(runDir, VERDICT_FILENAME));
562
+ } catch (err) {
563
+ await rm(tmpPath, { force: true }).catch(() => {
564
+ });
565
+ throw err;
566
+ }
567
+ }
568
+ function isVerdictArtifact(parsed) {
569
+ if (typeof parsed !== "object" || parsed === null) return false;
570
+ const v = parsed;
571
+ if (v.version !== VERDICT_ARTIFACT_VERSION) return false;
572
+ if (v.source !== "cloud-finalize") return false;
573
+ if (typeof v.session_id !== "string") return false;
574
+ if (typeof v.task_name !== "string") return false;
575
+ if (typeof v.task_path !== "string") return false;
576
+ if (v.group_id !== null && typeof v.group_id !== "string") return false;
577
+ if (typeof v.finalized_at !== "string") return false;
578
+ if (typeof v.passed !== "boolean") return false;
579
+ if (typeof v.score !== "number") return false;
580
+ if (typeof v.state !== "string" || !VALID_STATES.has(v.state)) return false;
581
+ if (typeof v.evaluated !== "number") return false;
582
+ if (typeof v.not_evaluated !== "number") return false;
583
+ if (typeof v.pre_satisfied !== "number") return false;
584
+ if (typeof v.total !== "number") return false;
585
+ if (!Array.isArray(v.criteria_results)) return false;
586
+ return v.criteria_results.every((r) => {
587
+ if (typeof r !== "object" || r === null) return false;
588
+ const result = r;
589
+ const criterion = result.criterion;
590
+ return typeof criterion === "object" && criterion !== null && typeof criterion.text === "string" && typeof result.reason === "string" && typeof result.passed === "boolean" && typeof result.skipped === "boolean";
591
+ });
592
+ }
593
+ var MISSING_ERROR_CODES = /* @__PURE__ */ new Set([
594
+ "ENOENT",
595
+ "ENOTDIR",
596
+ "ELOOP",
597
+ "ENAMETOOLONG"
598
+ ]);
599
+ function isMissingFileError(err) {
600
+ const code = err?.code;
601
+ return typeof code === "string" && MISSING_ERROR_CODES.has(code);
602
+ }
603
+ async function readVerdictArtifactDetailed(runDir) {
604
+ const path = join(runDir, VERDICT_FILENAME);
605
+ let raw;
606
+ try {
607
+ raw = await readFile(path, "utf8");
608
+ } catch (err) {
609
+ return isMissingFileError(err) ? { status: "missing" } : { status: "unreadable" };
610
+ }
611
+ let parsed;
612
+ try {
613
+ parsed = JSON.parse(raw);
614
+ } catch {
615
+ return { status: "unreadable" };
616
+ }
617
+ if (!isVerdictArtifact(parsed)) return { status: "unreadable" };
618
+ return { status: "ok", trial: { runDir, verdict: parsed } };
619
+ }
620
+ async function scanVerdictArtifactsDetailed(artifactsRoot) {
621
+ const trials = [];
622
+ const unreadableDirs = [];
623
+ let slugs;
624
+ try {
625
+ slugs = await readdir(artifactsRoot);
626
+ } catch {
627
+ return { trials, unreadableDirs };
628
+ }
629
+ for (const slug of slugs) {
630
+ const slugDir = join(artifactsRoot, slug);
631
+ let runIds;
632
+ try {
633
+ runIds = await readdir(slugDir);
634
+ } catch {
635
+ continue;
636
+ }
637
+ for (const runId of runIds) {
638
+ const runDir = join(slugDir, runId);
639
+ const result = await readVerdictArtifactDetailed(runDir);
640
+ if (result.status === "ok") trials.push(result.trial);
641
+ else if (result.status === "unreadable") unreadableDirs.push(runDir);
642
+ }
643
+ }
644
+ unreadableDirs.sort();
645
+ return { trials, unreadableDirs };
646
+ }
647
+ async function discoverRunSet(target) {
648
+ const anchorResult = await readVerdictArtifactDetailed(target);
649
+ if (anchorResult.status === "unreadable") {
650
+ return {
651
+ kind: "trial-dir",
652
+ set: null,
653
+ incompleteSet: null,
654
+ totalSets: 0,
655
+ unreadableCount: 1,
656
+ unreadablePaths: [target]
657
+ };
658
+ }
659
+ if (anchorResult.status === "ok") {
660
+ const anchor = anchorResult.trial;
661
+ const root = join(target, "..", "..");
662
+ const { trials: trials2, unreadableDirs: unreadableDirs2 } = await scanVerdictArtifactsDetailed(root);
663
+ const sets2 = groupRunSets(trials2);
664
+ const own = sets2.find(
665
+ (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
666
+ ) ?? groupRunSets([anchor])[0];
667
+ return {
668
+ kind: "trial-dir",
669
+ set: own,
670
+ incompleteSet: null,
671
+ totalSets: Math.max(sets2.length, 1),
672
+ unreadableCount: unreadableDirs2.length,
673
+ unreadablePaths: unreadableDirs2
674
+ };
675
+ }
676
+ const { trials, unreadableDirs } = await scanVerdictArtifactsDetailed(target);
677
+ const sets = groupRunSets(trials);
678
+ const failedSet = latestFailedRunSet(sets);
679
+ return {
680
+ kind: "root",
681
+ set: failedSet,
682
+ incompleteSet: failedSet ? null : latestIncompleteRunSet(sets),
683
+ totalSets: sets.length,
684
+ unreadableCount: unreadableDirs.length,
685
+ unreadablePaths: unreadableDirs
686
+ };
687
+ }
500
688
  var PER_TWIN_PROVENANCE = /* @__PURE__ */ Symbol("pome.perTwinFromCloud");
501
689
  function markPerTwinProvenance(session, fromCloud) {
502
690
  Object.defineProperty(session, PER_TWIN_PROVENANCE, {
@@ -1215,6 +1403,645 @@ function normalizeAgentSdk(value) {
1215
1403
  const trimmed = value.trim();
1216
1404
  return trimmed.length > 0 ? trimmed : null;
1217
1405
  }
1406
+ var execFileAsync = promisify(execFile);
1407
+ var SERVICE = "sh.pome.cli";
1408
+ var ACCOUNT = "hosted";
1409
+ function keychainSupported() {
1410
+ if (process.env.POME_CLI_DISABLE_KEYCHAIN === "1") return false;
1411
+ return process.platform === "darwin";
1412
+ }
1413
+ async function writeKeychainCredentials(creds) {
1414
+ if (!keychainSupported()) return;
1415
+ const payload = JSON.stringify({
1416
+ api_key: creds.api_key,
1417
+ api_url: creds.api_url,
1418
+ dashboard_url: creds.dashboard_url,
1419
+ team_id: creds.team_id,
1420
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1421
+ });
1422
+ await execFileAsync("security", [
1423
+ "add-generic-password",
1424
+ "-U",
1425
+ "-s",
1426
+ SERVICE,
1427
+ "-a",
1428
+ ACCOUNT,
1429
+ "-w",
1430
+ payload
1431
+ ]);
1432
+ }
1433
+ async function readKeychainCredentials() {
1434
+ if (!keychainSupported()) return null;
1435
+ try {
1436
+ const { stdout } = await execFileAsync("security", [
1437
+ "find-generic-password",
1438
+ "-s",
1439
+ SERVICE,
1440
+ "-a",
1441
+ ACCOUNT,
1442
+ "-w"
1443
+ ]);
1444
+ const text = stdout.trim();
1445
+ if (!text) return null;
1446
+ const parsed = JSON.parse(text);
1447
+ if (typeof parsed.api_key !== "string" || !parsed.api_key.trim()) {
1448
+ return null;
1449
+ }
1450
+ return parsed;
1451
+ } catch {
1452
+ return null;
1453
+ }
1454
+ }
1455
+ async function deleteKeychainCredentials() {
1456
+ if (!keychainSupported()) return false;
1457
+ try {
1458
+ await execFileAsync("security", [
1459
+ "delete-generic-password",
1460
+ "-s",
1461
+ SERVICE,
1462
+ "-a",
1463
+ ACCOUNT
1464
+ ]);
1465
+ return true;
1466
+ } catch {
1467
+ return false;
1468
+ }
1469
+ }
1470
+
1471
+ // src/cli/credentials.ts
1472
+ var DEFAULT_PATH = () => join(homedir(), ".pome", "credentials.json");
1473
+ async function persistCredentialsAfterLogin(credentials, path = DEFAULT_PATH()) {
1474
+ if (keychainSupported()) {
1475
+ try {
1476
+ await writeKeychainCredentials(credentials);
1477
+ try {
1478
+ await rm(path, { force: true });
1479
+ } catch {
1480
+ }
1481
+ return { stored: "keychain" };
1482
+ } catch {
1483
+ }
1484
+ }
1485
+ await writeCredentialsFile(credentials, path);
1486
+ return { stored: "file", path };
1487
+ }
1488
+ async function clearLocalCredentials(path = DEFAULT_PATH()) {
1489
+ await deleteKeychainCredentials();
1490
+ try {
1491
+ await rm(path, { force: true });
1492
+ } catch {
1493
+ }
1494
+ }
1495
+ async function resolveCredentials(input) {
1496
+ const envKey = process.env.POME_API_KEY;
1497
+ if (envKey && envKey.trim().length > 0) {
1498
+ const apiBaseUrl = resolveApiBaseUrl(input.apiBaseUrl, void 0);
1499
+ return { apiKey: envKey.trim(), apiBaseUrl };
1500
+ }
1501
+ const keychain = await readKeychainCredentials();
1502
+ if (keychain && typeof keychain.api_key === "string" && keychain.api_key.trim().length > 0) {
1503
+ return {
1504
+ apiKey: keychain.api_key.trim(),
1505
+ apiBaseUrl: resolveApiBaseUrl(input.apiBaseUrl, keychain.api_url),
1506
+ teamId: normalizeTeamId(keychain.team_id)
1507
+ };
1508
+ }
1509
+ const path = input.credentialsPath ?? DEFAULT_PATH();
1510
+ let raw;
1511
+ try {
1512
+ await assertSafeCredentialFileMode(path);
1513
+ raw = await readFile(path, "utf8");
1514
+ } catch (err) {
1515
+ if (err instanceof Error && err.code === "ENOENT") {
1516
+ throw new HostedAuthError(
1517
+ `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}.`
1518
+ );
1519
+ }
1520
+ throw err;
1521
+ }
1522
+ let parsed;
1523
+ try {
1524
+ parsed = JSON.parse(raw);
1525
+ } catch {
1526
+ throw new Error(`${path} is not valid JSON.`);
1527
+ }
1528
+ if (typeof parsed.api_key !== "string" || parsed.api_key.trim().length === 0) {
1529
+ throw new Error(`${path} is missing "api_key".`);
1530
+ }
1531
+ return {
1532
+ apiKey: parsed.api_key.trim(),
1533
+ apiBaseUrl: resolveApiBaseUrl(input.apiBaseUrl, parsed.api_url),
1534
+ teamId: normalizeTeamId(parsed.team_id)
1535
+ };
1536
+ }
1537
+ function normalizeTeamId(raw) {
1538
+ if (typeof raw !== "string") return void 0;
1539
+ const trimmed = raw.trim();
1540
+ return trimmed.length > 0 ? trimmed : void 0;
1541
+ }
1542
+ function resolveApiBaseUrl(callerInput, storedApiUrl) {
1543
+ const candidate = callerInput ?? storedApiUrl;
1544
+ if (typeof candidate !== "string" || candidate.trim().length === 0) {
1545
+ throw new Error(
1546
+ "Refusing to resolve hosted credentials: no apiBaseUrl supplied and no stored api_url found."
1547
+ );
1548
+ }
1549
+ return normalizeBaseUrl(candidate);
1550
+ }
1551
+ async function assertSafeCredentialFileMode(path) {
1552
+ const st = await stat(path);
1553
+ const perm = st.mode & 511;
1554
+ if ((perm & 63) !== 0) {
1555
+ throw new Error(
1556
+ `Refusing to read credentials at ${path}: mode 0${perm.toString(8)} is too permissive (group/other bits must be 0). Run: chmod 600 ${path}`
1557
+ );
1558
+ }
1559
+ }
1560
+ async function writeCredentialsFile(credentials, path = DEFAULT_PATH()) {
1561
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
1562
+ await writeFile(
1563
+ path,
1564
+ `${JSON.stringify(
1565
+ {
1566
+ ...credentials,
1567
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1568
+ },
1569
+ null,
1570
+ 2
1571
+ )}
1572
+ `,
1573
+ { mode: 384 }
1574
+ );
1575
+ await chmod(path, 384);
1576
+ }
1577
+ function normalizeBaseUrl(raw) {
1578
+ return new URL(raw).toString().replace(/\/$/, "");
1579
+ }
1580
+ function resolveSeams(opts) {
1581
+ return {
1582
+ stdinIsTTY: opts.stdinIsTTY ?? Boolean(process.stdin.isTTY),
1583
+ confirm: opts.confirm ?? promptYesNo,
1584
+ ask: opts.ask ?? promptLine
1585
+ };
1586
+ }
1587
+ async function requestAgent(creds, body) {
1588
+ const res = await fetch(`${creds.apiBaseUrl}/v1/agents`, {
1589
+ method: "POST",
1590
+ headers: { "x-api-key": creds.apiKey, "content-type": "application/json" },
1591
+ body: JSON.stringify(prunePostBody(body))
1592
+ }).catch((err) => {
1593
+ throw new HostedOrchError(err instanceof Error ? err.message : "network error");
1594
+ });
1595
+ const text = await res.text();
1596
+ let json = {};
1597
+ try {
1598
+ json = text.length ? JSON.parse(text) : {};
1599
+ } catch {
1600
+ throw new HostedOrchError(`non-JSON response (status ${res.status})`);
1601
+ }
1602
+ return { ok: res.ok, status: res.status, json };
1603
+ }
1604
+ function prunePostBody(body) {
1605
+ const out = { name: body.name };
1606
+ if (body.slug !== void 0) out.slug = body.slug;
1607
+ if (body.description !== void 0) out.description = body.description;
1608
+ if (body.version !== void 0) out.version = body.version;
1609
+ if (body.framework !== void 0) out.framework = body.framework;
1610
+ if (body.twins && body.twins.length > 0) out.twins = body.twins;
1611
+ if (body.confirm !== void 0) out.confirm = body.confirm;
1612
+ return out;
1613
+ }
1614
+ function parseOkAgent(json) {
1615
+ const parsed = agentResponseSchema.safeParse(json);
1616
+ if (!parsed.success) {
1617
+ throw new HostedOrchError("POST /v1/agents returned an unexpected shape");
1618
+ }
1619
+ return parsed.data;
1620
+ }
1621
+ function mapHttpError(status, json) {
1622
+ const err = json.error;
1623
+ if (status === 401 || status === 403) {
1624
+ return new HostedAuthError(err?.message ?? `HTTP ${status}`);
1625
+ }
1626
+ if (status === 404) {
1627
+ return new HostedOrchError(
1628
+ err?.message ?? "POST /v1/agents returned 404 \u2014 check that --api-url/POME_API_URL points at a Pome control plane."
1629
+ );
1630
+ }
1631
+ return new HostedOrchError(err?.message ?? `POST /v1/agents \u2192 HTTP ${status}`);
1632
+ }
1633
+ function nearMissSuggestion(json) {
1634
+ const error = json.error;
1635
+ if (error?.type !== "conflict") return void 0;
1636
+ const details = error.details;
1637
+ if (!details) return void 0;
1638
+ const candidate = details.suggestion ?? details.slug ?? (Array.isArray(details.suggestions) ? details.suggestions[0] : void 0);
1639
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
1640
+ }
1641
+ async function postAgentResolver(creds, body, seams) {
1642
+ const first = await requestAgent(creds, body);
1643
+ if (first.ok) return parseOkAgent(first.json);
1644
+ if (first.status === 409) {
1645
+ const suggestion = nearMissSuggestion(first.json);
1646
+ if (suggestion) {
1647
+ const interactive = seams.stdinIsTTY && !process.env.CI;
1648
+ let retry;
1649
+ if (interactive) {
1650
+ const yes = await seams.confirm(
1651
+ `An agent named "${suggestion}" already exists on your team. Did you mean "${suggestion}"? [y/N] `
1652
+ );
1653
+ retry = yes ? { ...body, slug: suggestion } : { ...body, confirm: true };
1654
+ } else {
1655
+ console.error(
1656
+ `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).`
1657
+ );
1658
+ retry = { ...body, confirm: true };
1659
+ }
1660
+ const second = await requestAgent(creds, retry);
1661
+ if (second.ok) return parseOkAgent(second.json);
1662
+ throw mapHttpError(second.status, second.json);
1663
+ }
1664
+ }
1665
+ throw mapHttpError(first.status, first.json);
1666
+ }
1667
+ async function promptYesNo(question) {
1668
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1669
+ try {
1670
+ const answer = await new Promise((resolve) => rl.question(question, resolve));
1671
+ return /^y(es)?$/i.test(answer.trim());
1672
+ } finally {
1673
+ rl.close();
1674
+ }
1675
+ }
1676
+ async function promptLine(question) {
1677
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1678
+ try {
1679
+ return (await new Promise((resolve) => rl.question(question, resolve))).trim();
1680
+ } finally {
1681
+ rl.close();
1682
+ }
1683
+ }
1684
+ var POME_DIR = ".pome";
1685
+ var LINK_FILE = "link.json";
1686
+ var GITIGNORE_ENTRY = `${POME_DIR}/`;
1687
+ function linkPath(projectDir) {
1688
+ return join(projectDir, POME_DIR, LINK_FILE);
1689
+ }
1690
+ async function readLinkCache(projectDir) {
1691
+ let text;
1692
+ try {
1693
+ text = await readFile(linkPath(projectDir), "utf8");
1694
+ } catch (err) {
1695
+ if (err.code === "ENOENT") return null;
1696
+ return null;
1697
+ }
1698
+ let parsed;
1699
+ try {
1700
+ parsed = JSON.parse(text);
1701
+ } catch {
1702
+ return null;
1703
+ }
1704
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.agent_id !== "string" || typeof parsed.team_id !== "string") {
1705
+ return null;
1706
+ }
1707
+ const { agent_id, team_id } = parsed;
1708
+ const resolved_at = typeof parsed.resolved_at === "string" ? parsed.resolved_at : "";
1709
+ return { agent_id, team_id, resolved_at };
1710
+ }
1711
+ async function writeLinkCache(projectDir, entry) {
1712
+ await mkdir(join(projectDir, POME_DIR), { recursive: true });
1713
+ const body = {
1714
+ agent_id: entry.agent_id,
1715
+ team_id: entry.team_id,
1716
+ resolved_at: (/* @__PURE__ */ new Date()).toISOString()
1717
+ };
1718
+ await writeFile(linkPath(projectDir), `${JSON.stringify(body, null, 2)}
1719
+ `);
1720
+ }
1721
+ function resolveCachedAgentId(cache, callerTeamId) {
1722
+ if (!cache || !callerTeamId) return void 0;
1723
+ return cache.team_id === callerTeamId ? cache.agent_id : void 0;
1724
+ }
1725
+ async function ensurePomeGitignored(projectDir) {
1726
+ const path = join(projectDir, ".gitignore");
1727
+ let existing;
1728
+ try {
1729
+ existing = await readFile(path, "utf8");
1730
+ } catch (err) {
1731
+ if (err.code !== "ENOENT") throw err;
1732
+ existing = null;
1733
+ }
1734
+ if (existing === null) {
1735
+ await writeFile(path, `${GITIGNORE_ENTRY}
1736
+ `);
1737
+ return;
1738
+ }
1739
+ const alreadyIgnored = existing.split(/\r?\n/).some((line) => line.trim() === GITIGNORE_ENTRY || line.trim() === POME_DIR);
1740
+ if (alreadyIgnored) return;
1741
+ const separator = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
1742
+ await writeFile(path, `${existing}${separator}${GITIGNORE_ENTRY}
1743
+ `);
1744
+ }
1745
+
1746
+ // src/cli/agent-identity.ts
1747
+ async function resolveRunAgentIdentity(input) {
1748
+ const manifestRead = await readManifest(input.startDir);
1749
+ if (!manifestRead) {
1750
+ return input.agentVersionOverride ? { agentVersion: input.agentVersionOverride } : {};
1751
+ }
1752
+ const { agent } = manifestRead.manifest;
1753
+ const agentVersion = input.agentVersionOverride ?? agent.version;
1754
+ const base = {
1755
+ agentVersion,
1756
+ framework: agent.framework,
1757
+ agentSlug: agent.slug
1758
+ };
1759
+ const projectDir = dirname(manifestRead.path);
1760
+ try {
1761
+ const creds = await resolveCredentials({
1762
+ apiBaseUrl: input.apiBaseUrl,
1763
+ credentialsPath: input.credentialsPath
1764
+ });
1765
+ const cachedId = resolveCachedAgentId(await readLinkCache(projectDir), creds.teamId);
1766
+ if (cachedId) {
1767
+ return { ...base, agentId: cachedId };
1768
+ }
1769
+ const resolved = await postAgentResolver(
1770
+ creds,
1771
+ {
1772
+ name: agent.name ?? agent.slug,
1773
+ slug: agent.slug,
1774
+ description: agent.description,
1775
+ version: agent.version,
1776
+ framework: agent.framework,
1777
+ twins: normalizeManifestTwins(manifestRead.manifest.twins)
1778
+ },
1779
+ resolveSeams({ stdinIsTTY: false })
1780
+ );
1781
+ if (creds.teamId) {
1782
+ await writeLinkCache(projectDir, { agent_id: resolved.id, team_id: creds.teamId });
1783
+ await ensurePomeGitignored(projectDir);
1784
+ }
1785
+ return { ...base, agentId: resolved.id };
1786
+ } catch (err) {
1787
+ console.error(
1788
+ `pome: could not resolve agent identity (${err instanceof Error ? err.message : String(err)}); running unattributed.`
1789
+ );
1790
+ return base;
1791
+ }
1792
+ }
1793
+
1794
+ // src/cli/defaults.ts
1795
+ var DEFAULT_CONTROL_PLANE_URL = "https://api.pome.sh";
1796
+ var DEFAULT_DASHBOARD_URL = "https://app.pome.sh";
1797
+ var DEFAULT_DOCS_SITE_ORIGIN = "https://docs.pome.sh";
1798
+ function ensureMcpSuffix(url) {
1799
+ return /\/mcp\/?$/.test(url) ? url : `${url.replace(/\/$/, "")}/mcp`;
1800
+ }
1801
+ var ALLOWED_TWINS = new Set(MOUNTED_TWINS);
1802
+ var SESSION_TWIN_NAMES = [...ALLOWED_TWINS];
1803
+ function redactSession(res) {
1804
+ const pcIn = res.provider_credentials;
1805
+ const pcOut = {};
1806
+ if (pcIn.github) {
1807
+ pcOut.github = { ...pcIn.github, token: "***redacted***" };
1808
+ }
1809
+ if (pcIn.stripe) {
1810
+ pcOut.stripe = { ...pcIn.stripe, api_key: "***redacted***" };
1811
+ }
1812
+ if (pcIn.slack) {
1813
+ pcOut.slack = { ...pcIn.slack, token: "***redacted***" };
1814
+ }
1815
+ return {
1816
+ session_id: res.session_id,
1817
+ session_token: res.session_token ? "***redacted***" : res.session_id,
1818
+ twin_url: res.twin_url,
1819
+ expires_at: res.expires_at,
1820
+ openapi_url: res.openapi_url,
1821
+ per_twin: res.per_twin,
1822
+ provider_credentials: pcOut,
1823
+ agent_token: "***redacted***"
1824
+ };
1825
+ }
1826
+ function formatEnvExport(res, twins) {
1827
+ const lines = [
1828
+ `# Pome hosted sandbox \u2014 treat as secret. Twins: ${twins.join(", ")}`,
1829
+ `export POME_AUTH_TOKEN=${JSON.stringify(res.agent_token)}`,
1830
+ `export POME_SESSION_ID=${JSON.stringify(res.session_id)}`,
1831
+ // Legacy single-endpoint URL (= the primary twin's api_url). Kept for
1832
+ // agents written against the pre-multi-twin contract.
1833
+ `export POME_TWIN_URL=${JSON.stringify(res.twin_url)}`,
1834
+ `export POME_TWIN_NAMES=${JSON.stringify(twins.join(","))}`
1835
+ ];
1836
+ const fromCloud = perTwinReturnedByCloud(res);
1837
+ for (const twin of twins) {
1838
+ const upper = twin.toUpperCase();
1839
+ const pt = res.per_twin?.[twin];
1840
+ if (pt?.api_url) {
1841
+ const mcpUrl = fromCloud ? ensureMcpSuffix(pt.mcp_url) : ensureMcpSuffix(pt.api_url);
1842
+ lines.push(`export POME_${upper}_REST_URL=${JSON.stringify(pt.api_url)}`);
1843
+ lines.push(
1844
+ `export POME_${upper}_MCP_URL=${JSON.stringify(mcpUrl)}`
1845
+ );
1846
+ }
1847
+ if (twin === "github") {
1848
+ const gh = res.provider_credentials.github;
1849
+ if (gh) {
1850
+ lines.push(`export POME_GITHUB_TOKEN=${JSON.stringify(gh.token)}`);
1851
+ }
1852
+ } else if (twin === "stripe") {
1853
+ const st = res.provider_credentials.stripe;
1854
+ if (st) {
1855
+ lines.push(`export POME_STRIPE_API_KEY=${JSON.stringify(st.api_key)}`);
1856
+ }
1857
+ if (pt?.api_url) {
1858
+ lines.push(`export POME_STRIPE_API_BASE=${JSON.stringify(pt.api_url)}`);
1859
+ }
1860
+ } else if (twin === "slack") {
1861
+ lines.push(`export POME_SLACK_TOKEN=${JSON.stringify(res.agent_token)}`);
1862
+ } else if (twin === "gmail") {
1863
+ lines.push(`export POME_GMAIL_TOKEN=${JSON.stringify(res.agent_token)}`);
1864
+ }
1865
+ }
1866
+ return `${lines.join("\n")}
1867
+ `;
1868
+ }
1869
+ function normalizeSessionTwins(raw) {
1870
+ const twins = raw.map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0);
1871
+ for (const twin of twins) {
1872
+ if (!ALLOWED_TWINS.has(twin)) {
1873
+ throw new Error(
1874
+ `Unknown twin "${twin}". Supported: ${[...ALLOWED_TWINS].join(", ")}.`
1875
+ );
1876
+ }
1877
+ }
1878
+ const deduped = [...new Set(twins)];
1879
+ if (deduped.length === 0) {
1880
+ throw new Error(
1881
+ `No twin specified. Pass --twin <name> (repeat for multi-twin). Supported: ${[...ALLOWED_TWINS].join(", ")}.`
1882
+ );
1883
+ }
1884
+ return deduped;
1885
+ }
1886
+ async function resolveSessionSeed(seedPath, requestedTwins) {
1887
+ const raw = readSeedFileText(seedPath, "pome sandbox create --seed");
1888
+ const origin = `--seed ${seedPath}`;
1889
+ const file = parseSeedFileText(raw, origin);
1890
+ const sole = soleTwinOf(file);
1891
+ if (requestedTwins.length === 0 && sole === void 0) {
1892
+ const named = twinsNamedBy(file);
1893
+ throw new Error(
1894
+ 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}`
1895
+ );
1896
+ }
1897
+ const twins = normalizeSessionTwins(
1898
+ requestedTwins.length > 0 ? requestedTwins : [sole]
1899
+ );
1900
+ const byTwin = await seedsForTwins(file, twins, origin);
1901
+ return {
1902
+ twins,
1903
+ seed: isMultiTwinSeedEnvelope(twins) ? byTwin : byTwin[twins[0]]
1904
+ };
1905
+ }
1906
+ async function runSessionCreate(opts) {
1907
+ const resolvedSeed = opts.seedPath === void 0 ? void 0 : await resolveSessionSeed(opts.seedPath, opts.twins);
1908
+ const twins = resolvedSeed?.twins ?? normalizeSessionTwins(opts.twins);
1909
+ const creds = await resolveCredentials({ apiBaseUrl: opts.apiBaseUrl });
1910
+ const client = createHostedClient({
1911
+ baseUrl: creds.apiBaseUrl,
1912
+ apiKey: creds.apiKey
1913
+ });
1914
+ const identity = await resolveRunAgentIdentity({
1915
+ startDir: process.cwd(),
1916
+ apiBaseUrl: creds.apiBaseUrl
1917
+ });
1918
+ const session = await client.createSession({
1919
+ twins,
1920
+ taskSource: "# ..\n",
1921
+ idempotencyKey: randomUUID(),
1922
+ agentId: identity.agentId,
1923
+ agentVersion: identity.agentVersion,
1924
+ ...resolvedSeed ? { seed: resolvedSeed.seed } : {}
1925
+ });
1926
+ if (opts.secretsFile) {
1927
+ await writeSecretsFile(opts.secretsFile, formatEnvExport(session, twins));
1928
+ console.error(`Wrote sandbox secrets to ${opts.secretsFile} (mode 0600).`);
1929
+ }
1930
+ if (opts.json) {
1931
+ const payload = redactSession(session);
1932
+ console.log(JSON.stringify(payload, null, 2));
1933
+ return;
1934
+ }
1935
+ console.error(`Sandbox: ${session.session_id}`);
1936
+ console.error(`Expires: ${session.expires_at}`);
1937
+ if (opts.seedPath !== void 0) {
1938
+ console.error(`Seed: ${opts.seedPath} (replaces the default for ${twins.join(", ")}).`);
1939
+ }
1940
+ let printedAny = false;
1941
+ const mcpFromCloud = perTwinReturnedByCloud(session);
1942
+ for (const twin of twins) {
1943
+ const pt = session.per_twin?.[twin];
1944
+ if (pt) {
1945
+ const label = twins.length > 1 ? `${twin} ` : "";
1946
+ const mcpUrl = mcpFromCloud ? ensureMcpSuffix(pt.mcp_url) : ensureMcpSuffix(pt.api_url);
1947
+ console.error(`${label}API: ${pt.api_url}`);
1948
+ console.error(`${label}MCP: ${mcpUrl}`);
1949
+ printedAny = true;
1950
+ }
1951
+ }
1952
+ if (!printedAny) {
1953
+ console.error(`Twin URL: ${session.twin_url}`);
1954
+ }
1955
+ console.error("Secrets redacted.");
1956
+ console.error(`Dashboard: ${DEFAULT_DASHBOARD_URL}/twins/${session.session_id}`);
1957
+ }
1958
+ async function writeSecretsFile(path, contents) {
1959
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
1960
+ await writeFile(path, contents, { mode: 384 });
1961
+ await chmod(path, 384);
1962
+ }
1963
+ function displayState(serverState) {
1964
+ if (serverState === "ready") return "running";
1965
+ return serverState;
1966
+ }
1967
+ function matchesStateFilter(serverState, filter) {
1968
+ if (filter === "all") return true;
1969
+ if (filter === "running") return serverState === "ready" || serverState === "running";
1970
+ return serverState === filter;
1971
+ }
1972
+ async function runSessionList(opts) {
1973
+ const creds = await resolveCredentials({ apiBaseUrl: opts.apiBaseUrl });
1974
+ const client = createHostedClient({
1975
+ baseUrl: creds.apiBaseUrl,
1976
+ apiKey: creds.apiKey
1977
+ });
1978
+ const fetchLimit = opts.state === "all" ? opts.limit : Math.min(opts.limit * 4, 200);
1979
+ const all = await client.listSessions({ limit: fetchLimit });
1980
+ const filtered = all.filter((r) => matchesStateFilter(r.state, opts.state));
1981
+ const rows = filtered.slice(0, opts.limit);
1982
+ if (opts.json) {
1983
+ console.log(JSON.stringify(rows, null, 2));
1984
+ return;
1985
+ }
1986
+ if (rows.length === 0) {
1987
+ const scope = opts.state === "all" ? "any" : `state=${opts.state}`;
1988
+ console.error(`No sandboxes returned (${scope}).`);
1989
+ return;
1990
+ }
1991
+ for (const r of rows) {
1992
+ console.error(
1993
+ `${r.id} ${displayState(r.state)} ${r.twins.join(",")} expires ${r.expires_at}`
1994
+ );
1995
+ }
1996
+ }
1997
+ async function runSessionStop(opts) {
1998
+ const creds = await resolveCredentials({ apiBaseUrl: opts.apiBaseUrl });
1999
+ const client = createHostedClient({
2000
+ baseUrl: creds.apiBaseUrl,
2001
+ apiKey: creds.apiKey
2002
+ });
2003
+ try {
2004
+ await client.deleteSession(opts.sessionId, false, {
2005
+ discard: opts.discard === true
2006
+ });
2007
+ } catch (err) {
2008
+ if (err instanceof HostedDiscardRefusedError) {
2009
+ console.error(
2010
+ `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.`
2011
+ );
2012
+ if (err.taskName) console.error(` Task: ${err.taskName}`);
2013
+ console.error(` Open for ${err.openSeconds}s.`);
2014
+ console.error(
2015
+ ` To keep the run, finalize it instead. To discard it anyway: pome sandbox stop ${err.sessionId} --discard`
2016
+ );
2017
+ }
2018
+ throw err;
2019
+ }
2020
+ console.error(`Stopped sandbox ${opts.sessionId}.`);
2021
+ }
2022
+ function friendlyHostedError(err) {
2023
+ if (err instanceof HostedDiscardRefusedError) {
2024
+ return "";
2025
+ }
2026
+ if (err instanceof HostedAuthError) {
2027
+ return `${err.message} \xB7 Run \`pome login\` or set a valid POME_API_KEY.`;
2028
+ }
2029
+ if (err instanceof HostedQuotaError) {
2030
+ return `${err.message} \xB7 Quota or billing limit \u2014 check your team plan in the dashboard.`;
2031
+ }
2032
+ if (err instanceof HostedOrchError) {
2033
+ const m = err.message;
2034
+ if (/422|validation/i.test(m)) {
2035
+ return `${m} \xB7 Check twin name and request body.`;
2036
+ }
2037
+ if (/503|spawn/i.test(m)) {
2038
+ return `${m} \xB7 Control plane could not start the sandbox \u2014 retry later.`;
2039
+ }
2040
+ return m;
2041
+ }
2042
+ if (err instanceof Error) return err.message;
2043
+ return String(err);
2044
+ }
1218
2045
 
1219
2046
  // ../packages/wire/dist/run-completeness.js
1220
2047
  var PRE_SATISFIED_REASON = "already_true_in_seed";
@@ -1330,36 +2157,186 @@ function runScoreLine(score, passThreshold, unevaluatedNumericLabel) {
1330
2157
  }
1331
2158
  return `score: ${score.satisfaction}/100`;
1332
2159
  }
1333
- async function putBlob(url, body, contentType, label) {
1334
- const ctrl = new AbortController();
1335
- const timer = setTimeout(() => ctrl.abort(), 3e4);
1336
- try {
1337
- const putRes = await fetch(url, {
1338
- method: "PUT",
1339
- headers: {
1340
- "content-type": contentType,
1341
- "content-encoding": "gzip"
1342
- },
1343
- body: gzipSync(Buffer.from(body, "utf8")),
1344
- signal: ctrl.signal
1345
- });
1346
- if (!putRes.ok) {
1347
- console.warn(
1348
- `[pome] ${label} upload PUT ${url} \u2192 ${putRes.status}; continuing with key=null`
1349
- );
1350
- return false;
1351
- }
1352
- return true;
1353
- } finally {
1354
- clearTimeout(timer);
1355
- }
1356
- }
1357
- async function uploadRunBlobs(client, sessionId, blobs) {
1358
- async function uploadEvents() {
1359
- try {
1360
- const upload = await client.requestEventsUploadUrl(sessionId);
1361
- const ok = await putBlob(
1362
- upload.url,
2160
+ async function runAgentCommand(input) {
2161
+ return new Promise((resolve) => {
2162
+ const inheritAllEnv = process.env.POME_INHERIT_AGENT_ENV === "1";
2163
+ const useShell = process.env.POME_TRUST_AGENT_COMMAND === "1";
2164
+ const childEnv = buildAgentEnv(input.env, inheritAllEnv, Boolean(input.preflight));
2165
+ const command = useShell ? null : splitCommand(input.command);
2166
+ if (!useShell && !command) {
2167
+ resolve({
2168
+ stdout: "",
2169
+ stderr: "Could not parse agent command. Use POME_TRUST_AGENT_COMMAND=1 to run it through a shell.\n",
2170
+ exitCode: 2,
2171
+ timedOut: false
2172
+ });
2173
+ return;
2174
+ }
2175
+ if (useShell) {
2176
+ console.warn("[pome] POME_TRUST_AGENT_COMMAND=1: running agent command through a shell.");
2177
+ }
2178
+ if (inheritAllEnv) {
2179
+ console.warn("[pome] POME_INHERIT_AGENT_ENV=1: passing the full parent environment to the agent.");
2180
+ }
2181
+ const child = useShell ? spawn(input.command, {
2182
+ shell: true,
2183
+ stdio: ["ignore", "pipe", "pipe"],
2184
+ env: childEnv
2185
+ }) : spawn(command.file, command.args, {
2186
+ shell: false,
2187
+ stdio: ["ignore", "pipe", "pipe"],
2188
+ env: childEnv
2189
+ });
2190
+ let stdout = "";
2191
+ let stderr = "";
2192
+ let timedOut = false;
2193
+ let settled = false;
2194
+ const timer = setTimeout(() => {
2195
+ timedOut = true;
2196
+ child.kill("SIGTERM");
2197
+ setTimeout(() => child.kill("SIGKILL"), 1e3).unref();
2198
+ }, input.timeoutSeconds * 1e3);
2199
+ child.stdout?.on("data", (chunk) => {
2200
+ stdout += chunk.toString();
2201
+ });
2202
+ child.stderr?.on("data", (chunk) => {
2203
+ stderr += chunk.toString();
2204
+ });
2205
+ child.on("error", (error) => {
2206
+ if (settled) return;
2207
+ settled = true;
2208
+ clearTimeout(timer);
2209
+ resolve({
2210
+ stdout,
2211
+ stderr: `${stderr}${error.message}
2212
+ `,
2213
+ exitCode: 2,
2214
+ timedOut
2215
+ });
2216
+ });
2217
+ child.on("close", (exitCode) => {
2218
+ if (settled) return;
2219
+ settled = true;
2220
+ clearTimeout(timer);
2221
+ resolve({ stdout, stderr, exitCode, timedOut });
2222
+ });
2223
+ });
2224
+ }
2225
+ var SAFE_PARENT_ENV = /* @__PURE__ */ new Set([
2226
+ "PATH",
2227
+ "HOME",
2228
+ "USERPROFILE",
2229
+ "SystemRoot",
2230
+ "COMSPEC",
2231
+ "TMPDIR",
2232
+ "TEMP",
2233
+ "TMP",
2234
+ "LANG",
2235
+ "LC_ALL"
2236
+ ]);
2237
+ var DEFAULT_AGENT_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
2238
+ "AI_GATEWAY_API_KEY",
2239
+ "ANTHROPIC_API_KEY",
2240
+ // Claude subscription auth (`claude setup-token`). Without
2241
+ // this the Claude Agent SDK inside the agent subprocess only sees an API
2242
+ // key, and subscription-only users fail auth under `pome run` while the
2243
+ // same agent works when launched by hand.
2244
+ "CLAUDE_CODE_OAUTH_TOKEN",
2245
+ "GOOGLE_API_KEY",
2246
+ "GOOGLE_GENERATIVE_AI_API_KEY",
2247
+ "OPENAI_API_KEY",
2248
+ "OPENROUTER_API_KEY"
2249
+ ]);
2250
+ function buildAgentEnv(inputEnv, inheritAll, preflight) {
2251
+ const base = {};
2252
+ if (inheritAll) {
2253
+ Object.assign(base, process.env);
2254
+ } else {
2255
+ for (const key of [...SAFE_PARENT_ENV, ...DEFAULT_AGENT_ENV_ALLOWLIST]) {
2256
+ const value = process.env[key];
2257
+ if (value !== void 0) base[key] = value;
2258
+ }
2259
+ for (const key of (process.env.POME_AGENT_ENV_ALLOWLIST ?? "").split(",")) {
2260
+ const trimmed = key.trim();
2261
+ if (!trimmed) continue;
2262
+ const value = process.env[trimmed];
2263
+ if (value !== void 0) base[trimmed] = value;
2264
+ }
2265
+ }
2266
+ return {
2267
+ ...base,
2268
+ ...inputEnv,
2269
+ ...preflight ? { POME_PREFLIGHT: "1" } : {}
2270
+ };
2271
+ }
2272
+ function splitCommand(command) {
2273
+ const parts = [];
2274
+ let current = "";
2275
+ let quote = null;
2276
+ let escaping = false;
2277
+ for (const char of command.trim()) {
2278
+ if (escaping) {
2279
+ current += char;
2280
+ escaping = false;
2281
+ continue;
2282
+ }
2283
+ if (char === "\\") {
2284
+ escaping = true;
2285
+ continue;
2286
+ }
2287
+ if (quote) {
2288
+ if (char === quote) quote = null;
2289
+ else current += char;
2290
+ continue;
2291
+ }
2292
+ if (char === "'" || char === '"') {
2293
+ quote = char;
2294
+ continue;
2295
+ }
2296
+ if (/\s/.test(char)) {
2297
+ if (current.length > 0) {
2298
+ parts.push(current);
2299
+ current = "";
2300
+ }
2301
+ continue;
2302
+ }
2303
+ current += char;
2304
+ }
2305
+ if (escaping || quote) return null;
2306
+ if (current.length > 0) parts.push(current);
2307
+ const [file, ...args] = parts;
2308
+ return file ? { file, args } : null;
2309
+ }
2310
+ async function putBlob(url, body, contentType, label) {
2311
+ const ctrl = new AbortController();
2312
+ const timer = setTimeout(() => ctrl.abort(), 3e4);
2313
+ try {
2314
+ const putRes = await fetch(url, {
2315
+ method: "PUT",
2316
+ headers: {
2317
+ "content-type": contentType,
2318
+ "content-encoding": "gzip"
2319
+ },
2320
+ body: gzipSync(Buffer.from(body, "utf8")),
2321
+ signal: ctrl.signal
2322
+ });
2323
+ if (!putRes.ok) {
2324
+ console.warn(
2325
+ `[pome] ${label} upload PUT ${url} \u2192 ${putRes.status}; continuing with key=null`
2326
+ );
2327
+ return false;
2328
+ }
2329
+ return true;
2330
+ } finally {
2331
+ clearTimeout(timer);
2332
+ }
2333
+ }
2334
+ async function uploadRunBlobs(client, sessionId, blobs) {
2335
+ async function uploadEvents() {
2336
+ try {
2337
+ const upload = await client.requestEventsUploadUrl(sessionId);
2338
+ const ok = await putBlob(
2339
+ upload.url,
1363
2340
  blobs.eventsJsonl,
1364
2341
  "application/x-ndjson",
1365
2342
  "events.jsonl"
@@ -1535,155 +2512,411 @@ function redactJsonl(body) {
1535
2512
  return redacted.length > 0 ? `${redacted.join("\n")}
1536
2513
  ` : "";
1537
2514
  }
1538
- async function runAgentCommand(input) {
1539
- return new Promise((resolve) => {
1540
- const inheritAllEnv = process.env.POME_INHERIT_AGENT_ENV === "1";
1541
- const useShell = process.env.POME_TRUST_AGENT_COMMAND === "1";
1542
- const childEnv = buildAgentEnv(input.env, inheritAllEnv, Boolean(input.preflight));
1543
- const command = useShell ? null : splitCommand(input.command);
1544
- if (!useShell && !command) {
1545
- resolve({
1546
- stdout: "",
1547
- stderr: "Could not parse agent command. Use POME_TRUST_AGENT_COMMAND=1 to run it through a shell.\n",
1548
- exitCode: 2,
1549
- timedOut: false
1550
- });
1551
- return;
1552
- }
1553
- if (useShell) {
1554
- console.warn("[pome] POME_TRUST_AGENT_COMMAND=1: running agent command through a shell.");
2515
+
2516
+ // src/runner/runTaskHosted.ts
2517
+ function buildAgentEnv2(params) {
2518
+ const {
2519
+ session,
2520
+ twins,
2521
+ perTwinFromCloud,
2522
+ prompt,
2523
+ otlpEndpoint,
2524
+ apiKey,
2525
+ agentId,
2526
+ runId,
2527
+ artifactsDir,
2528
+ slug,
2529
+ signalsPath
2530
+ } = params;
2531
+ const env = {
2532
+ POME_TASK: prompt,
2533
+ POME_TWIN_NAMES: twins.join(","),
2534
+ POME_OTEL_EXPORTER_OTLP_ENDPOINT: otlpEndpoint,
2535
+ POME_OTEL_EXPORTER_OTLP_HEADERS: `x-api-key=${apiKey}`,
2536
+ OTEL_SERVICE_NAME: agentId ?? "pome-agent",
2537
+ OTEL_RESOURCE_ATTRIBUTES: `pome.session_id=${session.session_id},pome.run_id=${runId}${agentId ? `,pome.agent_id=${agentId}` : ""}`,
2538
+ // The twin base URL, same value the self-host docs teach agents to fall
2539
+ // back on. Agent code written against the standalone docker twin
2540
+ // reads POME_TWIN_BASE_URL with a 127.0.0.1:3333 fallback; without this
2541
+ // injection that fallback fires on hosted runs and the agent probes a
2542
+ // loopback port nothing listens on. Legacy = the primary twin's bare url.
2543
+ POME_TWIN_BASE_URL: session.twin_url,
2544
+ // Legacy unconditional block (origin/main parity). Injected on EVERY hosted
2545
+ // run regardless of twin; the per-twin overlay below overwrites these in
2546
+ // place for github/stripe when those twins are in the session.
2547
+ POME_GITHUB_REST_URL: session.twin_url,
2548
+ POME_GITHUB_MCP_URL: `${session.twin_url}/mcp`,
2549
+ POME_GITHUB_TOKEN: session.provider_credentials.github?.token ?? session.agent_token,
2550
+ POME_STRIPE_API_BASE: session.twin_url,
2551
+ // Always use the JWT agent_token as the Stripe SDK's Bearer in hosted mode.
2552
+ // The cloud also returns `provider_credentials.stripe.api_key` (an
2553
+ // HMAC-signed `sk_test_pome_*`), but the pome-cloud proxy only accepts
2554
+ // JWTs — it routes by verifying the bearer against `agent_token`. A
2555
+ // stripe-style key short-circuits to the proxy's opaque 404 before the
2556
+ // request reaches the twin pod. The Stripe SDK sends any opaque string as
2557
+ // `Authorization: Bearer …`, and twin-stripe's dual-auth accepts JWTs
2558
+ // natively, so the JWT round-trips cleanly end-to-end.
2559
+ POME_STRIPE_API_KEY: session.agent_token,
2560
+ POME_AUTH_TOKEN: session.agent_token,
2561
+ POME_RUN_ID: runId,
2562
+ POME_ARTIFACTS_DIR: join(artifactsDir, slug, runId),
2563
+ POME_ADAPTER_SIGNALS_PATH: signalsPath
2564
+ };
2565
+ for (const twin of twins) {
2566
+ const upper = twin.toUpperCase();
2567
+ const pt = session.per_twin?.[twin];
2568
+ const apiUrl = pt?.api_url ?? session.twin_url;
2569
+ const mcpUrl = perTwinFromCloud && pt?.mcp_url ? ensureMcpSuffix(pt.mcp_url) : `${apiUrl.replace(/\/$/, "")}/mcp`;
2570
+ env[`POME_${upper}_REST_URL`] = apiUrl;
2571
+ env[`POME_${upper}_MCP_URL`] = mcpUrl;
2572
+ if (twin === "github") {
2573
+ env.POME_GITHUB_TOKEN = session.provider_credentials.github?.token ?? session.agent_token;
2574
+ } else if (twin === "stripe") {
2575
+ env.POME_STRIPE_API_BASE = apiUrl;
2576
+ env.POME_STRIPE_API_KEY = session.agent_token;
2577
+ } else if (twin === "slack") {
2578
+ env.POME_SLACK_TOKEN = session.agent_token;
2579
+ } else if (twin === "gmail") {
2580
+ env.POME_GMAIL_TOKEN = session.agent_token;
1555
2581
  }
1556
- if (inheritAllEnv) {
1557
- console.warn("[pome] POME_INHERIT_AGENT_ENV=1: passing the full parent environment to the agent.");
2582
+ }
2583
+ return env;
2584
+ }
2585
+ async function runTaskHosted(options) {
2586
+ const scenario = await parseTaskFile(options.taskPath);
2587
+ const taskSource = await readFile(options.taskPath, "utf8");
2588
+ const taskHash = createHash("sha256").update(taskSource).digest("hex");
2589
+ const artifactsDir = options.artifactsDir ?? "runs";
2590
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2591
+ const client = options.client ?? createHostedClient({ baseUrl: options.hosted.baseUrl, apiKey: options.hosted.apiKey });
2592
+ const identity = await resolveRunAgentIdentity({
2593
+ startDir: dirname(options.taskPath),
2594
+ apiBaseUrl: options.hosted.baseUrl,
2595
+ agentVersionOverride: options.agentVersion
2596
+ });
2597
+ const agentId = identity.agentId;
2598
+ const agentVersion = identity.agentVersion;
2599
+ const agentSdk = identity.framework ?? null;
2600
+ const signalsDir = await mkdtemp(join(tmpdir(), "pome-signals-"));
2601
+ const signalsPath = join(signalsDir, `${randomUUID()}.jsonl`);
2602
+ await writeFile(signalsPath, "");
2603
+ const session = await createSessionOrExplain(
2604
+ client,
2605
+ options.premintedSession,
2606
+ {
2607
+ taskSource,
2608
+ twins: scenario.config.twins,
2609
+ agentId,
2610
+ agentVersion,
2611
+ seed: scenario.seedState
1558
2612
  }
1559
- const child = useShell ? spawn(input.command, {
1560
- shell: true,
1561
- stdio: ["ignore", "pipe", "pipe"],
1562
- env: childEnv
1563
- }) : spawn(command.file, command.args, {
1564
- shell: false,
1565
- stdio: ["ignore", "pipe", "pipe"],
1566
- env: childEnv
1567
- });
1568
- let stdout = "";
1569
- let stderr = "";
1570
- let timedOut = false;
1571
- let settled = false;
1572
- const timer = setTimeout(() => {
1573
- timedOut = true;
1574
- child.kill("SIGTERM");
1575
- setTimeout(() => child.kill("SIGKILL"), 1e3).unref();
1576
- }, input.timeoutSeconds * 1e3);
1577
- child.stdout?.on("data", (chunk) => {
1578
- stdout += chunk.toString();
2613
+ );
2614
+ const twins = scenario.config.twins;
2615
+ const primaryTwin = twins[0];
2616
+ const isMultiTwin = twins.length > 1;
2617
+ const twinApiUrl = (twin) => session.per_twin?.[twin]?.api_url ?? session.twin_url;
2618
+ const runId = session.session_id;
2619
+ try {
2620
+ const stateInitialByTwin = await fetchStateForTwins(
2621
+ client,
2622
+ twins,
2623
+ twinApiUrl,
2624
+ session.agent_token
2625
+ );
2626
+ const stateInitial = stateInitialByTwin[primaryTwin];
2627
+ const otlpEndpoint = process.env.POME_OTEL_COLLECTOR_URL?.trim() || new URL(
2628
+ `/v1/sessions/${session.session_id}/traces`,
2629
+ options.hosted.baseUrl
2630
+ ).toString();
2631
+ const env = buildAgentEnv2({
2632
+ session,
2633
+ twins,
2634
+ perTwinFromCloud: perTwinReturnedByCloud(session),
2635
+ prompt: scenario.prompt,
2636
+ otlpEndpoint,
2637
+ apiKey: options.hosted.apiKey,
2638
+ agentId,
2639
+ runId,
2640
+ artifactsDir,
2641
+ slug: scenario.slug,
2642
+ signalsPath
1579
2643
  });
1580
- child.stderr?.on("data", (chunk) => {
1581
- stderr += chunk.toString();
2644
+ const preflightTimeoutSeconds = Math.min(10, scenario.config.timeout);
2645
+ const preflight = await runAgentCommand({
2646
+ command: options.agentCommand,
2647
+ env,
2648
+ timeoutSeconds: preflightTimeoutSeconds,
2649
+ preflight: true
1582
2650
  });
1583
- child.on("error", (error) => {
1584
- if (settled) return;
1585
- settled = true;
1586
- clearTimeout(timer);
1587
- resolve({
1588
- stdout,
1589
- stderr: `${stderr}${error.message}
1590
- `,
1591
- exitCode: 2,
1592
- timedOut
2651
+ let agentResult = preflight;
2652
+ if (preflight.exitCode === 0) {
2653
+ await writeFile(signalsPath, "");
2654
+ agentResult = await runAgentCommand({
2655
+ command: options.agentCommand,
2656
+ env,
2657
+ timeoutSeconds: scenario.config.timeout
1593
2658
  });
2659
+ }
2660
+ if (options.abandonOnFailure && (agentResult.timedOut || agentResult.exitCode !== 0)) {
2661
+ const failed = preflight.exitCode !== 0 ? preflight : agentResult;
2662
+ const cause = stderrTail(failed.stderr);
2663
+ const failure = preflight.exitCode !== 0 ? {
2664
+ errorCode: "preflight_failed",
2665
+ reason: preflight.timedOut ? `agent preflight timed out after ${preflightTimeoutSeconds}s` : `agent preflight failed${cause ? ` \u2014 ${cause}` : ""}`
2666
+ } : agentResult.timedOut ? {
2667
+ errorCode: "agent_timeout",
2668
+ reason: `agent timed out after ${scenario.config.timeout}s`
2669
+ } : {
2670
+ errorCode: "agent_exit_nonzero",
2671
+ reason: `agent exited non-zero (exit ${agentResult.exitCode})${cause ? ` \u2014 ${cause}` : ""}`
2672
+ };
2673
+ try {
2674
+ const runDir = join(artifactsDir, scenario.slug, runId);
2675
+ await mkdir(runDir, { recursive: true });
2676
+ await writeFile(join(runDir, "stdout.txt"), redactSecrets(failed.stdout));
2677
+ await writeFile(join(runDir, "stderr.log"), redactSecrets(failed.stderr));
2678
+ } catch {
2679
+ }
2680
+ await abandonBestEffort(client, session.session_id, failure.errorCode);
2681
+ throw new HostedTrialError(failure.reason, failure.errorCode);
2682
+ }
2683
+ const stateFinalByTwin = {};
2684
+ const eventsByTwin = {};
2685
+ await Promise.all(
2686
+ twins.map(async (twin) => {
2687
+ const [sf, ev] = await Promise.all([
2688
+ client.fetchState({ twinUrl: twinApiUrl(twin), agentToken: session.agent_token }),
2689
+ client.fetchEvents({ twinUrl: twinApiUrl(twin), agentToken: session.agent_token })
2690
+ ]);
2691
+ stateFinalByTwin[twin] = sf;
2692
+ eventsByTwin[twin] = isMultiTwin ? ev.map((e) => tagEventTwin(e, twin)) : ev;
2693
+ })
2694
+ );
2695
+ const stateFinal = stateFinalByTwin[primaryTwin];
2696
+ const events = isMultiTwin ? mergeEventsByTimestamp(twins.flatMap((t) => eventsByTwin[t] ?? [])) : eventsByTwin[primaryTwin] ?? [];
2697
+ const legacyEvents = events;
2698
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
2699
+ const durationMs = Date.parse(completedAt) - Date.parse(startedAt);
2700
+ const artifacts = await writeRunArtifactsCore({
2701
+ artifactsDir,
2702
+ runId,
2703
+ scenario,
2704
+ startedAt,
2705
+ completedAt,
2706
+ stdout: agentResult.stdout,
2707
+ stderr: agentResult.stderr,
2708
+ exitCode: agentResult.exitCode,
2709
+ events: legacyEvents,
2710
+ stateInitial,
2711
+ stateFinal
1594
2712
  });
1595
- child.on("close", (exitCode) => {
1596
- if (settled) return;
1597
- settled = true;
1598
- clearTimeout(timer);
1599
- resolve({ stdout, stderr, exitCode, timedOut });
2713
+ if (isMultiTwin) {
2714
+ for (const twin of twins) {
2715
+ if (twin === primaryTwin) continue;
2716
+ try {
2717
+ await writeFile(
2718
+ join(artifacts.runDir, `state_final.${twin}.json`),
2719
+ `${JSON.stringify(redactSecrets(stateFinalByTwin[twin]), null, 2)}
2720
+ `
2721
+ );
2722
+ } catch (err) {
2723
+ console.warn(
2724
+ `[pome] state_final.${twin}.json not written (${err instanceof Error ? err.message : String(err)})`
2725
+ );
2726
+ }
2727
+ }
2728
+ }
2729
+ const eventsJsonl = legacyEvents.map((e) => JSON.stringify(redactEvent(toTwinHttpEvent(e)))).join("\n") + "\n";
2730
+ const stateInitialJson = JSON.stringify(redactSecrets(stateInitial));
2731
+ const stateFinalJson = JSON.stringify(redactSecrets(stateFinal));
2732
+ let signalsJsonl = "";
2733
+ try {
2734
+ signalsJsonl = redactJsonl(
2735
+ await readFile(signalsPath, "utf8").catch(() => "")
2736
+ );
2737
+ } catch (err) {
2738
+ console.warn(
2739
+ `[pome] signals.jsonl upload skipped (${err instanceof Error ? err.message : String(err)}); continuing with signals_storage_key=null`
2740
+ );
2741
+ }
2742
+ const metaJson = await readFile(join(artifacts.runDir, "meta.json"), "utf8").catch(
2743
+ () => "{}"
2744
+ );
2745
+ const perTwinState = isMultiTwin ? Object.fromEntries(
2746
+ twins.map((twin) => [
2747
+ twin,
2748
+ {
2749
+ initialJson: JSON.stringify(redactSecrets(stateInitialByTwin[twin])),
2750
+ finalJson: JSON.stringify(redactSecrets(stateFinalByTwin[twin]))
2751
+ }
2752
+ ])
2753
+ ) : void 0;
2754
+ const uploaded = await uploadRunBlobs(client, session.session_id, {
2755
+ eventsJsonl,
2756
+ stateInitialJson,
2757
+ stateFinalJson,
2758
+ signalsJsonl,
2759
+ metaJson,
2760
+ twins: isMultiTwin ? twins : void 0,
2761
+ perTwinState
1600
2762
  });
1601
- });
1602
- }
1603
- var SAFE_PARENT_ENV = /* @__PURE__ */ new Set([
1604
- "PATH",
1605
- "HOME",
1606
- "USERPROFILE",
1607
- "SystemRoot",
1608
- "COMSPEC",
1609
- "TMPDIR",
1610
- "TEMP",
1611
- "TMP",
1612
- "LANG",
1613
- "LC_ALL"
1614
- ]);
1615
- var DEFAULT_AGENT_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
1616
- "AI_GATEWAY_API_KEY",
1617
- "ANTHROPIC_API_KEY",
1618
- // Claude subscription auth (`claude setup-token`). Without
1619
- // this the Claude Agent SDK inside the agent subprocess only sees an API
1620
- // key, and subscription-only users fail auth under `pome run` while the
1621
- // same agent works when launched by hand.
1622
- "CLAUDE_CODE_OAUTH_TOKEN",
1623
- "GOOGLE_API_KEY",
1624
- "GOOGLE_GENERATIVE_AI_API_KEY",
1625
- "OPENAI_API_KEY",
1626
- "OPENROUTER_API_KEY"
1627
- ]);
1628
- function buildAgentEnv(inputEnv, inheritAll, preflight) {
1629
- const base = {};
1630
- if (inheritAll) {
1631
- Object.assign(base, process.env);
1632
- } else {
1633
- for (const key of [...SAFE_PARENT_ENV, ...DEFAULT_AGENT_ENV_ALLOWLIST]) {
1634
- const value = process.env[key];
1635
- if (value !== void 0) base[key] = value;
2763
+ const eventsJsonlUrl = uploaded.eventsKey;
2764
+ const stateKeys = {
2765
+ initialKey: uploaded.stateInitialKey,
2766
+ finalKey: uploaded.stateFinalKey
2767
+ };
2768
+ const signalsKey = uploaded.signalsKey;
2769
+ const criteriaDefs = scenario.criteria.map((c, idx) => ({
2770
+ id: `crit_${idx}`,
2771
+ text: c.text,
2772
+ kind: c.type,
2773
+ ...c.twin ? { twin: c.twin } : {},
2774
+ ...c.alwaysScored ? { always_scored: true } : {}
2775
+ }));
2776
+ const stopReason = agentResult.timedOut ? "agent_timeout" : agentResult.exitCode === 0 ? "agent_exit_0" : "agent_exit_nonzero";
2777
+ const finalized = await client.finalize(session.session_id, {
2778
+ stopReason,
2779
+ exitCode: agentResult.exitCode ?? 0,
2780
+ durationMs,
2781
+ agentModel: options.agentModel ?? "unknown",
2782
+ agentSdk,
2783
+ criteria: criteriaDefs,
2784
+ taskName: scenario.slug,
2785
+ taskHash,
2786
+ taskPrompt: scenario.prompt,
2787
+ expectedBehavior: scenario.expectedBehavior,
2788
+ // Explicit overrides when uploads succeeded; otherwise let cloud fall
2789
+ // back. For events.jsonl the conventional `team-<>/session-<>/events.jsonl`
2790
+ // path matches what /result-upload-url signs, so omitting traceStorageKey
2791
+ // still resolves correctly. For state blobs there is no conventional
2792
+ // fallback today — omitting them means cloud's judge sees
2793
+ // "{}", which we accept as best-effort degradation.
2794
+ traceStorageKey: eventsJsonlUrl ?? void 0,
2795
+ stateInitialStorageKey: stateKeys.initialKey ?? void 0,
2796
+ stateFinalStorageKey: stateKeys.finalKey ?? void 0,
2797
+ signalsStorageKey: signalsKey ?? void 0,
2798
+ // Multi-twin (M3): per-twin state storage keys (>=1 key per entry).
2799
+ // Undefined for single-twin / an older cloud that returned no per_twin
2800
+ // upload block — the cloud then scores the primary twin from the flat keys.
2801
+ perTwinStateKeys: uploaded.perTwinStateKeys
2802
+ });
2803
+ const score = scoreFromFinalizeResponse(finalized);
2804
+ const verdict = scoreStatus(score, scenario.config.passThreshold);
2805
+ const exitCode = verdict === "pass" ? 0 : 1;
2806
+ try {
2807
+ const counts = evaluationCounts(score);
2808
+ await writeVerdictArtifact(artifacts.runDir, {
2809
+ version: VERDICT_ARTIFACT_VERSION,
2810
+ source: "cloud-finalize",
2811
+ task_name: scenario.slug,
2812
+ task_path: options.taskPath,
2813
+ group_id: options.groupId ?? null,
2814
+ session_id: session.session_id,
2815
+ cloud_run_id: finalized.run_id,
2816
+ cloud_dashboard_url: finalized.dashboard_url,
2817
+ judge_model: score.judge_model,
2818
+ score: finalized.score,
2819
+ pass_threshold: scenario.config.passThreshold,
2820
+ state: verdict,
2821
+ passed: exitCode === 0,
2822
+ evaluated: counts.evaluated,
2823
+ not_evaluated: counts.notEvaluated,
2824
+ pre_satisfied: counts.preSatisfied,
2825
+ total: counts.total,
2826
+ criteria_results: score.results,
2827
+ duration_ms: durationMs,
2828
+ finalized_at: (/* @__PURE__ */ new Date()).toISOString()
2829
+ });
2830
+ } catch (err) {
2831
+ console.warn(
2832
+ `[pome] verdict.json not written (${err instanceof Error ? err.message : String(err)}); pome fix-prompt won't see this trial`
2833
+ );
1636
2834
  }
1637
- for (const key of (process.env.POME_AGENT_ENV_ALLOWLIST ?? "").split(",")) {
1638
- const trimmed = key.trim();
1639
- if (!trimmed) continue;
1640
- const value = process.env[trimmed];
1641
- if (value !== void 0) base[trimmed] = value;
2835
+ return {
2836
+ scenario,
2837
+ runId,
2838
+ cloudRunId: finalized.run_id,
2839
+ cloudDashboardUrl: finalized.dashboard_url,
2840
+ artifacts,
2841
+ score,
2842
+ verdict,
2843
+ exitCode,
2844
+ durationMs
2845
+ };
2846
+ } catch (err) {
2847
+ if (options.abandonOnFailure && !(err instanceof HostedTrialError)) {
2848
+ await abandonBestEffort(client, session.session_id, "trial_crashed");
1642
2849
  }
2850
+ throw err;
2851
+ } finally {
2852
+ await client.deleteSession(session.session_id, true, { discard: true }).catch(() => void 0);
2853
+ await rm(signalsDir, { recursive: true, force: true }).catch(() => void 0);
1643
2854
  }
1644
- return {
1645
- ...base,
1646
- ...inputEnv,
1647
- ...preflight ? { POME_PREFLIGHT: "1" } : {}
1648
- };
1649
2855
  }
1650
- function splitCommand(command) {
1651
- const parts = [];
1652
- let current = "";
1653
- let quote = null;
1654
- let escaping = false;
1655
- for (const char of command.trim()) {
1656
- if (escaping) {
1657
- current += char;
1658
- escaping = false;
1659
- continue;
1660
- }
1661
- if (char === "\\") {
1662
- escaping = true;
1663
- continue;
1664
- }
1665
- if (quote) {
1666
- if (char === quote) quote = null;
1667
- else current += char;
1668
- continue;
1669
- }
1670
- if (char === "'" || char === '"') {
1671
- quote = char;
1672
- continue;
2856
+ async function createSessionOrExplain(client, preminted, input) {
2857
+ if (preminted) return preminted;
2858
+ try {
2859
+ return await client.createSession(input);
2860
+ } catch (err) {
2861
+ if (err instanceof HostedOrchError && err.status === 422 && (err.type === "multi_twin_unsupported" || /multi_twin_unsupported/i.test(err.message))) {
2862
+ throw new HostedOrchError(
2863
+ "this pome cloud does not support multi-twin sessions yet",
2864
+ err.requestId,
2865
+ err.status,
2866
+ err.type
2867
+ );
1673
2868
  }
1674
- if (/\s/.test(char)) {
1675
- if (current.length > 0) {
1676
- parts.push(current);
1677
- current = "";
1678
- }
1679
- continue;
2869
+ throw err;
2870
+ }
2871
+ }
2872
+ async function fetchStateForTwins(client, twins, twinApiUrl, agentToken) {
2873
+ const byTwin = {};
2874
+ await Promise.all(
2875
+ twins.map(async (twin) => {
2876
+ byTwin[twin] = await client.fetchState({
2877
+ twinUrl: twinApiUrl(twin),
2878
+ agentToken
2879
+ });
2880
+ })
2881
+ );
2882
+ return byTwin;
2883
+ }
2884
+ function tagEventTwin(event, twin) {
2885
+ if (event && typeof event === "object") {
2886
+ const existing = event.twin;
2887
+ if (existing === void 0 || existing === null || existing === "") {
2888
+ return { ...event, twin };
1680
2889
  }
1681
- current += char;
1682
2890
  }
1683
- if (escaping || quote) return null;
1684
- if (current.length > 0) parts.push(current);
1685
- const [file, ...args] = parts;
1686
- return file ? { file, args } : null;
2891
+ return event;
2892
+ }
2893
+ function mergeEventsByTimestamp(events) {
2894
+ const tsOf = (e) => {
2895
+ const ts = e.ts;
2896
+ return typeof ts === "string" ? ts : "";
2897
+ };
2898
+ return events.map((event, index) => ({ event, index })).sort((a, b) => {
2899
+ const ta = tsOf(a.event);
2900
+ const tb = tsOf(b.event);
2901
+ if (ta < tb) return -1;
2902
+ if (ta > tb) return 1;
2903
+ return a.index - b.index;
2904
+ }).map(({ event }) => event);
2905
+ }
2906
+ function stderrTail(stderr) {
2907
+ const last = stderr.split("\n").map((line) => line.trim()).filter(Boolean).at(-1);
2908
+ if (!last) return null;
2909
+ const flat = redactSecrets(last.replace(/\s+/g, " ")).trim();
2910
+ return flat.length > 160 ? `${flat.slice(0, 157)}\u2026` : flat;
2911
+ }
2912
+ async function abandonBestEffort(client, sessionId, errorCode) {
2913
+ try {
2914
+ await client.abandonSession(sessionId, { errorCode });
2915
+ } catch (err) {
2916
+ console.warn(
2917
+ `[pome] session abandon skipped (${err instanceof Error ? err.message : String(err)}); the expiry sweeper will close ${sessionId}`
2918
+ );
2919
+ }
1687
2920
  }
1688
2921
 
1689
- export { createHostedClient, criterionPhrase, criterionRowLine, evaluationCounts, isNarrated, isPreSatisfied, narratorReadingLines, outcomeOf, parseGitHubSeedState, parseTaskFile, perTwinReturnedByCloud, readCodeCriteria, readConfigTwins, readLatestRun, readMetaSummary, redactJsonl, runAgentCommand, runScoreLine, scoreCountsSummary, scoreFromFinalizeResponse, scoreStatus, seedStateForTwin, toTwinHttpEvent, uploadRunBlobs, writeRunArtifactsCore };
2922
+ export { DEFAULT_CONTROL_PLANE_URL, DEFAULT_DASHBOARD_URL, DEFAULT_DOCS_SITE_ORIGIN, SESSION_TWIN_NAMES, clearLocalCredentials, createHostedClient, criterionPhrase, criterionRowLine, discoverRunSet, ensurePomeGitignored, friendlyHostedError, isNarrated, isPreSatisfied, narratorReadingLines, outcomeOf, parseGitHubSeedState, parseTaskFile, persistCredentialsAfterLogin, postAgentResolver, readCodeCriteria, readConfigTwins, readLatestRun, readLinkCache, readMetaSummary, redactJsonl, resolveCachedAgentId, resolveCredentials, resolveRunAgentIdentity, resolveSeams, runAgentCommand, runScoreLine, runSessionCreate, runSessionList, runSessionStop, runTaskHosted, scoreCountsSummary, scoreFromFinalizeResponse, scoreStatus, seedStateForTwin, uploadRunBlobs, writeLinkCache, writeRunArtifactsCore };