@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 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
- Run it between substantial work steps. `relay-applied` is an explicit receipt;
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>
@@ -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 details = [
1181
- relay.summary,
1182
- relay.requestedOutcome ? `What to deliver: ${relay.requestedOutcome}` : "",
1183
- relay.constraints?.length
1184
- ? `Constraints: ${relay.constraints.join("; ")}`
1185
- : "",
1186
- relay.acceptanceCriteria?.length
1187
- ? `Done means: ${relay.acceptanceCriteria.join("; ")}`
1188
- : "",
1189
- ].filter(Boolean);
1190
- return `## Relay #${relay.id}${unapplied.has(Number(relay.id)) ? " (not yet acknowledged as applied)" : " (applied receipt confirmed)"}\n\n${details.join("\n\n")}`;
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. 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.
1212
- 3. Run \`lumine sponsor job pulse ${jobState.job.id}\` between substantial work steps to keep the lease alive and receive approved follow-ups.
1213
- 4. After applying a relay, record its exact ID with \`lumine sponsor job relay-applied ${jobState.job.id} <relay-id...>\`.
1214
- 5. Finish with \`lumine sponsor job complete ${jobState.job.id} --summary "..."\`.
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
- await fs.rm(tempDir, { recursive: true, force: true });
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>",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.58",
3
+ "version": "0.2.60",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {