@stage5/lumine 0.3.42 → 0.3.44

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.
@@ -253,6 +253,7 @@ export async function runFeaturedWorkflow({
253
253
  operation,
254
254
  authToken,
255
255
  runId,
256
+ sourceRunId = 0,
256
257
  request = requestJson,
257
258
  }) {
258
259
  const apiUrl = String(options.apiUrl).replace(/\/$/, "");
@@ -273,9 +274,9 @@ export async function runFeaturedWorkflow({
273
274
  options.adminFile,
274
275
  options.adminApprove,
275
276
  );
276
- if (plan.runId !== runId)
277
+ if (plan.runId !== runId && !(sourceRunId > 0 && plan.runId === sourceRunId))
277
278
  throw invalid(
278
- "This plan belongs to a different run; prepare and approve a fresh plan.",
279
+ "This plan belongs to a different run; prepare and approve a fresh plan, or apply it from an approvals run started on its full run (--scope approvals --source-run).",
279
280
  );
280
281
  const result = await call(
281
282
  "/plan/apply",
package/lib/admin.js CHANGED
@@ -382,7 +382,7 @@ function buildApprovalOperation({ action, target, options }) {
382
382
  // Code agent passes wrote (no model is called), then review what they could
383
383
  // not settle (Mikey 2026-10-06: bulk AI work runs on Claude Code agents).
384
384
  const ENGLISH_USAGE =
385
- "Use: lumine admin english import <verdicts.json> --pass a|b | adjudicate <verdicts.json> | add-questions <questions.json> | relevel <moves.json> | mastery <userId> --reason <why> | backfill-distractors | summary | review [--limit <n>] | accept|retire <questionId> [--reason <text>] [--fault] | retag <questionId> --skill <code> [--reason <text>] | build-candidates [--after <questionId>] [--limit <n>] | build-import <verdicts.json> | check-candidates [--after <questionId>] [--limit <n>] | check-import <verdicts.json> | check-broken [--after <questionId>] [--limit <n>] | check-apply <fixes.json> | check-verify <checked.json> | check-follow-faulty [--after <next>] [--limit <n>] | lesson-candidates [--after <skillCode>] [--limit <n>] | lesson-import <lessons.json> | lesson-approve <approvals.json> | lessons [--status draft|approved]";
385
+ "Use: lumine admin english import <verdicts.json> --pass a|b | adjudicate <verdicts.json> | add-questions <questions.json> | relevel <moves.json> | mastery <userId> --reason <why> | backfill-distractors | summary | review [--limit <n>] | accept|retire <questionId> [--reason <text>] [--fault] | retag <questionId> --skill <code> [--reason <text>] | build-candidates [--after <questionId>] [--limit <n>] | build-import <verdicts.json> | check-candidates [--after <questionId>] [--limit <n>] | check-import <verdicts.json> | check-broken [--after <questionId>] [--limit <n>] | check-apply <fixes.json> | check-verify <checked.json> | check-follow-faulty [--after <next>] [--limit <n>] | audit-backlog [--after <questionId>] [--limit <n>] | audit-mark <marks.json> | lesson-candidates [--after <skillCode>] [--limit <n>] | lesson-import <lessons.json> | lesson-approve <approvals.json> | lessons [--status draft|approved]";
386
386
  const ENGLISH_IMPORT_MAX = 500;
387
387
 
388
388
  function buildEnglishOperation({ action, target, options }) {
@@ -464,6 +464,25 @@ function buildEnglishOperation({ action, target, options }) {
464
464
  if (after && !/^(?:[ufc]:)?\d+:\d+$/.test(after)) throw cliValidationError("--after takes the last reply's next (<phase:id:version>).");
465
465
  return writeOperation("english.check_follow_faulty", "POST", "/cli/admin/english/item-checks/follow-faulty", { after, limit: limitOf(200, 100) }, { requiresRun: false, timeoutMs: 300000 });
466
466
  }
467
+ // Grammar Quest audit passes (Mikey 2026-10-17): the checked items no pass
468
+ // has looked at yet, first; and marking what a pass read,
469
+ // { audit: "<name>", items: [{ questionId, version, contentHash, outcome }] }
470
+ if (action === "audit-backlog") {
471
+ // the parser leaves an omitted --after as ""
472
+ const after = options.adminAfter ? parseRequiredInteger(options.adminAfter, "--after", 0, 2147483647) : 0;
473
+ return readOperation("english.audit_backlog", withQuery("/cli/admin/english/item-audits/backlog", { after, limit: limitOf(1000, 200) }), { requiresRun: false });
474
+ }
475
+ if (action === "audit-mark") {
476
+ if (!target) throw cliValidationError(ENGLISH_USAGE);
477
+ const raw = readAdminJsonFile(target, "<marks.json>", { maxBytes: 4 * 1024 * 1024 });
478
+ const audit = String(raw?.audit || "");
479
+ if (!/^[a-z0-9][a-z0-9-]{2,63}$/.test(audit)) throw cliValidationError('The file needs "audit": the pass\'s name (a-z, 0-9, -), e.g. crack-ambiguity-20261017.');
480
+ const items = Array.isArray(raw?.items) ? raw.items : null;
481
+ if (!items || !items.length || items.length > ENGLISH_IMPORT_MAX) {
482
+ throw cliValidationError(`The file must hold { audit, items: [...] } with 1 to ${ENGLISH_IMPORT_MAX} marks.`);
483
+ }
484
+ return writeOperation("english.audit_mark", "POST", "/cli/admin/english/item-audits/mark", { audit, items }, { requiresRun: false, timeoutMs: 180000 });
485
+ }
467
486
  // Grammar Quest lessons (Mikey 2026-10-11): what the writing pass draws on
468
487
  // (each rule-first point with live sound questions on it), importing
469
488
  // drafts, approving reviewed versions [{ skillCode, version }], the list
@@ -1312,6 +1331,8 @@ export async function adminCommand(options) {
1312
1331
  });
1313
1332
  let runId = 0;
1314
1333
  let runScope = null;
1334
+ // The full run an approvals run applies (0 for every other run).
1335
+ let sourceRunId = 0;
1315
1336
  let correctionSessionId = 0;
1316
1337
  let correctionSession = null;
1317
1338
  let commentSessionId = 0;
@@ -1406,6 +1427,8 @@ export async function adminCommand(options) {
1406
1427
  if (selectedRun) {
1407
1428
  runScope = canonicalAdminRunScope(selectedRun);
1408
1429
  assertAdminOperationAllowedForRunScope({ operation, runScope });
1430
+ sourceRunId =
1431
+ runScope === "approvals" ? Number(selectedRun.sourceRunId || 0) : 0;
1409
1432
  }
1410
1433
  if (options.adminIdentity && commentSession) {
1411
1434
  const requestedIdentity = parseIdentity(options.adminIdentity);
@@ -1519,6 +1542,8 @@ export async function adminCommand(options) {
1519
1542
  operation,
1520
1543
  authToken: auth.token,
1521
1544
  runId,
1545
+ // An approvals run applies its source full run's Featured plan.
1546
+ sourceRunId,
1522
1547
  });
1523
1548
  } else if (operation.name === "build.review") {
1524
1549
  result = await runManagedBuildReview({
@@ -2302,7 +2327,8 @@ export function canonicalAdminRunScope(run) {
2302
2327
  const raw = run?.runScope;
2303
2328
  if (raw === undefined || raw === null || raw === "") return "full";
2304
2329
  const scope = String(raw);
2305
- if (["full", "featured", "newspaper"].includes(scope)) return scope;
2330
+ if (["full", "featured", "newspaper", "approvals"].includes(scope))
2331
+ return scope;
2306
2332
  throw cliValidationError(
2307
2333
  "The API returned an invalid administrator run scope.",
2308
2334
  );
@@ -2334,6 +2360,17 @@ const FEATURED_RUN_OPERATIONS = new Set([
2334
2360
  "daily-run.fail",
2335
2361
  ]);
2336
2362
 
2363
+ // An approvals run acts on Mikey's decisions whenever he gets to them: a full
2364
+ // run's write work without the newspaper, sponsor scan or bank seeding (the
2365
+ // API holds the same line through the run's scopes).
2366
+ const APPROVALS_RUN_EXCLUDED_OPERATIONS = new Set([
2367
+ "news.claim",
2368
+ "news.print",
2369
+ "news.submit",
2370
+ "sponsor.integrity.scan",
2371
+ "reward-bank.seed",
2372
+ ]);
2373
+
2337
2374
  export function assertAdminOperationAllowedForRunScope({
2338
2375
  operation,
2339
2376
  runScope,
@@ -2354,6 +2391,12 @@ export function assertAdminOperationAllowedForRunScope({
2354
2391
  `A newspaper-only run does not authorize ${operation.name}.`,
2355
2392
  );
2356
2393
  }
2394
+ if (runScope === "approvals") {
2395
+ if (!APPROVALS_RUN_EXCLUDED_OPERATIONS.has(operation.name)) return;
2396
+ throw cliValidationError(
2397
+ `An approvals run does not do ${operation.name}; that belongs to a full daily run.`,
2398
+ );
2399
+ }
2357
2400
  if (runScope !== "featured") return;
2358
2401
  const subjectCommentAlias =
2359
2402
  operation.name === "post.comments" &&
@@ -3788,8 +3831,23 @@ export function parseAdminOperation(options) {
3788
3831
  if (namespace === "daily-run") {
3789
3832
  if (action === "start") {
3790
3833
  const runScope = parseDailyRunScope(options.adminScope || "full");
3791
- const commentMode = parseCommentMode(options.commentMode || "off");
3792
- if (runScope !== "full" && commentMode !== "off") {
3834
+ // An approvals run acts on decisions, so it posts unless told otherwise.
3835
+ const commentMode = parseCommentMode(
3836
+ options.commentMode || (runScope === "approvals" ? "post" : "off"),
3837
+ );
3838
+ // Optional: lets an approvals run reuse that run's plans and drafts.
3839
+ const sourceRunId =
3840
+ runScope === "approvals" && options.adminSourceRun
3841
+ ? parseRequiredInteger(options.adminSourceRun, "--source-run", 1)
3842
+ : undefined;
3843
+ if (runScope !== "approvals" && options.adminSourceRun) {
3844
+ throw cliValidationError("--source-run is only for --scope approvals.");
3845
+ }
3846
+ if (
3847
+ runScope !== "full" &&
3848
+ runScope !== "approvals" &&
3849
+ commentMode !== "off"
3850
+ ) {
3793
3851
  throw cliValidationError("A scoped run requires --comment-mode off.");
3794
3852
  }
3795
3853
  return writeOperation(
@@ -3805,6 +3863,7 @@ export function parseAdminOperation(options) {
3805
3863
  commentMode,
3806
3864
  scope: runScope,
3807
3865
  runKey: options.runKey || defaultDailyRunKey(runScope),
3866
+ ...(sourceRunId ? { sourceRunId } : {}),
3808
3867
  },
3809
3868
  );
3810
3869
  }
@@ -5455,8 +5514,10 @@ function parseDailyRunScope(value) {
5455
5514
  const scope = String(value || "full")
5456
5515
  .trim()
5457
5516
  .toLowerCase();
5458
- if (!["full", "featured", "newspaper"].includes(scope)) {
5459
- throw cliValidationError("--scope must be full, featured, or newspaper.");
5517
+ if (!["full", "featured", "newspaper", "approvals"].includes(scope)) {
5518
+ throw cliValidationError(
5519
+ "--scope must be full, featured, newspaper, or approvals.",
5520
+ );
5460
5521
  }
5461
5522
  return scope;
5462
5523
  }
@@ -5788,17 +5849,24 @@ function printAdminEnergyBudget(energyBudget) {
5788
5849
  );
5789
5850
  }
5790
5851
  console.log(
5791
- "day | charged | overflow | users | rechg | runs | calls/run avg/p90 | $/run avg/p90 | stops chg/unchg | busy | queue rst/stop/resume | telemetry rows",
5852
+ "day | charged | overflow | users | rechg | runs | calls/run avg/p90 | $/run avg/p90 | stops chg/unchg | busy | queue rst/stop/resume/wait-cancel | telemetry rows",
5792
5853
  );
5793
5854
  for (const day of energyBudget.byDay || []) {
5794
5855
  const m = day.telemetry.metrics;
5795
5856
  const runs = day.runs.total;
5796
5857
  // Queue metrics are absent from APIs older than the queued-request telemetry.
5797
- const queue = ["queued_restored", "queued_stopped", "busy_resume_requested"]
5858
+ // worker_wait_cancelled: a request cancelled because the Build worker
5859
+ // stayed unreachable through the API handoff wait.
5860
+ const queue = [
5861
+ "queued_restored",
5862
+ "queued_stopped",
5863
+ "busy_resume_requested",
5864
+ "worker_wait_cancelled",
5865
+ ]
5798
5866
  .map((metric) => String(m[metric]?.count ?? "-"))
5799
5867
  .join("/");
5800
5868
  console.log(
5801
- `${day.dayKey}${day.inProgress ? "*" : " "}| ${formatAdminUsd(day.chargedUsd).padStart(7)} | ${formatAdminUsd(day.overflowUsd).padStart(8)} | ${String(day.users).padStart(5)} | ${String(day.recharges).padStart(5)} | ${String(runs.runs).padStart(4)} | ${String(runs.callsPerRun.avg).padStart(7)}/${String(runs.callsPerRun.p90).padEnd(9)} | ${formatAdminUsd(runs.usdPerRun.avg).padStart(6)}/${formatAdminUsd(runs.usdPerRun.p90).padEnd(6)} | ${String(m.budget_stop_changed.count).padStart(7)}/${String(m.budget_stop_unchanged.count).padEnd(7)} | ${String(m.busy_refusal.count).padStart(4)} | ${queue.padEnd(21)} | ${day.telemetry.rowCount}`,
5869
+ `${day.dayKey}${day.inProgress ? "*" : " "}| ${formatAdminUsd(day.chargedUsd).padStart(7)} | ${formatAdminUsd(day.overflowUsd).padStart(8)} | ${String(day.users).padStart(5)} | ${String(day.recharges).padStart(5)} | ${String(runs.runs).padStart(4)} | ${String(runs.callsPerRun.avg).padStart(7)}/${String(runs.callsPerRun.p90).padEnd(9)} | ${formatAdminUsd(runs.usdPerRun.avg).padStart(6)}/${formatAdminUsd(runs.usdPerRun.p90).padEnd(6)} | ${String(m.budget_stop_changed.count).padStart(7)}/${String(m.budget_stop_unchanged.count).padEnd(7)} | ${String(m.busy_refusal.count).padStart(4)} | ${queue.padEnd(33)} | ${day.telemetry.rowCount}`,
5802
5870
  );
5803
5871
  }
5804
5872
  console.log("* = in-progress UTC day; never headline it.");
@@ -909,15 +909,19 @@ async function runChromeReview({
909
909
  }
910
910
  }
911
911
  await cdp.send("Browser.close").catch(() => {});
912
+ let forcedTermination = false;
912
913
  const closeResult = await Promise.race([
913
914
  closed,
914
915
  delay(5_000).then(() => {
916
+ forcedTermination = true;
915
917
  child.kill("SIGTERM");
916
918
  return closed;
917
919
  }),
918
920
  ]);
919
921
  return {
920
922
  ...closeResult,
923
+ closeRequested: true,
924
+ forcedTermination,
921
925
  stdout,
922
926
  stderr,
923
927
  consoleLines: consoleLines.slice(-MAX_CAPTURED_LOG_LINES),
@@ -934,6 +938,31 @@ async function runChromeReview({
934
938
  }
935
939
  }
936
940
 
941
+ const CHROME_KILLED_AFTER_CLOSE_CODE = 2;
942
+
943
+ // Chrome sometimes exits 2 (RESULT_CODE_KILLED) while tearing down a
944
+ // GPU-heavy page after our own Browser.close. Every piece of evidence was
945
+ // captured before that close, so that one shutdown result is cleanup, not a
946
+ // failed review. Any other code, a signal, or our forced kill still fails.
947
+ export function reviewBrowserExitedCleanly(browser) {
948
+ return (
949
+ browser?.code === 0 ||
950
+ (browser?.code === CHROME_KILLED_AFTER_CLOSE_CODE &&
951
+ !browser.signal &&
952
+ browser.closeRequested === true &&
953
+ browser.forcedTermination === false)
954
+ );
955
+ }
956
+
957
+ // The last few Chrome stderr lines, for diagnosing a nonzero exit.
958
+ function chromeStderrTail(stderr) {
959
+ return String(stderr || "")
960
+ .split(/\r?\n/)
961
+ .filter(Boolean)
962
+ .slice(-20)
963
+ .map((line) => line.slice(0, 400));
964
+ }
965
+
937
966
  function captureConsoleEvidence(stderr, cdpLines = []) {
938
967
  const chromeLines = String(stderr || "")
939
968
  .split(/\r?\n/)
@@ -1003,8 +1032,9 @@ export async function runManagedBuildReview({ options, authToken, buildId }) {
1003
1032
  const interactionOutcome = browser.interaction || null;
1004
1033
  const interactionCompleted =
1005
1034
  !interaction || interactionOutcome?.status === "completed";
1035
+ const browserExitedCleanly = reviewBrowserExitedCleanly(browser);
1006
1036
  const confirmed =
1007
- browser.code === 0 &&
1037
+ browserExitedCleanly &&
1008
1038
  screenshotBytes > 0 &&
1009
1039
  browser.runtimeReadiness?.ready === true &&
1010
1040
  beforeVersion === afterVersion &&
@@ -1025,6 +1055,10 @@ export async function runManagedBuildReview({ options, authToken, buildId }) {
1025
1055
  executable,
1026
1056
  exitCode: browser.code,
1027
1057
  signal: browser.signal,
1058
+ closeRequested: browser.closeRequested === true,
1059
+ forcedTermination: browser.forcedTermination === true,
1060
+ exitedCleanly: browserExitedCleanly,
1061
+ stderrTail: browser.code === 0 ? null : chromeStderrTail(browser.stderr),
1028
1062
  runtimeReadiness: browser.runtimeReadiness,
1029
1063
  },
1030
1064
  screenshot: {
@@ -1059,7 +1093,12 @@ export async function runManagedBuildReview({ options, authToken, buildId }) {
1059
1093
  ? "The Build was republished during review; review the new artifact before commenting."
1060
1094
  : browser.runtimeReadiness?.runtimeErrorVisible
1061
1095
  ? "The isolated Build runtime exposed a runtime error instead of a reviewable app."
1062
- : interactionFailure
1096
+ : !browserExitedCleanly &&
1097
+ screenshotBytes > 0 &&
1098
+ browser.runtimeReadiness?.ready === true &&
1099
+ interactionCompleted
1100
+ ? `Chrome exited with ${browser.signal ? `signal ${browser.signal}` : `code ${browser.code}`}${browser.forcedTermination ? " after the CLI had to stop it" : ""} once the review evidence was captured. The app loaded and the screenshots are in the receipt, but the receipt is not confirmed; see browser.stderrTail and review again.`
1101
+ : interactionFailure
1063
1102
  ? `Interaction step ${interactionOutcome?.failedStep?.index ?? "?"} (${interactionOutcome?.failedStep?.kind ?? "?"}) failed: ${interactionOutcome?.failedStep?.error ?? "unknown"} The receipt lists the ${interactionOutcome?.stepsCompleted ?? 0} completed step(s) and their screenshots; fix the script and review again.`
1064
1103
  : "The isolated Build runtime did not become visibly reviewable before the observation window ended.",
1065
1104
  interactionFailure
package/lib/commands.js CHANGED
@@ -2798,6 +2798,7 @@ export function parseArgs(args) {
2798
2798
  adminAs: raw.as ? String(raw.as) : "",
2799
2799
  adminContextLimit: raw.limit === undefined ? 20 : Number(raw.limit),
2800
2800
  adminRun: raw.run ? String(raw.run) : "",
2801
+ adminSourceRun: raw.sourceRun ? String(raw.sourceRun) : "",
2801
2802
  adminTarget: raw.target ? String(raw.target) : "",
2802
2803
  adminActions: raw.actions ? String(raw.actions) : "",
2803
2804
  adminDate: raw.date ? String(raw.date) : "",
@@ -3235,6 +3236,7 @@ export function printHelp() {
3235
3236
  lumine admin ai-email-policy get --email <address> [--json]
3236
3237
  lumine admin ai-email-policy set --email <address> --mode <automatic|separate_accounts> --note <text> [--json]
3237
3238
  lumine admin daily-run start [--scope full|featured|newspaper] [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
3239
+ lumine admin daily-run start --scope approvals [--source-run <run-id>] [--identity zero|ciel] [--comment-mode off|draft|post] [--json] # act on Mikey's decisions; --source-run reuses that run's plans and drafts
3238
3240
  lumine admin daily-run status|report|complete|fail [--run <completed-run-id>] [--reason <text>] [--json]
3239
3241
  lumine admin daily-run escalation add --target <target> --note <summary> [--severity attention|urgent] [--json]
3240
3242
  lumine admin escalation list [--status open|acknowledged|resolved|all] [--limit <number>] [--json]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.42",
3
+ "version": "0.3.44",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,30 @@ canonical structured data.
28
28
  `off`. It cannot inspect queues, conduct, logs, Builds, or Featured.
29
29
  Completion does not advance full-review windows or carry-over telemetry.
30
30
  Its distinct `/daily-runs/start/newspaper` endpoint fails closed on older APIs.
31
+ - `--scope approvals [--source-run <id>]` acts on Mikey's decisions whenever
32
+ he gets to them (Mikey, 10-17: don't be trapped by the morning run). It has
33
+ a full run's write work: Featured plans and changes, Featured-comment and
34
+ generic recommendations, rewards, comments (draft, post, edit) and chat. It
35
+ has none of the newspaper, sponsor scan, bank seeding or reading duties, and
36
+ never counts as a full review, so review windows don't move.
37
+ - `--source-run` is optional. It lets the run use that run's prepared
38
+ Featured plan (same hash) and review receipts for up to 7 days, abandoned
39
+ runs included. A stale board is still refused at apply, so prepare a new
40
+ plan then.
41
+ - Source drafts expire after 30 minutes: re-draft an approved comment with
42
+ its approved text (with a fresh `builds review` receipt for an app). The
43
+ API refuses a second published comment by the same identity in the same
44
+ place for the same source run.
45
+ - Identity defaults to the source run's (else today's scheduled one) and
46
+ `--identity` overrides it. Either way it is named explicitly, so Bangkok
47
+ midnight doesn't end the run. `--comment-mode` defaults to `post`.
48
+ Its distinct `/daily-runs/start/approvals` endpoint fails closed on older
49
+ APIs.
50
+ - **Complete a full run when its report is delivered** (`daily-run complete`),
51
+ even with Mikey's decisions still pending. Do not hold it open for them: an
52
+ expired run can never be completed, so the review would not count, its
53
+ report would be unavailable, and the next run would re-read the whole
54
+ window. When Mikey answers, start an approvals run on it.
31
55
  - The public content actor is Zero or Ciel. Mikey's operator ID is retained in
32
56
  private audit rows and is not embedded in public comment metadata.
33
57
  - Delegated HTTP work never authenticates as the bot, opens a bot socket, changes
@@ -1017,6 +1041,8 @@ identity, or advances rotation.
1017
1041
  lumine admin daily-run start --identity auto --comment-mode off --json
1018
1042
  lumine admin daily-run start --scope featured --identity auto --json
1019
1043
  lumine admin daily-run start --scope newspaper --identity auto --json
1044
+ lumine admin daily-run start --scope approvals --source-run 125 --json
1045
+ lumine admin daily-run start --scope approvals --identity zero --json
1020
1046
  lumine admin daily-run start --identity ciel --comment-mode draft \
1021
1047
  --run-key daily:2026-08-06:review --json
1022
1048
  lumine admin daily-run status --json
@@ -2769,7 +2795,9 @@ inferred. The CLI then reads the live board and verifies exact membership/order.
2769
2795
  Retry the identical apply command after uncertain transport failure; it reuses
2770
2796
  the same idempotency key. If the final read fails or finds an intervening change,
2771
2797
  the error retains the canonical apply receipt and never restores old state.
2772
- Expired/different runs require a fresh plan and renewed approval.
2798
+ A different run requires a fresh plan and renewed approval, except an
2799
+ approvals run started with `--source-run <the plan's run>`, which can apply
2800
+ that exact plan with the same hash (or prepare a new one).
2773
2801
 
2774
2802
  Use the following workflow for full daily comment review, or only when Mikey's
2775
2803
  Featured slice includes comment encouragement: