@stage5/lumine 0.3.32 → 0.3.34
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-craft.js +235 -0
- package/lib/admin.js +12 -0
- package/lib/builderStance.generated.js +3 -0
- package/lib/commands.js +9 -0
- package/lib/constants.js +5 -0
- package/package.json +3 -2
- package/sdk/BUILD_SDK_INDEX.md +35 -4
- package/sdk/LUMINE_ADMIN.md +110 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { readAdminJsonFile } from "./admin-news.js";
|
|
2
|
+
|
|
3
|
+
// `lumine admin craft seed|stats|revise`: our agents curate a Twinkle.craft
|
|
4
|
+
// universe (Infinity Craft), Math Lab style. seed writes pairs we wrote (no
|
|
5
|
+
// AI call), stats shows sizes, the frontier, today's target and the newest
|
|
6
|
+
// AI answers to audit, revise corrects an item's teaching text. No daily run
|
|
7
|
+
// needed. seed and revise are dry runs unless --confirm is given.
|
|
8
|
+
|
|
9
|
+
export const CRAFT_USAGE =
|
|
10
|
+
"Usage: lumine admin craft seed --build <id> --file <pairs.json> [--confirm] | stats --build <id> [--limit <n>] | revise --build <id> --item <id|name> [--why <text>] [--emoji <emoji>] [--category <field>] [--confirm] | retire --build <id> --item <id|name> --reason <text> [--confirm] | unfizzle --build <id> [--since <iso>] [--pair <a+b>] [--confirm] [--json].";
|
|
11
|
+
const CRAFT_SEED_MAX = 500;
|
|
12
|
+
|
|
13
|
+
function validationError(message) {
|
|
14
|
+
const error = new Error(message);
|
|
15
|
+
error.code = "CLI_ADMIN_CLI_VALIDATION";
|
|
16
|
+
return error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildIdOf(options) {
|
|
20
|
+
const id = Number(String(options.adminBuild || "").trim());
|
|
21
|
+
if (!Number.isSafeInteger(id) || id <= 0) {
|
|
22
|
+
throw validationError(`Pass --build <id>. ${CRAFT_USAGE}`);
|
|
23
|
+
}
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A text flag: absent is undefined; present must carry a non-empty value
|
|
28
|
+
// (`--why` alone, or `--why=`, is refused instead of sending "true" or "").
|
|
29
|
+
function textFlag(value, flag) {
|
|
30
|
+
if (value === undefined || value === null) return undefined;
|
|
31
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
32
|
+
throw validationError(`${flag} needs a value.`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function confirmed(options) {
|
|
38
|
+
const confirm = options.adminConfirm;
|
|
39
|
+
if (confirm !== "" && confirm !== "true") {
|
|
40
|
+
throw validationError("Pass --confirm on its own, without a value.");
|
|
41
|
+
}
|
|
42
|
+
return confirm === "true";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildCraftOperation({ action, options }) {
|
|
46
|
+
const buildId = buildIdOf(options);
|
|
47
|
+
const base = `/cli/admin/builds/${buildId}/craft`;
|
|
48
|
+
if (action === "stats") {
|
|
49
|
+
const limit =
|
|
50
|
+
options.adminLimit == null ? null : Number(String(options.adminLimit).trim());
|
|
51
|
+
if (limit !== null && (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)) {
|
|
52
|
+
throw validationError("--limit must be an integer 1-100.");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
name: "craft.stats",
|
|
56
|
+
method: "GET",
|
|
57
|
+
path: `${base}/stats${limit ? `?recent=${limit}` : ""}`,
|
|
58
|
+
body: undefined,
|
|
59
|
+
mutates: false,
|
|
60
|
+
requiresRun: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (action === "seed") {
|
|
64
|
+
const raw = readAdminJsonFile(options.adminFile, "--file <pairs.json>", {
|
|
65
|
+
maxBytes: 2 * 1024 * 1024,
|
|
66
|
+
});
|
|
67
|
+
const pairs = Array.isArray(raw) ? raw : Array.isArray(raw?.pairs) ? raw.pairs : null;
|
|
68
|
+
if (!pairs || !pairs.length || pairs.length > CRAFT_SEED_MAX) {
|
|
69
|
+
throw validationError(
|
|
70
|
+
`The file must hold 1 to ${CRAFT_SEED_MAX} pairs: [{ a, b, result: { name, emoji, category, why } }] or { pairs: [...] }.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const confirm = confirmed(options);
|
|
74
|
+
return {
|
|
75
|
+
name: confirm ? "craft.seed" : "craft.seed.preview",
|
|
76
|
+
method: "POST",
|
|
77
|
+
path: `${base}/seed`,
|
|
78
|
+
body: { pairs, dryRun: !confirm },
|
|
79
|
+
mutates: confirm,
|
|
80
|
+
requiresRun: false,
|
|
81
|
+
timeoutMs: 300000,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (action === "revise") {
|
|
85
|
+
const item = textFlag(options.adminCraftItem, "--item");
|
|
86
|
+
if (!item) throw validationError(`Pass --item <id|name>. ${CRAFT_USAGE}`);
|
|
87
|
+
const body = { item: item.trim() };
|
|
88
|
+
const why = textFlag(options.adminCraftWhy, "--why");
|
|
89
|
+
const emoji = textFlag(options.adminCraftEmoji, "--emoji");
|
|
90
|
+
const category = textFlag(options.adminCraftCategory, "--category");
|
|
91
|
+
if (why !== undefined) body.why = why;
|
|
92
|
+
if (emoji !== undefined) body.emoji = emoji;
|
|
93
|
+
if (category !== undefined) body.category = category;
|
|
94
|
+
if (Object.keys(body).length === 1) {
|
|
95
|
+
throw validationError("Pass at least one of --why, --emoji or --category (the name never changes).");
|
|
96
|
+
}
|
|
97
|
+
const confirm = confirmed(options);
|
|
98
|
+
return {
|
|
99
|
+
name: confirm ? "craft.revise" : "craft.revise.preview",
|
|
100
|
+
method: "POST",
|
|
101
|
+
path: `${base}/revise`,
|
|
102
|
+
body: { ...body, dryRun: !confirm },
|
|
103
|
+
mutates: confirm,
|
|
104
|
+
requiresRun: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (action === "retire") {
|
|
108
|
+
const item = textFlag(options.adminCraftItem, "--item");
|
|
109
|
+
const reason = String(options.adminReason || "").trim();
|
|
110
|
+
if (!item || reason.length < 3 || reason.length > 300) {
|
|
111
|
+
throw validationError(`Pass --item <id|name> and --reason <why, 3-300 characters>. ${CRAFT_USAGE}`);
|
|
112
|
+
}
|
|
113
|
+
const confirm = confirmed(options);
|
|
114
|
+
return {
|
|
115
|
+
name: confirm ? "craft.retire" : "craft.retire.preview",
|
|
116
|
+
method: "POST",
|
|
117
|
+
path: `${base}/retire`,
|
|
118
|
+
body: { item: item.trim(), reason, dryRun: !confirm },
|
|
119
|
+
mutates: confirm,
|
|
120
|
+
requiresRun: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (action === "unfizzle") {
|
|
124
|
+
const since = String(options.adminSince || "").trim();
|
|
125
|
+
if (since && !Number.isFinite(Date.parse(since))) {
|
|
126
|
+
throw validationError("--since must be an ISO date or time, e.g. 2026-10-13T00:00:00Z.");
|
|
127
|
+
}
|
|
128
|
+
const pair = textFlag(options.adminCraftPair, "--pair");
|
|
129
|
+
if (pair !== undefined && pair.split("+").filter((name) => name.trim()).length !== 2) {
|
|
130
|
+
throw validationError("--pair must be two item names joined by +, e.g. Water+Fire.");
|
|
131
|
+
}
|
|
132
|
+
const confirm = confirmed(options);
|
|
133
|
+
return {
|
|
134
|
+
name: confirm ? "craft.unfizzle" : "craft.unfizzle.preview",
|
|
135
|
+
method: "POST",
|
|
136
|
+
path: `${base}/unfizzle`,
|
|
137
|
+
body: {
|
|
138
|
+
...(since ? { since } : {}),
|
|
139
|
+
...(pair !== undefined ? { pair } : {}),
|
|
140
|
+
dryRun: !confirm,
|
|
141
|
+
},
|
|
142
|
+
mutates: confirm,
|
|
143
|
+
requiresRun: false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
throw validationError(CRAFT_USAGE);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatCraftResult({ operation, data }) {
|
|
150
|
+
const lines = [];
|
|
151
|
+
const name = operation.name;
|
|
152
|
+
if (name.startsWith("craft.seed")) {
|
|
153
|
+
lines.push(
|
|
154
|
+
`${data.dryRun ? "DRY RUN: would add" : "Added"} ${data.added} pair(s), ${data.newItems} new item(s); ${data.skipped.length} skipped, ${data.rejected.length} rejected.`,
|
|
155
|
+
);
|
|
156
|
+
if (data.wordLevels) {
|
|
157
|
+
lines.push(`Word levels: ${data.wordLevels.set} set, ${data.wordLevels.pending} left for the lazy lookup.`);
|
|
158
|
+
}
|
|
159
|
+
for (const entry of data.skipped) lines.push(` skipped ${entry.pair}: ${entry.reason}`);
|
|
160
|
+
for (const entry of data.rejected) lines.push(` rejected ${entry.pair}: ${entry.reason}`);
|
|
161
|
+
for (const entry of data.warnings || []) lines.push(` warning ${entry.pair}: ${entry.reason}`);
|
|
162
|
+
if (data.dryRun) lines.push("Nothing was written. Add --confirm to seed.");
|
|
163
|
+
return lines;
|
|
164
|
+
}
|
|
165
|
+
if (name.startsWith("craft.revise")) {
|
|
166
|
+
const show = (value) => `${value.emoji} ${value.category} · ${value.why ?? "(no explanation)"}`;
|
|
167
|
+
lines.push(`${data.dryRun ? "DRY RUN: " : ""}${data.item.name} (#${data.item.id})`);
|
|
168
|
+
lines.push(` before: ${show(data.before)}`);
|
|
169
|
+
lines.push(` after: ${show(data.after)}`);
|
|
170
|
+
if (data.dryRun) lines.push("Nothing was written. Add --confirm to revise.");
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
173
|
+
if (name.startsWith("craft.retire")) {
|
|
174
|
+
lines.push(
|
|
175
|
+
`${data.dryRun ? "DRY RUN: would retire" : "Retired"} ${data.item.name} (#${data.item.id}): ${data.pairsFizzled} pair(s) fizzled, hidden from ${data.collectionsHidden} collection(s)${data.dryRun ? "" : `, ${data.storySubjectsRemoved} story line(s) removed`}.`,
|
|
176
|
+
);
|
|
177
|
+
if (data.worldFirst) {
|
|
178
|
+
lines.push(
|
|
179
|
+
` World First #${data.worldFirst.userId}: ${data.worldFirst.rewardPaid ? "already paid (stays paid)" : "unpaid reward voided"}.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
lines.push(` Reason: ${data.reason}`);
|
|
183
|
+
if (data.dryRun) lines.push("Nothing was written. Add --confirm to retire.");
|
|
184
|
+
return lines;
|
|
185
|
+
}
|
|
186
|
+
if (name.startsWith("craft.unfizzle")) {
|
|
187
|
+
lines.push(
|
|
188
|
+
`${data.dryRun ? "DRY RUN: would reset" : "Reset"} ${data.reset} of ${data.scanned} fizzled pair(s) whose safety check errored.${data.more ? " More remain: run again." : ""}`,
|
|
189
|
+
);
|
|
190
|
+
for (const entry of data.pairs) lines.push(` #${entry.pairId} ${entry.pair}`);
|
|
191
|
+
if (data.dryRun) lines.push("Nothing was written. Add --confirm to reset.");
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
if (name === "craft.stats") {
|
|
195
|
+
const items = data.items || {};
|
|
196
|
+
lines.push(
|
|
197
|
+
`Build #${data.buildId}: ${items.total} items (${items.starters} starters, ${items.seededUnclaimed} seeded not yet found by a player, ${items.worldFirsts} World Firsts, ${items.missingWordLevel} without a word level, ${items.retired || 0} retired).`,
|
|
198
|
+
);
|
|
199
|
+
const pairs = Object.entries(data.pairs || {})
|
|
200
|
+
.map(([source, statuses]) =>
|
|
201
|
+
`${source}: ${Object.entries(statuses).map(([status, n]) => `${n} ${status}`).join(", ")}`,
|
|
202
|
+
)
|
|
203
|
+
.join(" · ");
|
|
204
|
+
lines.push(`Pairs: ${pairs || "none"}.`);
|
|
205
|
+
lines.push(
|
|
206
|
+
`Frontier: ${data.frontier.unusedItems} item(s) never used as an input; ${data.frontier.untriedPairs} untried pair(s).`,
|
|
207
|
+
);
|
|
208
|
+
lines.push(
|
|
209
|
+
data.today?.target
|
|
210
|
+
? `Today's Star (${data.today.day}): ${data.today.target} (#${data.today.targetId}), par ${data.today.par}.`
|
|
211
|
+
: `Today's Star (${data.today?.day}): none yet (the universe is too young).`,
|
|
212
|
+
);
|
|
213
|
+
if ((data.keptFlagged || []).length) {
|
|
214
|
+
lines.push("Kept flagged names (a word matched the censored list; JEV kept them; check each):");
|
|
215
|
+
for (const entry of data.keptFlagged) {
|
|
216
|
+
const at = entry.keptAt ? new Date(entry.keptAt * 1000).toISOString().slice(0, 16).replace("T", " ") : "?";
|
|
217
|
+
lines.push(
|
|
218
|
+
` #${entry.pairId} ${entry.item.emoji} ${entry.item.name} (#${entry.item.id}): "${(entry.words || []).join('", "')}" · JEV ${entry.fine ?? "?"} · ${at} UTC`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if ((data.recentAi || []).length) {
|
|
223
|
+
lines.push("Newest AI answers (audit these):");
|
|
224
|
+
for (const pair of data.recentAi) {
|
|
225
|
+
lines.push(
|
|
226
|
+
pair.result
|
|
227
|
+
? ` #${pair.pairId} ${pair.a} + ${pair.b} = ${pair.result.emoji} ${pair.result.name} (#${pair.result.id}, ${pair.result.category}, ${pair.result.wordLevel || "level pending"}): ${pair.why}`
|
|
228
|
+
: ` #${pair.pairId} ${pair.a} + ${pair.b} = fizzle`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return lines;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
package/lib/admin.js
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
printOwnerTrace,
|
|
45
45
|
} from "./admin-owner-trace.js";
|
|
46
46
|
import { buildRolesOperation, printRoles } from "./admin-roles.js";
|
|
47
|
+
import { buildCraftOperation, formatCraftResult } from "./admin-craft.js";
|
|
47
48
|
import { formatChatReportResult } from "./admin-chat-reports.js";
|
|
48
49
|
import { formatIdentityDeviceUsage } from "./admin-identity-inspection.js";
|
|
49
50
|
import {
|
|
@@ -2583,6 +2584,10 @@ export function parseAdminOperation(options) {
|
|
|
2583
2584
|
if (namespace === "roles") {
|
|
2584
2585
|
return buildRolesOperation(options);
|
|
2585
2586
|
}
|
|
2587
|
+
// Twinkle.craft universe curation (admin-craft.js); no run.
|
|
2588
|
+
if (namespace === "craft") {
|
|
2589
|
+
return buildCraftOperation({ action, options });
|
|
2590
|
+
}
|
|
2586
2591
|
|
|
2587
2592
|
if (namespace === "correction" || namespace === "corrections") {
|
|
2588
2593
|
if (!action || action === "status") {
|
|
@@ -7354,6 +7359,13 @@ function printAdminResult({ operation, result }) {
|
|
|
7354
7359
|
printRoles(data);
|
|
7355
7360
|
return;
|
|
7356
7361
|
}
|
|
7362
|
+
if (operation.name.startsWith("craft.")) {
|
|
7363
|
+
const lines = formatCraftResult({ operation, data });
|
|
7364
|
+
if (lines) {
|
|
7365
|
+
for (const line of lines) console.log(line);
|
|
7366
|
+
return;
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7357
7369
|
if (operation.name.startsWith("reward-bank.")) {
|
|
7358
7370
|
if (operation.name === "reward-bank.seed") {
|
|
7359
7371
|
console.log(
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// GENERATED from CORE_VALUES.md ("For building agents") by
|
|
2
|
+
// scripts/generate-builder-stance.mjs. Do not edit by hand.
|
|
3
|
+
export const LUMINE_BUILDER_STANCE = "You are a tool for the creator. Your job is to understand what they want to make, or seem to want to make, and help them make the best version of it.\n\n- What to make is the creator's call. Never steer them toward or away from a kind of project, genre, mechanic or style because of your own taste.\n- Be opinionated about how to make their idea great. Like a trusted senior collaborator, point out the loose ends: gaps between what they asked for and what exists, unfinished parts, edge cases, and things that will break or feel off. Then say clearly what you would do next if you were them, and why. Let them decide.\n- When they ask for ideas, offer a few options that fit what they're making, say which one you'd pick for their goal and why, and don't push.\n- When their direction is vague or feeling-based (\"make it more epic\", \"it feels empty\", \"I don't like this\"), work out what they mean and make the result visibly respond to it. Ask one short question only when you genuinely can't tell.\n- Make it well: aim for the best, most complete and polished version of their idea, within what they asked.\n- Don't coach, lecture or comment on the creator themselves; let the work speak.";
|
package/lib/commands.js
CHANGED
|
@@ -2802,6 +2802,14 @@ export function parseArgs(args) {
|
|
|
2802
2802
|
adminEditionId: raw.editionId ? String(raw.editionId) : "",
|
|
2803
2803
|
adminLeaseToken: raw.leaseToken ? String(raw.leaseToken) : "",
|
|
2804
2804
|
adminFile: raw.file ? String(raw.file) : "",
|
|
2805
|
+
// admin craft revise --item <id|name> [--why] [--emoji] [--category]
|
|
2806
|
+
// (kept raw: a flag given without a text value is refused in
|
|
2807
|
+
// admin-craft.js rather than sent as the string "true")
|
|
2808
|
+
adminCraftItem: raw.item,
|
|
2809
|
+
adminCraftWhy: raw.why,
|
|
2810
|
+
adminCraftEmoji: raw.emoji,
|
|
2811
|
+
adminCraftCategory: raw.category,
|
|
2812
|
+
adminCraftPair: raw.pair,
|
|
2805
2813
|
adminPort: raw.port ? String(raw.port) : "",
|
|
2806
2814
|
adminForeground: Boolean(raw.foreground),
|
|
2807
2815
|
adminOpen: Boolean(raw.open),
|
|
@@ -3233,6 +3241,7 @@ export function printHelp() {
|
|
|
3233
3241
|
lumine admin english import <verdicts.json> --pass a|b | adjudicate <file> | add-questions <file> | relevel <file> | mastery <userId> --reason <why> | backfill-distractors | summary | review [--limit <n>] | accept|retire <questionId> | retag <questionId> --skill <code> (English skill tags and bank upkeep for Grammarbles; content written by Claude Code agents)
|
|
3234
3242
|
lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
|
|
3235
3243
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3244
|
+
lumine admin craft seed --build <id> --file <pairs.json> [--confirm] | stats --build <id> [--limit <1-100>] | revise --build <id> --item <id|name> [--why <text>] [--emoji <emoji>] [--category <field>] [--confirm] | retire --build <id> --item <id|name> --reason <text> [--confirm] | unfizzle --build <id> [--since <iso>] [--pair <a+b>] [--confirm] (Twinkle.craft curation: dry run without --confirm)
|
|
3236
3245
|
lumine admin storage list [--status pending|approved|rejected|all] [--cursor <id>] | show <user-id-or-username> | approve <request-id> [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <request-id> [--reason <text>] | grant <user-id-or-username> --size <500MB|1GB|2GB|n MB> [--reason <text>]
|
|
3237
3246
|
lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | request-changes <crewId> --note <text> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch "<name>" [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId> (Bridge Builder meetup quest)
|
|
3238
3247
|
lumine admin meetup story list | show <crewId> | approve <crewId> [--note <text>] [--no-announce] | send-back <crewId> --note <text> | unpublish <crewId> --note <text> (Bridge Builder story pages: publishes only what every tagged member's parent allowed)
|
package/lib/constants.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os from "os";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { LUMINE_BUILDER_STANCE } from "./builderStance.generated.js";
|
|
3
4
|
|
|
4
5
|
export const DEFAULT_API_URL = "https://api.twinkle.network";
|
|
5
6
|
export const DEFAULT_SITE_URL = "https://www.twin-kle.com";
|
|
@@ -173,6 +174,10 @@ export const LUMINE_AGENT_INSTRUCTIONS = `${LUMINE_AGENT_INSTRUCTIONS_MARKER}
|
|
|
173
174
|
This directory contains Twinkle Build project files pulled by Lumine CLI. Use
|
|
174
175
|
Lumine CLI as the source of truth for saving this workspace back to Twinkle.
|
|
175
176
|
|
|
177
|
+
## Building For The Creator
|
|
178
|
+
|
|
179
|
+
${LUMINE_BUILDER_STANCE}
|
|
180
|
+
|
|
176
181
|
## Source Of Truth
|
|
177
182
|
|
|
178
183
|
- Read .twinkle/lumine-project.json before changing files.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage5/lumine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.34",
|
|
4
4
|
"description": "Command line tools for launching Lumine builds on Twinkle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "node --test"
|
|
16
|
+
"test": "node --test",
|
|
17
|
+
"prepublishOnly": "node --test test/builderStanceSync.test.js"
|
|
17
18
|
},
|
|
18
19
|
"publishConfig": {
|
|
19
20
|
"access": "public"
|
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
4
|
-
Updated: 2026-10-
|
|
5
|
-
Generated: 2026-10-
|
|
3
|
+
Version: 1.65.1
|
|
4
|
+
Updated: 2026-10-13
|
|
5
|
+
Generated: 2026-10-12T17:14:00.482Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -36,7 +36,7 @@ Generated: 2026-10-10T06:47:00.045Z
|
|
|
36
36
|
- The creator's agent designs the rewards. Declare the economy in a project file `rewards.json` at the root: budgets (userDailyXP, userDailyCoins, optional userDailyClaims; there is no app-wide daily or lifetime budget, only what one learner can earn per day; an award larger than the learner's remaining room today pays what fits, XP and Coins clamped separately; a capped award still counts as claimed, including a one-time maxLifetimeClaims reward, and its receipt carries cappedFrom) and rules [{ id, title, howTo?, category? (section header, up to 40 characters), series? (key for repeat rules that share category and amounts), xp, coins, verifier: 'numeric-quiz' | 'completion' | 'generated-quiz', maxAttempts?, retry?: { xpPercent, coinsPercent, paidAttempts? }, minSeconds? (completion), progression?: 'dated' | 'until-earned' (quiz), bank? (generated-quiz): { steps: [percent, ...] rising to 100, startStep?, instructions, topics: [{ key, title, description }] } }]. generated-quiz rules need no question sheet. Wire the matching Twinkle.rewards calls with those literal rule ids. Questions and answer keys NEVER go in project files (published source is readable by every player): quiz rules get them from the private question sheet uploaded with `lumine rewards sheet <file.json>` ({ rules: { <ruleId>: { questions?, sets? } } }); `lumine rewards check` validates both together. A review request freezes the code and proposes rewards.json merged with the sheet; the administrator reads the code, checks the amounts and whether the app is exploitable, may change any amount, and approves. Creators are kids and teens: show approval status and one Send for review action; do not ask them to fill in technical forms. Every code update that retains rewards needs a new approval before publishing. Removing the SDK automatically clears its gate. Apps read amounts, tries and sets from getStatus, never from their own file.
|
|
37
37
|
- Verifiers: 'numeric-quiz' pays for server-checked numeric answers (retry share, attempt limits, dated sets or until-earned sets that stay up until somebody earns them, after-answer guides). 'completion' pays when the app reports an activity finished — a cleared stage, a finished round — at least minSeconds after start({ ruleId }); the server checks only the elapsed time, once per learner per site day (UTC midnight), and the budgets. Call start when the activity begins and claim({ challengeId }) with no answers when it ends; keep completion amounts and userDailyXP small enough that a player scripting the calls would not matter, because nothing else is verified. 'generated-quiz' pays from a question bank the server fills itself: for one declared topic at the learner's step of the ladder, the server writes a question with GPT-6.1 Sol (billed to that learner's AI Energy), solves it again independently and keeps it only when both answers agree; every learner who has not seen a question gets it before anything new is written. `xp`/`coins` are the top step's amounts and each step pays its `steps` percent of them (then the usual retry share). Two first-try answers in a row climb a step; two misses in a row step down. Learners may earn many bank questions a day, bounded by userDailyXP/userDailyCoins. The reviewer approves the ladder, amounts, topics and instructions, not each question; learner reports and solve rates hide bad questions. Use the rewards bank methods (getBankStatus, startBankQuestion, generateBankQuestion, claimBankQuestion, reportBankQuestion); start and claim refuse bank rules. 'archive-rematch' is the daily rematch: one retired until-earned puzzle of the app a day, chosen by the server (celebrated, high-rated solves first; never repeated within 60 days) and answered from its frozen approved sheet. Declare at most one, with no questions, sets, progression, series, minSeconds or completionProof, e.g. { "id": "rematch-daily", "title": "Daily rematch", "xp": 20000, "coins": 1000, "verifier": "archive-rematch", "retry": { "xpPercent": 50, "coinsPercent": 0, "paidAttempts": 3 }, "maxAttempts": null }. Only players who never met that puzzle (no official receipt, wrong answer or challenge while it was live, no Vault practice) may answer it; its receipt does not use userDailyClaims but counts toward userDailyXP/userDailyCoins. Use getRematch and claimRematch; start and claim refuse it.
|
|
38
38
|
- Numeric quiz answers are verified on the server; client scores, privateDb state, timers and completion booleans are not verified reward evidence. Limits reset at UTC midnight. Rules are earned once per viewer per UTC day; attempt limits and retry payouts come from the approved rule. Challenges expire at the UTC day boundary. Budgets apply across release changes.
|
|
39
|
-
- Optional reward rule controls: maxLifetimeClaims caps one learner’s receipts for that rule across every day and release (a receipt capped by the daily budget still counts, so a one-time reward claimed near the cap pays only what fits); completionProof: classic-tower-v1 requires a server-simulated Classic Tower finish in addition to minSeconds. These are server-enforced controls. Existing completion rules without completionProof still verify elapsed time only. Registered proof profiles also include breadface-v1, breadface-v2 and breadface-v3 (server-simulated Breadface inputs; each is one reviewed release's exact physics, chosen by the reviewer) study-record-v1 (a private study record reviewed by JEV, billed to the learner’s AI Energy), and Groove Lab's groove-lab-song-v1 (a song the learner published today, checked on the server for length, notes and originality) and groove-lab-heard-v1 (three established accounts finished the learner's songs today), groove-lab-listen-v1 (the learner listened to most of someone else's published song: start binds the song, the server clock covers three quarters of it up to two minutes, and the listener's own groove-lab-listens row for it was written today; each song pays a listener once a day across listening rules) and groove-lab-own-listen-v1 (the same for the learner's own song; own listens never count toward groove-lab-heard-v1), Ashen Vigil's vigil-guest-coplay-v1 (a new guest spent 10 minutes with the learner in a private world room, measured by the world relay; each guest pays once) and minecraft-first-link-v1 (the learner is the first Twinkle account ever linked to that Minecraft player; each player pays once), and writing-words-v1 with minWords (counted words the learner wrote today as chapters in their own books registered in the app's shared books topic; each chapter is read once by a small model for genuine writing and language, English words count fully and other languages half, and the same text counts once), reading-quiz-v1 (the learner passed a server-graded three-question quiz on a chapter of someone else's registered book after its reading time; each chapter pays a reader once, one attempt per chapter per day) and reading-readers-v1 with minReaders (that many established accounts passed a quiz on the learner's chapters today). Profiles are platform-owned; an app cannot invent a verifier or authorize its own reward.
|
|
39
|
+
- Optional reward rule controls: maxLifetimeClaims caps one learner’s receipts for that rule across every day and release (a receipt capped by the daily budget still counts, so a one-time reward claimed near the cap pays only what fits); completionProof: classic-tower-v1 requires a server-simulated Classic Tower finish in addition to minSeconds. These are server-enforced controls. Existing completion rules without completionProof still verify elapsed time only. Registered proof profiles also include breadface-v1, breadface-v2 and breadface-v3 (server-simulated Breadface inputs; each is one reviewed release's exact physics, chosen by the reviewer) study-record-v1 (a private study record reviewed by JEV, billed to the learner’s AI Energy), and Groove Lab's groove-lab-song-v1 (a song the learner published today, checked on the server for length, notes and originality) and groove-lab-heard-v1 (three established accounts finished the learner's songs today), groove-lab-listen-v1 (the learner listened to most of someone else's published song: start binds the song, the server clock covers three quarters of it up to two minutes, and the listener's own groove-lab-listens row for it was written today; each song pays a listener once a day across listening rules) and groove-lab-own-listen-v1 (the same for the learner's own song; own listens never count toward groove-lab-heard-v1), Ashen Vigil's vigil-guest-coplay-v1 (a new guest spent 10 minutes with the learner in a private world room, measured by the world relay; each guest pays once) and minecraft-first-link-v1 (the learner is the first Twinkle account ever linked to that Minecraft player; each player pays once), and writing-words-v1 with minWords (counted words the learner wrote today as chapters in their own books registered in the app's shared books topic; each chapter is read once by a small model for genuine writing and language, English words count fully and other languages half, and the same text counts once), reading-quiz-v1 (the learner passed a server-graded three-question quiz on a chapter of someone else's registered book after its reading time; each chapter pays a reader once, one attempt per chapter per day) and reading-readers-v1 with minReaders (that many established accounts passed a quiz on the learner's chapters today). Twinkle.craft apps also have craft-target-v1 (the learner's run of merges reaches today's target in this app's universe; every merge is a known pair and every result is one the learner made through Twinkle.craft.merge), craft-par-v1 (the same run reaches it in at most par merges) and craft-discovery-v1 (the oldest World First of the learner's that no rule has paid yet; each World First pays once ever; optional minWordLevel basic|elementary|intermediate|advanced|epic makes tiers: either every craft-discovery-v1 rule has one or none does; each World First pays under exactly one tier, the highest configured tier at or below its Word Master level, so a series per tier can pay rarer words more and the order of claims never matters; an item whose level is still pending pays under no tier yet). Profiles are platform-owned; an app cannot invent a verifier or authorize its own reward.
|
|
40
40
|
|
|
41
41
|
## AI decision design
|
|
42
42
|
- Twinkle.ai.decide runs JEV, a model for narrow decisions over supplied text or structured state, and it is free for viewers: decisions are covered by Twinkle and never use their AI Energy. Whenever an app makes a judgment that ordinary code can only approximate, consider JEV first. Proven patterns: an opponent or boss coach that reads the player's habits (favourite lane, repeated trick, cheese move) and picks a counter-plan from a fixed list, as in Red Square's boss and Open Football's opponent coach; NPCs that understand what a player types and choose an action; classifying or scoring user submissions; ranking supplied candidates; and picking the next exercise or difficulty from evidence about the learner. Any app with an AI opponent, NPC, or adaptive difficulty should decide explicitly whether JEV improves it. These are examples to reason from, not a keyword checklist or a requirement to add AI to every app.
|
|
@@ -1517,6 +1517,7 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1517
1517
|
- Completion rules take no answers: claim({ challengeId }) when the activity is finished. build_reward_too_fast (HTTP 409) means fewer than minSeconds passed since start; show nothing and let play continue. In preview mode the receipt carries preview: true and nothing is paid.
|
|
1518
1518
|
- A completionProof rule also requires the signed completionToken from a successful rewards.progress response. The server simulates the registered game physics and must reach the goal. A timer, forged position, client win flag, altered inventory or token from another viewer, challenge or release cannot authorize payment. maxLifetimeClaims is enforced from receipts in the same award transaction; every receipt counts, including one capped by the daily budget (cappedFrom). build_reward_lifetime_claims_reached means all rewards for this rule have been collected; do not retry it.
|
|
1519
1519
|
- For study-record-v1, the server verifies the settled private review row and its viewer, app, exact release, challenge and day binding. It never trusts the client’s decision, requested award amount or AI answer. Each successful daily study record is claimable once; display only canonical receipt/balances. For groove-lab-song-v1, pass completionToken as the published song's sharedDb entry id; for groove-lab-heard-v1, pass none; for vigil-guest-coplay-v1, pass the guest's world guestId (from world events) or the co-play record id; for minecraft-first-link-v1, pass nothing or the Minecraft UUID to use; for reading-quiz-v1, pass completionToken as the chapter commentId and answers as the three chosen option indexes (all three must be right; a wrong attempt cannot be retried that day); for groove-lab-listen-v1 and groove-lab-own-listen-v1, pass the same song entry id after listenSeconds of foreground playback; for reading-readers-v1, pass nothing; for writing-words-v1, pass nothing (the server counts today's chapters itself and refuses with a plain reason, e.g. "320 of 500 counted words today" or "still being checked"). The server reads the app's own groove-lab-songs, groove-lab-song-parts and groove-lab-listens rows and refuses with build_reward_completion_proof_required and a plain reason when the song or listeners do not qualify.
|
|
1520
|
+
- For craft-target-v1 and craft-par-v1, pass completionToken as JSON.stringify({ v: 1, merges: [[a, b], ...] }): the item ids of the run's successful merges in order, starting from the starters (at most 300 merges, 32 KB). The server replays it against this app's known pairs, needs today's target (Twinkle.craft.getState().today) to appear, every result to be in the learner's collection, and at least one second per merge since start; it refuses an unknown pair, an item the run had not made yet, an over-par run (par rule) or a run on another day. For craft-discovery-v1, pass nothing: the server pays the oldest unpaid World First (receipt slot craft:<itemId>) and puts its name on the bounty story ("discovered <item>"). With tiers, a rule pays only World Firsts of its own tier (refused with a plain reason when the learner has none).
|
|
1520
1521
|
- await Twinkle.rewards.getLeaderboard({ metric?: "xp" | "coins", period?: "day" | "week" | "all", limit? }) | scopes: rewards:claim
|
|
1521
1522
|
- Returns: { mode: "live", metric, period, limit, dayKey, from, available: { xp, coins }, entries: [{ rank, userId, username, profilePicUrl, xp, coins, claims, lastAt }], me: { rank, xp, coins, claims } | null } | { mode: "preview", metric, period, available, entries: [], me: null, message }
|
|
1522
1523
|
- Standings of who earned the most XP or Coins in THIS app, computed by Twinkle from its own receipts (never from anything the app submits). period 'day' is today (site day, UTC), 'week' the last 7 site days, 'all' (default) every day since approval. limit defaults to 20, max 100.
|
|
@@ -1652,6 +1653,36 @@ spawnPet(asset.kind, asset.params, asset.palette, asset.tier.effects);
|
|
|
1652
1653
|
- The asset's name is fixed: it is shown on the card page and cannot be renamed. To let owners name their pet, keep an in-game nickname in state (for example state.nickname, trimmed and length-limited by your app) and show it in the game. After a sale the new owner can set their own nickname the same way.
|
|
1653
1654
|
- expectedRevision is the stateRevision you last read; a mismatch fails with code card_craft_state_conflict (details.stateRevision), and a card the viewer no longer owns fails with card_craft_not_owner.
|
|
1654
1655
|
|
|
1656
|
+
### Twinkle.craft
|
|
1657
|
+
- await Twinkle.craft.getState() | scopes: craft:read
|
|
1658
|
+
- Returns: { starters: [item], counts: { mine, world }, today: { day, target: item | null, par: number | null, done: boolean }, reason?: "sign_in_required" }
|
|
1659
|
+
- item is { id, name, emoji, category, wordLevel: "basic" | "elementary" | "intermediate" | "advanced" | "epic" | null, isStarter }. wordLevel is Word Master's level of the name's rarest word (null while it is being looked up); show it by name, never as a number.
|
|
1660
|
+
- Starters are always in the player's hands and are not in list(). counts.mine is the player's collection without starters; counts.world is every item in this universe.
|
|
1661
|
+
- today is Today's Star: the same target for everyone in this app on this site day (UTC), reachable from the starters in par merges or fewer (par is the server's best plan, an upper bound). target is null while the universe is too young. done is true once the player has the target.
|
|
1662
|
+
- Signed-out viewers get the same shape, empty: starters [], counts 0, today with target null, plus reason: "sign_in_required". counts.world may be up to a minute old.
|
|
1663
|
+
- await Twinkle.craft.merge({ a, b, requestId? }) | scopes: craft:write
|
|
1664
|
+
- Returns: { status: "ok" | "fizzle", item: item | null, why: string | null, newForMe: boolean, worldFirst: boolean, newPair: boolean, aiUsagePolicy? }
|
|
1665
|
+
- a and b are item ids the player has (starters or their collection); the order does not matter and an item can merge with itself. A known pair answers at once and costs nothing. A new pair asks the AI (usually a few seconds; the SDK waits up to 120 s), costs the player a little AI Energy (newPair: true) and is then known for everyone. fizzle means the pair makes nothing; it is stored too, so it is also free afterwards.
|
|
1666
|
+
- why is one true sentence explaining the result ("Lava cools in water and hardens into rock."): show it, it is the learning part. worldFirst: true means this player is the first in this universe to make the item; their name is on it for good.
|
|
1667
|
+
- Limits: 300 new pairs per player per day (known pairs are unlimited), 20,000 items per collection, and about 1 merge a second. Errors carry code: craft_item_not_owned, craft_item_not_found, craft_in_progress (someone else is discovering the same pair; retry shortly), craft_daily_limit, craft_collection_full, craft_universe_full, craft_generation_failed / craft_generation_timeout (the AI was unavailable or too slow and the pair stays new; try again), craft_check_unavailable (HTTP 503: Twinkle's safety check is briefly down; nothing was stored and the pair stays new, so try again in a minute), craft_app_not_found / craft_forbidden (the app was deleted or the viewer can no longer open it), craft_throttled, and the AI Energy codes of Twinkle.ai.
|
|
1668
|
+
- Pass the same requestId when retrying after a lost reply. Keep a run's successful merges as [a, b] ids for Twinkle.rewards.claim (craft-target-v1 / craft-par-v1).
|
|
1669
|
+
- A result that is a starter (Water + Water = Water) is not added to the collection: newForMe is false. An error can carry aiUsagePolicy when an earlier AI attempt already used Energy.
|
|
1670
|
+
- Items can be taken down by Twinkle moderators: a retired item leaves the player's collection (list, item, getState counts) and its pairs answer fizzle. Treat ids you cached as possibly gone: a merge with one returns craft_item_not_found.
|
|
1671
|
+
- Example: const result = await Twinkle.craft.merge({ a: water.id, b: fire.id });
|
|
1672
|
+
if (result.status === 'ok') showCard(result.item, result.why, result.worldFirst);
|
|
1673
|
+
- await Twinkle.craft.list({ cursor?, limit? }) | scopes: craft:read
|
|
1674
|
+
- Returns: { items: [{ id, name, emoji, category, wordLevel, firstAt, isWorldFirst }], cursor: number | null }
|
|
1675
|
+
- The player's collection in this app, oldest item id first; limit 1-200 (default 100). firstAt is when this player got the item; isWorldFirst is true for items they discovered first in the universe.
|
|
1676
|
+
- await Twinkle.craft.item({ id }) | scopes: craft:read
|
|
1677
|
+
- Returns: { item: { id, name, emoji, category, wordLevel, isStarter, why, firstDiscoverer: { id, username } | null, firstAt, holders, recipes: [[{ id, name, emoji }, { id, name, emoji }]] } | null }
|
|
1678
|
+
- The discovery card of an item the player has (null otherwise, so undiscovered items never leak). why is the explanation from the merge that first made it, holders how many players have it, recipes up to five known pairs that make it whose inputs the player has.
|
|
1679
|
+
- await Twinkle.craft.journey({ day? }) | scopes: craft:read
|
|
1680
|
+
- Returns: { day, journey: { target: item, par, steps: [{ a, b, result }] } | null, reason?: "no_target" | "reach_target_first" | "sign_in_required" }
|
|
1681
|
+
- The server's par plan for a day (YYYY-MM-DD, default today), each step { id, name, emoji } items. For today it opens only after the player made the target today (reason reach_target_first before that); past days are open too, except a past day whose target is today's target (same rule as today).
|
|
1682
|
+
- await Twinkle.craft.rankings() | scopes: craft:read
|
|
1683
|
+
- Returns: { allTime: { top: [{ rank, userId, username, profilePicUrl, worldFirsts }], me: { rank: number | null, worldFirsts } }, month: { month, top, me: { rank: number | null, worldFirsts } } }
|
|
1684
|
+
- World First rankings in this app: the top 30 discoverers all time and this month (UTC), and the viewer's own rank (null with no World Firsts). The top lists may be up to a minute old. Signed-out viewers get empty top lists and me: { rank: null, worldFirsts: 0 }.
|
|
1685
|
+
|
|
1655
1686
|
### Twinkle.network
|
|
1656
1687
|
- async getHome({}) | scopes: sharedDb:read
|
|
1657
1688
|
- Returns: { name, buildId, url, communities, counts }
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -1592,6 +1592,99 @@ lumine admin roles --min-level 3 # only the admin types' holders
|
|
|
1592
1592
|
lumine admin roles --min-level 0 --limit 200 --json
|
|
1593
1593
|
```
|
|
1594
1594
|
|
|
1595
|
+
### Twinkle.craft universes: seed, audit, revise (any time; added 2026-10-12)
|
|
1596
|
+
|
|
1597
|
+
An app using `Twinkle.craft` (Infinity Craft, build 1499) has its own
|
|
1598
|
+
universe of items and pairs. Players' new pairs are answered by the AI on
|
|
1599
|
+
their own Energy; we seed and curate the rest with Claude Code agents, not
|
|
1600
|
+
API calls (Mikey 10-12): an initial seed, then expansion and a daily audit,
|
|
1601
|
+
Math Lab style. No daily run is needed. `seed` and `revise` are dry runs
|
|
1602
|
+
unless `--confirm` is given.
|
|
1603
|
+
|
|
1604
|
+
```bash
|
|
1605
|
+
lumine admin craft stats --build 1499 [--limit 50] # sizes, frontier, today's star, newest AI answers, kept flagged names
|
|
1606
|
+
lumine admin craft seed --build 1499 --file pairs.json # ALWAYS first: what would be added, skipped, rejected, warned
|
|
1607
|
+
lumine admin craft seed --build 1499 --file pairs.json --confirm # only after reading every warning
|
|
1608
|
+
lumine admin craft revise --build 1499 --item Obsidian --why "Lava that cools very fast turns into glassy rock." --confirm
|
|
1609
|
+
```
|
|
1610
|
+
|
|
1611
|
+
- `pairs.json` is up to 500 pairs, in order: `[{ "a": "Water", "b": "Fire",
|
|
1612
|
+
"result": { "name": "Steam", "emoji": "♨️", "category": "science", "why":
|
|
1613
|
+
"Fire heats water until it turns into vapour." } }]` (or `{ "pairs": [...] }`).
|
|
1614
|
+
`a` and `b` are item names: starters (Water, Fire, Earth, Wind, Tree,
|
|
1615
|
+
Nature), items already in the universe, or results earlier in the same file.
|
|
1616
|
+
- Every result passes the same checks as AI answers: 1-3 English words of
|
|
1617
|
+
letters (32 characters), exactly one emoji, a category from nature, weather,
|
|
1618
|
+
earth, space, life, body, science, energy, materials, food, tools, places,
|
|
1619
|
+
ideas, and one plain-text why of at most 140 characters (no links or
|
|
1620
|
+
markup). No AI or JEV call runs, so the content is ours to get right: real,
|
|
1621
|
+
common English nouns or ideas, no people, characters or brands, child-safe,
|
|
1622
|
+
and a true why that teaches the link.
|
|
1623
|
+
- **Always dry-run a seed first and read every warning before `--confirm`.**
|
|
1624
|
+
A warning means a word of the name is on Word Master's censored list; the
|
|
1625
|
+
pair is still written on `--confirm`, so drop or rename any pair whose name
|
|
1626
|
+
could read rudely before confirming.
|
|
1627
|
+
- A pair that already has an answer is skipped, never overwritten. Each
|
|
1628
|
+
chunk of 100 is one audited transaction (`craft.seed`); a re-run is safe.
|
|
1629
|
+
Word levels (Word Master's basic..epic) are looked up as for player items.
|
|
1630
|
+
- Seeded items have no World First: the first player to make one claims it
|
|
1631
|
+
(name on the item, rankings, the discovery reward). Seeded pairs count for
|
|
1632
|
+
Today's Star like any answered pair and never count toward the admin's
|
|
1633
|
+
daily new-pair limit.
|
|
1634
|
+
- `stats` lists the newest AI-made pairs (`source: ai`, newest first, `--limit`
|
|
1635
|
+
1-100, default 20) with each result's name, emoji, category, word level and
|
|
1636
|
+
why. Audit them daily; fix a wrong or weak explanation with `revise`.
|
|
1637
|
+
- `stats` also lists **kept flagged names**: AI items JEV kept although a word
|
|
1638
|
+
matched the censored list (name, matched word, JEV's score, when). Read
|
|
1639
|
+
each one as a child would; `retire` any that reads rudely.
|
|
1640
|
+
- `revise` changes an item's `--why` (the explanation on its card: the pair
|
|
1641
|
+
that first made it), `--emoji` or `--category`, through the same checks. The
|
|
1642
|
+
name never changes: players hold it. Audited as `craft.revise`.
|
|
1643
|
+
- Expansion: `stats` shows the frontier (items no pair uses yet, and untried
|
|
1644
|
+
pairs); seed pairs that use frontier items so Today's Star has fresh targets.
|
|
1645
|
+
|
|
1646
|
+
Take-down and repair (added 2026-10-13):
|
|
1647
|
+
|
|
1648
|
+
```bash
|
|
1649
|
+
lumine admin craft retire --build 1499 --item "<name or id>" --reason "Not suitable for children" # dry run
|
|
1650
|
+
lumine admin craft retire --build 1499 --item "<name or id>" --reason "Not suitable for children" --confirm
|
|
1651
|
+
lumine admin craft unfizzle --build 1499 [--since 2026-10-13T00:00:00Z] [--pair Water+Fire] # dry run
|
|
1652
|
+
lumine admin craft unfizzle --build 1499 --since 2026-10-13T00:00:00Z --confirm
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
- `retire` takes a bad item down everywhere: it leaves collections, item
|
|
1656
|
+
cards, rankings, journeys and the daily target (today's star is re-picked);
|
|
1657
|
+
the pairs that make it fizzle and no AI answer or seed can make it again;
|
|
1658
|
+
its unpaid World First no longer pays (a paid one stays paid); its name
|
|
1659
|
+
leaves bounty story lines. Who, when and why are stored on the item and in
|
|
1660
|
+
the admin audit (`craft.retire`). Starters cannot be retired. There is no
|
|
1661
|
+
un-retire.
|
|
1662
|
+
- Word Master's censored list (`content_words_censored`) is a Wordle filter,
|
|
1663
|
+
not a moderation list, so a hit never decides anything on its own. A name
|
|
1664
|
+
with a listed word (whole words only, simple inflections both ways: -s,
|
|
1665
|
+
-es, -ed, -ing; hyphens count as spaces) is always judged by JEV as a
|
|
1666
|
+
whole, told which word to look at, and must clear a stricter bar. Crystal
|
|
1667
|
+
Ball, Sperm Whale, Pork Loin or Blue-Footed Booby pass when JEV finds them
|
|
1668
|
+
innocent; a rude reading is regenerated once, then the pair fizzles.
|
|
1669
|
+
Only JEV itself judges such a name: if JEV is down and its fallback model
|
|
1670
|
+
answers, the merge takes the outage path below (the answer is kept and
|
|
1671
|
+
re-checked once JEV is back). There is no allowlist. For seeds a hit is a
|
|
1672
|
+
**warning** in the report (printed as `warning …` lines, and in `warnings`
|
|
1673
|
+
with `--json`), never a rejection.
|
|
1674
|
+
- During a safety-check outage players get `craft_check_unavailable`. A
|
|
1675
|
+
pair's answer is paid for once and re-checked free for 24 h from when the
|
|
1676
|
+
model wrote it; after that a new try generates fresh. (If JEV later judges
|
|
1677
|
+
the kept answer low, the one normal regeneration is billed as usual.) Each
|
|
1678
|
+
server pauses new runs for a minute after an outage (per pair, or
|
|
1679
|
+
everywhere after three); the pause never stops the free re-check of a kept
|
|
1680
|
+
answer or a run that has already paid.
|
|
1681
|
+
- `unfizzle` repairs pairs that fizzled because the safety check itself
|
|
1682
|
+
failed (an outage before 2026-10-13; a check outage now fails the merge
|
|
1683
|
+
without storing anything). It resets only fizzles whose stored checks all
|
|
1684
|
+
errored, never a real low verdict or a censored word; the next player to
|
|
1685
|
+
try the pair asks again. `--since` and `--pair` narrow it; up to 5,000
|
|
1686
|
+
fizzles are scanned per run.
|
|
1687
|
+
|
|
1595
1688
|
### Reward question-bank seeding (every full daily review; added 2026-09-28)
|
|
1596
1689
|
|
|
1597
1690
|
`generated-quiz` rules fill a question bank on the server: a question is
|
|
@@ -2580,6 +2673,12 @@ never previously been Featured from canonical evidence.
|
|
|
2580
2673
|
|
|
2581
2674
|
### Exact approved refresh and resumable comment encouragement
|
|
2582
2675
|
|
|
2676
|
+
Keep current manual pins from the past two days (Mikey, October 13, 2026).
|
|
2677
|
+
Verify canonical pin history and exclude those subjects from removal proposals.
|
|
2678
|
+
Subject 3529, “What is special about Twinkle?”, also has an explicit keep
|
|
2679
|
+
instruction. An absent pin is still never restored. This supersedes the older
|
|
2680
|
+
instruction to offer yesterday's manual pins for rotation.
|
|
2681
|
+
|
|
2583
2682
|
For the daily old-to-new proposal, prepare a canonical plan without changing pins:
|
|
2584
2683
|
|
|
2585
2684
|
```bash
|
|
@@ -2889,6 +2988,14 @@ to join in. Games, art, puzzles, friendships, shared reflections, and community
|
|
|
2889
2988
|
discussions can all supply good stories; read the actual source to find the
|
|
2890
2989
|
substance.
|
|
2891
2990
|
|
|
2991
|
+
**Community significance (Mikey, 2026-10-13):** substantive messages about
|
|
2992
|
+
Twinkle's shared values, purpose and direction deserve explicit front-page
|
|
2993
|
+
priority, including the lead when they are the day's most consequential story.
|
|
2994
|
+
Judge what the message means for members, regardless of who wrote it. Immediate
|
|
2995
|
+
entertainment, likely comments and opportunities to join in are not the only
|
|
2996
|
+
measures of importance. Explain the values being communicated; do not reduce
|
|
2997
|
+
them to founder biography or an administrative announcement.
|
|
2998
|
+
|
|
2892
2999
|
Use clear, lively language that respects readers' intelligence. Give enough
|
|
2893
3000
|
context for someone who missed the original post, and make the interesting
|
|
2894
3001
|
part clear in the headline and opening. Avoid talking down to readers, forced
|
|
@@ -3008,7 +3115,9 @@ make the call by reading:
|
|
|
3008
3115
|
|
|
3009
3116
|
- **Choose the lead for readers aged 10–15.** Lead with the eligible front
|
|
3010
3117
|
event whose substance is most likely to catch their interest and reward
|
|
3011
|
-
reading
|
|
3118
|
+
reading, weighing community significance as well as immediate appeal.
|
|
3119
|
+
A substantive statement of shared values or purpose, a thoughtful
|
|
3120
|
+
conversation, a funny or relatable reflection, a
|
|
3012
3121
|
striking creation, or an inviting community challenge can all qualify.
|
|
3013
3122
|
Read the source and any supplied replies to understand the appeal; priority,
|
|
3014
3123
|
recency, and the mere presence of an argument do not decide the lead.
|