@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 +8 -0
- package/lib/cli.mjs +1 -1
- package/lib/commands/admin.mjs +152 -1
- package/package.json +1 -1
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.
|
|
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.
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -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
|
}
|