@stage5/lumine 0.3.74 → 0.3.76

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.
@@ -112,6 +112,13 @@ export function buildArenaOperation({ action, options }) {
112
112
  }
113
113
  if (action === "import") {
114
114
  const additive = Boolean(options.adminArenaAdditive);
115
+ if (!options.adminFile && !options.adminDir) {
116
+ throw validationError(
117
+ additive
118
+ ? "--additive needs --file <reviewed-items.json> or --dir <folder>; it never reads the current folder."
119
+ : ARENA_USAGE,
120
+ );
121
+ }
115
122
  if ((options.adminFile && options.adminDir) || (options.adminFile && !additive)) {
116
123
  throw validationError("Use --dir for a complete paper, or --additive with exactly one of --file or --dir for a refill.");
117
124
  }
package/lib/admin.js CHANGED
@@ -3063,9 +3063,15 @@ export function parseAdminOperation(options) {
3063
3063
  const raw = readAdminJsonFile(target, "<reviewed-definitions.json>", { maxBytes: 512 * 1024 });
3064
3064
  const items = Array.isArray(raw) ? raw : raw?.items;
3065
3065
  if (!Array.isArray(items) || !items.length || items.length > 100) throw cliValidationError("Review 1 to 100 Korean definitions, keeping exported word IDs, revisions and source hashes.");
3066
- return writeOperation("dictionary.korean-review", "POST", "/cli/admin/dictionary/korean/review", { items, dryRun: Boolean(options.adminDryRun) }, { requiresRun: false, mutates: !options.adminDryRun, timeoutMs: 120000 });
3066
+ // A preview (dry run) unless --confirm, like arena import and today-import.
3067
+ if (options.adminConfirm && options.adminConfirm !== "true") {
3068
+ throw cliValidationError("Pass --confirm on its own, without a value.");
3069
+ }
3070
+ const confirm = options.adminConfirm === "true";
3071
+ if (confirm && options.adminDryRun) throw cliValidationError("Choose --confirm or --dry-run, not both.");
3072
+ return writeOperation("dictionary.korean-review", "POST", "/cli/admin/dictionary/korean/review", { items, dryRun: !confirm }, { requiresRun: false, mutates: confirm, timeoutMs: 120000 });
3067
3073
  }
3068
- throw cliValidationError("Use: lumine admin dictionary korean-status | korean-pending [--cursor <time:id>] [--limit <1-100>] | korean-review <reviewed-definitions.json> [--dry-run]");
3074
+ throw cliValidationError("Use: lumine admin dictionary korean-status | korean-pending [--cursor <time:id>] [--limit <1-100>] | korean-review <reviewed-definitions.json> [--confirm]");
3069
3075
  }
3070
3076
  if (namespace === "word-games") {
3071
3077
  if (action === "status") return readOperation("word-games.status", "/cli/admin/word-games/status", { requiresRun: false });
@@ -3173,7 +3179,7 @@ export function parseAdminOperation(options) {
3173
3179
  // as the website. Check-ins provide evidence; every meetup completion
3174
3180
  // waits for Mikey's decision (2026-10-22).
3175
3181
  const usage =
3176
- "Usage: lumine admin meetup list [--status review|active|completed|all] | show <crewId> | review-completion <crewId> --decision approve|reject --fingerprint <review fingerprint> | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch <name> [--note <text>] | branches [list|show|approve|reject|merge] | emails [list|send <id> [--fingerprint <revision>]|discard <id>|review on|off] | friends [status|on|off] | funnel [--days <1-90>] | story ... | announce-checkins [--send] [--as zero|ciel].";
3182
+ "Usage: lumine admin meetup list [--status review|active|completed|all] | show <crewId> | review-completion <crewId> --decision approve|reject --fingerprint <review fingerprint> | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch <name> [--note <text>] | branches [list|show|approve|reject|merge] | emails [list|send <id> --fingerprint <revision>|discard <id>|review on|off] | friends [status|on|off] | funnel [--days <1-90>] | story ... | announce-checkins [--send] [--as zero|ciel].";
3177
3183
  const note = String(options.note || "").trim();
3178
3184
  if (note.length > 1000) {
3179
3185
  throw cliValidationError("--note must be at most 1000 characters.");
@@ -3279,11 +3285,18 @@ export function parseAdminOperation(options) {
3279
3285
  }
3280
3286
  if (sub === "send" || sub === "discard") {
3281
3287
  const id = parseRequiredInteger(extra, "Email ID", 1);
3282
- const revision = sub === "send" ? String(options.adminFingerprint || "").trim() : "";
3288
+ // A send names the exact draft the admin reviewed, so a draft that
3289
+ // changed after review is never sent unseen.
3290
+ const revision = sub === "send" ? String(options.adminFingerprint || "").trim().toLowerCase() : "";
3291
+ if (sub === "send" && !revision) {
3292
+ throw cliValidationError(
3293
+ "Sending a parent email needs --fingerprint <revision>. Run lumine admin meetup emails list, review the draft, then send it with the fingerprint printed beside it.",
3294
+ );
3295
+ }
3283
3296
  if (revision && !/^[0-9a-f]{64}$/.test(revision)) throw cliValidationError("Use --fingerprint from the reviewed email in meetup emails list.");
3284
3297
  return writeOperation("meetup.emails", "POST", `/cli/admin/meetup-quest/emails/${id}/${sub}`, revision ? { revision } : {}, { requiresRun: false });
3285
3298
  }
3286
- throw cliValidationError("lumine admin meetup emails [list | send <id> | discard <id> | review on|off]");
3299
+ throw cliValidationError("lumine admin meetup emails [list | send <id> --fingerprint <revision> | discard <id> | review on|off]");
3287
3300
  }
3288
3301
  if (action === "friends") {
3289
3302
  const sub = target || "status";
package/lib/commands.js CHANGED
@@ -2545,6 +2545,17 @@ export function parseArgs(args) {
2545
2545
  ? `${raw.branch},${inlineValue}`
2546
2546
  : inlineValue;
2547
2547
  } else if (booleanFlags.has(camelKey)) {
2548
+ // A switch never takes a spaced value, so `--live false` would turn the
2549
+ // switch on and leave "false" behind. Refuse it rather than guess.
2550
+ const nextValue = rest[i + 1];
2551
+ if (
2552
+ nextValue !== undefined &&
2553
+ ["false", "no", "off", "0"].includes(nextValue.trim().toLowerCase())
2554
+ ) {
2555
+ throw new Error(
2556
+ `--${key} is a switch and does not take "${nextValue}" after a space. Leave --${key} out (or write --${key}=false) to turn it off.`,
2557
+ );
2558
+ }
2548
2559
  raw[camelKey] = true;
2549
2560
  } else if (
2550
2561
  optionalValueFlags.has(camelKey) &&
@@ -2683,7 +2694,7 @@ export function parseArgs(args) {
2683
2694
  adminSinceRun: Boolean(raw.sinceRun),
2684
2695
  adminSince: raw.since ? String(raw.since) : "",
2685
2696
  adminUntil: raw.until ? String(raw.until) : "",
2686
- adminIncludeSynthetic: Boolean(raw.includeSynthetic),
2697
+ adminIncludeSynthetic: parseBoolean(raw.includeSynthetic, false),
2687
2698
  adminPath: raw.path ? String(raw.path) : "",
2688
2699
  adminPerf: Boolean(raw.perf),
2689
2700
  adminWake: Boolean(raw.wake),
@@ -3319,7 +3330,7 @@ export function printHelp() {
3319
3330
  lumine admin teachers audit [--limit <1-100>] [--cursor <c>] [--all] | revoke <userId> --reason <text> (approved teachers, a page at a time or --all: new flags first, then changed since your review; revoke removes the status cleanly)
3320
3331
  lumine admin teachers branches suggest | networks | confirm <networkId> --branch <name> [--branch <name>] [--note <text>] | unconfirm <networkId> | set <userId|username> --branch <name> [--note <text>] | clear <userId|username> | apply | list [--branch <name>|none] (which branch each teacher works at, confirmed by you through the academy networks they use: suggest finds the networks, confirm names their branch (repeat --branch for a building branches share), apply gives teachers the branch most of their actions there point at and lists the unclear ones; set wins over networks)
3321
3332
  lumine admin teachers review <userId> --decision legit|revoke --note <text> [--fingerprint <fp>, required for revoke] | review --all-flagged --expect <id[:fp],...> --note <text> (record your review of the facts shown; --all-flagged records only the --expect teachers; nothing is recorded if they changed since. A reviewed teacher comes back when its flags or linked accounts change; accounts sharing a network count only when they change a network flag)
3322
- lumine admin dictionary korean-status | korean-pending [--cursor <time:id>] [--limit <1-100>] | korean-review <reviewed-definitions.json> [--dry-run] (shared Korean word-bank definitions; daily review, no paid generation)
3333
+ lumine admin dictionary korean-status | korean-pending [--cursor <time:id>] [--limit <1-100>] | korean-review <reviewed-definitions.json> [--confirm] (shared Korean word-bank definitions; daily review, no paid generation; a preview unless --confirm)
3323
3334
  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)
3324
3335
  lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
3325
3336
  lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
package/lib/workspace.js CHANGED
@@ -8,9 +8,11 @@ import {
8
8
  EXCLUDED_UPLOAD_FILES,
9
9
  LUMINE_AGENT_INSTRUCTIONS,
10
10
  LUMINE_AGENT_INSTRUCTIONS_MARKER,
11
+ LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER,
11
12
  LUMINE_REFERENCE_INSTRUCTIONS,
12
13
  LUMINE_REFERENCE_INSTRUCTIONS_MARKER,
13
14
  LUMINE_SDK_REFERENCE_MARKER,
15
+ LUMINE_VERSION_CHECKOUT_INSTRUCTIONS_MARKER,
14
16
  PROJECT_EFFECTIVE_LINE_MAX_COLUMNS,
15
17
  PROJECT_MAX_EFFECTIVE_FILE_LINES,
16
18
  PROJECT_MAX_FILES,
@@ -39,14 +41,23 @@ export async function writeReferenceInstructions({ dir }) {
39
41
  });
40
42
  }
41
43
 
42
- // Write AGENTS.md/CLAUDE.md, but never clobber a file the user customized:
43
- // only (re)write when the file is absent or still carries our marker.
44
+ // All checkout modes generate these same two files. A previous mode's header
45
+ // still identifies a CLI-managed guide and must change with the checkout.
46
+ // Custom guides omit the generated header; quoting a marker later is not ownership.
44
47
  export async function writeInstructionFiles({ dir, marker, content }) {
48
+ const managedHeaders = new Set([
49
+ marker,
50
+ LUMINE_AGENT_INSTRUCTIONS_MARKER,
51
+ LUMINE_REFERENCE_INSTRUCTIONS_MARKER,
52
+ LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER,
53
+ LUMINE_VERSION_CHECKOUT_INSTRUCTIONS_MARKER,
54
+ ]);
45
55
  for (const fileName of AGENT_INSTRUCTION_FILES) {
46
56
  const filePath = path.join(dir, fileName);
47
57
  try {
48
58
  const existing = await fs.readFile(filePath, "utf8");
49
- if (!existing.includes(marker)) {
59
+ const header = existing.replace(/^\uFEFF/, "").split(/\r?\n/, 1)[0];
60
+ if (!managedHeaders.has(header)) {
50
61
  continue;
51
62
  }
52
63
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.74",
3
+ "version": "0.3.76",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,8 @@
1
1
  # Build SDK Index
2
2
 
3
- Version: 1.74.0
4
- Updated: 2026-10-29
5
- Generated: 2026-10-28T20:08:43.889Z
3
+ Version: 1.75.0
4
+ Updated: 2026-10-31
5
+ Generated: 2026-10-31T07:54:50.319Z
6
6
 
7
7
  ## Platform rules
8
8
  Each rule below is a way an app breaks once it runs inside Twinkle's sandboxed frame, even when the code itself is correct. They outrank every other guideline: follow them in every change, including small ones, and fix any break of them you see in code you touch.
@@ -32,6 +32,7 @@ Features every app needs whenever they apply. When you build a new app or make a
32
32
  - Reward is part of completion: a reward-paying run must stay pending until awarded:true and its canonical receipt (or getReceipt status awarded). Do not show final solved/completed status, advance rewarded progress, or discard finished proof before that. Preserve the exact challenge/proof and acknowledged progress in a durable per-viewer outbox; reconcile getReceipt after a lost reply and retry the same challenge, never inventing a payment. Explain explicit practice, already collected, exhausted approved limits, preview and disclosed zero-value retry outcomes. Use the start response as a conservative elapsed-time reference, and test delayed start, network failure, lost claim reply, reload, day/viewer change and duplicate recovery.
33
33
  - This SDK is injected into Build iframes via the Build preview/runtime.
34
34
  - Widgets call SDK methods; the parent proxies to the API.
35
+ - When an SDK request never got a reply from Twinkle's server (the device went offline, the network dropped, or the connection was cut first), its rejected error has error.details.isTransportError === true. Its error.status is 500 for older apps, so check this flag before treating a 500 as a server failure. The request may or may not have been applied: retry a write only with the same operationId or requestId, and only after a later user action or decision point, never in a loop. Errors that came back from the server never carry the flag.
35
36
  - Data API methods require scoped tokens handled by the parent; some namespaces include write methods.
36
37
  - Use Twinkle.privateDb for LOW-frequency durable private per-user state such as preferences, drafts, settings, inventory checkpoints, and saved progress. It is NOT for high-frequency or per-frame/per-tick writes; the server rate-limits writes and returns 429.
37
38
  - Match storage to update frequency: privateDb and sharedDb are for LOW-frequency durable state that changes on a user action. NEVER write per-frame/per-tick state to them (camera or cursor position, animation, live movement, presence, autosave every frame/tick). Keep live state in client memory, broadcast realtime/presence via Twinkle.world, and flush only occasional durable snapshots (on an interval or on exit, never per frame). The server enforces per-key write rate limits and returns 429 on excess; never retry-loop a 429.
@@ -1403,17 +1404,18 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
1403
1404
  - For oldest-first chronological reads, pass order: 'asc' or order: 'oldest'. sort and direction are accepted as aliases.
1404
1405
  - Supports cursor-based pagination. Newest-first cursors page toward older entries; oldest-first cursors page toward newer entries.
1405
1406
  - async queryEntries(topicName, { limit, pageSize, cursor, order, authorUserId, fields, where, countBy, includeAuthor } = {}) | scopes: sharedDb:read
1406
- - Returns: { entries?, groups?, cursor, hasMore, queryVersion: 1 }
1407
+ - Returns: { entries?, groups?, cursor, hasMore, queryVersion: 1, truncated? }
1407
1408
  - Read selected shared data fields with server-side creator filtering, scalar matching and grouped counts.
1408
1409
  - Returns the same paginated entries as getEntries, plus queryVersion: 1.
1409
1410
  - authorUserId filters to one entry creator on the server. It does not change read or write permissions.
1410
1411
  - fields selects at most 24 top-level data field names; missing selected fields are null. Omit fields for complete data; [] returns only row metadata.
1411
1412
  - where: { field, equals } filters one top-level data field by exact JSON scalar equality (string, number, boolean or null).
1412
1413
  - Alternatively where: { field, truthy: true } requires a present JSON value other than null, false, zero or an empty string.
1413
- - countBy groups matching entries by one top-level data field and returns { groups: [{ value, count }], cursor, hasMore, queryVersion: 1 } instead of entries. This avoids transferring documents just to count comments. Groups page in first-entry order; use the returned cursor with the same query. Per-viewer hidden entries are excluded before counting.
1414
+ - countBy groups matching entries by one top-level data field and returns { groups: [{ value, count }], cursor, hasMore, queryVersion: 1 } instead of entries. This avoids transferring documents just to count comments. Groups page in first-entry order; use the returned cursor with the same query. Per-viewer hidden entries are excluded before counting. Counts cover at most the newest 4000 entries of the topic (or of authorUserId's entries in it); when the topic is larger the result has truncated: true, so keep a running tally field instead of recounting a huge topic.
1414
1415
  - includeAuthor: false skips username/profile-photo enrichment for count/index reads; id, topicId, userId and timestamps remain available.
1415
1416
  - Field names use letters, digits and underscores, starting with a letter or underscore, at most 64 characters. No SQL or nested paths.
1416
- - Keep the same filters/fields/order when passing a cursor. Reads retain app/topic boundaries, published guest opt-ins and per-viewer content visibility. Signed-in queries read canonical writer state.
1417
+ - Keep the same filters/fields/order when passing a cursor. Reads retain app/topic boundaries, published guest opt-ins and per-viewer content visibility. Signed-in entry queries read canonical writer state; grouped counts may trail a just-saved entry by a moment.
1418
+ - queryEntries calls are limited to 60 a minute per viewer (per IP for guests) and 600 a minute per app; a busy app gets HTTP 429 and should retry later rather than loop.
1417
1419
  - Use metadata lists for browsing and getEntriesByIds only when a member opens a full recording/document.
1418
1420
  - async loadMoreEntries(topicName, { limit, pageSize, cursor, order, sort, direction } = {}) | scopes: sharedDb:read
1419
1421
  - Returns: { entries: [{ id, topicId, userId, username, profilePicUrl, data, createdAt, updatedAt }], cursor?, hasMore }
@@ -3176,6 +3176,9 @@ unrelated release authority. Keep release evidence and private diagnostics out
3176
3176
  of the public article. Follow the repository's daily website-code update
3177
3177
  procedure and the existing dated-revision contract for an approved correction.
3178
3178
 
3179
+ **Plain words (Mikey, 2026-10-30):** write the article for 10-15-year-olds with no technical background. Describe what members can now do or what feels better in everyday words; avoid developer terms and internal names (API, SDK, frame rate, memory, cache, deploy, version numbers, counterproposal and the like). The same applies to the newspaper's front-page piece about it. A published update Subject cannot be edited through the CLI, so check the wording before `--confirm`.
3180
+ Model to follow (Mikey, 2026-10-30): the Subject 37274 form in `agent-guides/daily-update-post-template.md`: short headline, one-line greeting from the duty bot, the biggest change first in bold with a link, a short bold-labelled paragraph for each other change, and a closing question.
3181
+
3179
3182
  Use the scoped posting command after composing and reviewing the article:
3180
3183
 
3181
3184
  ```bash
@@ -5568,7 +5571,7 @@ Bridge Builder qualifies from QR check-ins: 7 different people, or one meetup a
5568
5571
  - The private note and the change history are admin-only; nobody is messaged.
5569
5572
  - It refuses members who left and crews that are not active.
5570
5573
  - `lumine admin meetup funnel [--days <1-90>]` shows views, crews by stage, and check-ins: pairs, how many were on one network or at an academy, and refusals by reason.
5571
- - `lumine admin meetup emails` lists parent emails held for review. Kind `witness` is a child asking a parent to be the grown-up.
5574
+ - `lumine admin meetup emails` lists parent emails held for review. Kind `witness` is a child asking a parent to be the grown-up. `lumine admin meetup emails send <id> --fingerprint <revision>` sends one; the fingerprint is the reviewed draft's revision that `emails list` prints, and a send without it is refused. If the draft changed after review, list and review it again. `emails discard <id>` drops one.
5572
5575
  - `lumine admin meetup announce-checkins [--send] [--as zero|ciel]` is about the one-time announcement of the check-in rework in each crew chat. It posts itself when the website with check-ins is out. The messages are pre-written; without `--send` this only shows them. `--send` retries a crew whose post failed. Each crew gets it once.
5573
5576
  - **Held unlocks:** every physical-meetup qualification gets a `meetup` approval item (`lumine admin approvals list --type meetup`). `approvals show <id> --json` includes `meetupEvidence` for all participants. After Mikey decides, use `approvals approve|reject <id> --fingerprint <meetupEvidence.fingerprint>`. Proximity never bypasses review.
5574
5577
  - **Crew completions:** `meetup show <crewId> --json` includes the same owner-only scan packet. After Mikey decides, use `meetup review-completion <crewId> --decision approve|reject --fingerprint <meetupEvidence.fingerprint>`. The decision fails if the shown evidence or proposed attendance changed. New attendees and tier upgrades need a new decision.
@@ -5621,6 +5624,6 @@ Every full management run includes this dedicated review, requested by Mikey Oct
5621
5624
 
5622
5625
  - `lumine admin dictionary korean-status --json`: current generated/reviewed/pending coverage, oldest pending time, outstanding jobs and recent failures.
5623
5626
  - `lumine admin dictionary korean-pending --limit 100 [--cursor <time:id>] --json`: full text, English reference meanings, exact IDs/revisions/source hashes. Continue through `nextCursor` until null. Read and assess every entry; collection alone is not review.
5624
- - `lumine admin dictionary korean-review <reviewed.json> --dry-run --json`, then omit `--dry-run` to apply the same reviewed payload within authorized content-maintenance scope. Accepts an array or `{ "items": [...] }`, 1–100 entries, each `{ "wordId": 123, "word": "example", "expectedRevision": 1, "expectedSourceHash": "<exported SHA-256>", "koreanDefinition": "<checked Korean meaning>", "reason": "<what was checked or corrected>" }`. Keep exact exported identity/version fields; supply a reason even when accepting unchanged text. All entries commit together with a mutation audit. Conflicts require re-export and re-review, never blind retry with new revisions.
5627
+ - `lumine admin dictionary korean-review <reviewed.json> --json` previews (validation only, nothing written); then add `--confirm` to apply the same reviewed payload within authorized content-maintenance scope. Accepts an array or `{ "items": [...] }`, 1–100 entries, each `{ "wordId": 123, "word": "example", "expectedRevision": 1, "expectedSourceHash": "<exported SHA-256>", "koreanDefinition": "<checked Korean meaning>", "reason": "<what was checked or corrected>" }`. Keep exact exported identity/version fields; supply a reason even when accepting unchanged text. All entries commit together with a mutation audit. Conflicts require re-export and re-review, never blind retry with new revisions.
5625
5628
 
5626
5629
  Assess common senses, part of speech and inflections, precision and natural Korean for learners. Use your own language judgment and source checks where needed; no paid batch-generation calls. Corrections update the shared word-bank field for all apps; successful reviews mark it reviewed and advance its revision. Future paid generation never overwrites existing text. The final run report includes reviewed/corrected/remaining counts, oldest waiting age, generation/Battery recovery failures, pagination coverage and carryovers. Re-read status after applying. Missing tables/endpoints mean the release is unavailable, not an empty healthy queue.