@letterstory/cli 0.52.0 → 0.54.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 +9 -0
- package/lib/cli.mjs +1 -1
- package/lib/commands/admin.mjs +128 -1
- package/lib/commands/arena.mjs +81 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -358,6 +358,7 @@ letterstory admin tuning candidate <service-kernel-id> --file hazel.json --note
|
|
|
358
358
|
letterstory admin tuning compare --title "Echo vs live" --candidates Hazel:21,Aspen:20
|
|
359
359
|
letterstory admin tuning comparison <id> # per-measure verdicts (better / worse / within noise / refused)
|
|
360
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
|
|
361
362
|
letterstory admin tuning promote <experiment-id> --versions <service-kernel-id>:21 # from a concluded "ship" decision
|
|
362
363
|
letterstory admin experiments list --q colons # what we've tried
|
|
363
364
|
letterstory admin experiments new "We use too many colons" --as "claude-code"
|
|
@@ -519,6 +520,14 @@ letterstory arena endpoint config <arena-id> [--file cfg.json]
|
|
|
519
520
|
letterstory arena endpoint update <arena-id> --name "Docs research v2" # creator or org admin only
|
|
520
521
|
letterstory arena endpoint delete <arena-id> --yes
|
|
521
522
|
|
|
523
|
+
# The research agent as ONE versioned file (like a kernel file; schema at /api/schema/arena):
|
|
524
|
+
letterstory arena file <arena-id> > arena.json # the file the arena runs now
|
|
525
|
+
letterstory arena file <arena-id> --patch '{"rules":{"context-figures":"measure"}}' --note "try it"
|
|
526
|
+
letterstory arena file <arena-id> --file arena.json # save a whole file as the next version
|
|
527
|
+
letterstory arena versions <arena-id> # your copy's versions and our releases
|
|
528
|
+
letterstory arena pin <arena-id> <version-id|none> # pin one of our releases, or follow live
|
|
529
|
+
letterstory arena offer <arena-id> [--accept] # our newer version, over your own changes
|
|
530
|
+
|
|
522
531
|
# Ledes: a prospect's context in, ready-to-send opening lines out (chosen from a
|
|
523
532
|
# hand-approved library, never generated; the top choice is flagged). Synchronous.
|
|
524
533
|
letterstory lede create --name "Clay outbound table"
|
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.
|
|
57
|
+
export const VERSION = "0.54.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.
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -427,8 +427,31 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
427
427
|
* admin tuning comparisons | comparison <id> list / read one (per-measure verdicts)
|
|
428
428
|
* admin tuning picks <comparison-id> the blind queue (next pairs)
|
|
429
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
|
|
430
431
|
* admin tuning promote <experiment-id> --versions <service-kernel-id>:<version>,… promote from a "ship" decision
|
|
432
|
+
*
|
|
433
|
+
* Research (Phase 3): our arena's versions as files, research comparisons, the deploy split.
|
|
434
|
+
* admin tuning arenas | arena <version-id> our arena's versions / one version's file and diff
|
|
435
|
+
* admin tuning arena-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
|
|
436
|
+
* admin tuning research-compare --title "…" (--candidate <version-id> | --patch '{…}' | --null-check) [--keys …]
|
|
437
|
+
* admin tuning research-inputs [--set v1] research's fixed inputs
|
|
438
|
+
* admin tuning research-split [--since-hours 72] [--at <iso>,…] what each deploy did to research in production
|
|
439
|
+
* admin tuning research-standing live's level on the fixed inputs, week by week
|
|
440
|
+
* admin tuning promote-arena <experiment-id> --version <arena-version-id> promote our arena from a "ship" decision
|
|
431
441
|
*/
|
|
442
|
+
/** `--flag '<json>'` → the parsed object, or undefined when the flag is absent. */
|
|
443
|
+
function jsonFlag(raw, name) {
|
|
444
|
+
const text = flagStr(raw);
|
|
445
|
+
if (text === undefined) return undefined;
|
|
446
|
+
try {
|
|
447
|
+
const v = JSON.parse(text);
|
|
448
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error("not an object");
|
|
449
|
+
return v;
|
|
450
|
+
} catch (err) {
|
|
451
|
+
throw new CliError(`${name} is not a JSON object: ${err.message}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
432
455
|
async function cmdAdminTuning(ctx) {
|
|
433
456
|
const [sub, id] = ctx.positionals;
|
|
434
457
|
const { flags, io } = ctx;
|
|
@@ -523,6 +546,110 @@ async function cmdAdminTuning(ctx) {
|
|
|
523
546
|
await staffRequest(ctx, "POST", `${T}/comparisons/${id}/picks`, { cell_key: cell, outcome: pick })
|
|
524
547
|
);
|
|
525
548
|
}
|
|
549
|
+
case "reading-list": {
|
|
550
|
+
if (!id) throw new CliError("Usage: admin tuning reading-list <comparison-id> [--n 10]");
|
|
551
|
+
const n = flagStr(flags.n);
|
|
552
|
+
const r = await staffRequest(
|
|
553
|
+
ctx,
|
|
554
|
+
"GET",
|
|
555
|
+
`${T}/comparisons/${id}/reading-list${n ? `?${new URLSearchParams({ n })}` : ""}`
|
|
556
|
+
);
|
|
557
|
+
return json ? printResult(io, flags, r) : io.log(r.text);
|
|
558
|
+
}
|
|
559
|
+
// --- Research: our arena's versions, research comparisons, the deploy split ---
|
|
560
|
+
case "arenas": {
|
|
561
|
+
const r = await staffRequest(ctx, "GET", `${T}/arena-files`);
|
|
562
|
+
if (json) return printResult(io, flags, r);
|
|
563
|
+
return io.log(
|
|
564
|
+
r.versions
|
|
565
|
+
.map((v) => `${v.label.padEnd(18)} ${v.status.padEnd(9)} ${v.id}${v.note ? ` ${v.note}` : ""}`)
|
|
566
|
+
.join("\n") || "(no versions)"
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
case "arena": {
|
|
570
|
+
if (!id) throw new CliError("Usage: admin tuning arena <version-id>");
|
|
571
|
+
return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/arena-files/${id}`));
|
|
572
|
+
}
|
|
573
|
+
case "arena-candidate": {
|
|
574
|
+
const patch = jsonFlag(flags.patch, "--patch");
|
|
575
|
+
const file = jsonFile({ "arena-file": flags.file }, "arena");
|
|
576
|
+
if ((patch === undefined) === (file === undefined))
|
|
577
|
+
throw new CliError(
|
|
578
|
+
'Usage: admin tuning arena-candidate --patch \'{"rules":{"context-figures":"off"}}\' | --file arena.json [--of <version-id>] [--note "…"]'
|
|
579
|
+
);
|
|
580
|
+
const r = await staffRequest(
|
|
581
|
+
ctx,
|
|
582
|
+
"POST",
|
|
583
|
+
`${T}/arena-files`,
|
|
584
|
+
compact({ patch, file, patch_of: flagStr(flags.of), note: flagStr(flags.note) })
|
|
585
|
+
);
|
|
586
|
+
return json ? printResult(io, flags, r) : io.log(`saved candidate ${r.version.label} (${r.version.id})`);
|
|
587
|
+
}
|
|
588
|
+
case "research-compare": {
|
|
589
|
+
const title = flagStr(flags.title);
|
|
590
|
+
const versionId = flagStr(flags.candidate);
|
|
591
|
+
const patch = jsonFlag(flags.patch, "--patch");
|
|
592
|
+
const nullCheck = flagBool(flags["null-check"]);
|
|
593
|
+
const named = [versionId !== undefined, patch !== undefined, nullCheck === true].filter(Boolean).length;
|
|
594
|
+
if (!title || named !== 1)
|
|
595
|
+
throw new CliError(
|
|
596
|
+
"Usage: admin tuning research-compare --title \"…\" (--candidate <version-id> | --patch '{…}' | --null-check) [--keys k1,k2] [--concurrency 6] [--experiment <id>]"
|
|
597
|
+
);
|
|
598
|
+
const candidate = versionId ? { version_id: versionId } : patch ? { patch } : { null_check: true };
|
|
599
|
+
const r = await staffRequest(
|
|
600
|
+
ctx,
|
|
601
|
+
"POST",
|
|
602
|
+
`${T}/comparisons`,
|
|
603
|
+
compact({
|
|
604
|
+
primitive: "research",
|
|
605
|
+
title,
|
|
606
|
+
candidate,
|
|
607
|
+
keys: flagList(flags.keys),
|
|
608
|
+
concurrency: flagNum(flags.concurrency),
|
|
609
|
+
experiment_id: flagStr(flags.experiment),
|
|
610
|
+
})
|
|
611
|
+
);
|
|
612
|
+
return json ? printResult(io, flags, r) : io.log(`research comparison ${r.comparison?.id ?? ""} started`);
|
|
613
|
+
}
|
|
614
|
+
case "research-inputs": {
|
|
615
|
+
const set = flagStr(flags.set);
|
|
616
|
+
return printResult(
|
|
617
|
+
io,
|
|
618
|
+
{ ...flags, json: true },
|
|
619
|
+
await staffRequest(ctx, "GET", `${T}/research/inputs${set ? `?${new URLSearchParams({ set })}` : ""}`)
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
case "research-split": {
|
|
623
|
+
const q = new URLSearchParams(
|
|
624
|
+
compact({
|
|
625
|
+
since_hours: flagStr(flags["since-hours"]),
|
|
626
|
+
window_hours: flagStr(flags["window-hours"]),
|
|
627
|
+
at: flagStr(flags.at),
|
|
628
|
+
})
|
|
629
|
+
);
|
|
630
|
+
return printResult(
|
|
631
|
+
io,
|
|
632
|
+
{ ...flags, json: true },
|
|
633
|
+
await staffRequest(ctx, "GET", `${T}/research/deploy-split${q.size ? `?${q}` : ""}`)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
case "research-standing":
|
|
637
|
+
return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/research/standing`));
|
|
638
|
+
case "promote-arena": {
|
|
639
|
+
const version = flagStr(flags.version);
|
|
640
|
+
if (!id || !version)
|
|
641
|
+
throw new CliError("Usage: admin tuning promote-arena <experiment-id> --version <arena-version-id>");
|
|
642
|
+
return printResult(
|
|
643
|
+
io,
|
|
644
|
+
{ ...flags, json: true },
|
|
645
|
+
await staffRequest(
|
|
646
|
+
ctx,
|
|
647
|
+
"POST",
|
|
648
|
+
`${EXPERIMENTS}/${id}/promote`,
|
|
649
|
+
compact({ arena_version_id: version, actor: actorFrom(flags) })
|
|
650
|
+
)
|
|
651
|
+
);
|
|
652
|
+
}
|
|
526
653
|
case "promote": {
|
|
527
654
|
const list = flagList(flags.versions);
|
|
528
655
|
if (!id || !list?.length)
|
|
@@ -546,7 +673,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
546
673
|
}
|
|
547
674
|
default:
|
|
548
675
|
throw new CliError(
|
|
549
|
-
"Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|promote (see --help)"
|
|
676
|
+
"Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|reading-list|promote|arenas|arena|arena-candidate|research-compare|research-inputs|research-split|research-standing|promote-arena (see --help)"
|
|
550
677
|
);
|
|
551
678
|
}
|
|
552
679
|
}
|
package/lib/commands/arena.mjs
CHANGED
|
@@ -186,6 +186,33 @@ export const ARGS = {
|
|
|
186
186
|
arena_id: { positional: 1, label: "arena-id", required: true },
|
|
187
187
|
config: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
188
188
|
},
|
|
189
|
+
// The arena's research agent as a versioned file (the kernel file's twin): `arena file <id>`
|
|
190
|
+
// prints it, `--file f.json` saves a whole file, `--patch '{…}'` only the fields to change.
|
|
191
|
+
get_arena_file: {
|
|
192
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
193
|
+
},
|
|
194
|
+
save_arena_file: {
|
|
195
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
196
|
+
file: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
197
|
+
patch: { value: (flags) => readJsonObjectFlag(flags, "patch") },
|
|
198
|
+
note: "note",
|
|
199
|
+
},
|
|
200
|
+
list_arena_versions: {
|
|
201
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
202
|
+
},
|
|
203
|
+
pin_arena_version: {
|
|
204
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
205
|
+
version_id: {
|
|
206
|
+
value: (_flags, positionals) => {
|
|
207
|
+
const v = positionals[1];
|
|
208
|
+
if (v === undefined) throw new CliError("Missing required <version-id|none>");
|
|
209
|
+
return v === "none" ? null : v;
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
accept_arena_offer: {
|
|
214
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
215
|
+
},
|
|
189
216
|
};
|
|
190
217
|
|
|
191
218
|
export async function cmdArena(ctx) {
|
|
@@ -218,9 +245,17 @@ export async function cmdArena(ctx) {
|
|
|
218
245
|
return arenaList(rest);
|
|
219
246
|
case "endpoint":
|
|
220
247
|
return arenaEndpoint(rest);
|
|
248
|
+
case "file":
|
|
249
|
+
return arenaFile(rest);
|
|
250
|
+
case "versions":
|
|
251
|
+
return arenaTool(rest, "list_arena_versions");
|
|
252
|
+
case "pin":
|
|
253
|
+
return arenaTool(rest, "pin_arena_version");
|
|
254
|
+
case "offer":
|
|
255
|
+
return rest.flags.accept === true ? arenaTool(rest, "accept_arena_offer") : arenaOffer(rest);
|
|
221
256
|
default:
|
|
222
257
|
throw new CliError(
|
|
223
|
-
`Unknown arena subcommand: ${sub ?? "(none)"}. Try: run, run-status, runs, latest, direction, cancel, balance, list, endpoint, roles, role`
|
|
258
|
+
`Unknown arena subcommand: ${sub ?? "(none)"}. Try: run, run-status, runs, latest, direction, cancel, balance, list, endpoint, file, versions, pin, offer, roles, role`
|
|
224
259
|
);
|
|
225
260
|
}
|
|
226
261
|
}
|
|
@@ -386,6 +421,51 @@ async function arenaList(ctx) {
|
|
|
386
421
|
* `arena endpoint create|delete|config` — the arena's own CRUD, and its configuration as
|
|
387
422
|
* one file (the same document `get_arena_config` returns and `set_arena_config` accepts).
|
|
388
423
|
*/
|
|
424
|
+
/**
|
|
425
|
+
* `arena file <arena-id>` prints the file the arena runs (the file itself, so `> f.json` round-trips);
|
|
426
|
+
* `arena file <arena-id> --file f.json | --patch '{…}' [--note …]` saves a new live version of the
|
|
427
|
+
* arena's own copy.
|
|
428
|
+
*/
|
|
429
|
+
async function arenaFile(ctx) {
|
|
430
|
+
const { client, flags, io } = ctx;
|
|
431
|
+
const saving =
|
|
432
|
+
flagStr(flags.file) !== undefined ||
|
|
433
|
+
flagStr(flags.patch) !== undefined ||
|
|
434
|
+
flagStr(flags["patch-file"]) !== undefined;
|
|
435
|
+
if (!saving) {
|
|
436
|
+
const result = await client.callTool(
|
|
437
|
+
"get_arena_file",
|
|
438
|
+
argsFromFlags(ARGS.get_arena_file, flags, ctx.positionals)
|
|
439
|
+
);
|
|
440
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.file ?? r, null, 2));
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
const result = await client.callTool(
|
|
444
|
+
"save_arena_file",
|
|
445
|
+
argsFromFlags(ARGS.save_arena_file, flags, ctx.positionals)
|
|
446
|
+
);
|
|
447
|
+
ok(ctx, `Saved ${toolField(result, "version")?.label ?? "a new version"}; the arena runs it from now on.`);
|
|
448
|
+
printResult(io, flags, result);
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** `arena offer <arena-id>`: what our newer version would change; `--accept` takes it. */
|
|
453
|
+
async function arenaOffer(ctx) {
|
|
454
|
+
const { client, flags, io } = ctx;
|
|
455
|
+
const result = await client.callTool("get_arena_file", argsFromFlags(ARGS.get_arena_file, flags, ctx.positionals));
|
|
456
|
+
const offer = toolField(result, "offer");
|
|
457
|
+
if (!offer) ok(ctx, "No offer: your arena runs our latest version, or a copy based on it.");
|
|
458
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.offer ?? null, null, 2));
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** One arena-file tool, its arguments from ARGS. */
|
|
463
|
+
async function arenaTool(ctx, tool) {
|
|
464
|
+
const result = await ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
|
|
465
|
+
printResult(ctx.io, ctx.flags, result);
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
|
|
389
469
|
async function arenaEndpoint(ctx) {
|
|
390
470
|
const { client, positionals, flags, io } = ctx;
|
|
391
471
|
const action = positionals[0];
|