@stage5/lumine 0.2.56 → 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,932 +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 heartbeatDuty = async () => {
316
- if (dutyHeartbeatRunning) return;
317
- dutyHeartbeatRunning = true;
318
- try {
319
- canonicalDuty = await sponsorRequest({
320
- options,
321
- auth,
322
- method: "POST",
323
- path: `/duty/${dutyId}/heartbeat`,
324
- body: { leaseToken },
325
- });
326
- dutyLeaseError = null;
327
- if (canonicalDuty.state === "stopped") stopRequested = true;
328
- } catch (error) {
329
- const status = await sponsorRequest({
330
- options,
331
- auth,
332
- path: "/status",
333
- }).catch(() => null);
334
- const confirmedDuty = (status?.duties || []).find(
335
- (duty) => Number(duty.id) === dutyId,
336
- );
337
- if (confirmedDuty?.state === "stopped") {
338
- canonicalDuty = confirmedDuty;
339
- dutyLeaseError = null;
340
- stopRequested = true;
341
- return;
342
- }
343
- dutyLeaseError = error;
344
- stopRequested = true;
345
- } finally {
346
- dutyHeartbeatRunning = false;
347
- }
348
- };
349
- const dutyHeartbeat = setInterval(
350
- () => void heartbeatDuty(),
351
- Math.max(10, Number(started.heartbeatEverySeconds || 30)) * 1_000,
352
- );
353
-
354
- console.log(`Shared Zero/Ciel sponsor duty started with ${provider}.`);
355
- printDutyRuntime(canonicalDuty);
356
- console.log(
357
- "The Workshop is now discoverable to users. Press Ctrl-C for a graceful stop.",
358
- );
359
-
360
- try {
361
- while (!stopRequested) {
362
- if (canonicalDuty.state !== "active") {
363
- await sleep(options.sponsorPollMs || DEFAULT_DUTY_POLL_MS);
364
- continue;
365
- }
366
- const concurrency = Math.max(
367
- 1,
368
- Number(canonicalDuty.capacity?.maxConcurrentTasks || 1),
369
- );
370
- while (!stopRequested && activeJobs.size < concurrency) {
371
- const claim = await sponsorRequest({
372
- options,
373
- auth,
374
- method: "POST",
375
- path: "/jobs/claim",
376
- body: { dutySessionId: dutyId, leaseToken },
377
- });
378
- if (!claim.job) break;
379
- const work = processSponsorJob({
380
- options,
381
- sponsorAuth: auth,
382
- dutyId,
383
- dutyLeaseToken: leaseToken,
384
- provider,
385
- claim,
386
- commandServices,
387
- })
388
- .catch((error) => {
389
- console.error(
390
- `Workshop job #${claim.job.id} ended: ${error?.message || error}`,
391
- );
392
- })
393
- .finally(() => activeJobs.delete(work));
394
- activeJobs.add(work);
395
- }
396
- await sleep(options.sponsorPollMs || DEFAULT_DUTY_POLL_MS);
397
- }
398
- if (!dutyLeaseError && canonicalDuty.state === "active") {
399
- const paused = await sponsorRequest({
400
- options,
401
- auth,
402
- method: "POST",
403
- path: "/duty/state",
404
- body: { dutySessionId: dutyId, state: "paused" },
405
- });
406
- canonicalDuty = paused.duty || canonicalDuty;
407
- }
408
- await Promise.allSettled(Array.from(activeJobs));
409
- if (dutyLeaseError) throw dutyLeaseError;
410
- } finally {
411
- clearInterval(dutyHeartbeat);
412
- process.off("SIGINT", signalHandler);
413
- process.off("SIGTERM", signalHandler);
414
- if (canonicalDuty.state !== "stopped") {
415
- await sponsorRequest({
416
- options,
417
- auth,
418
- method: "POST",
419
- path: "/duty/state",
420
- body: { dutySessionId: dutyId, state: "stopped" },
421
- }).catch((error) => {
422
- if (!dutyLeaseError) {
423
- console.error(
424
- `Could not mark duty stopped: ${error?.message || error}`,
425
- );
426
- }
427
- });
428
- }
429
- }
430
- console.log("Shared Zero/Ciel sponsor duty stopped.");
431
- }
432
-
433
- async function processSponsorJob({
434
- options,
435
- sponsorAuth,
436
- dutyId,
437
- dutyLeaseToken,
438
- provider,
439
- claim,
440
- commandServices,
441
- }) {
442
- const job = claim.job;
443
- const persona = normalizeJobPersona(job?.persona);
444
- const jobId = Number(job.id);
445
- const attemptToken = String(claim.attempt?.token || "");
446
- const workspaceToken = String(claim.workspaceToken?.accessToken || "");
447
- if (!jobId || !attemptToken || !workspaceToken) {
448
- throw new Error("Twinkle returned an incomplete Workshop job lease.");
449
- }
450
- const runtime = {
451
- provider: String(claim.runtime?.provider || ""),
452
- requestedModel: String(claim.runtime?.requestedModel || "").trim() || null,
453
- requestedEffort:
454
- String(claim.runtime?.requestedEffort || "").trim() || null,
455
- requestedServiceTier:
456
- String(claim.runtime?.requestedServiceTier || "").trim() || null,
457
- };
458
- if (!PROVIDERS.has(runtime.provider) || runtime.provider !== provider) {
459
- throw new Error(
460
- "Twinkle returned a Workshop runtime that does not match this duty.",
461
- );
462
- }
463
- const relayById = new Map(
464
- (claim.relays || []).map((relay) => [relay.id, relay]),
465
- );
466
- let heartbeatError = null;
467
- let heartbeatRunning = false;
468
- const jobAbortController = new AbortController();
469
- const heartbeatEveryMs =
470
- Math.max(10, Number(claim.heartbeatEverySeconds || 30)) * 1_000;
471
- let lastHeartbeatSuccessAt = Date.now();
472
- const abortWorkerIfLeaseIsGone = (error) => {
473
- const status = Number(error?.status || 0);
474
- const serverRejectedLease = [401, 403, 409].includes(status);
475
- const leaseWindowElapsed =
476
- Date.now() - lastHeartbeatSuccessAt >=
477
- Math.max(120_000, heartbeatEveryMs * 3);
478
- if (
479
- !jobAbortController.signal.aborted &&
480
- (serverRejectedLease || leaseWindowElapsed)
481
- ) {
482
- jobAbortController.abort(
483
- error instanceof Error
484
- ? error
485
- : new Error("The sponsored Workshop lease ended."),
486
- );
487
- }
488
- };
489
- const heartbeatJob = async () => {
490
- if (heartbeatRunning) return null;
491
- heartbeatRunning = true;
492
- try {
493
- const result = await sponsorJobRequest({
494
- options,
495
- sponsorAuth,
496
- dutyId,
497
- dutyLeaseToken,
498
- jobId,
499
- attemptToken,
500
- path: "/heartbeat",
501
- });
502
- heartbeatError = null;
503
- lastHeartbeatSuccessAt = Date.now();
504
- for (const relay of result.relays || []) relayById.set(relay.id, relay);
505
- return result;
506
- } catch (error) {
507
- heartbeatError = error;
508
- abortWorkerIfLeaseIsGone(error);
509
- return null;
510
- } finally {
511
- heartbeatRunning = false;
512
- }
513
- };
514
- const tempDir = await fs.mkdtemp(
515
- path.join(os.tmpdir(), `lumine-${persona}-job-${jobId}-`),
516
- );
517
- const workspaceDir = path.join(tempDir, "workspace");
518
- const authFile = path.join(tempDir, "job-auth.json");
519
- const jobOptions = {
520
- ...options,
521
- authToken: null,
522
- authFile,
523
- dir: workspaceDir,
524
- target: String(job.contributionBuild.id),
525
- buildIdFlag: String(job.contributionBuild.id),
526
- provider: runtime.provider,
527
- model: runtime.requestedModel || "",
528
- agentEffort: runtime.requestedEffort || "",
529
- serviceTier: runtime.requestedServiceTier || "",
530
- agentAbortSignal: jobAbortController.signal,
531
- reviewLoop: false,
532
- openBrowser: false,
533
- skipAssetManifest: true,
534
- };
535
- // Do not start a long-lived lease timer until the owned temporary workspace
536
- // exists. If temp creation fails there is then no interval keeping the duty
537
- // process alive after this job promise rejects.
538
- const heartbeatTimer = setInterval(
539
- () => void heartbeatJob(),
540
- heartbeatEveryMs,
541
- );
542
- const logicalAgents = [];
543
- let lastSave = null;
544
-
545
- console.log(
546
- `Claimed Workshop job #${jobId} for ${job.requester.username || `user ${job.requester.userId}`} (${job.contributionBuild.title}).`,
547
- );
548
- try {
549
- await writeAuthFile(jobOptions, {
550
- token: workspaceToken,
551
- username: claim.workspaceToken?.user?.username || displayPersona(persona),
552
- userId: claim.workspaceToken?.user?.id || job.personaUserId,
553
- expiresAt: Number(claim.workspaceToken?.expiresAt || 0) * 1_000,
554
- apiUrl: options.apiUrl,
555
- createdAt: new Date().toISOString(),
556
- });
557
- const jobAuth = await resolveAuth(jobOptions);
558
- await commandServices.pullWorkspace({
559
- options: jobOptions,
560
- auth: jobAuth,
561
- buildId: Number(job.contributionBuild.id),
562
- });
563
- const forumContext = await loadForumContext({
564
- options: jobOptions,
565
- auth: jobAuth,
566
- buildId: Number(job.contributionBuild.id),
567
- });
568
- const coordinator = await startLogicalAgent({
569
- options,
570
- sponsorAuth,
571
- dutyId,
572
- jobId,
573
- attemptToken,
574
- runtime,
575
- role: "coordinator",
576
- ordinal: 0,
577
- parentAgentId: null,
578
- });
579
- logicalAgents.push(coordinator);
580
- const appliedRelayIds = new Set();
581
- const coordinatorResults = [];
582
- const initialRelays = Array.from(relayById.values());
583
- const initialResult = await runWorkshopAgentPass({
584
- jobOptions,
585
- saveWorkspace: commandServices.saveWorkspace,
586
- job,
587
- persona,
588
- relays: initialRelays,
589
- forumContext,
590
- helperOrdinal: 0,
591
- });
592
- coordinatorResults.push(initialResult);
593
- await acknowledgeRelaysApplied({
594
- options,
595
- sponsorAuth,
596
- dutyId,
597
- jobId,
598
- attemptToken,
599
- relays: initialRelays,
600
- appliedRelayIds,
601
- });
602
- lastSave = initialResult.saveResult || lastSave;
603
-
604
- const helperCount = Math.max(0, Number(job.requestedSubagents || 0));
605
- for (let ordinal = 1; ordinal <= helperCount; ordinal += 1) {
606
- await heartbeatJob();
607
- if (heartbeatError) throw heartbeatError;
608
- const helper = await startLogicalAgent({
609
- options,
610
- sponsorAuth,
611
- dutyId,
612
- jobId,
613
- attemptToken,
614
- runtime,
615
- role: "helper",
616
- ordinal,
617
- parentAgentId: coordinator.agentId,
618
- });
619
- logicalAgents.push(helper);
620
- try {
621
- const helperRelays = Array.from(relayById.values());
622
- const helperResult = await runWorkshopAgentPass({
623
- jobOptions,
624
- saveWorkspace: commandServices.saveWorkspace,
625
- job,
626
- persona,
627
- relays: helperRelays,
628
- forumContext,
629
- helperOrdinal: ordinal,
630
- });
631
- await acknowledgeRelaysApplied({
632
- options,
633
- sponsorAuth,
634
- dutyId,
635
- jobId,
636
- attemptToken,
637
- relays: helperRelays,
638
- appliedRelayIds,
639
- });
640
- lastSave = helperResult.saveResult || lastSave;
641
- await completeLogicalAgent({
642
- options,
643
- sponsorAuth,
644
- dutyId,
645
- jobId,
646
- attemptToken,
647
- agent: helper,
648
- status: "completed",
649
- results: [helperResult],
650
- });
651
- helper.completed = true;
652
- } catch (error) {
653
- await completeLogicalAgent({
654
- options,
655
- sponsorAuth,
656
- dutyId,
657
- jobId,
658
- attemptToken,
659
- agent: helper,
660
- status: "failed",
661
- results: [],
662
- error,
663
- }).catch(() => undefined);
664
- helper.completed = true;
665
- throw error;
666
- }
667
- }
668
-
669
- let relayStreamClosed = false;
670
- for (let pass = 0; pass < MAX_FOLLOW_UP_PASSES; pass += 1) {
671
- await heartbeatJob();
672
- if (heartbeatError) throw heartbeatError;
673
- const pendingRelays = Array.from(relayById.values()).filter(
674
- (relay) => !appliedRelayIds.has(relay.id),
675
- );
676
- if (pendingRelays.length > 0) {
677
- const followUpResult = await runWorkshopAgentPass({
678
- jobOptions,
679
- saveWorkspace: commandServices.saveWorkspace,
680
- job,
681
- persona,
682
- relays: pendingRelays,
683
- forumContext: "",
684
- helperOrdinal: 0,
685
- isFollowUp: true,
686
- });
687
- coordinatorResults.push(followUpResult);
688
- await acknowledgeRelaysApplied({
689
- options,
690
- sponsorAuth,
691
- dutyId,
692
- jobId,
693
- attemptToken,
694
- relays: pendingRelays,
695
- appliedRelayIds,
696
- });
697
- lastSave = followUpResult.saveResult || lastSave;
698
- }
699
- const closure = await retrySponsorTransport(() =>
700
- sponsorJobRequest({
701
- options,
702
- sponsorAuth,
703
- dutyId,
704
- jobId,
705
- attemptToken,
706
- path: "/relays/close",
707
- }),
708
- );
709
- for (const relay of closure.relays || []) relayById.set(relay.id, relay);
710
- if (closure.closed) {
711
- relayStreamClosed = true;
712
- break;
713
- }
714
- }
715
- if (!relayStreamClosed) {
716
- throw new Error(
717
- "The Workshop kept receiving follow-ups and could not close a fully applied relay stream safely.",
718
- );
719
- }
720
-
721
- await completeLogicalAgent({
722
- options,
723
- sponsorAuth,
724
- dutyId,
725
- jobId,
726
- attemptToken,
727
- agent: coordinator,
728
- status: "completed",
729
- results: coordinatorResults,
730
- });
731
- coordinator.completed = true;
732
- if (!lastSave?.artifactVersion?.versionId || !lastSave?.filesHash) {
733
- throw new Error(
734
- "The agent did not produce a new canonical saved artifact for this Workshop job.",
735
- );
736
- }
737
- const outcomeSummary = summarizeWorkshopOutcome(
738
- coordinatorResults.at(-1)?.finalText,
739
- job,
740
- );
741
- const branchNoticeMessageId = await ensureCanonicalBranchNotice({
742
- options,
743
- sponsorAuth,
744
- dutyId,
745
- jobId,
746
- attemptToken,
747
- jobOptions,
748
- jobAuth,
749
- job,
750
- outcomeSummary,
751
- });
752
- await retrySponsorTransport(() =>
753
- sponsorJobRequest({
754
- options,
755
- sponsorAuth,
756
- dutyId,
757
- jobId,
758
- attemptToken,
759
- path: "/complete",
760
- body: {
761
- artifactVersionId: Number(lastSave.artifactVersion.versionId),
762
- branchNoticeMessageId,
763
- outcomeSummary,
764
- reportedFilesHash: lastSave.filesHash,
765
- },
766
- }),
767
- );
768
- console.log(
769
- `Completed Workshop job #${jobId}; ${displayPersona(persona)} sent the branch update.`,
770
- );
771
- } catch (error) {
772
- for (const agent of logicalAgents.filter((item) => !item.completed)) {
773
- await completeLogicalAgent({
774
- options,
775
- sponsorAuth,
776
- dutyId,
777
- jobId,
778
- attemptToken,
779
- agent,
780
- status: "failed",
781
- results: [],
782
- error,
783
- }).catch(() => undefined);
784
- agent.completed = true;
785
- }
786
- await retrySponsorTransport(() =>
787
- sponsorJobRequest({
788
- options,
789
- sponsorAuth,
790
- dutyId,
791
- jobId,
792
- attemptToken,
793
- path: "/fail",
794
- body: {
795
- failureCode: "worker_failed",
796
- failureReason: String(error?.message || error).slice(0, 1000),
797
- },
798
- }),
799
- ).catch(() => undefined);
800
- throw error;
801
- } finally {
802
- clearInterval(heartbeatTimer);
803
- try {
804
- await fs.rm(tempDir, { recursive: true, force: true });
805
- } catch (error) {
806
- console.error(
807
- `lumine: could not remove temporary Workshop directory ${tempDir} (${error?.message || error})`,
808
- );
809
- }
810
- }
811
- }
812
-
813
- async function runWorkshopAgentPass({
814
- jobOptions,
815
- saveWorkspace,
816
- job,
817
- persona,
818
- relays,
819
- forumContext,
820
- helperOrdinal,
821
- isFollowUp = false,
822
- }) {
823
- const prompt = workshopAgentPrompt({
824
- job,
825
- persona,
826
- relays,
827
- forumContext,
828
- helperOrdinal,
829
- isFollowUp,
830
- });
831
- return await agentCommand(
832
- { ...jobOptions, agentPrompt: prompt },
833
- { saveWorkspace },
834
- );
835
- }
836
-
837
- async function startLogicalAgent({
838
- options,
839
- sponsorAuth,
840
- dutyId,
841
- jobId,
842
- attemptToken,
843
- runtime,
844
- role,
845
- ordinal,
846
- parentAgentId,
847
- }) {
848
- return await retrySponsorTransport(() =>
849
- sponsorJobRequest({
850
- options,
851
- sponsorAuth,
852
- dutyId,
853
- jobId,
854
- attemptToken,
855
- path: "/agents",
856
- body: {
857
- role,
858
- ordinal,
859
- parentAgentId,
860
- provider: runtime.provider,
861
- requestedModel: runtime.requestedModel,
862
- requestedEffort: runtime.requestedEffort,
863
- requestedServiceTier: runtime.requestedServiceTier,
864
- },
865
- }),
866
- );
867
- }
868
-
869
- async function completeLogicalAgent({
870
- options,
871
- sponsorAuth,
872
- dutyId,
873
- jobId,
874
- attemptToken,
875
- agent,
876
- status,
877
- results,
878
- error = null,
879
- }) {
880
- const provenance = summarizeProviderRuns(results);
881
- const allChangedPaths = Array.from(
882
- new Set(results.flatMap((result) => result.changedPaths || [])),
883
- );
884
- return await retrySponsorTransport(() =>
885
- sponsorJobRequest({
886
- options,
887
- sponsorAuth,
888
- dutyId,
889
- jobId,
890
- attemptToken,
891
- path: `/agents/${agent.agentId}/complete`,
892
- body: {
893
- status,
894
- resolvedModel: provenance.resolvedModel,
895
- resolvedEffort: provenance.resolvedEffort,
896
- resolvedServiceTier: provenance.resolvedServiceTier,
897
- runtimeVersion: provenance.runtimeVersion,
898
- evidenceTier: provenance.evidenceTier,
899
- usage: provenance.usage,
900
- outcome: {
901
- providerPassCount: provenance.providerPassCount,
902
- changedPathCount: allChangedPaths.length,
903
- changedPaths: allChangedPaths
904
- .map((changedPath) => String(changedPath || "").slice(0, 300))
905
- .filter(Boolean)
906
- .slice(0, 50),
907
- finalText: String(results.at(-1)?.finalText || "").slice(0, 2000),
908
- ...(error
909
- ? { error: String(error?.message || error).slice(0, 1000) }
910
- : {}),
911
- },
912
- },
913
- }),
914
- );
915
- }
916
-
917
- async function acknowledgeRelaysApplied({
918
- options,
919
- sponsorAuth,
920
- dutyId,
921
- jobId,
922
- attemptToken,
923
- relays,
924
- appliedRelayIds,
925
- }) {
926
- const relayIds = Array.from(
927
- new Set(relays.map((relay) => Number(relay.id)).filter((id) => id > 0)),
928
- );
929
- if (relayIds.length === 0) return;
930
- for (let index = 0; index < relayIds.length; index += 50) {
931
- const relayIdBatch = relayIds.slice(index, index + 50);
932
- const receipt = await retrySponsorTransport(() =>
933
- sponsorJobRequest({
934
- options,
935
- sponsorAuth,
936
- dutyId,
937
- jobId,
938
- attemptToken,
939
- path: "/relays/applied",
940
- body: { relayIds: relayIdBatch },
941
- }),
942
- );
943
- const canonicalIds = new Set(
944
- (receipt.appliedRelayIds || []).map((relayId) => Number(relayId)),
945
- );
946
- if (relayIdBatch.some((relayId) => !canonicalIds.has(relayId))) {
947
- throw new Error("Twinkle did not confirm every applied Workshop relay.");
948
- }
949
- for (const relayId of canonicalIds) appliedRelayIds.add(relayId);
950
- }
951
- }
952
-
953
- async function ensureCanonicalBranchNotice({
954
- options,
955
- sponsorAuth,
956
- dutyId,
957
- jobId,
958
- attemptToken,
959
- jobOptions,
960
- jobAuth,
961
- job,
962
- outcomeSummary,
963
- }) {
964
- const lookup = () =>
965
- sponsorJobRequest({
966
- options,
967
- sponsorAuth,
968
- dutyId,
969
- jobId,
970
- attemptToken,
971
- path: "/branch-notice",
972
- });
973
- const existing = await lookup();
974
- const existingMessageId = Number(existing.branchNotice?.id || 0);
975
- if (existingMessageId) return existingMessageId;
976
-
977
- try {
978
- const notice = await notifyBuildOwnerOfContribution({
979
- options: jobOptions,
980
- auth: jobAuth,
981
- rootBuildId: Number(job.rootBuild.id),
982
- contributionBuildId: Number(job.contributionBuild.id),
983
- note: outcomeSummary,
984
- });
985
- const messageId = Number(notice?.message?.id || 0);
986
- if (!messageId) {
987
- throw new Error("Twinkle did not confirm the branch update message.");
988
- }
989
- return messageId;
990
- } catch (error) {
991
- for (const delayMs of [250, 750, 1_500]) {
992
- await sleep(delayMs);
993
- const recovered = await lookup().catch(() => null);
994
- const recoveredMessageId = Number(recovered?.branchNotice?.id || 0);
995
- if (recoveredMessageId) return recoveredMessageId;
996
- }
997
- throw error;
998
- }
999
- }
1000
-
1001
- function summarizeProviderRuns(results) {
1002
- const runs = results.flatMap((result) => result.providerRuns || []);
1003
- const lastRun = runs.at(-1) || {};
1004
- const valueFromLastRun = (key, maximum) =>
1005
- String(lastRun?.[key] || "")
1006
- .trim()
1007
- .slice(0, maximum) || null;
1008
- const evidenceTiers = runs.map((run) => run?.evidenceTier);
1009
- const evidenceTier =
1010
- evidenceTiers.length > 0 &&
1011
- evidenceTiers.every((tier) => tier === "runtime_observed")
1012
- ? "runtime_observed"
1013
- : evidenceTiers.length > 0 &&
1014
- evidenceTiers.every((tier) =>
1015
- ["runtime_observed", "provider_reported"].includes(tier),
1016
- )
1017
- ? "provider_reported"
1018
- : "requested_only";
1019
- return {
1020
- resolvedModel: valueFromLastRun("resolvedModel", 160),
1021
- resolvedEffort: valueFromLastRun("resolvedEffort", 40),
1022
- resolvedServiceTier: valueFromLastRun("resolvedServiceTier", 40),
1023
- runtimeVersion: valueFromLastRun("runtimeVersion", 64),
1024
- evidenceTier,
1025
- providerPassCount: runs.length,
1026
- usage: {
1027
- passes: runs.slice(0, 20).map((run) => ({
1028
- phase: run.phase || null,
1029
- resolvedModel: run.resolvedModel || null,
1030
- resolvedEffort: run.resolvedEffort || null,
1031
- resolvedServiceTier: run.resolvedServiceTier || null,
1032
- runtimeVersion: run.runtimeVersion || null,
1033
- evidenceTier: run.evidenceTier || "requested_only",
1034
- usage: run.usage || {},
1035
- })),
1036
- },
1037
- };
1038
- }
1039
-
1040
- async function loadForumContext({ options, auth, buildId }) {
1041
- try {
1042
- const snapshot = await readCompleteBuildForumSnapshot({
1043
- options: { ...options, limit: 100 },
1044
- auth,
1045
- buildId,
1046
- maxPages: 100,
1047
- });
1048
- const events = (snapshot.events || []).slice(-50);
1049
- if (events.length === 0) return "";
1050
- return JSON.stringify(events).slice(0, MAX_FORUM_CONTEXT_CHARS);
1051
- } catch (error) {
1052
- console.error(
1053
- `lumine: scoped Forum context unavailable (${error?.message || error})`,
1054
- );
1055
- return "";
1056
- }
1057
- }
1058
-
1059
- function workshopAgentPrompt({
1060
- job,
1061
- persona,
1062
- relays,
1063
- forumContext,
1064
- helperOrdinal,
1065
- isFollowUp,
1066
- }) {
1067
- const relayText = relays
1068
- .map((relay, index) => {
1069
- const details = [
1070
- relay.summary,
1071
- relay.requestedOutcome ? `Outcome: ${relay.requestedOutcome}` : "",
1072
- relay.constraints?.length
1073
- ? `Constraints: ${relay.constraints.join("; ")}`
1074
- : "",
1075
- relay.acceptanceCriteria?.length
1076
- ? `Acceptance: ${relay.acceptanceCriteria.join("; ")}`
1077
- : "",
1078
- ].filter(Boolean);
1079
- return `${index + 1}. ${details.join("\n ")}`;
1080
- })
1081
- .join("\n");
1082
- const role =
1083
- helperOrdinal > 0
1084
- ? `You are helper ${helperOrdinal}. Review the current implementation and fix concrete gaps.`
1085
- : isFollowUp
1086
- ? "Apply these newly relayed follow-up requirements to the current implementation."
1087
- : `Implement this hands-on Lumine Build request on ${displayPersona(persona)}'s assigned contribution branch.`;
1088
- return `${role}
1089
-
1090
- 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.
1091
-
1092
- Approved relay:
1093
- ${relayText || "No additional relay text was supplied."}
1094
- ${forumContext ? `\nScoped Build Forum snapshot (project-visible context only):\n${forumContext}\n` : ""}
1095
- Return a short factual outcome summary after saving the canonical branch.`;
1096
- }
1097
-
1098
- async function sponsorJobRequest({
1099
- options,
1100
- sponsorAuth,
1101
- dutyId,
1102
- dutyLeaseToken,
1103
- jobId,
1104
- attemptToken,
1105
- path: suffix,
1106
- body = {},
1107
- }) {
1108
- return await sponsorRequest({
1109
- options,
1110
- auth: sponsorAuth,
1111
- method: "POST",
1112
- path: `/jobs/${jobId}${suffix}`,
1113
- body: {
1114
- dutySessionId: dutyId,
1115
- attemptToken,
1116
- ...(dutyLeaseToken ? { dutyLeaseToken } : {}),
1117
- ...body,
1118
- },
1119
- });
1120
- }
1121
-
1122
- async function retrySponsorTransport(operation) {
1123
- let lastError;
1124
- for (const delayMs of [0, 250, 750, 1_500]) {
1125
- if (delayMs) await sleep(delayMs);
1126
- try {
1127
- return await operation();
1128
- } catch (error) {
1129
- if (Number(error?.status || 0) > 0) throw error;
1130
- lastError = error;
1131
- }
1132
- }
1133
- throw lastError;
1134
- }
1135
-
1136
249
  async function sponsorRequest({
1137
250
  options,
1138
251
  auth,
@@ -1158,6 +271,14 @@ async function printSponsorStatus(options) {
1158
271
  }
1159
272
  console.log(`Application: ${status.application?.status || "not submitted"}`);
1160
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
+ }
1161
282
  if (status.sponsor?.capacity) {
1162
283
  const capacity = status.sponsor.capacity;
1163
284
  console.log(
@@ -1173,7 +294,7 @@ async function printSponsorStatus(options) {
1173
294
  ? "shared Zero/Ciel"
1174
295
  : `legacy assistant user ${duty.personaUserId}`;
1175
296
  console.log(
1176
- `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"}`,
1177
298
  );
1178
299
  }
1179
300
  }
@@ -1203,15 +324,6 @@ function printSponsorJobs(jobs, options) {
1203
324
  }
1204
325
  }
1205
326
 
1206
- function printDutyRuntime(duty) {
1207
- console.log(
1208
- `Runtime: model=${duty.requestedModel || "provider default"}, effort=${duty.requestedEffort || "provider default"}, service-tier=${duty.requestedServiceTier || "provider default"}`,
1209
- );
1210
- console.log(
1211
- `Limits: ${duty.capacity?.maxConcurrentTasks || 1} concurrent, ${duty.capacity?.maxSubagentsPerTask || 0} helpers, ${duty.capacity?.dailyTaskLimit || 0}/day, ${duty.capacity?.weeklyTaskLimit || 0}/week`,
1212
- );
1213
- }
1214
-
1215
327
  function printJsonOrLines(options, value, lines) {
1216
328
  if (options.json) console.log(JSON.stringify(value, null, 2));
1217
329
  else for (const line of lines) console.log(line);
@@ -1232,7 +344,7 @@ async function requiredApplicationText({ supplied, label, interactive, rl }) {
1232
344
 
1233
345
  async function confirmExactAgreement(rl) {
1234
346
  const answer = await rl.question(
1235
- "Type I ACCEPT to confirm this agreement and submit the application: ",
347
+ "Type I ACCEPT to confirm this sponsor agreement: ",
1236
348
  );
1237
349
  return answer.trim() === "I ACCEPT";
1238
350
  }
@@ -1262,50 +374,27 @@ function normalizeProviderList(value) {
1262
374
  return Array.from(new Set(submitted));
1263
375
  }
1264
376
 
1265
- function normalizeProvider(value) {
1266
- const provider = String(value || "")
1267
- .trim()
1268
- .toLowerCase();
1269
- if (!PROVIDERS.has(provider)) {
1270
- throw new Error("Pass --provider codex or --provider claude-code.");
1271
- }
1272
- return provider;
1273
- }
1274
-
1275
- function normalizeJobPersona(value) {
1276
- const persona = String(value || "")
1277
- .trim()
1278
- .toLowerCase();
1279
- if (!PERSONAS.has(persona)) {
1280
- throw new Error("Twinkle returned an invalid Workshop job assistant.");
1281
- }
1282
- return persona;
1283
- }
1284
-
1285
377
  function displayPersona(persona) {
1286
378
  return String(persona || "").toLowerCase() === "ciel" ? "Ciel" : "Zero";
1287
379
  }
1288
380
 
1289
- function summarizeWorkshopOutcome(finalText, job) {
1290
- const summary = String(finalText || "")
1291
- .trim()
1292
- .slice(0, 1000);
1293
- return (
1294
- summary ||
1295
- `Updated ${job.rootBuild.title} on the assigned contribution branch.`
1296
- );
1297
- }
1298
-
1299
381
  function sponsorUsage() {
1300
382
  return [
1301
383
  "Usage:",
1302
384
  " lumine sponsor agreement",
385
+ " lumine sponsor agreement accept [--accept-agreement]",
1303
386
  " lumine sponsor apply --providers codex[,claude-code] [--motivation <text>] [--availability <text>] [--accept-agreement]",
1304
387
  " lumine sponsor status",
1305
388
  " lumine sponsor withdraw [--yes]",
1306
389
  " lumine sponsor capacity [--concurrency <n>] [--helpers <n>] [--daily-limit <n>] [--weekly-limit <n>]",
1307
- " lumine sponsor duty start --provider <codex|claude-code> [--model <name>] [--effort <level>] [--service-tier <tier>]",
1308
- " 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>",
1309
398
  " lumine sponsor jobs [--limit <n>]",
1310
399
  ].join("\n");
1311
400
  }