@stage5/lumine 0.2.62 → 0.2.64
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 +2 -0
- package/lib/admin-workflows.js +8 -1
- package/lib/admin.js +154 -16
- package/lib/commands.js +13 -0
- package/lib/sponsor-duty.js +127 -12
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +177 -25
package/README.md
CHANGED
|
@@ -381,6 +381,8 @@ lumine admin featured list --json
|
|
|
381
381
|
lumine admin subject feature 123 --json
|
|
382
382
|
lumine admin subject unfeature 123 --json
|
|
383
383
|
lumine admin featured reorder --subject-ids 30,20,10 --json
|
|
384
|
+
lumine admin featured rotate --remove-subject-ids 30,20 \
|
|
385
|
+
--add-subject-ids 50,40 --json
|
|
384
386
|
lumine admin brief --days 3 --json
|
|
385
387
|
lumine admin notable add Minecrarft_guy --note "Created 8 thoughtful subjects and helped peers in 23 comments this window." --json
|
|
386
388
|
lumine admin post recommend comment:456 --anyone-can-reward --reward-twinkles 3 --json
|
package/lib/admin-workflows.js
CHANGED
|
@@ -542,6 +542,12 @@ function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
|
|
|
542
542
|
const last = lastPage || { ok: true, status: "success", data: {} };
|
|
543
543
|
const lastData = { ...(last.data || {}) };
|
|
544
544
|
delete lastData[operation.pagination.collectionKey];
|
|
545
|
+
const lastPagination = { ...(last.data?.pagination || {}) };
|
|
546
|
+
const pageScannedCount = normalizeScannedCount(
|
|
547
|
+
lastPagination.scannedCount,
|
|
548
|
+
0,
|
|
549
|
+
);
|
|
550
|
+
delete lastPagination.scannedCount;
|
|
545
551
|
if (state.filterSummariesComplete) {
|
|
546
552
|
if (state.clientFilter) lastData.clientFilter = state.clientFilter;
|
|
547
553
|
else delete lastData.clientFilter;
|
|
@@ -568,7 +574,8 @@ function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
|
|
|
568
574
|
data: {
|
|
569
575
|
...lastData,
|
|
570
576
|
pagination: {
|
|
571
|
-
...
|
|
577
|
+
...lastPagination,
|
|
578
|
+
pageScannedCount,
|
|
572
579
|
nextCursor: state.nextCursor,
|
|
573
580
|
hasMore: !state.exhausted,
|
|
574
581
|
exhausted: state.exhausted,
|
package/lib/admin.js
CHANGED
|
@@ -171,12 +171,32 @@ export async function adminCommand(options) {
|
|
|
171
171
|
scope: operation.mutates ? "build:write" : "build:read",
|
|
172
172
|
});
|
|
173
173
|
let runId = 0;
|
|
174
|
+
let correctionSessionId = 0;
|
|
175
|
+
let correctionSession = null;
|
|
174
176
|
if (adminOperationRequiresRun(operation)) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
if (operation.correctionEligible) {
|
|
178
|
+
const correctionStatus = await requestJson({
|
|
179
|
+
url: `${options.apiUrl}/cli/admin/corrections/status`,
|
|
180
|
+
authToken: auth.token,
|
|
181
|
+
timeoutMs: options.timeoutMs,
|
|
182
|
+
});
|
|
183
|
+
const correction = correctionStatus?.data?.correction || null;
|
|
184
|
+
if (
|
|
185
|
+
correction?.status === "active" &&
|
|
186
|
+
Number(correction.correctionCommentId || 0) ===
|
|
187
|
+
Number(operation.correctionCommentId || 0)
|
|
188
|
+
) {
|
|
189
|
+
correctionSessionId = Number(correction.id || 0);
|
|
190
|
+
correctionSession = correction;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const runStatus = correctionSessionId
|
|
194
|
+
? null
|
|
195
|
+
: await requestJson({
|
|
196
|
+
url: `${options.apiUrl}/cli/admin/daily-runs/status`,
|
|
197
|
+
authToken: auth.token,
|
|
198
|
+
timeoutMs: options.timeoutMs,
|
|
199
|
+
});
|
|
180
200
|
const activeRun = runStatus?.data?.run || null;
|
|
181
201
|
const retryableFinishedRun = [
|
|
182
202
|
"daily-run.complete",
|
|
@@ -186,15 +206,29 @@ export async function adminCommand(options) {
|
|
|
186
206
|
: null;
|
|
187
207
|
const selectedRun = activeRun || retryableFinishedRun;
|
|
188
208
|
runId = Number(selectedRun?.id || 0);
|
|
189
|
-
if (!runId
|
|
190
|
-
|
|
209
|
+
if (!runId && !correctionSessionId) {
|
|
210
|
+
if (operation.correctionEligible) {
|
|
211
|
+
const error = new Error(
|
|
212
|
+
`Start a correction session first: lumine admin correction start ${operation.correctionCommentId}.`,
|
|
213
|
+
);
|
|
214
|
+
error.code = "CLI_ADMIN_CORRECTION_NOT_ACTIVE";
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
throw noActiveRunError();
|
|
218
|
+
}
|
|
219
|
+
if (options.adminIdentity && (selectedRun || correctionSession)) {
|
|
191
220
|
const requestedIdentity = parseIdentity(options.adminIdentity);
|
|
221
|
+
const canonicalIdentity = correctionSession
|
|
222
|
+
? correctionSession.identity?.key
|
|
223
|
+
: selectedRun?.identity?.key;
|
|
192
224
|
if (
|
|
193
225
|
requestedIdentity !== "auto" &&
|
|
194
|
-
requestedIdentity !==
|
|
226
|
+
requestedIdentity !== canonicalIdentity
|
|
195
227
|
) {
|
|
196
228
|
throw cliValidationError(
|
|
197
|
-
|
|
229
|
+
correctionSession
|
|
230
|
+
? `--identity ${requestedIdentity} does not match correction session #${correctionSessionId} (${canonicalIdentity || "unknown"}).`
|
|
231
|
+
: `--identity ${requestedIdentity} does not match active run #${runId} (${canonicalIdentity || "unknown"}).`,
|
|
198
232
|
);
|
|
199
233
|
}
|
|
200
234
|
}
|
|
@@ -211,6 +245,13 @@ export async function adminCommand(options) {
|
|
|
211
245
|
body: operation.body,
|
|
212
246
|
headers: {
|
|
213
247
|
...(runId ? { "x-lumine-admin-run-id": String(runId) } : {}),
|
|
248
|
+
...(correctionSessionId
|
|
249
|
+
? {
|
|
250
|
+
"x-lumine-admin-correction-session-id": String(
|
|
251
|
+
correctionSessionId,
|
|
252
|
+
),
|
|
253
|
+
}
|
|
254
|
+
: {}),
|
|
214
255
|
...(requestId ? { "x-lumine-idempotency-key": requestId } : {}),
|
|
215
256
|
},
|
|
216
257
|
timeoutMs: options.timeoutMs,
|
|
@@ -663,6 +704,9 @@ function adminOperationRequiresRun(operation) {
|
|
|
663
704
|
"rescue.wordle.audit",
|
|
664
705
|
"daily-run.start",
|
|
665
706
|
"daily-run.status",
|
|
707
|
+
"correction.start",
|
|
708
|
+
"correction.status",
|
|
709
|
+
"correction.complete",
|
|
666
710
|
"escalation.list",
|
|
667
711
|
"escalation.set",
|
|
668
712
|
"notable.add",
|
|
@@ -694,6 +738,39 @@ export function parseAdminOperation(options) {
|
|
|
694
738
|
const [namespace = "", action = "", target = "", extra = ""] =
|
|
695
739
|
options.positional;
|
|
696
740
|
|
|
741
|
+
if (namespace === "correction" || namespace === "corrections") {
|
|
742
|
+
if (!action || action === "status") {
|
|
743
|
+
return readOperation("correction.status", "/cli/admin/corrections/status");
|
|
744
|
+
}
|
|
745
|
+
if (action === "start") {
|
|
746
|
+
const commentId = parseRequiredInteger(target, "comment ID", 1);
|
|
747
|
+
return writeOperation(
|
|
748
|
+
"correction.start",
|
|
749
|
+
"POST",
|
|
750
|
+
"/cli/admin/corrections",
|
|
751
|
+
{
|
|
752
|
+
commentId,
|
|
753
|
+
},
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
if (action === "complete" || action === "stop") {
|
|
757
|
+
const correctionId = parseRequiredInteger(
|
|
758
|
+
target,
|
|
759
|
+
"correction session ID",
|
|
760
|
+
1,
|
|
761
|
+
);
|
|
762
|
+
return writeOperation(
|
|
763
|
+
"correction.complete",
|
|
764
|
+
"POST",
|
|
765
|
+
`/cli/admin/corrections/${correctionId}/complete`,
|
|
766
|
+
{},
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
throw cliValidationError(
|
|
770
|
+
"Usage: lumine admin correction start <commentId> | correction status | correction complete <sessionId>.",
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
697
774
|
if (namespace === "identity") {
|
|
698
775
|
if (action === "list") {
|
|
699
776
|
return readOperation("identity.list", "/cli/admin/identities");
|
|
@@ -1256,11 +1333,17 @@ export function parseAdminOperation(options) {
|
|
|
1256
1333
|
if (action === "reorder") {
|
|
1257
1334
|
return featuredReorderOperation(options);
|
|
1258
1335
|
}
|
|
1336
|
+
if (action === "rotate") {
|
|
1337
|
+
return featuredRotateOperation(options);
|
|
1338
|
+
}
|
|
1259
1339
|
}
|
|
1260
1340
|
|
|
1261
1341
|
if (namespace === "comments" && action === "get") {
|
|
1262
1342
|
const commentId = parseRequiredInteger(target, "comment ID", 1);
|
|
1263
|
-
return readOperation("comments.get", `/cli/admin/comments/${commentId}
|
|
1343
|
+
return readOperation("comments.get", `/cli/admin/comments/${commentId}`, {
|
|
1344
|
+
correctionEligible: true,
|
|
1345
|
+
correctionCommentId: commentId,
|
|
1346
|
+
});
|
|
1264
1347
|
}
|
|
1265
1348
|
|
|
1266
1349
|
if (namespace === "post") {
|
|
@@ -1674,6 +1757,10 @@ export function parseAdminOperation(options) {
|
|
|
1674
1757
|
"PUT",
|
|
1675
1758
|
`/cli/admin/comments/${commentId}`,
|
|
1676
1759
|
{ content: readComposedTextFile(options.adminFile) },
|
|
1760
|
+
{
|
|
1761
|
+
correctionEligible: true,
|
|
1762
|
+
correctionCommentId: commentId,
|
|
1763
|
+
},
|
|
1677
1764
|
);
|
|
1678
1765
|
}
|
|
1679
1766
|
if (action === "post") {
|
|
@@ -1738,7 +1825,10 @@ function postGetOperation(target, options) {
|
|
|
1738
1825
|
return subjectGetOperation(parsedTarget.id, options);
|
|
1739
1826
|
}
|
|
1740
1827
|
if (parsedTarget.type === "comment") {
|
|
1741
|
-
return readOperation("post.get", `/cli/admin/comments/${parsedTarget.id}
|
|
1828
|
+
return readOperation("post.get", `/cli/admin/comments/${parsedTarget.id}`, {
|
|
1829
|
+
correctionEligible: true,
|
|
1830
|
+
correctionCommentId: parsedTarget.id,
|
|
1831
|
+
});
|
|
1742
1832
|
}
|
|
1743
1833
|
return readOperation(
|
|
1744
1834
|
"post.get",
|
|
@@ -1775,6 +1865,7 @@ function legacySubjectsOperation({ action, target, options }) {
|
|
|
1775
1865
|
);
|
|
1776
1866
|
}
|
|
1777
1867
|
if (action === "reorder") return featuredReorderOperation(options);
|
|
1868
|
+
if (action === "rotate") return featuredRotateOperation(options);
|
|
1778
1869
|
throw cliValidationError(
|
|
1779
1870
|
`Unknown subjects action: ${action || "(missing)"}.`,
|
|
1780
1871
|
);
|
|
@@ -1799,6 +1890,37 @@ function featuredReorderOperation(options) {
|
|
|
1799
1890
|
);
|
|
1800
1891
|
}
|
|
1801
1892
|
|
|
1893
|
+
function featuredRotateOperation(options) {
|
|
1894
|
+
const removeIds = parseFeaturedSubjectIds(
|
|
1895
|
+
options.adminRemoveIds,
|
|
1896
|
+
"--remove-subject-ids",
|
|
1897
|
+
);
|
|
1898
|
+
const addIds = parseFeaturedSubjectIds(
|
|
1899
|
+
options.adminAddIds,
|
|
1900
|
+
"--add-subject-ids",
|
|
1901
|
+
);
|
|
1902
|
+
if (removeIds.length !== addIds.length) {
|
|
1903
|
+
throw cliValidationError(
|
|
1904
|
+
"Featured rotation requires the same number of removal and addition IDs.",
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
const removeSet = new Set(removeIds);
|
|
1908
|
+
if (addIds.some((id) => removeSet.has(id))) {
|
|
1909
|
+
throw cliValidationError(
|
|
1910
|
+
"Featured rotation removal and addition IDs must not overlap.",
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
return writeOperation(
|
|
1914
|
+
"featured.rotate",
|
|
1915
|
+
"PUT",
|
|
1916
|
+
"/cli/admin/subjects/featured/rotation",
|
|
1917
|
+
{
|
|
1918
|
+
removeIds,
|
|
1919
|
+
addIds,
|
|
1920
|
+
},
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1802
1924
|
function recommendOperation(target, options) {
|
|
1803
1925
|
const recommendationTarget = parseRecommendationTarget({
|
|
1804
1926
|
target,
|
|
@@ -2066,8 +2188,8 @@ function bodyReadOperation(name, method, path, body) {
|
|
|
2066
2188
|
return { name, method, path, body, mutates: false };
|
|
2067
2189
|
}
|
|
2068
2190
|
|
|
2069
|
-
function writeOperation(name, method, path, body) {
|
|
2070
|
-
return { name, method, path, body, mutates: true };
|
|
2191
|
+
function writeOperation(name, method, path, body, extra = {}) {
|
|
2192
|
+
return { name, method, path, body, mutates: true, ...extra };
|
|
2071
2193
|
}
|
|
2072
2194
|
|
|
2073
2195
|
function withQuery(path, values) {
|
|
@@ -2172,18 +2294,24 @@ function parseTodoListStatus(value) {
|
|
|
2172
2294
|
}
|
|
2173
2295
|
|
|
2174
2296
|
function parseOrderedIds(value) {
|
|
2297
|
+
return parseFeaturedSubjectIds(value, "--subject-ids", true);
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
function parseFeaturedSubjectIds(value, flag, completeList = false) {
|
|
2175
2301
|
const ids = String(value || "")
|
|
2176
2302
|
.split(",")
|
|
2177
2303
|
.map((part) => part.trim())
|
|
2178
2304
|
.filter(Boolean)
|
|
2179
2305
|
.map((part) => parseRequiredInteger(part, "Featured subject ID", 1));
|
|
2180
|
-
if (!String(value || "").trim()) {
|
|
2306
|
+
if (!String(value || "").trim() || ids.length === 0) {
|
|
2181
2307
|
throw cliValidationError(
|
|
2182
|
-
|
|
2308
|
+
completeList
|
|
2309
|
+
? "Pass the complete ordered list with --subject-ids <id,id,...>."
|
|
2310
|
+
: `Pass at least one subject with ${flag} <id,id,...>.`,
|
|
2183
2311
|
);
|
|
2184
2312
|
}
|
|
2185
2313
|
if (new Set(ids).size !== ids.length) {
|
|
2186
|
-
throw cliValidationError(
|
|
2314
|
+
throw cliValidationError(`${flag} subject IDs must be unique.`);
|
|
2187
2315
|
}
|
|
2188
2316
|
return ids;
|
|
2189
2317
|
}
|
|
@@ -2810,6 +2938,11 @@ function printAdminResult({ operation, result }) {
|
|
|
2810
2938
|
console.log(
|
|
2811
2939
|
`Run #${data.run.id}: ${data.run.status}; identity ${data.run.identity.key}; comments ${data.run.commentMode}.`,
|
|
2812
2940
|
);
|
|
2941
|
+
if (data.scheduledDay && data.scheduledIdentity) {
|
|
2942
|
+
console.log(
|
|
2943
|
+
`Bangkok schedule for ${data.scheduledDay}: ${data.scheduledIdentity.key}.`,
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2813
2946
|
if (data.carryoverTodos) {
|
|
2814
2947
|
printTodoItems(data.carryoverTodos.items || [], "Carry-over work");
|
|
2815
2948
|
}
|
|
@@ -2824,6 +2957,11 @@ function printAdminResult({ operation, result }) {
|
|
|
2824
2957
|
}
|
|
2825
2958
|
if (Object.hasOwn(data, "activeRun")) {
|
|
2826
2959
|
console.log(`Preferred identity: ${data.preferredIdentity}.`);
|
|
2960
|
+
if (data.scheduledDay && data.scheduledIdentity) {
|
|
2961
|
+
console.log(
|
|
2962
|
+
`Bangkok schedule for ${data.scheduledDay}: ${data.scheduledIdentity.key}.`,
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2827
2965
|
console.log(
|
|
2828
2966
|
`Last completed identity: ${data.lastCompletedIdentity || "none"}.`,
|
|
2829
2967
|
);
|
package/lib/commands.js
CHANGED
|
@@ -2436,6 +2436,16 @@ export function parseArgs(args) {
|
|
|
2436
2436
|
: raw.ids
|
|
2437
2437
|
? String(raw.ids)
|
|
2438
2438
|
: "",
|
|
2439
|
+
adminRemoveIds: raw.removeSubjectIds
|
|
2440
|
+
? String(raw.removeSubjectIds)
|
|
2441
|
+
: raw.removeIds
|
|
2442
|
+
? String(raw.removeIds)
|
|
2443
|
+
: "",
|
|
2444
|
+
adminAddIds: raw.addSubjectIds
|
|
2445
|
+
? String(raw.addSubjectIds)
|
|
2446
|
+
: raw.addIds
|
|
2447
|
+
? String(raw.addIds)
|
|
2448
|
+
: "",
|
|
2439
2449
|
adminBucketId: raw.bucketId ? String(raw.bucketId) : "",
|
|
2440
2450
|
adminLabel: raw.label ? String(raw.label) : "",
|
|
2441
2451
|
adminUserIds: raw.userIds ? String(raw.userIds) : "",
|
|
@@ -2834,6 +2844,7 @@ export function printHelp() {
|
|
|
2834
2844
|
lumine admin subject feature|unfeature <subject-id> [--json]
|
|
2835
2845
|
lumine admin featured list [--unviewed|--viewed] [--json]
|
|
2836
2846
|
lumine admin featured reorder --subject-ids <id,id,...> [--json]
|
|
2847
|
+
lumine admin featured rotate --remove-subject-ids <id,id,...> --add-subject-ids <id,id,...> [--json]
|
|
2837
2848
|
lumine admin post get <target> [--type subject|comment|aiStory|dailyReflection] [--json]
|
|
2838
2849
|
lumine admin post comments <target> [--type subject|aiStory|dailyReflection] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
2839
2850
|
lumine admin post recommend <target> [--type subject|comment|aiStory|dailyReflection] [--anyone-can-reward] [--reward-twinkles 3] [--json]
|
|
@@ -2954,6 +2965,8 @@ Options:
|
|
|
2954
2965
|
--idempotency-key <k> Stable retry key for one admin mutation
|
|
2955
2966
|
--level <1|2|3> Admin subject effort level
|
|
2956
2967
|
--subject-ids <ids> Complete ordered Featured subject IDs
|
|
2968
|
+
--remove-subject-ids <ids> Featured subjects approved for rotation removal
|
|
2969
|
+
--add-subject-ids <ids> Ordered replacement subjects for Featured rotation
|
|
2957
2970
|
--bucket-id <id> Unbanned AI identity bucket for account consolidation
|
|
2958
2971
|
--label <name> Name for a new unbanned AI identity bucket
|
|
2959
2972
|
--user-ids <ids> Explicit user IDs for an AI bucket batch (up to 500)
|
package/lib/sponsor-duty.js
CHANGED
|
@@ -478,8 +478,12 @@ async function changeDutyState(options, action) {
|
|
|
478
478
|
);
|
|
479
479
|
if (duties.length === 0) {
|
|
480
480
|
if (action === "stop") {
|
|
481
|
-
const
|
|
482
|
-
|
|
481
|
+
const localRecord = await readSponsorStateForStop(options);
|
|
482
|
+
const localState = localRecord.state;
|
|
483
|
+
if (localState) assertSponsorStateAccount(localState, auth);
|
|
484
|
+
const preservesAnotherAccountState =
|
|
485
|
+
sponsorStateBelongsToAnotherAccount(localRecord.invalidState, auth);
|
|
486
|
+
if (!localState && !localRecord.invalidStatePath) {
|
|
483
487
|
printJsonOrLines(
|
|
484
488
|
options,
|
|
485
489
|
{ changed: false, duty: null },
|
|
@@ -487,30 +491,52 @@ async function changeDutyState(options, action) {
|
|
|
487
491
|
);
|
|
488
492
|
return;
|
|
489
493
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
+
const preservedWorkspaces = localState
|
|
495
|
+
? dutyWorkspacePaths(localState)
|
|
496
|
+
: [];
|
|
497
|
+
const invalidArchive =
|
|
498
|
+
localRecord.invalidStatePath && !preservesAnotherAccountState
|
|
499
|
+
? await tryArchiveInvalidSponsorStateForStop(options)
|
|
500
|
+
: null;
|
|
501
|
+
const localArchive = invalidArchive
|
|
502
|
+
? invalidArchive.localArchive
|
|
503
|
+
: preservedWorkspaces.length > 0
|
|
494
504
|
? await archiveSponsorState(options, localState)
|
|
495
505
|
: null;
|
|
496
|
-
if (!localArchive) await removeSponsorState(options);
|
|
506
|
+
if (!localArchive && localState) await removeSponsorState(options);
|
|
497
507
|
printJsonOrLines(
|
|
498
508
|
options,
|
|
499
509
|
{
|
|
500
510
|
changed: false,
|
|
501
511
|
duty: null,
|
|
502
512
|
...(localArchive ? { localArchive, preservedWorkspaces } : {}),
|
|
513
|
+
...(invalidArchive?.localCleanupWarning
|
|
514
|
+
? { localCleanupWarning: invalidArchive.localCleanupWarning }
|
|
515
|
+
: preservesAnotherAccountState
|
|
516
|
+
? {
|
|
517
|
+
localCleanupWarning:
|
|
518
|
+
"Preserved an outdated local duty record belonging to another sponsor account.",
|
|
519
|
+
}
|
|
520
|
+
: {}),
|
|
503
521
|
},
|
|
504
522
|
[
|
|
505
523
|
"No active sponsor duty session.",
|
|
506
524
|
...(localArchive
|
|
507
525
|
? [
|
|
508
|
-
|
|
526
|
+
localRecord.invalidStatePath
|
|
527
|
+
? `Preserved the unreadable or outdated local duty record at ${localArchive}.`
|
|
528
|
+
: `Preserved the expired job record at ${localArchive}.`,
|
|
509
529
|
...preservedWorkspaces.map(
|
|
510
530
|
(workspace) => `Preserved workspace: ${workspace}`,
|
|
511
531
|
),
|
|
512
532
|
]
|
|
513
|
-
:
|
|
533
|
+
: invalidArchive?.localCleanupWarning
|
|
534
|
+
? [invalidArchive.localCleanupWarning]
|
|
535
|
+
: preservesAnotherAccountState
|
|
536
|
+
? [
|
|
537
|
+
"Preserved an outdated local duty record belonging to another sponsor account.",
|
|
538
|
+
]
|
|
539
|
+
: ["Removed the stale local duty record."]),
|
|
514
540
|
],
|
|
515
541
|
);
|
|
516
542
|
return;
|
|
@@ -523,8 +549,18 @@ async function changeDutyState(options, action) {
|
|
|
523
549
|
);
|
|
524
550
|
}
|
|
525
551
|
const dutyId = Number(duties[0].id || 0);
|
|
526
|
-
|
|
552
|
+
const localRecord =
|
|
553
|
+
action === "stop"
|
|
554
|
+
? await readSponsorStateForStop(options)
|
|
555
|
+
: {
|
|
556
|
+
state: await readSponsorState(options, { required: false }),
|
|
557
|
+
invalidStatePath: null,
|
|
558
|
+
invalidState: null,
|
|
559
|
+
};
|
|
560
|
+
let localState = localRecord.state;
|
|
527
561
|
if (localState) assertSponsorStateAccount(localState, auth);
|
|
562
|
+
const preservesAnotherAccountState =
|
|
563
|
+
sponsorStateBelongsToAnotherAccount(localRecord.invalidState, auth);
|
|
528
564
|
if (action === "resume") {
|
|
529
565
|
localState = await loadOwnedState({ options, auth, state: localState });
|
|
530
566
|
if (Number(localState.duty.id) !== dutyId) {
|
|
@@ -550,8 +586,16 @@ async function changeDutyState(options, action) {
|
|
|
550
586
|
},
|
|
551
587
|
});
|
|
552
588
|
let localArchive = null;
|
|
589
|
+
let localCleanupWarning = null;
|
|
553
590
|
let preservedWorkspaces = [];
|
|
554
|
-
if (
|
|
591
|
+
if (
|
|
592
|
+
nextState === "stopped" &&
|
|
593
|
+
localRecord.invalidStatePath &&
|
|
594
|
+
!preservesAnotherAccountState
|
|
595
|
+
) {
|
|
596
|
+
({ localArchive, localCleanupWarning } =
|
|
597
|
+
await tryArchiveInvalidSponsorStateForStop(options));
|
|
598
|
+
} else if (localState && Number(localState.duty?.id) === dutyId) {
|
|
555
599
|
if (nextState === "stopped") {
|
|
556
600
|
preservedWorkspaces = dutyWorkspacePaths(localState);
|
|
557
601
|
if (preservedWorkspaces.length > 0) {
|
|
@@ -568,6 +612,10 @@ async function changeDutyState(options, action) {
|
|
|
568
612
|
await writeSponsorState(options, localState);
|
|
569
613
|
}
|
|
570
614
|
}
|
|
615
|
+
if (nextState === "stopped" && preservesAnotherAccountState) {
|
|
616
|
+
localCleanupWarning =
|
|
617
|
+
"Preserved an outdated local duty record belonging to another sponsor account.";
|
|
618
|
+
}
|
|
571
619
|
printJsonOrLines(
|
|
572
620
|
options,
|
|
573
621
|
{
|
|
@@ -575,17 +623,21 @@ async function changeDutyState(options, action) {
|
|
|
575
623
|
...(localArchive
|
|
576
624
|
? { localArchive, preservedWorkspaces }
|
|
577
625
|
: {}),
|
|
626
|
+
...(localCleanupWarning ? { localCleanupWarning } : {}),
|
|
578
627
|
},
|
|
579
628
|
[
|
|
580
629
|
`Sponsor duty is now ${nextState}.`,
|
|
581
630
|
...(localArchive
|
|
582
631
|
? [
|
|
583
|
-
|
|
632
|
+
localRecord.invalidStatePath
|
|
633
|
+
? `Preserved the unreadable or outdated local duty record at ${localArchive}.`
|
|
634
|
+
: `Preserved the expired job record at ${localArchive}.`,
|
|
584
635
|
...preservedWorkspaces.map(
|
|
585
636
|
(workspace) => `Preserved workspace: ${workspace}`,
|
|
586
637
|
),
|
|
587
638
|
]
|
|
588
639
|
: []),
|
|
640
|
+
...(localCleanupWarning ? [localCleanupWarning] : []),
|
|
589
641
|
],
|
|
590
642
|
);
|
|
591
643
|
}
|
|
@@ -1511,6 +1563,14 @@ function assertSponsorStateAccount(state, auth) {
|
|
|
1511
1563
|
}
|
|
1512
1564
|
}
|
|
1513
1565
|
|
|
1566
|
+
function sponsorStateBelongsToAnotherAccount(state, auth) {
|
|
1567
|
+
return Boolean(
|
|
1568
|
+
auth.userId &&
|
|
1569
|
+
state?.sponsorUserId &&
|
|
1570
|
+
Number(auth.userId) !== Number(state.sponsorUserId),
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1514
1574
|
async function readSponsorState(options, { required = true } = {}) {
|
|
1515
1575
|
const filePath = sponsorDutyStatePath(options);
|
|
1516
1576
|
try {
|
|
@@ -1536,6 +1596,31 @@ async function readSponsorState(options, { required = true } = {}) {
|
|
|
1536
1596
|
}
|
|
1537
1597
|
}
|
|
1538
1598
|
|
|
1599
|
+
async function readSponsorStateForStop(options) {
|
|
1600
|
+
const filePath = sponsorDutyStatePath(options);
|
|
1601
|
+
try {
|
|
1602
|
+
const rawState = await fs.readFile(filePath, "utf8");
|
|
1603
|
+
try {
|
|
1604
|
+
const state = JSON.parse(rawState);
|
|
1605
|
+
if (
|
|
1606
|
+
Number(state?.version) === STATE_VERSION &&
|
|
1607
|
+
state?.duty?.id &&
|
|
1608
|
+
state?.duty?.leaseToken
|
|
1609
|
+
) {
|
|
1610
|
+
return { state, invalidStatePath: null, invalidState: null };
|
|
1611
|
+
}
|
|
1612
|
+
return { state: null, invalidStatePath: filePath, invalidState: state };
|
|
1613
|
+
} catch {
|
|
1614
|
+
return { state: null, invalidStatePath: filePath, invalidState: null };
|
|
1615
|
+
}
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
if (error.code === "ENOENT") {
|
|
1618
|
+
return { state: null, invalidStatePath: null, invalidState: null };
|
|
1619
|
+
}
|
|
1620
|
+
return { state: null, invalidStatePath: filePath, invalidState: null };
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1539
1624
|
async function writeSponsorState(options, state) {
|
|
1540
1625
|
const filePath = sponsorDutyStatePath(options);
|
|
1541
1626
|
const directory = path.dirname(filePath);
|
|
@@ -1566,6 +1651,36 @@ async function removeSponsorState(options) {
|
|
|
1566
1651
|
});
|
|
1567
1652
|
}
|
|
1568
1653
|
|
|
1654
|
+
async function archiveInvalidSponsorState(options) {
|
|
1655
|
+
const filePath = sponsorDutyStatePath(options);
|
|
1656
|
+
const archivePath = `${filePath}.invalid-${Date.now()}`;
|
|
1657
|
+
try {
|
|
1658
|
+
await fs.rename(filePath, archivePath);
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
if (error.code === "ENOENT") return null;
|
|
1661
|
+
throw error;
|
|
1662
|
+
}
|
|
1663
|
+
await fs.chmod(archivePath, 0o600);
|
|
1664
|
+
return archivePath;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
async function tryArchiveInvalidSponsorStateForStop(options) {
|
|
1668
|
+
try {
|
|
1669
|
+
return {
|
|
1670
|
+
localArchive: await archiveInvalidSponsorState(options),
|
|
1671
|
+
localCleanupWarning: null,
|
|
1672
|
+
};
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
const statePath = sponsorDutyStatePath(options);
|
|
1675
|
+
const message =
|
|
1676
|
+
error instanceof Error ? error.message : "unknown local filesystem error";
|
|
1677
|
+
return {
|
|
1678
|
+
localArchive: null,
|
|
1679
|
+
localCleanupWarning: `The canonical duty stopped, but the unreadable local record remains at ${statePath}: ${message}`,
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1569
1684
|
async function withSponsorStateLock(options, operation) {
|
|
1570
1685
|
const lockPath = `${sponsorDutyStatePath(options)}.lock`;
|
|
1571
1686
|
const lockDirectory = path.dirname(lockPath);
|
package/package.json
CHANGED
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -39,11 +39,19 @@ canonical structured data.
|
|
|
39
39
|
described below. The human's normal AI Energy and sponsor path applies;
|
|
40
40
|
Lumine does not need to remain running.
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
The Zero/Ciel shift is assigned by **Asia/Bangkok calendar day, not by run**.
|
|
43
|
+
The schedule is anchored with 2026-08-31 assigned to Zero and alternates by
|
|
44
|
+
elapsed Bangkok dates from there. Every automatic primary, supplemental,
|
|
45
|
+
retry, read-only, or resumed run started on one date uses that date's same
|
|
46
|
+
actor; a skipped date still counts in the alternation. Completing, failing,
|
|
47
|
+
abandoning, or expiring a run never changes the schedule. An explicit
|
|
48
|
+
`--identity zero` or `--identity ciel` is an auditable override for that run
|
|
49
|
+
only: it does not rewrite the scheduled identity. Automatic runs expire at the
|
|
50
|
+
next Bangkok midnight so yesterday's actor cannot continue as today's
|
|
51
|
+
automatic identity. Reusing a live run key still returns the original run and
|
|
52
|
+
identity. `identity use zero|ciel` is the separate, explicit persistent
|
|
53
|
+
override for future starts; it remains in force until `identity use auto`, and
|
|
54
|
+
does not alter the underlying calendar schedule reported by status commands.
|
|
47
55
|
|
|
48
56
|
Comment mode is stored only on the current run:
|
|
49
57
|
|
|
@@ -245,7 +253,15 @@ posts that most need Zero or Ciel are the ones nobody else answered.
|
|
|
245
253
|
mention is what notifies him (`postComment` runs `processMentions` /
|
|
246
254
|
`postMentions` and emits `new_targeted_upload`), so a bug-report comment
|
|
247
255
|
without `@mikey` fails its main job. Restate what the child observed; never
|
|
248
|
-
promise a fix or a timeline.
|
|
256
|
+
promise a fix or a timeline. This reporting duty does not suspend the acting
|
|
257
|
+
bot's character: the public comment must still sound like Zero or Ciel
|
|
258
|
+
talking naturally to that member, not like an operator, ticket, or incident
|
|
259
|
+
report. Read the full thread first. If the bot already noticed, explained, or
|
|
260
|
+
apologized for the bug, do not post another reply that treats it as a fresh
|
|
261
|
+
discovery or repeats the same acknowledgement. Continue from what the bot
|
|
262
|
+
already said and add only what is missing, such as naturally bringing
|
|
263
|
+
`@mikey` into the conversation. Put the formal defect summary, evidence, and
|
|
264
|
+
review request in the private run escalation.
|
|
249
265
|
- **Guide users through the website, without waiting to be asked.** A post can
|
|
250
266
|
show that a child is stuck, confused, or unaware a feature exists without ever
|
|
251
267
|
containing a question — someone begging for coins who does not know about
|
|
@@ -262,15 +278,45 @@ posts that most need Zero or Ciel are the ones nobody else answered.
|
|
|
262
278
|
question should normally receive Level 3. Level 3 is the highest delegated
|
|
263
279
|
setting, and the level tells respondents that substantial, carefully
|
|
264
280
|
reasoned answers are wanted.
|
|
281
|
+
- **Ongoing series are participation, not noise.** Daily records, journals,
|
|
282
|
+
logs, recurring updates, and serialized creative work are not spam, filler,
|
|
283
|
+
engagement farming, or duplicates merely because they reuse a title or
|
|
284
|
+
format, or because one installment is concise. Review the series context and
|
|
285
|
+
what the current entry contributes. Never lower effort, skip, withhold a
|
|
286
|
+
recommendation or comment, or propose Featured removal solely because of
|
|
287
|
+
that recurring format or brevity; only an actual duplicate or genuine noise
|
|
288
|
+
is treated as such.
|
|
265
289
|
- **Sincere requests for personal help are Featured material.** Posts asking
|
|
266
290
|
the community for advice about school, friendships, loneliness, or other
|
|
267
291
|
ordinary real-life problems embody Twinkle's purpose; being personal is never
|
|
268
|
-
a reason to suppress or remove them.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
292
|
+
a reason to suppress or remove them. Receiving meaningful support does not
|
|
293
|
+
make one stale or create a duty to rotate it out; keep judging what the live
|
|
294
|
+
board contributes now, and never use prior recognition as a removal proxy.
|
|
295
|
+
- **Speculative privacy is never an editorial signal.** Do not lower, remove,
|
|
296
|
+
unfeature, or escalate content because it might identify someone, mentions a
|
|
297
|
+
school, city, class, or ordinary location, or invites everyday community
|
|
298
|
+
context. Those possibilities carry zero weight in Featured decisions. An
|
|
299
|
+
actual sensitive disclosure or an author's explicit removal request follows
|
|
300
|
+
the separate concrete-safety path; it does not make the post low-quality.
|
|
301
|
+
- **Featured values participation, continuity, and child voice—not adult
|
|
302
|
+
polish.** Accessible questions that many children can answer and ongoing
|
|
303
|
+
personal series such as records, journals, or recurring updates are strong
|
|
304
|
+
Featured forms. Age, brevity, simplicity, prior recognition, or a modest
|
|
305
|
+
description is not a removal reason. Review the whole subject and its role in
|
|
306
|
+
the community instead of grading its opening text like an essay.
|
|
307
|
+
- **A "new" Featured addition has two non-negotiable eligibility gates.** When
|
|
308
|
+
Mikey asks for new Featured subjects, additions, or replacements, a candidate
|
|
309
|
+
must both (1) have been posted recently and (2) have never appeared on the
|
|
310
|
+
Featured board before. Unless Mikey gives a different recency window,
|
|
311
|
+
"recently" means the preceding seven calendar days; do not widen into older
|
|
312
|
+
inventory merely to fill capacity or produce a longer proposal list. Absence
|
|
313
|
+
from the current `featured list` proves only that a Subject is not Featured
|
|
314
|
+
now — it does not prove that the Subject has never been Featured. Verify
|
|
315
|
+
lifetime Featured history from canonical evidence before recommending or
|
|
316
|
+
adding it. If the available CLI/API cannot prove that history, leave the
|
|
317
|
+
candidate out and report the capability gap instead of guessing. Quality is
|
|
318
|
+
still required after both eligibility gates pass; recent and never-Featured
|
|
319
|
+
does not make filler acceptable.
|
|
274
320
|
- **The live Featured board is Mikey's word.** Do not remove or reorder a
|
|
275
321
|
currently Featured subject without first showing Mikey the planned removals
|
|
276
322
|
and replacements and getting his go-ahead. Additions have standing approval
|
|
@@ -282,7 +328,8 @@ posts that most need Zero or Ciel are the ones nobody else answered.
|
|
|
282
328
|
deliberately — never re-feature it to "restore" the board, even when space
|
|
283
329
|
is available, and never treat any subject as a permanent fixture from memory
|
|
284
330
|
or old run notes. Derive the board fresh from `featured list` at the start of
|
|
285
|
-
every
|
|
331
|
+
every Featured review or mutation; the only pins that exist are the ones
|
|
332
|
+
currently on it.
|
|
286
333
|
|
|
287
334
|
Sensitive disclosures, active disputes, and anything needing crisis or medical
|
|
288
335
|
judgment remain out of scope for a bot comment no matter how neglected the post
|
|
@@ -296,11 +343,13 @@ happen. **Every run ends with an escalation list**, and it belongs in the run's
|
|
|
296
343
|
final report whether or not anyone asks for it.
|
|
297
344
|
|
|
298
345
|
Keep that list narrow enough to be useful. Escalate concrete child-safety,
|
|
299
|
-
exploitation,
|
|
346
|
+
exploitation, targeted harassment, or platform/system-abuse risk — not
|
|
300
347
|
ordinary children experimenting, arguing, making rumors, proposing informal
|
|
301
348
|
in-site loans or contests, asking where media can be found, or making an
|
|
302
349
|
unverified ownership claim. Those may merit a normal age-appropriate response,
|
|
303
350
|
but they are not escalations without credible harmful conduct or a real victim.
|
|
351
|
+
Hypothetical identifiability and ordinary school, city, class, or location
|
|
352
|
+
references are not escalation signals.
|
|
304
353
|
|
|
305
354
|
Escalate, with the canonical `https://www.twin-kle.com/subjects/<id>` or
|
|
306
355
|
`/comments/<id>` URL, a one-line summary, and why it needs him:
|
|
@@ -565,6 +614,8 @@ type DailyRun = {
|
|
|
565
614
|
failedAt: number | null;
|
|
566
615
|
failureReason: string | null;
|
|
567
616
|
identity: Identity;
|
|
617
|
+
scheduledDay: string; // YYYY-MM-DD in Asia/Bangkok
|
|
618
|
+
scheduledIdentity: Identity;
|
|
568
619
|
};
|
|
569
620
|
```
|
|
570
621
|
|
|
@@ -592,6 +643,9 @@ type IdentityList = Success<{
|
|
|
592
643
|
type IdentityStatus = Success<{
|
|
593
644
|
preferredIdentity: "auto" | "zero" | "ciel";
|
|
594
645
|
lastCompletedIdentity: "zero" | "ciel" | null;
|
|
646
|
+
scheduledDay: string;
|
|
647
|
+
scheduledIdentity: Identity;
|
|
648
|
+
scheduleTimeZone: "Asia/Bangkok";
|
|
595
649
|
activeRun: DailyRun | null;
|
|
596
650
|
}>;
|
|
597
651
|
|
|
@@ -643,8 +697,10 @@ type IdentityCandidate = {
|
|
|
643
697
|
};
|
|
644
698
|
```
|
|
645
699
|
|
|
646
|
-
`identity use` changes
|
|
647
|
-
|
|
700
|
+
`identity use zero|ciel` changes the explicit persistent override for future
|
|
701
|
+
starts; `identity use auto` returns future starts to the Bangkok calendar
|
|
702
|
+
schedule. It never changes an active run, alters the reported scheduled
|
|
703
|
+
identity, or advances rotation.
|
|
648
704
|
|
|
649
705
|
```bash
|
|
650
706
|
lumine admin daily-run start --identity auto --comment-mode off --json
|
|
@@ -668,6 +724,9 @@ Schemas:
|
|
|
668
724
|
```ts
|
|
669
725
|
type DailyRunStart = Success<{
|
|
670
726
|
run: DailyRun;
|
|
727
|
+
scheduledDay: string;
|
|
728
|
+
scheduledIdentity: Identity;
|
|
729
|
+
scheduleTimeZone: "Asia/Bangkok";
|
|
671
730
|
carryoverTodos: CarryoverTodos;
|
|
672
731
|
}>;
|
|
673
732
|
type DailyRunStatus = Success<{
|
|
@@ -676,7 +735,7 @@ type DailyRunStatus = Success<{
|
|
|
676
735
|
}>;
|
|
677
736
|
type DailyRunComplete = Success<{
|
|
678
737
|
run: DailyRun;
|
|
679
|
-
rotationAdvanced:
|
|
738
|
+
rotationAdvanced: false; // legacy field; calendar schedules never advance by run
|
|
680
739
|
}>;
|
|
681
740
|
type DailyRunFail = DailyRunComplete;
|
|
682
741
|
```
|
|
@@ -730,9 +789,12 @@ section.** Base it on a fresh `featured list`. When capacity exists, make and
|
|
|
730
789
|
report strong additions during the run under the standing approval above; do
|
|
731
790
|
not defer them as proposals. Then name each current Subject proposed for
|
|
732
791
|
removal with its canonical URL and a concrete editorial reason, followed by
|
|
733
|
-
any replacement that would require that removal.
|
|
734
|
-
|
|
735
|
-
|
|
792
|
+
any replacement that would require that removal. Every proposed or completed
|
|
793
|
+
addition described as new must include its posting date and canonical evidence
|
|
794
|
+
that it has never been Featured; omit it when either gate is unverified. Never
|
|
795
|
+
omit the section; when no removal or reorder is honestly warranted, say `None`
|
|
796
|
+
and explain why. Removing or reordering pins remains a proposal until Mikey
|
|
797
|
+
gives his go-ahead.
|
|
736
798
|
|
|
737
799
|
Creating an escalation belongs to the active run; acknowledging, annotating,
|
|
738
800
|
resolving, or reopening it does not. Use the run-independent `escalation`
|
|
@@ -1108,6 +1170,7 @@ type CommentGet = Success<{
|
|
|
1108
1170
|
id: number;
|
|
1109
1171
|
url: string;
|
|
1110
1172
|
title: string | null;
|
|
1173
|
+
author: Author;
|
|
1111
1174
|
secretShown: boolean;
|
|
1112
1175
|
} | null;
|
|
1113
1176
|
}>;
|
|
@@ -1139,6 +1202,8 @@ lumine admin subject feature 123 --json
|
|
|
1139
1202
|
lumine admin subject unfeature 123 --json
|
|
1140
1203
|
lumine admin featured list --json
|
|
1141
1204
|
lumine admin featured reorder --subject-ids 30,20,10 --json
|
|
1205
|
+
lumine admin featured rotate --remove-subject-ids 30,20 \
|
|
1206
|
+
--add-subject-ids 50,40 --json
|
|
1142
1207
|
```
|
|
1143
1208
|
|
|
1144
1209
|
Schemas:
|
|
@@ -1175,6 +1240,17 @@ type SubjectFeature = FeaturedList & {
|
|
|
1175
1240
|
|
|
1176
1241
|
type SubjectUnfeature = SubjectFeature;
|
|
1177
1242
|
type FeaturedReorder = SubjectFeature;
|
|
1243
|
+
type FeaturedRotate = FeaturedList & {
|
|
1244
|
+
status: "success" | "already_done";
|
|
1245
|
+
changed: boolean;
|
|
1246
|
+
data: FeaturedList["data"] & {
|
|
1247
|
+
rotation: {
|
|
1248
|
+
removeSubjectIds: number[];
|
|
1249
|
+
addSubjectIds: number[];
|
|
1250
|
+
finalSubjectIds: number[];
|
|
1251
|
+
};
|
|
1252
|
+
};
|
|
1253
|
+
};
|
|
1178
1254
|
```
|
|
1179
1255
|
|
|
1180
1256
|
`reveal` publishes the existing hidden “viewed without responding” notification
|
|
@@ -1191,6 +1267,29 @@ unknown/deleted IDs, missing current members, non-subject rows, and more than
|
|
|
1191
1267
|
20 subjects. Permanent pins and editorial ordering policy are deliberately not
|
|
1192
1268
|
hardcoded.
|
|
1193
1269
|
|
|
1270
|
+
Featured rotate is the direct, atomic replacement verb for an approved
|
|
1271
|
+
rotation. `--remove-subject-ids` names the exact current members Mikey approved
|
|
1272
|
+
for removal; `--add-subject-ids` names the same number of replacements in
|
|
1273
|
+
descending editorial relevance. The API locks the canonical board, requires
|
|
1274
|
+
every removal to still be present and every addition to still be absent,
|
|
1275
|
+
validates every subject, then commits one complete-set replacement. Additions
|
|
1276
|
+
become the front of the board in the supplied order and all surviving members
|
|
1277
|
+
keep their relative order. A stale or partly mismatched plan fails without
|
|
1278
|
+
changing anything; an exact retry returns the canonical completed response.
|
|
1279
|
+
The response includes the canonical final board and the confirmed rotation
|
|
1280
|
+
IDs, so callers never synthesize Featured state locally.
|
|
1281
|
+
|
|
1282
|
+
The two lists must have the same nonzero length, so rotation never changes the
|
|
1283
|
+
board's count. That also lets an approved swap proceed when the website has
|
|
1284
|
+
left a pre-existing board above the delegated maximum without growing it;
|
|
1285
|
+
repair the inherited overflow separately with an approved `subject unfeature`.
|
|
1286
|
+
|
|
1287
|
+
This mutation deliberately does not choose candidates or override the
|
|
1288
|
+
editorial gates above. Before invoking it, the management agent must have
|
|
1289
|
+
freshly reviewed the board, shown Mikey the removals and replacements, received
|
|
1290
|
+
his go-ahead, and verified that every proposed new addition is recent and has
|
|
1291
|
+
never previously been Featured from canonical evidence.
|
|
1292
|
+
|
|
1194
1293
|
## Recommendation, Karma approval, and Twinkle rewards
|
|
1195
1294
|
|
|
1196
1295
|
```bash
|
|
@@ -2484,8 +2583,29 @@ type InsightsBrief = Success<{
|
|
|
2484
2583
|
}>;
|
|
2485
2584
|
```
|
|
2486
2585
|
|
|
2586
|
+
### Narrow comment-correction sessions
|
|
2587
|
+
|
|
2588
|
+
Correcting one existing bot comment does not require opening a daily run:
|
|
2589
|
+
|
|
2590
|
+
```bash
|
|
2591
|
+
lumine admin correction start <commentId> --json
|
|
2592
|
+
lumine admin comments get <commentId> --json
|
|
2593
|
+
lumine admin comment edit <commentId> --file corrected.md --json
|
|
2594
|
+
lumine admin correction complete <sessionId> --json
|
|
2595
|
+
```
|
|
2596
|
+
|
|
2597
|
+
The server derives Zero or Ciel from the comment's actual author; the caller
|
|
2598
|
+
cannot choose or override that identity. The authorization lasts ten minutes,
|
|
2599
|
+
is bound to that exact comment, permits only reading that target and editing
|
|
2600
|
+
it, and never runs daily duties, changes the Bangkok calendar assignment, or
|
|
2601
|
+
contributes to a daily-run mutation count. A correction cannot target a human
|
|
2602
|
+
comment, notification record, deleted comment, or Build thread. Build comments
|
|
2603
|
+
still require a fresh version-bound correction reply after genuinely reviewing
|
|
2604
|
+
the published app. Starting a newer correction supersedes an older active one.
|
|
2605
|
+
Finish it explicitly after the canonical edit is confirmed.
|
|
2606
|
+
|
|
2487
2607
|
**Editing the bot's own comments.** `comment edit <commentId> --file
|
|
2488
|
-
<comment.md>` replaces the text of a comment the
|
|
2608
|
+
<comment.md>` replaces the text of a comment the acting bot itself authored —
|
|
2489
2609
|
for correcting a factual error, an unfulfillable claim, or outdated guidance
|
|
2490
2610
|
in Zero/Ciel's own words. It is deliberately not a moderation verb: comments
|
|
2491
2611
|
by the other bot, by any human, and hidden notification records are all
|
|
@@ -2495,12 +2615,14 @@ composed-comment rules (plain UTF-8, 10,000-character limit, truth about what
|
|
|
2495
2615
|
the session actually did) and publishes through the website's canonical
|
|
2496
2616
|
comment-edit pipeline — mentions are reprocessed (a newly added `@mikey`
|
|
2497
2617
|
notifies him), and Earn-candidate projections resync. Submitting identical
|
|
2498
|
-
text returns `already_done`.
|
|
2499
|
-
comment-mode `post` run, and is
|
|
2500
|
-
content in `beforeState` and
|
|
2618
|
+
text returns `already_done`. It requires either the exact active correction
|
|
2619
|
+
session above or the `comment:post` scope of a comment-mode `post` run, and is
|
|
2620
|
+
audited as `comment.edit` with the previous content in `beforeState` and
|
|
2621
|
+
`data.edit.previousContent`. Edit sparingly:
|
|
2501
2622
|
kids may have already read the original, so a comment that changed meaning
|
|
2502
2623
|
(not just wording) usually deserves a follow-up reply instead of a silent
|
|
2503
|
-
rewrite.
|
|
2624
|
+
rewrite. Inside a normal daily run it still requires that run's `comment:post`
|
|
2625
|
+
scope; for a one-comment repair, use the narrower correction session above.
|
|
2504
2626
|
|
|
2505
2627
|
## Direct bot chat messages
|
|
2506
2628
|
|
|
@@ -2657,6 +2779,33 @@ from memory:
|
|
|
2657
2779
|
decisions are yours to make and record with `post skip` or in the run
|
|
2658
2780
|
report).
|
|
2659
2781
|
|
|
2782
|
+
Persona and conversational state are continuous across the whole thread,
|
|
2783
|
+
including acknowledgements, corrections, bug reports, `@mikey` notifications,
|
|
2784
|
+
and other reporting duties. The acting bot must remember what it already said:
|
|
2785
|
+
do not reintroduce a conclusion, apology, explanation, question, or offer as if
|
|
2786
|
+
the earlier bot comment did not exist. A follow-up should advance the existing
|
|
2787
|
+
conversation and make its relationship to the prior reply natural and clear.
|
|
2788
|
+
Accuracy and escalation requirements never authorize a sudden formal admin
|
|
2789
|
+
voice. Before posting, read the full thread, identify the acting bot's existing
|
|
2790
|
+
claims and commitments, and preserve the canonical character plus the
|
|
2791
|
+
established conversational register where they agree: phrasing,
|
|
2792
|
+
capitalization, warmth, humor, and emoji restraint should feel like the same
|
|
2793
|
+
Zero or Ciel continuing the conversation. Do not mechanically copy typos,
|
|
2794
|
+
mistakes, unsafe behavior, or a voice that conflicts with the canonical
|
|
2795
|
+
persona. Keep the two surfaces distinct: the public comment is an in-character
|
|
2796
|
+
continuation for the member, while `daily-run escalation add` and the final
|
|
2797
|
+
management report carry the concise operator-facing diagnosis and evidence. A
|
|
2798
|
+
comment that is factually correct but ignores the bot's earlier reply, repeats
|
|
2799
|
+
it as new, or reads like a support ticket or management audit fails this
|
|
2800
|
+
requirement.
|
|
2801
|
+
|
|
2802
|
+
Keep participant ownership exact while continuing that thread. The
|
|
2803
|
+
`subject.author` (or root post's `author`) owns the topic, subject, post, or
|
|
2804
|
+
project. A selected comment's `author` owns only that comment. Addressing a
|
|
2805
|
+
commenter does not make the root content theirs: never call the root topic,
|
|
2806
|
+
subject, post, or project “yours” unless that participant is also its canonical
|
|
2807
|
+
author. Read both author fields before composing a reply.
|
|
2808
|
+
|
|
2660
2809
|
**Lumine Build apps and app posts are composed-only, and only after actually looking
|
|
2661
2810
|
(Mikey's direction, 2026-08-10).** When a post is about a Build app — the
|
|
2662
2811
|
author shares their app, announces an update, or asks for feedback on their
|
|
@@ -2891,6 +3040,9 @@ and indexes;
|
|
|
2891
3040
|
there are no runtime schema checks. The comment-targets migration backfills
|
|
2892
3041
|
existing subject drafts into the generalized target columns. The local CLI changes
|
|
2893
3042
|
are not available to users until a separately authorized npm publication.
|
|
3043
|
+
Deploy and verify the API's `/cli/admin/subjects/featured/rotation` route before
|
|
3044
|
+
publishing a CLI release that exposes `featured rotate`; an older API rejects
|
|
3045
|
+
the command without changing Featured state.
|
|
2894
3046
|
|
|
2895
3047
|
Legacy aliases such as `subjects list`, `subjects get`, `subjects featured`,
|
|
2896
3048
|
`comments get`, and `recommend` remain accepted, but the singular command forms
|