@stage5/lumine 0.2.18 → 0.2.20
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 +58 -0
- package/bin/lumine.js +6 -1
- package/lib/admin.js +890 -0
- package/lib/commands.js +75 -2
- package/lib/constants.js +1 -0
- package/lib/http.js +4 -1
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +907 -0
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
# Lumine delegated-administrator contracts
|
|
2
|
+
|
|
3
|
+
`lumine admin` exposes deterministic community-management primitives. It does
|
|
4
|
+
not decide whether content is good, harmful, original, deserving of effort, or
|
|
5
|
+
worth commenting on. An LLM or human operator makes those judgments from the
|
|
6
|
+
canonical structured data.
|
|
7
|
+
|
|
8
|
+
## Security and run model
|
|
9
|
+
|
|
10
|
+
- The saved Lumine login always authenticates the real operator. The API reloads
|
|
11
|
+
that user's current role from the writer database on every request.
|
|
12
|
+
- Only the server-configured administrator with authoritative administrator
|
|
13
|
+
management authority may delegate. Effective Level 5 alone is rejected.
|
|
14
|
+
Only the immutable
|
|
15
|
+
server-owned Zero and Ciel user IDs are approved. Usernames and CLI flags are
|
|
16
|
+
not authority.
|
|
17
|
+
- `daily-run start` creates or returns a six-hour `delegated-admin` run with
|
|
18
|
+
explicit scopes and one public actor. Every run-scoped CLI command loads the
|
|
19
|
+
canonical active run and sends its ID; the API rejects a missing, expired, or
|
|
20
|
+
mismatched run.
|
|
21
|
+
- The public content actor is Zero or Ciel. Mikey's operator ID is retained in
|
|
22
|
+
private audit rows and is not embedded in public comment metadata.
|
|
23
|
+
- Delegated HTTP work never authenticates as the bot, opens a bot socket, changes
|
|
24
|
+
bot sessions, or updates bot presence/last-seen/typing state. Normal content
|
|
25
|
+
mutations still emit Twinkle's canonical real-time content events.
|
|
26
|
+
- A later human reply to a delegated Zero/Ciel comment enters the existing
|
|
27
|
+
server-side autonomous comment-assistant pipeline. Lumine does not need to
|
|
28
|
+
remain running.
|
|
29
|
+
|
|
30
|
+
`auto` selects Zero first when no completed rotation exists, then alternates
|
|
31
|
+
after a successfully completed run that performed a mutation. Failed,
|
|
32
|
+
abandoned, expired, and read-only runs do not advance rotation. Reusing a run
|
|
33
|
+
key returns the original run and identity. One writer-locked identity-state row
|
|
34
|
+
serializes concurrent starts.
|
|
35
|
+
|
|
36
|
+
Comment mode is stored only on the current run:
|
|
37
|
+
|
|
38
|
+
- `off` (default): no draft or post scope.
|
|
39
|
+
- `draft`: server-generated drafts, no public comment.
|
|
40
|
+
- `post`: drafts plus idempotent publication through the ordinary comment path.
|
|
41
|
+
|
|
42
|
+
## Common JSON types
|
|
43
|
+
|
|
44
|
+
All `--json` success output is one uncolored JSON value:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
type Success<D> = {
|
|
48
|
+
ok: true;
|
|
49
|
+
status: "success" | "already_done" | "no_op" | "maximum_reached";
|
|
50
|
+
changed?: boolean; // mutations only
|
|
51
|
+
data: D;
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Failures print one JSON value, write no progress prose, and exit nonzero:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
type Failure = {
|
|
59
|
+
ok: false;
|
|
60
|
+
status:
|
|
61
|
+
| "unauthenticated"
|
|
62
|
+
| "forbidden"
|
|
63
|
+
| "not_found"
|
|
64
|
+
| "validation_error"
|
|
65
|
+
| "partial_failure"
|
|
66
|
+
| "internal_error"
|
|
67
|
+
| "error";
|
|
68
|
+
error: {
|
|
69
|
+
code: string;
|
|
70
|
+
message: string;
|
|
71
|
+
details: unknown | null;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Shared records:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
type Author = { id: number | null; username: string | null };
|
|
80
|
+
|
|
81
|
+
type Attachment = {
|
|
82
|
+
filePath: string | null;
|
|
83
|
+
fileName: string | null;
|
|
84
|
+
fileSize: number | null;
|
|
85
|
+
thumbUrl: string | null;
|
|
86
|
+
url: string | null; // canonical attachment URL when path and name exist
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type RecommendationState = {
|
|
90
|
+
recommendedByActor: boolean;
|
|
91
|
+
actorRecommendationId: number | null;
|
|
92
|
+
anyoneCanReward: boolean | null;
|
|
93
|
+
count: number;
|
|
94
|
+
items: Array<{
|
|
95
|
+
id: number;
|
|
96
|
+
actor: Author;
|
|
97
|
+
anyoneCanReward: boolean;
|
|
98
|
+
createdAt: number | null;
|
|
99
|
+
}>;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type RewardState = {
|
|
103
|
+
totalTwinkles: number;
|
|
104
|
+
actorTwinkles: number;
|
|
105
|
+
actorAlreadyRewardedThree: boolean;
|
|
106
|
+
caps: {
|
|
107
|
+
maxRewardAmount: number;
|
|
108
|
+
maxRewardAmountForOnePerson: number;
|
|
109
|
+
} | null;
|
|
110
|
+
items: Array<{
|
|
111
|
+
id: number;
|
|
112
|
+
recipientUserId: number | null;
|
|
113
|
+
rewarder: Author;
|
|
114
|
+
type: string | null;
|
|
115
|
+
amount: number;
|
|
116
|
+
comment: string | null;
|
|
117
|
+
claimed: boolean;
|
|
118
|
+
createdAt: number | null;
|
|
119
|
+
}>;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type Subject = {
|
|
123
|
+
id: number;
|
|
124
|
+
url: string; // https://www.twin-kle.com/subjects/<id>
|
|
125
|
+
author: Author;
|
|
126
|
+
createdAt: number | null; // Unix seconds
|
|
127
|
+
updatedAt: null;
|
|
128
|
+
title: string | null;
|
|
129
|
+
description: string | null;
|
|
130
|
+
attachment: Attachment | null;
|
|
131
|
+
hasSecretAnswer: boolean;
|
|
132
|
+
hasSecretAttachment: boolean;
|
|
133
|
+
secret: { hasSecret: boolean; revealed: boolean | null };
|
|
134
|
+
effortLevel: number; // 0 means unassigned
|
|
135
|
+
effortRevision: number;
|
|
136
|
+
featured: { member: boolean; order: number | null }; // one-based order
|
|
137
|
+
createdByAuthor: boolean;
|
|
138
|
+
recommendation: RecommendationState;
|
|
139
|
+
reward: RewardState;
|
|
140
|
+
root: { type: string | null; id: number | null };
|
|
141
|
+
ageRestriction: string | null;
|
|
142
|
+
deleted: false;
|
|
143
|
+
unavailable: false;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type Comment = {
|
|
147
|
+
id: number;
|
|
148
|
+
url: string; // https://www.twin-kle.com/comments/<id>
|
|
149
|
+
subjectUrl: string | null;
|
|
150
|
+
author: Author;
|
|
151
|
+
createdAt: number | null;
|
|
152
|
+
updatedAt: null;
|
|
153
|
+
content: string | null;
|
|
154
|
+
contentHidden: boolean;
|
|
155
|
+
attachment: Attachment | null;
|
|
156
|
+
parentCommentId: number | null;
|
|
157
|
+
replyToCommentId: number | null;
|
|
158
|
+
isNotification: boolean;
|
|
159
|
+
ageRestriction: string | null;
|
|
160
|
+
deleted: false;
|
|
161
|
+
unavailable: false;
|
|
162
|
+
recommendation: RecommendationState;
|
|
163
|
+
reward: RewardState;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
type StandalonePost = {
|
|
167
|
+
contentType: "aiStory" | "dailyReflection";
|
|
168
|
+
contentId: number;
|
|
169
|
+
id: number;
|
|
170
|
+
url: string;
|
|
171
|
+
author: Author;
|
|
172
|
+
createdAt: number | null;
|
|
173
|
+
updatedAt: null;
|
|
174
|
+
title: string | null;
|
|
175
|
+
question: string | null;
|
|
176
|
+
content: string | null;
|
|
177
|
+
explanation: string | null;
|
|
178
|
+
imagePath: string | null;
|
|
179
|
+
audioPath: string | null;
|
|
180
|
+
deleted: false;
|
|
181
|
+
unavailable: false;
|
|
182
|
+
recommendation: RecommendationState;
|
|
183
|
+
reward: RewardState;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
type Pagination = {
|
|
187
|
+
nextCursor: string | null;
|
|
188
|
+
hasMore: boolean;
|
|
189
|
+
exhausted: boolean;
|
|
190
|
+
snapshotMaxId: number;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
type Identity = {
|
|
194
|
+
key: "zero" | "ciel";
|
|
195
|
+
userId: number;
|
|
196
|
+
username: string;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
type DailyRun = {
|
|
200
|
+
id: number;
|
|
201
|
+
runKey: string;
|
|
202
|
+
operatorUserId: number;
|
|
203
|
+
publicActorUserId: number;
|
|
204
|
+
identityMode: "auto" | "zero" | "ciel";
|
|
205
|
+
commentMode: "off" | "draft" | "post";
|
|
206
|
+
sessionKind: "delegated-admin";
|
|
207
|
+
scopes: string[];
|
|
208
|
+
status: "active" | "completed" | "failed" | "expired";
|
|
209
|
+
successfulMutationCount: number;
|
|
210
|
+
startedAt: number;
|
|
211
|
+
expiresAt: number;
|
|
212
|
+
completedAt: number | null;
|
|
213
|
+
failedAt: number | null;
|
|
214
|
+
failureReason: string | null;
|
|
215
|
+
identity: Identity;
|
|
216
|
+
};
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Identity and daily-run commands
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
lumine admin identity list --json
|
|
223
|
+
lumine admin identity status --json
|
|
224
|
+
lumine admin identity use zero --json
|
|
225
|
+
lumine admin identity use ciel --json
|
|
226
|
+
lumine admin identity use auto --json
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Schemas:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
type IdentityList = Success<{
|
|
233
|
+
identities: Identity[];
|
|
234
|
+
preferredIdentity: "auto" | "zero" | "ciel";
|
|
235
|
+
lastCompletedIdentity: "zero" | "ciel" | null;
|
|
236
|
+
}>;
|
|
237
|
+
|
|
238
|
+
type IdentityStatus = Success<{
|
|
239
|
+
preferredIdentity: "auto" | "zero" | "ciel";
|
|
240
|
+
lastCompletedIdentity: "zero" | "ciel" | null;
|
|
241
|
+
activeRun: DailyRun | null;
|
|
242
|
+
}>;
|
|
243
|
+
|
|
244
|
+
type IdentityUse = IdentityStatus;
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`identity use` changes only the preference for a future start. It never changes
|
|
248
|
+
an active run or advances rotation.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
lumine admin daily-run start --identity auto --comment-mode off --json
|
|
252
|
+
lumine admin daily-run start --identity ciel --comment-mode draft \
|
|
253
|
+
--run-key daily:2026-08-06:review --json
|
|
254
|
+
lumine admin daily-run status --json
|
|
255
|
+
lumine admin daily-run complete --json
|
|
256
|
+
lumine admin daily-run fail --reason "operator stopped" --json
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Schemas:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
type DailyRunStart = Success<{ run: DailyRun }>;
|
|
263
|
+
type DailyRunStatus = Success<{
|
|
264
|
+
run: DailyRun | null;
|
|
265
|
+
lastRun: DailyRun | null;
|
|
266
|
+
}>;
|
|
267
|
+
type DailyRunComplete = Success<{
|
|
268
|
+
run: DailyRun;
|
|
269
|
+
rotationAdvanced: boolean;
|
|
270
|
+
}>;
|
|
271
|
+
type DailyRunFail = DailyRunComplete;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`lastRun` makes a lost-response retry of `complete` or `fail` possible after
|
|
275
|
+
the active pointer has been cleared. Other run-scoped commands accept only the
|
|
276
|
+
current unexpired `active` run. Completion first finalizes any mutation whose
|
|
277
|
+
content change committed but whose audit bookkeeping was still pending,
|
|
278
|
+
counting it toward the run's rotation signal. It then rejects only while a
|
|
279
|
+
mutation from the last ten minutes is genuinely in flight (the 409 lists the
|
|
280
|
+
pending mutations and a `retryAfterSeconds`); older in-flight rows are treated
|
|
281
|
+
as orphans of a dead process and no longer block completion. `fail` remains
|
|
282
|
+
available to abandon a run without advancing rotation, including a run whose
|
|
283
|
+
six-hour authorization has expired; an expired run can never be completed.
|
|
284
|
+
A TTL-expired run is reported with status `expired` even before the next
|
|
285
|
+
start reaps it, so `daily-run status` never shows an unusable run as
|
|
286
|
+
`active`.
|
|
287
|
+
|
|
288
|
+
Starting with a run key that belongs to a finished or expired run fails with
|
|
289
|
+
`CLI_ADMIN_RUN_KEY_ALREADY_USED`; supply a fresh `--run-key` (for example
|
|
290
|
+
`daily:2026-08-07:2`) to start again the same day. Reusing the key of the
|
|
291
|
+
live active run returns that run only when the requested `--comment-mode`
|
|
292
|
+
and any explicit `--identity` match it; otherwise the start fails with
|
|
293
|
+
`CLI_ADMIN_RUN_SETTINGS_MISMATCH` instead of silently returning a run with
|
|
294
|
+
different scopes. The same check applies when a start without the active
|
|
295
|
+
run's key would fall back to that active run.
|
|
296
|
+
|
|
297
|
+
The default run key is `daily:YYYY-MM-DD` in Asia/Bangkok. Supply `--run-key`
|
|
298
|
+
for a separate explicit run. `--idempotency-key` may be supplied to any
|
|
299
|
+
mutation when a caller needs the same retry identity across processes. The CLI
|
|
300
|
+
generates a fresh key for every mutation invocation; if a mutation fails, its
|
|
301
|
+
JSON error includes `details.retryIdempotencyKey` for a safe exact retry.
|
|
302
|
+
|
|
303
|
+
## Canonical lists and inspection
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
lumine admin recommendations list --kind recommend \
|
|
307
|
+
--content-types comment,dailyReflection --cursor '<cursor>' --json
|
|
308
|
+
lumine admin subjects candidates --after 2026-08-01T00:00:00Z \
|
|
309
|
+
--cursor '<cursor>' --json
|
|
310
|
+
lumine admin subjects candidates --effort unassigned --json
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Schemas:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
type RecommendationQueueList = Success<{
|
|
317
|
+
items: Array<{
|
|
318
|
+
queueId: string;
|
|
319
|
+
feedId: number;
|
|
320
|
+
contentType: "comment" | "aiStory" | "dailyReflection";
|
|
321
|
+
contentId: number;
|
|
322
|
+
url: string | null;
|
|
323
|
+
subjectUrl: string | null;
|
|
324
|
+
author: Author;
|
|
325
|
+
createdAt: number | null;
|
|
326
|
+
title?: string | null;
|
|
327
|
+
question?: string | null;
|
|
328
|
+
content: string | null;
|
|
329
|
+
explanation?: string | null;
|
|
330
|
+
attachment?: Attachment | null;
|
|
331
|
+
imagePath?: string | null;
|
|
332
|
+
audioPath?: string | null;
|
|
333
|
+
subject?: {
|
|
334
|
+
id: number;
|
|
335
|
+
title: string | null;
|
|
336
|
+
effortLevel: number;
|
|
337
|
+
url: string;
|
|
338
|
+
};
|
|
339
|
+
recommendation: RecommendationState;
|
|
340
|
+
reward: RewardState;
|
|
341
|
+
}>;
|
|
342
|
+
pagination: Pagination & {
|
|
343
|
+
scannedCount: number;
|
|
344
|
+
contentTypes?: Array<"comment" | "aiStory" | "dailyReflection">;
|
|
345
|
+
};
|
|
346
|
+
clientFilter?: {
|
|
347
|
+
contentTypes: Array<"comment" | "aiStory" | "dailyReflection">;
|
|
348
|
+
excludedItems: number;
|
|
349
|
+
};
|
|
350
|
+
}>;
|
|
351
|
+
|
|
352
|
+
type SubjectCandidates = Success<{
|
|
353
|
+
subjects: Subject[];
|
|
354
|
+
pagination: Pagination & { scannedCount: number };
|
|
355
|
+
}>;
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Both cursors freeze a primary-key high-water mark and traverse descending IDs,
|
|
359
|
+
so concurrent inserts cannot shift or duplicate later pages. Both walks scan a
|
|
360
|
+
bounded primary-key window (500 rows) per call before applying their residual
|
|
361
|
+
filters, so a page — recommendation or subject — can be empty while `hasMore`
|
|
362
|
+
remains true; continue until `exhausted`. Subject `--after` is inclusive, and
|
|
363
|
+
the opaque cursor is bound to its original date and effort filters.
|
|
364
|
+
|
|
365
|
+
`--content-types` is sent to APIs that support server-side filtering so excluded
|
|
366
|
+
types do not run their eligibility/content queries. The local CLI also filters
|
|
367
|
+
the returned page defensively for deployment compatibility. The server cursor
|
|
368
|
+
still advances across every underlying feed row, so excluding `aiStory` cannot
|
|
369
|
+
create gaps in later comment or Daily Reflection pages. New server cursors bind
|
|
370
|
+
the canonical content-type set; one legacy unbound cursor can be resumed and is
|
|
371
|
+
then reissued as bound. `clientFilter.excludedItems` makes any client-side
|
|
372
|
+
filtering explicit in JSON output.
|
|
373
|
+
|
|
374
|
+
For a run-scoped command, `--identity zero|ciel` is an assertion against the
|
|
375
|
+
server-selected run identity; it cannot switch actors locally. A mismatch
|
|
376
|
+
fails before the mutation. `--identity auto` accepts the run's canonical
|
|
377
|
+
selection.
|
|
378
|
+
|
|
379
|
+
### Query and index design
|
|
380
|
+
|
|
381
|
+
Subject and queue traversal are bounded primary-key walks; the subject walk
|
|
382
|
+
reads at most 500 `content_subjects` rows per cursor step, and the queue reads
|
|
383
|
+
at most 500 `noti_feeds` rows per cursor step before applying the existing Earn
|
|
384
|
+
Recommend eligibility predicates. The effort projection's
|
|
385
|
+
`UPDATE noti_feeds ... WHERE type = 'subject' AND contentId = ?` reuses the
|
|
386
|
+
website's canonical reward-level projection shape; deployment should verify
|
|
387
|
+
`noti_feeds` carries an index whose leading columns cover `(type, contentId)`
|
|
388
|
+
(or `(contentId, ...)`) as the canonical route already requires. The joins/`NOT EXISTS` checks are necessary
|
|
389
|
+
to preserve the normal recommendation and skip rules, but they run only for
|
|
390
|
+
IDs in that bounded window. Subject-comment traversal uses
|
|
391
|
+
`idx_comments_isDeleted_subject_id`; standalone-post comments use the existing
|
|
392
|
+
`idx_content_comments_root_deleted` index (whose InnoDB entries also carry the
|
|
393
|
+
primary ID). Recommendation identity uses
|
|
394
|
+
`uniq_content_recommendations_active_identity`; `earn_comment_candidates` is
|
|
395
|
+
driven by its primary key. No offset scan or new broad table scan was added.
|
|
396
|
+
|
|
397
|
+
Deployment can verify the required existing index definitions with:
|
|
398
|
+
|
|
399
|
+
```sql
|
|
400
|
+
SELECT TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX, COLUMN_NAME
|
|
401
|
+
FROM information_schema.STATISTICS
|
|
402
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
403
|
+
AND (
|
|
404
|
+
(TABLE_NAME = 'content_comments'
|
|
405
|
+
AND INDEX_NAME IN (
|
|
406
|
+
'idx_comments_isDeleted_subject_id',
|
|
407
|
+
'idx_content_comments_root_deleted'
|
|
408
|
+
))
|
|
409
|
+
OR (TABLE_NAME = 'content_recommendations'
|
|
410
|
+
AND INDEX_NAME = 'uniq_content_recommendations_active_identity')
|
|
411
|
+
OR (TABLE_NAME = 'earn_comment_candidates' AND INDEX_NAME = 'PRIMARY')
|
|
412
|
+
)
|
|
413
|
+
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX;
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
If a pre-migration environment lacks them, the repository's existing
|
|
417
|
+
`add-build-pinned-comments.sql`, active-recommendation-identity migration, and
|
|
418
|
+
Earn candidate migration are the canonical creation SQL; apply those migrations
|
|
419
|
+
instead of creating runtime checks or duplicate indexes.
|
|
420
|
+
|
|
421
|
+
For schema review, the exact missing-index DDL represented by those migrations
|
|
422
|
+
is:
|
|
423
|
+
|
|
424
|
+
```sql
|
|
425
|
+
ALTER TABLE content_comments
|
|
426
|
+
ADD INDEX idx_comments_isDeleted_subject_id (isDeleted, subjectId, id);
|
|
427
|
+
ALTER TABLE content_comments
|
|
428
|
+
ADD INDEX idx_content_comments_root_deleted (rootType, rootId, isDeleted);
|
|
429
|
+
ALTER TABLE content_recommendations
|
|
430
|
+
ADD UNIQUE INDEX uniq_content_recommendations_active_identity
|
|
431
|
+
(rootType, rootId, rootTargetType, activeUserId);
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Run only the repository migrations after confirming an index is absent; do not
|
|
435
|
+
execute these statements blindly on a deployed database.
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
lumine admin subject get 123 --include-comments --json
|
|
439
|
+
lumine admin subject comments 123 --cursor '<cursor>' --json
|
|
440
|
+
lumine admin comments get 456 --json
|
|
441
|
+
lumine admin post get https://www.twin-kle.com/ai-stories/88 --json
|
|
442
|
+
lumine admin post comments dailyReflection:99 --cursor '<cursor>' --json
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Schemas:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
type SubjectGet = Success<{
|
|
449
|
+
subject: Subject & {
|
|
450
|
+
secret: {
|
|
451
|
+
hasSecretAnswer: boolean;
|
|
452
|
+
hasSecretAttachment: boolean;
|
|
453
|
+
shown: boolean;
|
|
454
|
+
answer: string | null;
|
|
455
|
+
attachment: unknown | null;
|
|
456
|
+
};
|
|
457
|
+
// True only when comments were actually returned; a secret-gated subject
|
|
458
|
+
// reports false here (with secret.shown false) even when they were
|
|
459
|
+
// requested.
|
|
460
|
+
commentsIncluded: boolean;
|
|
461
|
+
// The inline list is capped at 200 comments in conversation order; when
|
|
462
|
+
// true, page through `subject comments` for the rest.
|
|
463
|
+
commentsTruncated: boolean;
|
|
464
|
+
comments: Comment[];
|
|
465
|
+
};
|
|
466
|
+
}>;
|
|
467
|
+
|
|
468
|
+
type SubjectComments = Success<{
|
|
469
|
+
subject: { id: number; url: string; title: string | null };
|
|
470
|
+
comments: Comment[];
|
|
471
|
+
pagination: Pagination;
|
|
472
|
+
}>;
|
|
473
|
+
|
|
474
|
+
type CommentGet = Success<{
|
|
475
|
+
comment: Comment;
|
|
476
|
+
subject: {
|
|
477
|
+
id: number;
|
|
478
|
+
url: string;
|
|
479
|
+
title: string | null;
|
|
480
|
+
secretShown: boolean;
|
|
481
|
+
} | null;
|
|
482
|
+
}>;
|
|
483
|
+
|
|
484
|
+
type StandalonePostGet = Success<{ post: StandalonePost }>;
|
|
485
|
+
|
|
486
|
+
type StandalonePostComments = Success<{
|
|
487
|
+
post: {
|
|
488
|
+
contentType: "aiStory" | "dailyReflection";
|
|
489
|
+
contentId: number;
|
|
490
|
+
url: string;
|
|
491
|
+
};
|
|
492
|
+
comments: Comment[];
|
|
493
|
+
pagination: Pagination;
|
|
494
|
+
}>;
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Inspection never silently bypasses secret semantics. Until the selected bot is
|
|
498
|
+
the author or has canonically responded/revealed, secret values and comments
|
|
499
|
+
remain unavailable.
|
|
500
|
+
|
|
501
|
+
## Subject and Featured mutations
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
lumine admin subject reveal 123 --json
|
|
505
|
+
lumine admin subject effort set 123 --level 2 --json
|
|
506
|
+
lumine admin subject creator set-made-by-poster 123 --json
|
|
507
|
+
lumine admin subject feature 123 --json
|
|
508
|
+
lumine admin subject unfeature 123 --json
|
|
509
|
+
lumine admin featured list --json
|
|
510
|
+
lumine admin featured reorder --subject-ids 30,20,10 --json
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Schemas:
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
type SubjectReveal = SubjectGet & {
|
|
517
|
+
status: "success" | "already_done";
|
|
518
|
+
changed: boolean;
|
|
519
|
+
data: SubjectGet["data"] & {
|
|
520
|
+
reveal: {
|
|
521
|
+
status: "created" | "already_revealed";
|
|
522
|
+
notificationCommentId: number | null;
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
type SubjectEffortSet = SubjectGet & {
|
|
528
|
+
status: "success" | "already_done";
|
|
529
|
+
changed: boolean;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
type SubjectCreatorSet = SubjectEffortSet;
|
|
533
|
+
|
|
534
|
+
type FeaturedList = Success<{
|
|
535
|
+
subjects: Subject[];
|
|
536
|
+
count: number;
|
|
537
|
+
maximum: 20;
|
|
538
|
+
}>;
|
|
539
|
+
|
|
540
|
+
type SubjectFeature = FeaturedList & {
|
|
541
|
+
status: "success" | "already_done";
|
|
542
|
+
changed: boolean;
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
type SubjectUnfeature = SubjectFeature;
|
|
546
|
+
type FeaturedReorder = SubjectFeature;
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
`reveal` publishes the existing hidden “viewed without responding” notification
|
|
550
|
+
as the selected bot, with the ordinary notification and socket side effects.
|
|
551
|
+
Effort assignment rejects an unrevealed secret subject. Creator attribution
|
|
552
|
+
requires an attachment. Both effort assignment and creator attribution enforce
|
|
553
|
+
the website's moderator-precedence rule: when a strictly higher-level
|
|
554
|
+
moderator recorded the current value, the mutation fails with
|
|
555
|
+
`CLI_ADMIN_MODERATOR_PRECEDENCE` (the bots compare at the shared canonical
|
|
556
|
+
effective level). Every response is reloaded from the writer.
|
|
557
|
+
|
|
558
|
+
Featured reorder is a complete-set replacement: it rejects duplicates,
|
|
559
|
+
unknown/deleted IDs, missing current members, non-subject rows, and more than
|
|
560
|
+
20 subjects. Permanent pins and editorial ordering policy are deliberately not
|
|
561
|
+
hardcoded.
|
|
562
|
+
|
|
563
|
+
## Recommendation, Karma approval, and Twinkle rewards
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
lumine admin post recommend 123 --json
|
|
567
|
+
lumine admin post recommend https://www.twin-kle.com/ai-stories/88 --json
|
|
568
|
+
lumine admin post recommend comment:456 --anyone-can-reward \
|
|
569
|
+
--reward-twinkles 3 --idempotency-key review-456-v1 --json
|
|
570
|
+
lumine admin post reward comment:456 --twinkles 3 --json
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Numeric targets default to `subject`. Use `subject:<id>`, `comment:<id>`,
|
|
574
|
+
`aiStory:<id>`, `dailyReflection:<id>`, a canonical URL, or the corresponding
|
|
575
|
+
`--type`.
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
type PriorRecommendationApproval = {
|
|
579
|
+
recommendationId: number;
|
|
580
|
+
recipientUserId: number;
|
|
581
|
+
karmaRecipientUserId: number;
|
|
582
|
+
karmaAwarded: number; // 10 for a new canonical approval; 0 on retry
|
|
583
|
+
karmaPoints: number; // writer-confirmed absolute balance after recomputation
|
|
584
|
+
status: "created" | "already_approved";
|
|
585
|
+
reward: Record<string, unknown>; // canonical users_rewards row
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
type Recommend = Success<
|
|
589
|
+
(SubjectGet["data"] | CommentGet["data"] | StandalonePostGet["data"]) & {
|
|
590
|
+
priorRecommendationApprovals: PriorRecommendationApproval[];
|
|
591
|
+
managementBotDeduplication?: {
|
|
592
|
+
alreadyProcessed: true;
|
|
593
|
+
recommendationId: number;
|
|
594
|
+
publicActorUserId: number;
|
|
595
|
+
reason: "already_recommended_by_approved_management_bot";
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
>;
|
|
599
|
+
|
|
600
|
+
type MaxRecommend = Success<
|
|
601
|
+
Recommend["data"] & {
|
|
602
|
+
pairing: {
|
|
603
|
+
recommendationId: number;
|
|
604
|
+
anyoneCanReward: true;
|
|
605
|
+
requestedTwinkles: 3;
|
|
606
|
+
rewardStatus:
|
|
607
|
+
| "created"
|
|
608
|
+
| "already_rewarded"
|
|
609
|
+
| "maximum_reached"
|
|
610
|
+
| "insufficient_coins"
|
|
611
|
+
| null;
|
|
612
|
+
alreadyRewarded: boolean;
|
|
613
|
+
rewardCaps: RewardState["caps"] | null;
|
|
614
|
+
retrySafe: true;
|
|
615
|
+
existingManagementReward?: {
|
|
616
|
+
rewardId: number;
|
|
617
|
+
publicActorUserId: number;
|
|
618
|
+
amount: number;
|
|
619
|
+
};
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
>;
|
|
623
|
+
|
|
624
|
+
type RewardThree = Success<
|
|
625
|
+
(SubjectGet["data"] | CommentGet["data"] | StandalonePostGet["data"]) & {
|
|
626
|
+
rewardOperation: {
|
|
627
|
+
status: "created" | "already_rewarded" | "maximum_reached" | null;
|
|
628
|
+
amount: number;
|
|
629
|
+
reward: Record<string, unknown> | null;
|
|
630
|
+
caps: RewardState["caps"] | null;
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
>;
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Zero/Ciel are normalized to effective Level 5 only inside the shared canonical
|
|
637
|
+
recommendation-approval decision. This narrow rule does not grant delegation,
|
|
638
|
+
management authority, or any other permission. A newly activated qualifying
|
|
639
|
+
recommendation
|
|
640
|
+
approves eligible earlier lower-level recommenders through the existing
|
|
641
|
+
`users_rewards` recommendation mechanism and multiplier. It excludes the
|
|
642
|
+
content author's self-recommendation, the approving bot, both management bots,
|
|
643
|
+
Level 5+ users, deleted rows, and existing ineligible rows. The approval then
|
|
644
|
+
runs the same absolute canonical Karma recomputation as `/user/karma`, using
|
|
645
|
+
writer-locked state. A new approval reports the canonical 10-point contribution;
|
|
646
|
+
a retry reports zero newly awarded and the same confirmed absolute balance.
|
|
647
|
+
|
|
648
|
+
Recommendation history is checked across both management bots, so rotation
|
|
649
|
+
does not recommend the same target again — unless the request asks for
|
|
650
|
+
`--anyone-can-reward` and the other bot's recommendation does not carry that
|
|
651
|
+
permission, in which case the actor proceeds with its own recommendation so
|
|
652
|
+
the requested permission and paired reward are honored rather than silently
|
|
653
|
+
dropped. When the other bot's recommendation does satisfy the request, the
|
|
654
|
+
`managementBotDeduplication` payload is returned and any requested 3-Twinkle
|
|
655
|
+
reward is still processed. A bare recommend never downgrades an existing
|
|
656
|
+
recommendation's anyone-can-reward permission; only an explicit
|
|
657
|
+
`--anyone-can-reward` changes it, and only in the granting direction.
|
|
658
|
+
Changing only `anyoneCanReward` does not rerun prior-recommender approval.
|
|
659
|
+
Approval reward rows are locked and writer-read, so concurrent or restored
|
|
660
|
+
attempts cannot insert the same approval twice.
|
|
661
|
+
|
|
662
|
+
Both the standalone and combined reward paths also inspect existing 3-Twinkle
|
|
663
|
+
management rewards across Zero and Ciel. A canonical three from either bot is
|
|
664
|
+
reported as already rewarded instead of adding another management reward.
|
|
665
|
+
|
|
666
|
+
The separate 3-Twinkle reward targets the worthwhile canonical post or comment.
|
|
667
|
+
It uses the selected bot and Twinkle's ordinary canonical Level and recipient
|
|
668
|
+
rules; Mikey is never charged while Zero/Ciel is displayed. Zero and Ciel are
|
|
669
|
+
exempt from recommendation and reward coin charges in the shared canonical
|
|
670
|
+
mutation helpers, so neither `insufficient_coins` nor a bot balance decrease
|
|
671
|
+
can occur for these actors; every human actor still pays under the existing
|
|
672
|
+
Level-based rules. The
|
|
673
|
+
reward transaction serializes the rewarder and cap-bearing content row, then
|
|
674
|
+
adds only the amount needed for that actor to total exactly three. Existing
|
|
675
|
+
three is `already_done`; a cap is `maximum_reached`.
|
|
676
|
+
If recommendation succeeds but reward fails, the command exits nonzero with
|
|
677
|
+
`partial_failure` and `retrySafe: true`.
|
|
678
|
+
|
|
679
|
+
## Skip decisions
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
lumine admin post skip dailyReflection:99 --json
|
|
683
|
+
lumine admin post skip comment:456 --reason "one-line answer, nothing to add" --json
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
A skip records that the management rotation has judged a recommend-queue item
|
|
687
|
+
and decided not to act, so neither bot's queue resurfaces it. It writes the
|
|
688
|
+
same canonical `users_earn_skip_status` row the website's Earn page writes
|
|
689
|
+
(`earnType 'karma'`, `action 'recommendation'`) under the acting bot, and the
|
|
690
|
+
queue eligibility predicates honor either bot's row. Human moderators' own
|
|
691
|
+
Earn queues are deliberately unaffected: the bots are not database supermods,
|
|
692
|
+
so a bot skip never hides content from a human, who may judge differently.
|
|
693
|
+
|
|
694
|
+
Targets are `comment`, `aiStory`, and `dailyReflection` only; subjects leave
|
|
695
|
+
their queue through effort assignment. Skipping an already-skipped item (by
|
|
696
|
+
either bot) returns `already_done` with `changed: false`. The optional
|
|
697
|
+
`--reason` (at most 500 characters) is stored in the private audit row's
|
|
698
|
+
metadata — it is the agent's memory of the judgment, not public content.
|
|
699
|
+
The skip requires the `recommendation:write` scope and is audited like every
|
|
700
|
+
other mutation.
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
type PostSkip = Success<{
|
|
704
|
+
skip: {
|
|
705
|
+
contentType: "comment" | "aiStory" | "dailyReflection";
|
|
706
|
+
contentId: number;
|
|
707
|
+
url: string;
|
|
708
|
+
skippedByUserId: number;
|
|
709
|
+
skippedAt: number | null;
|
|
710
|
+
};
|
|
711
|
+
}>;
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
## Audit history
|
|
715
|
+
|
|
716
|
+
```bash
|
|
717
|
+
lumine admin audit list --json
|
|
718
|
+
lumine admin audit list --run current --json
|
|
719
|
+
lumine admin audit list --run last --actions recommendation.skip --json
|
|
720
|
+
lumine admin audit list --target dailyReflection:99 --full --json
|
|
721
|
+
lumine admin audit list --cursor '<cursor>' --limit 50 --json
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
Lists the operator's own private audit events, newest first, so an agent can
|
|
725
|
+
see what earlier runs did. `--run` accepts `current`, `last`, or a run ID;
|
|
726
|
+
`--target` accepts `<targetType>:<id>`; `--actions` is a comma-separated
|
|
727
|
+
action list. Filters are bound into the cursor exactly like the other list
|
|
728
|
+
cursors. The walk is a bounded descending primary-key traversal over the
|
|
729
|
+
existing operator/run/target audit indexes.
|
|
730
|
+
|
|
731
|
+
Rows are compact by default (identifiers, action, target, result, response
|
|
732
|
+
`status`/`changed`, timestamps, and the request's idempotency key). `--full`
|
|
733
|
+
adds the stored `beforeState`, `afterState`, `responseJson`, and `metadata`
|
|
734
|
+
payloads — the same data the mutation already returned to this operator. The
|
|
735
|
+
private per-attempt fencing token is never returned. Reading audit history
|
|
736
|
+
requires only the `content:read` scope of an active run.
|
|
737
|
+
|
|
738
|
+
```ts
|
|
739
|
+
type AuditEvent = {
|
|
740
|
+
id: number;
|
|
741
|
+
runId: number | null;
|
|
742
|
+
publicActorUserId: number | null;
|
|
743
|
+
sessionKind: string;
|
|
744
|
+
action: string;
|
|
745
|
+
targetType: string | null;
|
|
746
|
+
targetId: number | null;
|
|
747
|
+
requestId: string;
|
|
748
|
+
result: string; // in_progress | completed | failed | partial_failure | bookkeeping_pending
|
|
749
|
+
status: string | null; // response status, e.g. success | already_done
|
|
750
|
+
changed: boolean | null;
|
|
751
|
+
createdAt: number;
|
|
752
|
+
completedAt: number | null;
|
|
753
|
+
// Present only with --full:
|
|
754
|
+
beforeState?: unknown;
|
|
755
|
+
afterState?: unknown;
|
|
756
|
+
responseJson?: unknown;
|
|
757
|
+
metadata?: unknown;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
type AuditList = Success<{
|
|
761
|
+
events: AuditEvent[];
|
|
762
|
+
pagination: Pagination;
|
|
763
|
+
}>;
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
## Persona-backed comments and replies
|
|
767
|
+
|
|
768
|
+
```bash
|
|
769
|
+
lumine admin daily-run start --identity auto --comment-mode draft --json
|
|
770
|
+
lumine admin comment draft 123 --identity auto --json
|
|
771
|
+
|
|
772
|
+
lumine admin daily-run start --identity ciel --comment-mode post \
|
|
773
|
+
--run-key daily:2026-08-06:comments --json
|
|
774
|
+
lumine admin comment draft 123 --identity ciel \
|
|
775
|
+
--idempotency-key comment-123-draft-v1 --json
|
|
776
|
+
lumine admin comment draft dailyReflection:99 --json
|
|
777
|
+
lumine admin comment reply comment:456 --json
|
|
778
|
+
lumine admin comment post --draft-id 77 \
|
|
779
|
+
--idempotency-key comment-123-post-v1 --json
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
A draft targets one of:
|
|
783
|
+
|
|
784
|
+
- `subject:<id>` (or a bare numeric ID) — a top-level comment on the subject;
|
|
785
|
+
- `aiStory:<id>` / `dailyReflection:<id>` — a top-level comment on the
|
|
786
|
+
standalone post;
|
|
787
|
+
- `comment:<id>` — a public reply to that specific comment. `comment reply`
|
|
788
|
+
is the same operation and requires a comment target.
|
|
789
|
+
|
|
790
|
+
A reply's container resolves canonically from the target comment: its subject,
|
|
791
|
+
or its AI Story / Daily Reflection root. Comments under any other root are
|
|
792
|
+
rejected with `CLI_ADMIN_UNSUPPORTED_REPLY_ROOT`. Replies to Zero/Ciel
|
|
793
|
+
comments and to notification comments are rejected with
|
|
794
|
+
`CLI_ADMIN_INVALID_REPLY_TARGET` — the bots never thread with themselves or
|
|
795
|
+
each other, and a human's later reply to a delegated comment still enters the
|
|
796
|
+
existing autonomous comment-assistant pipeline. Published replies carry the
|
|
797
|
+
ordinary thread linkage (thread root and reply-to), and notification fan-out
|
|
798
|
+
uses the normal canonical path.
|
|
799
|
+
|
|
800
|
+
```ts
|
|
801
|
+
type CommentDraft = Success<{
|
|
802
|
+
draft: {
|
|
803
|
+
id: number;
|
|
804
|
+
runId: number;
|
|
805
|
+
targetType: "subject" | "comment" | "aiStory" | "dailyReflection";
|
|
806
|
+
targetId: number;
|
|
807
|
+
targetUrl: string;
|
|
808
|
+
subjectId: number | null; // container subject; null for standalone posts
|
|
809
|
+
subjectUrl: string | null;
|
|
810
|
+
publicActorUserId: number;
|
|
811
|
+
commentMode: "draft" | "post";
|
|
812
|
+
personaRevision: string; // SHA-256; raw prompt is never returned
|
|
813
|
+
contextRevision: string; // SHA-256 of canonical container/comments/target
|
|
814
|
+
decision: "draft" | "skip";
|
|
815
|
+
reason: string | null;
|
|
816
|
+
content: string | null;
|
|
817
|
+
status: "ready" | "published";
|
|
818
|
+
createdAt: number;
|
|
819
|
+
expiresAt: number;
|
|
820
|
+
publishedCommentId: number | null;
|
|
821
|
+
};
|
|
822
|
+
}>;
|
|
823
|
+
|
|
824
|
+
type CommentPost = CommentGet & {
|
|
825
|
+
status: "success" | "already_done";
|
|
826
|
+
changed: boolean;
|
|
827
|
+
data: CommentGet["data"] & {
|
|
828
|
+
draft: {
|
|
829
|
+
id: number;
|
|
830
|
+
status: "published";
|
|
831
|
+
personaRevision: string;
|
|
832
|
+
contextRevision: string;
|
|
833
|
+
};
|
|
834
|
+
published: {
|
|
835
|
+
commentId: number;
|
|
836
|
+
targetType: "subject" | "comment" | "aiStory" | "dailyReflection";
|
|
837
|
+
targetId: number;
|
|
838
|
+
subjectId: number | null;
|
|
839
|
+
subjectUrl: string | null;
|
|
840
|
+
containerUrl: string;
|
|
841
|
+
commentUrl: string;
|
|
842
|
+
};
|
|
843
|
+
};
|
|
844
|
+
};
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
The server loads the canonical container (subject or standalone post) and its
|
|
848
|
+
complete visible comment context, then invokes the existing exact Zero/Ciel
|
|
849
|
+
system prompt through the shared response assembler with a mode-specific
|
|
850
|
+
decision policy (comment vs reply). The raw prompt is never returned or
|
|
851
|
+
audited. The model—not regexes or keyword rules—chooses `draft` or `skip`
|
|
852
|
+
under the run policy.
|
|
853
|
+
|
|
854
|
+
Draft IDs are bound to operator, run, public bot, target, comment mode,
|
|
855
|
+
context revision, persona revision, expiry, and idempotency key. The context
|
|
856
|
+
revision covers the container, every visible comment, and the target binding,
|
|
857
|
+
so a thread that changes between draft and publish rejects publication.
|
|
858
|
+
Posting locks the draft and context in the same transaction as the ordinary
|
|
859
|
+
comment insert. Changed context or persona rejects publication and requires
|
|
860
|
+
regeneration. Retries return the already-published comment instead of
|
|
861
|
+
duplicating it. Secret-subject gating applies whenever the container is a
|
|
862
|
+
subject, including replies inside it.
|
|
863
|
+
|
|
864
|
+
Draft idempotency keys are permanent per operator: supplying a key that an
|
|
865
|
+
earlier run (or another target) already used fails rather than resolving to
|
|
866
|
+
the old reservation — normally as the audit layer's
|
|
867
|
+
`CLI_ADMIN_AUDIT_IDENTITY_MISMATCH` (different run) or
|
|
868
|
+
`CLI_ADMIN_IDEMPOTENCY_KEY_MISMATCH` (different target), with
|
|
869
|
+
`CLI_ADMIN_DRAFT_KEY_REUSED` as the draft-table backstop. All three mean the
|
|
870
|
+
same thing: embed the run or date in any caller-supplied draft key and retry
|
|
871
|
+
with a fresh one. Note that the agent's
|
|
872
|
+
own `subject effort set` or `creator set-made-by-poster` between draft and
|
|
873
|
+
post changes the context revision — order those mutations before drafting.
|
|
874
|
+
|
|
875
|
+
## Audit, sockets, and deployment
|
|
876
|
+
|
|
877
|
+
Every delegated mutation reserves a private audit row before execution and
|
|
878
|
+
records run ID, real operator ID, public bot ID, session kind, action, target,
|
|
879
|
+
idempotency key, before/after state, result, comment mode, and persona revision
|
|
880
|
+
where applicable. It never stores authorization headers, bearer tokens,
|
|
881
|
+
cookies, passwords, or raw system prompts.
|
|
882
|
+
|
|
883
|
+
Retry acquisition and completion are row-locked and fenced by a private
|
|
884
|
+
per-attempt token, so an expired request cannot overwrite a newer retry. A new
|
|
885
|
+
recommendation commits atomically (the management bots are exempt from the
|
|
886
|
+
recommendation coin charge); the canonical prior-recommender approval and the
|
|
887
|
+
separate 3-Twinkle reward remain independently retryable.
|
|
888
|
+
|
|
889
|
+
Public content actions use ordinary Twinkle fan-out:
|
|
890
|
+
|
|
891
|
+
- comments and secret-view notifications use normal comment notifications;
|
|
892
|
+
- comments emit `new_upload` after commit;
|
|
893
|
+
- recommendations emit `new_recommendation`;
|
|
894
|
+
- recommendation-approval and 3-Twinkle rewards emit `new_reward`;
|
|
895
|
+
- effort/creator changes emit `edit_content`;
|
|
896
|
+
- Featured changes emit a canonical `home_outdated` refresh.
|
|
897
|
+
|
|
898
|
+
Apply `twinkle-api/scripts/migrations/add-lumine-admin-delegation.sql` and
|
|
899
|
+
then `add-lumine-admin-comment-targets.sql` before deploying the API. They add
|
|
900
|
+
only focused daily-run, rotation, draft, and audit tables/columns and indexes;
|
|
901
|
+
there are no runtime schema checks. The comment-targets migration backfills
|
|
902
|
+
existing subject drafts into the generalized target columns. The local CLI changes
|
|
903
|
+
are not available to users until a separately authorized npm publication.
|
|
904
|
+
|
|
905
|
+
Legacy aliases such as `subjects list`, `subjects get`, `subjects featured`,
|
|
906
|
+
`comments get`, and `recommend` remain accepted, but the singular command forms
|
|
907
|
+
shown above are the canonical interface.
|