@letterstory/cli 0.51.0 → 0.53.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,14 @@ 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 reading-list <id> --n 10 # your next blind pairs as links (Text A / Text B), to read off the screen; pick in the console
362
+ letterstory admin tuning promote <experiment-id> --versions <service-kernel-id>:21 # from a concluded "ship" decision
355
363
  letterstory admin experiments list --q colons # what we've tried
356
364
  letterstory admin experiments new "We use too many colons" --as "claude-code"
357
365
  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.53.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,155 @@ 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 reading-list <comparison-id> [--n 10] your next blind pairs as links, to read off the screen
431
+ * admin tuning promote <experiment-id> --versions <service-kernel-id>:<version>,… promote from a "ship" decision
432
+ */
433
+ async function cmdAdminTuning(ctx) {
434
+ const [sub, id] = ctx.positionals;
435
+ const { flags, io } = ctx;
436
+ const json = flagBool(flags.json);
437
+ const T = "/api/superadmin/tuning";
438
+ switch (sub) {
439
+ case "kernels": {
440
+ const r = await staffRequest(ctx, "GET", `${T}/kernel-files`);
441
+ if (json) return printResult(io, flags, r);
442
+ return io.log(
443
+ r.kernels
444
+ .map((k) => {
445
+ const c = k.versions
446
+ .filter((v) => v.status === "candidate")
447
+ .map((v) => `v${v.version} ${v.writer}`);
448
+ return `${k.kernel.padEnd(12)} live v${k.live} ${k.writer} ${k.serviceKernelId}${c.length ? `\n candidates: ${c.join(", ")}` : ""}`;
449
+ })
450
+ .join("\n") || "(no kernels)"
451
+ );
452
+ }
453
+ case "kernel": {
454
+ if (!id) throw new CliError("Usage: admin tuning kernel <service-kernel-id> [--version N|live]");
455
+ const version = flagStr(flags.version) ?? "live";
456
+ return printResult(
457
+ io,
458
+ { ...flags, json: true },
459
+ await staffRequest(ctx, "GET", `${T}/kernel-files/${id}/versions/${version}`)
460
+ );
461
+ }
462
+ case "candidate": {
463
+ const path = flagStr(flags.file);
464
+ let file;
465
+ if (path) {
466
+ try {
467
+ file = JSON.parse(readFileSync(path, "utf8"));
468
+ } catch (err) {
469
+ throw new CliError(`--file ${path}: ${err.message}`);
470
+ }
471
+ }
472
+ if (!id || !file)
473
+ throw new CliError('Usage: admin tuning candidate <service-kernel-id> --file kernel.json [--note "…"]');
474
+ const r = await staffRequest(
475
+ ctx,
476
+ "POST",
477
+ `${T}/kernel-files/${id}/versions`,
478
+ compact({ file, note: flagStr(flags.note) })
479
+ );
480
+ return json ? printResult(io, flags, r) : io.log(`saved candidate v${r.version}`);
481
+ }
482
+ case "compare": {
483
+ const title = flagStr(flags.title);
484
+ const list = flagList(flags.candidates);
485
+ if (!title || !list?.length)
486
+ throw new CliError(
487
+ 'Usage: admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>]'
488
+ );
489
+ const candidates = list.map((item) => {
490
+ const at = item.lastIndexOf(":");
491
+ const version = Number(item.slice(at + 1));
492
+ if (at < 1 || !Number.isInteger(version))
493
+ throw new CliError(`--candidates items are kernel:version, got "${item}"`);
494
+ return { kernel: item.slice(0, at), candidate_version: version };
495
+ });
496
+ const r = await staffRequest(
497
+ ctx,
498
+ "POST",
499
+ `${T}/comparisons`,
500
+ compact({ title, candidates, experiment_id: flagStr(flags.experiment) })
501
+ );
502
+ return json ? printResult(io, flags, r) : io.log(`comparison ${r.comparison?.id ?? r.id ?? ""} started`);
503
+ }
504
+ case "comparisons":
505
+ return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/comparisons`));
506
+ case "comparison":
507
+ if (!id) throw new CliError("Usage: admin tuning comparison <id>");
508
+ return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/comparisons/${id}`));
509
+ case "picks":
510
+ if (!id) throw new CliError("Usage: admin tuning picks <comparison-id>");
511
+ return printResult(
512
+ io,
513
+ { ...flags, json: true },
514
+ await staffRequest(ctx, "GET", `${T}/comparisons/${id}/picks`)
515
+ );
516
+ case "pick": {
517
+ const cell = flagStr(flags.cell);
518
+ const pick = flagStr(flags.pick);
519
+ if (!id || !cell || !["left", "right", "tie"].includes(pick ?? ""))
520
+ throw new CliError("Usage: admin tuning pick <comparison-id> --cell <key> --pick left|right|tie");
521
+ return printResult(
522
+ io,
523
+ flags,
524
+ await staffRequest(ctx, "POST", `${T}/comparisons/${id}/picks`, { cell_key: cell, outcome: pick })
525
+ );
526
+ }
527
+ case "reading-list": {
528
+ if (!id) throw new CliError("Usage: admin tuning reading-list <comparison-id> [--n 10]");
529
+ const n = flagStr(flags.n);
530
+ const r = await staffRequest(
531
+ ctx,
532
+ "GET",
533
+ `${T}/comparisons/${id}/reading-list${n ? `?${new URLSearchParams({ n })}` : ""}`
534
+ );
535
+ return json ? printResult(io, flags, r) : io.log(r.text);
536
+ }
537
+ case "promote": {
538
+ const list = flagList(flags.versions);
539
+ if (!id || !list?.length)
540
+ throw new CliError(
541
+ "Usage: admin tuning promote <experiment-id> --versions <service-kernel-id>:<version>,…"
542
+ );
543
+ const versions = list.map((item) => {
544
+ const at = item.lastIndexOf(":");
545
+ return { service_kernel_id: item.slice(0, at), version: Number(item.slice(at + 1)) };
546
+ });
547
+ return printResult(
548
+ io,
549
+ { ...flags, json: true },
550
+ await staffRequest(
551
+ ctx,
552
+ "POST",
553
+ `${EXPERIMENTS}/${id}/promote`,
554
+ compact({ versions, actor: actorFrom(flags) })
555
+ )
556
+ );
557
+ }
558
+ default:
559
+ throw new CliError(
560
+ "Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|reading-list|promote (see --help)"
561
+ );
562
+ }
563
+ }
564
+
415
565
  /* ------------------------------------------------------------ the staff CRM */
416
566
 
417
567
  const CRM = "/api/superadmin/crm";
@@ -1078,8 +1228,9 @@ export async function cmdAdmin(ctx) {
1078
1228
  if (sub === "experiments") return cmdAdminExperiments({ ...ctx, positionals: ctx.positionals.slice(1) });
1079
1229
  if (sub === "tuning-context") return cmdAdminTuningContext({ ...ctx, positionals: ctx.positionals.slice(1) });
1080
1230
  if (sub === "tuning-import") return cmdAdminTuningImport({ ...ctx, positionals: ctx.positionals.slice(1) });
1231
+ if (sub === "tuning") return cmdAdminTuning({ ...ctx, positionals: ctx.positionals.slice(1) });
1081
1232
  if (sub === "evals") return cmdAdminEvals({ ...ctx, positionals: ctx.positionals.slice(1) });
1082
1233
  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)'
1234
+ '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
1235
  );
1085
1236
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",