@stage5/lumine 0.2.54 → 0.2.56

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 CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { once } from "node:events";
2
3
  import { readFileSync } from "node:fs";
3
4
  import { assertAuthScope, resolveAuth } from "./auth.js";
4
5
  import { requestJson } from "./http.js";
@@ -8,7 +9,13 @@ import {
8
9
  validateNewsEditorial,
9
10
  writeNewsClaimArtifacts,
10
11
  } from "./admin-news.js";
11
- import { runAutomaticPagination, runBatchSkips } from "./admin-workflows.js";
12
+ import {
13
+ forEachPaginatedResultItem,
14
+ getPaginatedResultStorage,
15
+ runAutomaticPagination,
16
+ runBatchSkips,
17
+ writePaginatedResultJson,
18
+ } from "./admin-workflows.js";
12
19
  import {
13
20
  parseBuildReviewReceipt,
14
21
  runManagedBuildReview,
@@ -380,9 +387,25 @@ export function assertRecommendationWindowResult({ operation, result }) {
380
387
  }
381
388
  }
382
389
 
383
- function finishAdminOutput({ options, operation, result }) {
390
+ async function finishAdminOutput({ options, operation, result }) {
391
+ const paginationStorage = getPaginatedResultStorage(result);
384
392
  if (options.json) {
385
- console.log(JSON.stringify(result));
393
+ if (paginationStorage) {
394
+ await writePaginatedResultJson({
395
+ result,
396
+ write: async (chunk) => {
397
+ if (!process.stdout.write(chunk)) {
398
+ await once(process.stdout, "drain");
399
+ }
400
+ },
401
+ });
402
+ } else {
403
+ console.log(JSON.stringify(result));
404
+ }
405
+ return result;
406
+ }
407
+ if (paginationStorage) {
408
+ await printSpooledAdminResult({ result, storage: paginationStorage });
386
409
  return result;
387
410
  }
388
411
  printAdminResult({ operation, result });
@@ -2375,6 +2398,57 @@ function printAdminMonthlyMediaCosts(monthlyMediaCosts) {
2375
2398
  );
2376
2399
  }
2377
2400
 
2401
+ async function printSpooledAdminResult({ result, storage }) {
2402
+ const data = result?.data || {};
2403
+ const count = Number(storage.candidateCount || 0);
2404
+ if (storage.collectionKey === "subjects") {
2405
+ console.log(`${count} subject(s):`);
2406
+ await forEachPaginatedResultItem(result, async (subject) => {
2407
+ console.log(
2408
+ `#${subject.id} ${subject.title || "(untitled)"} — ${subject.author?.username || "unknown"} — effort ${subject.effortLevel ?? "unknown"}`,
2409
+ );
2410
+ console.log(` ${subject.url}`);
2411
+ });
2412
+ } else if (storage.collectionKey === "events") {
2413
+ console.log(`${count} audit event(s):`);
2414
+ await forEachPaginatedResultItem(result, async (event) => {
2415
+ const target =
2416
+ event.targetType && event.targetId
2417
+ ? ` ${event.targetType}:${event.targetId}`
2418
+ : "";
2419
+ console.log(
2420
+ `#${event.id} run ${event.runId ?? "-"} ${event.action}${target} — ${event.result}${event.changed === true ? " (changed)" : event.changed === false ? " (no change)" : ""}`,
2421
+ );
2422
+ });
2423
+ } else if (storage.collectionKey === "comments") {
2424
+ console.log(`${count} comment(s) for ${data.subject?.url || "this target"}:`);
2425
+ await forEachPaginatedResultItem(result, async (comment) => {
2426
+ console.log(
2427
+ `#${comment.id} ${comment.author?.username || "unknown"}: ${comment.content || "(empty)"}`,
2428
+ );
2429
+ });
2430
+ } else if (storage.collectionKey === "items") {
2431
+ console.log(`${count} recommendation candidate(s):`);
2432
+ await forEachPaginatedResultItem(result, async (item) => {
2433
+ console.log(`#${item.contentId} ${item.contentType}`);
2434
+ if (item.url || item.subjectUrl) {
2435
+ console.log(` ${item.url || item.subjectUrl}`);
2436
+ }
2437
+ });
2438
+ } else if (storage.collectionKey === "builds") {
2439
+ console.log(`${count} Build candidate(s):`);
2440
+ await forEachPaginatedResultItem(result, async (build) => {
2441
+ console.log(`#${build.id} ${build.title || "(untitled)"}`);
2442
+ if (build.url) console.log(` ${build.url}`);
2443
+ });
2444
+ } else {
2445
+ console.log(
2446
+ `${count} ${storage.collectionKey} candidate(s) saved at ${storage.spoolPath}.`,
2447
+ );
2448
+ }
2449
+ printPagination(data.pagination);
2450
+ }
2451
+
2378
2452
  function printAdminResult({ operation, result }) {
2379
2453
  const data = result?.data || {};
2380
2454
  if (Array.isArray(data.applications)) {
package/lib/auth.js CHANGED
@@ -6,6 +6,9 @@ import { requestJson } from "./http.js";
6
6
  import { sleep, trimTrailingSlash } from "./util.js";
7
7
 
8
8
  export async function login(options) {
9
+ const report = options.loginProgressToStderr
10
+ ? (...args) => console.error(...args)
11
+ : (...args) => console.log(...args);
9
12
  const start = await requestJson({
10
13
  method: "POST",
11
14
  url: `${options.apiUrl}/cli/device/start`,
@@ -23,15 +26,15 @@ export async function login(options) {
23
26
  });
24
27
 
25
28
  const approvalUrl = start.verificationUriComplete || start.verificationUri;
26
- console.log("Connect Lumine CLI to Twinkle.");
29
+ report("Connect Lumine CLI to Twinkle.");
27
30
  if (options.openBrowser && approvalUrl) {
28
- console.log("Opening Twinkle in your browser...");
31
+ report("Opening Twinkle in your browser...");
29
32
  const opened = await openBrowser(approvalUrl);
30
- if (!opened) console.log("Could not open the browser automatically.");
33
+ if (!opened) report("Could not open the browser automatically.");
31
34
  }
32
- console.log(`Approval link: ${approvalUrl}`);
33
- console.log(`Code: ${start.userCode}`);
34
- console.log("Leave this terminal open. Waiting for approval...");
35
+ report(`Approval link: ${approvalUrl}`);
36
+ report(`Code: ${start.userCode}`);
37
+ report("Leave this terminal open. Waiting for approval...");
35
38
 
36
39
  const intervalMs = Math.max(Number(start.interval || 3), 1) * 1000;
37
40
  const startedAt = Date.now();
@@ -52,10 +55,10 @@ export async function login(options) {
52
55
  userId: tokenResponse.user?.id || null,
53
56
  expiresAt: Date.now() + Number(tokenResponse.expiresIn || 0) * 1000,
54
57
  });
55
- console.log(
58
+ report(
56
59
  `Logged in${tokenResponse.user?.username ? ` as ${tokenResponse.user.username}` : ""}.`,
57
60
  );
58
- console.log("You can now run `lumine` to choose a project.");
61
+ report("You can now run `lumine` to choose a project.");
59
62
  return;
60
63
  }
61
64
 
@@ -112,7 +115,10 @@ export async function ensureAuth(options) {
112
115
  } catch (error) {
113
116
  if (options.authToken || !isMissingLoginError(error)) throw error;
114
117
  }
115
- await login(options);
118
+ await login({
119
+ ...options,
120
+ loginProgressToStderr: Boolean(options.json),
121
+ });
116
122
  return await resolveAuth(options);
117
123
  }
118
124
 
package/lib/commands.js CHANGED
@@ -2730,8 +2730,8 @@ export function printHelp() {
2730
2730
  lumine agent --provider <codex|claude-code> "<build request>"
2731
2731
  lumine sponsor agreement|apply|status|withdraw
2732
2732
  lumine sponsor capacity [--concurrency <n>] [--helpers <n>] [--daily-limit <n>] [--weekly-limit <n>]
2733
- lumine sponsor duty start <zero|ciel> --provider <codex|claude-code> [--model <name>] [--effort <level>] [--service-tier <tier>]
2734
- lumine sponsor duty pause|resume|stop <zero|ciel>
2733
+ lumine sponsor duty start --provider <codex|claude-code> [--model <name>] [--effort <level>] [--service-tier <tier>]
2734
+ lumine sponsor duty pause|resume|stop
2735
2735
  lumine sponsor jobs
2736
2736
  lumine app-mcp <published-app-url-or-id> [--no-open]
2737
2737
  lumine new [title]
@@ -2907,7 +2907,7 @@ Options:
2907
2907
  --since-run Recommendations since the previous completed run (default)
2908
2908
  --include-legacy Explicitly include the full historical recommendation queue
2909
2909
  --all Automatically read every page in the canonical snapshot
2910
- --checkpoint <file> Persist automatic pagination or batch progress
2910
+ --checkpoint <file> Persist bounded pagination metadata or batch progress
2911
2911
  --resume Resume the exact request recorded by --checkpoint
2912
2912
  --output <file> Save an admin result or newspaper claim as JSON
2913
2913
  --output-dir <dir> Directory for managed Build review evidence
package/lib/sponsor.js CHANGED
@@ -6,7 +6,7 @@ import { stdin as input, stdout as output } from "node:process";
6
6
 
7
7
  import { agentCommand } from "./agent.js";
8
8
  import { notifyBuildOwnerOfContribution } from "./api.js";
9
- import { resolveAuth, writeAuthFile } from "./auth.js";
9
+ import { ensureAuth, resolveAuth, writeAuthFile } from "./auth.js";
10
10
  import { readCompleteBuildForumSnapshot } from "./forum.js";
11
11
  import { requestJson } from "./http.js";
12
12
  import { sleep } from "./util.js";
@@ -24,7 +24,7 @@ export async function sponsorCommand(options, commandServices) {
24
24
  .trim()
25
25
  .toLowerCase();
26
26
  if (area === "agreement") {
27
- const auth = await resolveAuth(options);
27
+ const auth = await ensureAuth(options);
28
28
  const agreement = await sponsorRequest({
29
29
  options,
30
30
  auth,
@@ -50,7 +50,7 @@ export async function sponsorCommand(options, commandServices) {
50
50
  return;
51
51
  }
52
52
  if (area === "jobs") {
53
- const auth = await resolveAuth(options);
53
+ const auth = await ensureAuth(options);
54
54
  const result = await sponsorRequest({
55
55
  options,
56
56
  auth,
@@ -67,7 +67,7 @@ export async function sponsorCommand(options, commandServices) {
67
67
  }
68
68
 
69
69
  async function applyToSponsor(options) {
70
- const auth = await resolveAuth(options);
70
+ const auth = await ensureAuth(options);
71
71
  const agreement = await sponsorRequest({ options, auth, path: "/agreement" });
72
72
  if (!options.json) {
73
73
  printSponsorAgreement(agreement, { ...options, json: false });
@@ -135,7 +135,7 @@ async function applyToSponsor(options) {
135
135
  }
136
136
 
137
137
  async function withdrawSponsorApplication(options) {
138
- const auth = await resolveAuth(options);
138
+ const auth = await ensureAuth(options);
139
139
  if (!options.assumeYes && input.isTTY && output.isTTY) {
140
140
  const rl = readline.createInterface({ input, output });
141
141
  try {
@@ -162,7 +162,7 @@ async function withdrawSponsorApplication(options) {
162
162
  }
163
163
 
164
164
  async function updateSponsorCapacity(options) {
165
- const auth = await resolveAuth(options);
165
+ const auth = await ensureAuth(options);
166
166
  const status = await sponsorRequest({ options, auth, path: "/status" });
167
167
  if (!status.sponsor) {
168
168
  throw new Error("An approved sponsor account is required first.");
@@ -212,24 +212,41 @@ async function sponsorDutyCommand(options, args, commandServices) {
212
212
  .trim()
213
213
  .toLowerCase();
214
214
  if (action === "status") {
215
+ if (args.length > 1) throw new Error(sponsorUsage());
215
216
  await printSponsorStatus(options);
216
217
  return;
217
218
  }
218
- const persona = normalizePersona(args[1] || options.sponsorPersona);
219
- const auth = await resolveAuth(options);
219
+ if (args.length > 1 || options.sponsorPersona) {
220
+ throw new Error(
221
+ "Sponsor duty is shared by Zero and Ciel. Remove the persona argument.",
222
+ );
223
+ }
220
224
  if (["pause", "resume", "stop"].includes(action)) {
225
+ const auth = await ensureAuth(options);
226
+ const status = await sponsorRequest({ options, auth, path: "/status" });
227
+ const currentDuties = (status.duties || []).filter((duty) =>
228
+ ["active", "paused"].includes(String(duty.state || "")),
229
+ );
230
+ if (currentDuties.length === 0) {
231
+ throw new Error("No active sponsor duty session was found.");
232
+ }
233
+ if (currentDuties.length > 1) {
234
+ throw new Error(
235
+ `Multiple legacy duty sessions are still open (${currentDuties.map((duty) => `#${duty.id}`).join(", ")}). Stop them with the CLI version that started them before using shared duty.`,
236
+ );
237
+ }
238
+ const dutyId = Number(currentDuties[0].id || 0);
239
+ if (!dutyId) throw new Error("Twinkle returned an invalid duty session.");
221
240
  const state =
222
241
  action === "resume" ? "active" : action === "stop" ? "stopped" : "paused";
223
242
  const result = await sponsorRequest({
224
243
  options,
225
244
  auth,
226
245
  method: "POST",
227
- path: `/duty/${persona}/state`,
228
- body: { state },
246
+ path: "/duty/state",
247
+ body: { dutySessionId: dutyId, state },
229
248
  });
230
- printJsonOrLines(options, result, [
231
- `${displayPersona(persona)} duty is now ${state}.`,
232
- ]);
249
+ printJsonOrLines(options, result, [`Sponsor duty is now ${state}.`]);
233
250
  return;
234
251
  }
235
252
  if (action !== "start") throw new Error(sponsorUsage());
@@ -242,13 +259,14 @@ async function sponsorDutyCommand(options, args, commandServices) {
242
259
  "--service-tier is currently supported only by the Codex duty adapter.",
243
260
  );
244
261
  }
262
+ const auth = await ensureAuth(options);
245
263
  const started = await sponsorRequest({
246
264
  options,
247
265
  auth,
248
266
  method: "POST",
249
267
  path: "/duty/start",
250
268
  body: {
251
- persona,
269
+ scope: "shared",
252
270
  provider,
253
271
  requestedModel: options.model || null,
254
272
  requestedEffort: options.sponsorEffort || null,
@@ -259,7 +277,6 @@ async function sponsorDutyCommand(options, args, commandServices) {
259
277
  await runSponsorDuty({
260
278
  options,
261
279
  auth,
262
- persona,
263
280
  provider,
264
281
  started,
265
282
  commandServices,
@@ -269,7 +286,6 @@ async function sponsorDutyCommand(options, args, commandServices) {
269
286
  async function runSponsorDuty({
270
287
  options,
271
288
  auth,
272
- persona,
273
289
  provider,
274
290
  started,
275
291
  commandServices,
@@ -279,6 +295,9 @@ async function runSponsorDuty({
279
295
  if (!dutyId || !leaseToken) {
280
296
  throw new Error("Twinkle did not return a valid sponsor duty lease.");
281
297
  }
298
+ if (started.duty?.scope !== "shared") {
299
+ throw new Error("Twinkle did not return a shared sponsor duty lease.");
300
+ }
282
301
  let canonicalDuty = started.duty;
283
302
  let stopRequested = false;
284
303
  let dutyLeaseError = null;
@@ -332,9 +351,7 @@ async function runSponsorDuty({
332
351
  Math.max(10, Number(started.heartbeatEverySeconds || 30)) * 1_000,
333
352
  );
334
353
 
335
- console.log(
336
- `${displayPersona(persona)} sponsor duty started with ${provider}.`,
337
- );
354
+ console.log(`Shared Zero/Ciel sponsor duty started with ${provider}.`);
338
355
  printDutyRuntime(canonicalDuty);
339
356
  console.log(
340
357
  "The Workshop is now discoverable to users. Press Ctrl-C for a graceful stop.",
@@ -364,7 +381,6 @@ async function runSponsorDuty({
364
381
  sponsorAuth: auth,
365
382
  dutyId,
366
383
  dutyLeaseToken: leaseToken,
367
- persona,
368
384
  provider,
369
385
  claim,
370
386
  commandServices,
@@ -384,8 +400,8 @@ async function runSponsorDuty({
384
400
  options,
385
401
  auth,
386
402
  method: "POST",
387
- path: `/duty/${persona}/state`,
388
- body: { state: "paused" },
403
+ path: "/duty/state",
404
+ body: { dutySessionId: dutyId, state: "paused" },
389
405
  });
390
406
  canonicalDuty = paused.duty || canonicalDuty;
391
407
  }
@@ -400,8 +416,8 @@ async function runSponsorDuty({
400
416
  options,
401
417
  auth,
402
418
  method: "POST",
403
- path: `/duty/${persona}/state`,
404
- body: { state: "stopped" },
419
+ path: "/duty/state",
420
+ body: { dutySessionId: dutyId, state: "stopped" },
405
421
  }).catch((error) => {
406
422
  if (!dutyLeaseError) {
407
423
  console.error(
@@ -411,7 +427,7 @@ async function runSponsorDuty({
411
427
  });
412
428
  }
413
429
  }
414
- console.log(`${displayPersona(persona)} sponsor duty stopped.`);
430
+ console.log("Shared Zero/Ciel sponsor duty stopped.");
415
431
  }
416
432
 
417
433
  async function processSponsorJob({
@@ -419,12 +435,12 @@ async function processSponsorJob({
419
435
  sponsorAuth,
420
436
  dutyId,
421
437
  dutyLeaseToken,
422
- persona,
423
438
  provider,
424
439
  claim,
425
440
  commandServices,
426
441
  }) {
427
442
  const job = claim.job;
443
+ const persona = normalizeJobPersona(job?.persona);
428
444
  const jobId = Number(job.id);
429
445
  const attemptToken = String(claim.attempt?.token || "");
430
446
  const workspaceToken = String(claim.workspaceToken?.accessToken || "");
@@ -1134,7 +1150,7 @@ async function sponsorRequest({
1134
1150
  }
1135
1151
 
1136
1152
  async function printSponsorStatus(options) {
1137
- const auth = await resolveAuth(options);
1153
+ const auth = await ensureAuth(options);
1138
1154
  const status = await sponsorRequest({ options, auth, path: "/status" });
1139
1155
  if (options.json) {
1140
1156
  console.log(JSON.stringify(status, null, 2));
@@ -1152,8 +1168,12 @@ async function printSponsorStatus(options) {
1152
1168
  );
1153
1169
  }
1154
1170
  for (const duty of status.duties || []) {
1171
+ const scope =
1172
+ duty.scope === "shared"
1173
+ ? "shared Zero/Ciel"
1174
+ : `legacy assistant user ${duty.personaUserId}`;
1155
1175
  console.log(
1156
- `Duty #${duty.id}: user ${duty.personaUserId} ${duty.state} · ${duty.provider} · model=${duty.requestedModel || "provider default"} · effort=${duty.requestedEffort || "provider default"}`,
1176
+ `Duty #${duty.id}: ${scope} · ${duty.state} · ${duty.provider} · model=${duty.requestedModel || "provider default"} · effort=${duty.requestedEffort || "provider default"}`,
1157
1177
  );
1158
1178
  }
1159
1179
  }
@@ -1252,12 +1272,12 @@ function normalizeProvider(value) {
1252
1272
  return provider;
1253
1273
  }
1254
1274
 
1255
- function normalizePersona(value) {
1275
+ function normalizeJobPersona(value) {
1256
1276
  const persona = String(value || "")
1257
1277
  .trim()
1258
1278
  .toLowerCase();
1259
1279
  if (!PERSONAS.has(persona)) {
1260
- throw new Error("Choose a duty persona: zero or ciel.");
1280
+ throw new Error("Twinkle returned an invalid Workshop job assistant.");
1261
1281
  }
1262
1282
  return persona;
1263
1283
  }
@@ -1284,8 +1304,8 @@ function sponsorUsage() {
1284
1304
  " lumine sponsor status",
1285
1305
  " lumine sponsor withdraw [--yes]",
1286
1306
  " lumine sponsor capacity [--concurrency <n>] [--helpers <n>] [--daily-limit <n>] [--weekly-limit <n>]",
1287
- " lumine sponsor duty start <zero|ciel> --provider <codex|claude-code> [--model <name>] [--effort <level>] [--service-tier <tier>]",
1288
- " lumine sponsor duty pause|resume|stop <zero|ciel>",
1307
+ " lumine sponsor duty start --provider <codex|claude-code> [--model <name>] [--effort <level>] [--service-tier <tier>]",
1308
+ " lumine sponsor duty pause|resume|stop",
1289
1309
  " lumine sponsor jobs [--limit <n>]",
1290
1310
  ].join("\n");
1291
1311
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.54",
3
+ "version": "0.2.56",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -663,7 +663,9 @@ artifact snapshot, branch-notice evidence, and requested/resolved provider,
663
663
  model, effort, service-tier, runtime, usage, and agent-tree records. It never
664
664
  includes raw Zero/Ciel chat.
665
665
 
666
- `clear` qualifies that unique handoff for its flat 50 KP award;
666
+ `clear` qualifies a contribution to another user for its flat 50 KP award;
667
+ self-sponsored testing remains ineligible even when its integrity evidence is
668
+ cleared.
667
669
  `disqualify` makes it ineligible. `hold` and `flag` require an evidence note and
668
670
  remain open. The scan itself never changes sponsor status or applies a sanction.
669
671
  Use the separate, audited `sponsor status set` command for an explicit human
@@ -881,12 +883,15 @@ feed-ID high-water mark and the server timestamp, then traverse the indexed
881
883
  row moved forward when it was reshared. Explicit legacy scans retain the
882
884
  descending primary-key walk. A page
883
885
  can be empty while `hasMore` remains true; continue until `exhausted`. `--all`
884
- does that automatically and writes a private checkpoint after every
885
- server-confirmed page; `--resume` continues only when the checkpoint belongs to
886
- the same API, run, and exact request. The final result can be copied to
887
- `--output`, while `--checkpoint` is resumable operational state. Subject
888
- `--after` is inclusive, and every opaque cursor is bound to its original
889
- filters.
886
+ does that automatically, fsyncs each confirmed page to a private NDJSON
887
+ candidate spool, and writes only bounded cursor, boundary, count, byte-length,
888
+ and digest metadata to the private checkpoint. `--resume` continues only when
889
+ the checkpoint belongs to the same API, run, and exact request and its confirmed
890
+ spool prefix still matches those metadata; an unconfirmed fsynced tail is
891
+ discarded before the next request. The final JSON collection is streamed from
892
+ the spool and can be copied to a separate `--output` file, while `--checkpoint`
893
+ remains resumable operational state. Subject `--after` is inclusive, and every
894
+ opaque cursor is bound to its original filters.
890
895
 
891
896
  For `--all --json`, stdout remains exactly one JSON value. Scan-start, first-
892
897
  page, every-tenth-page, and exhaustion progress is written to **stderr** with