@stage5/lumine 0.2.53 → 0.2.54

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/sponsor.js ADDED
@@ -0,0 +1,1291 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import readline from "node:readline/promises";
5
+ import { stdin as input, stdout as output } from "node:process";
6
+
7
+ import { agentCommand } from "./agent.js";
8
+ import { notifyBuildOwnerOfContribution } from "./api.js";
9
+ import { resolveAuth, writeAuthFile } from "./auth.js";
10
+ import { readCompleteBuildForumSnapshot } from "./forum.js";
11
+ import { requestJson } from "./http.js";
12
+ import { sleep } from "./util.js";
13
+
14
+ const SPONSOR_PATH = "/cli/sponsor";
15
+ const PROVIDERS = new Set(["codex", "claude-code"]);
16
+ const PERSONAS = new Set(["zero", "ciel"]);
17
+ const DEFAULT_DUTY_POLL_MS = 3_000;
18
+ const MAX_FORUM_CONTEXT_CHARS = 8_000;
19
+ const MAX_FOLLOW_UP_PASSES = 4;
20
+
21
+ export async function sponsorCommand(options, commandServices) {
22
+ const args = options.sponsorArgs || [];
23
+ const area = String(args[0] || "status")
24
+ .trim()
25
+ .toLowerCase();
26
+ if (area === "agreement") {
27
+ const auth = await resolveAuth(options);
28
+ const agreement = await sponsorRequest({
29
+ options,
30
+ auth,
31
+ path: "/agreement",
32
+ });
33
+ printSponsorAgreement(agreement, options);
34
+ return;
35
+ }
36
+ if (area === "apply") {
37
+ await applyToSponsor(options);
38
+ return;
39
+ }
40
+ if (area === "withdraw") {
41
+ await withdrawSponsorApplication(options);
42
+ return;
43
+ }
44
+ if (area === "capacity") {
45
+ await updateSponsorCapacity(options);
46
+ return;
47
+ }
48
+ if (area === "duty") {
49
+ await sponsorDutyCommand(options, args.slice(1), commandServices);
50
+ return;
51
+ }
52
+ if (area === "jobs") {
53
+ const auth = await resolveAuth(options);
54
+ const result = await sponsorRequest({
55
+ options,
56
+ auth,
57
+ path: `/jobs?limit=${encodeURIComponent(String(options.limit || 20))}`,
58
+ });
59
+ printSponsorJobs(result, options);
60
+ return;
61
+ }
62
+ if (area === "status") {
63
+ await printSponsorStatus(options);
64
+ return;
65
+ }
66
+ throw new Error(sponsorUsage());
67
+ }
68
+
69
+ async function applyToSponsor(options) {
70
+ const auth = await resolveAuth(options);
71
+ const agreement = await sponsorRequest({ options, auth, path: "/agreement" });
72
+ if (!options.json) {
73
+ printSponsorAgreement(agreement, { ...options, json: false });
74
+ }
75
+
76
+ const interactive = Boolean(input.isTTY && output.isTTY);
77
+ const rl = interactive ? readline.createInterface({ input, output }) : null;
78
+ try {
79
+ const motivation = await requiredApplicationText({
80
+ supplied: options.sponsorMotivation,
81
+ label: "Why do you want to sponsor Lumine Build work? ",
82
+ interactive,
83
+ rl,
84
+ });
85
+ const availability = await requiredApplicationText({
86
+ supplied: options.sponsorAvailability,
87
+ label: "Describe your typical availability and limits: ",
88
+ interactive,
89
+ rl,
90
+ });
91
+ const providers = normalizeProviderList(options.sponsorProviders);
92
+ if (providers.length === 0) {
93
+ throw new Error(
94
+ "Choose at least one provider with --providers codex,claude-code.",
95
+ );
96
+ }
97
+ const accepted = options.sponsorAcceptAgreement
98
+ ? true
99
+ : interactive
100
+ ? await confirmExactAgreement(rl)
101
+ : false;
102
+ if (!accepted) {
103
+ throw new Error(
104
+ "The sponsor agreement must be explicitly accepted. Re-run with --accept-agreement after reading it.",
105
+ );
106
+ }
107
+ const application = await sponsorRequest({
108
+ options,
109
+ auth,
110
+ method: "POST",
111
+ path: "/applications",
112
+ body: {
113
+ motivation,
114
+ availability,
115
+ agreementVersion: agreement.version,
116
+ agreementAccepted: true,
117
+ capabilities: {
118
+ providers,
119
+ notes: options.sponsorCapabilityNotes || null,
120
+ usesOwnSubscription: true,
121
+ willReportActualModel: true,
122
+ willProtectUserData: true,
123
+ acceptsIntegrityReview: true,
124
+ },
125
+ },
126
+ });
127
+ printJsonOrLines(options, application, [
128
+ `Sponsor application #${application.id} submitted.`,
129
+ "Status: pending administrator review",
130
+ "Sponsorship remains unavailable until the application is approved.",
131
+ ]);
132
+ } finally {
133
+ rl?.close();
134
+ }
135
+ }
136
+
137
+ async function withdrawSponsorApplication(options) {
138
+ const auth = await resolveAuth(options);
139
+ if (!options.assumeYes && input.isTTY && output.isTTY) {
140
+ const rl = readline.createInterface({ input, output });
141
+ try {
142
+ const answer = await rl.question(
143
+ "Withdraw your current sponsor application? Type yes to continue: ",
144
+ );
145
+ if (answer.trim().toLowerCase() !== "yes") {
146
+ console.log("Sponsor application unchanged.");
147
+ return;
148
+ }
149
+ } finally {
150
+ rl.close();
151
+ }
152
+ } else if (!options.assumeYes) {
153
+ throw new Error("Pass --yes to withdraw non-interactively.");
154
+ }
155
+ const result = await sponsorRequest({
156
+ options,
157
+ auth,
158
+ method: "DELETE",
159
+ path: "/applications",
160
+ });
161
+ printJsonOrLines(options, result, ["Sponsor application withdrawn."]);
162
+ }
163
+
164
+ async function updateSponsorCapacity(options) {
165
+ const auth = await resolveAuth(options);
166
+ const status = await sponsorRequest({ options, auth, path: "/status" });
167
+ if (!status.sponsor) {
168
+ throw new Error("An approved sponsor account is required first.");
169
+ }
170
+ const current = status.sponsor.capacity || {};
171
+ const body = {
172
+ maxConcurrentTasks: integerOption(
173
+ options.sponsorConcurrency,
174
+ current.maxConcurrentTasks,
175
+ "--concurrency",
176
+ ),
177
+ maxSubagentsPerTask: integerOption(
178
+ options.sponsorHelpers,
179
+ current.maxSubagentsPerTask,
180
+ "--helpers",
181
+ { allowZero: true },
182
+ ),
183
+ dailyTaskLimit: integerOption(
184
+ options.sponsorDailyLimit,
185
+ current.dailyTaskLimit,
186
+ "--daily-limit",
187
+ ),
188
+ weeklyTaskLimit: integerOption(
189
+ options.sponsorWeeklyLimit,
190
+ current.weeklyTaskLimit,
191
+ "--weekly-limit",
192
+ ),
193
+ };
194
+ const capacity = await sponsorRequest({
195
+ options,
196
+ auth,
197
+ method: "PUT",
198
+ path: "/duty/capacity",
199
+ body,
200
+ });
201
+ printJsonOrLines(options, capacity, [
202
+ "Sponsor capacity updated from canonical server state.",
203
+ `Concurrent tasks: ${capacity.maxConcurrentTasks}`,
204
+ `Helpers per task: ${capacity.maxSubagentsPerTask}`,
205
+ `Daily limit: ${capacity.dailyTaskLimit}`,
206
+ `Weekly limit: ${capacity.weeklyTaskLimit}`,
207
+ ]);
208
+ }
209
+
210
+ async function sponsorDutyCommand(options, args, commandServices) {
211
+ const action = String(args[0] || "status")
212
+ .trim()
213
+ .toLowerCase();
214
+ if (action === "status") {
215
+ await printSponsorStatus(options);
216
+ return;
217
+ }
218
+ const persona = normalizePersona(args[1] || options.sponsorPersona);
219
+ const auth = await resolveAuth(options);
220
+ if (["pause", "resume", "stop"].includes(action)) {
221
+ const state =
222
+ action === "resume" ? "active" : action === "stop" ? "stopped" : "paused";
223
+ const result = await sponsorRequest({
224
+ options,
225
+ auth,
226
+ method: "POST",
227
+ path: `/duty/${persona}/state`,
228
+ body: { state },
229
+ });
230
+ printJsonOrLines(options, result, [
231
+ `${displayPersona(persona)} duty is now ${state}.`,
232
+ ]);
233
+ return;
234
+ }
235
+ if (action !== "start") throw new Error(sponsorUsage());
236
+ if (options.json) {
237
+ throw new Error("A foreground sponsor duty cannot run with --json.");
238
+ }
239
+ const provider = normalizeProvider(options.provider);
240
+ if (provider !== "codex" && options.sponsorServiceTier) {
241
+ throw new Error(
242
+ "--service-tier is currently supported only by the Codex duty adapter.",
243
+ );
244
+ }
245
+ const started = await sponsorRequest({
246
+ options,
247
+ auth,
248
+ method: "POST",
249
+ path: "/duty/start",
250
+ body: {
251
+ persona,
252
+ provider,
253
+ requestedModel: options.model || null,
254
+ requestedEffort: options.sponsorEffort || null,
255
+ requestedServiceTier: options.sponsorServiceTier || null,
256
+ cliVersion: options.lumineCli?.version || null,
257
+ },
258
+ });
259
+ await runSponsorDuty({
260
+ options,
261
+ auth,
262
+ persona,
263
+ provider,
264
+ started,
265
+ commandServices,
266
+ });
267
+ }
268
+
269
+ async function runSponsorDuty({
270
+ options,
271
+ auth,
272
+ persona,
273
+ provider,
274
+ started,
275
+ commandServices,
276
+ }) {
277
+ const dutyId = Number(started.duty?.id || 0);
278
+ const leaseToken = String(started.leaseToken || "");
279
+ if (!dutyId || !leaseToken) {
280
+ throw new Error("Twinkle did not return a valid sponsor duty lease.");
281
+ }
282
+ let canonicalDuty = started.duty;
283
+ let stopRequested = false;
284
+ let dutyLeaseError = null;
285
+ let dutyHeartbeatRunning = false;
286
+ const activeJobs = new Set();
287
+ const signalHandler = () => {
288
+ if (!stopRequested) {
289
+ stopRequested = true;
290
+ console.log("\nStopping after active Workshop jobs finish...");
291
+ }
292
+ };
293
+ process.on("SIGINT", signalHandler);
294
+ process.on("SIGTERM", signalHandler);
295
+
296
+ const heartbeatDuty = async () => {
297
+ if (dutyHeartbeatRunning) return;
298
+ dutyHeartbeatRunning = true;
299
+ try {
300
+ canonicalDuty = await sponsorRequest({
301
+ options,
302
+ auth,
303
+ method: "POST",
304
+ path: `/duty/${dutyId}/heartbeat`,
305
+ body: { leaseToken },
306
+ });
307
+ dutyLeaseError = null;
308
+ if (canonicalDuty.state === "stopped") stopRequested = true;
309
+ } catch (error) {
310
+ const status = await sponsorRequest({
311
+ options,
312
+ auth,
313
+ path: "/status",
314
+ }).catch(() => null);
315
+ const confirmedDuty = (status?.duties || []).find(
316
+ (duty) => Number(duty.id) === dutyId,
317
+ );
318
+ if (confirmedDuty?.state === "stopped") {
319
+ canonicalDuty = confirmedDuty;
320
+ dutyLeaseError = null;
321
+ stopRequested = true;
322
+ return;
323
+ }
324
+ dutyLeaseError = error;
325
+ stopRequested = true;
326
+ } finally {
327
+ dutyHeartbeatRunning = false;
328
+ }
329
+ };
330
+ const dutyHeartbeat = setInterval(
331
+ () => void heartbeatDuty(),
332
+ Math.max(10, Number(started.heartbeatEverySeconds || 30)) * 1_000,
333
+ );
334
+
335
+ console.log(
336
+ `${displayPersona(persona)} sponsor duty started with ${provider}.`,
337
+ );
338
+ printDutyRuntime(canonicalDuty);
339
+ console.log(
340
+ "The Workshop is now discoverable to users. Press Ctrl-C for a graceful stop.",
341
+ );
342
+
343
+ try {
344
+ while (!stopRequested) {
345
+ if (canonicalDuty.state !== "active") {
346
+ await sleep(options.sponsorPollMs || DEFAULT_DUTY_POLL_MS);
347
+ continue;
348
+ }
349
+ const concurrency = Math.max(
350
+ 1,
351
+ Number(canonicalDuty.capacity?.maxConcurrentTasks || 1),
352
+ );
353
+ while (!stopRequested && activeJobs.size < concurrency) {
354
+ const claim = await sponsorRequest({
355
+ options,
356
+ auth,
357
+ method: "POST",
358
+ path: "/jobs/claim",
359
+ body: { dutySessionId: dutyId, leaseToken },
360
+ });
361
+ if (!claim.job) break;
362
+ const work = processSponsorJob({
363
+ options,
364
+ sponsorAuth: auth,
365
+ dutyId,
366
+ dutyLeaseToken: leaseToken,
367
+ persona,
368
+ provider,
369
+ claim,
370
+ commandServices,
371
+ })
372
+ .catch((error) => {
373
+ console.error(
374
+ `Workshop job #${claim.job.id} ended: ${error?.message || error}`,
375
+ );
376
+ })
377
+ .finally(() => activeJobs.delete(work));
378
+ activeJobs.add(work);
379
+ }
380
+ await sleep(options.sponsorPollMs || DEFAULT_DUTY_POLL_MS);
381
+ }
382
+ if (!dutyLeaseError && canonicalDuty.state === "active") {
383
+ const paused = await sponsorRequest({
384
+ options,
385
+ auth,
386
+ method: "POST",
387
+ path: `/duty/${persona}/state`,
388
+ body: { state: "paused" },
389
+ });
390
+ canonicalDuty = paused.duty || canonicalDuty;
391
+ }
392
+ await Promise.allSettled(Array.from(activeJobs));
393
+ if (dutyLeaseError) throw dutyLeaseError;
394
+ } finally {
395
+ clearInterval(dutyHeartbeat);
396
+ process.off("SIGINT", signalHandler);
397
+ process.off("SIGTERM", signalHandler);
398
+ if (canonicalDuty.state !== "stopped") {
399
+ await sponsorRequest({
400
+ options,
401
+ auth,
402
+ method: "POST",
403
+ path: `/duty/${persona}/state`,
404
+ body: { state: "stopped" },
405
+ }).catch((error) => {
406
+ if (!dutyLeaseError) {
407
+ console.error(
408
+ `Could not mark duty stopped: ${error?.message || error}`,
409
+ );
410
+ }
411
+ });
412
+ }
413
+ }
414
+ console.log(`${displayPersona(persona)} sponsor duty stopped.`);
415
+ }
416
+
417
+ async function processSponsorJob({
418
+ options,
419
+ sponsorAuth,
420
+ dutyId,
421
+ dutyLeaseToken,
422
+ persona,
423
+ provider,
424
+ claim,
425
+ commandServices,
426
+ }) {
427
+ const job = claim.job;
428
+ const jobId = Number(job.id);
429
+ const attemptToken = String(claim.attempt?.token || "");
430
+ const workspaceToken = String(claim.workspaceToken?.accessToken || "");
431
+ if (!jobId || !attemptToken || !workspaceToken) {
432
+ throw new Error("Twinkle returned an incomplete Workshop job lease.");
433
+ }
434
+ const runtime = {
435
+ provider: String(claim.runtime?.provider || ""),
436
+ requestedModel: String(claim.runtime?.requestedModel || "").trim() || null,
437
+ requestedEffort:
438
+ String(claim.runtime?.requestedEffort || "").trim() || null,
439
+ requestedServiceTier:
440
+ String(claim.runtime?.requestedServiceTier || "").trim() || null,
441
+ };
442
+ if (!PROVIDERS.has(runtime.provider) || runtime.provider !== provider) {
443
+ throw new Error(
444
+ "Twinkle returned a Workshop runtime that does not match this duty.",
445
+ );
446
+ }
447
+ const relayById = new Map(
448
+ (claim.relays || []).map((relay) => [relay.id, relay]),
449
+ );
450
+ let heartbeatError = null;
451
+ let heartbeatRunning = false;
452
+ const jobAbortController = new AbortController();
453
+ const heartbeatEveryMs =
454
+ Math.max(10, Number(claim.heartbeatEverySeconds || 30)) * 1_000;
455
+ let lastHeartbeatSuccessAt = Date.now();
456
+ const abortWorkerIfLeaseIsGone = (error) => {
457
+ const status = Number(error?.status || 0);
458
+ const serverRejectedLease = [401, 403, 409].includes(status);
459
+ const leaseWindowElapsed =
460
+ Date.now() - lastHeartbeatSuccessAt >=
461
+ Math.max(120_000, heartbeatEveryMs * 3);
462
+ if (
463
+ !jobAbortController.signal.aborted &&
464
+ (serverRejectedLease || leaseWindowElapsed)
465
+ ) {
466
+ jobAbortController.abort(
467
+ error instanceof Error
468
+ ? error
469
+ : new Error("The sponsored Workshop lease ended."),
470
+ );
471
+ }
472
+ };
473
+ const heartbeatJob = async () => {
474
+ if (heartbeatRunning) return null;
475
+ heartbeatRunning = true;
476
+ try {
477
+ const result = await sponsorJobRequest({
478
+ options,
479
+ sponsorAuth,
480
+ dutyId,
481
+ dutyLeaseToken,
482
+ jobId,
483
+ attemptToken,
484
+ path: "/heartbeat",
485
+ });
486
+ heartbeatError = null;
487
+ lastHeartbeatSuccessAt = Date.now();
488
+ for (const relay of result.relays || []) relayById.set(relay.id, relay);
489
+ return result;
490
+ } catch (error) {
491
+ heartbeatError = error;
492
+ abortWorkerIfLeaseIsGone(error);
493
+ return null;
494
+ } finally {
495
+ heartbeatRunning = false;
496
+ }
497
+ };
498
+ const tempDir = await fs.mkdtemp(
499
+ path.join(os.tmpdir(), `lumine-${persona}-job-${jobId}-`),
500
+ );
501
+ const workspaceDir = path.join(tempDir, "workspace");
502
+ const authFile = path.join(tempDir, "job-auth.json");
503
+ const jobOptions = {
504
+ ...options,
505
+ authToken: null,
506
+ authFile,
507
+ dir: workspaceDir,
508
+ target: String(job.contributionBuild.id),
509
+ buildIdFlag: String(job.contributionBuild.id),
510
+ provider: runtime.provider,
511
+ model: runtime.requestedModel || "",
512
+ agentEffort: runtime.requestedEffort || "",
513
+ serviceTier: runtime.requestedServiceTier || "",
514
+ agentAbortSignal: jobAbortController.signal,
515
+ reviewLoop: false,
516
+ openBrowser: false,
517
+ skipAssetManifest: true,
518
+ };
519
+ // Do not start a long-lived lease timer until the owned temporary workspace
520
+ // exists. If temp creation fails there is then no interval keeping the duty
521
+ // process alive after this job promise rejects.
522
+ const heartbeatTimer = setInterval(
523
+ () => void heartbeatJob(),
524
+ heartbeatEveryMs,
525
+ );
526
+ const logicalAgents = [];
527
+ let lastSave = null;
528
+
529
+ console.log(
530
+ `Claimed Workshop job #${jobId} for ${job.requester.username || `user ${job.requester.userId}`} (${job.contributionBuild.title}).`,
531
+ );
532
+ try {
533
+ await writeAuthFile(jobOptions, {
534
+ token: workspaceToken,
535
+ username: claim.workspaceToken?.user?.username || displayPersona(persona),
536
+ userId: claim.workspaceToken?.user?.id || job.personaUserId,
537
+ expiresAt: Number(claim.workspaceToken?.expiresAt || 0) * 1_000,
538
+ apiUrl: options.apiUrl,
539
+ createdAt: new Date().toISOString(),
540
+ });
541
+ const jobAuth = await resolveAuth(jobOptions);
542
+ await commandServices.pullWorkspace({
543
+ options: jobOptions,
544
+ auth: jobAuth,
545
+ buildId: Number(job.contributionBuild.id),
546
+ });
547
+ const forumContext = await loadForumContext({
548
+ options: jobOptions,
549
+ auth: jobAuth,
550
+ buildId: Number(job.contributionBuild.id),
551
+ });
552
+ const coordinator = await startLogicalAgent({
553
+ options,
554
+ sponsorAuth,
555
+ dutyId,
556
+ jobId,
557
+ attemptToken,
558
+ runtime,
559
+ role: "coordinator",
560
+ ordinal: 0,
561
+ parentAgentId: null,
562
+ });
563
+ logicalAgents.push(coordinator);
564
+ const appliedRelayIds = new Set();
565
+ const coordinatorResults = [];
566
+ const initialRelays = Array.from(relayById.values());
567
+ const initialResult = await runWorkshopAgentPass({
568
+ jobOptions,
569
+ saveWorkspace: commandServices.saveWorkspace,
570
+ job,
571
+ persona,
572
+ relays: initialRelays,
573
+ forumContext,
574
+ helperOrdinal: 0,
575
+ });
576
+ coordinatorResults.push(initialResult);
577
+ await acknowledgeRelaysApplied({
578
+ options,
579
+ sponsorAuth,
580
+ dutyId,
581
+ jobId,
582
+ attemptToken,
583
+ relays: initialRelays,
584
+ appliedRelayIds,
585
+ });
586
+ lastSave = initialResult.saveResult || lastSave;
587
+
588
+ const helperCount = Math.max(0, Number(job.requestedSubagents || 0));
589
+ for (let ordinal = 1; ordinal <= helperCount; ordinal += 1) {
590
+ await heartbeatJob();
591
+ if (heartbeatError) throw heartbeatError;
592
+ const helper = await startLogicalAgent({
593
+ options,
594
+ sponsorAuth,
595
+ dutyId,
596
+ jobId,
597
+ attemptToken,
598
+ runtime,
599
+ role: "helper",
600
+ ordinal,
601
+ parentAgentId: coordinator.agentId,
602
+ });
603
+ logicalAgents.push(helper);
604
+ try {
605
+ const helperRelays = Array.from(relayById.values());
606
+ const helperResult = await runWorkshopAgentPass({
607
+ jobOptions,
608
+ saveWorkspace: commandServices.saveWorkspace,
609
+ job,
610
+ persona,
611
+ relays: helperRelays,
612
+ forumContext,
613
+ helperOrdinal: ordinal,
614
+ });
615
+ await acknowledgeRelaysApplied({
616
+ options,
617
+ sponsorAuth,
618
+ dutyId,
619
+ jobId,
620
+ attemptToken,
621
+ relays: helperRelays,
622
+ appliedRelayIds,
623
+ });
624
+ lastSave = helperResult.saveResult || lastSave;
625
+ await completeLogicalAgent({
626
+ options,
627
+ sponsorAuth,
628
+ dutyId,
629
+ jobId,
630
+ attemptToken,
631
+ agent: helper,
632
+ status: "completed",
633
+ results: [helperResult],
634
+ });
635
+ helper.completed = true;
636
+ } catch (error) {
637
+ await completeLogicalAgent({
638
+ options,
639
+ sponsorAuth,
640
+ dutyId,
641
+ jobId,
642
+ attemptToken,
643
+ agent: helper,
644
+ status: "failed",
645
+ results: [],
646
+ error,
647
+ }).catch(() => undefined);
648
+ helper.completed = true;
649
+ throw error;
650
+ }
651
+ }
652
+
653
+ let relayStreamClosed = false;
654
+ for (let pass = 0; pass < MAX_FOLLOW_UP_PASSES; pass += 1) {
655
+ await heartbeatJob();
656
+ if (heartbeatError) throw heartbeatError;
657
+ const pendingRelays = Array.from(relayById.values()).filter(
658
+ (relay) => !appliedRelayIds.has(relay.id),
659
+ );
660
+ if (pendingRelays.length > 0) {
661
+ const followUpResult = await runWorkshopAgentPass({
662
+ jobOptions,
663
+ saveWorkspace: commandServices.saveWorkspace,
664
+ job,
665
+ persona,
666
+ relays: pendingRelays,
667
+ forumContext: "",
668
+ helperOrdinal: 0,
669
+ isFollowUp: true,
670
+ });
671
+ coordinatorResults.push(followUpResult);
672
+ await acknowledgeRelaysApplied({
673
+ options,
674
+ sponsorAuth,
675
+ dutyId,
676
+ jobId,
677
+ attemptToken,
678
+ relays: pendingRelays,
679
+ appliedRelayIds,
680
+ });
681
+ lastSave = followUpResult.saveResult || lastSave;
682
+ }
683
+ const closure = await retrySponsorTransport(() =>
684
+ sponsorJobRequest({
685
+ options,
686
+ sponsorAuth,
687
+ dutyId,
688
+ jobId,
689
+ attemptToken,
690
+ path: "/relays/close",
691
+ }),
692
+ );
693
+ for (const relay of closure.relays || []) relayById.set(relay.id, relay);
694
+ if (closure.closed) {
695
+ relayStreamClosed = true;
696
+ break;
697
+ }
698
+ }
699
+ if (!relayStreamClosed) {
700
+ throw new Error(
701
+ "The Workshop kept receiving follow-ups and could not close a fully applied relay stream safely.",
702
+ );
703
+ }
704
+
705
+ await completeLogicalAgent({
706
+ options,
707
+ sponsorAuth,
708
+ dutyId,
709
+ jobId,
710
+ attemptToken,
711
+ agent: coordinator,
712
+ status: "completed",
713
+ results: coordinatorResults,
714
+ });
715
+ coordinator.completed = true;
716
+ if (!lastSave?.artifactVersion?.versionId || !lastSave?.filesHash) {
717
+ throw new Error(
718
+ "The agent did not produce a new canonical saved artifact for this Workshop job.",
719
+ );
720
+ }
721
+ const outcomeSummary = summarizeWorkshopOutcome(
722
+ coordinatorResults.at(-1)?.finalText,
723
+ job,
724
+ );
725
+ const branchNoticeMessageId = await ensureCanonicalBranchNotice({
726
+ options,
727
+ sponsorAuth,
728
+ dutyId,
729
+ jobId,
730
+ attemptToken,
731
+ jobOptions,
732
+ jobAuth,
733
+ job,
734
+ outcomeSummary,
735
+ });
736
+ await retrySponsorTransport(() =>
737
+ sponsorJobRequest({
738
+ options,
739
+ sponsorAuth,
740
+ dutyId,
741
+ jobId,
742
+ attemptToken,
743
+ path: "/complete",
744
+ body: {
745
+ artifactVersionId: Number(lastSave.artifactVersion.versionId),
746
+ branchNoticeMessageId,
747
+ outcomeSummary,
748
+ reportedFilesHash: lastSave.filesHash,
749
+ },
750
+ }),
751
+ );
752
+ console.log(
753
+ `Completed Workshop job #${jobId}; ${displayPersona(persona)} sent the branch update.`,
754
+ );
755
+ } catch (error) {
756
+ for (const agent of logicalAgents.filter((item) => !item.completed)) {
757
+ await completeLogicalAgent({
758
+ options,
759
+ sponsorAuth,
760
+ dutyId,
761
+ jobId,
762
+ attemptToken,
763
+ agent,
764
+ status: "failed",
765
+ results: [],
766
+ error,
767
+ }).catch(() => undefined);
768
+ agent.completed = true;
769
+ }
770
+ await retrySponsorTransport(() =>
771
+ sponsorJobRequest({
772
+ options,
773
+ sponsorAuth,
774
+ dutyId,
775
+ jobId,
776
+ attemptToken,
777
+ path: "/fail",
778
+ body: {
779
+ failureCode: "worker_failed",
780
+ failureReason: String(error?.message || error).slice(0, 1000),
781
+ },
782
+ }),
783
+ ).catch(() => undefined);
784
+ throw error;
785
+ } finally {
786
+ clearInterval(heartbeatTimer);
787
+ try {
788
+ await fs.rm(tempDir, { recursive: true, force: true });
789
+ } catch (error) {
790
+ console.error(
791
+ `lumine: could not remove temporary Workshop directory ${tempDir} (${error?.message || error})`,
792
+ );
793
+ }
794
+ }
795
+ }
796
+
797
+ async function runWorkshopAgentPass({
798
+ jobOptions,
799
+ saveWorkspace,
800
+ job,
801
+ persona,
802
+ relays,
803
+ forumContext,
804
+ helperOrdinal,
805
+ isFollowUp = false,
806
+ }) {
807
+ const prompt = workshopAgentPrompt({
808
+ job,
809
+ persona,
810
+ relays,
811
+ forumContext,
812
+ helperOrdinal,
813
+ isFollowUp,
814
+ });
815
+ return await agentCommand(
816
+ { ...jobOptions, agentPrompt: prompt },
817
+ { saveWorkspace },
818
+ );
819
+ }
820
+
821
+ async function startLogicalAgent({
822
+ options,
823
+ sponsorAuth,
824
+ dutyId,
825
+ jobId,
826
+ attemptToken,
827
+ runtime,
828
+ role,
829
+ ordinal,
830
+ parentAgentId,
831
+ }) {
832
+ return await retrySponsorTransport(() =>
833
+ sponsorJobRequest({
834
+ options,
835
+ sponsorAuth,
836
+ dutyId,
837
+ jobId,
838
+ attemptToken,
839
+ path: "/agents",
840
+ body: {
841
+ role,
842
+ ordinal,
843
+ parentAgentId,
844
+ provider: runtime.provider,
845
+ requestedModel: runtime.requestedModel,
846
+ requestedEffort: runtime.requestedEffort,
847
+ requestedServiceTier: runtime.requestedServiceTier,
848
+ },
849
+ }),
850
+ );
851
+ }
852
+
853
+ async function completeLogicalAgent({
854
+ options,
855
+ sponsorAuth,
856
+ dutyId,
857
+ jobId,
858
+ attemptToken,
859
+ agent,
860
+ status,
861
+ results,
862
+ error = null,
863
+ }) {
864
+ const provenance = summarizeProviderRuns(results);
865
+ const allChangedPaths = Array.from(
866
+ new Set(results.flatMap((result) => result.changedPaths || [])),
867
+ );
868
+ return await retrySponsorTransport(() =>
869
+ sponsorJobRequest({
870
+ options,
871
+ sponsorAuth,
872
+ dutyId,
873
+ jobId,
874
+ attemptToken,
875
+ path: `/agents/${agent.agentId}/complete`,
876
+ body: {
877
+ status,
878
+ resolvedModel: provenance.resolvedModel,
879
+ resolvedEffort: provenance.resolvedEffort,
880
+ resolvedServiceTier: provenance.resolvedServiceTier,
881
+ runtimeVersion: provenance.runtimeVersion,
882
+ evidenceTier: provenance.evidenceTier,
883
+ usage: provenance.usage,
884
+ outcome: {
885
+ providerPassCount: provenance.providerPassCount,
886
+ changedPathCount: allChangedPaths.length,
887
+ changedPaths: allChangedPaths
888
+ .map((changedPath) => String(changedPath || "").slice(0, 300))
889
+ .filter(Boolean)
890
+ .slice(0, 50),
891
+ finalText: String(results.at(-1)?.finalText || "").slice(0, 2000),
892
+ ...(error
893
+ ? { error: String(error?.message || error).slice(0, 1000) }
894
+ : {}),
895
+ },
896
+ },
897
+ }),
898
+ );
899
+ }
900
+
901
+ async function acknowledgeRelaysApplied({
902
+ options,
903
+ sponsorAuth,
904
+ dutyId,
905
+ jobId,
906
+ attemptToken,
907
+ relays,
908
+ appliedRelayIds,
909
+ }) {
910
+ const relayIds = Array.from(
911
+ new Set(relays.map((relay) => Number(relay.id)).filter((id) => id > 0)),
912
+ );
913
+ if (relayIds.length === 0) return;
914
+ for (let index = 0; index < relayIds.length; index += 50) {
915
+ const relayIdBatch = relayIds.slice(index, index + 50);
916
+ const receipt = await retrySponsorTransport(() =>
917
+ sponsorJobRequest({
918
+ options,
919
+ sponsorAuth,
920
+ dutyId,
921
+ jobId,
922
+ attemptToken,
923
+ path: "/relays/applied",
924
+ body: { relayIds: relayIdBatch },
925
+ }),
926
+ );
927
+ const canonicalIds = new Set(
928
+ (receipt.appliedRelayIds || []).map((relayId) => Number(relayId)),
929
+ );
930
+ if (relayIdBatch.some((relayId) => !canonicalIds.has(relayId))) {
931
+ throw new Error("Twinkle did not confirm every applied Workshop relay.");
932
+ }
933
+ for (const relayId of canonicalIds) appliedRelayIds.add(relayId);
934
+ }
935
+ }
936
+
937
+ async function ensureCanonicalBranchNotice({
938
+ options,
939
+ sponsorAuth,
940
+ dutyId,
941
+ jobId,
942
+ attemptToken,
943
+ jobOptions,
944
+ jobAuth,
945
+ job,
946
+ outcomeSummary,
947
+ }) {
948
+ const lookup = () =>
949
+ sponsorJobRequest({
950
+ options,
951
+ sponsorAuth,
952
+ dutyId,
953
+ jobId,
954
+ attemptToken,
955
+ path: "/branch-notice",
956
+ });
957
+ const existing = await lookup();
958
+ const existingMessageId = Number(existing.branchNotice?.id || 0);
959
+ if (existingMessageId) return existingMessageId;
960
+
961
+ try {
962
+ const notice = await notifyBuildOwnerOfContribution({
963
+ options: jobOptions,
964
+ auth: jobAuth,
965
+ rootBuildId: Number(job.rootBuild.id),
966
+ contributionBuildId: Number(job.contributionBuild.id),
967
+ note: outcomeSummary,
968
+ });
969
+ const messageId = Number(notice?.message?.id || 0);
970
+ if (!messageId) {
971
+ throw new Error("Twinkle did not confirm the branch update message.");
972
+ }
973
+ return messageId;
974
+ } catch (error) {
975
+ for (const delayMs of [250, 750, 1_500]) {
976
+ await sleep(delayMs);
977
+ const recovered = await lookup().catch(() => null);
978
+ const recoveredMessageId = Number(recovered?.branchNotice?.id || 0);
979
+ if (recoveredMessageId) return recoveredMessageId;
980
+ }
981
+ throw error;
982
+ }
983
+ }
984
+
985
+ function summarizeProviderRuns(results) {
986
+ const runs = results.flatMap((result) => result.providerRuns || []);
987
+ const lastRun = runs.at(-1) || {};
988
+ const valueFromLastRun = (key, maximum) =>
989
+ String(lastRun?.[key] || "")
990
+ .trim()
991
+ .slice(0, maximum) || null;
992
+ const evidenceTiers = runs.map((run) => run?.evidenceTier);
993
+ const evidenceTier =
994
+ evidenceTiers.length > 0 &&
995
+ evidenceTiers.every((tier) => tier === "runtime_observed")
996
+ ? "runtime_observed"
997
+ : evidenceTiers.length > 0 &&
998
+ evidenceTiers.every((tier) =>
999
+ ["runtime_observed", "provider_reported"].includes(tier),
1000
+ )
1001
+ ? "provider_reported"
1002
+ : "requested_only";
1003
+ return {
1004
+ resolvedModel: valueFromLastRun("resolvedModel", 160),
1005
+ resolvedEffort: valueFromLastRun("resolvedEffort", 40),
1006
+ resolvedServiceTier: valueFromLastRun("resolvedServiceTier", 40),
1007
+ runtimeVersion: valueFromLastRun("runtimeVersion", 64),
1008
+ evidenceTier,
1009
+ providerPassCount: runs.length,
1010
+ usage: {
1011
+ passes: runs.slice(0, 20).map((run) => ({
1012
+ phase: run.phase || null,
1013
+ resolvedModel: run.resolvedModel || null,
1014
+ resolvedEffort: run.resolvedEffort || null,
1015
+ resolvedServiceTier: run.resolvedServiceTier || null,
1016
+ runtimeVersion: run.runtimeVersion || null,
1017
+ evidenceTier: run.evidenceTier || "requested_only",
1018
+ usage: run.usage || {},
1019
+ })),
1020
+ },
1021
+ };
1022
+ }
1023
+
1024
+ async function loadForumContext({ options, auth, buildId }) {
1025
+ try {
1026
+ const snapshot = await readCompleteBuildForumSnapshot({
1027
+ options: { ...options, limit: 100 },
1028
+ auth,
1029
+ buildId,
1030
+ maxPages: 100,
1031
+ });
1032
+ const events = (snapshot.events || []).slice(-50);
1033
+ if (events.length === 0) return "";
1034
+ return JSON.stringify(events).slice(0, MAX_FORUM_CONTEXT_CHARS);
1035
+ } catch (error) {
1036
+ console.error(
1037
+ `lumine: scoped Forum context unavailable (${error?.message || error})`,
1038
+ );
1039
+ return "";
1040
+ }
1041
+ }
1042
+
1043
+ function workshopAgentPrompt({
1044
+ job,
1045
+ persona,
1046
+ relays,
1047
+ forumContext,
1048
+ helperOrdinal,
1049
+ isFollowUp,
1050
+ }) {
1051
+ const relayText = relays
1052
+ .map((relay, index) => {
1053
+ const details = [
1054
+ relay.summary,
1055
+ relay.requestedOutcome ? `Outcome: ${relay.requestedOutcome}` : "",
1056
+ relay.constraints?.length
1057
+ ? `Constraints: ${relay.constraints.join("; ")}`
1058
+ : "",
1059
+ relay.acceptanceCriteria?.length
1060
+ ? `Acceptance: ${relay.acceptanceCriteria.join("; ")}`
1061
+ : "",
1062
+ ].filter(Boolean);
1063
+ return `${index + 1}. ${details.join("\n ")}`;
1064
+ })
1065
+ .join("\n");
1066
+ const role =
1067
+ helperOrdinal > 0
1068
+ ? `You are helper ${helperOrdinal}. Review the current implementation and fix concrete gaps.`
1069
+ : isFollowUp
1070
+ ? "Apply these newly relayed follow-up requirements to the current implementation."
1071
+ : `Implement this hands-on Lumine Build request on ${displayPersona(persona)}'s assigned contribution branch.`;
1072
+ return `${role}
1073
+
1074
+ The user owns Build #${job.rootBuild.id}; you may edit only contribution Build #${job.contributionBuild.id}. Do not merge, replace Main, publish, contact the user directly, or infer anything from private chat. The text below is the structured relay explicitly approved for this named sponsor. Preserve existing sound UX and leave a complete, validated project.
1075
+
1076
+ Approved relay:
1077
+ ${relayText || "No additional relay text was supplied."}
1078
+ ${forumContext ? `\nScoped Build Forum snapshot (project-visible context only):\n${forumContext}\n` : ""}
1079
+ Return a short factual outcome summary after saving the canonical branch.`;
1080
+ }
1081
+
1082
+ async function sponsorJobRequest({
1083
+ options,
1084
+ sponsorAuth,
1085
+ dutyId,
1086
+ dutyLeaseToken,
1087
+ jobId,
1088
+ attemptToken,
1089
+ path: suffix,
1090
+ body = {},
1091
+ }) {
1092
+ return await sponsorRequest({
1093
+ options,
1094
+ auth: sponsorAuth,
1095
+ method: "POST",
1096
+ path: `/jobs/${jobId}${suffix}`,
1097
+ body: {
1098
+ dutySessionId: dutyId,
1099
+ attemptToken,
1100
+ ...(dutyLeaseToken ? { dutyLeaseToken } : {}),
1101
+ ...body,
1102
+ },
1103
+ });
1104
+ }
1105
+
1106
+ async function retrySponsorTransport(operation) {
1107
+ let lastError;
1108
+ for (const delayMs of [0, 250, 750, 1_500]) {
1109
+ if (delayMs) await sleep(delayMs);
1110
+ try {
1111
+ return await operation();
1112
+ } catch (error) {
1113
+ if (Number(error?.status || 0) > 0) throw error;
1114
+ lastError = error;
1115
+ }
1116
+ }
1117
+ throw lastError;
1118
+ }
1119
+
1120
+ async function sponsorRequest({
1121
+ options,
1122
+ auth,
1123
+ method = "GET",
1124
+ path: suffix,
1125
+ body,
1126
+ }) {
1127
+ return await requestJson({
1128
+ method,
1129
+ url: `${options.apiUrl}${SPONSOR_PATH}${suffix}`,
1130
+ authToken: auth.token,
1131
+ body,
1132
+ timeoutMs: options.timeoutMs,
1133
+ });
1134
+ }
1135
+
1136
+ async function printSponsorStatus(options) {
1137
+ const auth = await resolveAuth(options);
1138
+ const status = await sponsorRequest({ options, auth, path: "/status" });
1139
+ if (options.json) {
1140
+ console.log(JSON.stringify(status, null, 2));
1141
+ return;
1142
+ }
1143
+ console.log(`Application: ${status.application?.status || "not submitted"}`);
1144
+ console.log(`Sponsor access: ${status.sponsor?.status || "not approved"}`);
1145
+ if (status.sponsor?.capacity) {
1146
+ const capacity = status.sponsor.capacity;
1147
+ console.log(
1148
+ `Capacity: ${capacity.maxConcurrentTasks} concurrent, ${capacity.maxSubagentsPerTask} helpers, ${capacity.dailyTaskLimit}/day, ${capacity.weeklyTaskLimit}/week`,
1149
+ );
1150
+ console.log(
1151
+ `Usage: ${status.usage?.dailyStarted || 0} today, ${status.usage?.weeklyStarted || 0} this week, ${status.usage?.activeTasks || 0} active`,
1152
+ );
1153
+ }
1154
+ for (const duty of status.duties || []) {
1155
+ console.log(
1156
+ `Duty #${duty.id}: user ${duty.personaUserId} ${duty.state} · ${duty.provider} · model=${duty.requestedModel || "provider default"} · effort=${duty.requestedEffort || "provider default"}`,
1157
+ );
1158
+ }
1159
+ }
1160
+
1161
+ function printSponsorAgreement(agreement, options) {
1162
+ if (options.json) {
1163
+ console.log(JSON.stringify(agreement, null, 2));
1164
+ return;
1165
+ }
1166
+ console.log(`Lumine sponsor agreement ${agreement.version}`);
1167
+ for (const line of agreement.disclosure || []) console.log(`- ${line}`);
1168
+ }
1169
+
1170
+ function printSponsorJobs(jobs, options) {
1171
+ if (options.json) {
1172
+ console.log(JSON.stringify(jobs, null, 2));
1173
+ return;
1174
+ }
1175
+ if (!Array.isArray(jobs) || jobs.length === 0) {
1176
+ console.log("No sponsored Workshop jobs yet.");
1177
+ return;
1178
+ }
1179
+ for (const job of jobs) {
1180
+ console.log(
1181
+ `#${job.id} ${job.status} · ${displayPersona(job.persona)} · ${job.requester?.username || `user ${job.requester?.userId}`} · ${job.contributionBuild?.title || `Build ${job.contributionBuild?.id}`}`,
1182
+ );
1183
+ }
1184
+ }
1185
+
1186
+ function printDutyRuntime(duty) {
1187
+ console.log(
1188
+ `Runtime: model=${duty.requestedModel || "provider default"}, effort=${duty.requestedEffort || "provider default"}, service-tier=${duty.requestedServiceTier || "provider default"}`,
1189
+ );
1190
+ console.log(
1191
+ `Limits: ${duty.capacity?.maxConcurrentTasks || 1} concurrent, ${duty.capacity?.maxSubagentsPerTask || 0} helpers, ${duty.capacity?.dailyTaskLimit || 0}/day, ${duty.capacity?.weeklyTaskLimit || 0}/week`,
1192
+ );
1193
+ }
1194
+
1195
+ function printJsonOrLines(options, value, lines) {
1196
+ if (options.json) console.log(JSON.stringify(value, null, 2));
1197
+ else for (const line of lines) console.log(line);
1198
+ }
1199
+
1200
+ async function requiredApplicationText({ supplied, label, interactive, rl }) {
1201
+ const value = String(supplied || "").trim();
1202
+ if (value) return value;
1203
+ if (!interactive) {
1204
+ throw new Error(
1205
+ "Non-interactive applications require --motivation and --availability.",
1206
+ );
1207
+ }
1208
+ const answer = String(await rl.question(label)).trim();
1209
+ if (!answer) throw new Error("Sponsor application answers cannot be empty.");
1210
+ return answer;
1211
+ }
1212
+
1213
+ async function confirmExactAgreement(rl) {
1214
+ const answer = await rl.question(
1215
+ "Type I ACCEPT to confirm this agreement and submit the application: ",
1216
+ );
1217
+ return answer.trim() === "I ACCEPT";
1218
+ }
1219
+
1220
+ function integerOption(value, fallback, label, { allowZero = false } = {}) {
1221
+ const selected =
1222
+ value === undefined || value === null || value === ""
1223
+ ? Number(fallback)
1224
+ : Number(value);
1225
+ if (!Number.isSafeInteger(selected) || selected < (allowZero ? 0 : 1)) {
1226
+ throw new Error(
1227
+ `${label} must be ${allowZero ? "a non-negative" : "a positive"} integer.`,
1228
+ );
1229
+ }
1230
+ return selected;
1231
+ }
1232
+
1233
+ function normalizeProviderList(value) {
1234
+ const submitted = String(value || "")
1235
+ .split(",")
1236
+ .map((provider) => provider.trim().toLowerCase())
1237
+ .filter(Boolean);
1238
+ const unsupported = submitted.find((provider) => !PROVIDERS.has(provider));
1239
+ if (unsupported) {
1240
+ throw new Error(`Unsupported provider: ${unsupported}.`);
1241
+ }
1242
+ return Array.from(new Set(submitted));
1243
+ }
1244
+
1245
+ function normalizeProvider(value) {
1246
+ const provider = String(value || "")
1247
+ .trim()
1248
+ .toLowerCase();
1249
+ if (!PROVIDERS.has(provider)) {
1250
+ throw new Error("Pass --provider codex or --provider claude-code.");
1251
+ }
1252
+ return provider;
1253
+ }
1254
+
1255
+ function normalizePersona(value) {
1256
+ const persona = String(value || "")
1257
+ .trim()
1258
+ .toLowerCase();
1259
+ if (!PERSONAS.has(persona)) {
1260
+ throw new Error("Choose a duty persona: zero or ciel.");
1261
+ }
1262
+ return persona;
1263
+ }
1264
+
1265
+ function displayPersona(persona) {
1266
+ return String(persona || "").toLowerCase() === "ciel" ? "Ciel" : "Zero";
1267
+ }
1268
+
1269
+ function summarizeWorkshopOutcome(finalText, job) {
1270
+ const summary = String(finalText || "")
1271
+ .trim()
1272
+ .slice(0, 1000);
1273
+ return (
1274
+ summary ||
1275
+ `Updated ${job.rootBuild.title} on the assigned contribution branch.`
1276
+ );
1277
+ }
1278
+
1279
+ function sponsorUsage() {
1280
+ return [
1281
+ "Usage:",
1282
+ " lumine sponsor agreement",
1283
+ " lumine sponsor apply --providers codex[,claude-code] [--motivation <text>] [--availability <text>] [--accept-agreement]",
1284
+ " lumine sponsor status",
1285
+ " lumine sponsor withdraw [--yes]",
1286
+ " 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>",
1289
+ " lumine sponsor jobs [--limit <n>]",
1290
+ ].join("\n");
1291
+ }