@stage5/lumine 0.2.32 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -169,6 +169,7 @@ lumine admin daily-run start --identity auto --comment-mode off --json
169
169
  lumine admin recommendations list --cursor '<cursor>' --json
170
170
  lumine admin subjects candidates --after 2026-08-01T00:00:00Z --json
171
171
  lumine admin subjects candidates --effort unassigned --json
172
+ lumine admin builds candidates --cursor '<cursor>' --json
172
173
  lumine admin subject get 123 --include-comments --json
173
174
  lumine admin post get https://www.twin-kle.com/ai-stories/88 --json
174
175
  lumine admin post comments dailyReflection:99 --json
@@ -184,6 +185,9 @@ lumine admin brief --days 3 --json
184
185
  lumine admin notable add Minecrarft_guy --note "Created 8 thoughtful subjects and helped peers in 23 comments this window." --json
185
186
  lumine admin post recommend comment:456 --anyone-can-reward --reward-twinkles 3 --json
186
187
  lumine admin post reward comment:456 --twinkles 3 --json
188
+ lumine admin comment draft build:884 --file comment.md \
189
+ --reviewed-version 4512 --reviewed-via runtime --json
190
+ lumine admin comment post --draft-id 77 --json
187
191
  lumine admin daily-run complete --json
188
192
  ```
189
193
 
@@ -195,6 +199,13 @@ and posts use the selected bot's server-owned canonical persona; a later human
195
199
  reply is handled by Twinkle's existing autonomous Zero/Ciel responder without
196
200
  Lumine remaining active.
197
201
 
202
+ Management agents also inspect recent public Build candidates during each run.
203
+ A direct Build comment is never server-generated: first open and try the
204
+ published app or pull and read its code, then compose with `--file` and bind
205
+ the draft to the exact `publishedArtifactVersionId` using
206
+ `--reviewed-version` and `--reviewed-via runtime|code`. The server rejects
207
+ missing or stale review evidence and any app/thread change before publication.
208
+
198
209
  Every operation is noninteractive when its required arguments are present.
199
210
  `--json` prints exactly one uncolored JSON value and returns a nonzero status
200
211
  for forbidden, not-found, validation, and partial-failure responses. Pass a
package/lib/admin.js CHANGED
@@ -125,6 +125,12 @@ export async function adminCommand(options) {
125
125
  },
126
126
  timeoutMs: options.timeoutMs,
127
127
  });
128
+ if (operation.name === "builds.candidates") {
129
+ result = normalizeAdminBuildCandidatesResult({
130
+ result,
131
+ siteUrl: options.siteUrl,
132
+ });
133
+ }
128
134
  if (recommendationContentTypes) {
129
135
  result = filterRecommendationQueueResult({
130
136
  result,
@@ -179,6 +185,38 @@ export async function adminCommand(options) {
179
185
  return result;
180
186
  }
181
187
 
188
+ export function normalizeAdminBuildCandidatesResult({ result, siteUrl }) {
189
+ const builds = Array.isArray(result?.builds) ? result.builds : [];
190
+ const nextCursor = String(result?.cursor || "").trim() || null;
191
+ return {
192
+ ok: true,
193
+ status: "success",
194
+ data: {
195
+ builds: builds.map((build) => {
196
+ const id = Number(build?.id || 0);
197
+ return {
198
+ ...build,
199
+ url:
200
+ id > 0
201
+ ? `${String(siteUrl || "").replace(/\/$/, "")}/app/${id}`
202
+ : null,
203
+ review: {
204
+ publishedArtifactVersionId:
205
+ Number(build?.publishedArtifactVersionId || 0) || null,
206
+ codePullAvailable: build?.collaborationMode === "open_source",
207
+ requiredBeforeComment: true,
208
+ },
209
+ };
210
+ }),
211
+ pagination: {
212
+ nextCursor,
213
+ hasMore: Boolean(nextCursor),
214
+ exhausted: !nextCursor,
215
+ },
216
+ },
217
+ };
218
+ }
219
+
182
220
  export function assertComposedCommentDraftResult({ result, expectedContent }) {
183
221
  const draft = result?.data?.draft;
184
222
  if (
@@ -313,13 +351,15 @@ export function resolveOperatorViewFilter({ operation, unviewed, viewed }) {
313
351
  }
314
352
 
315
353
  function adminOperationRequiresRun(operation) {
316
- return ![
317
- "identity.list",
318
- "identity.status",
319
- "identity.use",
320
- "daily-run.start",
321
- "daily-run.status",
322
- ].includes(operation.name);
354
+ return (
355
+ ![
356
+ "identity.list",
357
+ "identity.status",
358
+ "identity.use",
359
+ "daily-run.start",
360
+ "daily-run.status",
361
+ ].includes(operation.name) && !operation.name.startsWith("ai-bucket.")
362
+ );
323
363
  }
324
364
 
325
365
  function noActiveRunError() {
@@ -360,6 +400,50 @@ export function parseAdminOperation(options) {
360
400
  }
361
401
  }
362
402
 
403
+ if (namespace === "ai-bucket" || namespace === "ai-buckets") {
404
+ const bucketId = parseRequiredInteger(
405
+ options.adminBucketId,
406
+ "AI bucket ID",
407
+ 1,
408
+ );
409
+ if (action === "get" || action === "status") {
410
+ return readOperation(
411
+ "ai-bucket.get",
412
+ `/cli/admin/ai-buckets/${bucketId}`,
413
+ );
414
+ }
415
+ if (action === "accounts" && target === "add") {
416
+ return writeOperation(
417
+ "ai-bucket.accounts.add",
418
+ "POST",
419
+ `/cli/admin/ai-buckets/${bucketId}/accounts`,
420
+ {
421
+ userIds: parseAiBucketUserIds(options.adminUserIds),
422
+ note: options.note || undefined,
423
+ },
424
+ );
425
+ }
426
+ if (action === "note" && target === "set") {
427
+ const note = String(options.note || "").trim();
428
+ if (!note) {
429
+ throw cliValidationError(
430
+ "Pass the quota-only context with --note <text>.",
431
+ );
432
+ }
433
+ if (note.length > 255) {
434
+ throw cliValidationError(
435
+ "An AI bucket note can be at most 255 characters.",
436
+ );
437
+ }
438
+ return writeOperation(
439
+ "ai-bucket.note.set",
440
+ "PUT",
441
+ `/cli/admin/ai-buckets/${bucketId}/note`,
442
+ { note },
443
+ );
444
+ }
445
+ }
446
+
363
447
  if (namespace === "daily-run") {
364
448
  if (action === "start") {
365
449
  return writeOperation(
@@ -407,6 +491,18 @@ export function parseAdminOperation(options) {
407
491
  );
408
492
  }
409
493
 
494
+ if (namespace === "builds" && action === "candidates") {
495
+ return readOperation(
496
+ "builds.candidates",
497
+ withQuery("/build/public/list", {
498
+ sort: "recent",
499
+ scope: "all",
500
+ cursor: options.adminCursor,
501
+ limit: options.limit,
502
+ }),
503
+ );
504
+ }
505
+
410
506
  if (
411
507
  namespace === "subjects" &&
412
508
  (action === "list" || action === "candidates")
@@ -644,7 +740,7 @@ export function parseAdminOperation(options) {
644
740
  }
645
741
 
646
742
  if (namespace === "bot-output" && !action) {
647
- if (options.adminDays !== undefined) {
743
+ if (options.adminDays) {
648
744
  const days = Number(options.adminDays);
649
745
  if (!Number.isInteger(days) || days < 1 || days > 30) {
650
746
  throw cliValidationError("--days must be an integer between 1 and 30.");
@@ -676,7 +772,7 @@ export function parseAdminOperation(options) {
676
772
 
677
773
  if (namespace === "comment") {
678
774
  if (action === "draft" || action === "reply") {
679
- const parsedTarget = parseRecommendationTarget({
775
+ const parsedTarget = parseAdminCommentTarget({
680
776
  target,
681
777
  explicitType: options.adminType,
682
778
  });
@@ -685,6 +781,35 @@ export function parseAdminOperation(options) {
685
781
  "comment reply targets a comment: lumine admin comment reply comment:<id>.",
686
782
  );
687
783
  }
784
+ const reviewedBuildVersionId = options.adminReviewedBuildVersion
785
+ ? parseRequiredInteger(
786
+ options.adminReviewedBuildVersion,
787
+ "--reviewed-version",
788
+ 1,
789
+ )
790
+ : undefined;
791
+ const buildReviewMethod = options.adminBuildReviewMethod
792
+ ? parseAdminBuildReviewMethod(options.adminBuildReviewMethod)
793
+ : undefined;
794
+ if (parsedTarget.type === "build") {
795
+ if (!options.adminFile) {
796
+ throw cliValidationError(
797
+ "Build comments are management-agent composed only; pass --file <comment.md> after reviewing the project.",
798
+ );
799
+ }
800
+ if (!reviewedBuildVersionId || !buildReviewMethod) {
801
+ throw cliValidationError(
802
+ "After reviewing the project, pass both --reviewed-version <published-artifact-id> and --reviewed-via runtime|code.",
803
+ );
804
+ }
805
+ } else if (
806
+ parsedTarget.type !== "comment" &&
807
+ (reviewedBuildVersionId || buildReviewMethod)
808
+ ) {
809
+ throw cliValidationError(
810
+ "Build review evidence applies only to build:<id> or a comment:<id> inside a Build.",
811
+ );
812
+ }
688
813
  return writeOperation(
689
814
  "comment.draft",
690
815
  "POST",
@@ -698,6 +823,8 @@ export function parseAdminOperation(options) {
698
823
  ...(options.adminFile
699
824
  ? { content: readComposedCommentFile(options.adminFile) }
700
825
  : {}),
826
+ ...(reviewedBuildVersionId ? { reviewedBuildVersionId } : {}),
827
+ ...(buildReviewMethod ? { buildReviewMethod } : {}),
701
828
  },
702
829
  );
703
830
  }
@@ -741,7 +868,7 @@ export function parseAdminOperation(options) {
741
868
  }
742
869
 
743
870
  throw cliValidationError(
744
- "Usage: lumine admin identity|daily-run|recommendations|post|subjects|subject|featured|comment|chat|news|audit|brief|bot-output|notable ...",
871
+ "Usage: lumine admin identity|daily-run|recommendations|builds|post|subjects|subject|featured|comment|chat|news|audit|brief|bot-output|notable ...",
745
872
  );
746
873
  }
747
874
 
@@ -920,6 +1047,50 @@ export function parseRecommendationTarget({ target, explicitType = "" }) {
920
1047
  return { type, id };
921
1048
  }
922
1049
 
1050
+ export function parseAdminCommentTarget({ target, explicitType = "" }) {
1051
+ const normalized = String(target || "").trim();
1052
+ const prefixed = normalized.match(
1053
+ /^(subject|comment|build|aistory|dailyreflection):(\d+)$/i,
1054
+ );
1055
+ const buildUrl = normalized.match(/\/(?:app|build)\/(\d+)(?:[/?#]|$)/i);
1056
+ const baseTarget = parseRecommendationTarget({
1057
+ target:
1058
+ prefixed?.[1]?.toLowerCase() === "build" || buildUrl
1059
+ ? `subject:${prefixed?.[2] || buildUrl?.[1]}`
1060
+ : normalized,
1061
+ explicitType:
1062
+ String(explicitType || "").toLowerCase() === "build"
1063
+ ? "subject"
1064
+ : explicitType,
1065
+ });
1066
+ const inferredBuild = prefixed?.[1]?.toLowerCase() === "build" || !!buildUrl;
1067
+ const explicitBuild = String(explicitType || "").toLowerCase() === "build";
1068
+ if (explicitType && !explicitBuild && inferredBuild) {
1069
+ throw cliValidationError(
1070
+ "The target and --type identify different content types.",
1071
+ );
1072
+ }
1073
+ if (explicitBuild && !inferredBuild && !/^\d+$/.test(normalized)) {
1074
+ throw cliValidationError(
1075
+ "The target and --type identify different content types.",
1076
+ );
1077
+ }
1078
+ if (inferredBuild || explicitBuild) {
1079
+ return { type: "build", id: baseTarget.id };
1080
+ }
1081
+ return baseTarget;
1082
+ }
1083
+
1084
+ function parseAdminBuildReviewMethod(value) {
1085
+ const normalized = String(value || "")
1086
+ .trim()
1087
+ .toLowerCase();
1088
+ if (normalized !== "runtime" && normalized !== "code") {
1089
+ throw cliValidationError("--reviewed-via must be runtime or code.");
1090
+ }
1091
+ return normalized;
1092
+ }
1093
+
923
1094
  function normalizeRecommendationTargetType(value) {
924
1095
  const normalized = String(value || "")
925
1096
  .trim()
@@ -1010,6 +1181,29 @@ function parseOrderedIds(value) {
1010
1181
  return ids;
1011
1182
  }
1012
1183
 
1184
+ function parseAiBucketUserIds(value) {
1185
+ const raw = String(value || "").trim();
1186
+ if (!raw) {
1187
+ throw cliValidationError(
1188
+ "Pass explicit accounts with --user-ids <id,id,...>.",
1189
+ );
1190
+ }
1191
+ const ids = raw
1192
+ .split(",")
1193
+ .map((part) => part.trim())
1194
+ .filter(Boolean)
1195
+ .map((part) => parseRequiredInteger(part, "AI bucket user ID", 1));
1196
+ if (ids.length > 500) {
1197
+ throw cliValidationError(
1198
+ "An AI bucket batch can contain at most 500 users.",
1199
+ );
1200
+ }
1201
+ if (new Set(ids).size !== ids.length) {
1202
+ throw cliValidationError("AI bucket user IDs must be unique.");
1203
+ }
1204
+ return ids;
1205
+ }
1206
+
1013
1207
  function parseRequiredInteger(
1014
1208
  value,
1015
1209
  label,
@@ -1049,6 +1243,15 @@ function cliValidationError(message) {
1049
1243
 
1050
1244
  function printAdminResult({ operation, result }) {
1051
1245
  const data = result?.data || {};
1246
+ if (data.bucket && Array.isArray(data.memberUserIds)) {
1247
+ const added = Array.isArray(data.accounts)
1248
+ ? `; added ${data.accounts.length} explicit account(s)`
1249
+ : "";
1250
+ console.log(
1251
+ `AI bucket #${data.bucket.id} (${data.bucket.label}): ${data.memberCount} canonical member account(s)${added}.`,
1252
+ );
1253
+ return;
1254
+ }
1052
1255
  if (data.run !== undefined) {
1053
1256
  if (!data.run) {
1054
1257
  console.log("No active delegated administrator daily run.");
package/lib/commands.js CHANGED
@@ -2172,6 +2172,8 @@ export function parseArgs(args) {
2172
2172
  : raw.ids
2173
2173
  ? String(raw.ids)
2174
2174
  : "",
2175
+ adminBucketId: raw.bucketId ? String(raw.bucketId) : "",
2176
+ adminUserIds: raw.userIds ? String(raw.userIds) : "",
2175
2177
  adminType: raw.type ? String(raw.type) : "",
2176
2178
  adminKind: raw.kind ? String(raw.kind) : "",
2177
2179
  adminContentTypes: raw.contentTypes ? String(raw.contentTypes) : "",
@@ -2192,6 +2194,10 @@ export function parseArgs(args) {
2192
2194
  adminEditionId: raw.editionId ? String(raw.editionId) : "",
2193
2195
  adminLeaseToken: raw.leaseToken ? String(raw.leaseToken) : "",
2194
2196
  adminFile: raw.file ? String(raw.file) : "",
2197
+ adminReviewedBuildVersion: raw.reviewedVersion
2198
+ ? String(raw.reviewedVersion)
2199
+ : "",
2200
+ adminBuildReviewMethod: raw.reviewedVia ? String(raw.reviewedVia) : "",
2195
2201
  adminFull: parseBoolean(raw.full, false),
2196
2202
  rewardTwinkles: raw.rewardTwinkles ? String(raw.rewardTwinkles) : "",
2197
2203
  includeComments: parseBoolean(raw.includeComments, false),
@@ -2507,9 +2513,13 @@ export function printHelp() {
2507
2513
  lumine thumbnail generate ["<prompt>"] --model <gpt-image-2|nano-banana>
2508
2514
  lumine doctor runtime-assets
2509
2515
  lumine admin identity list|status|use <zero|ciel|auto> [--json]
2516
+ lumine admin ai-bucket get --bucket-id <id> [--json]
2517
+ lumine admin ai-bucket accounts add --bucket-id <id> --user-ids <id,id,...> [--note <text>] [--json]
2518
+ lumine admin ai-bucket note set --bucket-id <id> --note <text> [--json]
2510
2519
  lumine admin daily-run start [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
2511
2520
  lumine admin daily-run status|complete|fail [--reason <text>] [--json]
2512
2521
  lumine admin recommendations list [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2522
+ lumine admin builds candidates [--cursor <cursor>] [--limit <number>] [--json]
2513
2523
  lumine admin subjects candidates [--after <date>] [--effort unassigned] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2514
2524
  lumine admin subject get|reveal <subject-url-or-id> [--json]
2515
2525
  lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--cursor <cursor>] [--json]
@@ -2523,8 +2533,8 @@ export function printHelp() {
2523
2533
  lumine admin post recommend <target> [--type subject|comment|aiStory|dailyReflection] [--anyone-can-reward] [--reward-twinkles 3] [--json]
2524
2534
  lumine admin post skip <target> [--type comment|aiStory|dailyReflection] [--reason <text>] [--json]
2525
2535
  lumine admin post reward <target> [--type subject|comment|aiStory|dailyReflection] --twinkles 3 [--json]
2526
- lumine admin comment draft <target> [--type subject|comment|aiStory|dailyReflection] [--identity zero|ciel|auto] [--json]
2527
- lumine admin comment reply comment:<id> [--identity zero|ciel|auto] [--json]
2536
+ 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]
2537
+ lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
2528
2538
  lumine admin comment post --draft-id <id> [--json]
2529
2539
  lumine admin comment edit <comment-id> --file <comment.md> [--json]
2530
2540
  lumine admin brief [--days <1..30>] [--json]
@@ -2593,7 +2603,7 @@ Options:
2593
2603
  --description <text> Build description for new/describe
2594
2604
  --no-description Skip New description or clear with describe
2595
2605
  --summary <text> Save summary
2596
- --note <text> Branch-suggestion message or notable-user rationale
2606
+ --note <text> Suggestion, notable-user, or AI-bucket context
2597
2607
  --cursor <id> Continue an owner suggestion inbox listing
2598
2608
  --after <date> Admin subjects: inclusive Unix/ISO creation boundary
2599
2609
  --effort unassigned Admin subjects: show only unassigned effort
@@ -2605,13 +2615,17 @@ Options:
2605
2615
  --idempotency-key <k> Stable retry key for one admin mutation
2606
2616
  --level <1|2|3> Admin subject effort level
2607
2617
  --subject-ids <ids> Complete ordered Featured subject IDs
2608
- --type <type> Admin target: subject, comment, aiStory, or dailyReflection
2618
+ --bucket-id <id> Unbanned AI identity bucket for account consolidation
2619
+ --user-ids <ids> Explicit user IDs for an AI bucket batch (up to 500)
2620
+ --type <type> Admin target: subject, comment, build, aiStory, or dailyReflection
2609
2621
  --kind recommend Admin recommendation queue kind
2610
2622
  --anyone-can-reward Enable canonical reward eligibility
2611
2623
  --reward-twinkles 3 Pair a recommendation with exactly 3 Twinkles
2612
2624
  --twinkles 3 Give exactly 3 Twinkles through the normal economy
2613
2625
  --draft-id <id> Canonical delegated comment draft ID
2614
2626
  --file <path> Editorial JSON or composed comment text file
2627
+ --reviewed-version <id> Published Build artifact version actually reviewed
2628
+ --reviewed-via <method> Build review method: runtime or code
2615
2629
  --reason <text> Reason when marking an admin run failed
2616
2630
  --force Overwrite server files even if this workspace is stale or missing filesHash
2617
2631
  --search <text> Search public open-source Builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.32",
3
+ "version": "0.2.34",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,6 +49,36 @@ Comment mode is stored only on the current run:
49
49
  - `draft`: server-generated drafts, no public comment.
50
50
  - `post`: drafts plus idempotent publication through the ordinary comment path.
51
51
 
52
+ ### Private AI-bucket maintenance
53
+
54
+ AI identity buckets are private operator bookkeeping, not a Zero/Ciel public
55
+ action. They therefore do not require or attach to a delegated daily run:
56
+
57
+ ```bash
58
+ lumine admin ai-bucket get --bucket-id 10 --json
59
+ lumine admin ai-bucket accounts add --bucket-id 10 \
60
+ --user-ids 3127,13037,15410,16288 \
61
+ --note "operator-confirmed account family" --json
62
+ lumine admin ai-bucket note set --bucket-id 10 \
63
+ --note "Quota accounting only; not a moderation flag." --json
64
+ ```
65
+
66
+ `accounts add` accepts 1-500 unique positive user IDs, preflights the complete
67
+ batch before writing, adds the canonical user and durable verified-email rules,
68
+ re-attributes current-day AI usage, and returns the canonical bucket members.
69
+ It is idempotent to retry. The API records the real operator in the private
70
+ Lumine audit log; no public bot identity is involved.
71
+
72
+ `note set` records up to 255 characters of private operational context on the
73
+ canonical bucket. Use it to distinguish quota bookkeeping from moderation;
74
+ the note itself changes no access, ban, or identity rules.
75
+
76
+ This surface intentionally accepts only an existing **unbanned** bucket. It
77
+ cannot ban accounts, block signup, add IP/device/risk-key rules, or infer an
78
+ account family. Identification remains a human/LLM evidence judgment and must
79
+ be explicitly requested by Mikey; routine administrator runs still escalate
80
+ suspected alternate accounts and never auto-enforce.
81
+
52
82
  ## Editorial priorities
53
83
 
54
84
  The CLI enforces none of this — it is the standing instruction for the operator
@@ -460,6 +490,7 @@ lumine admin subjects candidates --after 2026-08-01T00:00:00Z \
460
490
  --cursor '<cursor>' --json
461
491
  lumine admin subjects candidates --effort unassigned --json
462
492
  lumine admin subjects candidates --unviewed --json
493
+ lumine admin builds candidates --cursor '<cursor>' --limit 50 --json
463
494
  ```
464
495
 
465
496
  Schemas:
@@ -505,6 +536,29 @@ type SubjectCandidates = Success<{
505
536
  subjects: Subject[];
506
537
  pagination: Pagination & { scannedCount: number };
507
538
  }>;
539
+
540
+ type BuildCandidates = Success<{
541
+ builds: Array<{
542
+ id: number;
543
+ title: string | null;
544
+ description: string | null;
545
+ username: string | null;
546
+ publishedAt: number | null;
547
+ publishedArtifactVersionId: number | null;
548
+ collaborationMode: "private" | "open_source";
549
+ url: string;
550
+ review: {
551
+ publishedArtifactVersionId: number | null;
552
+ codePullAvailable: boolean;
553
+ requiredBeforeComment: true;
554
+ };
555
+ }>;
556
+ pagination: {
557
+ nextCursor: string | null;
558
+ hasMore: boolean;
559
+ exhausted: boolean;
560
+ };
561
+ }>;
508
562
  ```
509
563
 
510
564
  Both cursors freeze a primary-key high-water mark and traverse descending IDs,
@@ -514,6 +568,24 @@ filters, so a page — recommendation or subject — can be empty while `hasMore
514
568
  remains true; continue until `exhausted`. Subject `--after` is inclusive, and
515
569
  the opaque cursor is bound to its original date and effort filters.
516
570
 
571
+ `builds candidates` is a management-agent discovery view over the canonical
572
+ public Build browser, ordered by the current published release. It is
573
+ available through the `admin` namespace only while a delegated run is active;
574
+ page until `pagination.exhausted`. Each item includes its canonical app URL,
575
+ published artifact version, and whether its code is pullable. This list does
576
+ not decide that an app deserves a comment. The management agent must open and
577
+ genuinely try the published runtime, or pull and read an open-source project,
578
+ before making that judgment. Direct API/persona automation is never a review
579
+ substitute.
580
+
581
+ During every management run, scan recent Build candidates back through the
582
+ run's review window alongside Subjects and the recommendation queue. An app
583
+ that is thin, broken, private, unchanged since a prior substantive bot
584
+ comment, or not meaningfully understood may be left alone. A new or materially
585
+ updated app where specific, truthful feedback would help is comment-worthy.
586
+ This is editorial attention, not a quota: do not manufacture a Build comment
587
+ just to prove the queue was visited.
588
+
517
589
  `--content-types` is sent to APIs that support server-side filtering so excluded
518
590
  types do not run their eligibility/content queries. The local CLI also filters
519
591
  the returned page defensively for deployment compatibility. The server cursor
@@ -1629,7 +1701,7 @@ from memory:
1629
1701
  decisions are yours to make and record with `post skip` or in the run
1630
1702
  report).
1631
1703
 
1632
- **Lumine Build app posts are composed-only, and only after actually looking
1704
+ **Lumine Build apps and app posts are composed-only, and only after actually looking
1633
1705
  (Mikey's direction, 2026-08-10).** When a post is about a Build app — the
1634
1706
  author shares their app, announces an update, or asks for feedback on their
1635
1707
  build — never use the server-generated draft path: the API persona cannot
@@ -1647,6 +1719,29 @@ tried it — ask the author about it instead. This is the standing rule for
1647
1719
  every composed comment, applied to apps: never claim an experience the
1648
1720
  session did not actually have.
1649
1721
 
1722
+ The same rule covers comments placed **directly on a Build app**. This is
1723
+ deliberately management-agent-only: no server-generated draft and no pure API
1724
+ persona may author one. After reviewing the project, bind the composed comment
1725
+ to the exact published artifact you saw:
1726
+
1727
+ ```bash
1728
+ lumine admin comment draft build:884 --file comment.md \
1729
+ --reviewed-version 4512 --reviewed-via runtime --json
1730
+ lumine admin comment post --draft-id 77 --json
1731
+ ```
1732
+
1733
+ Use `--reviewed-via code` only when you actually pulled and read the project;
1734
+ `runtime` means you opened and tried the published app. Replies to human
1735
+ comments inside a Build use `comment:<id>` plus the same review flags. The API
1736
+ rejects missing review evidence, a server-generated Build draft, a mismatched
1737
+ published version, a private/noncanonical Build, or any Build/comment-context
1738
+ change between draft and publication. A changed version means review the new
1739
+ project state and compose again. The flags are an auditable statement of what
1740
+ the management agent did, not permission to infer experience from metadata.
1741
+ Autonomous mention/reply generation and sponsored AI replies are disabled in
1742
+ Build threads. The generic `comment edit` shortcut is also disabled there;
1743
+ review the current version and post a version-bound correction reply instead.
1744
+
1650
1745
  **Offer a Lumine prompt when the moment invites it (Mikey's direction,
1651
1746
  2026-08-10).** Zero and Ciel may include one concrete, copy-pasteable Lumine
1652
1747
  prompt in a comment or reply — a genuinely powerful one, tailored to what the
@@ -1686,15 +1781,18 @@ containers, and publication applies to both kinds of draft.
1686
1781
  A draft targets one of:
1687
1782
 
1688
1783
  - `subject:<id>` (or a bare numeric ID) — a top-level comment on the subject;
1784
+ - `build:<id>` — a top-level comment on a public canonical Build, composed
1785
+ after reviewing its exact published version;
1689
1786
  - `aiStory:<id>` / `dailyReflection:<id>` — a top-level comment on the
1690
1787
  standalone post;
1691
1788
  - `comment:<id>` — a public reply to that specific comment. `comment reply`
1692
1789
  is the same operation and requires a comment target.
1693
1790
 
1694
1791
  A reply's container resolves canonically from the target comment: its subject,
1695
- or its AI Story / Daily Reflection root. Comments under any other root are
1696
- rejected with `CLI_ADMIN_UNSUPPORTED_REPLY_ROOT`. Replies to Zero/Ciel
1697
- comments and to notification comments are rejected with
1792
+ Build, or AI Story / Daily Reflection root. Comments under any other root are
1793
+ rejected with `CLI_ADMIN_UNSUPPORTED_REPLY_ROOT`. Build replies require the
1794
+ same exact-version review evidence as top-level Build comments. Replies to
1795
+ Zero/Ciel comments and to notification comments are rejected with
1698
1796
  `CLI_ADMIN_INVALID_REPLY_TARGET` — the bots never thread with themselves or
1699
1797
  each other, and a human's later reply to a delegated comment still enters the
1700
1798
  existing autonomous comment-assistant pipeline. Published replies carry the
@@ -1706,7 +1804,7 @@ type CommentDraft = Success<{
1706
1804
  draft: {
1707
1805
  id: number;
1708
1806
  runId: number;
1709
- targetType: "subject" | "comment" | "aiStory" | "dailyReflection";
1807
+ targetType: "subject" | "comment" | "build" | "aiStory" | "dailyReflection";
1710
1808
  targetId: number;
1711
1809
  targetUrl: string;
1712
1810
  subjectId: number | null; // container subject; null for standalone posts
@@ -1737,7 +1835,8 @@ type CommentPost = CommentGet & {
1737
1835
  };
1738
1836
  published: {
1739
1837
  commentId: number;
1740
- targetType: "subject" | "comment" | "aiStory" | "dailyReflection";
1838
+ targetType:
1839
+ "subject" | "comment" | "build" | "aiStory" | "dailyReflection";
1741
1840
  targetId: number;
1742
1841
  subjectId: number | null;
1743
1842
  subjectUrl: string | null;
@@ -1748,12 +1847,13 @@ type CommentPost = CommentGet & {
1748
1847
  };
1749
1848
  ```
1750
1849
 
1751
- The server loads the canonical container (subject or standalone post) and its
1752
- complete visible comment context, then invokes the existing exact Zero/Ciel
1753
- system prompt through the shared response assembler with a mode-specific
1754
- decision policy (comment vs reply). The raw prompt is never returned or
1755
- audited. The model—not regexes or keyword rules—chooses `draft` or `skip`
1756
- under the run policy.
1850
+ For Subjects and standalone posts, the server loads the canonical container
1851
+ and its complete visible comment context, then invokes the existing exact
1852
+ Zero/Ciel system prompt through the shared response assembler with a
1853
+ mode-specific decision policy (comment vs reply). The raw prompt is never
1854
+ returned or audited. The model—not regexes or keyword rules—chooses `draft`
1855
+ or `skip` under the run policy. Build comments never enter that generated
1856
+ path; they require management-agent-composed content and review evidence.
1757
1857
 
1758
1858
  Draft IDs are bound to operator, run, public bot, target, comment mode,
1759
1859
  context revision, persona revision, expiry, and idempotency key. The context