@stage5/lumine 0.2.57 → 0.2.58

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 CHANGED
@@ -1,22 +1,15 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
1
  import readline from "node:readline/promises";
5
2
  import { stdin as input, stdout as output } from "node:process";
6
3
 
7
- import { agentCommand } from "./agent.js";
8
- import { notifyBuildOwnerOfContribution } from "./api.js";
9
- import { ensureAuth, resolveAuth, writeAuthFile } from "./auth.js";
10
- import { readCompleteBuildForumSnapshot } from "./forum.js";
4
+ import { ensureAuth } from "./auth.js";
5
+ import {
6
+ sponsorDutyCommand,
7
+ sponsorJobCommand,
8
+ } from "./sponsor-duty.js";
11
9
  import { requestJson } from "./http.js";
12
- import { sleep } from "./util.js";
13
10
 
14
11
  const SPONSOR_PATH = "/cli/sponsor";
15
12
  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
13
 
21
14
  export async function sponsorCommand(options, commandServices) {
22
15
  const args = options.sponsorArgs || [];
@@ -24,6 +17,12 @@ export async function sponsorCommand(options, commandServices) {
24
17
  .trim()
25
18
  .toLowerCase();
26
19
  if (area === "agreement") {
20
+ if (String(args[1] || "").trim().toLowerCase() === "accept") {
21
+ if (args.length > 2) throw new Error(sponsorUsage());
22
+ await acceptSponsorAgreement(options);
23
+ return;
24
+ }
25
+ if (args.length > 1) throw new Error(sponsorUsage());
27
26
  const auth = await ensureAuth(options);
28
27
  const agreement = await sponsorRequest({
29
28
  options,
@@ -49,6 +48,10 @@ export async function sponsorCommand(options, commandServices) {
49
48
  await sponsorDutyCommand(options, args.slice(1), commandServices);
50
49
  return;
51
50
  }
51
+ if (area === "job") {
52
+ await sponsorJobCommand(options, args.slice(1), commandServices);
53
+ return;
54
+ }
52
55
  if (area === "jobs") {
53
56
  const auth = await ensureAuth(options);
54
57
  const result = await sponsorRequest({
@@ -66,6 +69,42 @@ export async function sponsorCommand(options, commandServices) {
66
69
  throw new Error(sponsorUsage());
67
70
  }
68
71
 
72
+ async function acceptSponsorAgreement(options) {
73
+ const auth = await ensureAuth(options);
74
+ const agreement = await sponsorRequest({ options, auth, path: "/agreement" });
75
+ if (!options.json) {
76
+ printSponsorAgreement(agreement, { ...options, json: false });
77
+ }
78
+ const interactive = Boolean(input.isTTY && output.isTTY);
79
+ let accepted = options.sponsorAcceptAgreement;
80
+ if (!accepted && interactive) {
81
+ const rl = readline.createInterface({ input, output });
82
+ try {
83
+ accepted = await confirmExactAgreement(rl);
84
+ } finally {
85
+ rl.close();
86
+ }
87
+ }
88
+ if (!accepted) {
89
+ throw new Error(
90
+ "Explicit acceptance is required. Re-run with --accept-agreement after reading the current agreement.",
91
+ );
92
+ }
93
+ const result = await sponsorRequest({
94
+ options,
95
+ auth,
96
+ method: "POST",
97
+ path: "/agreement/accept",
98
+ body: {
99
+ agreementVersion: agreement.version,
100
+ agreementAccepted: true,
101
+ },
102
+ });
103
+ printJsonOrLines(options, result, [
104
+ `Sponsor agreement ${result.agreementVersion} accepted.`,
105
+ ]);
106
+ }
107
+
69
108
  async function applyToSponsor(options) {
70
109
  const auth = await ensureAuth(options);
71
110
  const agreement = await sponsorRequest({ options, auth, path: "/agreement" });
@@ -207,966 +246,6 @@ async function updateSponsorCapacity(options) {
207
246
  ]);
208
247
  }
209
248
 
210
- async function sponsorDutyCommand(options, args, commandServices) {
211
- const action = String(args[0] || "status")
212
- .trim()
213
- .toLowerCase();
214
- if (action === "status") {
215
- if (args.length > 1) throw new Error(sponsorUsage());
216
- await printSponsorStatus(options);
217
- return;
218
- }
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
- }
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.");
240
- const state =
241
- action === "resume" ? "active" : action === "stop" ? "stopped" : "paused";
242
- const result = await sponsorRequest({
243
- options,
244
- auth,
245
- method: "POST",
246
- path: "/duty/state",
247
- body: { dutySessionId: dutyId, state },
248
- });
249
- printJsonOrLines(options, result, [`Sponsor duty is now ${state}.`]);
250
- return;
251
- }
252
- if (action !== "start") throw new Error(sponsorUsage());
253
- if (options.json) {
254
- throw new Error("A foreground sponsor duty cannot run with --json.");
255
- }
256
- const provider = normalizeProvider(options.provider);
257
- if (provider !== "codex" && options.sponsorServiceTier) {
258
- throw new Error(
259
- "--service-tier is currently supported only by the Codex duty adapter.",
260
- );
261
- }
262
- const auth = await ensureAuth(options);
263
- const started = await sponsorRequest({
264
- options,
265
- auth,
266
- method: "POST",
267
- path: "/duty/start",
268
- body: {
269
- scope: "shared",
270
- provider,
271
- requestedModel: options.model || null,
272
- requestedEffort: options.sponsorEffort || null,
273
- requestedServiceTier: options.sponsorServiceTier || null,
274
- cliVersion: options.lumineCli?.version || null,
275
- },
276
- });
277
- await runSponsorDuty({
278
- options,
279
- auth,
280
- provider,
281
- started,
282
- commandServices,
283
- });
284
- }
285
-
286
- async function runSponsorDuty({
287
- options,
288
- auth,
289
- provider,
290
- started,
291
- commandServices,
292
- }) {
293
- const dutyId = Number(started.duty?.id || 0);
294
- const leaseToken = String(started.leaseToken || "");
295
- if (!dutyId || !leaseToken) {
296
- throw new Error("Twinkle did not return a valid sponsor duty lease.");
297
- }
298
- if (started.duty?.scope !== "shared") {
299
- throw new Error("Twinkle did not return a shared sponsor duty lease.");
300
- }
301
- let canonicalDuty = started.duty;
302
- let stopRequested = false;
303
- let dutyLeaseError = null;
304
- let dutyHeartbeatRunning = false;
305
- const activeJobs = new Set();
306
- const signalHandler = () => {
307
- if (!stopRequested) {
308
- stopRequested = true;
309
- console.log("\nStopping after active Workshop jobs finish...");
310
- }
311
- };
312
- process.on("SIGINT", signalHandler);
313
- process.on("SIGTERM", signalHandler);
314
-
315
- const dutyHeartbeatEveryMs =
316
- Math.max(10, Number(started.heartbeatEverySeconds || 30)) * 1_000;
317
- let lastDutyHeartbeatSuccessAt = Date.now();
318
- const heartbeatDuty = async () => {
319
- if (dutyHeartbeatRunning) return;
320
- dutyHeartbeatRunning = true;
321
- try {
322
- canonicalDuty = await sponsorRequest({
323
- options,
324
- auth,
325
- method: "POST",
326
- path: `/duty/${dutyId}/heartbeat`,
327
- body: { leaseToken },
328
- });
329
- dutyLeaseError = null;
330
- lastDutyHeartbeatSuccessAt = Date.now();
331
- if (canonicalDuty.state === "stopped") stopRequested = true;
332
- } catch (error) {
333
- const status = await sponsorRequest({
334
- options,
335
- auth,
336
- path: "/status",
337
- }).catch(() => null);
338
- const confirmedDuty = (status?.duties || []).find(
339
- (duty) => Number(duty.id) === dutyId,
340
- );
341
- if (confirmedDuty?.state === "stopped") {
342
- canonicalDuty = confirmedDuty;
343
- dutyLeaseError = null;
344
- stopRequested = true;
345
- return;
346
- }
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
- }
358
- } finally {
359
- dutyHeartbeatRunning = false;
360
- }
361
- };
362
- const dutyHeartbeat = setInterval(
363
- () => void heartbeatDuty(),
364
- dutyHeartbeatEveryMs,
365
- );
366
-
367
- console.log(`Shared Zero/Ciel sponsor duty started with ${provider}.`);
368
- printDutyRuntime(canonicalDuty);
369
- console.log(
370
- "The Workshop is now discoverable to users. Press Ctrl-C for a graceful stop.",
371
- );
372
-
373
- try {
374
- while (!stopRequested) {
375
- if (canonicalDuty.state !== "active") {
376
- await sleep(options.sponsorPollMs || DEFAULT_DUTY_POLL_MS);
377
- continue;
378
- }
379
- const concurrency = Math.max(
380
- 1,
381
- Number(canonicalDuty.capacity?.maxConcurrentTasks || 1),
382
- );
383
- while (!stopRequested && activeJobs.size < concurrency) {
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
- }
406
- if (!claim.job) break;
407
- const work = processSponsorJob({
408
- options,
409
- sponsorAuth: auth,
410
- dutyId,
411
- dutyLeaseToken: leaseToken,
412
- provider,
413
- claim,
414
- commandServices,
415
- })
416
- .catch((error) => {
417
- console.error(
418
- `Workshop job #${claim.job.id} ended: ${error?.message || error}`,
419
- );
420
- })
421
- .finally(() => activeJobs.delete(work));
422
- activeJobs.add(work);
423
- }
424
- await sleep(options.sponsorPollMs || DEFAULT_DUTY_POLL_MS);
425
- }
426
- if (!dutyLeaseError && canonicalDuty.state === "active") {
427
- const paused = await sponsorRequest({
428
- options,
429
- auth,
430
- method: "POST",
431
- path: "/duty/state",
432
- body: { dutySessionId: dutyId, state: "paused" },
433
- });
434
- canonicalDuty = paused.duty || canonicalDuty;
435
- }
436
- await Promise.allSettled(Array.from(activeJobs));
437
- if (dutyLeaseError) throw dutyLeaseError;
438
- } finally {
439
- clearInterval(dutyHeartbeat);
440
- process.off("SIGINT", signalHandler);
441
- process.off("SIGTERM", signalHandler);
442
- if (canonicalDuty.state !== "stopped") {
443
- await sponsorRequest({
444
- options,
445
- auth,
446
- method: "POST",
447
- path: "/duty/state",
448
- body: { dutySessionId: dutyId, state: "stopped" },
449
- }).catch((error) => {
450
- if (!dutyLeaseError) {
451
- console.error(
452
- `Could not mark duty stopped: ${error?.message || error}`,
453
- );
454
- }
455
- });
456
- }
457
- }
458
- console.log("Shared Zero/Ciel sponsor duty stopped.");
459
- }
460
-
461
- async function processSponsorJob({
462
- options,
463
- sponsorAuth,
464
- dutyId,
465
- dutyLeaseToken,
466
- provider,
467
- claim,
468
- commandServices,
469
- }) {
470
- const job = claim.job;
471
- const persona = normalizeJobPersona(job?.persona);
472
- const jobId = Number(job.id);
473
- const attemptToken = String(claim.attempt?.token || "");
474
- const workspaceToken = String(claim.workspaceToken?.accessToken || "");
475
- if (!jobId || !attemptToken || !workspaceToken) {
476
- throw new Error("Twinkle returned an incomplete Workshop job lease.");
477
- }
478
- const runtime = {
479
- provider: String(claim.runtime?.provider || ""),
480
- requestedModel: String(claim.runtime?.requestedModel || "").trim() || null,
481
- requestedEffort:
482
- String(claim.runtime?.requestedEffort || "").trim() || null,
483
- requestedServiceTier:
484
- String(claim.runtime?.requestedServiceTier || "").trim() || null,
485
- };
486
- if (!PROVIDERS.has(runtime.provider) || runtime.provider !== provider) {
487
- throw new Error(
488
- "Twinkle returned a Workshop runtime that does not match this duty.",
489
- );
490
- }
491
- const relayById = new Map(
492
- (claim.relays || []).map((relay) => [relay.id, relay]),
493
- );
494
- let heartbeatError = null;
495
- let heartbeatRunning = false;
496
- const jobAbortController = new AbortController();
497
- const heartbeatEveryMs =
498
- Math.max(10, Number(claim.heartbeatEverySeconds || 30)) * 1_000;
499
- let lastHeartbeatSuccessAt = Date.now();
500
- const abortWorkerIfLeaseIsGone = (error) => {
501
- const status = Number(error?.status || 0);
502
- const serverRejectedLease = [401, 403, 409].includes(status);
503
- const leaseWindowElapsed =
504
- Date.now() - lastHeartbeatSuccessAt >=
505
- Math.max(120_000, heartbeatEveryMs * 3);
506
- if (
507
- !jobAbortController.signal.aborted &&
508
- (serverRejectedLease || leaseWindowElapsed)
509
- ) {
510
- jobAbortController.abort(
511
- error instanceof Error
512
- ? error
513
- : new Error("The sponsored Workshop lease ended."),
514
- );
515
- }
516
- };
517
- const heartbeatJob = async () => {
518
- if (heartbeatRunning) return null;
519
- heartbeatRunning = true;
520
- try {
521
- const result = await sponsorJobRequest({
522
- options,
523
- sponsorAuth,
524
- dutyId,
525
- dutyLeaseToken,
526
- jobId,
527
- attemptToken,
528
- path: "/heartbeat",
529
- });
530
- heartbeatError = null;
531
- lastHeartbeatSuccessAt = Date.now();
532
- for (const relay of result.relays || []) relayById.set(relay.id, relay);
533
- return result;
534
- } catch (error) {
535
- heartbeatError = error;
536
- abortWorkerIfLeaseIsGone(error);
537
- return null;
538
- } finally {
539
- heartbeatRunning = false;
540
- }
541
- };
542
- const tempDir = await fs.mkdtemp(
543
- path.join(os.tmpdir(), `lumine-${persona}-job-${jobId}-`),
544
- );
545
- const workspaceDir = path.join(tempDir, "workspace");
546
- const authFile = path.join(tempDir, "job-auth.json");
547
- const jobOptions = {
548
- ...options,
549
- authToken: null,
550
- authFile,
551
- dir: workspaceDir,
552
- target: String(job.contributionBuild.id),
553
- buildIdFlag: String(job.contributionBuild.id),
554
- provider: runtime.provider,
555
- model: runtime.requestedModel || "",
556
- agentEffort: runtime.requestedEffort || "",
557
- serviceTier: runtime.requestedServiceTier || "",
558
- agentAbortSignal: jobAbortController.signal,
559
- reviewLoop: false,
560
- openBrowser: false,
561
- skipAssetManifest: true,
562
- };
563
- // Do not start a long-lived lease timer until the owned temporary workspace
564
- // exists. If temp creation fails there is then no interval keeping the duty
565
- // process alive after this job promise rejects.
566
- const heartbeatTimer = setInterval(
567
- () => void heartbeatJob(),
568
- heartbeatEveryMs,
569
- );
570
- const logicalAgents = [];
571
- let lastSave = null;
572
-
573
- console.log(
574
- `Claimed Workshop job #${jobId} for ${job.requester.username || `user ${job.requester.userId}`} (${job.contributionBuild.title}).`,
575
- );
576
- try {
577
- await writeAuthFile(jobOptions, {
578
- token: workspaceToken,
579
- username: claim.workspaceToken?.user?.username || displayPersona(persona),
580
- userId: claim.workspaceToken?.user?.id || job.personaUserId,
581
- expiresAt: Number(claim.workspaceToken?.expiresAt || 0) * 1_000,
582
- apiUrl: options.apiUrl,
583
- createdAt: new Date().toISOString(),
584
- });
585
- const jobAuth = await resolveAuth(jobOptions);
586
- await commandServices.pullWorkspace({
587
- options: jobOptions,
588
- auth: jobAuth,
589
- buildId: Number(job.contributionBuild.id),
590
- });
591
- const forumContext = await loadForumContext({
592
- options: jobOptions,
593
- auth: jobAuth,
594
- buildId: Number(job.contributionBuild.id),
595
- });
596
- const coordinator = await startLogicalAgent({
597
- options,
598
- sponsorAuth,
599
- dutyId,
600
- jobId,
601
- attemptToken,
602
- runtime,
603
- role: "coordinator",
604
- ordinal: 0,
605
- parentAgentId: null,
606
- });
607
- logicalAgents.push(coordinator);
608
- const appliedRelayIds = new Set();
609
- const coordinatorResults = [];
610
- const initialRelays = Array.from(relayById.values());
611
- const initialResult = await runWorkshopAgentPass({
612
- jobOptions,
613
- saveWorkspace: commandServices.saveWorkspace,
614
- job,
615
- persona,
616
- relays: initialRelays,
617
- forumContext,
618
- helperOrdinal: 0,
619
- });
620
- coordinatorResults.push(initialResult);
621
- await acknowledgeRelaysApplied({
622
- options,
623
- sponsorAuth,
624
- dutyId,
625
- jobId,
626
- attemptToken,
627
- relays: initialRelays,
628
- appliedRelayIds,
629
- });
630
- lastSave = initialResult.saveResult || lastSave;
631
-
632
- const helperCount = Math.max(0, Number(job.requestedSubagents || 0));
633
- for (let ordinal = 1; ordinal <= helperCount; ordinal += 1) {
634
- await heartbeatJob();
635
- if (heartbeatError) throw heartbeatError;
636
- const helper = await startLogicalAgent({
637
- options,
638
- sponsorAuth,
639
- dutyId,
640
- jobId,
641
- attemptToken,
642
- runtime,
643
- role: "helper",
644
- ordinal,
645
- parentAgentId: coordinator.agentId,
646
- });
647
- logicalAgents.push(helper);
648
- try {
649
- const helperRelays = Array.from(relayById.values());
650
- const helperResult = await runWorkshopAgentPass({
651
- jobOptions,
652
- saveWorkspace: commandServices.saveWorkspace,
653
- job,
654
- persona,
655
- relays: helperRelays,
656
- forumContext,
657
- helperOrdinal: ordinal,
658
- });
659
- await acknowledgeRelaysApplied({
660
- options,
661
- sponsorAuth,
662
- dutyId,
663
- jobId,
664
- attemptToken,
665
- relays: helperRelays,
666
- appliedRelayIds,
667
- });
668
- lastSave = helperResult.saveResult || lastSave;
669
- await completeLogicalAgent({
670
- options,
671
- sponsorAuth,
672
- dutyId,
673
- jobId,
674
- attemptToken,
675
- agent: helper,
676
- status: "completed",
677
- results: [helperResult],
678
- });
679
- helper.completed = true;
680
- } catch (error) {
681
- await completeLogicalAgent({
682
- options,
683
- sponsorAuth,
684
- dutyId,
685
- jobId,
686
- attemptToken,
687
- agent: helper,
688
- status: "failed",
689
- results: [],
690
- error,
691
- }).catch(() => undefined);
692
- helper.completed = true;
693
- throw error;
694
- }
695
- }
696
-
697
- let relayStreamClosed = false;
698
- for (let pass = 0; pass < MAX_FOLLOW_UP_PASSES; pass += 1) {
699
- await heartbeatJob();
700
- if (heartbeatError) throw heartbeatError;
701
- const pendingRelays = Array.from(relayById.values()).filter(
702
- (relay) => !appliedRelayIds.has(relay.id),
703
- );
704
- if (pendingRelays.length > 0) {
705
- const followUpResult = await runWorkshopAgentPass({
706
- jobOptions,
707
- saveWorkspace: commandServices.saveWorkspace,
708
- job,
709
- persona,
710
- relays: pendingRelays,
711
- forumContext: "",
712
- helperOrdinal: 0,
713
- isFollowUp: true,
714
- });
715
- coordinatorResults.push(followUpResult);
716
- await acknowledgeRelaysApplied({
717
- options,
718
- sponsorAuth,
719
- dutyId,
720
- jobId,
721
- attemptToken,
722
- relays: pendingRelays,
723
- appliedRelayIds,
724
- });
725
- lastSave = followUpResult.saveResult || lastSave;
726
- }
727
- const closure = await retrySponsorTransport(() =>
728
- sponsorJobRequest({
729
- options,
730
- sponsorAuth,
731
- dutyId,
732
- jobId,
733
- attemptToken,
734
- path: "/relays/close",
735
- }),
736
- );
737
- for (const relay of closure.relays || []) relayById.set(relay.id, relay);
738
- if (closure.closed) {
739
- relayStreamClosed = true;
740
- break;
741
- }
742
- }
743
- if (!relayStreamClosed) {
744
- throw new Error(
745
- "The Workshop kept receiving follow-ups and could not close a fully applied relay stream safely.",
746
- );
747
- }
748
-
749
- await completeLogicalAgent({
750
- options,
751
- sponsorAuth,
752
- dutyId,
753
- jobId,
754
- attemptToken,
755
- agent: coordinator,
756
- status: "completed",
757
- results: coordinatorResults,
758
- });
759
- coordinator.completed = true;
760
- if (!lastSave?.artifactVersion?.versionId || !lastSave?.filesHash) {
761
- throw new Error(
762
- "The agent did not produce a new canonical saved artifact for this Workshop job.",
763
- );
764
- }
765
- const outcomeSummary = summarizeWorkshopOutcome(
766
- coordinatorResults.at(-1)?.finalText,
767
- job,
768
- );
769
- const branchNoticeMessageId = await ensureCanonicalBranchNotice({
770
- options,
771
- sponsorAuth,
772
- dutyId,
773
- jobId,
774
- attemptToken,
775
- jobOptions,
776
- jobAuth,
777
- job,
778
- outcomeSummary,
779
- });
780
- await retrySponsorTransport(() =>
781
- sponsorJobRequest({
782
- options,
783
- sponsorAuth,
784
- dutyId,
785
- jobId,
786
- attemptToken,
787
- path: "/complete",
788
- body: {
789
- artifactVersionId: Number(lastSave.artifactVersion.versionId),
790
- branchNoticeMessageId,
791
- outcomeSummary,
792
- reportedFilesHash: lastSave.filesHash,
793
- },
794
- }),
795
- );
796
- console.log(
797
- `Completed Workshop job #${jobId}; ${displayPersona(persona)} sent the branch update.`,
798
- );
799
- } catch (error) {
800
- for (const agent of logicalAgents.filter((item) => !item.completed)) {
801
- await completeLogicalAgent({
802
- options,
803
- sponsorAuth,
804
- dutyId,
805
- jobId,
806
- attemptToken,
807
- agent,
808
- status: "failed",
809
- results: [],
810
- error,
811
- }).catch(() => undefined);
812
- agent.completed = true;
813
- }
814
- await retrySponsorTransport(() =>
815
- sponsorJobRequest({
816
- options,
817
- sponsorAuth,
818
- dutyId,
819
- jobId,
820
- attemptToken,
821
- path: "/fail",
822
- body: {
823
- failureCode: "worker_failed",
824
- failureReason: String(error?.message || error).slice(0, 1000),
825
- },
826
- }),
827
- ).catch(() => undefined);
828
- throw error;
829
- } finally {
830
- clearInterval(heartbeatTimer);
831
- try {
832
- await fs.rm(tempDir, { recursive: true, force: true });
833
- } catch (error) {
834
- console.error(
835
- `lumine: could not remove temporary Workshop directory ${tempDir} (${error?.message || error})`,
836
- );
837
- }
838
- }
839
- }
840
-
841
- async function runWorkshopAgentPass({
842
- jobOptions,
843
- saveWorkspace,
844
- job,
845
- persona,
846
- relays,
847
- forumContext,
848
- helperOrdinal,
849
- isFollowUp = false,
850
- }) {
851
- const prompt = workshopAgentPrompt({
852
- job,
853
- persona,
854
- relays,
855
- forumContext,
856
- helperOrdinal,
857
- isFollowUp,
858
- });
859
- return await agentCommand(
860
- { ...jobOptions, agentPrompt: prompt },
861
- { saveWorkspace },
862
- );
863
- }
864
-
865
- async function startLogicalAgent({
866
- options,
867
- sponsorAuth,
868
- dutyId,
869
- jobId,
870
- attemptToken,
871
- runtime,
872
- role,
873
- ordinal,
874
- parentAgentId,
875
- }) {
876
- return await retrySponsorTransport(() =>
877
- sponsorJobRequest({
878
- options,
879
- sponsorAuth,
880
- dutyId,
881
- jobId,
882
- attemptToken,
883
- path: "/agents",
884
- body: {
885
- role,
886
- ordinal,
887
- parentAgentId,
888
- provider: runtime.provider,
889
- requestedModel: runtime.requestedModel,
890
- requestedEffort: runtime.requestedEffort,
891
- requestedServiceTier: runtime.requestedServiceTier,
892
- },
893
- }),
894
- );
895
- }
896
-
897
- async function completeLogicalAgent({
898
- options,
899
- sponsorAuth,
900
- dutyId,
901
- jobId,
902
- attemptToken,
903
- agent,
904
- status,
905
- results,
906
- error = null,
907
- }) {
908
- const provenance = summarizeProviderRuns(results);
909
- const allChangedPaths = Array.from(
910
- new Set(results.flatMap((result) => result.changedPaths || [])),
911
- );
912
- return await retrySponsorTransport(() =>
913
- sponsorJobRequest({
914
- options,
915
- sponsorAuth,
916
- dutyId,
917
- jobId,
918
- attemptToken,
919
- path: `/agents/${agent.agentId}/complete`,
920
- body: {
921
- status,
922
- resolvedModel: provenance.resolvedModel,
923
- resolvedEffort: provenance.resolvedEffort,
924
- resolvedServiceTier: provenance.resolvedServiceTier,
925
- runtimeVersion: provenance.runtimeVersion,
926
- evidenceTier: provenance.evidenceTier,
927
- usage: provenance.usage,
928
- outcome: {
929
- providerPassCount: provenance.providerPassCount,
930
- changedPathCount: allChangedPaths.length,
931
- changedPaths: allChangedPaths
932
- .map((changedPath) => String(changedPath || "").slice(0, 300))
933
- .filter(Boolean)
934
- .slice(0, 50),
935
- finalText: String(results.at(-1)?.finalText || "").slice(0, 2000),
936
- ...(error
937
- ? { error: String(error?.message || error).slice(0, 1000) }
938
- : {}),
939
- },
940
- },
941
- }),
942
- );
943
- }
944
-
945
- async function acknowledgeRelaysApplied({
946
- options,
947
- sponsorAuth,
948
- dutyId,
949
- jobId,
950
- attemptToken,
951
- relays,
952
- appliedRelayIds,
953
- }) {
954
- const relayIds = Array.from(
955
- new Set(relays.map((relay) => Number(relay.id)).filter((id) => id > 0)),
956
- );
957
- if (relayIds.length === 0) return;
958
- for (let index = 0; index < relayIds.length; index += 50) {
959
- const relayIdBatch = relayIds.slice(index, index + 50);
960
- const receipt = await retrySponsorTransport(() =>
961
- sponsorJobRequest({
962
- options,
963
- sponsorAuth,
964
- dutyId,
965
- jobId,
966
- attemptToken,
967
- path: "/relays/applied",
968
- body: { relayIds: relayIdBatch },
969
- }),
970
- );
971
- const canonicalIds = new Set(
972
- (receipt.appliedRelayIds || []).map((relayId) => Number(relayId)),
973
- );
974
- if (relayIdBatch.some((relayId) => !canonicalIds.has(relayId))) {
975
- throw new Error("Twinkle did not confirm every applied Workshop relay.");
976
- }
977
- for (const relayId of canonicalIds) appliedRelayIds.add(relayId);
978
- }
979
- }
980
-
981
- async function ensureCanonicalBranchNotice({
982
- options,
983
- sponsorAuth,
984
- dutyId,
985
- jobId,
986
- attemptToken,
987
- jobOptions,
988
- jobAuth,
989
- job,
990
- outcomeSummary,
991
- }) {
992
- const lookup = () =>
993
- sponsorJobRequest({
994
- options,
995
- sponsorAuth,
996
- dutyId,
997
- jobId,
998
- attemptToken,
999
- path: "/branch-notice",
1000
- });
1001
- const existing = await lookup();
1002
- const existingMessageId = Number(existing.branchNotice?.id || 0);
1003
- if (existingMessageId) return existingMessageId;
1004
-
1005
- try {
1006
- const notice = await notifyBuildOwnerOfContribution({
1007
- options: jobOptions,
1008
- auth: jobAuth,
1009
- rootBuildId: Number(job.rootBuild.id),
1010
- contributionBuildId: Number(job.contributionBuild.id),
1011
- note: outcomeSummary,
1012
- });
1013
- const messageId = Number(notice?.message?.id || 0);
1014
- if (!messageId) {
1015
- throw new Error("Twinkle did not confirm the branch update message.");
1016
- }
1017
- return messageId;
1018
- } catch (error) {
1019
- for (const delayMs of [250, 750, 1_500]) {
1020
- await sleep(delayMs);
1021
- const recovered = await lookup().catch(() => null);
1022
- const recoveredMessageId = Number(recovered?.branchNotice?.id || 0);
1023
- if (recoveredMessageId) return recoveredMessageId;
1024
- }
1025
- throw error;
1026
- }
1027
- }
1028
-
1029
- function summarizeProviderRuns(results) {
1030
- const runs = results.flatMap((result) => result.providerRuns || []);
1031
- const lastRun = runs.at(-1) || {};
1032
- const valueFromLastRun = (key, maximum) =>
1033
- String(lastRun?.[key] || "")
1034
- .trim()
1035
- .slice(0, maximum) || null;
1036
- const evidenceTiers = runs.map((run) => run?.evidenceTier);
1037
- const evidenceTier =
1038
- evidenceTiers.length > 0 &&
1039
- evidenceTiers.every((tier) => tier === "runtime_observed")
1040
- ? "runtime_observed"
1041
- : evidenceTiers.length > 0 &&
1042
- evidenceTiers.every((tier) =>
1043
- ["runtime_observed", "provider_reported"].includes(tier),
1044
- )
1045
- ? "provider_reported"
1046
- : "requested_only";
1047
- return {
1048
- resolvedModel: valueFromLastRun("resolvedModel", 160),
1049
- resolvedEffort: valueFromLastRun("resolvedEffort", 40),
1050
- resolvedServiceTier: valueFromLastRun("resolvedServiceTier", 40),
1051
- runtimeVersion: valueFromLastRun("runtimeVersion", 64),
1052
- evidenceTier,
1053
- providerPassCount: runs.length,
1054
- usage: {
1055
- passes: runs.slice(0, 20).map((run) => ({
1056
- phase: run.phase || null,
1057
- resolvedModel: run.resolvedModel || null,
1058
- resolvedEffort: run.resolvedEffort || null,
1059
- resolvedServiceTier: run.resolvedServiceTier || null,
1060
- runtimeVersion: run.runtimeVersion || null,
1061
- evidenceTier: run.evidenceTier || "requested_only",
1062
- usage: run.usage || {},
1063
- })),
1064
- },
1065
- };
1066
- }
1067
-
1068
- async function loadForumContext({ options, auth, buildId }) {
1069
- try {
1070
- const snapshot = await readCompleteBuildForumSnapshot({
1071
- options: { ...options, limit: 100 },
1072
- auth,
1073
- buildId,
1074
- maxPages: 100,
1075
- });
1076
- const events = (snapshot.events || []).slice(-50);
1077
- if (events.length === 0) return "";
1078
- return JSON.stringify(events).slice(0, MAX_FORUM_CONTEXT_CHARS);
1079
- } catch (error) {
1080
- console.error(
1081
- `lumine: scoped Forum context unavailable (${error?.message || error})`,
1082
- );
1083
- return "";
1084
- }
1085
- }
1086
-
1087
- function workshopAgentPrompt({
1088
- job,
1089
- persona,
1090
- relays,
1091
- forumContext,
1092
- helperOrdinal,
1093
- isFollowUp,
1094
- }) {
1095
- const relayText = relays
1096
- .map((relay, index) => {
1097
- const details = [
1098
- relay.summary,
1099
- relay.requestedOutcome ? `Outcome: ${relay.requestedOutcome}` : "",
1100
- relay.constraints?.length
1101
- ? `Constraints: ${relay.constraints.join("; ")}`
1102
- : "",
1103
- relay.acceptanceCriteria?.length
1104
- ? `Acceptance: ${relay.acceptanceCriteria.join("; ")}`
1105
- : "",
1106
- ].filter(Boolean);
1107
- return `${index + 1}. ${details.join("\n ")}`;
1108
- })
1109
- .join("\n");
1110
- const role =
1111
- helperOrdinal > 0
1112
- ? `You are helper ${helperOrdinal}. Review the current implementation and fix concrete gaps.`
1113
- : isFollowUp
1114
- ? "Apply these newly relayed follow-up requirements to the current implementation."
1115
- : `Implement this hands-on Lumine Build request on ${displayPersona(persona)}'s assigned contribution branch.`;
1116
- return `${role}
1117
-
1118
- 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.
1119
-
1120
- Approved relay:
1121
- ${relayText || "No additional relay text was supplied."}
1122
- ${forumContext ? `\nScoped Build Forum snapshot (project-visible context only):\n${forumContext}\n` : ""}
1123
- Return a short factual outcome summary after saving the canonical branch.`;
1124
- }
1125
-
1126
- async function sponsorJobRequest({
1127
- options,
1128
- sponsorAuth,
1129
- dutyId,
1130
- dutyLeaseToken,
1131
- jobId,
1132
- attemptToken,
1133
- path: suffix,
1134
- body = {},
1135
- }) {
1136
- return await sponsorRequest({
1137
- options,
1138
- auth: sponsorAuth,
1139
- method: "POST",
1140
- path: `/jobs/${jobId}${suffix}`,
1141
- body: {
1142
- dutySessionId: dutyId,
1143
- attemptToken,
1144
- ...(dutyLeaseToken ? { dutyLeaseToken } : {}),
1145
- ...body,
1146
- },
1147
- });
1148
- }
1149
-
1150
- async function retrySponsorTransport(operation) {
1151
- let lastError;
1152
- for (const delayMs of [0, 250, 750, 1_500]) {
1153
- if (delayMs) await sleep(delayMs);
1154
- try {
1155
- return await operation();
1156
- } catch (error) {
1157
- if (Number(error?.status || 0) > 0) throw error;
1158
- lastError = error;
1159
- }
1160
- }
1161
- throw lastError;
1162
- }
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
-
1170
249
  async function sponsorRequest({
1171
250
  options,
1172
251
  auth,
@@ -1192,6 +271,14 @@ async function printSponsorStatus(options) {
1192
271
  }
1193
272
  console.log(`Application: ${status.application?.status || "not submitted"}`);
1194
273
  console.log(`Sponsor access: ${status.sponsor?.status || "not approved"}`);
274
+ if (
275
+ status.sponsor &&
276
+ status.application?.agreementVersion !== status.agreementVersion
277
+ ) {
278
+ console.log(
279
+ `Agreement update required: read version ${status.agreementVersion} with \`lumine sponsor agreement\`, then accept it before starting duty.`,
280
+ );
281
+ }
1195
282
  if (status.sponsor?.capacity) {
1196
283
  const capacity = status.sponsor.capacity;
1197
284
  console.log(
@@ -1207,7 +294,7 @@ async function printSponsorStatus(options) {
1207
294
  ? "shared Zero/Ciel"
1208
295
  : `legacy assistant user ${duty.personaUserId}`;
1209
296
  console.log(
1210
- `Duty #${duty.id}: ${scope} · ${duty.state} · ${duty.provider} · model=${duty.requestedModel || "provider default"} · effort=${duty.requestedEffort || "provider default"}`,
297
+ `Duty #${duty.id}: ${scope} · ${duty.state} · ${duty.provider} · model=${duty.requestedModel || "missing"} · effort=${duty.requestedEffort || "missing"}`,
1211
298
  );
1212
299
  }
1213
300
  }
@@ -1237,15 +324,6 @@ function printSponsorJobs(jobs, options) {
1237
324
  }
1238
325
  }
1239
326
 
1240
- function printDutyRuntime(duty) {
1241
- console.log(
1242
- `Runtime: model=${duty.requestedModel || "provider default"}, effort=${duty.requestedEffort || "provider default"}, service-tier=${duty.requestedServiceTier || "provider default"}`,
1243
- );
1244
- console.log(
1245
- `Limits: ${duty.capacity?.maxConcurrentTasks || 1} concurrent, ${duty.capacity?.maxSubagentsPerTask || 0} helpers, ${duty.capacity?.dailyTaskLimit || 0}/day, ${duty.capacity?.weeklyTaskLimit || 0}/week`,
1246
- );
1247
- }
1248
-
1249
327
  function printJsonOrLines(options, value, lines) {
1250
328
  if (options.json) console.log(JSON.stringify(value, null, 2));
1251
329
  else for (const line of lines) console.log(line);
@@ -1266,7 +344,7 @@ async function requiredApplicationText({ supplied, label, interactive, rl }) {
1266
344
 
1267
345
  async function confirmExactAgreement(rl) {
1268
346
  const answer = await rl.question(
1269
- "Type I ACCEPT to confirm this agreement and submit the application: ",
347
+ "Type I ACCEPT to confirm this sponsor agreement: ",
1270
348
  );
1271
349
  return answer.trim() === "I ACCEPT";
1272
350
  }
@@ -1296,50 +374,27 @@ function normalizeProviderList(value) {
1296
374
  return Array.from(new Set(submitted));
1297
375
  }
1298
376
 
1299
- function normalizeProvider(value) {
1300
- const provider = String(value || "")
1301
- .trim()
1302
- .toLowerCase();
1303
- if (!PROVIDERS.has(provider)) {
1304
- throw new Error("Pass --provider codex or --provider claude-code.");
1305
- }
1306
- return provider;
1307
- }
1308
-
1309
- function normalizeJobPersona(value) {
1310
- const persona = String(value || "")
1311
- .trim()
1312
- .toLowerCase();
1313
- if (!PERSONAS.has(persona)) {
1314
- throw new Error("Twinkle returned an invalid Workshop job assistant.");
1315
- }
1316
- return persona;
1317
- }
1318
-
1319
377
  function displayPersona(persona) {
1320
378
  return String(persona || "").toLowerCase() === "ciel" ? "Ciel" : "Zero";
1321
379
  }
1322
380
 
1323
- function summarizeWorkshopOutcome(finalText, job) {
1324
- const summary = String(finalText || "")
1325
- .trim()
1326
- .slice(0, 1000);
1327
- return (
1328
- summary ||
1329
- `Updated ${job.rootBuild.title} on the assigned contribution branch.`
1330
- );
1331
- }
1332
-
1333
381
  function sponsorUsage() {
1334
382
  return [
1335
383
  "Usage:",
1336
384
  " lumine sponsor agreement",
385
+ " lumine sponsor agreement accept [--accept-agreement]",
1337
386
  " lumine sponsor apply --providers codex[,claude-code] [--motivation <text>] [--availability <text>] [--accept-agreement]",
1338
387
  " lumine sponsor status",
1339
388
  " lumine sponsor withdraw [--yes]",
1340
389
  " lumine sponsor capacity [--concurrency <n>] [--helpers <n>] [--daily-limit <n>] [--weekly-limit <n>]",
1341
- " lumine sponsor duty start --provider <codex|claude-code> [--model <name>] [--effort <level>] [--service-tier <tier>]",
1342
- " lumine sponsor duty pause|resume|stop",
390
+ " lumine sponsor duty start [--provider <codex|claude-code>] --model <name> --effort <level> [--service-tier <tier>]",
391
+ " lumine sponsor duty watch [--wait-ms <ms>] [--json]",
392
+ " lumine sponsor duty status|pause|resume|stop",
393
+ " lumine sponsor job status|pulse|begin <job-id>",
394
+ " lumine sponsor job relay-applied <job-id> <relay-id...>",
395
+ " lumine sponsor job helper-start|helper-complete <job-id> [options]",
396
+ " lumine sponsor job complete <job-id> --summary <text>",
397
+ " lumine sponsor job fail <job-id> --reason <text>",
1343
398
  " lumine sponsor jobs [--limit <n>]",
1344
399
  ].join("\n");
1345
400
  }