@stage5/lumine 0.2.33 → 0.2.35

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 (
@@ -385,6 +423,25 @@ export function parseAdminOperation(options) {
385
423
  },
386
424
  );
387
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
+ }
388
445
  }
389
446
 
390
447
  if (namespace === "daily-run") {
@@ -434,6 +491,18 @@ export function parseAdminOperation(options) {
434
491
  );
435
492
  }
436
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
+
437
506
  if (
438
507
  namespace === "subjects" &&
439
508
  (action === "list" || action === "candidates")
@@ -671,7 +740,7 @@ export function parseAdminOperation(options) {
671
740
  }
672
741
 
673
742
  if (namespace === "bot-output" && !action) {
674
- if (options.adminDays !== undefined) {
743
+ if (options.adminDays) {
675
744
  const days = Number(options.adminDays);
676
745
  if (!Number.isInteger(days) || days < 1 || days > 30) {
677
746
  throw cliValidationError("--days must be an integer between 1 and 30.");
@@ -703,7 +772,7 @@ export function parseAdminOperation(options) {
703
772
 
704
773
  if (namespace === "comment") {
705
774
  if (action === "draft" || action === "reply") {
706
- const parsedTarget = parseRecommendationTarget({
775
+ const parsedTarget = parseAdminCommentTarget({
707
776
  target,
708
777
  explicitType: options.adminType,
709
778
  });
@@ -712,6 +781,35 @@ export function parseAdminOperation(options) {
712
781
  "comment reply targets a comment: lumine admin comment reply comment:<id>.",
713
782
  );
714
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
+ }
715
813
  return writeOperation(
716
814
  "comment.draft",
717
815
  "POST",
@@ -725,6 +823,8 @@ export function parseAdminOperation(options) {
725
823
  ...(options.adminFile
726
824
  ? { content: readComposedCommentFile(options.adminFile) }
727
825
  : {}),
826
+ ...(reviewedBuildVersionId ? { reviewedBuildVersionId } : {}),
827
+ ...(buildReviewMethod ? { buildReviewMethod } : {}),
728
828
  },
729
829
  );
730
830
  }
@@ -768,7 +868,7 @@ export function parseAdminOperation(options) {
768
868
  }
769
869
 
770
870
  throw cliValidationError(
771
- "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 ...",
772
872
  );
773
873
  }
774
874
 
@@ -947,6 +1047,50 @@ export function parseRecommendationTarget({ target, explicitType = "" }) {
947
1047
  return { type, id };
948
1048
  }
949
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
+
950
1094
  function normalizeRecommendationTargetType(value) {
951
1095
  const normalized = String(value || "")
952
1096
  .trim()
package/lib/commands.js CHANGED
@@ -2194,6 +2194,10 @@ export function parseArgs(args) {
2194
2194
  adminEditionId: raw.editionId ? String(raw.editionId) : "",
2195
2195
  adminLeaseToken: raw.leaseToken ? String(raw.leaseToken) : "",
2196
2196
  adminFile: raw.file ? String(raw.file) : "",
2197
+ adminReviewedBuildVersion: raw.reviewedVersion
2198
+ ? String(raw.reviewedVersion)
2199
+ : "",
2200
+ adminBuildReviewMethod: raw.reviewedVia ? String(raw.reviewedVia) : "",
2197
2201
  adminFull: parseBoolean(raw.full, false),
2198
2202
  rewardTwinkles: raw.rewardTwinkles ? String(raw.rewardTwinkles) : "",
2199
2203
  includeComments: parseBoolean(raw.includeComments, false),
@@ -2511,9 +2515,11 @@ export function printHelp() {
2511
2515
  lumine admin identity list|status|use <zero|ciel|auto> [--json]
2512
2516
  lumine admin ai-bucket get --bucket-id <id> [--json]
2513
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]
2514
2519
  lumine admin daily-run start [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
2515
2520
  lumine admin daily-run status|complete|fail [--reason <text>] [--json]
2516
2521
  lumine admin recommendations list [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2522
+ lumine admin builds candidates [--cursor <cursor>] [--limit <number>] [--json]
2517
2523
  lumine admin subjects candidates [--after <date>] [--effort unassigned] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2518
2524
  lumine admin subject get|reveal <subject-url-or-id> [--json]
2519
2525
  lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--cursor <cursor>] [--json]
@@ -2527,8 +2533,8 @@ export function printHelp() {
2527
2533
  lumine admin post recommend <target> [--type subject|comment|aiStory|dailyReflection] [--anyone-can-reward] [--reward-twinkles 3] [--json]
2528
2534
  lumine admin post skip <target> [--type comment|aiStory|dailyReflection] [--reason <text>] [--json]
2529
2535
  lumine admin post reward <target> [--type subject|comment|aiStory|dailyReflection] --twinkles 3 [--json]
2530
- lumine admin comment draft <target> [--type subject|comment|aiStory|dailyReflection] [--identity zero|ciel|auto] [--json]
2531
- 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]
2532
2538
  lumine admin comment post --draft-id <id> [--json]
2533
2539
  lumine admin comment edit <comment-id> --file <comment.md> [--json]
2534
2540
  lumine admin brief [--days <1..30>] [--json]
@@ -2597,7 +2603,7 @@ Options:
2597
2603
  --description <text> Build description for new/describe
2598
2604
  --no-description Skip New description or clear with describe
2599
2605
  --summary <text> Save summary
2600
- --note <text> Branch-suggestion message or notable-user rationale
2606
+ --note <text> Suggestion, notable-user, or AI-bucket context
2601
2607
  --cursor <id> Continue an owner suggestion inbox listing
2602
2608
  --after <date> Admin subjects: inclusive Unix/ISO creation boundary
2603
2609
  --effort unassigned Admin subjects: show only unassigned effort
@@ -2611,13 +2617,15 @@ Options:
2611
2617
  --subject-ids <ids> Complete ordered Featured subject IDs
2612
2618
  --bucket-id <id> Unbanned AI identity bucket for account consolidation
2613
2619
  --user-ids <ids> Explicit user IDs for an AI bucket batch (up to 500)
2614
- --type <type> Admin target: subject, comment, aiStory, or dailyReflection
2620
+ --type <type> Admin target: subject, comment, build, aiStory, or dailyReflection
2615
2621
  --kind recommend Admin recommendation queue kind
2616
2622
  --anyone-can-reward Enable canonical reward eligibility
2617
2623
  --reward-twinkles 3 Pair a recommendation with exactly 3 Twinkles
2618
2624
  --twinkles 3 Give exactly 3 Twinkles through the normal economy
2619
2625
  --draft-id <id> Canonical delegated comment draft ID
2620
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
2621
2629
  --reason <text> Reason when marking an admin run failed
2622
2630
  --force Overwrite server files even if this workspace is stale or missing filesHash
2623
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.33",
3
+ "version": "0.2.35",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,6 +59,8 @@ lumine admin ai-bucket get --bucket-id 10 --json
59
59
  lumine admin ai-bucket accounts add --bucket-id 10 \
60
60
  --user-ids 3127,13037,15410,16288 \
61
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
62
64
  ```
63
65
 
64
66
  `accounts add` accepts 1-500 unique positive user IDs, preflights the complete
@@ -67,6 +69,10 @@ re-attributes current-day AI usage, and returns the canonical bucket members.
67
69
  It is idempotent to retry. The API records the real operator in the private
68
70
  Lumine audit log; no public bot identity is involved.
69
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
+
70
76
  This surface intentionally accepts only an existing **unbanned** bucket. It
71
77
  cannot ban accounts, block signup, add IP/device/risk-key rules, or infer an
72
78
  account family. Identification remains a human/LLM evidence judgment and must
@@ -124,7 +130,11 @@ posts that most need Zero or Ciel are the ones nobody else answered.
124
130
  merely look unimpressive.
125
131
  - **Effort levels are not a verdict on the child.** Level 1 on a thin post is
126
132
  ordinary bookkeeping; it never means the author deserves less attention, and
127
- it pairs well with a warm comment.
133
+ it pairs well with a warm comment. Judge the depth the Subject invites, not
134
+ merely the number of words in its prompt: a genuinely thought-provoking
135
+ question should normally receive Level 3. Level 3 is the highest delegated
136
+ setting, and the level tells respondents that substantial, carefully
137
+ reasoned answers are wanted.
128
138
  - **Featured still selects for quality**, but when two candidates are close,
129
139
  prefer the child who has never been featured over the one who has.
130
140
  - **The live Featured board is Mikey's word.** Do not remove a currently
@@ -484,6 +494,7 @@ lumine admin subjects candidates --after 2026-08-01T00:00:00Z \
484
494
  --cursor '<cursor>' --json
485
495
  lumine admin subjects candidates --effort unassigned --json
486
496
  lumine admin subjects candidates --unviewed --json
497
+ lumine admin builds candidates --cursor '<cursor>' --limit 50 --json
487
498
  ```
488
499
 
489
500
  Schemas:
@@ -529,6 +540,29 @@ type SubjectCandidates = Success<{
529
540
  subjects: Subject[];
530
541
  pagination: Pagination & { scannedCount: number };
531
542
  }>;
543
+
544
+ type BuildCandidates = Success<{
545
+ builds: Array<{
546
+ id: number;
547
+ title: string | null;
548
+ description: string | null;
549
+ username: string | null;
550
+ publishedAt: number | null;
551
+ publishedArtifactVersionId: number | null;
552
+ collaborationMode: "private" | "open_source";
553
+ url: string;
554
+ review: {
555
+ publishedArtifactVersionId: number | null;
556
+ codePullAvailable: boolean;
557
+ requiredBeforeComment: true;
558
+ };
559
+ }>;
560
+ pagination: {
561
+ nextCursor: string | null;
562
+ hasMore: boolean;
563
+ exhausted: boolean;
564
+ };
565
+ }>;
532
566
  ```
533
567
 
534
568
  Both cursors freeze a primary-key high-water mark and traverse descending IDs,
@@ -538,6 +572,24 @@ filters, so a page — recommendation or subject — can be empty while `hasMore
538
572
  remains true; continue until `exhausted`. Subject `--after` is inclusive, and
539
573
  the opaque cursor is bound to its original date and effort filters.
540
574
 
575
+ `builds candidates` is a management-agent discovery view over the canonical
576
+ public Build browser, ordered by the current published release. It is
577
+ available through the `admin` namespace only while a delegated run is active;
578
+ page until `pagination.exhausted`. Each item includes its canonical app URL,
579
+ published artifact version, and whether its code is pullable. This list does
580
+ not decide that an app deserves a comment. The management agent must open and
581
+ genuinely try the published runtime, or pull and read an open-source project,
582
+ before making that judgment. Direct API/persona automation is never a review
583
+ substitute.
584
+
585
+ During every management run, scan recent Build candidates back through the
586
+ run's review window alongside Subjects and the recommendation queue. An app
587
+ that is thin, broken, private, unchanged since a prior substantive bot
588
+ comment, or not meaningfully understood may be left alone. A new or materially
589
+ updated app where specific, truthful feedback would help is comment-worthy.
590
+ This is editorial attention, not a quota: do not manufacture a Build comment
591
+ just to prove the queue was visited.
592
+
541
593
  `--content-types` is sent to APIs that support server-side filtering so excluded
542
594
  types do not run their eligibility/content queries. The local CLI also filters
543
595
  the returned page defensively for deployment compatibility. The server cursor
@@ -1653,7 +1705,7 @@ from memory:
1653
1705
  decisions are yours to make and record with `post skip` or in the run
1654
1706
  report).
1655
1707
 
1656
- **Lumine Build app posts are composed-only, and only after actually looking
1708
+ **Lumine Build apps and app posts are composed-only, and only after actually looking
1657
1709
  (Mikey's direction, 2026-08-10).** When a post is about a Build app — the
1658
1710
  author shares their app, announces an update, or asks for feedback on their
1659
1711
  build — never use the server-generated draft path: the API persona cannot
@@ -1671,6 +1723,29 @@ tried it — ask the author about it instead. This is the standing rule for
1671
1723
  every composed comment, applied to apps: never claim an experience the
1672
1724
  session did not actually have.
1673
1725
 
1726
+ The same rule covers comments placed **directly on a Build app**. This is
1727
+ deliberately management-agent-only: no server-generated draft and no pure API
1728
+ persona may author one. After reviewing the project, bind the composed comment
1729
+ to the exact published artifact you saw:
1730
+
1731
+ ```bash
1732
+ lumine admin comment draft build:884 --file comment.md \
1733
+ --reviewed-version 4512 --reviewed-via runtime --json
1734
+ lumine admin comment post --draft-id 77 --json
1735
+ ```
1736
+
1737
+ Use `--reviewed-via code` only when you actually pulled and read the project;
1738
+ `runtime` means you opened and tried the published app. Replies to human
1739
+ comments inside a Build use `comment:<id>` plus the same review flags. The API
1740
+ rejects missing review evidence, a server-generated Build draft, a mismatched
1741
+ published version, a private/noncanonical Build, or any Build/comment-context
1742
+ change between draft and publication. A changed version means review the new
1743
+ project state and compose again. The flags are an auditable statement of what
1744
+ the management agent did, not permission to infer experience from metadata.
1745
+ Autonomous mention/reply generation and sponsored AI replies are disabled in
1746
+ Build threads. The generic `comment edit` shortcut is also disabled there;
1747
+ review the current version and post a version-bound correction reply instead.
1748
+
1674
1749
  **Offer a Lumine prompt when the moment invites it (Mikey's direction,
1675
1750
  2026-08-10).** Zero and Ciel may include one concrete, copy-pasteable Lumine
1676
1751
  prompt in a comment or reply — a genuinely powerful one, tailored to what the
@@ -1710,15 +1785,18 @@ containers, and publication applies to both kinds of draft.
1710
1785
  A draft targets one of:
1711
1786
 
1712
1787
  - `subject:<id>` (or a bare numeric ID) — a top-level comment on the subject;
1788
+ - `build:<id>` — a top-level comment on a public canonical Build, composed
1789
+ after reviewing its exact published version;
1713
1790
  - `aiStory:<id>` / `dailyReflection:<id>` — a top-level comment on the
1714
1791
  standalone post;
1715
1792
  - `comment:<id>` — a public reply to that specific comment. `comment reply`
1716
1793
  is the same operation and requires a comment target.
1717
1794
 
1718
1795
  A reply's container resolves canonically from the target comment: its subject,
1719
- or its AI Story / Daily Reflection root. Comments under any other root are
1720
- rejected with `CLI_ADMIN_UNSUPPORTED_REPLY_ROOT`. Replies to Zero/Ciel
1721
- comments and to notification comments are rejected with
1796
+ Build, or AI Story / Daily Reflection root. Comments under any other root are
1797
+ rejected with `CLI_ADMIN_UNSUPPORTED_REPLY_ROOT`. Build replies require the
1798
+ same exact-version review evidence as top-level Build comments. Replies to
1799
+ Zero/Ciel comments and to notification comments are rejected with
1722
1800
  `CLI_ADMIN_INVALID_REPLY_TARGET` — the bots never thread with themselves or
1723
1801
  each other, and a human's later reply to a delegated comment still enters the
1724
1802
  existing autonomous comment-assistant pipeline. Published replies carry the
@@ -1730,7 +1808,7 @@ type CommentDraft = Success<{
1730
1808
  draft: {
1731
1809
  id: number;
1732
1810
  runId: number;
1733
- targetType: "subject" | "comment" | "aiStory" | "dailyReflection";
1811
+ targetType: "subject" | "comment" | "build" | "aiStory" | "dailyReflection";
1734
1812
  targetId: number;
1735
1813
  targetUrl: string;
1736
1814
  subjectId: number | null; // container subject; null for standalone posts
@@ -1761,7 +1839,8 @@ type CommentPost = CommentGet & {
1761
1839
  };
1762
1840
  published: {
1763
1841
  commentId: number;
1764
- targetType: "subject" | "comment" | "aiStory" | "dailyReflection";
1842
+ targetType:
1843
+ "subject" | "comment" | "build" | "aiStory" | "dailyReflection";
1765
1844
  targetId: number;
1766
1845
  subjectId: number | null;
1767
1846
  subjectUrl: string | null;
@@ -1772,12 +1851,13 @@ type CommentPost = CommentGet & {
1772
1851
  };
1773
1852
  ```
1774
1853
 
1775
- The server loads the canonical container (subject or standalone post) and its
1776
- complete visible comment context, then invokes the existing exact Zero/Ciel
1777
- system prompt through the shared response assembler with a mode-specific
1778
- decision policy (comment vs reply). The raw prompt is never returned or
1779
- audited. The model—not regexes or keyword rules—chooses `draft` or `skip`
1780
- under the run policy.
1854
+ For Subjects and standalone posts, the server loads the canonical container
1855
+ and its complete visible comment context, then invokes the existing exact
1856
+ Zero/Ciel system prompt through the shared response assembler with a
1857
+ mode-specific decision policy (comment vs reply). The raw prompt is never
1858
+ returned or audited. The model—not regexes or keyword rules—chooses `draft`
1859
+ or `skip` under the run policy. Build comments never enter that generated
1860
+ path; they require management-agent-composed content and review evidence.
1781
1861
 
1782
1862
  Draft IDs are bound to operator, run, public bot, target, comment mode,
1783
1863
  context revision, persona revision, expiry, and idempotency key. The context