@stage5/lumine 0.2.24 → 0.2.25
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 +71 -0
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +51 -4
package/lib/admin.js
CHANGED
|
@@ -4,6 +4,36 @@ import { assertAuthScope, resolveAuth } from "./auth.js";
|
|
|
4
4
|
import { requestJson } from "./http.js";
|
|
5
5
|
|
|
6
6
|
const MAX_EDITORIAL_FILE_BYTES = 256 * 1024;
|
|
7
|
+
const MAX_COMPOSED_COMMENT_FILE_BYTES = 64 * 1024;
|
|
8
|
+
const MAX_COMPOSED_COMMENT_LENGTH = 10_000;
|
|
9
|
+
|
|
10
|
+
// Operator-composed persona comment text (plain UTF-8, not JSON). The agent
|
|
11
|
+
// writes the comment in the bot's persona itself; the server never invokes
|
|
12
|
+
// its model and no AI Energy is spent.
|
|
13
|
+
function readComposedCommentFile(filePath) {
|
|
14
|
+
const normalizedPath = String(filePath || "").trim();
|
|
15
|
+
let contents;
|
|
16
|
+
try {
|
|
17
|
+
contents = readFileSync(normalizedPath, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
throw cliValidationError(`Could not read ${normalizedPath}.`);
|
|
20
|
+
}
|
|
21
|
+
if (Buffer.byteLength(contents, "utf8") > MAX_COMPOSED_COMMENT_FILE_BYTES) {
|
|
22
|
+
throw cliValidationError("The composed comment file must be under 64KB.");
|
|
23
|
+
}
|
|
24
|
+
const normalized = contents.trim();
|
|
25
|
+
if (!normalized) {
|
|
26
|
+
throw cliValidationError(
|
|
27
|
+
`${normalizedPath} is empty; a composed comment needs text.`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (normalized.length > MAX_COMPOSED_COMMENT_LENGTH) {
|
|
31
|
+
throw cliValidationError(
|
|
32
|
+
`A composed comment must be at most ${MAX_COMPOSED_COMMENT_LENGTH} characters.`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
7
37
|
|
|
8
38
|
function readEditorialFile(filePath) {
|
|
9
39
|
const normalizedPath = String(filePath || "").trim();
|
|
@@ -126,6 +156,15 @@ export async function adminCommand(options) {
|
|
|
126
156
|
}
|
|
127
157
|
throw error;
|
|
128
158
|
}
|
|
159
|
+
if (
|
|
160
|
+
operation.name === "comment.draft" &&
|
|
161
|
+
typeof operation.body?.content === "string"
|
|
162
|
+
) {
|
|
163
|
+
assertComposedCommentDraftResult({
|
|
164
|
+
result,
|
|
165
|
+
expectedContent: operation.body.content,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
129
168
|
if (options.json) {
|
|
130
169
|
console.log(JSON.stringify(result));
|
|
131
170
|
return result;
|
|
@@ -134,6 +173,35 @@ export async function adminCommand(options) {
|
|
|
134
173
|
return result;
|
|
135
174
|
}
|
|
136
175
|
|
|
176
|
+
export function assertComposedCommentDraftResult({
|
|
177
|
+
result,
|
|
178
|
+
expectedContent,
|
|
179
|
+
}) {
|
|
180
|
+
const draft = result?.data?.draft;
|
|
181
|
+
if (
|
|
182
|
+
draft?.decision === "draft" &&
|
|
183
|
+
draft?.reason === "operator-composed" &&
|
|
184
|
+
draft?.content === expectedContent &&
|
|
185
|
+
draft?.status === "ready"
|
|
186
|
+
) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const error = new Error(
|
|
190
|
+
"The API did not confirm the operator-composed draft. Stop without publishing it and deploy an API that supports composed drafts.",
|
|
191
|
+
);
|
|
192
|
+
error.code = "LUMINE_ADMIN_COMPOSED_COMMENT_UNSUPPORTED";
|
|
193
|
+
error.data = {
|
|
194
|
+
ok: false,
|
|
195
|
+
status: "validation_error",
|
|
196
|
+
error: {
|
|
197
|
+
code: error.code,
|
|
198
|
+
message: error.message,
|
|
199
|
+
details: null,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
|
|
137
205
|
const RECOMMENDATION_CONTENT_TYPES = new Map([
|
|
138
206
|
["comment", "comment"],
|
|
139
207
|
["aistory", "aiStory"],
|
|
@@ -551,6 +619,9 @@ export function parseAdminOperation(options) {
|
|
|
551
619
|
identity: options.adminIdentity
|
|
552
620
|
? parseIdentity(options.adminIdentity)
|
|
553
621
|
: undefined,
|
|
622
|
+
...(options.adminFile
|
|
623
|
+
? { content: readComposedCommentFile(options.adminFile) }
|
|
624
|
+
: {}),
|
|
554
625
|
},
|
|
555
626
|
);
|
|
556
627
|
}
|
package/package.json
CHANGED
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -103,6 +103,15 @@ posts that most need Zero or Ciel are the ones nobody else answered.
|
|
|
103
103
|
it pairs well with a warm comment.
|
|
104
104
|
- **Featured still selects for quality**, but when two candidates are close,
|
|
105
105
|
prefer the child who has never been featured over the one who has.
|
|
106
|
+
- **The live Featured board is Mikey's word.** Do not remove a currently
|
|
107
|
+
Featured subject without first showing Mikey the planned removals and
|
|
108
|
+
replacements and getting his go-ahead. In the other direction, if a subject
|
|
109
|
+
that was Featured or pinned during an earlier run is no longer on
|
|
110
|
+
`featured list`, treat that as Mikey having removed it deliberately — never
|
|
111
|
+
re-feature it to "restore" the board, and never treat any subject as a
|
|
112
|
+
permanent fixture from memory or old run notes. Derive the board fresh from
|
|
113
|
+
`featured list` at the start of every run; the only pins that exist are the
|
|
114
|
+
ones currently on it.
|
|
106
115
|
|
|
107
116
|
Sensitive disclosures, active disputes, and anything needing crisis or medical
|
|
108
117
|
judgment remain out of scope for a bot comment no matter how neglected the post
|
|
@@ -1082,19 +1091,57 @@ type AuditList = Success<{
|
|
|
1082
1091
|
## Persona-backed comments and replies
|
|
1083
1092
|
|
|
1084
1093
|
```bash
|
|
1085
|
-
lumine admin daily-run start --identity auto --comment-mode draft --json
|
|
1086
|
-
lumine admin comment draft 123 --identity auto --json
|
|
1087
|
-
|
|
1088
1094
|
lumine admin daily-run start --identity ciel --comment-mode post \
|
|
1089
1095
|
--run-key daily:2026-08-06:comments --json
|
|
1096
|
+
|
|
1097
|
+
# Default: the agent composes the comment in the bot's persona itself.
|
|
1098
|
+
lumine admin comment draft 123 --file comment.md --json
|
|
1099
|
+
lumine admin comment draft dailyReflection:99 --file comment.md --json
|
|
1100
|
+
lumine admin comment reply comment:456 --file reply.md --json
|
|
1101
|
+
|
|
1102
|
+
# Fallback (only when Mikey asks for it): server-generated persona drafts.
|
|
1090
1103
|
lumine admin comment draft 123 --identity ciel \
|
|
1091
1104
|
--idempotency-key comment-123-draft-v1 --json
|
|
1092
|
-
lumine admin comment draft dailyReflection:99 --json
|
|
1093
1105
|
lumine admin comment reply comment:456 --json
|
|
1106
|
+
|
|
1094
1107
|
lumine admin comment post --draft-id 77 \
|
|
1095
1108
|
--idempotency-key comment-123-post-v1 --json
|
|
1096
1109
|
```
|
|
1097
1110
|
|
|
1111
|
+
**Compose in persona by default (Mikey's standing direction, 2026-08-10).**
|
|
1112
|
+
A delegated agent writing Zero/Ciel comments should assume the bot's persona
|
|
1113
|
+
and write the comment text itself, submitting it with `--file` — exactly like
|
|
1114
|
+
the newspaper's claim/submit path, this spends no provider credits and no AI
|
|
1115
|
+
Energy (server-generated drafts bill the **operator's own** AI Energy
|
|
1116
|
+
battery). Use the no-`--file` server-generated path only when Mikey
|
|
1117
|
+
explicitly asks for it. Before composing, read the canonical persona sources
|
|
1118
|
+
so the voice and judgment match the real bots — do not improvise the persona
|
|
1119
|
+
from memory:
|
|
1120
|
+
|
|
1121
|
+
- `twinkle-api/constants/index.ts` — `SYS_PROMPT_FOR_CIEL` /
|
|
1122
|
+
`SYS_PROMPT_FOR_ZERO` (the exact persona system prompts) and
|
|
1123
|
+
`TWINKLE_FEATURES_EXPLANATION` (what the bots know about the site);
|
|
1124
|
+
- `twinkle-api/helpers/ai/comment-assistant/index.ts` —
|
|
1125
|
+
`ADMIN_COMMENT_DECISION_POLICY` / `ADMIN_REPLY_DECISION_POLICY` (the
|
|
1126
|
+
draft-vs-skip judgment rules, which still govern composed comments: skip
|
|
1127
|
+
decisions are yours to make and record with `post skip` or in the run
|
|
1128
|
+
report).
|
|
1129
|
+
|
|
1130
|
+
A composed draft (`--file`, plain UTF-8 text, at most the website's 10,000
|
|
1131
|
+
character comment limit) flows through the identical draft lifecycle —
|
|
1132
|
+
reservation, idempotency, context-revision CAS, publish fencing, audit
|
|
1133
|
+
(`metadata.composed: true`) — and is published with the same
|
|
1134
|
+
`comment post --draft-id`. It never invokes the server's model and records
|
|
1135
|
+
no AI Energy usage. Deployment guard: an API deployed before this capability
|
|
1136
|
+
silently ignores `content` and generates with the server's model instead. The
|
|
1137
|
+
CLI therefore requires the ready draft response to echo the exact submitted
|
|
1138
|
+
text with `reason: "operator-composed"`; otherwise it stops with
|
|
1139
|
+
`LUMINE_ADMIN_COMPOSED_COMMENT_UNSUPPORTED`. Never publish that rejected draft.
|
|
1140
|
+
Placement stays on the requested target: compose replies via `comment:<id>`
|
|
1141
|
+
targets (the generated path's model-chosen
|
|
1142
|
+
`replyTargetCommentId` does not apply). Everything below about targets,
|
|
1143
|
+
containers, and publication applies to both kinds of draft.
|
|
1144
|
+
|
|
1098
1145
|
A draft targets one of:
|
|
1099
1146
|
|
|
1100
1147
|
- `subject:<id>` (or a bare numeric ID) — a top-level comment on the subject;
|