@stage5/lumine 0.2.58 → 0.2.60
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 +6 -1
- package/lib/commands.js +3 -0
- package/lib/sponsor-duty.js +149 -21
- package/lib/sponsor.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -283,13 +283,18 @@ works:
|
|
|
283
283
|
|
|
284
284
|
```bash
|
|
285
285
|
lumine sponsor job begin 42
|
|
286
|
+
lumine sponsor job update 42 --file /path/to/lumine-update.txt --phase building
|
|
286
287
|
lumine sponsor job pulse 42
|
|
287
288
|
lumine sponsor job relay-applied 42 101
|
|
288
289
|
lumine sponsor job complete 42 --summary "Implemented and tested the approved change"
|
|
289
290
|
```
|
|
290
291
|
|
|
291
292
|
`pulse` renews the duty/job leases and returns any newly approved follow-up.
|
|
292
|
-
|
|
293
|
+
`update` sends the exact file text back to Zero or Ciel as a user-visible
|
|
294
|
+
message from Lumine, their added brain. Use it for concise, deliberate
|
|
295
|
+
milestones only; never send hidden reasoning, raw terminal output, credentials,
|
|
296
|
+
tokens, private paths, or unrelated data. Run `pulse` between substantial work
|
|
297
|
+
steps. `relay-applied` is an explicit receipt;
|
|
293
298
|
completion is rejected until every delivered relay has actually been applied
|
|
294
299
|
and acknowledged. Use `helper-start` and `helper-complete` to record native
|
|
295
300
|
same-session subagents within the configured helper limit—the CLI records them
|
package/lib/commands.js
CHANGED
|
@@ -2409,6 +2409,8 @@ 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
|
+
sponsorUpdateFile: raw.file ? String(raw.file) : "",
|
|
2413
|
+
sponsorUpdatePhase: raw.phase ? String(raw.phase) : "",
|
|
2412
2414
|
sponsorResolvedModel: raw.resolvedModel
|
|
2413
2415
|
? String(raw.resolvedModel)
|
|
2414
2416
|
: "",
|
|
@@ -2749,6 +2751,7 @@ export function printHelp() {
|
|
|
2749
2751
|
lumine sponsor duty watch [--wait-ms <ms>] [--json]
|
|
2750
2752
|
lumine sponsor duty pause|resume|stop
|
|
2751
2753
|
lumine sponsor job status|pulse|begin <job-id>
|
|
2754
|
+
lumine sponsor job update <job-id> --file <path> [--phase <name>]
|
|
2752
2755
|
lumine sponsor job relay-applied <job-id> <relay-id...>
|
|
2753
2756
|
lumine sponsor job helper-start|helper-complete <job-id> [options]
|
|
2754
2757
|
lumine sponsor job complete <job-id> --summary <text>
|
package/lib/sponsor-duty.js
CHANGED
|
@@ -66,6 +66,13 @@ export async function sponsorJobCommand(options, args, commandServices) {
|
|
|
66
66
|
await withSponsorStateLock(options, () => beginJob(options, jobId));
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
+
if (action === "update") {
|
|
70
|
+
assertNoExtraArgs(args, 2);
|
|
71
|
+
await withSponsorStateLock(options, () =>
|
|
72
|
+
publishDialogueUpdate(options, jobId),
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
69
76
|
if (action === "relay-applied") {
|
|
70
77
|
const relayIds = args
|
|
71
78
|
.slice(2)
|
|
@@ -234,7 +241,7 @@ async function reconcileExistingStateBeforeStart({
|
|
|
234
241
|
}
|
|
235
242
|
const preservedWorkspaces = dutyWorkspacePaths(existingState);
|
|
236
243
|
if (preservedWorkspaces.length > 0) {
|
|
237
|
-
const archivePath = await archiveSponsorState(options);
|
|
244
|
+
const archivePath = await archiveSponsorState(options, existingState);
|
|
238
245
|
console.error(
|
|
239
246
|
`lumine: preserved the expired duty record at ${archivePath}.`,
|
|
240
247
|
);
|
|
@@ -408,7 +415,7 @@ async function changeDutyState(options, action) {
|
|
|
408
415
|
const preservedWorkspaces = dutyWorkspacePaths(localState);
|
|
409
416
|
const localArchive =
|
|
410
417
|
preservedWorkspaces.length > 0
|
|
411
|
-
? await archiveSponsorState(options)
|
|
418
|
+
? await archiveSponsorState(options, localState)
|
|
412
419
|
: null;
|
|
413
420
|
if (!localArchive) await removeSponsorState(options);
|
|
414
421
|
printJsonOrLines(
|
|
@@ -472,7 +479,7 @@ async function changeDutyState(options, action) {
|
|
|
472
479
|
if (nextState === "stopped") {
|
|
473
480
|
preservedWorkspaces = dutyWorkspacePaths(localState);
|
|
474
481
|
if (preservedWorkspaces.length > 0) {
|
|
475
|
-
localArchive = await archiveSponsorState(options);
|
|
482
|
+
localArchive = await archiveSponsorState(options, localState);
|
|
476
483
|
} else {
|
|
477
484
|
await removeSponsorState(options);
|
|
478
485
|
}
|
|
@@ -539,10 +546,61 @@ async function beginJob(options, jobId) {
|
|
|
539
546
|
printJsonOrLines(options, { job: canonicalJobState.job, coordinator: agent }, [
|
|
540
547
|
`Workshop job #${jobId} is now being handled by this same on-duty agent session.`,
|
|
541
548
|
`Apply the approved plan in ${jobState.workspaceDir}.`,
|
|
549
|
+
`Introduce yourself to ${displayPersona(jobState.job.persona)} as Lumine with: lumine sponsor job update ${jobId} --file <message-file> --phase starting`,
|
|
542
550
|
`Check in with: lumine sponsor job pulse ${jobId}`,
|
|
543
551
|
]);
|
|
544
552
|
}
|
|
545
553
|
|
|
554
|
+
async function publishDialogueUpdate(options, jobId) {
|
|
555
|
+
const messageFile = String(options.sponsorUpdateFile || "").trim();
|
|
556
|
+
if (!messageFile) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
"Write the deliberate user-facing Lumine update to a file and pass --file <path>.",
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
const phase = String(options.sponsorUpdatePhase || "").trim();
|
|
562
|
+
if (phase.length > 40) {
|
|
563
|
+
throw new Error("--phase must be at most 40 characters.");
|
|
564
|
+
}
|
|
565
|
+
const messagePath = path.resolve(messageFile);
|
|
566
|
+
const messageStat = await fs.stat(messagePath);
|
|
567
|
+
if (!messageStat.isFile() || messageStat.size > 8_000) {
|
|
568
|
+
throw new Error("The Lumine update file must be a text file under 8 KB.");
|
|
569
|
+
}
|
|
570
|
+
const message = String(await fs.readFile(messagePath, "utf8")).trim();
|
|
571
|
+
if (!message) throw new Error("The Lumine update file is empty.");
|
|
572
|
+
if (message.length > 2_000) {
|
|
573
|
+
throw new Error("The Lumine update must be at most 2,000 characters.");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const auth = await ensureSponsorAuth(options);
|
|
577
|
+
let state = await loadOwnedState({ options, auth });
|
|
578
|
+
({ state } = await heartbeatJob({ options, auth, state, jobId }));
|
|
579
|
+
const jobState = requireJobState(state, jobId);
|
|
580
|
+
const clientUpdateKey = randomBytes(18).toString("base64url");
|
|
581
|
+
const result = await retrySponsorCheckIn(() =>
|
|
582
|
+
sponsorJobRequest({
|
|
583
|
+
options,
|
|
584
|
+
auth,
|
|
585
|
+
state,
|
|
586
|
+
jobState,
|
|
587
|
+
path: "/dialogue",
|
|
588
|
+
body: {
|
|
589
|
+
clientUpdateKey,
|
|
590
|
+
phase: phase || null,
|
|
591
|
+
message,
|
|
592
|
+
},
|
|
593
|
+
}),
|
|
594
|
+
);
|
|
595
|
+
if (!result?.update?.message) {
|
|
596
|
+
throw new Error("Twinkle did not confirm the canonical Lumine update.");
|
|
597
|
+
}
|
|
598
|
+
printJsonOrLines(options, result, [
|
|
599
|
+
`Lumine → ${displayPersona(jobState.job.persona)}:`,
|
|
600
|
+
result.update.message,
|
|
601
|
+
]);
|
|
602
|
+
}
|
|
603
|
+
|
|
546
604
|
async function startHelper(options, jobId) {
|
|
547
605
|
const auth = await ensureSponsorAuth(options);
|
|
548
606
|
let state = await loadOwnedState({ options, auth });
|
|
@@ -1177,25 +1235,36 @@ async function writeAssignment(jobState, state) {
|
|
|
1177
1235
|
const unapplied = new Set(unappliedRelayIds(jobState));
|
|
1178
1236
|
const relays = (jobState.relays || [])
|
|
1179
1237
|
.map((relay) => {
|
|
1180
|
-
const
|
|
1181
|
-
relay.
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1238
|
+
const dialogueText =
|
|
1239
|
+
String(relay.dialogueText || "").trim() ||
|
|
1240
|
+
[
|
|
1241
|
+
relay.summary,
|
|
1242
|
+
relay.projectTitleHint
|
|
1243
|
+
? `Project: ${relay.projectTitleHint}`
|
|
1244
|
+
: "",
|
|
1245
|
+
relay.requestedOutcome
|
|
1246
|
+
? `What to build: ${relay.requestedOutcome}`
|
|
1247
|
+
: "",
|
|
1248
|
+
relay.constraints?.length
|
|
1249
|
+
? `Keeping in mind:\n${relay.constraints.map((item) => `• ${item}`).join("\n")}`
|
|
1250
|
+
: "",
|
|
1251
|
+
relay.acceptanceCriteria?.length
|
|
1252
|
+
? `Done means:\n${relay.acceptanceCriteria.map((item) => `• ${item}`).join("\n")}`
|
|
1253
|
+
: "",
|
|
1254
|
+
]
|
|
1255
|
+
.filter(Boolean)
|
|
1256
|
+
.join("\n\n");
|
|
1257
|
+
return `## Relay #${relay.id}${unapplied.has(Number(relay.id)) ? " (not yet acknowledged as applied)" : " (applied receipt confirmed)"}\n\n${dialogueText}`;
|
|
1191
1258
|
})
|
|
1192
1259
|
.join("\n\n");
|
|
1193
1260
|
const content = `# Lumine Build Workshop assignment #${jobState.job.id}
|
|
1194
1261
|
|
|
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.
|
|
1262
|
+
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
1263
|
|
|
1197
1264
|
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
1265
|
|
|
1266
|
+
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.
|
|
1267
|
+
|
|
1199
1268
|
- User: @${jobState.job.requester.username}
|
|
1200
1269
|
- Visible teammate: ${displayPersona(jobState.job.persona)}
|
|
1201
1270
|
- Main project: ${jobState.job.rootBuild.title} (#${jobState.job.rootBuild.id})
|
|
@@ -1208,10 +1277,11 @@ ${jobState.forumContext ? `## Scoped Build Forum snapshot\n\n${jobState.forumCon
|
|
|
1208
1277
|
## Duty protocol
|
|
1209
1278
|
|
|
1210
1279
|
1. Run \`lumine sponsor job begin ${jobState.job.id}\` before editing.
|
|
1211
|
-
2.
|
|
1212
|
-
3.
|
|
1213
|
-
4.
|
|
1214
|
-
5.
|
|
1280
|
+
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.
|
|
1281
|
+
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.
|
|
1282
|
+
4. Run \`lumine sponsor job pulse ${jobState.job.id}\` between substantial work steps to keep the lease alive and receive approved follow-ups.
|
|
1283
|
+
5. After applying a relay, record its exact ID with \`lumine sponsor job relay-applied ${jobState.job.id} <relay-id...>\`.
|
|
1284
|
+
6. Finish with \`lumine sponsor job complete ${jobState.job.id} --summary "..."\`.
|
|
1215
1285
|
`;
|
|
1216
1286
|
await fs.writeFile(jobState.assignmentPath, content, {
|
|
1217
1287
|
encoding: "utf8",
|
|
@@ -1412,7 +1482,17 @@ async function sponsorLockIsStale(lockPath) {
|
|
|
1412
1482
|
}
|
|
1413
1483
|
}
|
|
1414
1484
|
|
|
1415
|
-
async function archiveSponsorState(options) {
|
|
1485
|
+
async function archiveSponsorState(options, state) {
|
|
1486
|
+
for (const jobState of Object.values(state.jobs || {})) {
|
|
1487
|
+
await scrubJobCredentials(jobState);
|
|
1488
|
+
}
|
|
1489
|
+
for (const preserved of Array.isArray(state.preservedWorkspaces)
|
|
1490
|
+
? state.preservedWorkspaces
|
|
1491
|
+
: []) {
|
|
1492
|
+
await scrubPreservedWorkspaceCredential(preserved);
|
|
1493
|
+
}
|
|
1494
|
+
state.duty = { ...state.duty, leaseToken: null };
|
|
1495
|
+
await writeSponsorState(options, state);
|
|
1416
1496
|
const filePath = sponsorDutyStatePath(options);
|
|
1417
1497
|
const archivePath = `${filePath}.stopped-${Date.now()}`;
|
|
1418
1498
|
await fs.rename(filePath, archivePath);
|
|
@@ -1422,6 +1502,7 @@ async function archiveSponsorState(options) {
|
|
|
1422
1502
|
|
|
1423
1503
|
async function removeCompletedJob({ options, state, jobId }) {
|
|
1424
1504
|
const jobState = requireJobState(state, jobId);
|
|
1505
|
+
await scrubJobCredentials(jobState);
|
|
1425
1506
|
const nextJobs = { ...(state.jobs || {}) };
|
|
1426
1507
|
delete nextJobs[String(jobId)];
|
|
1427
1508
|
state.jobs = nextJobs;
|
|
@@ -1435,6 +1516,7 @@ async function removeCompletedJob({ options, state, jobId }) {
|
|
|
1435
1516
|
|
|
1436
1517
|
async function preserveFailedJobWorkspace({ options, state, jobId, reason }) {
|
|
1437
1518
|
const jobState = requireJobState(state, jobId);
|
|
1519
|
+
await scrubJobCredentials(jobState);
|
|
1438
1520
|
const nextJobs = { ...(state.jobs || {}) };
|
|
1439
1521
|
delete nextJobs[String(jobId)];
|
|
1440
1522
|
state.jobs = nextJobs;
|
|
@@ -1449,6 +1531,7 @@ async function preserveFailedJobWorkspace({ options, state, jobId, reason }) {
|
|
|
1449
1531
|
workspaceDir: jobState.workspaceDir,
|
|
1450
1532
|
reason: reason.slice(0, 1000),
|
|
1451
1533
|
preservedAt: new Date().toISOString(),
|
|
1534
|
+
credentialsRemovedAt: jobState.credentialsRemovedAt,
|
|
1452
1535
|
},
|
|
1453
1536
|
];
|
|
1454
1537
|
await writeSponsorState(options, state);
|
|
@@ -1473,6 +1556,50 @@ function dutyWorkspacePaths(state) {
|
|
|
1473
1556
|
}
|
|
1474
1557
|
|
|
1475
1558
|
async function cleanupJobFiles(jobState) {
|
|
1559
|
+
const tempDir = resolveJobTempDir(jobState);
|
|
1560
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
async function scrubJobCredentials(jobState) {
|
|
1564
|
+
const tempDir = resolveJobTempDir(jobState);
|
|
1565
|
+
const expectedAuthFile = path.join(tempDir, "job-auth.json");
|
|
1566
|
+
const authFile = path.resolve(
|
|
1567
|
+
String(jobState?.authFile || expectedAuthFile),
|
|
1568
|
+
);
|
|
1569
|
+
if (authFile !== expectedAuthFile) {
|
|
1570
|
+
throw new Error(
|
|
1571
|
+
"Refused to remove an unrecognized Workshop credential file.",
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
await fs.unlink(authFile).catch((error) => {
|
|
1575
|
+
if (error.code !== "ENOENT") throw error;
|
|
1576
|
+
});
|
|
1577
|
+
jobState.authFile = null;
|
|
1578
|
+
jobState.workspaceToken = null;
|
|
1579
|
+
if (jobState.attempt) {
|
|
1580
|
+
jobState.attempt = { ...jobState.attempt, token: null };
|
|
1581
|
+
}
|
|
1582
|
+
jobState.credentialsRemovedAt = new Date().toISOString();
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
async function scrubPreservedWorkspaceCredential(preserved) {
|
|
1586
|
+
const workspaceDir = path.resolve(String(preserved?.workspaceDir || ""));
|
|
1587
|
+
if (path.basename(workspaceDir) !== "workspace") return;
|
|
1588
|
+
const tempDir = path.dirname(workspaceDir);
|
|
1589
|
+
try {
|
|
1590
|
+
resolveJobTempDir({ tempDir });
|
|
1591
|
+
} catch {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const credentialState = {
|
|
1595
|
+
tempDir,
|
|
1596
|
+
authFile: path.join(tempDir, "job-auth.json"),
|
|
1597
|
+
};
|
|
1598
|
+
await scrubJobCredentials(credentialState);
|
|
1599
|
+
preserved.credentialsRemovedAt = credentialState.credentialsRemovedAt;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function resolveJobTempDir(jobState) {
|
|
1476
1603
|
const tempRoot = path.resolve(os.tmpdir());
|
|
1477
1604
|
const tempDir = path.resolve(String(jobState?.tempDir || ""));
|
|
1478
1605
|
if (
|
|
@@ -1482,7 +1609,7 @@ async function cleanupJobFiles(jobState) {
|
|
|
1482
1609
|
) {
|
|
1483
1610
|
throw new Error("Refused to remove an unrecognized Workshop directory.");
|
|
1484
1611
|
}
|
|
1485
|
-
|
|
1612
|
+
return tempDir;
|
|
1486
1613
|
}
|
|
1487
1614
|
|
|
1488
1615
|
export function detectSponsorAgentSession({
|
|
@@ -1808,6 +1935,7 @@ function sponsorJobUsage() {
|
|
|
1808
1935
|
"Usage:",
|
|
1809
1936
|
" lumine sponsor job status|pulse <job-id>",
|
|
1810
1937
|
" lumine sponsor job begin <job-id>",
|
|
1938
|
+
" lumine sponsor job update <job-id> --file <path> [--phase <name>]",
|
|
1811
1939
|
" lumine sponsor job relay-applied <job-id> <relay-id...>",
|
|
1812
1940
|
" lumine sponsor job helper-start <job-id> [--ordinal <n>]",
|
|
1813
1941
|
" 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>",
|