@letterstory/cli 0.72.1 → 0.72.2

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/lib/cli.mjs CHANGED
@@ -56,7 +56,7 @@ import {
56
56
  } from "./commands.mjs";
57
57
 
58
58
  // Keep in sync with cli/package.json.
59
- export const VERSION = "0.72.1";
59
+ export const VERSION = "0.72.2";
60
60
 
61
61
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
62
62
  // can't accidentally swallow the id as --json's value.
@@ -646,7 +646,9 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
646
646
  (inputs, input, inputs-harvest, input-save),
647
647
  screens (screen-new, screens, screen,
648
648
  screen-advance), the bake-off (bakeoff,
649
- bakeoff-pick, bakeoff-merge),
649
+ bakeoff-pick, bakeoff-merge), programmes
650
+ (programmes, programme, programme-new,
651
+ programme-stop, programme-advance),
650
652
  and the arena, shredder
651
653
  and enhancer twins (every flag is listed in
652
654
  cli/lib/commands/admin.mjs).
@@ -514,6 +514,15 @@ async function cmdAdminTuningContext(ctx) {
514
514
  * admin tuning bakeoff-pick <screen-id> --input <key> --item <category:id|axis:id> --winner <version|none> a person's grade on one input and item (replaces the machine's)
515
515
  * admin tuning bakeoff-merge <screen-id> [--resolve item=version,…] [--note "…"] [--compare 10] save the merged file as a candidate of the base kernel (never live); --compare starts a comparison against live on that many pairs
516
516
  *
517
+ * Programmes: the bake-off run by the machine, generation after generation, on the cron, with a run
518
+ * budget and stop rules (the budget; N flat generations; a finalist that won its picks). Each
519
+ * generation screens the base against its challengers, merges the winners, compares the merge
520
+ * against live; a merge that cleared the gates and lost no craft becomes the base. People pick.
521
+ * admin tuning programmes | programme <id> list / one programme: every generation, its table, merge and comparison, and what happens next
522
+ * admin tuning programme-new --title "…" --kernel <kernel-id|service-kernel-id> --seeds 21,22,23 [--inputs id,…|--n 8] [--filter "…"] [--pairs 10] --budget 200 [--flat 2]
523
+ * admin tuning programme-stop <id> [--reason "…"] stop it by hand
524
+ * admin tuning programme-advance <id> one manual tick (the cron takes the same step every minute)
525
+ *
517
526
  * Research (Phase 3): our arena's versions as files, research comparisons, the deploy split.
518
527
  * admin tuning arenas | arena <version-id> our arena's versions / one version's file and diff
519
528
  * admin tuning arena-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
@@ -573,6 +582,49 @@ function tagFilterParams(raw) {
573
582
  * A session as an agent reads it: each trial's changes and runs with their headline numbers, and
574
583
  * (with `texts`) the latest finished text per trial. The full session (traces, records) is `--json`.
575
584
  */
585
+ /** A programme as a person reads it: the row, then one block per generation, then what happens next. */
586
+ function printProgramme(io, r) {
587
+ const p = r.programme;
588
+ io.log(
589
+ `programme ${p.title} (${p.id}): ${p.status}${p.stop_reason ? ` — ${p.stop_reason}` : ""} · generation ${p.generation} · base ${p.base_version === null ? "live" : `v${p.base_version}`} · ${p.runs_used}/${p.budget_runs} runs · seeds v${p.seed_versions.join(", v")} · ${p.pairs} pairs/generation · stop after ${p.max_flat_generations} flat`
590
+ );
591
+ for (const g of r.generations) {
592
+ const sm = g.summary ?? {};
593
+ io.log("");
594
+ io.log(
595
+ `generation ${g.generation}: ${g.status} · screen ${g.screen_id} · v${g.versions.join(", v")}${g.merged_version !== null ? ` · merged v${g.merged_version}` : ""}${g.comparison_id ? ` · comparison ${g.comparison_id}` : ""}`
596
+ );
597
+ if (sm.from)
598
+ io.log(
599
+ ` from: base ${sm.from.base === null ? "live" : `v${sm.from.base}`}${sm.from.seeds ? ` · seeds v${sm.from.seeds.join(", v")}` : ""}${sm.from.strongest?.length ? ` · strongest v${sm.from.strongest.join(", v")}` : ""}${sm.from.merged != null ? ` · merged v${sm.from.merged}` : ""}${sm.from.mutations?.length ? ` · mutations ${sm.from.mutations.map((m) => `${m.category} → v${m.version}`).join(", ")}` : ""}`
600
+ );
601
+ if (sm.leader !== undefined)
602
+ io.log(
603
+ ` leader ${sm.leader === null ? "none" : `v${sm.leader}`}${sm.flat ? " · flat (nothing moved off the base)" : ""}`
604
+ );
605
+ for (const row of sm.table ?? [])
606
+ if (row.leader !== null && row.leader !== sm.base)
607
+ io.log(
608
+ ` ${row.item.id.padEnd(32)} ${row.trusted ? "trusted " : "unvetted "} v${row.leader} ${Object.entries(
609
+ row.wins
610
+ )
611
+ .map(([v, n]) => `v${v}:${n}`)
612
+ .join(" ")}`
613
+ );
614
+ if (sm.merge)
615
+ io.log(
616
+ ` merge: from v${sm.merge.base} → v${sm.merge.version}; applied ${sm.merge.applied.map((a) => `${a.item} from v${a.from}`).join(", ") || "nothing"}${sm.merge.conflicts.length ? `; conflicts kept the base: ${sm.merge.conflicts.map((c) => c.path).join(", ")}` : ""}`
617
+ );
618
+ if (sm.comparison)
619
+ io.log(
620
+ ` comparison: quality ${sm.comparison.quality ?? "unread"}${sm.comparison.guards_worse.length ? ` · guards worse: ${sm.comparison.guards_worse.join(", ")}` : " · gates clean"} · ${sm.comparison.finalist ? "finalist" : "not a finalist"}${sm.comparison.picks ? ` · picks ${sm.comparison.picks}` : ""}${sm.comparison.delta?.ceiling != null ? ` · ceiling ${sm.comparison.delta.ceiling > 0 ? "+" : ""}${sm.comparison.delta.ceiling}` : ""}${sm.comparison.next_arm.length ? ` · next arm: ${sm.comparison.next_arm.map((a) => `${a.category}${a.patch ? " (patch)" : ""}`).join(", ")}` : ""}`
621
+ );
622
+ if (sm.error) io.log(` error: ${sm.error}`);
623
+ }
624
+ io.log("");
625
+ io.log(`next: ${r.next}`);
626
+ }
627
+
576
628
  function sessionSummary(session, texts) {
577
629
  const runsOf = (trialId) => session.runs.filter((r) => r.trialId === trialId);
578
630
  return {
@@ -1635,6 +1687,95 @@ async function cmdAdminTuning(ctx) {
1635
1687
  );
1636
1688
  return;
1637
1689
  }
1690
+ // --- Programmes: the bake-off run by the machine over generations ---
1691
+ case "programmes": {
1692
+ const r = await staffRequest(ctx, "GET", `${T}/programmes?limit=${flagNum(flags.limit) ?? 30}`);
1693
+ if (json) return printResult(io, flags, r);
1694
+ return io.log(
1695
+ r.programmes
1696
+ .map(
1697
+ (x) =>
1698
+ `${x.id} ${x.status.padEnd(7)} ${x.title} · generation ${x.generation} · base ${x.base_version === null ? "live" : `v${x.base_version}`} · ${x.runs_used}/${x.budget_runs} runs${x.stop_reason ? ` · ${x.stop_reason}` : ""}`
1699
+ )
1700
+ .join("\n") || "(no programmes)"
1701
+ );
1702
+ }
1703
+ case "programme": {
1704
+ if (!id) throw new CliError("Usage: admin tuning programme <programme-id>");
1705
+ const r = await staffRequest(ctx, "GET", `${T}/programmes/${id}`);
1706
+ if (json) return printResult(io, flags, r);
1707
+ printProgramme(io, r);
1708
+ return;
1709
+ }
1710
+ case "programme-new": {
1711
+ const title = flagStr(flags.title);
1712
+ const kernel = flagStr(flags.kernel);
1713
+ const seeds = flagList(flags.seeds)
1714
+ .flatMap((x) => x.split(","))
1715
+ .map((x) => Number(x.trim()))
1716
+ .filter((x) => Number.isFinite(x));
1717
+ const budget = flagNum(flags.budget);
1718
+ if (!title || !kernel || !seeds.length || budget === undefined)
1719
+ throw new CliError(
1720
+ 'Usage: admin tuning programme-new --title "…" --kernel <kernel-id|service-kernel-id> --seeds 21,22,23 [--inputs id,…|--n 8] [--filter "…"] [--pairs 10] --budget 200 [--flat 2]'
1721
+ );
1722
+ const filter = Object.fromEntries(
1723
+ Object.entries(tagFilterParams(flagStr(flags.filter))).map(([k, v]) => [
1724
+ k,
1725
+ v
1726
+ .split(",")
1727
+ .map((x) => x.trim())
1728
+ .filter(Boolean),
1729
+ ])
1730
+ );
1731
+ const inputIds = flagList(flags.inputs).flatMap((x) =>
1732
+ x
1733
+ .split(",")
1734
+ .map((y) => y.trim())
1735
+ .filter(Boolean)
1736
+ );
1737
+ const r = await staffRequest(
1738
+ ctx,
1739
+ "POST",
1740
+ `${T}/programmes`,
1741
+ compact({
1742
+ title,
1743
+ base_kernel_id: kernel,
1744
+ seed_versions: seeds,
1745
+ input_ids: inputIds.length ? inputIds : undefined,
1746
+ n: flagNum(flags.n),
1747
+ filter: Object.keys(filter).length ? filter : undefined,
1748
+ pairs: flagNum(flags.pairs),
1749
+ budget_runs: budget,
1750
+ max_flat_generations: flagNum(flags.flat),
1751
+ })
1752
+ );
1753
+ if (json) return printResult(io, flags, r);
1754
+ return io.log(
1755
+ `programme ${r.programme.id}: generation 1 screening v${r.programme.seed_versions.join(", v")} against live; budget ${r.programme.budget_runs} runs · ${r.next}`
1756
+ );
1757
+ }
1758
+ case "programme-stop": {
1759
+ if (!id) throw new CliError('Usage: admin tuning programme-stop <programme-id> [--reason "…"]');
1760
+ const r = await staffRequest(
1761
+ ctx,
1762
+ "POST",
1763
+ `${T}/programmes/${id}/stop`,
1764
+ compact({ reason: flagStr(flags.reason) })
1765
+ );
1766
+ if (json) return printResult(io, flags, r);
1767
+ return io.log(`programme ${r.programme.id} ${r.programme.status}: ${r.programme.stop_reason}`);
1768
+ }
1769
+ case "programme-advance": {
1770
+ if (!id) throw new CliError("Usage: admin tuning programme-advance <programme-id>");
1771
+ const r = await staffRequest(ctx, "POST", `${T}/programmes/${id}/advance`, {});
1772
+ if (json) return printResult(io, flags, r);
1773
+ if (!r.transitions.length) io.log("nothing to do yet");
1774
+ for (const t of r.transitions)
1775
+ io.log(`generation ${t.generation}: ${t.from} → ${t.to}${t.note ? ` (${t.note})` : ""}`);
1776
+ if (r.read) io.log(`next: ${r.read.next}`);
1777
+ return;
1778
+ }
1638
1779
  // --- Research: our arena's versions, research comparisons, the deploy split ---
1639
1780
  case "arenas": {
1640
1781
  const r = await staffRequest(ctx, "GET", `${T}/arena-files`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.72.1",
3
+ "version": "0.72.2",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",