@stage5/lumine 0.2.40 → 0.2.42

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,12 @@ export function parseArgs(args) {
2209
2222
  "full",
2210
2223
  "unviewed",
2211
2224
  "viewed",
2225
+ "all",
2226
+ "resume",
2227
+ "sinceRun",
2228
+ "includeLegacy",
2229
+ "includePrivateEvidence",
2230
+ "noReviewLoop",
2212
2231
  ]);
2213
2232
 
2214
2233
  for (let i = 0; i < rest.length; i += 1) {
@@ -2308,7 +2327,35 @@ export function parseArgs(args) {
2308
2327
  cursor: Math.max(0, Math.floor(Number(raw.cursor) || 0)),
2309
2328
  adminCursor: raw.cursor ? String(raw.cursor) : "",
2310
2329
  adminAfter: raw.after ? String(raw.after) : "",
2330
+ adminSinceRun: Boolean(raw.sinceRun),
2331
+ adminIncludeLegacy: Boolean(raw.includeLegacy),
2332
+ adminIncludePrivateEvidence: parseBoolean(raw.includePrivateEvidence, false),
2333
+ adminAll: Boolean(raw.all),
2334
+ adminResume: Boolean(raw.resume),
2335
+ adminCheckpoint: raw.checkpoint ? String(raw.checkpoint) : "",
2336
+ adminOutput: raw.output ? String(raw.output) : "",
2337
+ adminOutputDir: raw.outputDir ? String(raw.outputDir) : "",
2338
+ adminClaimFile: raw.claim ? String(raw.claim) : "",
2339
+ adminScaffoldFile: raw.scaffold ? String(raw.scaffold) : "",
2340
+ adminTargetFile: raw.targetFile ? String(raw.targetFile) : "",
2341
+ adminReviewReceipt: raw.reviewReceipt ? String(raw.reviewReceipt) : "",
2342
+ adminSeverity: raw.severity ? String(raw.severity) : "",
2343
+ adminStatus: raw.status ? String(raw.status) : "",
2344
+ adminWaitMs: raw.waitMs ? String(raw.waitMs) : "",
2345
+ adminBrowserPath: raw.browserPath ? String(raw.browserPath) : "",
2311
2346
  adminEffort: raw.effort ? String(raw.effort) : "",
2347
+ agentEffort:
2348
+ command === "agent" && raw.effort ? String(raw.effort) : "",
2349
+ provider: raw.provider ? String(raw.provider) : "",
2350
+ providerPath: raw.providerPath ? String(raw.providerPath) : "",
2351
+ agentPrompt:
2352
+ command === "agent"
2353
+ ? String(raw.prompt || positional.join(" ")).trim()
2354
+ : "",
2355
+ runtimeFile: raw.runtimeFile ? String(raw.runtimeFile) : "",
2356
+ traceFile: raw.traceFile ? String(raw.traceFile) : "",
2357
+ baseFilesHash: raw.baseFilesHash ? String(raw.baseFilesHash) : "",
2358
+ reviewLoop: parseBoolean(raw.noReviewLoop, false) ? false : true,
2312
2359
  adminLevel: raw.level ? String(raw.level) : "",
2313
2360
  adminIds: raw.subjectIds
2314
2361
  ? String(raw.subjectIds)
@@ -2620,6 +2667,7 @@ export function printHelp() {
2620
2667
  lumine login
2621
2668
  lumine whoami
2622
2669
  lumine logout
2670
+ lumine agent --provider <codex|claude-code> "<build request>"
2623
2671
  lumine new [title]
2624
2672
  lumine rename [title] [--target <twinkle-build-url-or-id>]
2625
2673
  lumine describe [description] [--target <twinkle-build-url-or-id>]
@@ -2662,28 +2710,34 @@ export function printHelp() {
2662
2710
  lumine thumbnail generate ["<prompt>"] --model <gpt-image-2|nano-banana>
2663
2711
  lumine doctor runtime-assets
2664
2712
  lumine admin identity list|status|use <zero|ciel|auto> [--json]
2713
+ lumine admin identity inspect <user-id|username> --reason <management-reason> [--include-private-evidence] [--json]
2665
2714
  lumine admin ai-bucket create --label <name> --note <text> [--json]
2666
2715
  lumine admin ai-bucket get --bucket-id <id> [--json]
2667
2716
  lumine admin ai-bucket accounts add --bucket-id <id> --user-ids <id,id,...> [--note <text>] [--json]
2668
2717
  lumine admin ai-bucket note set --bucket-id <id> --note <text> [--json]
2669
2718
  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]
2719
+ lumine admin daily-run status|report|complete|fail [--reason <text>] [--json]
2720
+ lumine admin daily-run escalation add --target <target> --note <summary> [--severity attention|urgent] [--json]
2721
+ lumine admin escalation list [--status open|acknowledged|resolved|all] [--limit <number>] [--json]
2722
+ lumine admin escalation set <audit-id> --status open|acknowledged|resolved --note <decision> [--json]
2723
+ lumine admin recommendations list [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2724
+ lumine admin builds candidates [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--limit <number>] [--json]
2725
+ lumine admin builds review <build-url-or-id> [--output-dir <dir>] [--wait-ms <ms>] [--browser-path <path>] [--json]
2726
+ lumine admin subjects candidates [--after <date>] [--effort unassigned] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2674
2727
  lumine admin subject get|reveal <subject-url-or-id> [--json]
2675
- lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--cursor <cursor>] [--json]
2728
+ lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2676
2729
  lumine admin subject effort set <subject-id> --level <1|2|3> [--json]
2677
2730
  lumine admin subject creator set-made-by-poster <subject-id> [--json]
2678
2731
  lumine admin subject feature|unfeature <subject-id> [--json]
2679
2732
  lumine admin featured list [--unviewed|--viewed] [--json]
2680
2733
  lumine admin featured reorder --subject-ids <id,id,...> [--json]
2681
2734
  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]
2735
+ lumine admin post comments <target> [--type subject|aiStory|dailyReflection] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2683
2736
  lumine admin post recommend <target> [--type subject|comment|aiStory|dailyReflection] [--anyone-can-reward] [--reward-twinkles 3] [--json]
2684
2737
  lumine admin post skip <target> [--type comment|aiStory|dailyReflection] [--reason <text>] [--json]
2738
+ lumine admin post skip-batch --target-file <json-or-lines> [--reason <text>] [--checkpoint <file> [--resume]] [--json]
2685
2739
  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]
2740
+ 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
2741
  lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
2688
2742
  lumine admin comment post --draft-id <id> [--json]
2689
2743
  lumine admin comment edit <comment-id> --file <comment.md> [--json]
@@ -2691,8 +2745,11 @@ export function printHelp() {
2691
2745
  lumine admin bot-output [--days <1..30>] [--json]
2692
2746
  lumine admin announcement post --file <announcement.md> [--json]
2693
2747
  lumine admin chat send <user-id|username> --file <message.md> [--json]
2748
+ lumine admin news claim [--date YYYY-MM-DD] [--output <claim.json>] [--scaffold <editorial.json>] [--json]
2749
+ lumine admin news validate --claim <claim.json> --file <editorial.json> [--json]
2750
+ lumine admin news submit --claim <claim.json> --file <editorial.json> [--model <name>] [--json]
2694
2751
  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]
2752
+ lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2696
2753
 
2697
2754
  Examples:
2698
2755
  npx @stage5/lumine@latest
@@ -2723,6 +2780,8 @@ Examples:
2723
2780
  npx @stage5/lumine@latest restore 41 --main
2724
2781
  npx @stage5/lumine@latest update-from-main
2725
2782
  npx @stage5/lumine@latest pull
2783
+ npx @stage5/lumine@latest agent --provider codex "Add keyboard controls"
2784
+ npx @stage5/lumine@latest agent --provider claude-code "Fix the mobile layout"
2726
2785
  npx @stage5/lumine@latest save
2727
2786
  npx @stage5/lumine@latest save --publish
2728
2787
  npx @stage5/lumine@latest launch --save
@@ -2750,6 +2809,10 @@ Options:
2750
2809
  --auth-file <path> Saved login path
2751
2810
  --auth-token <token> Override saved login
2752
2811
  --dir <path> Directory for pulled project files
2812
+ --provider <agent> Subscription agent for lumine agent: codex or claude-code
2813
+ --provider-path <p> Override the selected agent CLI executable
2814
+ --effort <level> Optional provider reasoning effort
2815
+ --no-review-loop Skip the evidence-based post-run loop retrospective
2753
2816
  --target <build> Explicit Build URL or ID for rename/describe/upgrade
2754
2817
  --main With pull/versions/restore: target the team project's main
2755
2818
  --version <n> With pull: read-only checkout of previous save v<n>
@@ -2760,7 +2823,21 @@ Options:
2760
2823
  --note <text> Suggestion, notable-user, or AI-bucket context
2761
2824
  --cursor <id> Continue suggestions, Forum activity, or admin listing
2762
2825
  --poll-ms <ms> Forum listener interval (1000-60000; default 3000)
2763
- --after <date> Admin subjects: inclusive Unix/ISO creation boundary
2826
+ --after <date> Admin listing: inclusive Unix/ISO creation boundary
2827
+ --since-run Recommendations since the previous completed run (default)
2828
+ --include-legacy Explicitly include the full historical recommendation queue
2829
+ --all Automatically read every page in the canonical snapshot
2830
+ --checkpoint <file> Persist automatic pagination or batch progress
2831
+ --resume Resume the exact request recorded by --checkpoint
2832
+ --output <file> Save an admin result or newspaper claim as JSON
2833
+ --output-dir <dir> Directory for managed Build review evidence
2834
+ --claim <file> Newspaper claim JSON used for validation/submission
2835
+ --scaffold <file> Write an editable newspaper editorial scaffold
2836
+ --target-file <file> JSON array or newline list for audited batch skips
2837
+ --review-receipt <f> Confirmed managed Build runtime review receipt
2838
+ --severity <level> Run escalation severity: attention or urgent
2839
+ --wait-ms <ms> Managed Build runtime observation time (1000-45000)
2840
+ --browser-path <path> Chrome/Chromium executable for managed Build review
2764
2841
  --effort unassigned Admin subjects: show only unassigned effort
2765
2842
  --unviewed Admin content lists: retain unviewed and unknown items
2766
2843
  --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",