@pome-sh/cli 0.43.1 → 0.45.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,30 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_CONTROL_PLANE_URL, DEFAULT_DASHBOARD_URL, clearLocalCredentials, friendlyHostedError, SESSION_TWIN_NAMES, runSessionCreate, runSessionList, runSessionStop, resolveCredentials, parseTaskFile, runTaskHosted, scoreStatus, runScoreLine, narratorReadingLines, readLatestRun, readMetaSummary, discoverRunSet, outcomeOf, persistCredentialsAfterLogin, DEFAULT_DOCS_SITE_ORIGIN, resolveSeams, readConfigTwins, resolveCachedAgentId, readLinkCache, seedStateForTwin, runAgentCommand, scoreCountsSummary, criterionRowLine, readCodeCriteria, postAgentResolver, writeLinkCache, ensurePomeGitignored, writeRunArtifactsCore, createHostedClient, redactJsonl, scoreFromFinalizeResponse, parseGitHubSeedState, uploadRunBlobs, isPreSatisfied } from '../../chunk-A6AM6KS4.js';
3
- import '../../chunk-GQSNU3YP.js';
2
+ import { getAvailablePort } from '../../chunk-XDU6TD4O.js';
3
+ import { DEFAULT_CONTROL_PLANE_URL, DEFAULT_DASHBOARD_URL, clearLocalCredentials, friendlyHostedError, SESSION_TWIN_NAMES, runSessionCreate, runSessionList, runSessionStop, resolveCredentials, parseTaskFile, runTaskHosted, scoreStatus, runScoreLine, narratorReadingLines, readLatestRun, readMetaSummary, discoverRunSet, outcomeOf, persistCredentialsAfterLogin, DEFAULT_DOCS_SITE_ORIGIN, resolveSeams, readConfigTwins, resolveCachedAgentId, readLinkCache, seedStateForTwin, runAgentCommand, scoreCountsSummary, criterionRowLine, readCodeCriteria, postAgentResolver, writeLinkCache, ensurePomeGitignored, writeRunArtifactsCore, createHostedClient, redactJsonl, scoreFromFinalizeResponse, parseGitHubSeedState, uploadRunBlobs, isPreSatisfied } from '../../chunk-HEEFOMSZ.js';
4
+ import '../../chunk-4LQ4IJOC.js';
5
+ import { readManifest, writeManifest, MANIFEST_JSON, exitCodeFor, readRequiredManifest, HostedUsageError, HostedOrchError, normalizeManifestTwins } from '../../chunk-26UAPLHK.js';
6
+ import { MOUNTED_TWINS, deriveAgentSlug } from '../../chunk-M7ATJ423.js';
4
7
  import '../../chunk-NW7HGA2K.js';
5
8
  import { GMAIL_CHECKS } from '../../chunk-JM6VS62R.js';
6
9
  import '../../chunk-PASFBRK4.js';
7
10
  import { LINEAR_CHECKS } from '../../chunk-TSOSDKXP.js';
8
11
  import '../../chunk-3FZY376K.js';
9
- import { createRecorder, bootTwin } from '../../chunk-64AWQJ7R.js';
10
- import { TWIN_NAME_LIST, TWIN_REGISTRY, createGitHubSmokeApp } from '../../chunk-4DXTT3RV.js';
11
- import { getAvailablePort } from '../../chunk-XDU6TD4O.js';
12
- import { MOUNTED_TWINS, readManifest, deriveAgentSlug, writeManifest, MANIFEST_JSON, exitCodeFor, readRequiredManifest, HostedUsageError, HostedOrchError, normalizeManifestTwins } from '../../chunk-CS7O2ZXB.js';
13
12
  import { buildEgressAllowlist, readBlockedEgress } from '../../chunk-CBFKZZBR.js';
13
+ import '../../chunk-DX2KCFJM.js';
14
+ import { createRecorder, bootTwin } from '../../chunk-EZYVICDB.js';
15
+ import { TWIN_NAME_LIST, TWIN_REGISTRY, createGitHubSmokeApp } from '../../chunk-BXYPNEJY.js';
14
16
  import { TAPE_ASSERTABLE_TOOLS } from '../../chunk-C4BVTUA3.js';
15
17
  import { seedSchema } from '../../chunk-NBOQN5VX.js';
16
18
  import { SLACK_CHECKS } from '../../chunk-2ZGVDTJC.js';
17
19
  import { oneOf, defineCheck, repoRef, VACUITY_SENTINEL_NUMBER, childStatePath, VACUITY_SENTINEL, statePath, templateSlots, renderCheck, checksDigest, checkPattern, checkNearMissPattern } from '../../chunk-JWJYNAWI.js';
18
20
  import '../../chunk-YBWG5JK2.js';
19
21
  import '../../chunk-TWURH7YM.js';
20
- import { eventSchema, isLegacyEventRow } from '../../chunk-5KFDRR53.js';
22
+ import { eventSchema, isLegacyEventRow } from '../../chunk-7VZBAHQ2.js';
21
23
  import { redactSecrets, redactEvent } from '../../chunk-SG6ZTIMT.js';
22
24
  import '../../chunk-2K6BJ3PI.js';
23
25
  import '../../chunk-FBSA5L36.js';
24
26
  import { Command } from 'commander';
25
27
  import { existsSync, readFileSync, realpathSync, statSync, constants } from 'node:fs';
26
- import { mkdir, readFile, writeFile, readdir, copyFile, stat, cp } from 'node:fs/promises';
27
- import { dirname, resolve, join, basename, relative, sep, extname } from 'node:path';
28
+ import { readFile, mkdir, writeFile, readdir, copyFile, stat, open, unlink, chmod, cp } from 'node:fs/promises';
29
+ import { dirname, resolve, join, basename, relative, extname } from 'node:path';
28
30
  import { fileURLToPath } from 'node:url';
29
31
  import { serve } from '@hono/node-server';
30
32
  import { randomBytes, randomUUID, createHash } from 'node:crypto';
@@ -35,6 +37,7 @@ import { createInterface } from 'node:readline';
35
37
  import { z } from 'zod';
36
38
  import Anthropic from '@anthropic-ai/sdk';
37
39
  import { createRequire } from 'node:module';
40
+ import { homedir } from 'node:os';
38
41
 
39
42
  async function readEventsJsonl(runDir) {
40
43
  const file = resolve(runDir, "events.jsonl");
@@ -322,78 +325,6 @@ function makeShutdown(child, graceMs) {
322
325
  return done;
323
326
  };
324
327
  }
325
- function resolveTwinHttpParents(rows) {
326
- const eventIdByToolUseId = /* @__PURE__ */ new Map();
327
- for (const row of rows) {
328
- if (row.kind === "ToolUseEvent") eventIdByToolUseId.set(row.tool_use_id, row.event_id);
329
- }
330
- if (eventIdByToolUseId.size === 0) return rows;
331
- return rows.map((row) => {
332
- if (row.kind !== "TwinHttpEvent") return row;
333
- if (row.parent_event_id != null) return row;
334
- const causingToolUseId = row.tool_call_id ?? row.correlation_id ?? null;
335
- if (causingToolUseId === null) return row;
336
- const parentEventId = eventIdByToolUseId.get(causingToolUseId);
337
- return parentEventId === void 0 ? row : { ...row, parent_event_id: parentEventId };
338
- });
339
- }
340
- async function mergeAdapterSignalsIntoEvents(signalsPath, eventsJsonlPath) {
341
- let rawSignals;
342
- try {
343
- rawSignals = await readFile(signalsPath, "utf8");
344
- } catch {
345
- return { appended: 0, dropped: 0 };
346
- }
347
- let dropped = 0;
348
- const signalRows = [];
349
- for (const line of rawSignals.split("\n")) {
350
- if (line.length === 0) continue;
351
- let parsed;
352
- try {
353
- parsed = JSON.parse(line);
354
- } catch {
355
- dropped += 1;
356
- continue;
357
- }
358
- const result = eventSchema.safeParse(parsed);
359
- if (!result.success) {
360
- dropped += 1;
361
- continue;
362
- }
363
- signalRows.push(redactEvent(result.data));
364
- }
365
- if (signalRows.length === 0) return { appended: 0, dropped };
366
- let rawEvents;
367
- try {
368
- rawEvents = await readFile(eventsJsonlPath, "utf8");
369
- } catch {
370
- rawEvents = "";
371
- }
372
- const eventRows = [];
373
- const unparseablePassthrough = [];
374
- for (const line of rawEvents.split("\n")) {
375
- if (line.length === 0) continue;
376
- let parsed;
377
- try {
378
- parsed = JSON.parse(line);
379
- } catch {
380
- unparseablePassthrough.push(line);
381
- continue;
382
- }
383
- const result = eventSchema.safeParse(parsed);
384
- if (result.success) {
385
- eventRows.push(redactEvent(result.data));
386
- } else {
387
- unparseablePassthrough.push(line);
388
- }
389
- }
390
- const merged = resolveTwinHttpParents(eventRows.concat(signalRows));
391
- merged.sort((a, b) => a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0);
392
- const sortedJsonl = merged.map((r) => JSON.stringify(r)).join("\n");
393
- const head = unparseablePassthrough.length > 0 ? unparseablePassthrough.join("\n") + "\n" : "";
394
- await writeFile(eventsJsonlPath, head + sortedJsonl + "\n");
395
- return { appended: signalRows.length, dropped };
396
- }
397
328
 
398
329
  // src/runner/runTaskCore.ts
399
330
  async function writeRunNoScore(input) {
@@ -428,9 +359,6 @@ async function runTask(options) {
428
359
  const writeRun = writeRunNoScore;
429
360
  const runDir = join(artifactsDir, scenario.slug, runId);
430
361
  await mkdir(runDir, { recursive: true });
431
- const signalsPath = join(runDir, "signals.jsonl");
432
- await writeFile(signalsPath, "");
433
- const signalsPathForEnv = sep === "\\" ? signalsPath.replace(/\\/g, "/") : signalsPath;
434
362
  const eventsJsonlPath = join(runDir, "events.jsonl");
435
363
  await writeFile(eventsJsonlPath, "");
436
364
  const egressAllowHosts = buildEgressAllowlist(process.env, {
@@ -525,7 +453,6 @@ async function runTask(options) {
525
453
  POME_AUTH_TOKEN: token,
526
454
  POME_RUN_ID: runId,
527
455
  POME_ARTIFACTS_DIR: runDir,
528
- POME_ADAPTER_SIGNALS_PATH: signalsPathForEnv,
529
456
  ...options.extraAgentEnv ?? {},
530
457
  ...proxyEnv
531
458
  };
@@ -575,7 +502,6 @@ async function runTask(options) {
575
502
  stateInitial,
576
503
  stateFinal: stateFinal2
577
504
  });
578
- await mergeAdapterSignalsIntoEvents(signalsPath, eventsJsonlPath);
579
505
  void exitCode2;
580
506
  const blockedEgress2 = await collectBlockedEgress();
581
507
  return { scenario, runId, artifacts: artifacts2, agent: preflight, exitCode: 3, blockedEgress: blockedEgress2 };
@@ -601,7 +527,6 @@ async function runTask(options) {
601
527
  stateInitial,
602
528
  stateFinal
603
529
  });
604
- await mergeAdapterSignalsIntoEvents(signalsPath, eventsJsonlPath);
605
530
  const blockedEgress = await collectBlockedEgress();
606
531
  return { scenario, runId, artifacts, agent, exitCode, blockedEgress };
607
532
  } finally {
@@ -1187,7 +1112,7 @@ function extractJsonPayload(text) {
1187
1112
 
1188
1113
  // src/task/seed-verifier.ts
1189
1114
  async function verifySeedWithTwin(seed) {
1190
- const { GitHubDomain, openGitHubCloneDatabase } = await import('../../src-G6CMFUR6.js');
1115
+ const { GitHubDomain, openGitHubCloneDatabase } = await import('../../src-46YGR2WY.js');
1191
1116
  const db = openGitHubCloneDatabase(":memory:");
1192
1117
  try {
1193
1118
  new GitHubDomain(db).seed(seed);
@@ -3395,7 +3320,7 @@ function localDigest(twin) {
3395
3320
  }
3396
3321
  function bakedVersions() {
3397
3322
  try {
3398
- return JSON.parse('{"@pome-sh/sdk":"0.11.6","@pome-sh/wire":"0.4.2","@pome-sh/twin-github":"0.12.0","@pome-sh/twin-gmail":"0.4.0","@pome-sh/twin-linear":"0.4.1","@pome-sh/twin-slack":"0.4.1","@pome-sh/twin-stripe":"0.4.7"}');
3323
+ return JSON.parse('{"@pome-sh/sdk":"0.11.6","@pome-sh/wire":"0.4.3","@pome-sh/twin-github":"0.12.0","@pome-sh/twin-gmail":"0.4.0","@pome-sh/twin-linear":"0.4.1","@pome-sh/twin-slack":"0.4.1","@pome-sh/twin-stripe":"0.4.7"}');
3399
3324
  } catch {
3400
3325
  return {};
3401
3326
  }
@@ -4093,13 +4018,13 @@ function validateJsonl(name, raw) {
4093
4018
  `pome eval: ${name} is corrupt \u2014 line ${i + 1} is not valid JSON.`
4094
4019
  );
4095
4020
  }
4096
- if (name === "events.jsonl" && isLegacyEventRow(parsed)) {
4021
+ if (isLegacyEventRow(parsed)) {
4097
4022
  throw new HostedUsageError(
4098
4023
  `pome eval: events.jsonl line ${i + 1} has no "kind" \u2014 it was recorded before the unified event shape, and the control plane refuses it. Re-run the task to record the trace again.`
4099
4024
  );
4100
4025
  }
4101
4026
  }
4102
- if (name === "events.jsonl" && nonEmpty === 0) {
4027
+ if (nonEmpty === 0) {
4103
4028
  throw new HostedUsageError(
4104
4029
  "pome eval: events.jsonl is empty \u2014 the run captured no trace to evaluate."
4105
4030
  );
@@ -4120,24 +4045,12 @@ async function readRunDirArtifacts(runDir) {
4120
4045
  parseJsonFile("state_initial.json", stateInitialJson);
4121
4046
  const stateFinalJson = await readRequiredFile(runDir, "state_final.json");
4122
4047
  parseJsonFile("state_final.json", stateFinalJson);
4123
- let signalsJsonl = null;
4124
- try {
4125
- signalsJsonl = await readFile(join(runDir, "signals.jsonl"), "utf8");
4126
- } catch (err) {
4127
- if (err.code !== "ENOENT") {
4128
- throw new HostedUsageError(
4129
- `pome eval: signals.jsonl could not be read: ${err instanceof Error ? err.message : String(err)}`
4130
- );
4131
- }
4132
- }
4133
- if (signalsJsonl !== null) validateJsonl("signals.jsonl", signalsJsonl);
4134
4048
  return {
4135
4049
  runDir,
4136
4050
  meta,
4137
4051
  eventsJsonl,
4138
4052
  stateInitialJson,
4139
4053
  stateFinalJson,
4140
- signalsJsonl,
4141
4054
  metaJson: metaRaw
4142
4055
  };
4143
4056
  }
@@ -4239,7 +4152,6 @@ async function runEval(options) {
4239
4152
  stateFinalJson: JSON.stringify(
4240
4153
  redactSecrets(JSON.parse(artifacts.stateFinalJson))
4241
4154
  ),
4242
- signalsJsonl: redactJsonl(artifacts.signalsJsonl ?? ""),
4243
4155
  // D18.1 — already validated as parseable JSON in readRunDirArtifacts.
4244
4156
  // Re-serialize with the SAME formatting writeRunArtifactsCore's writeJson
4245
4157
  // uses (2-space indent + trailing newline) so the bytes `pome eval`
@@ -4284,8 +4196,7 @@ async function runEval(options) {
4284
4196
  expectedBehavior: "",
4285
4197
  traceStorageKey: keys.eventsKey ?? void 0,
4286
4198
  stateInitialStorageKey: keys.stateInitialKey ?? void 0,
4287
- stateFinalStorageKey: keys.stateFinalKey ?? void 0,
4288
- signalsStorageKey: keys.signalsKey ?? void 0
4199
+ stateFinalStorageKey: keys.stateFinalKey ?? void 0
4289
4200
  });
4290
4201
  }
4291
4202
  let finalized;
@@ -4961,7 +4872,7 @@ function firstSentence(description) {
4961
4872
  function resolveExampleRef(env = process.env) {
4962
4873
  const override = env.POME_EXAMPLE_REF?.trim();
4963
4874
  if (override) return override;
4964
- const baked = "aa6e8ff0d76f2bfbdd8c01e72239431644cb849a".trim() ;
4875
+ const baked = "16bd8bf4255ab4a22c0d466529abcab652301773".trim() ;
4965
4876
  return FULL_SHA.test(baked) ? baked : "main";
4966
4877
  }
4967
4878
  function rawUrlFor(example, file, ref) {
@@ -5316,6 +5227,143 @@ async function loadTrialEvents(runDir) {
5316
5227
  }
5317
5228
  return events;
5318
5229
  }
5230
+ var USAGE_EVENT = "cli_usage";
5231
+ var USAGE_DOCS_URL = "https://github.com/pome-sh/digital-twins#telemetry";
5232
+ var DEFAULT_HOST = "https://us.i.posthog.com";
5233
+ var REQUEST_TIMEOUT_MS = 1500;
5234
+ function bakedIn(name) {
5235
+ const value = name === "key" ? "" : "" ;
5236
+ return value !== void 0 && value.length > 0 ? value : void 0;
5237
+ }
5238
+ function telemetryDestination(env = process.env) {
5239
+ const key = env.POME_TELEMETRY_KEY || bakedIn("key");
5240
+ if (!key) return void 0;
5241
+ const host = (env.POME_TELEMETRY_HOST || bakedIn("host") || DEFAULT_HOST).replace(/\/+$/, "");
5242
+ return { key, host };
5243
+ }
5244
+ function telemetryOptOut(env = process.env) {
5245
+ const off = (value) => value !== void 0 && /^(0|false|off|no)$/i.test(value.trim());
5246
+ const set = (value) => value !== void 0 && value.trim() !== "" && value.trim() !== "0";
5247
+ if (off(env.POME_TELEMETRY)) return "POME_TELEMETRY";
5248
+ if (set(env.DO_NOT_TRACK)) return "DO_NOT_TRACK";
5249
+ if (set(env.CI)) return "CI";
5250
+ return void 0;
5251
+ }
5252
+ function usageStatePath() {
5253
+ return join(homedir(), ".pome", "telemetry.json");
5254
+ }
5255
+ function usageProperties(input) {
5256
+ const nodeVersion = input.nodeVersion ?? process.version;
5257
+ return {
5258
+ command: input.command,
5259
+ cli_version: input.version,
5260
+ os: input.platform ?? process.platform,
5261
+ node_major: Number(nodeVersion.replace(/^v/, "").split(".")[0]),
5262
+ // Anonymous events: no person profile is created or updated in PostHog.
5263
+ $process_person_profile: false
5264
+ };
5265
+ }
5266
+ function commandName(command) {
5267
+ const names = [];
5268
+ let current = command;
5269
+ while (current && current.parent) {
5270
+ names.unshift(current.name());
5271
+ current = current.parent;
5272
+ }
5273
+ return names.join(" ");
5274
+ }
5275
+ var FIRST_RUN_NOTICE = `pome sends one anonymous usage event per day (CLI version, OS, Node major, command name; no arguments, paths or repo names). Turn it off with POME_TELEMETRY=0. ${USAGE_DOCS_URL}`;
5276
+ async function readState(path) {
5277
+ try {
5278
+ const parsed = JSON.parse(await readFile(path, "utf8"));
5279
+ if (typeof parsed.id === "string" && parsed.id.length > 0) {
5280
+ return {
5281
+ id: parsed.id,
5282
+ ...typeof parsed.last_sent === "string" ? { last_sent: parsed.last_sent } : {},
5283
+ ...parsed.notice_shown === true ? { notice_shown: true } : {}
5284
+ };
5285
+ }
5286
+ } catch {
5287
+ }
5288
+ return { id: randomUUID() };
5289
+ }
5290
+ async function writeState(path, state) {
5291
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
5292
+ await writeFile(path, `${JSON.stringify(state, null, 2)}
5293
+ `, { mode: 384 });
5294
+ await chmod(path, 384);
5295
+ }
5296
+ var STALE_LOCK_MS = 3e4;
5297
+ async function withStateLock(path, fn) {
5298
+ const lockPath = `${path}.lock`;
5299
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
5300
+ try {
5301
+ const handle = await open(lockPath, "wx", 384);
5302
+ await handle.close();
5303
+ } catch (err) {
5304
+ if (err.code !== "EEXIST") throw err;
5305
+ const age = await stat(lockPath).then((s) => Date.now() - s.mtimeMs, () => 0);
5306
+ if (age <= STALE_LOCK_MS) return "locked";
5307
+ await unlink(lockPath).catch(() => void 0);
5308
+ try {
5309
+ const handle = await open(lockPath, "wx", 384);
5310
+ await handle.close();
5311
+ } catch {
5312
+ return "locked";
5313
+ }
5314
+ }
5315
+ try {
5316
+ return await fn();
5317
+ } finally {
5318
+ await unlink(lockPath).catch(() => void 0);
5319
+ }
5320
+ }
5321
+ async function maybeSendUsageTick(input) {
5322
+ const env = input.env ?? process.env;
5323
+ const optOut = telemetryOptOut(env);
5324
+ if (optOut) return { sent: false, reason: "opt-out", detail: optOut };
5325
+ const destination = telemetryDestination(env);
5326
+ if (!destination) return { sent: false, reason: "no-key" };
5327
+ const statePath2 = input.statePath ?? usageStatePath();
5328
+ const today = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5329
+ let state;
5330
+ try {
5331
+ const claimed = await withStateLock(statePath2, async () => {
5332
+ const current = await readState(statePath2);
5333
+ if (current.last_sent === today) return void 0;
5334
+ if (!current.notice_shown) {
5335
+ (input.notify ?? ((line) => console.error(line)))(FIRST_RUN_NOTICE);
5336
+ current.notice_shown = true;
5337
+ }
5338
+ current.last_sent = today;
5339
+ await writeState(statePath2, current);
5340
+ return current;
5341
+ });
5342
+ if (claimed === "locked" || claimed === void 0) return { sent: false, reason: "already-today" };
5343
+ state = claimed;
5344
+ } catch (err) {
5345
+ return { sent: false, reason: "request-failed", detail: `state: ${err.message}` };
5346
+ }
5347
+ const body = {
5348
+ api_key: destination.key,
5349
+ event: USAGE_EVENT,
5350
+ distinct_id: state.id,
5351
+ properties: usageProperties({ command: input.command, version: input.version }),
5352
+ timestamp: (input.now ?? /* @__PURE__ */ new Date()).toISOString()
5353
+ };
5354
+ try {
5355
+ const res = await (input.fetchImpl ?? fetch)(`${destination.host}/capture/`, {
5356
+ method: "POST",
5357
+ headers: { "content-type": "application/json" },
5358
+ body: JSON.stringify(body),
5359
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
5360
+ });
5361
+ if (!res.ok) return { sent: false, reason: "request-failed", detail: `HTTP ${res.status}` };
5362
+ return { sent: true };
5363
+ } catch (err) {
5364
+ return { sent: false, reason: "request-failed", detail: err.message };
5365
+ }
5366
+ }
5319
5367
 
5320
5368
  // src/cli/main.ts
5321
5369
  var PACKAGE_VERSION = readPackageVersion();
@@ -5324,7 +5372,7 @@ var DEFAULT_AGENT_COMMAND = `node ${DEFAULT_AGENT_FILE}`;
5324
5372
  var MANIFEST_SCHEMA_URL = "https://pome.sh/schemas/v1/pome.json";
5325
5373
  var MAX_UNREADABLE_PATHS_SHOWN = 5;
5326
5374
  function readPackageVersion() {
5327
- if ("0.43.1".length > 0) return "0.43.1";
5375
+ if ("0.45.0".length > 0) return "0.45.0";
5328
5376
  try {
5329
5377
  const here = dirname(fileURLToPath(import.meta.url));
5330
5378
  const candidates = [
@@ -5353,8 +5401,91 @@ function createProgram() {
5353
5401
  process.env.POME_API_URL ?? DEFAULT_CONTROL_PLANE_URL
5354
5402
  ).option("--artifacts-dir <dir>", "Directory for run artifacts", "runs");
5355
5403
  program.name("pome").description(
5356
- "Test AI agents against digital twins of real SaaS APIs. Runs are recorded to app.pome.sh. Start with `pome init`."
5357
- ).version(PACKAGE_VERSION).showHelpAfterError("(add --help for usage)");
5404
+ "Stateful local twins of GitHub, Slack, Stripe, Gmail and Linear that your agent calls as if they were the real API. Start with `pome twin start <twin>`."
5405
+ ).version(PACKAGE_VERSION).showHelpAfterError("(add --help for usage)").hook("preAction", (_root, actionCommand) => {
5406
+ void maybeSendUsageTick({ command: commandName(actionCommand), version: PACKAGE_VERSION });
5407
+ });
5408
+ const twin = program.command("twin").summary("Run a twin on this machine").description(
5409
+ "Start a twin on this machine, print its status, show its tape, or write a starter seed file"
5410
+ );
5411
+ twin.command("start").argument(
5412
+ "[names...]",
5413
+ `Twin names (${TWIN_NAME_LIST.join(" | ")}). One, or several to boot together (each on its own port). Optional when --seed names exactly one twin.`
5414
+ ).option(
5415
+ "--port <port>",
5416
+ // Built from the registry rather than restated: the per-twin overrides
5417
+ // are the registry's to add, and this text went stale when linear's did.
5418
+ `Port to bind (default: $PORT, else ${TWIN_NAME_LIST.filter(
5419
+ (twin2) => TWIN_REGISTRY[twin2].portEnvName
5420
+ ).map((twin2) => `${TWIN_REGISTRY[twin2].portEnvName}/${TWIN_REGISTRY[twin2].defaultPort} for ${twin2}`).join(", ")}, otherwise 3333). With several twins: the first twin's port; the rest take the next free ports above it. Without it, each twin takes its default port, else the next free one.`
5421
+ ).option(
5422
+ "--seed <path>",
5423
+ "Boot from a JSON or YAML seed file instead of the default. A seed REPLACES the default; it does not merge. Takes the per-twin envelope { <twin>: { \u2026 } } or one twin's flat seed (several twins need the envelope, one entry per named twin). Overrides POME_SEED_JSON."
5424
+ ).description(
5425
+ "Start one or more standalone twins as a long-lived foreground server (Ctrl-C to stop)"
5426
+ ).action(async (names, options) => {
5427
+ const { runTwinStartCommand } = await import('../../twinStart-XN3BYOUF.js');
5428
+ await runTwinStartCommand(names, options);
5429
+ });
5430
+ twin.command("new-seed").argument("<name...>", `Twin name (${TWIN_NAME_LIST.join(" | ")}). Repeat for one file covering several.`).option("--out <path>", "Write to this file instead of stdout. Refuses to overwrite.").summary("Print a new starter seed file for a twin").description(
5431
+ "Print a new starter seed file for a twin, generated from the twin's own starting state. One twin is flat, several are the per-twin envelope. Boot it with `twin start <twin> --seed`, seed a sandbox with `sandbox create --twin <twin> --seed`, or drop it beside a task as <task>.seed.json"
5432
+ ).action(async (names, options) => {
5433
+ const { runTwinSeedCommand } = await import('../../twinSeed-ZFZO7BL7.js');
5434
+ await runTwinSeedCommand(names, options);
5435
+ });
5436
+ twin.command("status").summary("Say whether the local twins are running").description(
5437
+ "Say whether each twin `pome twin start` booted here is still running, and print its paste-able env lines"
5438
+ ).action(async () => {
5439
+ const statusPath = ".pome/twin-status.json";
5440
+ if (!existsSync(statusPath)) {
5441
+ console.log("No standalone twin status found.");
5442
+ return;
5443
+ }
5444
+ const { standaloneStatusEntries } = await import('../../twinStart-XN3BYOUF.js');
5445
+ let entries;
5446
+ try {
5447
+ entries = standaloneStatusEntries(JSON.parse(await readFile(statusPath, "utf8"))).map(
5448
+ (status) => ({ ...status, origin: new URL(status.rest_url).origin })
5449
+ );
5450
+ if (entries.length === 0 || entries.some((status) => status.name === "")) {
5451
+ throw new Error("no twins");
5452
+ }
5453
+ } catch {
5454
+ throw new Error(
5455
+ `pome twin status: ${statusPath} is unreadable \u2014 start a twin with \`pome twin start <${TWIN_NAME_LIST.join("|")}>\`.`
5456
+ );
5457
+ }
5458
+ for (const [index, status] of entries.entries()) {
5459
+ const running = await fetch(`${status.origin}/healthz`, {
5460
+ signal: AbortSignal.timeout(1e3)
5461
+ }).then(
5462
+ async (res) => res.ok && (await res.json().catch(() => ({}))).twin === status.name,
5463
+ () => false
5464
+ );
5465
+ if (index > 0) console.log("");
5466
+ console.log(
5467
+ running ? `${status.name} twin \u2014 running` : `${status.name} twin \u2014 not running (stale ${statusPath})`
5468
+ );
5469
+ const envName = TWIN_REGISTRY[status.name]?.envName ?? status.name.toUpperCase();
5470
+ console.log(`POME_${envName}_REST_URL=${status.rest_url}`);
5471
+ console.log(`POME_${envName}_MCP_URL=${status.mcp_url}`);
5472
+ console.log(`POME_AUTH_TOKEN=${status.auth_token}`);
5473
+ }
5474
+ });
5475
+ twin.command("tape").argument(
5476
+ "[name]",
5477
+ `Twin name (${TWIN_NAME_LIST.join(" | ")}). Optional when one twin is recorded in .pome/twin-status.json.`
5478
+ ).option(
5479
+ "--diff",
5480
+ "Also print the state diff since the twin booted \u2014 its seed, default or --seed \u2014 per collection: added, changed, removed.",
5481
+ false
5482
+ ).option("--json", "Print the tape (and the diff, with --diff) as one JSON envelope.", false).summary("Show what the agent did on a local twin").description(
5483
+ "Print the running twin's tape: one line per request with status, fidelity and whether state changed, so a call that claimed success but landed nothing stands out. Reads the twin's address and token from .pome/twin-status.json; no account, no hosted call."
5484
+ ).action(async (name, options) => {
5485
+ const { runTwinTapeCommand } = await import('../../twinTape-5P3HQ27N.js');
5486
+ await runTwinTapeCommand(name, options);
5487
+ });
5488
+ program.commandsGroup("Going further:");
5358
5489
  program.command("init").summary("Set up Pome in this project").description(
5359
5490
  "Create pome.json, plus starter files in a new project \u2014 or, with --example <id>, fetch a complete runnable example into ./<id>"
5360
5491
  ).option(
@@ -5728,7 +5859,7 @@ function createProgram() {
5728
5859
  return;
5729
5860
  }
5730
5861
  {
5731
- const { runDoctorChecks } = await import('../../checks-V442KL5O.js');
5862
+ const { runDoctorChecks } = await import('../../checks-RDU56OGI.js');
5732
5863
  const { renderDoctorReport } = await import('../../render-ZQQ4UMNO.js');
5733
5864
  const doctorReport = await runDoctorChecks({ mode: useLocal ? "full" : "hosted" });
5734
5865
  if (!doctorReport.ok) {
@@ -5766,7 +5897,7 @@ function createProgram() {
5766
5897
  taskForRuns.config.runs
5767
5898
  );
5768
5899
  if (k > 1) {
5769
- const { runTrialGroup } = await import('../../runTrialGroup-HYGGBLMM.js');
5900
+ const { runTrialGroup } = await import('../../runTrialGroup-MMTTTDWY.js');
5770
5901
  const fileForRerun = relative(process.cwd(), file);
5771
5902
  const rerunCommand = defaultTask ? options.trials !== void 0 ? `pome run -n ${k}` : "pome run" : `pome run ${fileForRerun && !fileForRerun.startsWith("..") ? fileForRerun : file} -n ${k}`;
5772
5903
  const groupResult = await runTrialGroup({
@@ -5848,7 +5979,7 @@ function createProgram() {
5848
5979
  program.command("doctor").summary("Check the agent and twin wiring").description(
5849
5980
  "Check the agent\u2194twin wiring: pome.json (or pome.yaml) present + valid, the local twin boots + serves, requests routed to the twin (not a hardcoded production host), egress floor active. On failure prints one named cause (file:line where knowable) + one concrete fix and exits non-zero."
5850
5981
  ).action(async () => {
5851
- const { runDoctorChecks } = await import('../../checks-V442KL5O.js');
5982
+ const { runDoctorChecks } = await import('../../checks-RDU56OGI.js');
5852
5983
  const { renderDoctorReport } = await import('../../render-ZQQ4UMNO.js');
5853
5984
  const report = await runDoctorChecks();
5854
5985
  for (const line of renderDoctorReport(report, { passNote: true })) console.error(line);
@@ -5999,65 +6130,6 @@ function createProgram() {
5999
6130
  })
6000
6131
  );
6001
6132
  });
6002
- const twin = program.command("twin").summary("Run a twin on this machine").description("Start a twin on this machine, print its status or a starter seed file");
6003
- twin.command("start").argument(
6004
- "[name]",
6005
- `Twin name (${TWIN_NAME_LIST.join(" | ")}). Optional when --seed names exactly one twin.`
6006
- ).option(
6007
- "--port <port>",
6008
- // Built from the registry rather than restated: the per-twin overrides
6009
- // are the registry's to add, and this text went stale when linear's did.
6010
- `Port to bind (default: $PORT, else ${TWIN_NAME_LIST.filter(
6011
- (twin2) => TWIN_REGISTRY[twin2].portEnvName
6012
- ).map((twin2) => `${TWIN_REGISTRY[twin2].portEnvName}/${TWIN_REGISTRY[twin2].defaultPort} for ${twin2}`).join(", ")}, otherwise 3333)`
6013
- ).option(
6014
- "--seed <path>",
6015
- "Boot this twin from a JSON or YAML seed file instead of its default. A seed REPLACES the default; it does not merge. Takes the per-twin envelope { <twin>: { \u2026 } } or one twin's flat seed. Overrides POME_SEED_JSON."
6016
- ).description(
6017
- "Start a standalone twin as a long-lived foreground server (Ctrl-C to stop)"
6018
- ).action(async (name, options) => {
6019
- const { runTwinStartCommand } = await import('../../twinStart-3Q74IURZ.js');
6020
- await runTwinStartCommand(name, options);
6021
- });
6022
- twin.command("new-seed").argument("<name...>", `Twin name (${TWIN_NAME_LIST.join(" | ")}). Repeat for one file covering several.`).option("--out <path>", "Write to this file instead of stdout. Refuses to overwrite.").summary("Print a new starter seed file for a twin").description(
6023
- "Print a new starter seed file for a twin, generated from the twin's own starting state. One twin is flat, several are the per-twin envelope. Boot it with `twin start <twin> --seed`, seed a sandbox with `sandbox create --twin <twin> --seed`, or drop it beside a task as <task>.seed.json"
6024
- ).action(async (names, options) => {
6025
- const { runTwinSeedCommand } = await import('../../twinSeed-J7UAY3GM.js');
6026
- await runTwinSeedCommand(names, options);
6027
- });
6028
- twin.command("status").summary("Say whether the local twin is running").description(
6029
- "Say whether the twin `pome twin start` last booted here is still running, and print its paste-able env lines"
6030
- ).action(async () => {
6031
- const statusPath = ".pome/twin-status.json";
6032
- if (!existsSync(statusPath)) {
6033
- console.log("No standalone twin status found.");
6034
- return;
6035
- }
6036
- let status;
6037
- let origin;
6038
- try {
6039
- status = JSON.parse(await readFile(statusPath, "utf8"));
6040
- if (typeof status.name !== "string" || status.name === "") throw new Error("no name");
6041
- origin = new URL(status.rest_url).origin;
6042
- } catch {
6043
- throw new Error(
6044
- `pome twin status: ${statusPath} is unreadable \u2014 start a twin with \`pome twin start <${TWIN_NAME_LIST.join("|")}>\`.`
6045
- );
6046
- }
6047
- const running = await fetch(`${origin}/healthz`, {
6048
- signal: AbortSignal.timeout(1e3)
6049
- }).then(
6050
- async (res) => res.ok && (await res.json().catch(() => ({}))).twin === status.name,
6051
- () => false
6052
- );
6053
- console.log(
6054
- running ? `${status.name} twin \u2014 running` : `${status.name} twin \u2014 not running (stale ${statusPath})`
6055
- );
6056
- const envName = TWIN_REGISTRY[status.name]?.envName ?? status.name.toUpperCase();
6057
- console.log(`POME_${envName}_REST_URL=${status.rest_url}`);
6058
- console.log(`POME_${envName}_MCP_URL=${status.mcp_url}`);
6059
- console.log(`POME_AUTH_TOKEN=${status.auth_token}`);
6060
- });
6061
6133
  program.command("capture-server").summary("Record the agent's model calls").description(
6062
6134
  "Boot an HTTP CONNECT-tunnel proxy that appends one LlmCallEvent per tunnel to events.jsonl. Spawned by `pome run`; agent traffic flows via HTTPS_PROXY."
6063
6135
  ).option("--port <port>", "TCP port to listen on (0 = ephemeral).", "8910").option(
@@ -6102,8 +6174,8 @@ async function taskFiles(target) {
6102
6174
  if (!existsSync(resolved)) {
6103
6175
  throw new HostedUsageError(`Task path not found: ${target}`);
6104
6176
  }
6105
- const stat3 = await import('node:fs/promises').then((fs) => fs.stat(resolved));
6106
- if (stat3.isFile()) return [resolved];
6177
+ const stat4 = await import('node:fs/promises').then((fs) => fs.stat(resolved));
6178
+ if (stat4.isFile()) return [resolved];
6107
6179
  const entries = await readdir(resolved);
6108
6180
  return entries.filter((entry) => entry.endsWith(".md")).sort().map((entry) => join(resolved, entry));
6109
6181
  }
@@ -3,7 +3,7 @@ import { defaultSeedState, parseSeed } from './chunk-NBOQN5VX.js';
3
3
  export { defaultSeedState, parseSeed, seedSchema } from './chunk-NBOQN5VX.js';
4
4
  import { routeInputDeclarer, integerInput, mountDeclaredRoute, UndeclaredInputError, MalformedBodyError } from './chunk-IZFM7W2V.js';
5
5
  import { loadMcpToolFixture, defineTwin, twinBuildInfo, deriveMcpToolTable, UnknownToolError, openTwinDatabase, createApp, typeDisagreements } from './chunk-TWURH7YM.js';
6
- import './chunk-5KFDRR53.js';
6
+ import './chunk-7VZBAHQ2.js';
7
7
  import './chunk-SG6ZTIMT.js';
8
8
  import './chunk-FBSA5L36.js';
9
9
  import { z, ZodError } from 'zod';
@@ -3081,12 +3081,6 @@ function checkRunJson(run, repo) {
3081
3081
  html_url: `https://github.com/${repo.full_name}/runs/${run.id}`,
3082
3082
  details_url: run.details_url,
3083
3083
  status: run.status,
3084
- // The twin's `conclusion` column carries the full GitHub check-run
3085
- // conclusion vocabulary including `"stale"`, but the pinned
3086
- // @octokit/openapi-types version's `check-run.conclusion` enum predates
3087
- // `"stale"`. The twin faithfully emits a real upstream value; narrow it to
3088
- // the schema's declared union for the type anchor only (twin
3089
- // tracks live GitHub, not a lagging spec snapshot), runtime value unchanged.
3090
3084
  conclusion: run.conclusion,
3091
3085
  started_at: run.started_at,
3092
3086
  completed_at: run.completed_at,
@@ -4,7 +4,7 @@ export { DEFAULT_GMAIL_AGENT_EMAIL, GmailError, SEARCH_MAILBOX_MESSAGE_BUDGET, a
4
4
  import './chunk-JWJYNAWI.js';
5
5
  import { routeInputDeclarer, integerInput, booleanInput, repeatedInput, mountDeclaredRoute, UndeclaredInputError, MalformedBodyError } from './chunk-IZFM7W2V.js';
6
6
  import { loadMcpToolFixture, deriveMcpToolTable, defineTwin, openTwinDatabase, createApp } from './chunk-TWURH7YM.js';
7
- import './chunk-5KFDRR53.js';
7
+ import './chunk-7VZBAHQ2.js';
8
8
  import './chunk-SG6ZTIMT.js';
9
9
  import './chunk-FBSA5L36.js';
10
10
  import { createHash, createHmac, timingSafeEqual, randomBytes } from 'node:crypto';
@@ -4,7 +4,7 @@ export { DEFAULT_LINEAR_CLOCK, DEFAULT_LINEAR_EMAIL, DEFAULT_LINEAR_PORT, DEFAUL
4
4
  import './chunk-JWJYNAWI.js';
5
5
  import { routeInputDeclarer, mountDeclaredRoute, UndeclaredInputError } from './chunk-IZFM7W2V.js';
6
6
  import { loadMcpToolFixture, deriveMcpToolTable, openTwinDatabase, defineTwin, createApp } from './chunk-TWURH7YM.js';
7
- import './chunk-5KFDRR53.js';
7
+ import './chunk-7VZBAHQ2.js';
8
8
  import './chunk-SG6ZTIMT.js';
9
9
  import './chunk-FBSA5L36.js';
10
10
  import { createHmac, timingSafeEqual, createHash, randomBytes } from 'node:crypto';
@@ -4,7 +4,7 @@ import { defaultSeedState, parseSeed } from './chunk-YBWG5JK2.js';
4
4
  export { defaultSeedState, loadSeedFromEnv, parseSeed, seedSchema } from './chunk-YBWG5JK2.js';
5
5
  import { routeInputDeclarer, booleanInput, integerInput, mountDeclaredRoute, UndeclaredInputError } from './chunk-IZFM7W2V.js';
6
6
  import { loadMcpToolFixture, defineTwin, queryTokenResolver, formTokenResolver, deriveMcpToolTable, UnknownToolError, openTwinDatabase, createApp, typeDisagreements } from './chunk-TWURH7YM.js';
7
- import './chunk-5KFDRR53.js';
7
+ import './chunk-7VZBAHQ2.js';
8
8
  import './chunk-SG6ZTIMT.js';
9
9
  import './chunk-FBSA5L36.js';
10
10
  import { z, ZodError } from 'zod';