@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.
package/lib/admin.js CHANGED
@@ -1,12 +1,28 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { assertAuthScope, resolveAuth } from "./auth.js";
4
4
  import { requestJson } from "./http.js";
5
+ import {
6
+ extractNewsClaim,
7
+ readAdminJsonFile,
8
+ validateNewsEditorial,
9
+ writeNewsClaimArtifacts,
10
+ } from "./admin-news.js";
11
+ import {
12
+ runAutomaticPagination,
13
+ runBatchSkips,
14
+ } from "./admin-workflows.js";
15
+ import {
16
+ parseBuildReviewReceipt,
17
+ runManagedBuildReview,
18
+ } from "./build-review.js";
5
19
 
6
20
  const MAX_EDITORIAL_FILE_BYTES = 256 * 1024;
7
21
  const MAX_COMPOSED_TEXT_FILE_BYTES = 64 * 1024;
8
22
  const MAX_COMPOSED_TEXT_LENGTH = 10_000;
9
23
  const MAX_NOTABLE_NOTE_LENGTH = 2_000;
24
+ const MAX_IDENTITY_INSPECTION_REASON_LENGTH = 500;
25
+ const MAX_ESCALATION_DECISION_NOTE_LENGTH = 2_000;
10
26
 
11
27
  // Operator-composed persona text (plain UTF-8, not JSON). The agent writes
12
28
  // the content in the bot's persona itself; the server never invokes
@@ -64,6 +80,19 @@ function readEditorialFile(filePath) {
64
80
 
65
81
  export async function adminCommand(options) {
66
82
  const operation = parseAdminOperation(options);
83
+ if (operation.name === "news.validate") {
84
+ const validation = validateNewsEditorial({
85
+ claim: operation.claim,
86
+ editorial: operation.editorial,
87
+ });
88
+ const result = {
89
+ ok: true,
90
+ status: "success",
91
+ changed: false,
92
+ data: { validation },
93
+ };
94
+ return finishAdminOutput({ options, operation, result });
95
+ }
67
96
  const viewFilter = resolveOperatorViewFilter({
68
97
  operation,
69
98
  unviewed: options.adminUnviewed,
@@ -112,34 +141,76 @@ export async function adminCommand(options) {
112
141
  options.idempotencyKey || (operation.mutates ? `cli:${randomUUID()}` : "");
113
142
  let result;
114
143
  try {
115
- result = await requestJson({
116
- method: operation.method,
117
- url: `${options.apiUrl}${operation.path}`,
118
- authToken: auth.token,
119
- body: operation.body,
120
- headers: {
121
- ...(runId ? { "x-lumine-admin-run-id": String(runId) } : {}),
122
- ...(requestId ? { "x-lumine-idempotency-key": requestId } : {}),
123
- },
124
- timeoutMs: options.timeoutMs,
125
- });
126
- if (operation.name === "builds.candidates") {
127
- result = normalizeAdminBuildCandidatesResult({
128
- result,
129
- siteUrl: options.siteUrl,
144
+ const fetchOperation = async (requestPath = operation.path) =>
145
+ requestJson({
146
+ method: operation.method,
147
+ url: `${options.apiUrl}${requestPath}`,
148
+ authToken: auth.token,
149
+ body: operation.body,
150
+ headers: {
151
+ ...(runId ? { "x-lumine-admin-run-id": String(runId) } : {}),
152
+ ...(requestId ? { "x-lumine-idempotency-key": requestId } : {}),
153
+ },
154
+ timeoutMs: options.timeoutMs,
130
155
  });
131
- }
132
- if (recommendationContentTypes) {
133
- result = filterRecommendationQueueResult({
134
- result,
135
- contentTypes: recommendationContentTypes,
156
+ const transformResult = (rawResult) =>
157
+ transformAdminResult({
158
+ operation,
159
+ result: rawResult,
160
+ options,
161
+ recommendationContentTypes,
162
+ viewFilter,
136
163
  });
137
- }
138
- if (viewFilter) {
139
- result = filterListResultByOperatorView({ result, viewFilter });
164
+ if (operation.name === "build.review") {
165
+ result = await runManagedBuildReview({
166
+ options,
167
+ authToken: auth.token,
168
+ buildId: operation.buildId,
169
+ });
170
+ } else if (operation.name === "post.skip-batch") {
171
+ result = await runBatchSkips({
172
+ options,
173
+ authToken: auth.token,
174
+ runId,
175
+ parseTarget: parseRecommendationTarget,
176
+ });
177
+ } else if (options.adminAll) {
178
+ if (options.adminCursor) {
179
+ throw cliValidationError(
180
+ "Use --resume with the scan checkpoint instead of combining --all with --cursor.",
181
+ );
182
+ }
183
+ result = await runAutomaticPagination({
184
+ options,
185
+ operation,
186
+ runId,
187
+ fetchPage: fetchOperation,
188
+ transformPage: transformResult,
189
+ recordCoverage: async (coverage) =>
190
+ requestJson({
191
+ method: "POST",
192
+ url: `${options.apiUrl}/cli/admin/daily-runs/coverage`,
193
+ authToken: auth.token,
194
+ body: coverage,
195
+ headers: {
196
+ "x-lumine-admin-run-id": String(runId),
197
+ "x-lumine-idempotency-key": `cli:queue-coverage:${runId}:${adminValueFingerprint(coverage).slice(0, 32)}`,
198
+ },
199
+ timeoutMs: options.timeoutMs,
200
+ }),
201
+ });
202
+ } else {
203
+ if (options.adminResume) {
204
+ throw cliValidationError("--resume requires --all or post skip-batch.");
205
+ }
206
+ result = transformResult(await fetchOperation());
140
207
  }
141
208
  } catch (error) {
142
- if (operation.mutates && requestId) {
209
+ if (
210
+ operation.mutates &&
211
+ requestId &&
212
+ operation.name !== "post.skip-batch"
213
+ ) {
143
214
  const retryInstruction = `Retry with --idempotency-key ${requestId}.`;
144
215
  error.data = error.data || {
145
216
  ok: false,
@@ -175,6 +246,81 @@ export async function adminCommand(options) {
175
246
  expectedContent: operation.body.content,
176
247
  });
177
248
  }
249
+ if (operation.name === "news.claim") {
250
+ const artifacts = writeNewsClaimArtifacts({
251
+ result,
252
+ outputPath: options.adminOutput,
253
+ scaffoldPath: options.adminScaffoldFile,
254
+ });
255
+ result = {
256
+ ...result,
257
+ data: { ...(result.data || {}), artifacts },
258
+ };
259
+ }
260
+ return finishAdminOutput({ options, operation, result });
261
+ }
262
+
263
+ function transformAdminResult({
264
+ operation,
265
+ result,
266
+ options,
267
+ recommendationContentTypes,
268
+ viewFilter,
269
+ }) {
270
+ let transformed = result;
271
+ if (operation.name === "recommendations.list") {
272
+ assertRecommendationWindowResult({ operation, result: transformed });
273
+ }
274
+ if (operation.name === "builds.candidates") {
275
+ transformed = normalizeAdminBuildCandidatesResult({
276
+ result: transformed,
277
+ siteUrl: options.siteUrl,
278
+ });
279
+ }
280
+ if (recommendationContentTypes) {
281
+ transformed = filterRecommendationQueueResult({
282
+ result: transformed,
283
+ contentTypes: recommendationContentTypes,
284
+ });
285
+ }
286
+ if (viewFilter) {
287
+ transformed = filterListResultByOperatorView({
288
+ result: transformed,
289
+ viewFilter,
290
+ });
291
+ }
292
+ return transformed;
293
+ }
294
+
295
+ export function assertRecommendationWindowResult({ operation, result }) {
296
+ const mode = operation?.pagination?.coverageMode;
297
+ if (!mode || mode === "legacy") return;
298
+ const after = result?.data?.pagination?.after;
299
+ const snapshotTimeStamp = result?.data?.pagination?.snapshotTimeStamp;
300
+ if (
301
+ !Number.isSafeInteger(after) ||
302
+ after < 0 ||
303
+ !Number.isSafeInteger(snapshotTimeStamp) ||
304
+ snapshotTimeStamp < 0
305
+ ) {
306
+ const error = new Error(
307
+ "The deployed API did not confirm the bounded recommendation window and snapshot. Deploy the matching API before using this CLI; use --include-legacy only for an intentional historical scan.",
308
+ );
309
+ error.code = "LUMINE_ADMIN_RECOMMENDATION_WINDOW_UNSUPPORTED";
310
+ error.data = {
311
+ ok: false,
312
+ status: "validation_error",
313
+ error: {
314
+ code: error.code,
315
+ message: error.message,
316
+ details: { requestedMode: mode },
317
+ },
318
+ };
319
+ throw error;
320
+ }
321
+ }
322
+
323
+ function finishAdminOutput({ options, operation, result }) {
178
324
  if (options.json) {
179
325
  console.log(JSON.stringify(result));
180
326
  return result;
@@ -354,8 +500,12 @@ function adminOperationRequiresRun(operation) {
354
500
  "identity.list",
355
501
  "identity.status",
356
502
  "identity.use",
503
+ "identity.inspect",
357
504
  "daily-run.start",
358
505
  "daily-run.status",
506
+ "escalation.list",
507
+ "escalation.set",
508
+ "notable.add",
359
509
  ].includes(operation.name) && !operation.name.startsWith("ai-bucket.")
360
510
  );
361
511
  }
@@ -396,6 +546,35 @@ export function parseAdminOperation(options) {
396
546
  { identity: parseIdentity(target) },
397
547
  );
398
548
  }
549
+ if (action === "inspect") {
550
+ const inspectionTarget = String(target || "").trim();
551
+ const reason = String(options.adminReason || "").trim();
552
+ if (!inspectionTarget) {
553
+ throw cliValidationError(
554
+ "Usage: lumine admin identity inspect <userId|username> --reason <management reason> [--include-private-evidence].",
555
+ );
556
+ }
557
+ if (!reason) {
558
+ throw cliValidationError(
559
+ "Explain why private identity evidence is needed with --reason <management reason>.",
560
+ );
561
+ }
562
+ if (reason.length > MAX_IDENTITY_INSPECTION_REASON_LENGTH) {
563
+ throw cliValidationError(
564
+ `An identity-inspection reason must be at most ${MAX_IDENTITY_INSPECTION_REASON_LENGTH} characters.`,
565
+ );
566
+ }
567
+ return writeOperation(
568
+ "identity.inspect",
569
+ "POST",
570
+ "/cli/admin/identity/inspect",
571
+ {
572
+ target: inspectionTarget,
573
+ reason,
574
+ includePrivateEvidence: options.adminIncludePrivateEvidence === true,
575
+ },
576
+ );
577
+ }
399
578
  }
400
579
 
401
580
  if (namespace === "ai-bucket" || namespace === "ai-buckets") {
@@ -460,6 +639,31 @@ export function parseAdminOperation(options) {
460
639
  if (action === "status") {
461
640
  return readOperation("daily-run.status", "/cli/admin/daily-runs/status");
462
641
  }
642
+ if (action === "report") {
643
+ return readOperation("daily-run.report", "/cli/admin/daily-runs/report");
644
+ }
645
+ if (action === "escalation" && target === "add") {
646
+ const rawTarget = String(options.adminTarget || "").trim();
647
+ const summary = String(options.note || "").trim();
648
+ if (!rawTarget || !summary) {
649
+ throw cliValidationError(
650
+ "Pass a concrete --target and --note when recording a run escalation.",
651
+ );
652
+ }
653
+ const parsedTarget = parseAdminEscalationTarget(rawTarget);
654
+ return writeOperation(
655
+ "daily-run.escalation.add",
656
+ "POST",
657
+ "/cli/admin/daily-runs/escalations",
658
+ {
659
+ targetType: parsedTarget.targetType || undefined,
660
+ targetId: parsedTarget.targetId || undefined,
661
+ url: parsedTarget.url || undefined,
662
+ summary,
663
+ severity: options.adminSeverity || "attention",
664
+ },
665
+ );
666
+ }
463
667
  if (action === "complete" || action === "fail") {
464
668
  return writeOperation(
465
669
  `daily-run.${action}`,
@@ -470,6 +674,47 @@ export function parseAdminOperation(options) {
470
674
  }
471
675
  }
472
676
 
677
+ if (namespace === "escalation" || namespace === "escalations") {
678
+ if (!action || action === "list") {
679
+ const status = parseEscalationListStatus(options.adminStatus || "open");
680
+ return readOperation(
681
+ "escalation.list",
682
+ withQuery("/cli/admin/escalations", {
683
+ status,
684
+ limit: options.limit,
685
+ }),
686
+ );
687
+ }
688
+ if (action === "set") {
689
+ const escalationAuditId = parseRequiredInteger(
690
+ target,
691
+ "Escalation audit ID",
692
+ 1,
693
+ );
694
+ const status = parseEscalationStatus(options.adminStatus);
695
+ const note = String(options.note || "").trim();
696
+ if (!note) {
697
+ throw cliValidationError(
698
+ "Record the decision or next step with --note <text>.",
699
+ );
700
+ }
701
+ if (note.length > MAX_ESCALATION_DECISION_NOTE_LENGTH) {
702
+ throw cliValidationError(
703
+ `An escalation decision note must be at most ${MAX_ESCALATION_DECISION_NOTE_LENGTH} characters.`,
704
+ );
705
+ }
706
+ return writeOperation(
707
+ "escalation.set",
708
+ "PUT",
709
+ `/cli/admin/escalations/${escalationAuditId}`,
710
+ { status, note },
711
+ );
712
+ }
713
+ throw cliValidationError(
714
+ "Usage: lumine admin escalation list [--status open|acknowledged|resolved|all] | escalation set <auditId> --status <status> --note <decision>.",
715
+ );
716
+ }
717
+
473
718
  if (
474
719
  (namespace === "recommend-queue" && (!action || action === "list")) ||
475
720
  (namespace === "recommendations" && action === "list")
@@ -478,6 +723,7 @@ export function parseAdminOperation(options) {
478
723
  if (kind !== "recommend") {
479
724
  throw cliValidationError("--kind currently supports only recommend.");
480
725
  }
726
+ const recommendationWindow = parseRecommendationWindow(options);
481
727
  return readOperation(
482
728
  "recommendations.list",
483
729
  withQuery("/cli/admin/recommendations", {
@@ -485,7 +731,33 @@ export function parseAdminOperation(options) {
485
731
  contentTypes: options.adminContentTypes,
486
732
  cursor: options.adminCursor,
487
733
  limit: options.limit,
734
+ sinceRun: recommendationWindow.mode === "since-run" ? "true" : "",
735
+ after:
736
+ recommendationWindow.mode === "after"
737
+ ? recommendationWindow.after
738
+ : "",
739
+ includeLegacy:
740
+ recommendationWindow.mode === "legacy" ? "true" : "",
488
741
  }),
742
+ {
743
+ pagination: {
744
+ collectionKey: "items",
745
+ coverageQueue: "recommendations",
746
+ coverageMode: recommendationWindow.mode,
747
+ after:
748
+ recommendationWindow.mode === "after"
749
+ ? parseAfterForCoverage(recommendationWindow.after)
750
+ : null,
751
+ filters: {
752
+ contentTypes: options.adminContentTypes || null,
753
+ operatorView: options.adminUnviewed
754
+ ? "unviewed"
755
+ : options.adminViewed
756
+ ? "viewed"
757
+ : null,
758
+ },
759
+ },
760
+ },
489
761
  );
490
762
  }
491
763
 
@@ -498,8 +770,33 @@ export function parseAdminOperation(options) {
498
770
  cursor: options.adminCursor,
499
771
  limit: options.limit,
500
772
  }),
773
+ {
774
+ pagination: {
775
+ collectionKey: "builds",
776
+ coverageQueue: "builds",
777
+ coverageMode: "all",
778
+ after: null,
779
+ filters: { sort: "recent", scope: "all" },
780
+ },
781
+ },
501
782
  );
502
783
  }
784
+ if (namespace === "builds" && action === "review") {
785
+ const parsed = parseAdminCommentTarget({ target, explicitType: "build" });
786
+ if (parsed.type !== "build") {
787
+ throw cliValidationError(
788
+ "Build review targets a Build URL or build:<id>.",
789
+ );
790
+ }
791
+ return {
792
+ name: "build.review",
793
+ method: "GET",
794
+ path: "",
795
+ body: undefined,
796
+ mutates: false,
797
+ buildId: parsed.id,
798
+ };
799
+ }
503
800
 
504
801
  if (
505
802
  namespace === "subjects" &&
@@ -524,6 +821,7 @@ export function parseAdminOperation(options) {
524
821
  cursor: options.adminCursor,
525
822
  limit: options.limit,
526
823
  }),
824
+ { pagination: { collectionKey: "comments" } },
527
825
  );
528
826
  }
529
827
  if (action === "reveal") {
@@ -591,6 +889,7 @@ export function parseAdminOperation(options) {
591
889
  cursor: options.adminCursor,
592
890
  limit: options.limit,
593
891
  }),
892
+ { pagination: { collectionKey: "comments" } },
594
893
  );
595
894
  }
596
895
  if (action === "recommend") {
@@ -613,6 +912,20 @@ export function parseAdminOperation(options) {
613
912
  { reason: options.adminReason || undefined },
614
913
  );
615
914
  }
915
+ if (action === "skip-batch") {
916
+ if (!options.adminTargetFile) {
917
+ throw cliValidationError(
918
+ "Pass batch skip targets with --target-file <file>.",
919
+ );
920
+ }
921
+ return {
922
+ name: "post.skip-batch",
923
+ method: "POST",
924
+ path: "",
925
+ body: undefined,
926
+ mutates: true,
927
+ };
928
+ }
616
929
  if (action === "reward") {
617
930
  const parsedTarget = parseRecommendationTarget({
618
931
  target,
@@ -653,22 +966,41 @@ export function parseAdminOperation(options) {
653
966
  ...(repairDate ? { date: repairDate } : {}),
654
967
  });
655
968
  }
656
- if (action === "submit") {
657
- const editionId = parseRequiredInteger(
658
- options.adminEditionId,
659
- "--edition-id",
660
- 1,
969
+ if (action === "validate") {
970
+ const claim = extractNewsClaim(
971
+ readAdminJsonFile(options.adminClaimFile, "--claim <claim.json>"),
661
972
  );
662
- const leaseToken = String(options.adminLeaseToken || "").trim();
973
+ return {
974
+ name: "news.validate",
975
+ local: true,
976
+ mutates: false,
977
+ claim,
978
+ editorial: readEditorialFile(options.adminFile),
979
+ };
980
+ }
981
+ if (action === "submit") {
982
+ const claim = options.adminClaimFile
983
+ ? extractNewsClaim(
984
+ readAdminJsonFile(options.adminClaimFile, "--claim <claim.json>"),
985
+ )
986
+ : null;
987
+ const editionId = claim
988
+ ? claim.editionId
989
+ : parseRequiredInteger(options.adminEditionId, "--edition-id", 1);
990
+ const leaseToken = claim
991
+ ? claim.leaseToken
992
+ : String(options.adminLeaseToken || "").trim();
663
993
  if (!leaseToken) {
664
994
  throw cliValidationError(
665
995
  "Pass the claim's lease token with --lease-token <token>.",
666
996
  );
667
997
  }
998
+ const editorial = readEditorialFile(options.adminFile);
999
+ if (claim) validateNewsEditorial({ claim, editorial });
668
1000
  return writeOperation("news.submit", "POST", "/cli/admin/news/submit", {
669
1001
  editionId,
670
1002
  leaseToken,
671
- editorial: readEditorialFile(options.adminFile),
1003
+ editorial,
672
1004
  model: options.model || undefined,
673
1005
  });
674
1006
  }
@@ -776,6 +1108,7 @@ export function parseAdminOperation(options) {
776
1108
  limit: options.limit,
777
1109
  full: options.adminFull ? "true" : "",
778
1110
  }),
1111
+ { pagination: { collectionKey: "events" } },
779
1112
  );
780
1113
  }
781
1114
 
@@ -800,20 +1133,46 @@ export function parseAdminOperation(options) {
800
1133
  const buildReviewMethod = options.adminBuildReviewMethod
801
1134
  ? parseAdminBuildReviewMethod(options.adminBuildReviewMethod)
802
1135
  : undefined;
1136
+ const reviewReceipt = options.adminReviewReceipt
1137
+ ? parseBuildReviewReceipt(options.adminReviewReceipt)
1138
+ : null;
1139
+ if (
1140
+ reviewReceipt &&
1141
+ (reviewedBuildVersionId || buildReviewMethod)
1142
+ ) {
1143
+ throw cliValidationError(
1144
+ "Pass either --review-receipt or manual --reviewed-version/--reviewed-via evidence, not both.",
1145
+ );
1146
+ }
1147
+ const confirmedBuildVersionId = reviewReceipt
1148
+ ? Number(reviewReceipt.publishedArtifactVersionId)
1149
+ : reviewedBuildVersionId;
1150
+ const confirmedBuildReviewMethod = reviewReceipt
1151
+ ? "runtime"
1152
+ : buildReviewMethod;
1153
+ if (
1154
+ reviewReceipt &&
1155
+ parsedTarget.type === "build" &&
1156
+ Number(reviewReceipt.buildId) !== parsedTarget.id
1157
+ ) {
1158
+ throw cliValidationError(
1159
+ "The review receipt belongs to a different Build.",
1160
+ );
1161
+ }
803
1162
  if (parsedTarget.type === "build") {
804
1163
  if (!options.adminFile) {
805
1164
  throw cliValidationError(
806
1165
  "Build comments are management-agent composed only; pass --file <comment.md> after reviewing the project.",
807
1166
  );
808
1167
  }
809
- if (!reviewedBuildVersionId || !buildReviewMethod) {
1168
+ if (!confirmedBuildVersionId || !confirmedBuildReviewMethod) {
810
1169
  throw cliValidationError(
811
1170
  "After reviewing the project, pass both --reviewed-version <published-artifact-id> and --reviewed-via runtime|code.",
812
1171
  );
813
1172
  }
814
1173
  } else if (
815
1174
  parsedTarget.type !== "comment" &&
816
- (reviewedBuildVersionId || buildReviewMethod)
1175
+ (confirmedBuildVersionId || confirmedBuildReviewMethod)
817
1176
  ) {
818
1177
  throw cliValidationError(
819
1178
  "Build review evidence applies only to build:<id> or a comment:<id> inside a Build.",
@@ -832,8 +1191,12 @@ export function parseAdminOperation(options) {
832
1191
  ...(options.adminFile
833
1192
  ? { content: readComposedTextFile(options.adminFile) }
834
1193
  : {}),
835
- ...(reviewedBuildVersionId ? { reviewedBuildVersionId } : {}),
836
- ...(buildReviewMethod ? { buildReviewMethod } : {}),
1194
+ ...(confirmedBuildVersionId
1195
+ ? { reviewedBuildVersionId: confirmedBuildVersionId }
1196
+ : {}),
1197
+ ...(confirmedBuildReviewMethod
1198
+ ? { buildReviewMethod: confirmedBuildReviewMethod }
1199
+ : {}),
837
1200
  },
838
1201
  );
839
1202
  }
@@ -877,7 +1240,7 @@ export function parseAdminOperation(options) {
877
1240
  }
878
1241
 
879
1242
  throw cliValidationError(
880
- "Usage: lumine admin identity|daily-run|recommendations|builds|post|subjects|subject|featured|comment|announcement|chat|news|audit|brief|bot-output|notable ...",
1243
+ "Usage: lumine admin identity|daily-run|escalation|recommendations|builds|post|subjects|subject|featured|comment|announcement|chat|news|audit|brief|bot-output|notable ...",
881
1244
  );
882
1245
  }
883
1246
 
@@ -890,6 +1253,17 @@ function subjectsListOperation(options) {
890
1253
  effort: options.adminEffort,
891
1254
  limit: options.limit,
892
1255
  }),
1256
+ {
1257
+ pagination: {
1258
+ collectionKey: "subjects",
1259
+ coverageQueue: "subjects",
1260
+ coverageMode: options.adminAfter ? "after" : "all",
1261
+ after: options.adminAfter
1262
+ ? parseAfterForCoverage(options.adminAfter)
1263
+ : null,
1264
+ filters: { effort: options.adminEffort || "all" },
1265
+ },
1266
+ },
893
1267
  );
894
1268
  }
895
1269
 
@@ -997,6 +1371,35 @@ function recommendOperation(target, options) {
997
1371
  );
998
1372
  }
999
1373
 
1374
+ export function parseRecommendationWindow(options) {
1375
+ const selected = [
1376
+ options.adminSinceRun ? "since-run" : "",
1377
+ options.adminAfter ? "after" : "",
1378
+ options.adminIncludeLegacy ? "legacy" : "",
1379
+ ].filter(Boolean);
1380
+ if (selected.length > 1) {
1381
+ throw cliValidationError(
1382
+ "Choose one recommendation window: --since-run, --after, or --include-legacy.",
1383
+ );
1384
+ }
1385
+ const mode = selected[0] || "since-run";
1386
+ if (mode === "after") parseAfterForCoverage(options.adminAfter);
1387
+ return { mode, after: mode === "after" ? options.adminAfter : "" };
1388
+ }
1389
+
1390
+ function parseAfterForCoverage(value) {
1391
+ const raw = String(value || "").trim();
1392
+ if (/^\d+$/.test(raw)) {
1393
+ const parsed = Number(raw);
1394
+ if (Number.isSafeInteger(parsed) && parsed >= 0) return parsed;
1395
+ }
1396
+ const parsedMs = Date.parse(raw);
1397
+ if (Number.isFinite(parsedMs)) return Math.floor(parsedMs / 1000);
1398
+ throw cliValidationError(
1399
+ "--after must be a Unix timestamp or ISO-8601 date.",
1400
+ );
1401
+ }
1402
+
1000
1403
  export function parseSubjectId(value) {
1001
1404
  const normalized = String(value || "").trim();
1002
1405
  const match = normalized.match(/(?:^subject:|\/subjects\/)(\d+)(?:[/?#]|$)/i);
@@ -1090,6 +1493,42 @@ export function parseAdminCommentTarget({ target, explicitType = "" }) {
1090
1493
  return baseTarget;
1091
1494
  }
1092
1495
 
1496
+ export function parseAdminEscalationTarget(value) {
1497
+ const normalized = String(value || "").trim();
1498
+ const genericTarget = normalized.match(
1499
+ /^([a-zA-Z][a-zA-Z0-9_-]{0,39}):(\d+)$/,
1500
+ );
1501
+ if (genericTarget) {
1502
+ return {
1503
+ targetType: genericTarget[1],
1504
+ targetId: parseRequiredInteger(genericTarget[2], "escalation target ID", 1),
1505
+ url: null,
1506
+ };
1507
+ }
1508
+ if (/^https:\/\//i.test(normalized)) {
1509
+ let parsedUrl;
1510
+ try {
1511
+ parsedUrl = new URL(normalized);
1512
+ } catch {
1513
+ throw cliValidationError("The escalation target URL is invalid.");
1514
+ }
1515
+ if (parsedUrl.protocol !== "https:") {
1516
+ throw cliValidationError("The escalation target URL must use HTTPS.");
1517
+ }
1518
+ if (
1519
+ /\/(?:subjects|comments|ai-stories|daily-reflections|app|build)\/\d+(?:[/?#]|$)/i.test(
1520
+ normalized,
1521
+ )
1522
+ ) {
1523
+ const parsed = parseAdminCommentTarget({ target: normalized });
1524
+ return { targetType: parsed.type, targetId: parsed.id, url: normalized };
1525
+ }
1526
+ return { targetType: null, targetId: null, url: normalized };
1527
+ }
1528
+ const parsed = parseAdminCommentTarget({ target: normalized });
1529
+ return { targetType: parsed.type, targetId: parsed.id, url: null };
1530
+ }
1531
+
1093
1532
  function parseAdminBuildReviewMethod(value) {
1094
1533
  const normalized = String(value || "")
1095
1534
  .trim()
@@ -1134,8 +1573,15 @@ export function formatAdminJsonError(error) {
1134
1573
  };
1135
1574
  }
1136
1575
 
1137
- function readOperation(name, path) {
1138
- return { name, method: "GET", path, body: undefined, mutates: false };
1576
+ function readOperation(name, path, extra = {}) {
1577
+ return {
1578
+ name,
1579
+ method: "GET",
1580
+ path,
1581
+ body: undefined,
1582
+ mutates: false,
1583
+ ...extra,
1584
+ };
1139
1585
  }
1140
1586
 
1141
1587
  function writeOperation(name, method, path, body) {
@@ -1173,6 +1619,30 @@ function parseCommentMode(value) {
1173
1619
  return mode;
1174
1620
  }
1175
1621
 
1622
+ function parseEscalationStatus(value) {
1623
+ const status = String(value || "")
1624
+ .trim()
1625
+ .toLowerCase();
1626
+ if (!["open", "acknowledged", "resolved"].includes(status)) {
1627
+ throw cliValidationError(
1628
+ "--status must be open, acknowledged, or resolved.",
1629
+ );
1630
+ }
1631
+ return status;
1632
+ }
1633
+
1634
+ function parseEscalationListStatus(value) {
1635
+ const status = String(value || "open")
1636
+ .trim()
1637
+ .toLowerCase();
1638
+ if (!["open", "acknowledged", "resolved", "all"].includes(status)) {
1639
+ throw cliValidationError(
1640
+ "--status must be open, acknowledged, resolved, or all.",
1641
+ );
1642
+ }
1643
+ return status;
1644
+ }
1645
+
1176
1646
  function parseOrderedIds(value) {
1177
1647
  const ids = String(value || "")
1178
1648
  .split(",")
@@ -1276,8 +1746,121 @@ function cliValidationError(message) {
1276
1746
  return error;
1277
1747
  }
1278
1748
 
1749
+ function adminValueFingerprint(value) {
1750
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
1751
+ }
1752
+
1279
1753
  function printAdminResult({ operation, result }) {
1280
1754
  const data = result?.data || {};
1755
+ if (data.review) {
1756
+ console.log(
1757
+ `Confirmed Build #${data.review.buildId} runtime at published artifact #${data.review.publishedArtifactVersionId}.`,
1758
+ );
1759
+ console.log(`Screenshot: ${data.screenshotPath}`);
1760
+ console.log(`Review receipt: ${data.receiptPath}`);
1761
+ return;
1762
+ }
1763
+ if (data.validation) {
1764
+ console.log(
1765
+ `Editorial valid for edition #${data.validation.editionId}: ${data.validation.citedEventCount} cited and ${data.validation.coveredEventCount} covered event(s).`,
1766
+ );
1767
+ return;
1768
+ }
1769
+ if (data.batch) {
1770
+ console.log(
1771
+ `Skipped ${data.batch.completedCount} audited target(s); ${data.batch.changedCount} changed canonical state.`,
1772
+ );
1773
+ console.log(`Checkpoint: ${data.batch.checkpointPath}`);
1774
+ return;
1775
+ }
1776
+ if (data.report) {
1777
+ const report = data.report;
1778
+ console.log(
1779
+ `Run #${report.run.id}: ${report.mutations.successfulMutationCount} successful mutation(s), ${report.queueCoverage.length} queue coverage record(s), ${report.escalations.length} escalation(s).`,
1780
+ );
1781
+ for (const coverage of report.queueCoverage) {
1782
+ console.log(
1783
+ ` ${coverage.queue}: ${coverage.candidateCount} candidate(s), ${coverage.scannedCount} row(s) scanned across ${coverage.pages} page(s).`,
1784
+ );
1785
+ }
1786
+ for (const escalation of report.escalations) {
1787
+ const target =
1788
+ escalation.url ||
1789
+ `${escalation.targetType || "target"}:${escalation.targetId || "?"}`;
1790
+ console.log(
1791
+ ` ${String(escalation.severity || "attention").toUpperCase()} ${target} — ${escalation.summary}`,
1792
+ );
1793
+ }
1794
+ const surfaces = report.brief?.engagementPulse?.surfaces;
1795
+ if (surfaces && typeof surfaces === "object") {
1796
+ const deltas = Object.entries(surfaces)
1797
+ .filter(([, value]) => Number(value?.delta || 0) !== 0)
1798
+ .sort(
1799
+ ([, left], [, right]) =>
1800
+ Math.abs(Number(right?.delta || 0)) -
1801
+ Math.abs(Number(left?.delta || 0)),
1802
+ )
1803
+ .slice(0, 5)
1804
+ .map(
1805
+ ([name, value]) =>
1806
+ `${name} ${Number(value.delta) > 0 ? "+" : ""}${Number(value.delta)}`,
1807
+ );
1808
+ if (deltas.length) console.log(`Engagement deltas: ${deltas.join(", ")}.`);
1809
+ }
1810
+ const notableCount = Array.isArray(report.brief?.notableCandidates)
1811
+ ? report.brief.notableCandidates.length
1812
+ : 0;
1813
+ console.log(`Notable-user candidates in this brief: ${notableCount}.`);
1814
+ return;
1815
+ }
1816
+ if (data.inspection) {
1817
+ const inspection = data.inspection;
1818
+ console.log(
1819
+ `Identity inspection for user #${inspection.targetUserId}: ${inspection.accounts?.length || 0} candidate account(s); oldest #${inspection.oldestAccount?.userId || "unknown"}.`,
1820
+ );
1821
+ if (inspection.manualBucket) {
1822
+ console.log(
1823
+ `AI bucket #${inspection.manualBucket.id} (${inspection.manualBucket.label}); ${inspection.manualBucket.memberCount} canonical member(s).`,
1824
+ );
1825
+ }
1826
+ for (const account of inspection.accounts || []) {
1827
+ console.log(
1828
+ ` #${account.userId} ${account.username || "(no username)"} — joined ${account.joinedAt || "unknown"}; ${account.relationBasis.join(", ") || "no relation evidence"}${account.hasDateOfBirth ? "; DOB on file" : "; no DOB on file"}.`,
1829
+ );
1830
+ if (account.privateEvidence) {
1831
+ console.log(
1832
+ ` Private evidence: DOB ${account.privateEvidence.dateOfBirth || "none"}; verified email(s) ${account.privateEvidence.verifiedEmails.join(", ") || "none"}.`,
1833
+ );
1834
+ }
1835
+ }
1836
+ return;
1837
+ }
1838
+ if (Array.isArray(data.escalations)) {
1839
+ console.log(`${data.escalations.length} escalation(s):`);
1840
+ for (const escalation of data.escalations) {
1841
+ const target =
1842
+ escalation.url ||
1843
+ `${escalation.targetType || "target"}:${escalation.targetId || "?"}`;
1844
+ console.log(
1845
+ ` #${escalation.auditId} ${String(escalation.status || "open").toUpperCase()} ${target} — ${escalation.summary}`,
1846
+ );
1847
+ if (escalation.decisionNote) {
1848
+ console.log(` Decision: ${escalation.decisionNote}`);
1849
+ }
1850
+ }
1851
+ if (data.truncated) {
1852
+ console.log(
1853
+ "More matching escalations exist than the requested limit; raise --limit or narrow --status.",
1854
+ );
1855
+ }
1856
+ return;
1857
+ }
1858
+ if (data.escalation?.decisionNote) {
1859
+ console.log(
1860
+ `Escalation #${data.escalation.auditId}: ${String(data.escalation.status).toUpperCase()} — ${data.escalation.decisionNote}`,
1861
+ );
1862
+ return;
1863
+ }
1281
1864
  if (data.bucket && Array.isArray(data.memberUserIds)) {
1282
1865
  const added = Array.isArray(data.accounts)
1283
1866
  ? `; added ${data.accounts.length} explicit account(s)`
@@ -1347,12 +1930,28 @@ function printAdminResult({ operation, result }) {
1347
1930
  return;
1348
1931
  }
1349
1932
  if (data.claim) {
1350
- console.log(
1351
- `Claimed edition #${data.claim.editionId} (${data.claim.dateKey}): ${data.claim.events.length} event(s); lease token ${data.claim.leaseToken}.`,
1352
- );
1353
- console.log(
1354
- `Write the editorial JSON, then run: lumine admin news submit --edition-id ${data.claim.editionId} --lease-token ${data.claim.leaseToken} --file editorial.json`,
1355
- );
1933
+ if (data.artifacts?.claimFile) {
1934
+ console.log(
1935
+ `Claimed edition #${data.claim.editionId} (${data.claim.dateKey}): ${data.claim.events.length} event(s).`,
1936
+ );
1937
+ console.log(`Claim file: ${data.artifacts.claimFile}`);
1938
+ if (data.artifacts?.scaffoldFile) {
1939
+ console.log(`Editorial scaffold: ${data.artifacts.scaffoldFile}`);
1940
+ }
1941
+ console.log(
1942
+ `Validate before submission: lumine admin news validate --claim ${data.artifacts.claimFile} --file ${data.artifacts.scaffoldFile || "editorial.json"}`,
1943
+ );
1944
+ console.log(
1945
+ `Submit the confirmed pair: lumine admin news submit --claim ${data.artifacts.claimFile} --file ${data.artifacts.scaffoldFile || "editorial.json"}`,
1946
+ );
1947
+ } else {
1948
+ console.log(
1949
+ `Claimed edition #${data.claim.editionId} (${data.claim.dateKey}): ${data.claim.events.length} event(s); lease token ${data.claim.leaseToken}.`,
1950
+ );
1951
+ console.log(
1952
+ `Write the editorial JSON, then run: lumine admin news submit --edition-id ${data.claim.editionId} --lease-token ${data.claim.leaseToken} --file editorial.json`,
1953
+ );
1954
+ }
1356
1955
  return;
1357
1956
  }
1358
1957
  if (data.newspaper) {