@stage5/lumine 0.2.59 → 0.2.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/lib/admin.js +133 -6
- package/lib/commands.js +9 -6
- package/lib/sponsor-duty.js +133 -16
- package/lib/sponsor.js +1 -0
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +35 -0
package/README.md
CHANGED
|
@@ -276,6 +276,13 @@ heartbeat, supervisor, or provider-spawning daemon is not sponsor duty. The CLI
|
|
|
276
276
|
rejects a provider name that differs from the agent session actually running
|
|
277
277
|
the command.
|
|
278
278
|
|
|
279
|
+
`duty watch` may return a `teamAccessRequest` instead of a job. Tell the
|
|
280
|
+
sponsor which requester, owner, and project issued the invitation, then return
|
|
281
|
+
to duty without waiting or accepting on the sponsor's behalf. The ordinary
|
|
282
|
+
Twinkle team invitation is already waiting for the sponsor; no Workshop work
|
|
283
|
+
starts unless they accept it there. This notice is invitation metadata only;
|
|
284
|
+
it does not expose the unapproved Workshop plan or private chat.
|
|
285
|
+
|
|
279
286
|
When a user approves a plan, `duty watch` returns the scoped assignment file and
|
|
280
287
|
contribution workspace to that same session. It does not launch a fresh Codex
|
|
281
288
|
or Claude process. The on-duty agent uses this explicit lifecycle while it
|
|
@@ -283,13 +290,18 @@ works:
|
|
|
283
290
|
|
|
284
291
|
```bash
|
|
285
292
|
lumine sponsor job begin 42
|
|
293
|
+
lumine sponsor job update 42 --file /path/to/lumine-update.txt --phase building
|
|
286
294
|
lumine sponsor job pulse 42
|
|
287
295
|
lumine sponsor job relay-applied 42 101
|
|
288
296
|
lumine sponsor job complete 42 --summary "Implemented and tested the approved change"
|
|
289
297
|
```
|
|
290
298
|
|
|
291
299
|
`pulse` renews the duty/job leases and returns any newly approved follow-up.
|
|
292
|
-
|
|
300
|
+
`update` sends the exact file text back to Zero or Ciel as a user-visible
|
|
301
|
+
message from Lumine, their added brain. Use it for concise, deliberate
|
|
302
|
+
milestones only; never send hidden reasoning, raw terminal output, credentials,
|
|
303
|
+
tokens, private paths, or unrelated data. Run `pulse` between substantial work
|
|
304
|
+
steps. `relay-applied` is an explicit receipt;
|
|
293
305
|
completion is rejected until every delivered relay has actually been applied
|
|
294
306
|
and acknowledged. Use `helper-start` and `helper-complete` to record native
|
|
295
307
|
same-session subagents within the configured helper limit—the CLI records them
|
package/lib/admin.js
CHANGED
|
@@ -310,6 +310,9 @@ export async function adminCommand(options) {
|
|
|
310
310
|
typeof operation.body.buildReviewUnderstanding === "string",
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
+
if (operation.name === "ai-email-policy.set") {
|
|
314
|
+
assertAiEmailPolicySetResult({ operation, result });
|
|
315
|
+
}
|
|
313
316
|
if (operation.name === "daily-run.start") {
|
|
314
317
|
assertAdminTodoHandoffResult(result);
|
|
315
318
|
}
|
|
@@ -493,6 +496,55 @@ export function assertComposedCommentDraftResult({
|
|
|
493
496
|
throw error;
|
|
494
497
|
}
|
|
495
498
|
|
|
499
|
+
export function assertAiEmailPolicySetResult({ operation, result }) {
|
|
500
|
+
const requestedEmail = String(operation?.body?.email || "");
|
|
501
|
+
const requestedMode = String(operation?.body?.mode || "");
|
|
502
|
+
const policy = result?.data?.policy;
|
|
503
|
+
const projection = result?.data?.projection;
|
|
504
|
+
const accountUserIds = Array.isArray(result?.data?.accountUserIds)
|
|
505
|
+
? result.data.accountUserIds
|
|
506
|
+
: [];
|
|
507
|
+
const accountCount = Number(result?.data?.accountCount);
|
|
508
|
+
const expectedIdentityType =
|
|
509
|
+
requestedMode === "separate_accounts"
|
|
510
|
+
? "separate_verified_email"
|
|
511
|
+
: "verified_email";
|
|
512
|
+
if (
|
|
513
|
+
policy?.exists === true &&
|
|
514
|
+
policy?.normalizedEmail === requestedEmail &&
|
|
515
|
+
policy?.mode === requestedMode &&
|
|
516
|
+
Number.isSafeInteger(accountCount) &&
|
|
517
|
+
accountCount >= 0 &&
|
|
518
|
+
accountUserIds.length === accountCount &&
|
|
519
|
+
projection?.expectedIdentityType === expectedIdentityType &&
|
|
520
|
+
projection?.accountCount === accountCount &&
|
|
521
|
+
projection?.matchingAccountCount === accountCount &&
|
|
522
|
+
Array.isArray(projection?.mismatchedAccountUserIds) &&
|
|
523
|
+
projection.mismatchedAccountUserIds.length === 0 &&
|
|
524
|
+
projection?.converged === true
|
|
525
|
+
) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const error = new Error(
|
|
529
|
+
"The API did not confirm that every matching account converged to the requested AI email policy. Retry only after reviewing the canonical response and deployed API.",
|
|
530
|
+
);
|
|
531
|
+
error.code = "LUMINE_ADMIN_AI_EMAIL_POLICY_NOT_CONVERGED";
|
|
532
|
+
error.data = {
|
|
533
|
+
ok: false,
|
|
534
|
+
status: "partial_failure",
|
|
535
|
+
error: {
|
|
536
|
+
code: error.code,
|
|
537
|
+
message: error.message,
|
|
538
|
+
details: {
|
|
539
|
+
requestedEmail,
|
|
540
|
+
requestedMode,
|
|
541
|
+
canonical: result?.data || null,
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
|
|
496
548
|
const RECOMMENDATION_CONTENT_TYPES = new Map([
|
|
497
549
|
["comment", "comment"],
|
|
498
550
|
["aistory", "aiStory"],
|
|
@@ -616,6 +668,7 @@ function adminOperationRequiresRun(operation) {
|
|
|
616
668
|
"notable.add",
|
|
617
669
|
].includes(operation.name) &&
|
|
618
670
|
!operation.name.startsWith("ai-bucket.") &&
|
|
671
|
+
!operation.name.startsWith("ai-email-policy.") &&
|
|
619
672
|
!operation.name.startsWith("todo.")
|
|
620
673
|
);
|
|
621
674
|
}
|
|
@@ -774,6 +827,30 @@ export function parseAdminOperation(options) {
|
|
|
774
827
|
}
|
|
775
828
|
}
|
|
776
829
|
|
|
830
|
+
if (namespace === "ai-email-policy" || namespace === "ai-email-policies") {
|
|
831
|
+
const email = parseAiEmailPolicyEmail(options.adminEmail);
|
|
832
|
+
if (action === "get" || action === "status") {
|
|
833
|
+
return bodyReadOperation(
|
|
834
|
+
"ai-email-policy.get",
|
|
835
|
+
"POST",
|
|
836
|
+
"/cli/admin/ai-email-policies/lookup",
|
|
837
|
+
{ email },
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
if (action === "set") {
|
|
841
|
+
return writeOperation(
|
|
842
|
+
"ai-email-policy.set",
|
|
843
|
+
"PUT",
|
|
844
|
+
"/cli/admin/ai-email-policies",
|
|
845
|
+
{
|
|
846
|
+
email,
|
|
847
|
+
mode: parseAiEmailPolicyMode(options.adminMode),
|
|
848
|
+
note: parseAiEmailPolicyNote(options.note),
|
|
849
|
+
},
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
777
854
|
if (namespace === "todo" || namespace === "todos") {
|
|
778
855
|
if (!action || action === "list") {
|
|
779
856
|
return readOperation(
|
|
@@ -1985,6 +2062,10 @@ function readOperation(name, path, extra = {}) {
|
|
|
1985
2062
|
};
|
|
1986
2063
|
}
|
|
1987
2064
|
|
|
2065
|
+
function bodyReadOperation(name, method, path, body) {
|
|
2066
|
+
return { name, method, path, body, mutates: false };
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1988
2069
|
function writeOperation(name, method, path, body) {
|
|
1989
2070
|
return { name, method, path, body, mutates: true };
|
|
1990
2071
|
}
|
|
@@ -2156,6 +2237,49 @@ function parseAiBucketUserIds(value) {
|
|
|
2156
2237
|
return ids;
|
|
2157
2238
|
}
|
|
2158
2239
|
|
|
2240
|
+
function parseAiEmailPolicyEmail(value) {
|
|
2241
|
+
const email = String(value || "")
|
|
2242
|
+
.trim()
|
|
2243
|
+
.toLowerCase();
|
|
2244
|
+
const at = email.indexOf("@");
|
|
2245
|
+
if (
|
|
2246
|
+
!email ||
|
|
2247
|
+
email.length > 320 ||
|
|
2248
|
+
/\s/.test(email) ||
|
|
2249
|
+
at <= 0 ||
|
|
2250
|
+
at !== email.lastIndexOf("@") ||
|
|
2251
|
+
at === email.length - 1
|
|
2252
|
+
) {
|
|
2253
|
+
throw cliValidationError(
|
|
2254
|
+
"Pass a valid verified email with --email <address>.",
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
return email;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
function parseAiEmailPolicyMode(value) {
|
|
2261
|
+
const mode = String(value || "")
|
|
2262
|
+
.trim()
|
|
2263
|
+
.toLowerCase();
|
|
2264
|
+
if (mode !== "automatic" && mode !== "separate_accounts") {
|
|
2265
|
+
throw cliValidationError("--mode must be automatic or separate_accounts.");
|
|
2266
|
+
}
|
|
2267
|
+
return mode;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function parseAiEmailPolicyNote(value) {
|
|
2271
|
+
const note = String(value || "").trim();
|
|
2272
|
+
if (!note) {
|
|
2273
|
+
throw cliValidationError("Pass the policy reason with --note <text>.");
|
|
2274
|
+
}
|
|
2275
|
+
if (note.length > 255) {
|
|
2276
|
+
throw cliValidationError(
|
|
2277
|
+
"An AI email-policy note can be at most 255 characters.",
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
return note;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2159
2283
|
function parseRequiredInteger(
|
|
2160
2284
|
value,
|
|
2161
2285
|
label,
|
|
@@ -2421,7 +2545,9 @@ async function printSpooledAdminResult({ result, storage }) {
|
|
|
2421
2545
|
);
|
|
2422
2546
|
});
|
|
2423
2547
|
} else if (storage.collectionKey === "comments") {
|
|
2424
|
-
console.log(
|
|
2548
|
+
console.log(
|
|
2549
|
+
`${count} comment(s) for ${data.subject?.url || "this target"}:`,
|
|
2550
|
+
);
|
|
2425
2551
|
await forEachPaginatedResultItem(result, async (comment) => {
|
|
2426
2552
|
console.log(
|
|
2427
2553
|
`#${comment.id} ${comment.author?.username || "unknown"}: ${comment.content || "(empty)"}`,
|
|
@@ -2502,11 +2628,12 @@ function printAdminResult({ operation, result }) {
|
|
|
2502
2628
|
console.log(
|
|
2503
2629
|
`Sponsor-integrity case #${item.id}: ${String(item.status).toUpperCase()} · job #${item.job?.id} · ${item.sponsorUsername || `sponsor ${item.sponsorUserId}`}.`,
|
|
2504
2630
|
);
|
|
2505
|
-
const relays =
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2631
|
+
const relays =
|
|
2632
|
+
Array.isArray(item.relays) && item.relays.length > 0
|
|
2633
|
+
? item.relays
|
|
2634
|
+
: item.relay
|
|
2635
|
+
? [item.relay]
|
|
2636
|
+
: [];
|
|
2510
2637
|
console.log(
|
|
2511
2638
|
`Relays: ${relays.map((relay) => `${relay.kind || "initial_request"}: ${relay.summary || "(missing)"}`).join(" | ") || "(missing)"}`,
|
|
2512
2639
|
);
|
package/lib/commands.js
CHANGED
|
@@ -2409,12 +2409,10 @@ export function parseArgs(args) {
|
|
|
2409
2409
|
sponsorAgentOrdinal: raw.ordinal,
|
|
2410
2410
|
sponsorOutcome: raw.outcome ? String(raw.outcome) : "",
|
|
2411
2411
|
sponsorFailureReason: raw.reason ? String(raw.reason) : "",
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
sponsorResolvedEffort: raw.resolvedEffort
|
|
2416
|
-
? String(raw.resolvedEffort)
|
|
2417
|
-
: "",
|
|
2412
|
+
sponsorUpdateFile: raw.file ? String(raw.file) : "",
|
|
2413
|
+
sponsorUpdatePhase: raw.phase ? String(raw.phase) : "",
|
|
2414
|
+
sponsorResolvedModel: raw.resolvedModel ? String(raw.resolvedModel) : "",
|
|
2415
|
+
sponsorResolvedEffort: raw.resolvedEffort ? String(raw.resolvedEffort) : "",
|
|
2418
2416
|
sponsorResolvedServiceTier: raw.resolvedServiceTier
|
|
2419
2417
|
? String(raw.resolvedServiceTier)
|
|
2420
2418
|
: "",
|
|
@@ -2440,6 +2438,8 @@ export function parseArgs(args) {
|
|
|
2440
2438
|
adminBucketId: raw.bucketId ? String(raw.bucketId) : "",
|
|
2441
2439
|
adminLabel: raw.label ? String(raw.label) : "",
|
|
2442
2440
|
adminUserIds: raw.userIds ? String(raw.userIds) : "",
|
|
2441
|
+
adminEmail: raw.email ? String(raw.email) : "",
|
|
2442
|
+
adminMode: raw.mode ? String(raw.mode) : "",
|
|
2443
2443
|
adminType: raw.type ? String(raw.type) : "",
|
|
2444
2444
|
adminKind: raw.kind ? String(raw.kind) : "",
|
|
2445
2445
|
adminContentTypes: raw.contentTypes ? String(raw.contentTypes) : "",
|
|
@@ -2749,6 +2749,7 @@ export function printHelp() {
|
|
|
2749
2749
|
lumine sponsor duty watch [--wait-ms <ms>] [--json]
|
|
2750
2750
|
lumine sponsor duty pause|resume|stop
|
|
2751
2751
|
lumine sponsor job status|pulse|begin <job-id>
|
|
2752
|
+
lumine sponsor job update <job-id> --file <path> [--phase <name>]
|
|
2752
2753
|
lumine sponsor job relay-applied <job-id> <relay-id...>
|
|
2753
2754
|
lumine sponsor job helper-start|helper-complete <job-id> [options]
|
|
2754
2755
|
lumine sponsor job complete <job-id> --summary <text>
|
|
@@ -2804,6 +2805,8 @@ export function printHelp() {
|
|
|
2804
2805
|
lumine admin ai-bucket get --bucket-id <id> [--json]
|
|
2805
2806
|
lumine admin ai-bucket accounts add --bucket-id <id> --user-ids <id,id,...> [--note <text>] [--json]
|
|
2806
2807
|
lumine admin ai-bucket note set --bucket-id <id> --note <text> [--json]
|
|
2808
|
+
lumine admin ai-email-policy get --email <address> [--json]
|
|
2809
|
+
lumine admin ai-email-policy set --email <address> --mode <automatic|separate_accounts> --note <text> [--json]
|
|
2807
2810
|
lumine admin daily-run start [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
|
|
2808
2811
|
lumine admin daily-run status|report|complete|fail [--reason <text>] [--json]
|
|
2809
2812
|
lumine admin daily-run escalation add --target <target> --note <summary> [--severity attention|urgent] [--json]
|
package/lib/sponsor-duty.js
CHANGED
|
@@ -21,6 +21,18 @@ const DEFAULT_DUTY_WATCH_MS = 20_000;
|
|
|
21
21
|
const MAX_DUTY_WATCH_MS = 60_000;
|
|
22
22
|
const MAX_FORUM_CONTEXT_CHARS = 8_000;
|
|
23
23
|
|
|
24
|
+
function sanitizeSponsorTerminalLabel(value, fallback) {
|
|
25
|
+
const label = String(value || "")
|
|
26
|
+
.replace(
|
|
27
|
+
/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,
|
|
28
|
+
" ",
|
|
29
|
+
)
|
|
30
|
+
.replace(/\s+/g, " ")
|
|
31
|
+
.trim()
|
|
32
|
+
.slice(0, 200);
|
|
33
|
+
return label || fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export async function sponsorDutyCommand(options, args, commandServices) {
|
|
25
37
|
const action = String(args[0] || "status")
|
|
26
38
|
.trim()
|
|
@@ -66,6 +78,13 @@ export async function sponsorJobCommand(options, args, commandServices) {
|
|
|
66
78
|
await withSponsorStateLock(options, () => beginJob(options, jobId));
|
|
67
79
|
return;
|
|
68
80
|
}
|
|
81
|
+
if (action === "update") {
|
|
82
|
+
assertNoExtraArgs(args, 2);
|
|
83
|
+
await withSponsorStateLock(options, () =>
|
|
84
|
+
publishDialogueUpdate(options, jobId),
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
69
88
|
if (action === "relay-applied") {
|
|
70
89
|
const relayIds = args
|
|
71
90
|
.slice(2)
|
|
@@ -343,6 +362,40 @@ async function watchDuty(options, commandServices) {
|
|
|
343
362
|
);
|
|
344
363
|
continue;
|
|
345
364
|
}
|
|
365
|
+
if (claim.teamAccessRequest) {
|
|
366
|
+
const request = claim.teamAccessRequest;
|
|
367
|
+
const requester = request.requesterUsername
|
|
368
|
+
? `@${sanitizeSponsorTerminalLabel(
|
|
369
|
+
request.requesterUsername,
|
|
370
|
+
`user-${request.requesterUserId}`,
|
|
371
|
+
)}`
|
|
372
|
+
: `user #${request.requesterUserId}`;
|
|
373
|
+
const owner = request.ownerUsername
|
|
374
|
+
? `@${sanitizeSponsorTerminalLabel(
|
|
375
|
+
request.ownerUsername,
|
|
376
|
+
`user-${request.ownerUserId}`,
|
|
377
|
+
)}`
|
|
378
|
+
: `user #${request.ownerUserId}`;
|
|
379
|
+
const buildTitle = sanitizeSponsorTerminalLabel(
|
|
380
|
+
request.buildTitle,
|
|
381
|
+
`Build #${request.buildId}`,
|
|
382
|
+
);
|
|
383
|
+
printJsonOrLines(
|
|
384
|
+
options,
|
|
385
|
+
{
|
|
386
|
+
duty: publicDuty(state.duty),
|
|
387
|
+
teamAccessRequest: request,
|
|
388
|
+
assignment: null,
|
|
389
|
+
nextCommand: "lumine sponsor duty watch --json",
|
|
390
|
+
},
|
|
391
|
+
[
|
|
392
|
+
`${requester} asked to invite this sponsor account to ${buildTitle}, owned by ${owner}.`,
|
|
393
|
+
"Ask the sponsor whether they want to join. Their usual Twinkle team invitation is already waiting; no Workshop work starts unless they accept it.",
|
|
394
|
+
"Do not hold the Workshop queue open for a reply. Return to duty after sharing this notice.",
|
|
395
|
+
],
|
|
396
|
+
);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
346
399
|
if (claim.job) {
|
|
347
400
|
state = await recordClaim({ options, state, claim });
|
|
348
401
|
const prepared = await prepareClaimedJob({
|
|
@@ -539,10 +592,61 @@ async function beginJob(options, jobId) {
|
|
|
539
592
|
printJsonOrLines(options, { job: canonicalJobState.job, coordinator: agent }, [
|
|
540
593
|
`Workshop job #${jobId} is now being handled by this same on-duty agent session.`,
|
|
541
594
|
`Apply the approved plan in ${jobState.workspaceDir}.`,
|
|
595
|
+
`Introduce yourself to ${displayPersona(jobState.job.persona)} as Lumine with: lumine sponsor job update ${jobId} --file <message-file> --phase starting`,
|
|
542
596
|
`Check in with: lumine sponsor job pulse ${jobId}`,
|
|
543
597
|
]);
|
|
544
598
|
}
|
|
545
599
|
|
|
600
|
+
async function publishDialogueUpdate(options, jobId) {
|
|
601
|
+
const messageFile = String(options.sponsorUpdateFile || "").trim();
|
|
602
|
+
if (!messageFile) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
"Write the deliberate user-facing Lumine update to a file and pass --file <path>.",
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const phase = String(options.sponsorUpdatePhase || "").trim();
|
|
608
|
+
if (phase.length > 40) {
|
|
609
|
+
throw new Error("--phase must be at most 40 characters.");
|
|
610
|
+
}
|
|
611
|
+
const messagePath = path.resolve(messageFile);
|
|
612
|
+
const messageStat = await fs.stat(messagePath);
|
|
613
|
+
if (!messageStat.isFile() || messageStat.size > 8_000) {
|
|
614
|
+
throw new Error("The Lumine update file must be a text file under 8 KB.");
|
|
615
|
+
}
|
|
616
|
+
const message = String(await fs.readFile(messagePath, "utf8")).trim();
|
|
617
|
+
if (!message) throw new Error("The Lumine update file is empty.");
|
|
618
|
+
if (message.length > 2_000) {
|
|
619
|
+
throw new Error("The Lumine update must be at most 2,000 characters.");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const auth = await ensureSponsorAuth(options);
|
|
623
|
+
let state = await loadOwnedState({ options, auth });
|
|
624
|
+
({ state } = await heartbeatJob({ options, auth, state, jobId }));
|
|
625
|
+
const jobState = requireJobState(state, jobId);
|
|
626
|
+
const clientUpdateKey = randomBytes(18).toString("base64url");
|
|
627
|
+
const result = await retrySponsorCheckIn(() =>
|
|
628
|
+
sponsorJobRequest({
|
|
629
|
+
options,
|
|
630
|
+
auth,
|
|
631
|
+
state,
|
|
632
|
+
jobState,
|
|
633
|
+
path: "/dialogue",
|
|
634
|
+
body: {
|
|
635
|
+
clientUpdateKey,
|
|
636
|
+
phase: phase || null,
|
|
637
|
+
message,
|
|
638
|
+
},
|
|
639
|
+
}),
|
|
640
|
+
);
|
|
641
|
+
if (!result?.update?.message) {
|
|
642
|
+
throw new Error("Twinkle did not confirm the canonical Lumine update.");
|
|
643
|
+
}
|
|
644
|
+
printJsonOrLines(options, result, [
|
|
645
|
+
`Lumine → ${displayPersona(jobState.job.persona)}:`,
|
|
646
|
+
result.update.message,
|
|
647
|
+
]);
|
|
648
|
+
}
|
|
649
|
+
|
|
546
650
|
async function startHelper(options, jobId) {
|
|
547
651
|
const auth = await ensureSponsorAuth(options);
|
|
548
652
|
let state = await loadOwnedState({ options, auth });
|
|
@@ -1177,25 +1281,36 @@ async function writeAssignment(jobState, state) {
|
|
|
1177
1281
|
const unapplied = new Set(unappliedRelayIds(jobState));
|
|
1178
1282
|
const relays = (jobState.relays || [])
|
|
1179
1283
|
.map((relay) => {
|
|
1180
|
-
const
|
|
1181
|
-
relay.
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1284
|
+
const dialogueText =
|
|
1285
|
+
String(relay.dialogueText || "").trim() ||
|
|
1286
|
+
[
|
|
1287
|
+
relay.summary,
|
|
1288
|
+
relay.projectTitleHint
|
|
1289
|
+
? `Project: ${relay.projectTitleHint}`
|
|
1290
|
+
: "",
|
|
1291
|
+
relay.requestedOutcome
|
|
1292
|
+
? `What to build: ${relay.requestedOutcome}`
|
|
1293
|
+
: "",
|
|
1294
|
+
relay.constraints?.length
|
|
1295
|
+
? `Keeping in mind:\n${relay.constraints.map((item) => `• ${item}`).join("\n")}`
|
|
1296
|
+
: "",
|
|
1297
|
+
relay.acceptanceCriteria?.length
|
|
1298
|
+
? `Done means:\n${relay.acceptanceCriteria.map((item) => `• ${item}`).join("\n")}`
|
|
1299
|
+
: "",
|
|
1300
|
+
]
|
|
1301
|
+
.filter(Boolean)
|
|
1302
|
+
.join("\n\n");
|
|
1303
|
+
return `## Relay #${relay.id}${unapplied.has(Number(relay.id)) ? " (not yet acknowledged as applied)" : " (applied receipt confirmed)"}\n\n${dialogueText}`;
|
|
1191
1304
|
})
|
|
1192
1305
|
.join("\n\n");
|
|
1193
1306
|
const content = `# Lumine Build Workshop assignment #${jobState.job.id}
|
|
1194
1307
|
|
|
1195
|
-
You are the same live ${displayProvider(state.operatorSession.provider)} agent session that opened sponsor duty. Zero or Ciel is the user's visible teammate; you are lending that teammate an added brain. Perform this work in this session. Do not launch a replacement coding provider or leave an unattended heartbeat process standing in for you.
|
|
1308
|
+
You are the same live ${displayProvider(state.operatorSession.provider)} agent session that opened sponsor duty. Zero or Ciel is the user's visible teammate; you are lending that teammate an added brain. In every user-facing Workshop update, you are Lumine: the added brain talking with that teammate. Perform this work in this session. Do not launch a replacement coding provider or leave an unattended heartbeat process standing in for you.
|
|
1196
1309
|
|
|
1197
1310
|
The user approved sharing only this structured plan, active-job follow-ups, the assigned Build project, and its scoped Forum. Never inspect or infer from their private Zero/Ciel chat. Edit only contribution Build #${jobState.job.contributionBuild.id}; never merge Main, publish, or contact the user directly.
|
|
1198
1311
|
|
|
1312
|
+
Lumine updates are a deliberate public channel. Write concise messages about what you are checking, what you found, or what happens next. Never publish hidden chain-of-thought, raw terminal output, credentials, tokens, private paths, or unrelated data. The exact file text you submit is shown in Twinkle and echoed back by the CLI.
|
|
1313
|
+
|
|
1199
1314
|
- User: @${jobState.job.requester.username}
|
|
1200
1315
|
- Visible teammate: ${displayPersona(jobState.job.persona)}
|
|
1201
1316
|
- Main project: ${jobState.job.rootBuild.title} (#${jobState.job.rootBuild.id})
|
|
@@ -1208,10 +1323,11 @@ ${jobState.forumContext ? `## Scoped Build Forum snapshot\n\n${jobState.forumCon
|
|
|
1208
1323
|
## Duty protocol
|
|
1209
1324
|
|
|
1210
1325
|
1. Run \`lumine sponsor job begin ${jobState.job.id}\` before editing.
|
|
1211
|
-
2.
|
|
1212
|
-
3.
|
|
1213
|
-
4.
|
|
1214
|
-
5.
|
|
1326
|
+
2. Introduce yourself and publish meaningful milestones with \`lumine sponsor job update ${jobState.job.id} --file <message-file> --phase <name>\`. Speak as Lumine, never as the underlying provider.
|
|
1327
|
+
3. Work only in the workspace above. Use your native same-session subagents only after registering each with \`helper-start\`. The coordinator alone runs sponsor CLI commands; helpers report their results back to it.
|
|
1328
|
+
4. Run \`lumine sponsor job pulse ${jobState.job.id}\` between substantial work steps to keep the lease alive and receive approved follow-ups.
|
|
1329
|
+
5. After applying a relay, record its exact ID with \`lumine sponsor job relay-applied ${jobState.job.id} <relay-id...>\`.
|
|
1330
|
+
6. Finish with \`lumine sponsor job complete ${jobState.job.id} --summary "..."\`.
|
|
1215
1331
|
`;
|
|
1216
1332
|
await fs.writeFile(jobState.assignmentPath, content, {
|
|
1217
1333
|
encoding: "utf8",
|
|
@@ -1865,6 +1981,7 @@ function sponsorJobUsage() {
|
|
|
1865
1981
|
"Usage:",
|
|
1866
1982
|
" lumine sponsor job status|pulse <job-id>",
|
|
1867
1983
|
" lumine sponsor job begin <job-id>",
|
|
1984
|
+
" lumine sponsor job update <job-id> --file <path> [--phase <name>]",
|
|
1868
1985
|
" lumine sponsor job relay-applied <job-id> <relay-id...>",
|
|
1869
1986
|
" lumine sponsor job helper-start <job-id> [--ordinal <n>]",
|
|
1870
1987
|
" lumine sponsor job helper-complete <job-id> --ordinal <n> --outcome <text> [--resolved-model <name>] [--resolved-effort <level>]",
|
package/lib/sponsor.js
CHANGED
|
@@ -391,6 +391,7 @@ function sponsorUsage() {
|
|
|
391
391
|
" lumine sponsor duty watch [--wait-ms <ms>] [--json]",
|
|
392
392
|
" lumine sponsor duty status|pause|resume|stop",
|
|
393
393
|
" lumine sponsor job status|pulse|begin <job-id>",
|
|
394
|
+
" lumine sponsor job update <job-id> --file <path> [--phase <name>]",
|
|
394
395
|
" lumine sponsor job relay-applied <job-id> <relay-id...>",
|
|
395
396
|
" lumine sponsor job helper-start|helper-complete <job-id> [options]",
|
|
396
397
|
" lumine sponsor job complete <job-id> --summary <text>",
|
package/package.json
CHANGED
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -93,6 +93,41 @@ Mikey; routine administrator runs still escalate suspected alternate accounts
|
|
|
93
93
|
and never auto-enforce. `accounts add` and `note set` still accept only an
|
|
94
94
|
existing **unbanned** bucket.
|
|
95
95
|
|
|
96
|
+
### Shared verified-email policies
|
|
97
|
+
|
|
98
|
+
A verified address proves that an account can use that mailbox; it does not
|
|
99
|
+
always prove that every account using the mailbox is one person. When a teacher,
|
|
100
|
+
school, or other provisioning owner intentionally verifies separate people's
|
|
101
|
+
accounts with one address, record that confirmed operational fact explicitly:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
lumine admin ai-email-policy get --email teacher@example.com --json
|
|
105
|
+
lumine admin ai-email-policy set --email teacher@example.com \
|
|
106
|
+
--mode separate_accounts \
|
|
107
|
+
--note "Teacher-confirmed classroom provisioning address" --json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`separate_accounts` preserves ordinary email verification and recovery while
|
|
111
|
+
giving every current and future account whose stable verified address matches
|
|
112
|
+
its own AI Energy and AI Card quota identity. The mutation re-attributes the
|
|
113
|
+
current day's per-account usage, updates all affected live quota projections
|
|
114
|
+
from canonical server state, and records the real operator in the private
|
|
115
|
+
Lumine audit log. It is a provisioning policy, not an alternate-account finding
|
|
116
|
+
or moderation flag, and must never be inferred from account count, usernames,
|
|
117
|
+
devices, IPs, or email-provider heuristics.
|
|
118
|
+
|
|
119
|
+
The CLI accepts a successful `set` only when the writer response confirms the
|
|
120
|
+
requested policy and reports every matching account on its exact expected
|
|
121
|
+
durable projection. A missing or mismatched projection fails closed instead of
|
|
122
|
+
letting an operator mistake a stored policy row for completed account repair.
|
|
123
|
+
|
|
124
|
+
Use `--mode automatic` with a new explanatory `--note` to restore Twinkle's
|
|
125
|
+
default verified-email grouping. Both modes are durable and apply to later
|
|
126
|
+
signups and later verifications without another code change. An active
|
|
127
|
+
email-wide manual identity rule conflicts with `separate_accounts`, so the API
|
|
128
|
+
rejects either operation until the operator explicitly resolves the conflict.
|
|
129
|
+
Exact per-user manual bucket rules remain authoritative in both modes.
|
|
130
|
+
|
|
96
131
|
### Audited identity inspection
|
|
97
132
|
|
|
98
133
|
Account-family evidence is private operator work, never a Zero/Ciel public
|