@stage5/lumine 0.2.40 → 0.2.41

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.
@@ -0,0 +1,493 @@
1
+ import { spawn } from "node:child_process";
2
+ import {
3
+ existsSync,
4
+ mkdtempSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ statSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+
14
+ import { requestJson } from "./http.js";
15
+ import { writeAdminJsonFile } from "./admin-news.js";
16
+
17
+ const DEFAULT_REVIEW_WAIT_MS = 10_000;
18
+ const MAX_REVIEW_WAIT_MS = 45_000;
19
+ const MAX_CAPTURED_LOG_LINES = 500;
20
+ const CDP_COMMAND_TIMEOUT_MS = 10_000;
21
+
22
+ function validationError(message, code = "CLI_ADMIN_CLI_VALIDATION") {
23
+ const error = new Error(message);
24
+ error.code = code;
25
+ return error;
26
+ }
27
+
28
+ export function parseBuildReviewReceipt(filePath) {
29
+ let receipt;
30
+ try {
31
+ receipt = JSON.parse(readFileSync(String(filePath || ""), "utf8"));
32
+ } catch {
33
+ throw validationError(`Could not read Build review receipt ${filePath}.`);
34
+ }
35
+ const buildId = Number(receipt?.buildId || 0);
36
+ const publishedArtifactVersionId = Number(
37
+ receipt?.publishedArtifactVersionId || 0,
38
+ );
39
+ const versionAfterReview = Number(receipt?.versionAfterReview || 0);
40
+ const screenshotPath = String(receipt?.screenshot?.path || "");
41
+ const screenshotBytes =
42
+ screenshotPath && existsSync(screenshotPath)
43
+ ? Number(statSync(screenshotPath).size || 0)
44
+ : 0;
45
+ if (
46
+ receipt?.schemaVersion !== 2 ||
47
+ receipt?.reviewMethod !== "runtime" ||
48
+ receipt?.status !== "confirmed" ||
49
+ !Number.isSafeInteger(buildId) ||
50
+ buildId <= 0 ||
51
+ !Number.isSafeInteger(publishedArtifactVersionId) ||
52
+ publishedArtifactVersionId <= 0 ||
53
+ versionAfterReview !== publishedArtifactVersionId ||
54
+ receipt?.versionStable !== true ||
55
+ receipt?.browser?.runtimeReadiness?.ready !== true ||
56
+ screenshotBytes <= 0 ||
57
+ screenshotBytes !== Number(receipt?.screenshot?.bytes || 0)
58
+ ) {
59
+ throw validationError(
60
+ "The Build review receipt is not a confirmed managed-runtime review.",
61
+ );
62
+ }
63
+ return receipt;
64
+ }
65
+
66
+ function findChromeExecutable(explicitPath = "") {
67
+ const candidates = [
68
+ explicitPath,
69
+ process.env.CHROME_PATH,
70
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
71
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
72
+ "/usr/bin/google-chrome",
73
+ "/usr/bin/google-chrome-stable",
74
+ "/usr/bin/chromium",
75
+ "/usr/bin/chromium-browser",
76
+ ].filter(Boolean);
77
+ const match = candidates.find((candidate) => existsSync(candidate));
78
+ if (!match) {
79
+ throw validationError(
80
+ "A Chrome/Chromium executable was not found. Pass --browser-path <path>.",
81
+ "CLI_ADMIN_BUILD_REVIEW_BROWSER_MISSING",
82
+ );
83
+ }
84
+ return match;
85
+ }
86
+
87
+ function boundedWaitMs(value) {
88
+ if (value === undefined || value === null || value === "") {
89
+ return DEFAULT_REVIEW_WAIT_MS;
90
+ }
91
+ const parsed = Number(value);
92
+ if (!Number.isInteger(parsed) || parsed < 1000 || parsed > MAX_REVIEW_WAIT_MS) {
93
+ throw validationError(
94
+ `--wait-ms must be an integer between 1000 and ${MAX_REVIEW_WAIT_MS}.`,
95
+ );
96
+ }
97
+ return parsed;
98
+ }
99
+
100
+ async function loadPublishedVersion({ apiUrl, buildId, timeoutMs }) {
101
+ const result = await requestJson({
102
+ url: `${String(apiUrl).replace(/\/$/, "")}/build/${buildId}/published-version`,
103
+ timeoutMs,
104
+ });
105
+ const versionId = Number(result?.publishedArtifactVersionId || 0);
106
+ if (!Number.isSafeInteger(versionId) || versionId <= 0) {
107
+ throw validationError(
108
+ `Build #${buildId} has no canonical published artifact to review.`,
109
+ "CLI_ADMIN_BUILD_REVIEW_NOT_PUBLISHED",
110
+ );
111
+ }
112
+ return versionId;
113
+ }
114
+
115
+ function delay(milliseconds) {
116
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
117
+ }
118
+
119
+ function createCdpPipe(child) {
120
+ const commandPipe = child.stdio[3];
121
+ const eventPipe = child.stdio[4];
122
+ if (!commandPipe || !eventPipe) {
123
+ throw validationError(
124
+ "Chrome did not expose its managed review channel.",
125
+ "CLI_ADMIN_BUILD_REVIEW_BROWSER_CHANNEL_FAILED",
126
+ );
127
+ }
128
+ let nextId = 1;
129
+ let buffer = Buffer.alloc(0);
130
+ const pending = new Map();
131
+ const listeners = new Set();
132
+ eventPipe.on("data", (chunk) => {
133
+ buffer = Buffer.concat([buffer, chunk]);
134
+ for (let delimiter = buffer.indexOf(0); delimiter >= 0; delimiter = buffer.indexOf(0)) {
135
+ const payload = buffer.subarray(0, delimiter).toString("utf8");
136
+ buffer = buffer.subarray(delimiter + 1);
137
+ if (!payload) continue;
138
+ let message;
139
+ try {
140
+ message = JSON.parse(payload);
141
+ } catch {
142
+ continue;
143
+ }
144
+ if (message.id && pending.has(message.id)) {
145
+ const entry = pending.get(message.id);
146
+ pending.delete(message.id);
147
+ clearTimeout(entry.timer);
148
+ if (message.error) {
149
+ entry.reject(new Error(message.error.message || "Chrome command failed."));
150
+ } else {
151
+ entry.resolve(message.result || {});
152
+ }
153
+ continue;
154
+ }
155
+ for (const listener of [...listeners]) listener(message);
156
+ }
157
+ });
158
+ const rejectPending = () => {
159
+ for (const entry of pending.values()) {
160
+ clearTimeout(entry.timer);
161
+ entry.reject(new Error("Chrome closed its managed review channel."));
162
+ }
163
+ pending.clear();
164
+ };
165
+ child.once("close", rejectPending);
166
+ child.once("error", rejectPending);
167
+ return {
168
+ onEvent(listener) {
169
+ listeners.add(listener);
170
+ return () => listeners.delete(listener);
171
+ },
172
+ send(method, params = {}, sessionId = "") {
173
+ const id = nextId++;
174
+ return new Promise((resolve, reject) => {
175
+ const timer = setTimeout(() => {
176
+ pending.delete(id);
177
+ reject(new Error(`Chrome timed out while running ${method}.`));
178
+ }, CDP_COMMAND_TIMEOUT_MS);
179
+ pending.set(id, { resolve, reject, timer });
180
+ const message = { id, method, params };
181
+ if (sessionId) message.sessionId = sessionId;
182
+ commandPipe.write(`${JSON.stringify(message)}\0`);
183
+ });
184
+ },
185
+ };
186
+ }
187
+
188
+ async function waitForPageTarget(cdp, expectedUrl) {
189
+ const deadline = Date.now() + CDP_COMMAND_TIMEOUT_MS;
190
+ while (Date.now() < deadline) {
191
+ const result = await cdp.send("Target.getTargets");
192
+ const targets = Array.isArray(result.targetInfos) ? result.targetInfos : [];
193
+ const exact = targets.find(
194
+ (target) => target.type === "page" && target.url === expectedUrl,
195
+ );
196
+ const fallback = targets.find(
197
+ (target) =>
198
+ target.type === "page" &&
199
+ /^https?:/i.test(String(target.url || "")) &&
200
+ !String(target.url).startsWith("chrome://"),
201
+ );
202
+ if (exact || fallback) return exact || fallback;
203
+ await delay(100);
204
+ }
205
+ throw new Error("Chrome did not open the requested Build page.");
206
+ }
207
+
208
+ const RUNTIME_READINESS_EXPRESSION = `(() => {
209
+ const visible = (element) => {
210
+ if (!element) return false;
211
+ const style = getComputedStyle(element);
212
+ const rect = element.getBoundingClientRect();
213
+ return style.display !== 'none' && style.visibility !== 'hidden' &&
214
+ Number(style.opacity || 1) > 0 && rect.width > 1 && rect.height > 1;
215
+ };
216
+ const preview = document.querySelector('iframe[title="App preview"]');
217
+ const progress = document.querySelector('[role="progressbar"]');
218
+ const runtimeError = document.querySelector('[data-agent-status="preview-error"]');
219
+ return {
220
+ documentReady: document.readyState === 'complete',
221
+ previewPresent: Boolean(preview),
222
+ previewVisible: visible(preview),
223
+ progressVisible: visible(progress),
224
+ runtimeErrorVisible: visible(runtimeError),
225
+ title: document.title || ''
226
+ };
227
+ })()`;
228
+
229
+ function isRuntimeReady(state) {
230
+ return Boolean(
231
+ state?.documentReady &&
232
+ state?.previewPresent &&
233
+ state?.previewVisible &&
234
+ !state?.progressVisible &&
235
+ !state?.runtimeErrorVisible,
236
+ );
237
+ }
238
+
239
+ function remoteArgumentText(argument) {
240
+ if (Object.hasOwn(argument || {}, "value")) {
241
+ const value = argument.value;
242
+ return typeof value === "string" ? value : JSON.stringify(value);
243
+ }
244
+ return String(argument?.description || argument?.type || "");
245
+ }
246
+
247
+ async function runChromeReview({ executable, url, screenshotPath, profileDir, waitMs }) {
248
+ const args = [
249
+ "--headless=new",
250
+ "--no-first-run",
251
+ "--no-default-browser-check",
252
+ "--disable-background-networking",
253
+ "--disable-component-update",
254
+ "--disable-sync",
255
+ "--hide-scrollbars",
256
+ "--enable-logging=stderr",
257
+ "--remote-debugging-pipe",
258
+ `--user-data-dir=${profileDir}`,
259
+ "--window-size=1440,1100",
260
+ url,
261
+ ];
262
+ const child = spawn(executable, args, {
263
+ stdio: ["ignore", "pipe", "pipe", "pipe", "pipe"],
264
+ });
265
+ let stdout = "";
266
+ let stderr = "";
267
+ const consoleLines = [];
268
+ const append = (current, chunk) => `${current}${chunk}`.slice(-2_000_000);
269
+ child.stdout.on("data", (chunk) => {
270
+ stdout = append(stdout, chunk);
271
+ });
272
+ child.stderr.on("data", (chunk) => {
273
+ stderr = append(stderr, chunk);
274
+ });
275
+ const closed = new Promise((resolve, reject) => {
276
+ child.once("error", reject);
277
+ child.once("close", (code, signal) => {
278
+ resolve({ code: Number(code ?? -1), signal: signal || null });
279
+ });
280
+ });
281
+ const timer = setTimeout(() => child.kill("SIGTERM"), Math.min(waitMs + 30_000, 58_000));
282
+ try {
283
+ const cdp = createCdpPipe(child);
284
+ cdp.onEvent((message) => {
285
+ if (message.method === "Runtime.consoleAPICalled") {
286
+ const values = Array.isArray(message.params?.args)
287
+ ? message.params.args.map(remoteArgumentText).filter(Boolean)
288
+ : [];
289
+ consoleLines.push(
290
+ `[console.${message.params?.type || "log"}] ${values.join(" ")}`,
291
+ );
292
+ } else if (message.method === "Runtime.exceptionThrown") {
293
+ consoleLines.push(
294
+ `[exception] ${message.params?.exceptionDetails?.exception?.description || message.params?.exceptionDetails?.text || "Uncaught exception"}`,
295
+ );
296
+ } else if (message.method === "Log.entryAdded") {
297
+ const entry = message.params?.entry;
298
+ consoleLines.push(
299
+ `[${entry?.level || "log"}] ${entry?.text || ""}${entry?.url ? ` (${entry.url})` : ""}`,
300
+ );
301
+ } else if (message.method === "Target.attachedToTarget") {
302
+ const childSessionId = String(message.params?.sessionId || "");
303
+ if (childSessionId) {
304
+ Promise.all([
305
+ cdp.send("Runtime.enable", {}, childSessionId),
306
+ cdp.send("Log.enable", {}, childSessionId),
307
+ ]).catch(() => {});
308
+ }
309
+ }
310
+ });
311
+ const target = await waitForPageTarget(cdp, url);
312
+ const attached = await cdp.send("Target.attachToTarget", {
313
+ targetId: target.targetId,
314
+ flatten: true,
315
+ });
316
+ const sessionId = String(attached.sessionId || "");
317
+ if (!sessionId) throw new Error("Chrome did not attach to the Build page.");
318
+ await Promise.all([
319
+ cdp.send("Page.enable", {}, sessionId),
320
+ cdp.send("Runtime.enable", {}, sessionId),
321
+ cdp.send("Log.enable", {}, sessionId),
322
+ ]);
323
+ await cdp.send(
324
+ "Target.setAutoAttach",
325
+ {
326
+ autoAttach: true,
327
+ waitForDebuggerOnStart: false,
328
+ flatten: true,
329
+ },
330
+ sessionId,
331
+ );
332
+ const deadline = Date.now() + waitMs;
333
+ let firstReadyAt = null;
334
+ let readiness = null;
335
+ do {
336
+ const evaluated = await cdp.send(
337
+ "Runtime.evaluate",
338
+ { expression: RUNTIME_READINESS_EXPRESSION, returnByValue: true },
339
+ sessionId,
340
+ );
341
+ readiness = evaluated.result?.value || null;
342
+ if (!firstReadyAt && isRuntimeReady(readiness)) {
343
+ firstReadyAt = new Date().toISOString();
344
+ }
345
+ const remaining = deadline - Date.now();
346
+ if (remaining > 0) await delay(Math.min(250, remaining));
347
+ } while (Date.now() < deadline);
348
+ const screenshot = await cdp.send(
349
+ "Page.captureScreenshot",
350
+ { format: "png", fromSurface: true, captureBeyondViewport: false },
351
+ sessionId,
352
+ );
353
+ if (screenshot.data) {
354
+ writeFileSync(screenshotPath, Buffer.from(screenshot.data, "base64"));
355
+ }
356
+ await cdp.send("Browser.close").catch(() => {});
357
+ const closeResult = await Promise.race([
358
+ closed,
359
+ delay(5_000).then(() => {
360
+ child.kill("SIGTERM");
361
+ return closed;
362
+ }),
363
+ ]);
364
+ return {
365
+ ...closeResult,
366
+ stdout,
367
+ stderr,
368
+ consoleLines: consoleLines.slice(-MAX_CAPTURED_LOG_LINES),
369
+ runtimeReadiness: {
370
+ ...(readiness || {}),
371
+ ready: isRuntimeReady(readiness),
372
+ firstReadyAt,
373
+ },
374
+ };
375
+ } finally {
376
+ clearTimeout(timer);
377
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
378
+ }
379
+ }
380
+
381
+ function captureConsoleEvidence(stderr, cdpLines = []) {
382
+ const chromeLines = String(stderr || "")
383
+ .split(/\r?\n/)
384
+ .filter((line) =>
385
+ /(?:INFO:CONSOLE|Uncaught|Failed to load resource)/i.test(line),
386
+ )
387
+ .slice(-MAX_CAPTURED_LOG_LINES);
388
+ return [...new Set([...cdpLines, ...chromeLines])].slice(
389
+ -MAX_CAPTURED_LOG_LINES,
390
+ );
391
+ }
392
+
393
+ export async function runManagedBuildReview({ options, authToken, buildId }) {
394
+ const waitMs = boundedWaitMs(options.adminWaitMs);
395
+ const executable = findChromeExecutable(options.adminBrowserPath);
396
+ let outputDir;
397
+ if (options.adminOutputDir) {
398
+ const outputRoot = path.resolve(options.adminOutputDir);
399
+ mkdirSync(outputRoot, { recursive: true });
400
+ outputDir = mkdtempSync(path.join(outputRoot, `build-${buildId}-review-`));
401
+ } else {
402
+ outputDir = mkdtempSync(
403
+ path.join(os.tmpdir(), `lumine-build-${buildId}-review-`),
404
+ );
405
+ }
406
+ const profileDir = mkdtempSync(path.join(os.tmpdir(), "lumine-chrome-profile-"));
407
+ const screenshotPath = path.join(outputDir, "runtime.png");
408
+ const receiptPath = path.join(outputDir, "review.json");
409
+ const appUrl = `${String(options.siteUrl).replace(/\/$/, "")}/app/${buildId}`;
410
+ const beforeVersion = await loadPublishedVersion({
411
+ apiUrl: options.apiUrl,
412
+ buildId,
413
+ timeoutMs: options.timeoutMs,
414
+ authToken,
415
+ });
416
+ const startedAt = new Date().toISOString();
417
+ let browser;
418
+ try {
419
+ browser = await runChromeReview({
420
+ executable,
421
+ url: appUrl,
422
+ screenshotPath,
423
+ profileDir,
424
+ waitMs,
425
+ });
426
+ } finally {
427
+ rmSync(profileDir, { recursive: true, force: true });
428
+ }
429
+ const afterVersion = await loadPublishedVersion({
430
+ apiUrl: options.apiUrl,
431
+ buildId,
432
+ timeoutMs: options.timeoutMs,
433
+ authToken,
434
+ });
435
+ const screenshotBytes = existsSync(screenshotPath)
436
+ ? Number(statSync(screenshotPath).size || 0)
437
+ : 0;
438
+ const confirmed =
439
+ browser.code === 0 &&
440
+ screenshotBytes > 0 &&
441
+ browser.runtimeReadiness?.ready === true &&
442
+ beforeVersion === afterVersion;
443
+ const receipt = {
444
+ schemaVersion: 2,
445
+ status: confirmed ? "confirmed" : "failed",
446
+ reviewMethod: "runtime",
447
+ buildId,
448
+ appUrl,
449
+ publishedArtifactVersionId: beforeVersion,
450
+ versionAfterReview: afterVersion,
451
+ versionStable: beforeVersion === afterVersion,
452
+ startedAt,
453
+ completedAt: new Date().toISOString(),
454
+ waitMs,
455
+ browser: {
456
+ executable,
457
+ exitCode: browser.code,
458
+ signal: browser.signal,
459
+ runtimeReadiness: browser.runtimeReadiness,
460
+ },
461
+ screenshot: {
462
+ path: screenshotPath,
463
+ bytes: screenshotBytes,
464
+ },
465
+ console: captureConsoleEvidence(browser.stderr, browser.consoleLines),
466
+ };
467
+ writeAdminJsonFile(receiptPath, receipt);
468
+ if (!confirmed) {
469
+ const error = validationError(
470
+ beforeVersion !== afterVersion
471
+ ? "The Build was republished during review; review the new artifact before commenting."
472
+ : browser.runtimeReadiness?.runtimeErrorVisible
473
+ ? "The isolated Build runtime exposed a runtime error instead of a reviewable app."
474
+ : "The isolated Build runtime did not become visibly reviewable before the observation window ended.",
475
+ "CLI_ADMIN_BUILD_REVIEW_FAILED",
476
+ );
477
+ error.data = {
478
+ ok: false,
479
+ status: "validation_error",
480
+ error: {
481
+ code: error.code,
482
+ message: error.message,
483
+ details: { receiptPath, screenshotPath },
484
+ },
485
+ };
486
+ throw error;
487
+ }
488
+ return {
489
+ ok: true,
490
+ status: "success",
491
+ data: { review: receipt, receiptPath, screenshotPath },
492
+ };
493
+ }
package/lib/commands.js CHANGED
@@ -64,6 +64,8 @@ import { sdkCommand } from "./sdk.js";
64
64
  import { doctorCommand, normalizePreviewUrl } from "./doctor.js";
65
65
  import { adminCommand } from "./admin.js";
66
66
  import { runBuildForumCommand } from "./forum.js";
67
+ import { agentCommand } from "./agent.js";
68
+ import { agentMcpCommand } from "./agent/mcp-server.js";
67
69
  import {
68
70
  defaultMainCheckoutDir,
69
71
  defaultReferenceDir,
@@ -103,6 +105,12 @@ import {
103
105
 
104
106
  export async function main() {
105
107
  const options = parseArgs(process.argv.slice(2));
108
+ // MCP stdio must remain protocol-clean: no update checks, help text, or
109
+ // other stdout can run before the server takes ownership of the stream.
110
+ if (options.command === "agent-mcp") {
111
+ await agentMcpCommand(options);
112
+ return;
113
+ }
106
114
  options.lumineCli = await loadLumineCliVersionInfo({ options });
107
115
  if (options.help) {
108
116
  printHelp();
@@ -117,6 +125,11 @@ export async function main() {
117
125
  return;
118
126
  }
119
127
 
128
+ if (options.command === "agent") {
129
+ await agentCommand(options, { saveWorkspace: save });
130
+ return;
131
+ }
132
+
120
133
  if (options.command === "workspace") {
121
134
  await workspace(options);
122
135
  return;
@@ -2209,6 +2222,11 @@ export function parseArgs(args) {
2209
2222
  "full",
2210
2223
  "unviewed",
2211
2224
  "viewed",
2225
+ "all",
2226
+ "resume",
2227
+ "sinceRun",
2228
+ "includeLegacy",
2229
+ "noReviewLoop",
2212
2230
  ]);
2213
2231
 
2214
2232
  for (let i = 0; i < rest.length; i += 1) {
@@ -2308,7 +2326,33 @@ export function parseArgs(args) {
2308
2326
  cursor: Math.max(0, Math.floor(Number(raw.cursor) || 0)),
2309
2327
  adminCursor: raw.cursor ? String(raw.cursor) : "",
2310
2328
  adminAfter: raw.after ? String(raw.after) : "",
2329
+ adminSinceRun: Boolean(raw.sinceRun),
2330
+ adminIncludeLegacy: Boolean(raw.includeLegacy),
2331
+ adminAll: Boolean(raw.all),
2332
+ adminResume: Boolean(raw.resume),
2333
+ adminCheckpoint: raw.checkpoint ? String(raw.checkpoint) : "",
2334
+ adminOutput: raw.output ? String(raw.output) : "",
2335
+ adminOutputDir: raw.outputDir ? String(raw.outputDir) : "",
2336
+ adminClaimFile: raw.claim ? String(raw.claim) : "",
2337
+ adminScaffoldFile: raw.scaffold ? String(raw.scaffold) : "",
2338
+ adminTargetFile: raw.targetFile ? String(raw.targetFile) : "",
2339
+ adminReviewReceipt: raw.reviewReceipt ? String(raw.reviewReceipt) : "",
2340
+ adminSeverity: raw.severity ? String(raw.severity) : "",
2341
+ adminWaitMs: raw.waitMs ? String(raw.waitMs) : "",
2342
+ adminBrowserPath: raw.browserPath ? String(raw.browserPath) : "",
2311
2343
  adminEffort: raw.effort ? String(raw.effort) : "",
2344
+ agentEffort:
2345
+ command === "agent" && raw.effort ? String(raw.effort) : "",
2346
+ provider: raw.provider ? String(raw.provider) : "",
2347
+ providerPath: raw.providerPath ? String(raw.providerPath) : "",
2348
+ agentPrompt:
2349
+ command === "agent"
2350
+ ? String(raw.prompt || positional.join(" ")).trim()
2351
+ : "",
2352
+ runtimeFile: raw.runtimeFile ? String(raw.runtimeFile) : "",
2353
+ traceFile: raw.traceFile ? String(raw.traceFile) : "",
2354
+ baseFilesHash: raw.baseFilesHash ? String(raw.baseFilesHash) : "",
2355
+ reviewLoop: parseBoolean(raw.noReviewLoop, false) ? false : true,
2312
2356
  adminLevel: raw.level ? String(raw.level) : "",
2313
2357
  adminIds: raw.subjectIds
2314
2358
  ? String(raw.subjectIds)
@@ -2620,6 +2664,7 @@ export function printHelp() {
2620
2664
  lumine login
2621
2665
  lumine whoami
2622
2666
  lumine logout
2667
+ lumine agent --provider <codex|claude-code> "<build request>"
2623
2668
  lumine new [title]
2624
2669
  lumine rename [title] [--target <twinkle-build-url-or-id>]
2625
2670
  lumine describe [description] [--target <twinkle-build-url-or-id>]
@@ -2667,23 +2712,26 @@ export function printHelp() {
2667
2712
  lumine admin ai-bucket accounts add --bucket-id <id> --user-ids <id,id,...> [--note <text>] [--json]
2668
2713
  lumine admin ai-bucket note set --bucket-id <id> --note <text> [--json]
2669
2714
  lumine admin daily-run start [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
2670
- lumine admin daily-run status|complete|fail [--reason <text>] [--json]
2671
- lumine admin recommendations list [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2672
- lumine admin builds candidates [--cursor <cursor>] [--limit <number>] [--json]
2673
- lumine admin subjects candidates [--after <date>] [--effort unassigned] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2715
+ lumine admin daily-run status|report|complete|fail [--reason <text>] [--json]
2716
+ lumine admin daily-run escalation add --target <target> --note <summary> [--severity attention|urgent] [--json]
2717
+ lumine admin recommendations list [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2718
+ lumine admin builds candidates [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--limit <number>] [--json]
2719
+ lumine admin builds review <build-url-or-id> [--output-dir <dir>] [--wait-ms <ms>] [--browser-path <path>] [--json]
2720
+ lumine admin subjects candidates [--after <date>] [--effort unassigned] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2674
2721
  lumine admin subject get|reveal <subject-url-or-id> [--json]
2675
- lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--cursor <cursor>] [--json]
2722
+ lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2676
2723
  lumine admin subject effort set <subject-id> --level <1|2|3> [--json]
2677
2724
  lumine admin subject creator set-made-by-poster <subject-id> [--json]
2678
2725
  lumine admin subject feature|unfeature <subject-id> [--json]
2679
2726
  lumine admin featured list [--unviewed|--viewed] [--json]
2680
2727
  lumine admin featured reorder --subject-ids <id,id,...> [--json]
2681
2728
  lumine admin post get <target> [--type subject|comment|aiStory|dailyReflection] [--json]
2682
- lumine admin post comments <target> [--type subject|aiStory|dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2729
+ lumine admin post comments <target> [--type subject|aiStory|dailyReflection] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2683
2730
  lumine admin post recommend <target> [--type subject|comment|aiStory|dailyReflection] [--anyone-can-reward] [--reward-twinkles 3] [--json]
2684
2731
  lumine admin post skip <target> [--type comment|aiStory|dailyReflection] [--reason <text>] [--json]
2732
+ lumine admin post skip-batch --target-file <json-or-lines> [--reason <text>] [--checkpoint <file> [--resume]] [--json]
2685
2733
  lumine admin post reward <target> [--type subject|comment|aiStory|dailyReflection] --twinkles 3 [--json]
2686
- lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
2734
+ lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
2687
2735
  lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
2688
2736
  lumine admin comment post --draft-id <id> [--json]
2689
2737
  lumine admin comment edit <comment-id> --file <comment.md> [--json]
@@ -2691,8 +2739,11 @@ export function printHelp() {
2691
2739
  lumine admin bot-output [--days <1..30>] [--json]
2692
2740
  lumine admin announcement post --file <announcement.md> [--json]
2693
2741
  lumine admin chat send <user-id|username> --file <message.md> [--json]
2742
+ lumine admin news claim [--date YYYY-MM-DD] [--output <claim.json>] [--scaffold <editorial.json>] [--json]
2743
+ lumine admin news validate --claim <claim.json> --file <editorial.json> [--json]
2744
+ lumine admin news submit --claim <claim.json> --file <editorial.json> [--model <name>] [--json]
2694
2745
  lumine admin notable add <user-id|username> --note <text> [--json]
2695
- lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--cursor <cursor>] [--json]
2746
+ lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2696
2747
 
2697
2748
  Examples:
2698
2749
  npx @stage5/lumine@latest
@@ -2723,6 +2774,8 @@ Examples:
2723
2774
  npx @stage5/lumine@latest restore 41 --main
2724
2775
  npx @stage5/lumine@latest update-from-main
2725
2776
  npx @stage5/lumine@latest pull
2777
+ npx @stage5/lumine@latest agent --provider codex "Add keyboard controls"
2778
+ npx @stage5/lumine@latest agent --provider claude-code "Fix the mobile layout"
2726
2779
  npx @stage5/lumine@latest save
2727
2780
  npx @stage5/lumine@latest save --publish
2728
2781
  npx @stage5/lumine@latest launch --save
@@ -2750,6 +2803,10 @@ Options:
2750
2803
  --auth-file <path> Saved login path
2751
2804
  --auth-token <token> Override saved login
2752
2805
  --dir <path> Directory for pulled project files
2806
+ --provider <agent> Subscription agent for lumine agent: codex or claude-code
2807
+ --provider-path <p> Override the selected agent CLI executable
2808
+ --effort <level> Optional provider reasoning effort
2809
+ --no-review-loop Skip the evidence-based post-run loop retrospective
2753
2810
  --target <build> Explicit Build URL or ID for rename/describe/upgrade
2754
2811
  --main With pull/versions/restore: target the team project's main
2755
2812
  --version <n> With pull: read-only checkout of previous save v<n>
@@ -2760,7 +2817,21 @@ Options:
2760
2817
  --note <text> Suggestion, notable-user, or AI-bucket context
2761
2818
  --cursor <id> Continue suggestions, Forum activity, or admin listing
2762
2819
  --poll-ms <ms> Forum listener interval (1000-60000; default 3000)
2763
- --after <date> Admin subjects: inclusive Unix/ISO creation boundary
2820
+ --after <date> Admin listing: inclusive Unix/ISO creation boundary
2821
+ --since-run Recommendations since the previous completed run (default)
2822
+ --include-legacy Explicitly include the full historical recommendation queue
2823
+ --all Automatically read every page in the canonical snapshot
2824
+ --checkpoint <file> Persist automatic pagination or batch progress
2825
+ --resume Resume the exact request recorded by --checkpoint
2826
+ --output <file> Save an admin result or newspaper claim as JSON
2827
+ --output-dir <dir> Directory for managed Build review evidence
2828
+ --claim <file> Newspaper claim JSON used for validation/submission
2829
+ --scaffold <file> Write an editable newspaper editorial scaffold
2830
+ --target-file <file> JSON array or newline list for audited batch skips
2831
+ --review-receipt <f> Confirmed managed Build runtime review receipt
2832
+ --severity <level> Run escalation severity: attention or urgent
2833
+ --wait-ms <ms> Managed Build runtime observation time (1000-45000)
2834
+ --browser-path <path> Chrome/Chromium executable for managed Build review
2764
2835
  --effort unassigned Admin subjects: show only unassigned effort
2765
2836
  --unviewed Admin content lists: retain unviewed and unknown items
2766
2837
  --viewed Admin content lists: retain viewed and unknown items
package/lib/constants.js CHANGED
@@ -343,6 +343,8 @@ export const MAIN_CHECKOUT_READONLY_COMMANDS = new Set([
343
343
  ]);
344
344
  export const COMMANDS = new Set([
345
345
  "admin",
346
+ "agent",
347
+ "agent-mcp",
346
348
  "workspace",
347
349
  "login",
348
350
  "logout",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.40",
3
+ "version": "0.2.41",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {