@stage5/lumine 0.2.21 → 0.2.23
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 +78 -11
- package/lib/commands.js +24 -21
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +181 -4
package/lib/admin.js
CHANGED
|
@@ -30,6 +30,11 @@ function readEditorialFile(filePath) {
|
|
|
30
30
|
|
|
31
31
|
export async function adminCommand(options) {
|
|
32
32
|
const operation = parseAdminOperation(options);
|
|
33
|
+
const viewFilter = resolveOperatorViewFilter({
|
|
34
|
+
operation,
|
|
35
|
+
unviewed: options.adminUnviewed,
|
|
36
|
+
viewed: options.adminViewed,
|
|
37
|
+
});
|
|
33
38
|
const recommendationContentTypes =
|
|
34
39
|
operation.name === "recommendations.list"
|
|
35
40
|
? parseRecommendationContentTypes(options.adminContentTypes)
|
|
@@ -90,6 +95,9 @@ export async function adminCommand(options) {
|
|
|
90
95
|
contentTypes: recommendationContentTypes,
|
|
91
96
|
});
|
|
92
97
|
}
|
|
98
|
+
if (viewFilter) {
|
|
99
|
+
result = filterListResultByOperatorView({ result, viewFilter });
|
|
100
|
+
}
|
|
93
101
|
} catch (error) {
|
|
94
102
|
if (operation.mutates && requestId) {
|
|
95
103
|
const retryInstruction = `Retry with --idempotency-key ${requestId}.`;
|
|
@@ -169,6 +177,70 @@ export function filterRecommendationQueueResult({ result, contentTypes }) {
|
|
|
169
177
|
};
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
// Escalation lists are only useful when they exclude what Mikey already read,
|
|
181
|
+
// so list output can be narrowed by his own view state. The server stamps
|
|
182
|
+
// `operatorViewed` on every listed item; an item missing the field (an older
|
|
183
|
+
// deployed API) is treated as unknown and kept, so the filter can never hide
|
|
184
|
+
// something by accident.
|
|
185
|
+
export function filterListResultByOperatorView({ result, viewFilter }) {
|
|
186
|
+
if (!viewFilter) return result;
|
|
187
|
+
const collections = ["items", "subjects", "comments"];
|
|
188
|
+
const data = { ...(result?.data || {}) };
|
|
189
|
+
let excluded = 0;
|
|
190
|
+
let unknown = 0;
|
|
191
|
+
for (const key of collections) {
|
|
192
|
+
if (!Array.isArray(data[key])) continue;
|
|
193
|
+
const kept = data[key].filter((entry) => {
|
|
194
|
+
const state = entry?.operatorViewed;
|
|
195
|
+
if (!state || typeof state.viewed !== "boolean") {
|
|
196
|
+
unknown += 1;
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
const keep = viewFilter === "unviewed" ? !state.viewed : state.viewed;
|
|
200
|
+
if (!keep) excluded += 1;
|
|
201
|
+
return keep;
|
|
202
|
+
});
|
|
203
|
+
data[key] = kept;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
...result,
|
|
207
|
+
data: {
|
|
208
|
+
...data,
|
|
209
|
+
operatorViewFilter: {
|
|
210
|
+
mode: viewFilter,
|
|
211
|
+
excludedItems: excluded,
|
|
212
|
+
unknownStateItems: unknown,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function parseOperatorViewFilter({ unviewed, viewed }) {
|
|
219
|
+
if (unviewed && viewed) {
|
|
220
|
+
throw cliValidationError("Pass either --unviewed or --viewed, not both.");
|
|
221
|
+
}
|
|
222
|
+
if (unviewed) return "unviewed";
|
|
223
|
+
if (viewed) return "viewed";
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const OPERATOR_VIEW_FILTER_OPERATIONS = new Set([
|
|
228
|
+
"recommendations.list",
|
|
229
|
+
"subjects.candidates",
|
|
230
|
+
"featured.list",
|
|
231
|
+
"subject.comments",
|
|
232
|
+
"post.comments",
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
export function resolveOperatorViewFilter({ operation, unviewed, viewed }) {
|
|
236
|
+
const viewFilter = parseOperatorViewFilter({ unviewed, viewed });
|
|
237
|
+
if (!viewFilter) return null;
|
|
238
|
+
if (OPERATOR_VIEW_FILTER_OPERATIONS.has(operation.name)) return viewFilter;
|
|
239
|
+
throw cliValidationError(
|
|
240
|
+
"--unviewed and --viewed are supported only by admin content-list commands.",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
172
244
|
function adminOperationRequiresRun(operation) {
|
|
173
245
|
return ![
|
|
174
246
|
"identity.list",
|
|
@@ -428,17 +500,12 @@ export function parseAdminOperation(options) {
|
|
|
428
500
|
"Pass the claim's lease token with --lease-token <token>.",
|
|
429
501
|
);
|
|
430
502
|
}
|
|
431
|
-
return writeOperation(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
leaseToken,
|
|
438
|
-
editorial: readEditorialFile(options.adminFile),
|
|
439
|
-
model: options.model || undefined,
|
|
440
|
-
},
|
|
441
|
-
);
|
|
503
|
+
return writeOperation("news.submit", "POST", "/cli/admin/news/submit", {
|
|
504
|
+
editionId,
|
|
505
|
+
leaseToken,
|
|
506
|
+
editorial: readEditorialFile(options.adminFile),
|
|
507
|
+
model: options.model || undefined,
|
|
508
|
+
});
|
|
442
509
|
}
|
|
443
510
|
throw cliValidationError(
|
|
444
511
|
"Usage: lumine admin news [status] | news print | news claim | news submit --edition-id <id> --lease-token <token> --file <editorial.json>",
|
package/lib/commands.js
CHANGED
|
@@ -47,11 +47,7 @@ import {
|
|
|
47
47
|
suggestBuildThumbnailToOwner,
|
|
48
48
|
updateBuildMetadata,
|
|
49
49
|
} from "./api.js";
|
|
50
|
-
import {
|
|
51
|
-
assetsCommand,
|
|
52
|
-
confirmPrompt,
|
|
53
|
-
writeAssetsManifest,
|
|
54
|
-
} from "./assets.js";
|
|
50
|
+
import { assetsCommand, confirmPrompt, writeAssetsManifest } from "./assets.js";
|
|
55
51
|
import { thumbnailCommand } from "./thumbnail.js";
|
|
56
52
|
import {
|
|
57
53
|
assertAuthScope,
|
|
@@ -491,7 +487,9 @@ export async function suggestions(options) {
|
|
|
491
487
|
|
|
492
488
|
if (action === "merge") {
|
|
493
489
|
if (suggestion.type !== "branch") {
|
|
494
|
-
throw new Error(
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Suggestion #${suggestionId} is not a branch suggestion.`,
|
|
492
|
+
);
|
|
495
493
|
}
|
|
496
494
|
const mergeResult = await mergeContributionIntoMain({
|
|
497
495
|
options,
|
|
@@ -510,7 +508,9 @@ export async function suggestions(options) {
|
|
|
510
508
|
|
|
511
509
|
if (action === "replace-main") {
|
|
512
510
|
if (suggestion.type !== "branch") {
|
|
513
|
-
throw new Error(
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Suggestion #${suggestionId} is not a branch suggestion.`,
|
|
513
|
+
);
|
|
514
514
|
}
|
|
515
515
|
const replaceResult = await replaceMainWithContribution({
|
|
516
516
|
options,
|
|
@@ -578,9 +578,7 @@ async function resolveSuggestionRootBuildId(options, auth) {
|
|
|
578
578
|
auth,
|
|
579
579
|
buildId: requestedBuildId,
|
|
580
580
|
});
|
|
581
|
-
return (
|
|
582
|
-
Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0)
|
|
583
|
-
);
|
|
581
|
+
return Number(build?.contributionRootBuildId || 0) || Number(build?.id || 0);
|
|
584
582
|
}
|
|
585
583
|
|
|
586
584
|
export function printBuildSuggestions({
|
|
@@ -1872,9 +1870,7 @@ export function printPullResult(result) {
|
|
|
1872
1870
|
console.log(
|
|
1873
1871
|
'Notify the owner when ready: lumine suggest branch "Ready for review"',
|
|
1874
1872
|
);
|
|
1875
|
-
console.log(
|
|
1876
|
-
"Offer this branch's thumbnail: lumine suggest thumbnail",
|
|
1877
|
-
);
|
|
1873
|
+
console.log("Offer this branch's thumbnail: lumine suggest thumbnail");
|
|
1878
1874
|
} else {
|
|
1879
1875
|
console.log("Run `lumine check` or `lumine launch --save` when ready.");
|
|
1880
1876
|
console.log("Review team nudges: lumine suggestions");
|
|
@@ -2097,6 +2093,8 @@ export function parseArgs(args) {
|
|
|
2097
2093
|
"includeComments",
|
|
2098
2094
|
"anyoneCanReward",
|
|
2099
2095
|
"full",
|
|
2096
|
+
"unviewed",
|
|
2097
|
+
"viewed",
|
|
2100
2098
|
]);
|
|
2101
2099
|
|
|
2102
2100
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -2177,6 +2175,8 @@ export function parseArgs(args) {
|
|
|
2177
2175
|
adminType: raw.type ? String(raw.type) : "",
|
|
2178
2176
|
adminKind: raw.kind ? String(raw.kind) : "",
|
|
2179
2177
|
adminContentTypes: raw.contentTypes ? String(raw.contentTypes) : "",
|
|
2178
|
+
adminUnviewed: Boolean(raw.unviewed),
|
|
2179
|
+
adminViewed: Boolean(raw.viewed),
|
|
2180
2180
|
adminIdentity: raw.identity ? String(raw.identity) : "",
|
|
2181
2181
|
commentMode: raw.commentMode ? String(raw.commentMode) : "",
|
|
2182
2182
|
runKey: raw.runKey ? String(raw.runKey) : "",
|
|
@@ -2211,9 +2211,7 @@ export function parseArgs(args) {
|
|
|
2211
2211
|
target:
|
|
2212
2212
|
raw.url ||
|
|
2213
2213
|
raw.target ||
|
|
2214
|
-
(command === "rename" ||
|
|
2215
|
-
command === "describe" ||
|
|
2216
|
-
command === "suggest"
|
|
2214
|
+
(command === "rename" || command === "describe" || command === "suggest"
|
|
2217
2215
|
? ""
|
|
2218
2216
|
: command === "suggestions"
|
|
2219
2217
|
? suggestionListTarget
|
|
@@ -2510,14 +2508,17 @@ export function printHelp() {
|
|
|
2510
2508
|
lumine admin identity list|status|use <zero|ciel|auto> [--json]
|
|
2511
2509
|
lumine admin daily-run start [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
|
|
2512
2510
|
lumine admin daily-run status|complete|fail [--reason <text>] [--json]
|
|
2513
|
-
lumine admin recommendations list [--content-types comment,dailyReflection] [--cursor <cursor>] [--json]
|
|
2514
|
-
lumine admin subjects candidates [--after <date>] [--effort unassigned] [--cursor <cursor>] [--json]
|
|
2515
|
-
lumine admin subject get|
|
|
2511
|
+
lumine admin recommendations list [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
2512
|
+
lumine admin subjects candidates [--after <date>] [--effort unassigned] [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
2513
|
+
lumine admin subject get|reveal <subject-url-or-id> [--json]
|
|
2514
|
+
lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
2516
2515
|
lumine admin subject effort set <subject-id> --level <1|2|3> [--json]
|
|
2517
2516
|
lumine admin subject creator set-made-by-poster <subject-id> [--json]
|
|
2518
2517
|
lumine admin subject feature|unfeature <subject-id> [--json]
|
|
2519
|
-
lumine admin featured list
|
|
2520
|
-
lumine admin
|
|
2518
|
+
lumine admin featured list [--unviewed|--viewed] [--json]
|
|
2519
|
+
lumine admin featured reorder --subject-ids <id,id,...> [--json]
|
|
2520
|
+
lumine admin post get <target> [--type subject|comment|aiStory|dailyReflection] [--json]
|
|
2521
|
+
lumine admin post comments <target> [--type subject|aiStory|dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
2521
2522
|
lumine admin post recommend <target> [--type subject|comment|aiStory|dailyReflection] [--anyone-can-reward] [--reward-twinkles 3] [--json]
|
|
2522
2523
|
lumine admin post skip <target> [--type comment|aiStory|dailyReflection] [--reason <text>] [--json]
|
|
2523
2524
|
lumine admin post reward <target> [--type subject|comment|aiStory|dailyReflection] --twinkles 3 [--json]
|
|
@@ -2590,6 +2591,8 @@ Options:
|
|
|
2590
2591
|
--cursor <id> Continue an owner suggestion inbox listing
|
|
2591
2592
|
--after <date> Admin subjects: inclusive Unix/ISO creation boundary
|
|
2592
2593
|
--effort unassigned Admin subjects: show only unassigned effort
|
|
2594
|
+
--unviewed Admin content lists: retain unviewed and unknown items
|
|
2595
|
+
--viewed Admin content lists: retain viewed and unknown items
|
|
2593
2596
|
--identity <mode> Admin identity: zero, ciel, or auto
|
|
2594
2597
|
--comment-mode <mode> Admin run comments: off, draft, or post
|
|
2595
2598
|
--run-key <key> Idempotency key for an admin daily run
|
package/package.json
CHANGED
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -21,8 +21,18 @@ canonical structured data.
|
|
|
21
21
|
- The public content actor is Zero or Ciel. Mikey's operator ID is retained in
|
|
22
22
|
private audit rows and is not embedded in public comment metadata.
|
|
23
23
|
- Delegated HTTP work never authenticates as the bot, opens a bot socket, changes
|
|
24
|
-
bot sessions, or updates bot presence/
|
|
25
|
-
|
|
24
|
+
bot sessions, or updates bot presence/typing state. Normal content mutations
|
|
25
|
+
still emit Twinkle's canonical real-time content events.
|
|
26
|
+
- One deliberate exception to the last-seen rule: a delegated mutation that
|
|
27
|
+
actually changed something stamps the acting bot's `users.lastActive`, so
|
|
28
|
+
Zero's and Ciel's public "last online" reflects the real day they commented on
|
|
29
|
+
and rewarded kids' posts instead of whenever a socket last closed. It is
|
|
30
|
+
written after the audit transaction commits, throttled to once a minute, and
|
|
31
|
+
best-effort — it can never fail or deadlock the content mutation. Presence is
|
|
32
|
+
still untouched: no socket is opened and no `online_status_changed` is emitted,
|
|
33
|
+
so the bots remain absent from the online list. Note that `lastActive` is the
|
|
34
|
+
ordering key for the People directory, so the bots now surface there after a
|
|
35
|
+
run; that is the intended consequence of the timestamp being truthful.
|
|
26
36
|
- A later human reply to a delegated Zero/Ciel comment enters the existing
|
|
27
37
|
server-side autonomous comment-assistant pipeline. Lumine does not need to
|
|
28
38
|
remain running.
|
|
@@ -39,6 +49,117 @@ Comment mode is stored only on the current run:
|
|
|
39
49
|
- `draft`: server-generated drafts, no public comment.
|
|
40
50
|
- `post`: drafts plus idempotent publication through the ordinary comment path.
|
|
41
51
|
|
|
52
|
+
## Editorial priorities
|
|
53
|
+
|
|
54
|
+
The CLI enforces none of this — it is the standing instruction for the operator
|
|
55
|
+
or agent making the judgments, and it applies to every verb below: recommends,
|
|
56
|
+
rewards, effort levels, Featured, skips, comments, and replies.
|
|
57
|
+
|
|
58
|
+
**Twinkle is not Reddit.** Do not rank a run's attention by popularity,
|
|
59
|
+
recommendation count, or polish. Most users here are young children, and the
|
|
60
|
+
posts that most need Zero or Ciel are the ones nobody else answered.
|
|
61
|
+
|
|
62
|
+
- **Look first at new, quiet, and overlooked users.** A child's first post, or a
|
|
63
|
+
post from someone who rarely gets replies, is worth more of a run's attention
|
|
64
|
+
than another well-liked post that already has a lively thread.
|
|
65
|
+
- **Clumsy is not the same as low-effort.** Bad spelling, a one-line
|
|
66
|
+
description, a title that is just "hi", a drawing that did not come out right
|
|
67
|
+
— these are usually a child trying, not a child spamming. Read for the real
|
|
68
|
+
thing they were reaching for and respond to that.
|
|
69
|
+
- **Zero engagement is a reason to act, not to skip.** A post sitting at no
|
|
70
|
+
recommendations and no comments is the strongest signal in the queue that
|
|
71
|
+
someone should notice it.
|
|
72
|
+
- **Thought-provoking posts deserve substance, not applause — and an unnoticed
|
|
73
|
+
one is the highest priority of all.** When a child asks a real question or
|
|
74
|
+
makes a real argument and the thread is empty, that is the clearest case for
|
|
75
|
+
a Zero/Ciel comment on the whole site. Engage with the idea itself: answer it,
|
|
76
|
+
add a perspective or a counter-consideration, and leave the author somewhere
|
|
77
|
+
to go next. A good question that nobody answered teaches a child that thinking
|
|
78
|
+
hard is not worth it; that is the outcome these runs exist to prevent. This
|
|
79
|
+
cuts both ways with the point above — the two ends of the queue, the beginner
|
|
80
|
+
nobody noticed and the strong idea nobody engaged, both outrank the popular
|
|
81
|
+
post that already has a lively thread.
|
|
82
|
+
- **Always answer Twinkle usage questions.** "How do I change my username",
|
|
83
|
+
"why can't I reward", "what unlocks the Summoner" — a child stuck on the site
|
|
84
|
+
cannot use it. Answer concretely and verify anything you are unsure of against
|
|
85
|
+
the canonical rules before publishing; say you will check with Mikey rather
|
|
86
|
+
than guessing at mechanics.
|
|
87
|
+
- **Always respond to bug reports, and tag `@mikey` in the comment.** The
|
|
88
|
+
mention is what notifies him (`postComment` runs `processMentions` /
|
|
89
|
+
`postMentions` and emits `new_targeted_upload`), so a bug-report comment
|
|
90
|
+
without `@mikey` fails its main job. Restate what the child observed; never
|
|
91
|
+
promise a fix or a timeline.
|
|
92
|
+
- **Guide users through the website, without waiting to be asked.** A post can
|
|
93
|
+
show that a child is stuck, confused, or unaware a feature exists without ever
|
|
94
|
+
containing a question — someone begging for coins who does not know about
|
|
95
|
+
daily rewards, someone reposting because they could not find their own post.
|
|
96
|
+
Give them a short, friendly crash course on the exact thing they are stuck on.
|
|
97
|
+
Teaching a child to use the site is worth more than any single recommend.
|
|
98
|
+
- **Reserve `post skip` for genuine noise** — card-sale and coin-begging spam,
|
|
99
|
+
keyboard mash, duplicates, engagement farming — not for sincere posts that
|
|
100
|
+
merely look unimpressive.
|
|
101
|
+
- **Effort levels are not a verdict on the child.** Level 1 on a thin post is
|
|
102
|
+
ordinary bookkeeping; it never means the author deserves less attention, and
|
|
103
|
+
it pairs well with a warm comment.
|
|
104
|
+
- **Featured still selects for quality**, but when two candidates are close,
|
|
105
|
+
prefer the child who has never been featured over the one who has.
|
|
106
|
+
|
|
107
|
+
Sensitive disclosures, active disputes, and anything needing crisis or medical
|
|
108
|
+
judgment remain out of scope for a bot comment no matter how neglected the post
|
|
109
|
+
is. Those go to Mikey.
|
|
110
|
+
|
|
111
|
+
## Escalation to Mikey
|
|
112
|
+
|
|
113
|
+
A run is not finished when the mutations are done. Curation surfaces things only
|
|
114
|
+
a human owner can decide, and a finding nobody reports is a finding that did not
|
|
115
|
+
happen. **Every run ends with an escalation list**, and it belongs in the run's
|
|
116
|
+
final report whether or not anyone asks for it.
|
|
117
|
+
|
|
118
|
+
Escalate, with the canonical `https://www.twin-kle.com/subjects/<id>` or
|
|
119
|
+
`/comments/<id>` URL, a one-line summary, and why it needs him:
|
|
120
|
+
|
|
121
|
+
- **Child-safety and wellbeing** — distress or mental-health disclosures,
|
|
122
|
+
anything about self-harm, a child asking for a photo of themselves to be
|
|
123
|
+
removed, requests to delete or hide personal information, contact details
|
|
124
|
+
posted in public, or a child who says they are leaving because something
|
|
125
|
+
happened. These outrank every other category.
|
|
126
|
+
- **Account integrity** — someone posting from another person's account,
|
|
127
|
+
impersonation, shared logins, or a user operating a set of alternate accounts.
|
|
128
|
+
- **Economy manipulation** — coin or XP farming across alternate accounts,
|
|
129
|
+
paid-grinding arrangements, "invest and I will pay you back more" offers,
|
|
130
|
+
pay-me-to-win contests, and anything that teaches other children a method for
|
|
131
|
+
any of these. Note the recommendation count: a manipulation how-to that other
|
|
132
|
+
kids are recommending is spreading, and that is the urgent part.
|
|
133
|
+
- **Bug reports** the run encountered, even secondhand in a comment thread.
|
|
134
|
+
|
|
135
|
+
Two rules that keep the list worth reading:
|
|
136
|
+
|
|
137
|
+
- **Check the thread before escalating.** If Mikey already replied in it, the
|
|
138
|
+
matter is his and it is closed unless something new happened after his reply —
|
|
139
|
+
re-reporting it wastes the one channel that is supposed to mean "look at this."
|
|
140
|
+
Say so explicitly when a post looks alarming but he already handled it.
|
|
141
|
+
- **Check `operatorViewed` too.** Every Subject, Comment, StandalonePost, and
|
|
142
|
+
queue item reports whether Mikey has opened that content and when. Silence is
|
|
143
|
+
not the same as not having seen it — he often reads without replying. Lead the
|
|
144
|
+
escalation list with items where `viewed` is false, and mark the rest as
|
|
145
|
+
already-seen rather than dropping them, since he may have looked before the
|
|
146
|
+
thing you are escalating happened. `lumine admin subjects candidates --unviewed`
|
|
147
|
+
and `lumine admin recommendations list --unviewed` filter a page down to what
|
|
148
|
+
he has not opened (`--viewed` inverts it). The flags are supported by the
|
|
149
|
+
recommendation, Subject, Featured, and comment-list commands only; they are
|
|
150
|
+
rejected before any request on every other command.
|
|
151
|
+
|
|
152
|
+
Two limits make this a strong negative signal and a weak positive one: a view
|
|
153
|
+
is recorded only when the content **page** is opened, so reading a post inline
|
|
154
|
+
in a feed records nothing, and a user's view of their **own** content is never
|
|
155
|
+
recorded. So `viewed: true` reliably means he opened it; `viewed: false` means
|
|
156
|
+
"no page open recorded", not "he never saw it". Never tell a child, in public,
|
|
157
|
+
whether Mikey has or has not looked at their post.
|
|
158
|
+
|
|
159
|
+
- **Escalate; do not moderate.** Zero and Ciel have no moderation verbs here by
|
|
160
|
+
design. Do not delete, hide, argue with, or publicly accuse anyone, and do not
|
|
161
|
+
warn a child that they are in trouble. Report it and let Mikey decide.
|
|
162
|
+
|
|
42
163
|
## Common JSON types
|
|
43
164
|
|
|
44
165
|
All `--json` success output is one uncolored JSON value:
|
|
@@ -76,6 +197,15 @@ type Failure = {
|
|
|
76
197
|
Shared records:
|
|
77
198
|
|
|
78
199
|
```ts
|
|
200
|
+
// Present on Subject, Comment, StandalonePost, and every recommend-queue item.
|
|
201
|
+
// This is the OPERATOR's own view state (Mikey), never the bot's, and reading it
|
|
202
|
+
// records nothing.
|
|
203
|
+
type OperatorViewed = {
|
|
204
|
+
viewed: boolean;
|
|
205
|
+
firstViewedAt: number | null; // Unix seconds
|
|
206
|
+
lastViewedAt: number | null;
|
|
207
|
+
};
|
|
208
|
+
|
|
79
209
|
type Author = { id: number | null; username: string | null };
|
|
80
210
|
|
|
81
211
|
type Attachment = {
|
|
@@ -305,9 +435,11 @@ JSON error includes `details.retryIdempotencyKey` for a safe exact retry.
|
|
|
305
435
|
```bash
|
|
306
436
|
lumine admin recommendations list --kind recommend \
|
|
307
437
|
--content-types comment,dailyReflection --cursor '<cursor>' --json
|
|
438
|
+
lumine admin recommendations list --unviewed --json
|
|
308
439
|
lumine admin subjects candidates --after 2026-08-01T00:00:00Z \
|
|
309
440
|
--cursor '<cursor>' --json
|
|
310
441
|
lumine admin subjects candidates --effort unassigned --json
|
|
442
|
+
lumine admin subjects candidates --unviewed --json
|
|
311
443
|
```
|
|
312
444
|
|
|
313
445
|
Schemas:
|
|
@@ -743,8 +875,18 @@ re-checked transactionally at commit.
|
|
|
743
875
|
type GeneratedEditorial = {
|
|
744
876
|
mastheadHeadline: string;
|
|
745
877
|
mastheadDeck: string;
|
|
746
|
-
lead: {
|
|
747
|
-
|
|
878
|
+
lead: {
|
|
879
|
+
eventKey: string;
|
|
880
|
+
headline: string;
|
|
881
|
+
summary: string;
|
|
882
|
+
sourceQuote: string;
|
|
883
|
+
} | null;
|
|
884
|
+
stories: Array<{
|
|
885
|
+
eventKey: string;
|
|
886
|
+
headline: string;
|
|
887
|
+
summary: string;
|
|
888
|
+
sourceQuote: string;
|
|
889
|
+
}>;
|
|
748
890
|
editorsNote: string;
|
|
749
891
|
};
|
|
750
892
|
```
|
|
@@ -756,6 +898,41 @@ authored material; only a `section: "front"` event may be the lead. Preserve
|
|
|
756
898
|
substance, names, and numbers. Do not mention official announcements (the
|
|
757
899
|
server adds them), and give non-front events an empty `sourceQuote`.
|
|
758
900
|
|
|
901
|
+
**Editorial craft.** The rules above make an edition valid; they do not make
|
|
902
|
+
it good. The server already used the digest's `priority` numbers to select
|
|
903
|
+
the bounded set of eligible events; those numbers do not do the editor's job
|
|
904
|
+
within the returned digest. On a typical day every front subject arrives with
|
|
905
|
+
the same priority, so treat a tied score (or recency) as no signal at all and
|
|
906
|
+
make the call by reading:
|
|
907
|
+
|
|
908
|
+
- **Choose the lead by argument, not by score or recency.** The best lead is
|
|
909
|
+
the front event where something is actually *at stake*: a claim with
|
|
910
|
+
reasoning, a question with a position behind it — ideally one other member
|
|
911
|
+
are already responding to. A claim plus a reply is a conversation in
|
|
912
|
+
motion; a drawing, a greeting, or a link is a share, and shares belong
|
|
913
|
+
further down the page, not in the lead.
|
|
914
|
+
- **Thread a theme through the paper.** Pick the strongest idea of the day
|
|
915
|
+
and let the masthead, the lead, and the editor's note all carry it, with
|
|
916
|
+
the editor's note reprising a community value from one of the day's posts
|
|
917
|
+
rather than summarizing the edition. The paper should end on something a
|
|
918
|
+
child can take with them.
|
|
919
|
+
- **Cross-reference events into arcs.** The same thing often appears in the
|
|
920
|
+
digest several times (an app's release, its open-sourcing, and its maker's
|
|
921
|
+
Daily Reflection about it). Write those as one story arc — the origin
|
|
922
|
+
story up front, the notices pointing back to it — instead of three
|
|
923
|
+
disconnected blurbs. Each story still cites only its own `eventKey`.
|
|
924
|
+
- **Headlines tell the story; they do not restate the title.** "Logged Out
|
|
925
|
+
and Briefly Panicked, X Confirms: Twinkle Is 5% of Life" beats repeating
|
|
926
|
+
the post's title. Keep every fact traceable to digest text — the craft is
|
|
927
|
+
in selection and framing, never in invention.
|
|
928
|
+
- **Work mechanics under the ten-minute lease.** Dump every front event's
|
|
929
|
+
full summary to a file immediately after claiming, before writing a word.
|
|
930
|
+
Copy `sourceQuote`s byte-exact from the summary — curly apostrophes,
|
|
931
|
+
markdown asterisks, ellipsis dots and all (an inexact quote is silently
|
|
932
|
+
replaced with canonical text). An event with an empty summary gets an
|
|
933
|
+
empty `sourceQuote`; restate its title's facts in your story summary
|
|
934
|
+
instead.
|
|
935
|
+
|
|
759
936
|
Claim edge cases: a quiet day (no editorial events) is committed as the
|
|
760
937
|
canonical quiet edition at claim time — no editorial needed, the response
|
|
761
938
|
says so. If the claim is not submitted before the lease expires, the server's
|