@stage5/lumine 0.2.78 → 0.2.79
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 +127 -1
- package/lib/commands.js +3 -2
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +57 -10
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
|
|
|
@@ -3794,6 +3894,32 @@ function printRewardReviewResult({ operation, data }) {
|
|
|
3794
3894
|
console.log(
|
|
3795
3895
|
`Decision recorded: ${operation.decision}. ${review.reason ? `Reason: ${review.reason}` : ""}`.trim(),
|
|
3796
3896
|
);
|
|
3897
|
+
if (review.published) {
|
|
3898
|
+
console.log(
|
|
3899
|
+
`Published on approval: version ${review.published.version} (artifact ${review.published.artifactVersionId}, ${review.published.transition}). The app is live now.`,
|
|
3900
|
+
);
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
if (operation.name === "reward-review.propose") {
|
|
3904
|
+
console.log(
|
|
3905
|
+
`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).`,
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
if (review.proposal) {
|
|
3909
|
+
const summary = review.proposal.diffSummary || {};
|
|
3910
|
+
console.log(
|
|
3911
|
+
`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" : ""}`,
|
|
3912
|
+
);
|
|
3913
|
+
for (const file of review.proposal.changedFiles || [])
|
|
3914
|
+
console.log(` ${file.status}: ${file.path}`);
|
|
3915
|
+
}
|
|
3916
|
+
if (review.status === "rejected" && review.declinedByCreator) {
|
|
3917
|
+
console.log("The creator declined the proposed changes; the request is closed.");
|
|
3918
|
+
}
|
|
3919
|
+
if (review.status === "approved" && review.publishedArtifactVersionId) {
|
|
3920
|
+
console.log(
|
|
3921
|
+
`Approved and published (artifact version ${review.publishedArtifactVersionId}).`,
|
|
3922
|
+
);
|
|
3797
3923
|
}
|
|
3798
3924
|
if (Array.isArray(review.detectedRuleIds)) {
|
|
3799
3925
|
console.log(
|
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/package.json
CHANGED
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -1065,9 +1065,16 @@ and answer keys from a private question sheet the creator's Lumine uploads with
|
|
|
1065
1065
|
readable by every player). **Send for review** freezes the code and proposes
|
|
1066
1066
|
`rewards.json` merged with the sheet. Approval is Mikey's decision: read the
|
|
1067
1067
|
frozen code, check that the amounts are right and that the app cannot be
|
|
1068
|
-
farmed, change anything that is wrong, approve.
|
|
1069
|
-
|
|
1070
|
-
|
|
1068
|
+
farmed, change anything that is wrong, approve. **Approval publishes** (since
|
|
1069
|
+
2026-09-15): the exact frozen snapshot goes live in the same transaction, with
|
|
1070
|
+
no Publish click by the creator; the app's previous release stays up until
|
|
1071
|
+
that commit lands. Instead of approving, the reviewer may **propose changes**:
|
|
1072
|
+
edit a copy of the frozen snapshot and offer it as the condition of approval.
|
|
1073
|
+
The creator sees every changed line and either accepts (the proposed version
|
|
1074
|
+
is approved and published) or declines (the request is rejected). Nothing in
|
|
1075
|
+
that flow joins the creator's team. These commands need no daily run and can
|
|
1076
|
+
be used whenever a request arrives (the reviewer also receives a DM card per
|
|
1077
|
+
request).
|
|
1071
1078
|
|
|
1072
1079
|
```bash
|
|
1073
1080
|
lumine admin reward-review list --json # pending (default)
|
|
@@ -1077,7 +1084,10 @@ lumine admin reward-review show 2 --json # summary + file si
|
|
|
1077
1084
|
lumine admin reward-review show 2 --dir /private/tmp/reward-review-2 --json
|
|
1078
1085
|
lumine admin reward-review approve 2 --json # approve exactly what the app proposed
|
|
1079
1086
|
lumine admin reward-review approve 2 --config rules.json \
|
|
1080
|
-
--reason "Halved the stage amounts" --json # approve with changes
|
|
1087
|
+
--reason "Halved the stage amounts" --json # approve with changes (publishes)
|
|
1088
|
+
lumine admin reward-review show 2 --dir /private/tmp/reward-review-2 --json # then edit that directory…
|
|
1089
|
+
lumine admin reward-review propose 2 --dir /private/tmp/reward-review-2 \
|
|
1090
|
+
--config rules.json --reason "Moved the claim after the stage clears" --json # …and offer it
|
|
1081
1091
|
lumine admin reward-review reject 2 --reason "Rewards fire on game over; nothing is earned" --json
|
|
1082
1092
|
lumine admin reward-review revoke 2 --reason "Farmable; pausing until redesigned" --json
|
|
1083
1093
|
```
|
|
@@ -1156,12 +1166,49 @@ Arcade Typing (Mikey, 2026-09-12): XP for clearing campaign stages, up to
|
|
|
1156
1166
|
10,000 Coins per rule and per learner per day, 10,000,000 XP / 1,000,000 Coins
|
|
1157
1167
|
per app per day, 1,000,000,000 XP / 100,000,000 Coins per app lifetime.
|
|
1158
1168
|
|
|
1159
|
-
Approval freezes these rules with the reviewed snapshot
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
`
|
|
1169
|
+
Approval freezes these rules with the reviewed snapshot and publishes that
|
|
1170
|
+
snapshot immediately (the result carries `published.version`); an approval
|
|
1171
|
+
without at least one rule is refused, and an approval whose creator has saved
|
|
1172
|
+
past the frozen version is refused as `build_reward_review_stale` (the request
|
|
1173
|
+
also closes itself on that save). Rejection and revocation require a
|
|
1174
|
+
`--reason` the creator reads verbatim in their workspace. A later code save
|
|
1175
|
+
needs a new request. Never approve without reading the code; never approve a
|
|
1176
|
+
request whose `isLatest` is false.
|
|
1177
|
+
|
|
1178
|
+
`propose <id> --dir <edited> --config rules.json [--reason]` sends the edited
|
|
1179
|
+
directory (text files only; dotfiles and tool folders skipped) as the
|
|
1180
|
+
reviewer's proposal: the review moves to `changes_offered`, the creator's card
|
|
1181
|
+
and workspace show the note and every changed line, and the creator's
|
|
1182
|
+
**Accept & go live** publishes exactly those files with these rules (their
|
|
1183
|
+
workspace is replaced by the accepted version). **No thanks** rejects the
|
|
1184
|
+
request (`declinedByCreator: true`). A proposal must still use the rewards
|
|
1185
|
+
SDK, must differ from the submitted snapshot, and is refused once the creator
|
|
1186
|
+
saves past the submitted version. Offering again replaces the earlier offer;
|
|
1187
|
+
approving or rejecting while an offer is out decides the request as
|
|
1188
|
+
submitted. The website equivalent is the Management panel's "Edit a copy to
|
|
1189
|
+
propose changes" (a private workspace copy owned by the reviewer) followed by
|
|
1190
|
+
"Offer my copy with these rules".
|
|
1191
|
+
|
|
1192
|
+
Each offer has a server-owned revision. Changing the files, rules or note
|
|
1193
|
+
creates a new revision; a creator looking at an older comparison or decline
|
|
1194
|
+
confirmation cannot answer the replacement offer. The creator sees its reward
|
|
1195
|
+
amounts as well as its file changes. Proposed rules stay separate from the
|
|
1196
|
+
submitted rules until acceptance, so `approve` without `--config` still uses
|
|
1197
|
+
the original submitted configuration. The CLI audits the offer atomically
|
|
1198
|
+
and includes file contents in its retry fingerprint.
|
|
1199
|
+
|
|
1200
|
+
Approval also attempts a free preview thumbnail when the app has none. That
|
|
1201
|
+
capture uses the published version and cannot overwrite a later release or a
|
|
1202
|
+
thumbnail the creator chooses while it runs. It is best effort: a capture
|
|
1203
|
+
failure leaves publication successful and does not spend AI-image credits.
|
|
1204
|
+
|
|
1205
|
+
The review copy carries independent copies of referenced uploaded media.
|
|
1206
|
+
Before freezing an offer, the server reuses the creator's original media and
|
|
1207
|
+
copies new reviewer media into the creator's library within their storage
|
|
1208
|
+
quota. Its final URLs are included in the comparison, so acceptance publishes
|
|
1209
|
+
those exact files and does not depend on keeping the review copy. Re-offers
|
|
1210
|
+
reuse the media; a failed transaction cleans up its copied objects. Declining
|
|
1211
|
+
leaves the offered media as unused uploads in the creator's library.
|
|
1165
1212
|
|
|
1166
1213
|
### Reward activity report (standing duty, every full daily review; added 2026-09-12)
|
|
1167
1214
|
|