@stage5/lumine 0.2.99 → 0.3.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/admin.js CHANGED
@@ -640,6 +640,8 @@ export async function adminCommand(options) {
640
640
  let runScope = null;
641
641
  let correctionSessionId = 0;
642
642
  let correctionSession = null;
643
+ let commentSessionId = 0;
644
+ let commentSession = null;
643
645
  if (adminOperationRequiresRun(operation)) {
644
646
  if (operation.correctionEligible) {
645
647
  const correctionStatus = await requestJson({
@@ -657,6 +659,15 @@ export async function adminCommand(options) {
657
659
  correctionSession = correction;
658
660
  }
659
661
  }
662
+ if (!correctionSessionId && adminOperationUsesCommentSession(operation)) {
663
+ commentSession = await loadActiveCommentSession({
664
+ options,
665
+ authToken: auth.token,
666
+ });
667
+ commentSessionId = Number(commentSession?.id || 0);
668
+ }
669
+ // Look up the daily run too: when both are active the caller must choose
670
+ // one (--via), and exactly one authority is sent.
660
671
  const runStatus = correctionSessionId
661
672
  ? null
662
673
  : await requestJson({
@@ -671,9 +682,39 @@ export async function adminCommand(options) {
671
682
  ].includes(operation.name)
672
683
  ? runStatus?.data?.lastRun || null
673
684
  : null;
674
- const selectedRun = activeRun || retryableFinishedRun;
685
+ let selectedRun = activeRun || retryableFinishedRun;
686
+ const via = parseAdminVia(options.adminVia, operation);
687
+ if (via && correctionSessionId) {
688
+ throw cliValidationError(
689
+ `Correction session #${correctionSessionId} is active for this comment and handles it; drop --via or complete the correction first.`,
690
+ );
691
+ }
692
+ if (commentSessionId && selectedRun) {
693
+ let choice = via;
694
+ if (!choice) {
695
+ if (COMMENT_SESSION_WRITE_OPERATIONS.has(operation.name)) {
696
+ throw cliValidationError(
697
+ `Both comment session #${commentSessionId} (${commentSession.identity?.key || "unknown"}) and daily run #${selectedRun.id} (${selectedRun.identity?.key || "unknown"}) are active. Choose which one this ${operation.name} uses with --via session or --via run.`,
698
+ );
699
+ }
700
+ // Reads create nothing; without --via they keep using the daily run.
701
+ choice = "run";
702
+ }
703
+ if (choice === "session") {
704
+ selectedRun = null;
705
+ } else {
706
+ commentSessionId = 0;
707
+ commentSession = null;
708
+ }
709
+ } else if (via === "session" && !commentSessionId) {
710
+ throw cliValidationError(
711
+ "--via session: no comment session is active. Start one with lumine admin comment session start.",
712
+ );
713
+ } else if (via === "run" && !selectedRun && !correctionSessionId) {
714
+ throw noActiveRunError();
715
+ }
675
716
  runId = Number(selectedRun?.id || 0);
676
- if (!runId && !correctionSessionId) {
717
+ if (!runId && !correctionSessionId && !commentSessionId) {
677
718
  if (operation.correctionEligible) {
678
719
  const error = new Error(
679
720
  `Start a correction session first: lumine admin correction start ${operation.correctionCommentId}.`,
@@ -687,7 +728,17 @@ export async function adminCommand(options) {
687
728
  runScope = canonicalAdminRunScope(selectedRun);
688
729
  assertAdminOperationAllowedForRunScope({ operation, runScope });
689
730
  }
690
- if (options.adminIdentity && (selectedRun || correctionSession)) {
731
+ if (options.adminIdentity && commentSession) {
732
+ const requestedIdentity = parseIdentity(options.adminIdentity);
733
+ if (
734
+ requestedIdentity !== "auto" &&
735
+ requestedIdentity !== commentSession.identity?.key
736
+ ) {
737
+ throw cliValidationError(
738
+ `--identity ${requestedIdentity} does not match comment session #${commentSessionId} (${commentSession.identity?.key || "unknown"}).`,
739
+ );
740
+ }
741
+ } else if (options.adminIdentity && (selectedRun || correctionSession)) {
691
742
  const requestedIdentity = parseIdentity(options.adminIdentity);
692
743
  const canonicalIdentity = correctionSession
693
744
  ? correctionSession.identity?.key
@@ -722,6 +773,11 @@ export async function adminCommand(options) {
722
773
  String(correctionSessionId),
723
774
  }
724
775
  : {}),
776
+ ...(commentSessionId
777
+ ? {
778
+ "x-lumine-admin-comment-session-id": String(commentSessionId),
779
+ }
780
+ : {}),
725
781
  ...(requestId ? { "x-lumine-idempotency-key": requestId } : {}),
726
782
  },
727
783
  // Long operations (reward-bank seed) carry their own timeout.
@@ -1529,6 +1585,81 @@ export function assertAdminOperationAllowedForRunScope({
1529
1585
  );
1530
1586
  }
1531
1587
 
1588
+ // Commands a standalone comment session can serve: drafting, replying,
1589
+ // publishing and editing its own comments, plus the reads needed to compose
1590
+ // on its targets. The API still enforces the session's target lock on each.
1591
+ const COMMENT_SESSION_WRITE_OPERATIONS = new Set([
1592
+ "comment.draft",
1593
+ "comment.post",
1594
+ "comment.edit",
1595
+ ]);
1596
+ const COMMENT_SESSION_OPERATIONS = new Set([
1597
+ ...COMMENT_SESSION_WRITE_OPERATIONS,
1598
+ "subject.get",
1599
+ "subject.comments",
1600
+ "post.get",
1601
+ "post.comments",
1602
+ ]);
1603
+
1604
+ export function adminOperationUsesCommentSession(operation) {
1605
+ return COMMENT_SESSION_OPERATIONS.has(operation?.name);
1606
+ }
1607
+
1608
+ export function parseAdminVia(value, operation) {
1609
+ const via = String(value || "").trim().toLowerCase();
1610
+ if (!via) return "";
1611
+ if (via !== "session" && via !== "run") {
1612
+ throw cliValidationError("--via must be session or run.");
1613
+ }
1614
+ if (!adminOperationUsesCommentSession(operation)) {
1615
+ throw cliValidationError(
1616
+ "--via applies only to comment draft/reply/post/edit and the subject/post/comment reads.",
1617
+ );
1618
+ }
1619
+ return via;
1620
+ }
1621
+
1622
+ async function loadActiveCommentSession({ options, authToken }) {
1623
+ let status;
1624
+ try {
1625
+ status = await requestJson({
1626
+ url: `${options.apiUrl}/cli/admin/comment-sessions/status`,
1627
+ authToken,
1628
+ timeoutMs: options.timeoutMs,
1629
+ });
1630
+ } catch (error) {
1631
+ // An API without comment sessions yet: keep daily runs working.
1632
+ if (error?.status === 404) return null;
1633
+ throw error;
1634
+ }
1635
+ const session = status?.data?.commentSession || null;
1636
+ return session?.status === "active" && Number(session.id) > 0
1637
+ ? session
1638
+ : null;
1639
+ }
1640
+
1641
+ export function parseCommentSessionTargets(values) {
1642
+ const raw = values
1643
+ .flatMap((value) => String(value || "").split(","))
1644
+ .map((value) => value.trim())
1645
+ .filter(Boolean);
1646
+ if (!raw.length) {
1647
+ throw cliValidationError(
1648
+ "Name the targets with --target <subject:ID|comment:ID|build:ID|aiStory:ID|dailyReflection:ID>[,...].",
1649
+ );
1650
+ }
1651
+ const seen = new Set();
1652
+ const targets = [];
1653
+ for (const value of raw) {
1654
+ const target = parseAdminCommentTarget({ target: value });
1655
+ const key = `${target.type}:${target.id}`;
1656
+ if (seen.has(key)) continue;
1657
+ seen.add(key);
1658
+ targets.push(target);
1659
+ }
1660
+ return targets;
1661
+ }
1662
+
1532
1663
  function adminOperationRequiresRun(operation) {
1533
1664
  if (typeof operation.requiresRun === "boolean") {
1534
1665
  return operation.requiresRun;
@@ -1547,6 +1678,9 @@ function adminOperationRequiresRun(operation) {
1547
1678
  "correction.start",
1548
1679
  "correction.status",
1549
1680
  "correction.complete",
1681
+ "comment-session.start",
1682
+ "comment-session.status",
1683
+ "comment-session.close",
1550
1684
  "escalation.list",
1551
1685
  "escalation.set",
1552
1686
  "notable.add",
@@ -2283,7 +2417,7 @@ export function parseAdminOperation(options) {
2283
2417
  // long as two GPT-6 Sol calls, so it gets its own timeout. The daily
2284
2418
  // website management run calls it in a bounded loop (Mikey, 2026-09-28).
2285
2419
  const usage =
2286
- "Usage: lumine admin reward-bank coverage <buildId> [--max-step 3] | seed <buildId> [--max-step 3].";
2420
+ "Usage: lumine admin reward-bank coverage <buildId> [--max-step 3] | seed <buildId> [--max-step 3] | report <buildId> [--days 1].";
2287
2421
  const buildId = parseRequiredInteger(
2288
2422
  target || options.adminBuild,
2289
2423
  "Build ID",
@@ -2293,6 +2427,20 @@ export function parseAdminOperation(options) {
2293
2427
  ? parseRequiredInteger(options.adminMaxStep, "--max-step", 1)
2294
2428
  : 3;
2295
2429
  if (maxStep > 10) throw cliValidationError("--max-step must be at most 10.");
2430
+ if (action === "report") {
2431
+ const days = options.adminDays
2432
+ ? parseRequiredInteger(options.adminDays, "--days", 1)
2433
+ : 1;
2434
+ if (days > 31) throw cliValidationError("--days must be at most 31.");
2435
+ return readOperation(
2436
+ "reward-bank.report",
2437
+ withQuery("/cli/admin/reward-bank/report", {
2438
+ buildId: String(buildId),
2439
+ days: String(days),
2440
+ }),
2441
+ { requiresRun: false },
2442
+ );
2443
+ }
2296
2444
  if (action === "coverage" || action === "status") {
2297
2445
  return readOperation(
2298
2446
  "reward-bank.coverage",
@@ -3361,6 +3509,51 @@ export function parseAdminOperation(options) {
3361
3509
  }
3362
3510
 
3363
3511
  if (namespace === "comment") {
3512
+ if (action === "session") {
3513
+ const sessionAction = target || "status";
3514
+ if (sessionAction === "status") {
3515
+ return readOperation(
3516
+ "comment-session.status",
3517
+ "/cli/admin/comment-sessions/status",
3518
+ );
3519
+ }
3520
+ if (sessionAction === "start") {
3521
+ const identity = String(options.adminIdentity || "")
3522
+ .trim()
3523
+ .toLowerCase();
3524
+ if (identity !== "zero" && identity !== "ciel") {
3525
+ throw cliValidationError(
3526
+ "comment session start needs --identity zero|ciel.",
3527
+ );
3528
+ }
3529
+ const targets = parseCommentSessionTargets([
3530
+ options.adminTarget,
3531
+ ...(options.positional || []).slice(3),
3532
+ ]);
3533
+ const reason = String(options.adminReason || "").trim();
3534
+ return writeOperation(
3535
+ "comment-session.start",
3536
+ "POST",
3537
+ "/cli/admin/comment-sessions",
3538
+ {
3539
+ identity,
3540
+ targets,
3541
+ ...(reason ? { reason } : {}),
3542
+ },
3543
+ );
3544
+ }
3545
+ if (sessionAction === "close") {
3546
+ return writeOperation(
3547
+ "comment-session.close",
3548
+ "POST",
3549
+ "/cli/admin/comment-sessions/close",
3550
+ {},
3551
+ );
3552
+ }
3553
+ throw cliValidationError(
3554
+ "Usage: lumine admin comment session start --identity zero|ciel --target <type:id>[,<type:id>...] [--reason <text>] | comment session status | comment session close.",
3555
+ );
3556
+ }
3364
3557
  if (action === "draft" || action === "reply") {
3365
3558
  const parsedTarget = parseAdminCommentTarget({
3366
3559
  target,
@@ -5211,6 +5404,70 @@ function printReviewRequestResult({ operation, data, result }) {
5211
5404
  printReviewEvents(data.events);
5212
5405
  }
5213
5406
 
5407
+ // Human view of `lumine admin reward-bank report`; --json has everything.
5408
+ function printRewardBankReport(data) {
5409
+ const pct = (value) =>
5410
+ value === null || value === undefined ? "–" : `${Math.round(value * 100)}%`;
5411
+ console.log(`Question bank report · build #${data.buildId} · last ${data.days} day(s)`);
5412
+ if (data.coverage) {
5413
+ console.log(
5414
+ `Seeding coverage (steps 1-3): ${data.coverage.filled} / ${data.coverage.total}`,
5415
+ );
5416
+ }
5417
+ if (!data.generation) {
5418
+ console.log("Generation: no telemetry table yet.");
5419
+ } else {
5420
+ for (const row of data.generation.bySource) {
5421
+ if (!row.rounds) continue;
5422
+ console.log(
5423
+ `Generation (${row.source}): ${row.rounds} rounds · ${row.accepted} accepted (${pct(row.acceptRate)}) · off-brief ${row.offBrief} · answer mismatch ${row.answerMismatch} · malformed ${row.malformed} · errors ${row.errors} · $${row.costUsd} · avg ${row.avgSeconds ?? "–"}s (max ${row.maxSeconds ?? "–"}s)`,
5424
+ );
5425
+ }
5426
+ for (const row of data.generation.recentRejections.slice(0, 5)) {
5427
+ console.log(
5428
+ ` rejected ${row.outcome} ${row.ruleId}/${row.topicKey} step ${row.step}: ${row.problems || "(no reason given)"}`,
5429
+ );
5430
+ }
5431
+ }
5432
+ console.log(`Learners: ${data.learners.total}`);
5433
+ for (const row of data.learners.byRuleStep) {
5434
+ console.log(
5435
+ ` ${row.ruleId} step ${row.step}: served ${row.served} · answered ${row.answered} · first-try ${pct(row.firstTryRate)} · solved ${row.solved} · skipped ${row.skipped}`,
5436
+ );
5437
+ }
5438
+ const paid = data.payouts.reduce(
5439
+ (sum, row) => ({ xp: sum.xp + row.xp, coins: sum.coins + row.coins, receipts: sum.receipts + row.receipts }),
5440
+ { xp: 0, coins: 0, receipts: 0 },
5441
+ );
5442
+ console.log(
5443
+ `Payouts: ${paid.receipts} receipts · ${paid.xp.toLocaleString()} XP · ${paid.coins.toLocaleString()} Coins`,
5444
+ );
5445
+ for (const row of data.payouts) {
5446
+ console.log(
5447
+ ` ${row.ruleId} step ${row.step}: ${row.receipts} (${row.firstTry} first try) · ${row.earners} earners · ${row.xp.toLocaleString()} XP · ${row.coins.toLocaleString()} Coins`,
5448
+ );
5449
+ }
5450
+ if (data.ladder.length) {
5451
+ console.log(
5452
+ `Ladder now: ${data.ladder.map((row) => `${row.ruleId} s${row.step}:${row.topicLadders}`).join(", ")}`,
5453
+ );
5454
+ }
5455
+ const hidden = data.bank.filter((row) => row.status === "hidden");
5456
+ console.log(
5457
+ `Bank: ${data.bank.filter((row) => row.status === "active").reduce((n, row) => n + row.questions, 0)} active · ${hidden.reduce((n, row) => n + row.questions, 0)} hidden${hidden.length ? ` (${hidden.map((row) => `${row.hiddenReason}: ${row.questions}`).join(", ")})` : ""} · ${data.bank.reduce((n, row) => n + row.createdInWindow, 0)} new in window`,
5458
+ );
5459
+ for (const row of data.questionsToReview.reported) {
5460
+ console.log(
5461
+ ` reported #${row.id} (${row.reports}, ${row.status}) ${row.ruleId}/${row.topicKey} s${row.step}: ${row.reasons}`,
5462
+ );
5463
+ }
5464
+ for (const row of data.questionsToReview.hardest) {
5465
+ console.log(
5466
+ ` hardest #${row.id} ${row.ruleId}/${row.topicKey} s${row.step}: solved ${pct(row.solveRate)} of ${row.learners}`,
5467
+ );
5468
+ }
5469
+ }
5470
+
5214
5471
  function printAdminResult({ operation, result }) {
5215
5472
  const data = result?.data || {};
5216
5473
  if (printChatSafetyResult({ operation, data })) return;
@@ -5218,6 +5475,10 @@ function printAdminResult({ operation, result }) {
5218
5475
  printRewardActivity(data);
5219
5476
  return;
5220
5477
  }
5478
+ if (operation.name === "reward-bank.report") {
5479
+ printRewardBankReport(data);
5480
+ return;
5481
+ }
5221
5482
  if (operation.name.startsWith("reward-bank.")) {
5222
5483
  if (operation.name === "reward-bank.seed") {
5223
5484
  console.log(
package/lib/commands.js CHANGED
@@ -2597,6 +2597,7 @@ export function parseArgs(args) {
2597
2597
  adminUnviewed: Boolean(raw.unviewed),
2598
2598
  adminViewed: Boolean(raw.viewed),
2599
2599
  adminIdentity: raw.identity ? String(raw.identity) : "",
2600
+ adminVia: raw.via ? String(raw.via) : "",
2600
2601
  adminScope: raw.scope ? String(raw.scope) : "",
2601
2602
  commentMode: raw.commentMode ? String(raw.commentMode) : "",
2602
2603
  runKey: raw.runKey ? String(raw.runKey) : "",
@@ -3059,9 +3060,13 @@ export function printHelp() {
3059
3060
  lumine admin post reward <target> [--type subject|comment|aiStory|dailyReflection] --twinkles 3 [--json]
3060
3061
  lumine admin reward-bank coverage <buildId> [--max-step 3] [--json]
3061
3062
  lumine admin reward-bank seed <buildId> [--max-step 3] [--json] (daily run: writes ONE missing seed question, lowest step first, billed to Zero/Ciel's AI Energy)
3063
+ lumine admin reward-bank report <buildId> [--days 1] [--json] (question-bank telemetry: generation outcomes, learners, payouts, ladder, questions to review)
3062
3064
  lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
3063
3065
  lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
3064
3066
  lumine admin comment post --draft-id <id> [--json]
3067
+ lumine admin comment session start --identity zero|ciel --target <subject:ID|comment:ID|build:ID|aiStory:ID>[,...] [--reason <text>] [--json]
3068
+ lumine admin comment session status|close [--json]
3069
+ (with both a comment session and a daily run active, comment draft/reply/post/edit need --via session|run)
3065
3070
  lumine admin comment edit <comment-id> --file <comment.md> [--review-receipt <review.json> | --reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--json]
3066
3071
  lumine admin brief [--days <1..30>] [--json]
3067
3072
  lumine admin ai-costs monthly [--output <file.json>] [--json] (no active run required)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.99",
3
+ "version": "0.3.1",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1304,8 +1304,18 @@ them so the first learner on a unit never waits or pays.
1304
1304
  ```bash
1305
1305
  lumine admin reward-bank coverage <buildId> [--max-step 3] [--json] # read-only, no run
1306
1306
  lumine admin reward-bank seed <buildId> [--max-step 3] [--json] # delegated full run
1307
+ lumine admin reward-bank report <buildId> [--days 1] [--json] # read-only telemetry
1307
1308
  ```
1308
1309
 
1310
+ `report` covers the last `--days` (1-31): generation rounds by source
1311
+ (learner or seed) with outcomes (`accepted`, `off_brief`: the checker judged
1312
+ the question outside the brief's language, level or LaTeX rule;
1313
+ `answer_mismatch`; `malformed`; `error`), cost and time, and recent rejection
1314
+ reasons; learners served/answered/first-try/solved/skipped per rule and step;
1315
+ payouts; the current ladder distribution; active/hidden questions; the
1316
+ hardest questions (3+ learners) and every reported question with reasons;
1317
+ and seeding coverage for steps 1-3 once the app is approved.
1318
+
1309
1319
  - `seed` writes ONE question per call into the first missing slot, lowest
1310
1320
  step first across every topic of every generated-quiz rule, up to
1311
1321
  `--max-step` (default 3, at most 10). A slot is a (rule, topic, step) with
@@ -4171,6 +4181,74 @@ canonical review record ID before claiming success. Older APIs that still block
4171
4181
  Build edits must be deployed first; do not silently substitute a duplicate reply
4172
4182
  when Mikey requested an edit.
4173
4183
 
4184
+ ### Standalone comment sessions
4185
+
4186
+ Posting a few new comments or replies as Zero or Ciel does not require a full
4187
+ daily run. An approved operator names the identity and the exact targets:
4188
+
4189
+ ```bash
4190
+ lumine admin comment session start --identity ciel \
4191
+ --target subject:123,comment:456 --reason "Mikey asked for a reply" --json
4192
+ lumine admin subject get 123 --include-comments --json
4193
+ lumine admin comment draft subject:123 --file comment.md --json
4194
+ lumine admin comment reply comment:456 --file reply.md --json
4195
+ lumine admin comment post --draft-id <id> --json
4196
+ lumine admin comment edit <commentId> --file corrected.md --json
4197
+ lumine admin comment session status --json
4198
+ lumine admin comment session close --json
4199
+ ```
4200
+
4201
+ `--target` takes a comma-separated list (extra space-separated targets after
4202
+ it also count); each is `subject:ID`, `comment:ID`, `build:ID`, `aiStory:ID` or
4203
+ `dailyReflection:ID`, at most ten. Pass them in one `--target`: a repeated
4204
+ `--target` flag keeps only its last value. The server checks every target when
4205
+ the session starts and refuses one the draft path would refuse later.
4206
+
4207
+ What the session authorizes:
4208
+
4209
+ - **Scopes:** only `content:read`, `comment:draft` and `comment:post`. No chat,
4210
+ Featured, reward, recommendation, subject, newspaper or run-completion
4211
+ command works through it; those still need a daily run.
4212
+ - **Target lock:** drafts, replies and the reads above work only on the named
4213
+ targets. A reply target counts when it is a named `comment:ID`, or a comment
4214
+ sitting directly in a named subject, Build, AI Story or Daily Reflection.
4215
+ Anything else fails with `CLI_ADMIN_COMMENT_SESSION_TARGET_FORBIDDEN` and
4216
+ the allowed list. `comment post` accepts only drafts created in the session.
4217
+ - **Edits:** `comment edit <commentId>` works under the session only for a
4218
+ comment this session itself posted, as the same identity
4219
+ (`CLI_ADMIN_COMMENT_SESSION_EDIT_FORBIDDEN` otherwise). The usual edit rules
4220
+ apply unchanged, including fresh Build review evidence for a Build comment.
4221
+ Edits do not count toward the post cap.
4222
+ - **Limits:** 30 minutes, at most five published comments
4223
+ (`CLI_ADMIN_COMMENT_SESSION_POST_CAP`), one active session per operator.
4224
+ `close` or the TTL ends it; start a new session for more.
4225
+ - **Same bar as a daily run:** drafting and publishing use the exact daily-run
4226
+ draft/publish path. Build comments still require composed text, a review
4227
+ receipt or `--reviewed-version/--reviewed-via` evidence of the current
4228
+ published version, and `--review-context`; context and persona revisions
4229
+ are checked at publish time as usual.
4230
+ - **Identity:** the operator must be an approved delegated administrator, and
4231
+ the session posts only as the identity named at start (`--identity auto` is
4232
+ not accepted). It does not change the Bangkok calendar assignment or the
4233
+ identity preference.
4234
+
4235
+ A comment session and a daily run can be active together, but every request
4236
+ carries exactly one authority. The session never claims the daily-run slot.
4237
+
4238
+ - When only one of them is active, the CLI uses it.
4239
+ - When both are active, `comment draft`, `comment reply`, `comment post` and
4240
+ `comment edit` refuse to run until you choose with `--via session` or
4241
+ `--via run`. Target reads (`subject get`, `post get`, `comments get`, their
4242
+ comment lists) default to the daily run and take `--via session` to read
4243
+ through the session instead.
4244
+ - The API refuses a request that names a comment session together with a
4245
+ daily run or correction session (`CLI_ADMIN_AMBIGUOUS_AUTHORITY`); a session
4246
+ request outside the session is refused, never handed to the run.
4247
+ - A draft records the authority that created it, and only that authority can
4248
+ publish it (`CLI_ADMIN_DRAFT_AUTHORITY_MISMATCH`).
4249
+ - A correction session still takes precedence for its one comment; `--via` is
4250
+ refused while it is active for that comment.
4251
+
4174
4252
  **Editing the bot's own comments.** `comment edit <commentId> --file
4175
4253
  <comment.md>` replaces the text of a comment the acting bot itself authored —
4176
4254
  for correcting a factual error, an unfulfillable claim, or outdated guidance