@letterstory/cli 0.51.0 → 0.52.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.
package/README.md CHANGED
@@ -352,6 +352,13 @@ long-running agent can watch the fleet on a schedule without a browser.
352
352
  export LETTERSTORY_STAFF_API_KEY=<your staff key with tuning:experiment>
353
353
 
354
354
  letterstory admin tuning-context --q "colons" # what the agent knows (JSON)
355
+ letterstory admin tuning kernels # every catalog kernel: live version, writer, candidates
356
+ letterstory admin tuning kernel <service-kernel-id> --version live # one version's kernel file (JSON)
357
+ letterstory admin tuning candidate <service-kernel-id> --file hazel.json --note "Echo as the writer"
358
+ letterstory admin tuning compare --title "Echo vs live" --candidates Hazel:21,Aspen:20
359
+ letterstory admin tuning comparison <id> # per-measure verdicts (better / worse / within noise / refused)
360
+ letterstory admin tuning picks <id> # the blind queue; then: admin tuning pick <id> --cell <key> --pick left|right|tie
361
+ letterstory admin tuning promote <experiment-id> --versions <service-kernel-id>:21 # from a concluded "ship" decision
355
362
  letterstory admin experiments list --q colons # what we've tried
356
363
  letterstory admin experiments new "We use too many colons" --as "claude-code"
357
364
  letterstory admin experiments update <id> --plan-file plan.json --budget 120
package/lib/cli.mjs CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  } from "./commands.mjs";
55
55
 
56
56
  // Keep in sync with cli/package.json.
57
- export const VERSION = "0.51.0";
57
+ export const VERSION = "0.52.0";
58
58
 
59
59
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
60
60
  // can't accidentally swallow the id as --json's value.
@@ -7,6 +7,7 @@
7
7
  // admin experiments list|show|new|update|note|started|spend|conclude|abandon … (tuning:experiment)
8
8
  // admin tuning-context [--q "<question>"] (tuning:experiment)
9
9
  // admin tuning-import <brief-results.json> [--experiment <id>] (tuning:experiment)
10
+ // admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|promote … (tuning:experiment)
10
11
  // admin evals scoreboard|cases|runs|run|triage|guard|retire … (fleet:read / tuning:experiment)
11
12
  // admin crm import|capture|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
12
13
  // admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
@@ -412,6 +413,144 @@ async function cmdAdminTuningContext(ctx) {
412
413
  );
413
414
  }
414
415
 
416
+ /* ------------------------------------------------------------ the tuning console's Write area */
417
+
418
+ /**
419
+ * `admin tuning …` — kernel versions as files, comparisons of a candidate against live, blind picks
420
+ * and promotion from the ledger (docs/architecture/tuning-foundation.md): the headless half of the
421
+ * tuning console's Write › Kernels and Compare tabs. Needs `tuning:experiment` on the key.
422
+ *
423
+ * admin tuning kernels every catalog kernel, live version, writer, candidates
424
+ * admin tuning kernel <service-kernel-id> [--version N|live] one version's file (JSON)
425
+ * admin tuning candidate <service-kernel-id> --file f.json [--note "…"] save a file as a candidate version
426
+ * admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] start a comparison
427
+ * admin tuning comparisons | comparison <id> list / read one (per-measure verdicts)
428
+ * admin tuning picks <comparison-id> the blind queue (next pairs)
429
+ * admin tuning pick <comparison-id> --cell <key> --pick left|right|tie record a blind pick
430
+ * admin tuning promote <experiment-id> --versions <service-kernel-id>:<version>,… promote from a "ship" decision
431
+ */
432
+ async function cmdAdminTuning(ctx) {
433
+ const [sub, id] = ctx.positionals;
434
+ const { flags, io } = ctx;
435
+ const json = flagBool(flags.json);
436
+ const T = "/api/superadmin/tuning";
437
+ switch (sub) {
438
+ case "kernels": {
439
+ const r = await staffRequest(ctx, "GET", `${T}/kernel-files`);
440
+ if (json) return printResult(io, flags, r);
441
+ return io.log(
442
+ r.kernels
443
+ .map((k) => {
444
+ const c = k.versions
445
+ .filter((v) => v.status === "candidate")
446
+ .map((v) => `v${v.version} ${v.writer}`);
447
+ return `${k.kernel.padEnd(12)} live v${k.live} ${k.writer} ${k.serviceKernelId}${c.length ? `\n candidates: ${c.join(", ")}` : ""}`;
448
+ })
449
+ .join("\n") || "(no kernels)"
450
+ );
451
+ }
452
+ case "kernel": {
453
+ if (!id) throw new CliError("Usage: admin tuning kernel <service-kernel-id> [--version N|live]");
454
+ const version = flagStr(flags.version) ?? "live";
455
+ return printResult(
456
+ io,
457
+ { ...flags, json: true },
458
+ await staffRequest(ctx, "GET", `${T}/kernel-files/${id}/versions/${version}`)
459
+ );
460
+ }
461
+ case "candidate": {
462
+ const path = flagStr(flags.file);
463
+ let file;
464
+ if (path) {
465
+ try {
466
+ file = JSON.parse(readFileSync(path, "utf8"));
467
+ } catch (err) {
468
+ throw new CliError(`--file ${path}: ${err.message}`);
469
+ }
470
+ }
471
+ if (!id || !file)
472
+ throw new CliError('Usage: admin tuning candidate <service-kernel-id> --file kernel.json [--note "…"]');
473
+ const r = await staffRequest(
474
+ ctx,
475
+ "POST",
476
+ `${T}/kernel-files/${id}/versions`,
477
+ compact({ file, note: flagStr(flags.note) })
478
+ );
479
+ return json ? printResult(io, flags, r) : io.log(`saved candidate v${r.version}`);
480
+ }
481
+ case "compare": {
482
+ const title = flagStr(flags.title);
483
+ const list = flagList(flags.candidates);
484
+ if (!title || !list?.length)
485
+ throw new CliError(
486
+ 'Usage: admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>]'
487
+ );
488
+ const candidates = list.map((item) => {
489
+ const at = item.lastIndexOf(":");
490
+ const version = Number(item.slice(at + 1));
491
+ if (at < 1 || !Number.isInteger(version))
492
+ throw new CliError(`--candidates items are kernel:version, got "${item}"`);
493
+ return { kernel: item.slice(0, at), candidate_version: version };
494
+ });
495
+ const r = await staffRequest(
496
+ ctx,
497
+ "POST",
498
+ `${T}/comparisons`,
499
+ compact({ title, candidates, experiment_id: flagStr(flags.experiment) })
500
+ );
501
+ return json ? printResult(io, flags, r) : io.log(`comparison ${r.comparison?.id ?? r.id ?? ""} started`);
502
+ }
503
+ case "comparisons":
504
+ return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/comparisons`));
505
+ case "comparison":
506
+ if (!id) throw new CliError("Usage: admin tuning comparison <id>");
507
+ return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/comparisons/${id}`));
508
+ case "picks":
509
+ if (!id) throw new CliError("Usage: admin tuning picks <comparison-id>");
510
+ return printResult(
511
+ io,
512
+ { ...flags, json: true },
513
+ await staffRequest(ctx, "GET", `${T}/comparisons/${id}/picks`)
514
+ );
515
+ case "pick": {
516
+ const cell = flagStr(flags.cell);
517
+ const pick = flagStr(flags.pick);
518
+ if (!id || !cell || !["left", "right", "tie"].includes(pick ?? ""))
519
+ throw new CliError("Usage: admin tuning pick <comparison-id> --cell <key> --pick left|right|tie");
520
+ return printResult(
521
+ io,
522
+ flags,
523
+ await staffRequest(ctx, "POST", `${T}/comparisons/${id}/picks`, { cell_key: cell, outcome: pick })
524
+ );
525
+ }
526
+ case "promote": {
527
+ const list = flagList(flags.versions);
528
+ if (!id || !list?.length)
529
+ throw new CliError(
530
+ "Usage: admin tuning promote <experiment-id> --versions <service-kernel-id>:<version>,…"
531
+ );
532
+ const versions = list.map((item) => {
533
+ const at = item.lastIndexOf(":");
534
+ return { service_kernel_id: item.slice(0, at), version: Number(item.slice(at + 1)) };
535
+ });
536
+ return printResult(
537
+ io,
538
+ { ...flags, json: true },
539
+ await staffRequest(
540
+ ctx,
541
+ "POST",
542
+ `${EXPERIMENTS}/${id}/promote`,
543
+ compact({ versions, actor: actorFrom(flags) })
544
+ )
545
+ );
546
+ }
547
+ default:
548
+ throw new CliError(
549
+ "Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|promote (see --help)"
550
+ );
551
+ }
552
+ }
553
+
415
554
  /* ------------------------------------------------------------ the staff CRM */
416
555
 
417
556
  const CRM = "/api/superadmin/crm";
@@ -1078,8 +1217,9 @@ export async function cmdAdmin(ctx) {
1078
1217
  if (sub === "experiments") return cmdAdminExperiments({ ...ctx, positionals: ctx.positionals.slice(1) });
1079
1218
  if (sub === "tuning-context") return cmdAdminTuningContext({ ...ctx, positionals: ctx.positionals.slice(1) });
1080
1219
  if (sub === "tuning-import") return cmdAdminTuningImport({ ...ctx, positionals: ctx.positionals.slice(1) });
1220
+ if (sub === "tuning") return cmdAdminTuning({ ...ctx, positionals: ctx.positionals.slice(1) });
1081
1221
  if (sub === "evals") return cmdAdminEvals({ ...ctx, positionals: ctx.positionals.slice(1) });
1082
1222
  throw new CliError(
1083
- 'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin crm … | admin evals … | admin tuning-context (see --help)'
1223
+ 'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin crm … | admin evals … | admin tuning … | admin tuning-context (see --help)'
1084
1224
  );
1085
1225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",