@stage5/lumine 0.2.78 → 0.2.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/admin.js +160 -3
- package/lib/commands.js +3 -2
- package/lib/sdk.js +10 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +12 -1
- package/sdk/LUMINE_ADMIN.md +160 -30
package/lib/admin.js
CHANGED
|
@@ -139,6 +139,80 @@ function readBuildReviewContextFile(filePath) {
|
|
|
139
139
|
const MAX_REWARD_CONFIG_FILE_BYTES = 256 * 1024;
|
|
140
140
|
const REWARD_REVIEW_STATUSES = ["pending", "approved", "all"];
|
|
141
141
|
const REWARD_REVIEW_DECISIONS = ["approve", "reject", "revoke"];
|
|
142
|
+
const REWARD_PROPOSAL_MAX_FILES = 500;
|
|
143
|
+
const REWARD_PROPOSAL_MAX_BYTES = 5 * 1024 * 1024;
|
|
144
|
+
const REWARD_PROPOSAL_SKIPPED_DIRS = new Set([
|
|
145
|
+
".git",
|
|
146
|
+
"node_modules",
|
|
147
|
+
".lumine",
|
|
148
|
+
".twinkle",
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
// Reads a reviewer's edited copy of a reward-review snapshot (a directory
|
|
152
|
+
// written by `show --dir`, then edited) back into project files for
|
|
153
|
+
// `reward-review propose`. Text files only; dotfiles and tool directories
|
|
154
|
+
// are skipped; paths are confined to the directory.
|
|
155
|
+
export function readRewardProposalDirectory(directory) {
|
|
156
|
+
const requested = String(directory || "").trim();
|
|
157
|
+
if (!requested) {
|
|
158
|
+
throw cliValidationError(
|
|
159
|
+
"Pass the edited snapshot directory with --dir <path>.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const root = path.resolve(requested);
|
|
163
|
+
let rootStat;
|
|
164
|
+
try {
|
|
165
|
+
rootStat = lstatSync(root);
|
|
166
|
+
} catch {
|
|
167
|
+
throw cliValidationError(`--dir ${root} does not exist.`);
|
|
168
|
+
}
|
|
169
|
+
if (!rootStat.isDirectory()) {
|
|
170
|
+
throw cliValidationError(`--dir ${root} must be a directory.`);
|
|
171
|
+
}
|
|
172
|
+
const realRoot = realpathSync(root);
|
|
173
|
+
const files = [];
|
|
174
|
+
let bytes = 0;
|
|
175
|
+
const walk = (dir) => {
|
|
176
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
177
|
+
if (entry.name.startsWith(".") || REWARD_PROPOSAL_SKIPPED_DIRS.has(entry.name))
|
|
178
|
+
continue;
|
|
179
|
+
const fullPath = path.join(dir, entry.name);
|
|
180
|
+
if (entry.isSymbolicLink()) {
|
|
181
|
+
throw cliValidationError(
|
|
182
|
+
`Refusing to read symlink inside the proposal directory: ${fullPath}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
walk(fullPath);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!entry.isFile()) continue;
|
|
190
|
+
const buffer = readFileSync(fullPath);
|
|
191
|
+
if (buffer.includes(0)) {
|
|
192
|
+
throw cliValidationError(
|
|
193
|
+
`${fullPath} is not a text file. Twinkle project files must be UTF-8 text; media belongs in build assets.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
bytes += buffer.length;
|
|
197
|
+
const relative = path.relative(realRoot, fullPath).split(path.sep).join("/");
|
|
198
|
+
files.push({ path: `/${relative}`, content: buffer.toString("utf8") });
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
walk(realRoot);
|
|
202
|
+
if (files.length === 0) {
|
|
203
|
+
throw cliValidationError(`--dir ${root} holds no project files.`);
|
|
204
|
+
}
|
|
205
|
+
if (files.length > REWARD_PROPOSAL_MAX_FILES) {
|
|
206
|
+
throw cliValidationError(
|
|
207
|
+
`A proposal may carry at most ${REWARD_PROPOSAL_MAX_FILES} files.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (bytes > REWARD_PROPOSAL_MAX_BYTES) {
|
|
211
|
+
throw cliValidationError("A proposal may carry at most 5 MB of files.");
|
|
212
|
+
}
|
|
213
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
214
|
+
return files;
|
|
215
|
+
}
|
|
142
216
|
|
|
143
217
|
// The reviewer's earning rules for one Build reward approval: budgets plus
|
|
144
218
|
// server-verified numeric-quiz rules keyed by the rule IDs the app source
|
|
@@ -1453,6 +1527,32 @@ export function parseAdminOperation(options) {
|
|
|
1453
1527
|
{ requiresRun: false, reviewId, snapshotDir },
|
|
1454
1528
|
);
|
|
1455
1529
|
}
|
|
1530
|
+
if (action === "propose") {
|
|
1531
|
+
// Offer an edited copy of the snapshot as the condition of approval:
|
|
1532
|
+
// the creator accepts (which publishes it) or declines (which rejects).
|
|
1533
|
+
const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
|
|
1534
|
+
const reason = String(options.adminReason || "").trim();
|
|
1535
|
+
if (reason.length > 1000) {
|
|
1536
|
+
throw cliValidationError("--reason must be at most 1000 characters.");
|
|
1537
|
+
}
|
|
1538
|
+
if (!options.adminConfigFile) {
|
|
1539
|
+
throw cliValidationError(
|
|
1540
|
+
"lumine admin reward-review propose <id> needs --config <rules.json> (the rules the creator's acceptance publishes) and --dir <edited snapshot>.",
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
const files = readRewardProposalDirectory(options.dir);
|
|
1544
|
+
return writeOperation(
|
|
1545
|
+
"reward-review.propose",
|
|
1546
|
+
"POST",
|
|
1547
|
+
`/cli/admin/reward-reviews/${reviewId}/propose`,
|
|
1548
|
+
{
|
|
1549
|
+
files,
|
|
1550
|
+
config: readRewardConfigFile(options.adminConfigFile),
|
|
1551
|
+
reason,
|
|
1552
|
+
},
|
|
1553
|
+
{ requiresRun: false, reviewId, fileCount: files.length },
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1456
1556
|
if (REWARD_REVIEW_DECISIONS.includes(action)) {
|
|
1457
1557
|
const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
|
|
1458
1558
|
const reason = String(options.adminReason || "").trim();
|
|
@@ -1484,7 +1584,7 @@ export function parseAdminOperation(options) {
|
|
|
1484
1584
|
);
|
|
1485
1585
|
}
|
|
1486
1586
|
throw cliValidationError(
|
|
1487
|
-
"Usage: lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] | show <id> [--dir <path>] | approve <id> [--config <rules.json>] [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
|
|
1587
|
+
"Usage: lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] | show <id> [--dir <path>] | approve <id> [--config <rules.json>] [--reason <text>] | propose <id> --dir <edited-snapshot> --config <rules.json> [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
|
|
1488
1588
|
);
|
|
1489
1589
|
}
|
|
1490
1590
|
|
|
@@ -3519,13 +3619,17 @@ function printAdminEnergyBudget(energyBudget) {
|
|
|
3519
3619
|
);
|
|
3520
3620
|
}
|
|
3521
3621
|
console.log(
|
|
3522
|
-
"day | charged | overflow | users | rechg | runs | calls/run avg/p90 | $/run avg/p90 | stops chg/unchg | busy | telemetry rows",
|
|
3622
|
+
"day | charged | overflow | users | rechg | runs | calls/run avg/p90 | $/run avg/p90 | stops chg/unchg | busy | queue rst/stop/resume | telemetry rows",
|
|
3523
3623
|
);
|
|
3524
3624
|
for (const day of energyBudget.byDay || []) {
|
|
3525
3625
|
const m = day.telemetry.metrics;
|
|
3526
3626
|
const runs = day.runs.total;
|
|
3627
|
+
// Queue metrics are absent from APIs older than the queued-request telemetry.
|
|
3628
|
+
const queue = ["queued_restored", "queued_stopped", "busy_resume_requested"]
|
|
3629
|
+
.map((metric) => String(m[metric]?.count ?? "-"))
|
|
3630
|
+
.join("/");
|
|
3527
3631
|
console.log(
|
|
3528
|
-
`${day.dayKey}${day.inProgress ? "*" : " "}| ${formatAdminUsd(day.chargedUsd).padStart(7)} | ${formatAdminUsd(day.overflowUsd).padStart(8)} | ${String(day.users).padStart(5)} | ${String(day.recharges).padStart(5)} | ${String(runs.runs).padStart(4)} | ${String(runs.callsPerRun.avg).padStart(7)}/${String(runs.callsPerRun.p90).padEnd(9)} | ${formatAdminUsd(runs.usdPerRun.avg).padStart(6)}/${formatAdminUsd(runs.usdPerRun.p90).padEnd(6)} | ${String(m.budget_stop_changed.count).padStart(7)}/${String(m.budget_stop_unchanged.count).padEnd(7)} | ${String(m.busy_refusal.count).padStart(4)} | ${day.telemetry.rowCount}`,
|
|
3632
|
+
`${day.dayKey}${day.inProgress ? "*" : " "}| ${formatAdminUsd(day.chargedUsd).padStart(7)} | ${formatAdminUsd(day.overflowUsd).padStart(8)} | ${String(day.users).padStart(5)} | ${String(day.recharges).padStart(5)} | ${String(runs.runs).padStart(4)} | ${String(runs.callsPerRun.avg).padStart(7)}/${String(runs.callsPerRun.p90).padEnd(9)} | ${formatAdminUsd(runs.usdPerRun.avg).padStart(6)}/${formatAdminUsd(runs.usdPerRun.p90).padEnd(6)} | ${String(m.budget_stop_changed.count).padStart(7)}/${String(m.budget_stop_unchanged.count).padEnd(7)} | ${String(m.busy_refusal.count).padStart(4)} | ${queue.padEnd(21)} | ${day.telemetry.rowCount}`,
|
|
3529
3633
|
);
|
|
3530
3634
|
}
|
|
3531
3635
|
console.log("* = in-progress UTC day; never headline it.");
|
|
@@ -3794,6 +3898,32 @@ function printRewardReviewResult({ operation, data }) {
|
|
|
3794
3898
|
console.log(
|
|
3795
3899
|
`Decision recorded: ${operation.decision}. ${review.reason ? `Reason: ${review.reason}` : ""}`.trim(),
|
|
3796
3900
|
);
|
|
3901
|
+
if (review.published) {
|
|
3902
|
+
console.log(
|
|
3903
|
+
`Published on approval: version ${review.published.version} (artifact ${review.published.artifactVersionId}, ${review.published.transition}). The app is live now.`,
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
if (operation.name === "reward-review.propose") {
|
|
3908
|
+
console.log(
|
|
3909
|
+
`Proposal offered to the creator (${operation.fileCount} file(s) sent). They can accept (which publishes your version with these rules) or decline (which rejects the request).`,
|
|
3910
|
+
);
|
|
3911
|
+
}
|
|
3912
|
+
if (review.proposal) {
|
|
3913
|
+
const summary = review.proposal.diffSummary || {};
|
|
3914
|
+
console.log(
|
|
3915
|
+
`Proposal: ${summary.total ?? 0} file(s) changed (${summary.added ?? 0} added, ${summary.updated ?? 0} updated, ${summary.deleted ?? 0} deleted)${review.proposal.note ? ` · note: ${review.proposal.note}` : ""}${review.status === "changes_offered" ? " · waiting for the creator" : ""}`,
|
|
3916
|
+
);
|
|
3917
|
+
for (const file of review.proposal.changedFiles || [])
|
|
3918
|
+
console.log(` ${file.status}: ${file.path}`);
|
|
3919
|
+
}
|
|
3920
|
+
if (review.status === "rejected" && review.declinedByCreator) {
|
|
3921
|
+
console.log("The creator declined the proposed changes; the request is closed.");
|
|
3922
|
+
}
|
|
3923
|
+
if (review.status === "approved" && review.publishedArtifactVersionId) {
|
|
3924
|
+
console.log(
|
|
3925
|
+
`Approved and published (artifact version ${review.publishedArtifactVersionId}).`,
|
|
3926
|
+
);
|
|
3797
3927
|
}
|
|
3798
3928
|
if (Array.isArray(review.detectedRuleIds)) {
|
|
3799
3929
|
console.log(
|
|
@@ -3831,6 +3961,32 @@ function printRewardReviewResult({ operation, data }) {
|
|
|
3831
3961
|
console.log("Use --json for the full record.");
|
|
3832
3962
|
}
|
|
3833
3963
|
|
|
3964
|
+
function formatAdminCounts(counts) {
|
|
3965
|
+
return Object.entries(counts || {})
|
|
3966
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
3967
|
+
.map(([key, count]) => `${key} ${count}`)
|
|
3968
|
+
.join(" · ");
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
function printRewardReviewLifecycle(lifecycle) {
|
|
3972
|
+
// Absent from APIs older than the review-lifecycle telemetry.
|
|
3973
|
+
if (!lifecycle) return;
|
|
3974
|
+
const totals = lifecycle.totals || {};
|
|
3975
|
+
if (!Object.keys(totals.actions || {}).length) {
|
|
3976
|
+
console.log("Review lifecycle: no review events in the window.");
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
console.log(`Review lifecycle: ${formatAdminCounts(totals.actions)}.`);
|
|
3980
|
+
if (Object.keys(totals.thumbnails || {}).length)
|
|
3981
|
+
console.log(
|
|
3982
|
+
` automatic thumbnail after review publishes: ${formatAdminCounts(totals.thumbnails)}`,
|
|
3983
|
+
);
|
|
3984
|
+
if (Object.keys(totals.refusals || {}).length)
|
|
3985
|
+
console.log(
|
|
3986
|
+
` refused decisions (decision:code): ${formatAdminCounts(totals.refusals)}`,
|
|
3987
|
+
);
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3834
3990
|
function printRewardActivity(data) {
|
|
3835
3991
|
const apps = Array.isArray(data.apps) ? data.apps : [];
|
|
3836
3992
|
const suspects = Array.isArray(data.suspects) ? data.suspects : [];
|
|
@@ -3842,6 +3998,7 @@ function printRewardActivity(data) {
|
|
|
3842
3998
|
` app ${app.buildId} ${app.title}: ${app.claims} claim(s) · ${app.earners} earner(s) · ${app.xp} XP · ${app.coins} Coins · ${app.flagged} flagged`,
|
|
3843
3999
|
);
|
|
3844
4000
|
}
|
|
4001
|
+
printRewardReviewLifecycle(data.reviewLifecycle);
|
|
3845
4002
|
if (!suspects.length) {
|
|
3846
4003
|
console.log(
|
|
3847
4004
|
"Nothing unusual: no claim on the minimum time, no bursts, no sweeps, no repeated cap days, no guessing.",
|
package/lib/commands.js
CHANGED
|
@@ -2902,7 +2902,8 @@ export function printHelp() {
|
|
|
2902
2902
|
lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
|
|
2903
2903
|
lumine admin reward-activity [--date YYYY-MM-DD] [--days <1..31>] [--build <id>] [--json]
|
|
2904
2904
|
lumine admin reward-review show <review-id> [--dir <path>] [--json]
|
|
2905
|
-
lumine admin reward-review approve <review-id> [--config <rules.json>] [--reason <text>] [--json]
|
|
2905
|
+
lumine admin reward-review approve <review-id> [--config <rules.json>] [--reason <text>] [--json] (approval publishes the approved version)
|
|
2906
|
+
lumine admin reward-review propose <review-id> --dir <edited-snapshot> --config <rules.json> [--reason <text>] [--json]
|
|
2906
2907
|
lumine admin reward-review reject|revoke <review-id> --reason <text> [--json]
|
|
2907
2908
|
lumine admin recommendations list [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
2908
2909
|
lumine admin builds candidates [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--limit <number>] [--json]
|
|
@@ -3015,7 +3016,7 @@ Options:
|
|
|
3015
3016
|
--preview-url <url> Twinkle Build preview origin
|
|
3016
3017
|
--auth-file <path> Saved login path
|
|
3017
3018
|
--auth-token <token> Override saved login
|
|
3018
|
-
--dir <path> Directory for pulled project files or a reward-review
|
|
3019
|
+
--dir <path> Directory for pulled project files, a reward-review source snapshot, or the edited snapshot a reward-review proposal sends
|
|
3019
3020
|
--config <file> Replacement earning rules JSON for reward-review approve (default: the app's own proposal)
|
|
3020
3021
|
--provider <agent> Subscription agent for lumine agent: codex or claude-code
|
|
3021
3022
|
--provider-path <p> Override the selected agent CLI executable
|
package/lib/sdk.js
CHANGED
|
@@ -180,6 +180,16 @@ export const SDK_CLI_METHODS = {
|
|
|
180
180
|
readOnly: true,
|
|
181
181
|
mapArgs: (args) => ({ challengeId: args.challengeId }),
|
|
182
182
|
},
|
|
183
|
+
"rewards.getTimeline": {
|
|
184
|
+
path: "api/rewards/timeline", special: "rewards", operation: "timeline",
|
|
185
|
+
scopes: ["rewards:claim"], readOnly: true,
|
|
186
|
+
mapArgs: (args) => ({ ruleId: args.ruleId, cursor: args.cursor, limit: args.limit }),
|
|
187
|
+
},
|
|
188
|
+
"rewards.getArchivedProblem": {
|
|
189
|
+
path: "api/rewards/archived-problem", special: "rewards", operation: "archived-problem",
|
|
190
|
+
scopes: ["rewards:claim"], readOnly: true,
|
|
191
|
+
mapArgs: (args) => ({ receiptId: args.receiptId }),
|
|
192
|
+
},
|
|
183
193
|
"rewards.start": {
|
|
184
194
|
path: "api/rewards/start",
|
|
185
195
|
special: "rewards",
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Version: 1.45.0
|
|
4
4
|
Updated: 2026-09-14
|
|
5
|
-
Generated: 2026-09-
|
|
5
|
+
Generated: 2026-09-15T02:06:06.654Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -1037,6 +1037,17 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1037
1037
|
- 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.
|
|
1038
1038
|
- available says which boards this app's approved rules can pay: show a Coins board only when available.coins is true (an app whose rules pay XP only has no Coins standings). me is the signed-in viewer's own standing even when they fall outside the page, or null when they earned nothing in the period.
|
|
1039
1039
|
- Drafts and previews return mode 'preview' with no entries. Use Twinkle.leaderboards for app-defined scores; use this for real XP and Coins earned.
|
|
1040
|
+
- await Twinkle.rewards.getTimeline({ ruleId?, cursor?, limit? } = {}) | scopes: rewards:claim
|
|
1041
|
+
- Returns: { mode: "live", dayKey, entries: [{ receiptId, ruleId, ruleTitle, setKey, title, promptPreview, dayKey, closedAt, solvedAt, solver: { userId, username }, firstSolver, xp, coins, attempt }], nextCursor } | { mode: "preview", entries: [], nextCursor: null, message }
|
|
1042
|
+
- Browse confirmed solves of retired until-earned quiz bounties, newest first.
|
|
1043
|
+
- A solve appears only after the next site reset (UTC midnight, 9 AM Korea). Today’s solves, unsolved sets, standing quizzes and dated quizzes are excluded. The server decides retirement; client dates cannot unlock content. Zero-reward correct solves are included.
|
|
1044
|
+
- One entry per solve receipt, with firstSolver identifying the first receipt for that rule and set across approved versions. limit defaults to 20, max 50; pass nextCursor unchanged for older solves and omit it when changing ruleId. Pages may have fewer entries when old question sheets cannot be recovered; continue while nextCursor is present.
|
|
1045
|
+
- No daily-claim limit applies to reading. Requires the current approved published runtime grant; previews return an empty timeline. Never awards or changes balances. Open a receipt with getArchivedProblem to load the original question and guide.
|
|
1046
|
+
- await Twinkle.rewards.getArchivedProblem({ receiptId }) | scopes: rewards:claim
|
|
1047
|
+
- Returns: { mode: "live", entry: <same solve entry as getTimeline>, questions: [{ prompt, hint?, guide? }] } | { mode: "preview", entry: null, questions: [], message }
|
|
1048
|
+
- Read a retired bounty’s original questions and guides from the approval attached to its solve receipt.
|
|
1049
|
+
- Use a receiptId returned by getTimeline. Both reads independently check retirement and app ownership; guessing an active or other app’s receipt cannot reveal its questions or guides. Returns build_reward_archive_unavailable (404) if unavailable.
|
|
1050
|
+
- Uses that receipt’s frozen approved sheet, never today’s edited question or the mutable draft. Answer keys and tolerances are never returned. Render the question first and offer Reveal guide for learning, without a reward-claim button.
|
|
1040
1051
|
|
|
1041
1052
|
## Examples
|
|
1042
1053
|
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -1018,6 +1018,34 @@ Mikey gives his go-ahead; a pending proposal is not a completed refresh. After
|
|
|
1018
1018
|
approval, execute the entire approved plan and verify it without asking for
|
|
1019
1019
|
each swap again. Never omit this section from a full-run report.
|
|
1020
1020
|
|
|
1021
|
+
### Full management report in Chrome
|
|
1022
|
+
|
|
1023
|
+
Mikey's standing delivery preference (2026-09-15): after a full daily run, open
|
|
1024
|
+
the **complete management report as a browsable localhost page in Chrome**.
|
|
1025
|
+
Do this as part of finishing the authorized run; a Markdown path in chat alone
|
|
1026
|
+
is insufficient, and no additional confirmation is needed to open the report.
|
|
1027
|
+
|
|
1028
|
+
1. Save the complete report as
|
|
1029
|
+
`/private/tmp/twinkle-daily-YYYY-MM-DD/daily-management-report.md`, using the
|
|
1030
|
+
run's Bangkok date. Include every required reporting section, coverage gap,
|
|
1031
|
+
pending decision and carryover. Reflect later owner decisions accurately.
|
|
1032
|
+
2. Render the entire Markdown into a readable HTML page with section links,
|
|
1033
|
+
usable tables and access to the original Markdown. Preserve complete
|
|
1034
|
+
appendices and flagged rows; navigation or collapsible details must not
|
|
1035
|
+
discard them. Use local assets so the report does not depend on a CDN.
|
|
1036
|
+
3. Serve the view on `127.0.0.1` using an available port. Expose only the HTML,
|
|
1037
|
+
its required assets and the report Markdown through a dedicated directory
|
|
1038
|
+
or explicit routes; do not serve the surrounding private evidence folder.
|
|
1039
|
+
Keep the server available after the response so Mikey can browse it.
|
|
1040
|
+
4. Open the localhost URL in Mikey's Chrome. Verify that the page renders,
|
|
1041
|
+
section navigation works, and the full report is accessible. Keep the
|
|
1042
|
+
report tab open. Include both the localhost URL and Markdown file link in
|
|
1043
|
+
the final response.
|
|
1044
|
+
|
|
1045
|
+
For a follow-up that only opens or updates an existing report, reuse that
|
|
1046
|
+
report and its existing tab/server where available. This does not authorize
|
|
1047
|
+
starting another management run or repeating unrelated daily duties.
|
|
1048
|
+
|
|
1021
1049
|
Creating an escalation belongs to the active run; acknowledging, annotating,
|
|
1022
1050
|
resolving, or reopening it does not. Use the run-independent `escalation`
|
|
1023
1051
|
commands after Mikey responds instead of starting a follow-up delegated run.
|
|
@@ -1065,9 +1093,16 @@ and answer keys from a private question sheet the creator's Lumine uploads with
|
|
|
1065
1093
|
readable by every player). **Send for review** freezes the code and proposes
|
|
1066
1094
|
`rewards.json` merged with the sheet. Approval is Mikey's decision: read the
|
|
1067
1095
|
frozen code, check that the amounts are right and that the app cannot be
|
|
1068
|
-
farmed, change anything that is wrong, approve.
|
|
1069
|
-
|
|
1070
|
-
|
|
1096
|
+
farmed, change anything that is wrong, approve. **Approval publishes** (since
|
|
1097
|
+
2026-09-15): the exact frozen snapshot goes live in the same transaction, with
|
|
1098
|
+
no Publish click by the creator; the app's previous release stays up until
|
|
1099
|
+
that commit lands. Instead of approving, the reviewer may **propose changes**:
|
|
1100
|
+
edit a copy of the frozen snapshot and offer it as the condition of approval.
|
|
1101
|
+
The creator sees every changed line and either accepts (the proposed version
|
|
1102
|
+
is approved and published) or declines (the request is rejected). Nothing in
|
|
1103
|
+
that flow joins the creator's team. These commands need no daily run and can
|
|
1104
|
+
be used whenever a request arrives (the reviewer also receives a DM card per
|
|
1105
|
+
request).
|
|
1071
1106
|
|
|
1072
1107
|
```bash
|
|
1073
1108
|
lumine admin reward-review list --json # pending (default)
|
|
@@ -1077,7 +1112,10 @@ lumine admin reward-review show 2 --json # summary + file si
|
|
|
1077
1112
|
lumine admin reward-review show 2 --dir /private/tmp/reward-review-2 --json
|
|
1078
1113
|
lumine admin reward-review approve 2 --json # approve exactly what the app proposed
|
|
1079
1114
|
lumine admin reward-review approve 2 --config rules.json \
|
|
1080
|
-
--reason "Halved the stage amounts" --json # approve with changes
|
|
1115
|
+
--reason "Halved the stage amounts" --json # approve with changes (publishes)
|
|
1116
|
+
lumine admin reward-review show 2 --dir /private/tmp/reward-review-2 --json # then edit that directory…
|
|
1117
|
+
lumine admin reward-review propose 2 --dir /private/tmp/reward-review-2 \
|
|
1118
|
+
--config rules.json --reason "Moved the claim after the stage clears" --json # …and offer it
|
|
1081
1119
|
lumine admin reward-review reject 2 --reason "Rewards fire on game over; nothing is earned" --json
|
|
1082
1120
|
lumine admin reward-review revoke 2 --reason "Farmable; pausing until redesigned" --json
|
|
1083
1121
|
```
|
|
@@ -1156,12 +1194,49 @@ Arcade Typing (Mikey, 2026-09-12): XP for clearing campaign stages, up to
|
|
|
1156
1194
|
10,000 Coins per rule and per learner per day, 10,000,000 XP / 1,000,000 Coins
|
|
1157
1195
|
per app per day, 1,000,000,000 XP / 100,000,000 Coins per app lifetime.
|
|
1158
1196
|
|
|
1159
|
-
Approval freezes these rules with the reviewed snapshot
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
`
|
|
1197
|
+
Approval freezes these rules with the reviewed snapshot and publishes that
|
|
1198
|
+
snapshot immediately (the result carries `published.version`); an approval
|
|
1199
|
+
without at least one rule is refused, and an approval whose creator has saved
|
|
1200
|
+
past the frozen version is refused as `build_reward_review_stale` (the request
|
|
1201
|
+
also closes itself on that save). Rejection and revocation require a
|
|
1202
|
+
`--reason` the creator reads verbatim in their workspace. A later code save
|
|
1203
|
+
needs a new request. Never approve without reading the code; never approve a
|
|
1204
|
+
request whose `isLatest` is false.
|
|
1205
|
+
|
|
1206
|
+
`propose <id> --dir <edited> --config rules.json [--reason]` sends the edited
|
|
1207
|
+
directory (text files only; dotfiles and tool folders skipped) as the
|
|
1208
|
+
reviewer's proposal: the review moves to `changes_offered`, the creator's card
|
|
1209
|
+
and workspace show the note and every changed line, and the creator's
|
|
1210
|
+
**Accept & go live** publishes exactly those files with these rules (their
|
|
1211
|
+
workspace is replaced by the accepted version). **No thanks** rejects the
|
|
1212
|
+
request (`declinedByCreator: true`). A proposal must still use the rewards
|
|
1213
|
+
SDK, must differ from the submitted snapshot, and is refused once the creator
|
|
1214
|
+
saves past the submitted version. Offering again replaces the earlier offer;
|
|
1215
|
+
approving or rejecting while an offer is out decides the request as
|
|
1216
|
+
submitted. The website equivalent is the Management panel's "Edit a copy to
|
|
1217
|
+
propose changes" (a private workspace copy owned by the reviewer) followed by
|
|
1218
|
+
"Offer my copy with these rules".
|
|
1219
|
+
|
|
1220
|
+
Each offer has a server-owned revision. Changing the files, rules or note
|
|
1221
|
+
creates a new revision; a creator looking at an older comparison or decline
|
|
1222
|
+
confirmation cannot answer the replacement offer. The creator sees its reward
|
|
1223
|
+
amounts as well as its file changes. Proposed rules stay separate from the
|
|
1224
|
+
submitted rules until acceptance, so `approve` without `--config` still uses
|
|
1225
|
+
the original submitted configuration. The CLI audits the offer atomically
|
|
1226
|
+
and includes file contents in its retry fingerprint.
|
|
1227
|
+
|
|
1228
|
+
Approval also attempts a free preview thumbnail when the app has none. That
|
|
1229
|
+
capture uses the published version and cannot overwrite a later release or a
|
|
1230
|
+
thumbnail the creator chooses while it runs. It is best effort: a capture
|
|
1231
|
+
failure leaves publication successful and does not spend AI-image credits.
|
|
1232
|
+
|
|
1233
|
+
The review copy carries independent copies of referenced uploaded media.
|
|
1234
|
+
Before freezing an offer, the server reuses the creator's original media and
|
|
1235
|
+
copies new reviewer media into the creator's library within their storage
|
|
1236
|
+
quota. Its final URLs are included in the comparison, so acceptance publishes
|
|
1237
|
+
those exact files and does not depend on keeping the review copy. Re-offers
|
|
1238
|
+
reuse the media; a failed transaction cleans up its copied objects. Declining
|
|
1239
|
+
leaves the offered media as unused uploads in the creator's library.
|
|
1165
1240
|
|
|
1166
1241
|
### Reward activity report (standing duty, every full daily review; added 2026-09-12)
|
|
1167
1242
|
|
|
@@ -1192,6 +1267,24 @@ open the player with `admin identity inspect` before proposing anything, and
|
|
|
1192
1267
|
propose to Mikey (revoke the app's rule, or a bucket ban) rather than acting.
|
|
1193
1268
|
Never revoke an approval from a daily run.
|
|
1194
1269
|
|
|
1270
|
+
The same report carries `reviewLifecycle` (added 2026-09-15): the reviews'
|
|
1271
|
+
audit trail counted per UTC day (`byDay`) and for the window (`totals`).
|
|
1272
|
+
`actions` counts every event: `request`, `approve`/`reject`/`revoke`,
|
|
1273
|
+
`publish` (the approved version went live), `propose`, `proposal_viewed` (the
|
|
1274
|
+
creator opened an offer's comparison; once per revision), `accept`/`decline`,
|
|
1275
|
+
`thumbnail` and `<decision>_refused`. `thumbnails` is the automatic thumbnail
|
|
1276
|
+
after a review publish: `captured`, `existing` (the app already had one),
|
|
1277
|
+
`superseded` (the creator's own thumbnail or a newer release won),
|
|
1278
|
+
`not_needed`, `owner_missing` or `failed`. `refusals` counts conflicts that
|
|
1279
|
+
rolled back, keyed `decision:code` (for example
|
|
1280
|
+
`approve:build_reward_review_stale`, `accept:build_reward_proposal_stale`).
|
|
1281
|
+
Report the totals. Each `failed` thumbnail and each `publish` without a
|
|
1282
|
+
`thumbnail` event on a completed day is a carry-over todo with the review
|
|
1283
|
+
ids (`reward-review show <id>` lists its events). An `accept` without a
|
|
1284
|
+
`proposal_viewed` for that revision means the creator accepted without opening
|
|
1285
|
+
the comparison; mention it, it is not a fault. Refusals are the concurrency
|
|
1286
|
+
guards working; report them, and escalate only a repeated pattern on one app.
|
|
1287
|
+
|
|
1195
1288
|
## Private carry-over todos
|
|
1196
1289
|
|
|
1197
1290
|
```bash
|
|
@@ -2226,6 +2319,21 @@ type PostSkip = Success<{
|
|
|
2226
2319
|
|
|
2227
2320
|
## Twinkle Newspaper
|
|
2228
2321
|
|
|
2322
|
+
**Audience (Mikey, 2026-09-15): children and young Twinkle users aged 10–15.**
|
|
2323
|
+
Choose and write stories for what these readers would voluntarily spend time
|
|
2324
|
+
reading. Before selecting a story, identify its appeal to them: curiosity,
|
|
2325
|
+
humor, a relatable experience, a creative idea, something useful, or a chance
|
|
2326
|
+
to join in. Games, art, puzzles, friendships, shared reflections, and community
|
|
2327
|
+
discussions can all supply good stories; read the actual source to find the
|
|
2328
|
+
substance.
|
|
2329
|
+
|
|
2330
|
+
Use clear, lively language that respects readers' intelligence. Give enough
|
|
2331
|
+
context for someone who missed the original post, and make the interesting
|
|
2332
|
+
part clear in the headline and opening. Avoid talking down to readers, forced
|
|
2333
|
+
slang, preachy lessons, administrative summaries, and blurbs that merely say
|
|
2334
|
+
someone uploaded or posted something. Keep the appeal grounded in the source;
|
|
2335
|
+
never invent excitement, reactions, popularity, or drama.
|
|
2336
|
+
|
|
2229
2337
|
```bash
|
|
2230
2338
|
lumine admin news --json
|
|
2231
2339
|
lumine admin news claim --output claim.json --scaffold editorial.json --json
|
|
@@ -2264,6 +2372,7 @@ re-checked transactionally at commit.
|
|
|
2264
2372
|
|
|
2265
2373
|
```ts
|
|
2266
2374
|
type GeneratedEditorial = {
|
|
2375
|
+
excludedSubjectEventKeys?: string[]; // experiment-video Subjects omitted entirely
|
|
2267
2376
|
mastheadHeadline: string;
|
|
2268
2377
|
mastheadDeck: string;
|
|
2269
2378
|
lead: {
|
|
@@ -2289,6 +2398,20 @@ nothing disappears silently: digest events the editorial does not account for
|
|
|
2289
2398
|
are added back. Two mechanisms make real curation possible within that
|
|
2290
2399
|
guarantee:
|
|
2291
2400
|
|
|
2401
|
+
**Exception — experiment videos (Mikey, 2026-09-15):** exclude these entirely,
|
|
2402
|
+
including school science-contest entries. The editor/model identifies them from
|
|
2403
|
+
the supplied context and lists their exact keys in `excludedSubjectEventKeys`.
|
|
2404
|
+
Do not cite, summarize, or group them into newspaper coverage. The API must have
|
|
2405
|
+
this exclusion support deployed before submitting such an editorial; older APIs
|
|
2406
|
+
ignore the field and restore the posts. The server accepts only canonical Subject
|
|
2407
|
+
keys and never lets this field remove official announcements. After excluding
|
|
2408
|
+
these posts, look for worthwhile replacement stories among the edition's eligible
|
|
2409
|
+
Subjects and shared Daily Reflections. A bounded digest dominated by experiment
|
|
2410
|
+
videos does not establish that the day has no other stories. Check the available
|
|
2411
|
+
canonical sources within the coverage window, and keep replacement stories within
|
|
2412
|
+
the claim and citation contract. Do not stop at deletion when suitable material
|
|
2413
|
+
is available, or invent filler to reach an article count.
|
|
2414
|
+
|
|
2292
2415
|
- **`coveredEventKeys`** — an arc story may list the other events it narrates
|
|
2293
2416
|
(an app's release + its update stream + its open-sourcing; one member's
|
|
2294
2417
|
related posts). Covered events are omitted from the layout — the arc IS
|
|
@@ -2321,17 +2444,17 @@ within the returned digest. On a typical day every front subject arrives with
|
|
|
2321
2444
|
the same priority, so treat a tied score (or recency) as no signal at all and
|
|
2322
2445
|
make the call by reading:
|
|
2323
2446
|
|
|
2324
|
-
- **Choose the lead
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2447
|
+
- **Choose the lead for readers aged 10–15.** Lead with the eligible front
|
|
2448
|
+
event whose substance is most likely to catch their interest and reward
|
|
2449
|
+
reading. A thoughtful conversation, a funny or relatable reflection, a
|
|
2450
|
+
striking creation, or an inviting community challenge can all qualify.
|
|
2451
|
+
Read the source and any supplied replies to understand the appeal; priority,
|
|
2452
|
+
recency, and the mere presence of an argument do not decide the lead.
|
|
2330
2453
|
- **Thread a theme through the paper.** Pick the strongest idea of the day
|
|
2331
2454
|
and let the masthead, the lead, and the editor's note all carry it, with
|
|
2332
|
-
the editor's note
|
|
2333
|
-
|
|
2334
|
-
|
|
2455
|
+
the editor's note leaving readers with an observation, question, or invitation
|
|
2456
|
+
grounded in one of the day's posts. Let a theme emerge from the material;
|
|
2457
|
+
keep each story's meaning intact and avoid forcing a moral lesson.
|
|
2335
2458
|
- **Cross-reference events into arcs.** The same thing often appears in the
|
|
2336
2459
|
digest several times (an app's release, its open-sourcing, and its maker's
|
|
2337
2460
|
Daily Reflection about it). Write those as one story arc — the origin
|
|
@@ -2606,14 +2729,14 @@ The additive host-owner migration and compatible API must be live before this
|
|
|
2606
2729
|
CLI capability is published.
|
|
2607
2730
|
|
|
2608
2731
|
Review every participating host, including primary private-helper logs. A
|
|
2609
|
-
primary review does not cover the target.
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2732
|
+
primary review does not cover the target. An open review does not block a
|
|
2733
|
+
deployment or host hold. Its files, lease and database boundaries persist;
|
|
2734
|
+
active requests use the normal drain. A held or unavailable owner returns a
|
|
2735
|
+
retryable failure, so keep the session and retry when that host is available
|
|
2736
|
+
again. Release operators can review final shutdown deltas via management SSH
|
|
2737
|
+
and record their own evidence. An active review keeps ownership of clearing;
|
|
2738
|
+
otherwise API stderr is cleared with the existing guarded
|
|
2739
|
+
`npm run logs:clear-errors` plus post-clear re-read. A stopped target whose final logs
|
|
2617
2740
|
were reviewed does not need to be started for daily management; starting EC2
|
|
2618
2741
|
requires separate authority. See `twinkle-api/DEPLOY_TIME_HANDOFF.md`.
|
|
2619
2742
|
|
|
@@ -2988,9 +3111,16 @@ energy ledger (`chargedUsd`, `overflowUsd`, `users`, `recharges`; 1,000,000
|
|
|
2988
3111
|
units = $1), every telemetry counter (`busy_refusal`, `autofix_yielded`,
|
|
2989
3112
|
`autofix_superseded`, `reservation_admitted` with `avgRunBudgetUsd`,
|
|
2990
3113
|
`budget_stop_changed` / `budget_stop_unchanged` / `run_completed` with a
|
|
2991
|
-
per-model breakdown, `stop_settled`, `tool_limit_settled
|
|
2992
|
-
|
|
2993
|
-
|
|
3114
|
+
per-model breakdown, `stop_settled`, `tool_limit_settled`, and since
|
|
3115
|
+
2026-09-15 the queued-request counters `queued_restored` (a workspace reload
|
|
3116
|
+
restored its owner's still-queued request, shown with Stop), `queued_stopped`
|
|
3117
|
+
(Stop cancelled a request while it was still queued) and
|
|
3118
|
+
`busy_resume_requested` (after a busy refusal the website resumed the
|
|
3119
|
+
existing request)) and per-model per-run usage stats (`runs`, `callsPerRun`,
|
|
3120
|
+
`usdPerRun`, each avg and nearest-rank p90). `busy_refusal` with no
|
|
3121
|
+
`busy_resume_requested` on a day with website traffic means clients are not
|
|
3122
|
+
resuming the refused request; `queued_restored` shows how often creators
|
|
3123
|
+
reload while waiting in the queue. The current UTC day is returned with `inProgress: true`.
|
|
2994
3124
|
**Headline `lastCompletedDay` (its exact `dayKey`) — never the in-progress
|
|
2995
3125
|
day**, exactly as the closed-day AI-cost duty does.
|
|
2996
3126
|
|