@stage5/lumine 0.2.55 → 0.2.57

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/README.md CHANGED
@@ -229,7 +229,7 @@ claude "Read CLAUDE.md, then make the requested change."
229
229
  The login command uses a browser approval code and stores a scoped token and the
230
230
  selected project at `~/.twinkle/lumine-cli-auth.json`.
231
231
 
232
- ## Sponsoring Zero or Ciel Build Workshop duty
232
+ ## Sponsoring shared Zero and Ciel Build Workshop help
233
233
 
234
234
  Sponsorship is an approved contribution role, not an open switch and not a
235
235
  website-management role. Applications are submitted only through Lumine CLI;
@@ -243,12 +243,12 @@ lumine sponsor status
243
243
  ```
244
244
 
245
245
  After Mikey approves an application, configure conservative limits based on
246
- the subscription you are contributing, then start one foreground duty:
246
+ the subscription you are contributing, then start one shared foreground duty:
247
247
 
248
248
  ```bash
249
249
  lumine sponsor capacity --concurrency 1 --helpers 0 \
250
250
  --daily-limit 3 --weekly-limit 10
251
- lumine sponsor duty start zero --provider codex \
251
+ lumine sponsor duty start --provider codex \
252
252
  --model gpt-5.6-sol --effort max --service-tier priority
253
253
  ```
254
254
 
@@ -259,11 +259,14 @@ account may authenticate this way, but login never grants sponsor duty:
259
259
  `sponsor duty start` still requires that exact account to have a canonical,
260
260
  server-approved sponsor profile.
261
261
 
262
- While that foreground process has a fresh server lease, Zero or Ciel exposes a
263
- Build Workshop in chat and users may see the named sponsor and canonical queue.
262
+ While that foreground process has a fresh server lease, both Zero and Ciel can
263
+ delegate user-approved Build work to the shared worker pool. The user chooses
264
+ the assistant for each request, and may see the named sponsor and canonical
265
+ shared queue.
264
266
  With no live duty, the website stays in its ordinary chat state and shows none
265
267
  of the Workshop UI. Press Ctrl-C to stop accepting work and drain already
266
- claimed jobs before duty ends; `pause`, `resume`, and `stop` are also available.
268
+ claimed jobs before duty ends; `pause`, `resume`, and `stop` are also available
269
+ without a Zero/Ciel argument.
267
270
 
268
271
  Zero or Ciel remains the user-facing teammate. The local coding agent receives
269
272
  only the initial relay and active-job Build follow-ups covered by the user’s
@@ -273,8 +276,9 @@ cannot merge into Main, publish, or use website-management APIs. Lumine records
273
276
  the requested and provider-reported model, effort, service tier, runtime/usage
274
277
  evidence, coordinator/helper tree, saved artifact, and branch notice. Every
275
278
  completed handoff enters the daily integrity flow; probationary, hard-flagged,
276
- and sampled work requires review. Each unique cleared handoff earns a flat 50
277
- Karma Points, while retries and helper agents do not multiply the award.
279
+ and sampled work requires review. Each unique cleared contribution to another
280
+ user earns a flat 50 Karma Points, while self-sponsored testing, retries, and
281
+ helper agents do not earn or multiply the award.
278
282
 
279
283
  ## Privileged website administration
280
284
 
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]
package/lib/sponsor.js CHANGED
@@ -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
+ 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
+ }
219
224
  if (["pause", "resume", "stop"].includes(action)) {
220
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());
@@ -249,7 +266,7 @@ async function sponsorDutyCommand(options, args, commandServices) {
249
266
  method: "POST",
250
267
  path: "/duty/start",
251
268
  body: {
252
- persona,
269
+ scope: "shared",
253
270
  provider,
254
271
  requestedModel: options.model || null,
255
272
  requestedEffort: options.sponsorEffort || null,
@@ -260,7 +277,6 @@ async function sponsorDutyCommand(options, args, commandServices) {
260
277
  await runSponsorDuty({
261
278
  options,
262
279
  auth,
263
- persona,
264
280
  provider,
265
281
  started,
266
282
  commandServices,
@@ -270,7 +286,6 @@ async function sponsorDutyCommand(options, args, commandServices) {
270
286
  async function runSponsorDuty({
271
287
  options,
272
288
  auth,
273
- persona,
274
289
  provider,
275
290
  started,
276
291
  commandServices,
@@ -280,6 +295,9 @@ async function runSponsorDuty({
280
295
  if (!dutyId || !leaseToken) {
281
296
  throw new Error("Twinkle did not return a valid sponsor duty lease.");
282
297
  }
298
+ if (started.duty?.scope !== "shared") {
299
+ throw new Error("Twinkle did not return a shared sponsor duty lease.");
300
+ }
283
301
  let canonicalDuty = started.duty;
284
302
  let stopRequested = false;
285
303
  let dutyLeaseError = null;
@@ -294,6 +312,9 @@ async function runSponsorDuty({
294
312
  process.on("SIGINT", signalHandler);
295
313
  process.on("SIGTERM", signalHandler);
296
314
 
315
+ const dutyHeartbeatEveryMs =
316
+ Math.max(10, Number(started.heartbeatEverySeconds || 30)) * 1_000;
317
+ let lastDutyHeartbeatSuccessAt = Date.now();
297
318
  const heartbeatDuty = async () => {
298
319
  if (dutyHeartbeatRunning) return;
299
320
  dutyHeartbeatRunning = true;
@@ -306,6 +327,7 @@ async function runSponsorDuty({
306
327
  body: { leaseToken },
307
328
  });
308
329
  dutyLeaseError = null;
330
+ lastDutyHeartbeatSuccessAt = Date.now();
309
331
  if (canonicalDuty.state === "stopped") stopRequested = true;
310
332
  } catch (error) {
311
333
  const status = await sponsorRequest({
@@ -322,20 +344,27 @@ async function runSponsorDuty({
322
344
  stopRequested = true;
323
345
  return;
324
346
  }
325
- dutyLeaseError = error;
326
- stopRequested = true;
347
+ // Same lease policy as the per-job heartbeat: end the duty only when the
348
+ // server rejects the lease or the lease window has fully elapsed. A
349
+ // transient network failure retries on the next tick.
350
+ const terminalDutyError = !isRetryableSponsorDutyRequestError(error);
351
+ const leaseWindowElapsed =
352
+ Date.now() - lastDutyHeartbeatSuccessAt >=
353
+ Math.max(120_000, dutyHeartbeatEveryMs * 3);
354
+ if (terminalDutyError || leaseWindowElapsed) {
355
+ dutyLeaseError = error;
356
+ stopRequested = true;
357
+ }
327
358
  } finally {
328
359
  dutyHeartbeatRunning = false;
329
360
  }
330
361
  };
331
362
  const dutyHeartbeat = setInterval(
332
363
  () => void heartbeatDuty(),
333
- Math.max(10, Number(started.heartbeatEverySeconds || 30)) * 1_000,
364
+ dutyHeartbeatEveryMs,
334
365
  );
335
366
 
336
- console.log(
337
- `${displayPersona(persona)} sponsor duty started with ${provider}.`,
338
- );
367
+ console.log(`Shared Zero/Ciel sponsor duty started with ${provider}.`);
339
368
  printDutyRuntime(canonicalDuty);
340
369
  console.log(
341
370
  "The Workshop is now discoverable to users. Press Ctrl-C for a graceful stop.",
@@ -352,20 +381,34 @@ async function runSponsorDuty({
352
381
  Number(canonicalDuty.capacity?.maxConcurrentTasks || 1),
353
382
  );
354
383
  while (!stopRequested && activeJobs.size < concurrency) {
355
- const claim = await sponsorRequest({
356
- options,
357
- auth,
358
- method: "POST",
359
- path: "/jobs/claim",
360
- body: { dutySessionId: dutyId, leaseToken },
361
- });
384
+ let claim;
385
+ try {
386
+ claim = await sponsorRequest({
387
+ options,
388
+ auth,
389
+ method: "POST",
390
+ path: "/jobs/claim",
391
+ body: { dutySessionId: dutyId, leaseToken },
392
+ });
393
+ } catch (error) {
394
+ if (!isRetryableSponsorDutyRequestError(error)) {
395
+ dutyLeaseError = error;
396
+ stopRequested = true;
397
+ break;
398
+ }
399
+ // A failed claim must not kill the duty process; the heartbeat owns
400
+ // lease validity, and the outer loop polls again after a sleep.
401
+ console.error(
402
+ `Workshop claim failed; retrying next poll: ${error?.message || error}`,
403
+ );
404
+ break;
405
+ }
362
406
  if (!claim.job) break;
363
407
  const work = processSponsorJob({
364
408
  options,
365
409
  sponsorAuth: auth,
366
410
  dutyId,
367
411
  dutyLeaseToken: leaseToken,
368
- persona,
369
412
  provider,
370
413
  claim,
371
414
  commandServices,
@@ -385,8 +428,8 @@ async function runSponsorDuty({
385
428
  options,
386
429
  auth,
387
430
  method: "POST",
388
- path: `/duty/${persona}/state`,
389
- body: { state: "paused" },
431
+ path: "/duty/state",
432
+ body: { dutySessionId: dutyId, state: "paused" },
390
433
  });
391
434
  canonicalDuty = paused.duty || canonicalDuty;
392
435
  }
@@ -401,8 +444,8 @@ async function runSponsorDuty({
401
444
  options,
402
445
  auth,
403
446
  method: "POST",
404
- path: `/duty/${persona}/state`,
405
- body: { state: "stopped" },
447
+ path: "/duty/state",
448
+ body: { dutySessionId: dutyId, state: "stopped" },
406
449
  }).catch((error) => {
407
450
  if (!dutyLeaseError) {
408
451
  console.error(
@@ -412,7 +455,7 @@ async function runSponsorDuty({
412
455
  });
413
456
  }
414
457
  }
415
- console.log(`${displayPersona(persona)} sponsor duty stopped.`);
458
+ console.log("Shared Zero/Ciel sponsor duty stopped.");
416
459
  }
417
460
 
418
461
  async function processSponsorJob({
@@ -420,12 +463,12 @@ async function processSponsorJob({
420
463
  sponsorAuth,
421
464
  dutyId,
422
465
  dutyLeaseToken,
423
- persona,
424
466
  provider,
425
467
  claim,
426
468
  commandServices,
427
469
  }) {
428
470
  const job = claim.job;
471
+ const persona = normalizeJobPersona(job?.persona);
429
472
  const jobId = Number(job.id);
430
473
  const attemptToken = String(claim.attempt?.token || "");
431
474
  const workspaceToken = String(claim.workspaceToken?.accessToken || "");
@@ -1118,6 +1161,12 @@ async function retrySponsorTransport(operation) {
1118
1161
  throw lastError;
1119
1162
  }
1120
1163
 
1164
+ function isRetryableSponsorDutyRequestError(error) {
1165
+ const status = Number(error?.status || 0);
1166
+ if (!status) return true;
1167
+ return status === 408 || status === 425 || status === 429 || status >= 500;
1168
+ }
1169
+
1121
1170
  async function sponsorRequest({
1122
1171
  options,
1123
1172
  auth,
@@ -1153,8 +1202,12 @@ async function printSponsorStatus(options) {
1153
1202
  );
1154
1203
  }
1155
1204
  for (const duty of status.duties || []) {
1205
+ const scope =
1206
+ duty.scope === "shared"
1207
+ ? "shared Zero/Ciel"
1208
+ : `legacy assistant user ${duty.personaUserId}`;
1156
1209
  console.log(
1157
- `Duty #${duty.id}: user ${duty.personaUserId} ${duty.state} · ${duty.provider} · model=${duty.requestedModel || "provider default"} · effort=${duty.requestedEffort || "provider default"}`,
1210
+ `Duty #${duty.id}: ${scope} · ${duty.state} · ${duty.provider} · model=${duty.requestedModel || "provider default"} · effort=${duty.requestedEffort || "provider default"}`,
1158
1211
  );
1159
1212
  }
1160
1213
  }
@@ -1253,12 +1306,12 @@ function normalizeProvider(value) {
1253
1306
  return provider;
1254
1307
  }
1255
1308
 
1256
- function normalizePersona(value) {
1309
+ function normalizeJobPersona(value) {
1257
1310
  const persona = String(value || "")
1258
1311
  .trim()
1259
1312
  .toLowerCase();
1260
1313
  if (!PERSONAS.has(persona)) {
1261
- throw new Error("Choose a duty persona: zero or ciel.");
1314
+ throw new Error("Twinkle returned an invalid Workshop job assistant.");
1262
1315
  }
1263
1316
  return persona;
1264
1317
  }
@@ -1285,8 +1338,8 @@ function sponsorUsage() {
1285
1338
  " lumine sponsor status",
1286
1339
  " lumine sponsor withdraw [--yes]",
1287
1340
  " lumine sponsor capacity [--concurrency <n>] [--helpers <n>] [--daily-limit <n>] [--weekly-limit <n>]",
1288
- " lumine sponsor duty start <zero|ciel> --provider <codex|claude-code> [--model <name>] [--effort <level>] [--service-tier <tier>]",
1289
- " lumine sponsor duty pause|resume|stop <zero|ciel>",
1341
+ " lumine sponsor duty start --provider <codex|claude-code> [--model <name>] [--effort <level>] [--service-tier <tier>]",
1342
+ " lumine sponsor duty pause|resume|stop",
1290
1343
  " lumine sponsor jobs [--limit <n>]",
1291
1344
  ].join("\n");
1292
1345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.55",
3
+ "version": "0.2.57",
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