@letterstory/cli 0.98.0 → 0.98.1

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
@@ -58,7 +58,7 @@ import {
58
58
  } from "./commands.mjs";
59
59
 
60
60
  // Keep in sync with cli/package.json.
61
- export const VERSION = "0.98.0";
61
+ export const VERSION = "0.98.1";
62
62
 
63
63
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
64
64
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
@@ -92,6 +92,10 @@ export const BOOLEAN_FLAGS = new Set([
92
92
  "stdin",
93
93
  // `authors generate <id> --with-bios`.
94
94
  "with-bios",
95
+ // `admin tuning climb-new … --no-run`; `rung-propose --accept --with-replay` / `rung-accept --with-replay`
96
+ // (not `--replay`: `evals` takes `--replay <stage>`).
97
+ "no-run",
98
+ "with-replay",
95
99
  // `covers generate <id> --regenerate` and `arena run --gate --article <id>`
96
100
  // both put a value-less flag next to a value: without these entries the parser
97
101
  // hands the following token to the flag and the real argument disappears.
@@ -599,6 +599,20 @@ async function cmdAdminTuningContext(ctx) {
599
599
  * admin tuning trial-run <trial-id> [--repeats 2] queue runs (guardrails apply)
600
600
  * admin tuning trial-save <trial-id> [--note "…"] save a trial as a candidate version (never live)
601
601
  *
602
+ * The climb (docs/architecture/candidate-climb-2026-10-14.md): a candidate built from a kernel's FLOOR
603
+ * (the writer alone under the record's constraints) one rung of feedback at a time. Mark lines, rewrite
604
+ * them, pick sections, say what is wrong in a sentence; the proposer answers in your words and the dials';
605
+ * accept, and the next rung runs. Earlier feedback is watched on every later rung (gone / still there).
606
+ * admin tuning climb-new --title "…" --kernel <id|name> (--bank <input-id> | --input-file f.md | --input "…") [--kind …] [--version N] [--no-run]
607
+ * a session whose Base is the kernel's floor; its first run is queued unless --no-run
608
+ * admin tuning climb <session-id> the trail: each rung, what it changed, its run, how many earlier wishes it met; the ladder
609
+ * admin tuning ladder the floor, and the rungs a climb can add back by key (voice, rules-repair, house-style, …)
610
+ * admin tuning feedback-add --run <run-id> --span "…" [--comment "…"] [--fix "…"] [--section <key>] [--locus span|section|piece] [--polarity issue|strength]
611
+ * feedback on a rung's run (a climb); --locus piece with --comment for the whole piece; --locus section --section <key> --polarity strength keeps a section
612
+ * admin tuning rung-propose <session-id> [--trial <id>] [--feedback id,id] [--accept [--with-replay]] the next rung from the feedback on the head's run; --accept takes the first rung proposed
613
+ * admin tuning rung-accept <session-id> --parent <trial-id> (--preset <key> | --settings '[{dial,scope,value,plain,feedback_ids}]') [--name "…"] [--with-replay [--replay-n 4]]
614
+ * admin tuning watches <run-id> read every earlier feedback item against this run's text (gone / still there), and list the readings
615
+ *
602
616
  * The Write input bank: real kernel requests (above all the arena outlines Auto-Pen sends), as sent.
603
617
  * admin tuning inputs [--filter "origin=arena_outline;material=thin,typical"] [--q "…"] list by tag, with coverage per bucket
604
618
  * admin tuning input <input-id> one input in full: text, constraints, tags, run defaults, origin
@@ -1005,25 +1019,37 @@ async function cmdAdminTuning(ctx) {
1005
1019
  const side = flagStr(flags.side);
1006
1020
  const kind = flagStr(flags.kind);
1007
1021
  if (
1008
- !span ||
1022
+ (!span && flagStr(flags.locus) !== "piece") ||
1009
1023
  (side && !["control", "candidate"].includes(side)) ||
1010
1024
  (kind && !["narrow", "wide"].includes(kind))
1011
1025
  )
1012
1026
  throw new CliError(
1013
- 'Usage: admin tuning feedback-add --span "…" --comment "…" [--fix "…"] [--comparison <id> --cell <key> --side control|candidate] [--kind narrow|wide]'
1027
+ 'Usage: admin tuning feedback-add --span "…" --comment "…" [--fix "…"] [--comparison <id> --cell <key> --side control|candidate] [--kind narrow|wide] | --run <run-id> [--locus span|section|piece] [--section <key>] [--polarity issue|strength]'
1014
1028
  );
1029
+ const locus = flagStr(flags.locus);
1030
+ const polarity = flagStr(flags.polarity);
1031
+ if (
1032
+ (locus && !["span", "section", "piece"].includes(locus)) ||
1033
+ (polarity && !["issue", "strength"].includes(polarity))
1034
+ )
1035
+ throw new CliError("--locus is span|section|piece; --polarity is issue|strength");
1015
1036
  const r = await staffRequest(
1016
1037
  ctx,
1017
1038
  "POST",
1018
1039
  `${T}/proposals`,
1019
1040
  compact({
1020
- span,
1041
+ span: locus === "piece" && !flagStr(flags.span) ? "(whole piece)" : span,
1021
1042
  comment: flagStr(flags.comment),
1022
1043
  fix: flagStr(flags.fix),
1023
1044
  comparison_id: flagStr(flags.comparison),
1024
1045
  cell_key: flagStr(flags.cell),
1025
1046
  side,
1026
1047
  kind,
1048
+ // A climb: feedback on a rung's run, about a span, one section or the whole piece.
1049
+ run_id: flagStr(flags.run),
1050
+ locus,
1051
+ section_key: flagStr(flags.section),
1052
+ polarity,
1027
1053
  })
1028
1054
  );
1029
1055
  if (json) return printResult(io, flags, r);
@@ -1603,6 +1629,171 @@ async function cmdAdminTuning(ctx) {
1603
1629
  `${r.kernel}: saved as candidate v${r.version}${r.baseVersion ? ` (from v${r.baseVersion})` : ""}${r.leftBehind.length ? `; per-run, not saved: ${r.leftBehind.join(", ")}` : ""}`
1604
1630
  );
1605
1631
  }
1632
+ // --- The climb: from the floor, one rung of feedback at a time (lib/tuning/climb.ts) ---
1633
+ case "climb-new": {
1634
+ const title = flagStr(flags.title);
1635
+ const kernel = flagStr(flags.kernel);
1636
+ const path = flagStr(flags["input-file"]);
1637
+ let input = flagStr(flags.input);
1638
+ if (path) {
1639
+ try {
1640
+ input = readFileSync(path, "utf8");
1641
+ } catch (err) {
1642
+ throw new CliError(`--input-file ${path}: ${err.message}`);
1643
+ }
1644
+ }
1645
+ const bank = flagStr(flags.bank);
1646
+ if (!title || !kernel || (!input && !bank))
1647
+ throw new CliError(
1648
+ 'Usage: admin tuning climb-new --title "…" --kernel <id|name> (--bank <input-id> | --input-file f.md | --input "…") [--kind article|outline|brief] [--version N] [--no-run]'
1649
+ );
1650
+ let kernelId = kernel;
1651
+ if (!/^[0-9a-f-]{36}$/i.test(kernel)) {
1652
+ const { kernels } = await staffRequest(ctx, "GET", `${T}/kernels`);
1653
+ const matches = kernels.filter((k) => k.name.toLowerCase() === kernel.toLowerCase());
1654
+ const shared = matches.find((k) => !k.orgName) ?? matches[0];
1655
+ if (!shared) throw new CliError(`No kernel named ${kernel} (admin tuning base-kernels lists them).`);
1656
+ kernelId = shared.id;
1657
+ }
1658
+ const { session } = await staffRequest(
1659
+ ctx,
1660
+ "POST",
1661
+ T,
1662
+ compact({
1663
+ title,
1664
+ input_text: bank ? undefined : input,
1665
+ input_kind: flagStr(flags.kind) ?? "unknown",
1666
+ material: bank ? { source: "bank", input_id: bank } : undefined,
1667
+ base_kernel_id: kernelId,
1668
+ base_version: flagNum(flags.version),
1669
+ started_from: "floor",
1670
+ })
1671
+ );
1672
+ const floor = session.trials[0];
1673
+ let runs = [];
1674
+ if (!flagBool(flags["no-run"]) && floor && session.material?.ready !== false) {
1675
+ const r = await staffRequest(ctx, "POST", `${T}/trials/${floor.id}/run`, { repeats: 1 });
1676
+ runs = r.runs;
1677
+ }
1678
+ if (json) return printResult(io, flags, { session, runs });
1679
+ return io.log(
1680
+ `climb ${session.id} on ${session.baseKernelName}${session.baseVersion ? ` v${session.baseVersion}` : " (live)"}: floor trial ${floor?.id}${runs.length ? `, run ${runs[0].id} queued` : ""} → ${ctx.client.url}/superadmin/tuning/${session.id}`
1681
+ );
1682
+ }
1683
+ case "climb": {
1684
+ if (!id) throw new CliError("Usage: admin tuning climb <session-id>");
1685
+ const r = await staffRequest(ctx, "GET", `${T}/sessions/${id}/climb`);
1686
+ if (json) return printResult(io, flags, r);
1687
+ const lines = [`climb ${r.session_id} (from the ${r.started_from}); head ${r.head_trial_id ?? "—"}`];
1688
+ r.trail.forEach((rung, i) => {
1689
+ const w = rung.watches;
1690
+ const n = w.met + w.unmet + w.unread;
1691
+ lines.push(
1692
+ `${String(i).padStart(2)} ${rung.name} [${rung.trial_id}]${rung.run ? ` run ${rung.run.id} ${rung.run.status}${rung.run.words ? ` · ${rung.run.words} words` : ""}` : " (no run)"}${n ? ` · ${w.met} of ${n} wishes met` : ""}${rung.replay_screen_id ? ` · replay ${rung.replay_screen_id}` : ""}`
1693
+ );
1694
+ for (const s of rung.settings ?? []) lines.push(` ${s.plain}`);
1695
+ if (rung.changes.length) lines.push(` (${rung.changes.join("; ")})`);
1696
+ });
1697
+ const open = r.feedback.filter((f) => f.status === "proposed");
1698
+ lines.push(`feedback: ${r.feedback.length} (${open.length} not yet answered)`);
1699
+ for (const f of open.slice(0, 10))
1700
+ lines.push(
1701
+ ` ${f.id} ${f.locus}${f.section_key ? ` ${f.section_key}` : ""} ${f.polarity}: ${f.comment || f.span.slice(0, 80)}`
1702
+ );
1703
+ lines.push(`ladder: ${r.ladder.map((l) => `${l.key}${l.on ? " (on)" : ""}`).join(", ")}`);
1704
+ return io.log(lines.join("\n"));
1705
+ }
1706
+ case "ladder": {
1707
+ const r = await staffRequest(ctx, "GET", `${T}/ladder`);
1708
+ if (json) return printResult(io, flags, r);
1709
+ return io.log(
1710
+ [`floor: ${r.floor.what}`, ...r.ladder.map((l) => `${l.key.padEnd(16)} ${l.label} — ${l.help}`)].join(
1711
+ "\n"
1712
+ )
1713
+ );
1714
+ }
1715
+ case "rung-propose": {
1716
+ if (!id)
1717
+ throw new CliError(
1718
+ "Usage: admin tuning rung-propose <session-id> [--trial <id>] [--feedback id,id] [--accept [--with-replay]]"
1719
+ );
1720
+ const r = await staffRequest(
1721
+ ctx,
1722
+ "POST",
1723
+ `${T}/sessions/${id}/climb/propose`,
1724
+ compact({ trial_id: flagStr(flags.trial), feedback_ids: flagList(flags.feedback) ?? undefined })
1725
+ );
1726
+ let accepted = null;
1727
+ if (flagBool(flags.accept) && r.rungs[0]) {
1728
+ accepted = await staffRequest(
1729
+ ctx,
1730
+ "POST",
1731
+ `${T}/sessions/${id}/climb/rungs`,
1732
+ compact({
1733
+ parent_trial_id: r.parent_trial_id,
1734
+ name: r.rungs[0].name,
1735
+ settings: r.rungs[0].settings,
1736
+ replay: flagBool(flags["with-replay"]) || undefined,
1737
+ replay_n: flagNum(flags["replay-n"]),
1738
+ })
1739
+ );
1740
+ }
1741
+ if (json) return printResult(io, flags, { ...r, accepted });
1742
+ const lines = [];
1743
+ r.rungs.forEach((rung, i) => {
1744
+ lines.push(`rung ${i + 1}: ${rung.name}`);
1745
+ for (const s of rung.settings)
1746
+ lines.push(` ${s.plain} (${s.scope}:${s.dial}; answers ${s.feedback_ids.join(", ") || "—"})`);
1747
+ lines.push(` changes: ${rung.changes.join("; ")}`);
1748
+ });
1749
+ for (const u of r.unsupported)
1750
+ lines.push(`no setting expresses${u.feedback_id ? ` ${u.feedback_id}` : ""}: ${u.what}`);
1751
+ for (const x of r.rejected) lines.push(`dropped: ${x.dial} (${x.reason})`);
1752
+ if (accepted)
1753
+ lines.push(
1754
+ `accepted as trial ${accepted.trial.id} (${accepted.trial.name}); run ${accepted.runs[0]?.id ?? "—"} queued${accepted.replay_screen_id ? `; replay ${accepted.replay_screen_id}` : ""}`
1755
+ );
1756
+ return io.log(lines.join("\n") || "(nothing proposed)");
1757
+ }
1758
+ case "rung-accept": {
1759
+ const parent = flagStr(flags.parent);
1760
+ const preset = flagStr(flags.preset);
1761
+ const settings = flags.settings ? jsonFlag(flags.settings, "--settings") : undefined;
1762
+ if (!id || !parent || (!preset && !settings))
1763
+ throw new CliError(
1764
+ "Usage: admin tuning rung-accept <session-id> --parent <trial-id> (--preset <key> | --settings '[…]') [--name \"…\"] [--with-replay [--replay-n 4]]"
1765
+ );
1766
+ const r = await staffRequest(
1767
+ ctx,
1768
+ "POST",
1769
+ `${T}/sessions/${id}/climb/rungs`,
1770
+ compact({
1771
+ parent_trial_id: parent,
1772
+ name: flagStr(flags.name),
1773
+ preset,
1774
+ settings,
1775
+ replay: flagBool(flags["with-replay"]) || undefined,
1776
+ replay_n: flagNum(flags["replay-n"]),
1777
+ })
1778
+ );
1779
+ if (json) return printResult(io, flags, r);
1780
+ return io.log(
1781
+ `rung ${r.trial.id} (${r.trial.name}); run ${r.runs[0]?.id ?? "—"} queued${r.replay_screen_id ? `; replay ${r.replay_screen_id}` : ""}${r.filed.length ? `; ${r.filed.length} feedback filed as config` : ""}`
1782
+ );
1783
+ }
1784
+ case "watches": {
1785
+ if (!id) throw new CliError("Usage: admin tuning watches <run-id>");
1786
+ const r = await staffRequest(ctx, "POST", `${T}/runs/${id}/watches`, {});
1787
+ if (json) return printResult(io, flags, r);
1788
+ return io.log(
1789
+ r.readings
1790
+ .map(
1791
+ (w) =>
1792
+ `${w.proposal_id} ${w.verdict}${w.evidence ? ` “${w.evidence.slice(0, 100)}”` : ""} (${w.method})`
1793
+ )
1794
+ .join("\n") || "(nothing to watch yet)"
1795
+ );
1796
+ }
1606
1797
  // --- The Write input bank: real kernel requests a session can start from ---
1607
1798
  case "inputs": {
1608
1799
  const q = new URLSearchParams(
@@ -2319,7 +2510,7 @@ async function cmdAdminTuning(ctx) {
2319
2510
  }
2320
2511
  default:
2321
2512
  throw new CliError(
2322
- "Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|loop|ledger|checklist|checklist-add|checklist-set|checklist-labels|feedback|feedback-add|feedback-file|feedback-reject|reading-list|finalists|finalists-queue|pair|bank|bank-eval|bank-draft|bank-shape|bank-pool|bank-judge|bank-score|bank-try|people|invite|revoke|assignments|assign|unassign|tasks|side-by-side|candidates|base-kernels|base-versions|sessions|session|session-new|propose|trial-add|trial-run|trial-save|promote|arenas|arena|arena-candidate|arena-dials|comparison-extend|research-compare|research-inputs|research-split|research-standing|promote-arena|shred-compare|shred-inputs|shred-split|shred-standing|shredders|shredder|shredder-candidate|promote-shredder|enhance-compare|enhance-inputs|enhance-split|enhance-standing|enhancers|enhancer|enhancer-candidate|promote-enhancer (see --help)"
2513
+ "Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|loop|ledger|checklist|checklist-add|checklist-set|checklist-labels|feedback|feedback-add|feedback-file|feedback-reject|reading-list|finalists|finalists-queue|pair|bank|bank-eval|bank-draft|bank-shape|bank-pool|bank-judge|bank-score|bank-try|people|invite|revoke|assignments|assign|unassign|tasks|side-by-side|candidates|base-kernels|base-versions|sessions|session|session-new|propose|trial-add|trial-run|trial-save|climb-new|climb|ladder|rung-propose|rung-accept|watches|promote|arenas|arena|arena-candidate|arena-dials|comparison-extend|research-compare|research-inputs|research-split|research-standing|promote-arena|shred-compare|shred-inputs|shred-split|shred-standing|shredders|shredder|shredder-candidate|promote-shredder|enhance-compare|enhance-inputs|enhance-split|enhance-standing|enhancers|enhancer|enhancer-candidate|promote-enhancer (see --help)"
2323
2514
  );
2324
2515
  }
2325
2516
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.98.0",
3
+ "version": "0.98.1",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",