@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/README.md +78 -13
- package/lib/admin-news.js +236 -0
- package/lib/admin-workflows.js +434 -0
- package/lib/admin.js +644 -45
- package/lib/agent/mcp-server.js +138 -0
- package/lib/agent/providers/claude-code.js +214 -0
- package/lib/agent/providers/codex.js +522 -0
- package/lib/agent/providers/environment.js +40 -0
- package/lib/agent/providers/index.js +38 -0
- package/lib/agent/tool-session.js +301 -0
- package/lib/agent/trace.js +138 -0
- package/lib/agent.js +453 -0
- package/lib/api.js +81 -1
- package/lib/build-review.js +493 -0
- package/lib/commands.js +86 -9
- package/lib/constants.js +2 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +24 -3
- package/sdk/LUMINE_ADMIN.md +271 -32
package/README.md
CHANGED
|
@@ -164,7 +164,35 @@ version state in `.twinkle/lumine-project.json` so local agents can tell when
|
|
|
164
164
|
they should rerun with `npx @stage5/lumine@latest`. Use `--no-update-check` to
|
|
165
165
|
skip that advisory network check.
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
## Subscription agents through the Lumine loop
|
|
168
|
+
|
|
169
|
+
After pulling a project, a signed-in Codex or Claude Code subscription can
|
|
170
|
+
power Lumine's workspace loop without spending Twinkle AI Energy:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
lumine agent --provider codex "Add keyboard controls"
|
|
174
|
+
lumine agent --provider claude-code "Fix the mobile layout"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The external model cannot write project files directly. Lumine supplies the
|
|
178
|
+
same core workspace prompt/tool contract, read-before-edit behavior, bounded
|
|
179
|
+
scope checks, and validation-repair passes used by the hosted Build agent. A save happens only
|
|
180
|
+
after validation passes and still uses the workspace's server-issued
|
|
181
|
+
`filesHash`, so a concurrent server change stops the run instead of being
|
|
182
|
+
overwritten. Provider login and model usage stay inside the selected local CLI;
|
|
183
|
+
Twinkle never receives a provider credential and does not reserve AI Energy.
|
|
184
|
+
|
|
185
|
+
Every run writes a sanitized observable tool trace under
|
|
186
|
+
`.twinkle/agent-runs/`. By default, the selected subscription agent reviews
|
|
187
|
+
that trace after the pass and records evidence-based loop feedback without
|
|
188
|
+
collecting hidden chain-of-thought; use `--no-review-loop` to skip that extra
|
|
189
|
+
provider turn. Provider support is adapter-based rather than Codex-specific.
|
|
190
|
+
Codex uses its local app-server protocol and Claude Code uses the same Lumine
|
|
191
|
+
tools over MCP. Both adapters disable inherited model-side project tooling and
|
|
192
|
+
launch with a credential-minimized environment, so project access remains at
|
|
193
|
+
the Lumine tool boundary.
|
|
194
|
+
|
|
195
|
+
You can still use a coding agent directly for a manual edit-and-save workflow:
|
|
168
196
|
|
|
169
197
|
```bash
|
|
170
198
|
codex "Read AGENTS.md, then make the requested change."
|
|
@@ -186,11 +214,16 @@ events.
|
|
|
186
214
|
|
|
187
215
|
```bash
|
|
188
216
|
lumine admin identity list --json
|
|
217
|
+
lumine admin identity inspect Jay1216 \
|
|
218
|
+
--reason "Confirm account family before a quota-bucket change" --json
|
|
189
219
|
lumine admin daily-run start --identity auto --comment-mode off --json
|
|
190
|
-
lumine admin recommendations list --
|
|
220
|
+
lumine admin recommendations list --all --checkpoint recommendations.json --json
|
|
221
|
+
lumine admin recommendations list --after 2026-08-14T00:00:00Z --all --json
|
|
222
|
+
lumine admin recommendations list --include-legacy --all --json
|
|
191
223
|
lumine admin subjects candidates --after 2026-08-01T00:00:00Z --json
|
|
192
224
|
lumine admin subjects candidates --effort unassigned --json
|
|
193
|
-
lumine admin builds candidates --
|
|
225
|
+
lumine admin builds candidates --all --json
|
|
226
|
+
lumine admin builds review build:884 --output-dir ./build-review --json
|
|
194
227
|
lumine admin subject get 123 --include-comments --json
|
|
195
228
|
lumine admin post get https://www.twin-kle.com/ai-stories/88 --json
|
|
196
229
|
lumine admin post comments dailyReflection:99 --json
|
|
@@ -206,10 +239,20 @@ lumine admin brief --days 3 --json
|
|
|
206
239
|
lumine admin notable add Minecrarft_guy --note "Created 8 thoughtful subjects and helped peers in 23 comments this window." --json
|
|
207
240
|
lumine admin post recommend comment:456 --anyone-can-reward --reward-twinkles 3 --json
|
|
208
241
|
lumine admin post reward comment:456 --twinkles 3 --json
|
|
242
|
+
lumine admin post skip-batch --target-file skipped.json --checkpoint skip-progress.json --json
|
|
209
243
|
lumine admin comment draft build:884 --file comment.md \
|
|
210
|
-
--
|
|
244
|
+
--review-receipt /path/from-build-review/review.json --json
|
|
211
245
|
lumine admin comment post --draft-id 77 --json
|
|
246
|
+
lumine admin news claim --output claim.json --scaffold editorial.json --json
|
|
247
|
+
lumine admin news validate --claim claim.json --file editorial.json --json
|
|
248
|
+
lumine admin news submit --claim claim.json --file editorial.json --json
|
|
249
|
+
lumine admin daily-run escalation add --target subject:123 \
|
|
250
|
+
--note "Concrete privacy issue requiring owner review" --json
|
|
251
|
+
lumine admin daily-run report --json
|
|
212
252
|
lumine admin daily-run complete --json
|
|
253
|
+
lumine admin escalation list --status all --json
|
|
254
|
+
lumine admin escalation set 123 --status resolved \
|
|
255
|
+
--note "Final owner decision" --json
|
|
213
256
|
```
|
|
214
257
|
|
|
215
258
|
Numeric recommendation targets default to subjects. Use `comment:<id>`,
|
|
@@ -221,11 +264,15 @@ reply is handled by Twinkle's existing autonomous Zero/Ciel responder without
|
|
|
221
264
|
Lumine remaining active.
|
|
222
265
|
|
|
223
266
|
Management agents also inspect recent public Build candidates during each run.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
267
|
+
`builds review` opens one published app in an isolated temporary Chromium
|
|
268
|
+
profile, captures a screenshot and console evidence, verifies that the
|
|
269
|
+
published version stayed fixed, and writes a review receipt in a unique output
|
|
270
|
+
subdirectory. A direct Build comment is never server-generated: review the
|
|
271
|
+
runtime (or pull and read an
|
|
272
|
+
open-source app), compose with `--file`, and attach the receipt. The server
|
|
273
|
+
rejects missing or stale review evidence and any app/thread change before
|
|
274
|
+
publication. Manual `--reviewed-version` / `--reviewed-via` evidence remains
|
|
275
|
+
available for genuine code reviews.
|
|
229
276
|
|
|
230
277
|
Every operation is noninteractive when its required arguments are present.
|
|
231
278
|
`--json` prints exactly one uncolored JSON value and returns a nonzero status
|
|
@@ -235,10 +282,28 @@ server also enforces canonical no-duplicate invariants. Failed mutation JSON
|
|
|
235
282
|
includes `error.details.retryIdempotencyKey` so a partial attempt can be
|
|
236
283
|
resumed with the exact generated key.
|
|
237
284
|
|
|
238
|
-
Subject and
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
285
|
+
Subject and queue listings use opaque, stable snapshot cursors. `--all`
|
|
286
|
+
follows them automatically, saves a checkpoint after every canonical page,
|
|
287
|
+
and records completed queue coverage in the run audit; `--resume` continues
|
|
288
|
+
the exact same request. With `--all --json`, bounded progress goes to stderr so
|
|
289
|
+
stdout remains one pipe-safe JSON value. Recommendation scans default to the previous completed
|
|
290
|
+
run's start boundary for at-least-once coverage. Use `--after` for an explicit
|
|
291
|
+
timestamp or `--include-legacy`
|
|
292
|
+
for an intentional all-history scan. Subject `--after` is inclusive and
|
|
293
|
+
cursors are bound to the original date and effort filters.
|
|
294
|
+
|
|
295
|
+
`news claim` can write both the canonical leased digest and an editable
|
|
296
|
+
editorial scaffold. `news validate` is local and checks every citation and
|
|
297
|
+
quote before submission; `news submit --claim` reads the lease identity from
|
|
298
|
+
the claim file. `daily-run report` summarizes confirmed mutations, completed
|
|
299
|
+
queue coverage, explicitly recorded escalations, and the run brief before the
|
|
300
|
+
run is completed.
|
|
301
|
+
|
|
302
|
+
Identity inspection, escalation dispositions, AI-bucket maintenance, and
|
|
303
|
+
approved Notable User additions are private operator bookkeeping and do not
|
|
304
|
+
require a delegated daily run. Identity inspection always requires an audited
|
|
305
|
+
`--reason`; raw email/DOB evidence additionally requires
|
|
306
|
+
`--include-private-evidence`. Routine briefs omit raw email identities.
|
|
242
307
|
|
|
243
308
|
The complete run lifecycle, command contracts, nullable fields, Karma approval
|
|
244
309
|
behavior, pagination semantics, secret-subject behavior, presence isolation,
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const MAX_ADMIN_JSON_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
function validationError(message) {
|
|
13
|
+
const error = new Error(message);
|
|
14
|
+
error.code = "CLI_ADMIN_CLI_VALIDATION";
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatByteLimit(maxBytes) {
|
|
19
|
+
const megabytes = maxBytes / (1024 * 1024);
|
|
20
|
+
return Number.isInteger(megabytes)
|
|
21
|
+
? `${megabytes} MB`
|
|
22
|
+
: `${maxBytes} bytes`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readAdminJsonFile(
|
|
26
|
+
filePath,
|
|
27
|
+
label = "JSON file",
|
|
28
|
+
{ maxBytes = MAX_ADMIN_JSON_BYTES } = {},
|
|
29
|
+
) {
|
|
30
|
+
const normalizedPath = String(filePath || "").trim();
|
|
31
|
+
if (!normalizedPath) throw validationError(`Pass ${label}.`);
|
|
32
|
+
let contents;
|
|
33
|
+
try {
|
|
34
|
+
contents = readFileSync(normalizedPath, "utf8");
|
|
35
|
+
} catch {
|
|
36
|
+
throw validationError(`Could not read ${normalizedPath}.`);
|
|
37
|
+
}
|
|
38
|
+
if (Buffer.byteLength(contents, "utf8") > maxBytes) {
|
|
39
|
+
throw validationError(`${label} must be under ${formatByteLimit(maxBytes)}.`);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(contents);
|
|
43
|
+
} catch {
|
|
44
|
+
throw validationError(`${normalizedPath} is not valid JSON.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function writeAdminJsonFile(
|
|
49
|
+
filePath,
|
|
50
|
+
value,
|
|
51
|
+
{ privateFile = false, maxBytes = null } = {},
|
|
52
|
+
) {
|
|
53
|
+
const resolved = path.resolve(String(filePath || "").trim());
|
|
54
|
+
if (!String(filePath || "").trim()) {
|
|
55
|
+
throw validationError("An output file path is required.");
|
|
56
|
+
}
|
|
57
|
+
mkdirSync(path.dirname(resolved), { recursive: true });
|
|
58
|
+
const temporary = `${resolved}.tmp-${process.pid}`;
|
|
59
|
+
const contents = `${JSON.stringify(value, null, 2)}\n`;
|
|
60
|
+
if (
|
|
61
|
+
Number.isSafeInteger(maxBytes) &&
|
|
62
|
+
maxBytes > 0 &&
|
|
63
|
+
Buffer.byteLength(contents, "utf8") > maxBytes
|
|
64
|
+
) {
|
|
65
|
+
throw validationError(
|
|
66
|
+
`The output exceeds the ${formatByteLimit(maxBytes)} safety limit. Narrow the request before retrying.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
writeFileSync(temporary, contents, {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
mode: privateFile ? 0o600 : 0o644,
|
|
72
|
+
});
|
|
73
|
+
renameSync(temporary, resolved);
|
|
74
|
+
if (privateFile) chmodSync(resolved, 0o600);
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function extractNewsClaim(value) {
|
|
79
|
+
const claim = value?.claim || value?.data?.claim || value;
|
|
80
|
+
if (!claim || typeof claim !== "object" || Array.isArray(claim)) {
|
|
81
|
+
throw validationError("The claim file does not contain a newspaper claim.");
|
|
82
|
+
}
|
|
83
|
+
const editionId = Number(claim.editionId || 0);
|
|
84
|
+
const leaseToken = String(claim.leaseToken || "").trim();
|
|
85
|
+
const events = Array.isArray(claim.events) ? claim.events : null;
|
|
86
|
+
if (!Number.isSafeInteger(editionId) || editionId <= 0 || !leaseToken || !events) {
|
|
87
|
+
throw validationError(
|
|
88
|
+
"The claim file is missing editionId, leaseToken, or canonical events.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return { ...claim, editionId, leaseToken, events };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function exactQuoteFromSummary(summary, maximum) {
|
|
95
|
+
return String(summary || "").slice(0, Math.max(0, maximum));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function scaffoldStory(event, maximum) {
|
|
99
|
+
return {
|
|
100
|
+
eventKey: String(event.eventKey || ""),
|
|
101
|
+
headline: "",
|
|
102
|
+
summary: "",
|
|
103
|
+
sourceQuote:
|
|
104
|
+
String(event.section || "") === "front"
|
|
105
|
+
? exactQuoteFromSummary(event.summary, maximum)
|
|
106
|
+
: "",
|
|
107
|
+
coveredEventKeys: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createNewsEditorialScaffold(claimValue) {
|
|
112
|
+
const claim = extractNewsClaim(claimValue);
|
|
113
|
+
const maximum = Math.max(0, Number(claim.maxSourceQuoteLength || 360));
|
|
114
|
+
const frontIndex = claim.events.findIndex(
|
|
115
|
+
(event) => String(event?.section || "") === "front",
|
|
116
|
+
);
|
|
117
|
+
const lead = frontIndex >= 0 ? scaffoldStory(claim.events[frontIndex], maximum) : null;
|
|
118
|
+
const stories = claim.events
|
|
119
|
+
.filter((_event, index) => index !== frontIndex)
|
|
120
|
+
.map((event) => scaffoldStory(event, maximum));
|
|
121
|
+
return {
|
|
122
|
+
mastheadHeadline: "",
|
|
123
|
+
mastheadDeck: "",
|
|
124
|
+
...(lead ? { lead } : {}),
|
|
125
|
+
stories,
|
|
126
|
+
editorsNote: "",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function requireEditorialText(value, label) {
|
|
131
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
132
|
+
throw validationError(`${label} must be non-empty text.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function validateNewsEditorial({ claim: claimValue, editorial }) {
|
|
137
|
+
const claim = extractNewsClaim(claimValue);
|
|
138
|
+
if (!editorial || typeof editorial !== "object" || Array.isArray(editorial)) {
|
|
139
|
+
throw validationError("The editorial must be a JSON object.");
|
|
140
|
+
}
|
|
141
|
+
requireEditorialText(editorial.mastheadHeadline, "mastheadHeadline");
|
|
142
|
+
requireEditorialText(editorial.mastheadDeck, "mastheadDeck");
|
|
143
|
+
requireEditorialText(editorial.editorsNote, "editorsNote");
|
|
144
|
+
if (!Array.isArray(editorial.stories)) {
|
|
145
|
+
throw validationError("stories must be an array.");
|
|
146
|
+
}
|
|
147
|
+
const eventByKey = new Map(
|
|
148
|
+
claim.events.map((event) => [String(event?.eventKey || ""), event]),
|
|
149
|
+
);
|
|
150
|
+
const usedKeys = new Set();
|
|
151
|
+
const maximum = Math.max(0, Number(claim.maxSourceQuoteLength || 360));
|
|
152
|
+
const entries = [
|
|
153
|
+
...(editorial.lead ? [{ label: "lead", story: editorial.lead, lead: true }] : []),
|
|
154
|
+
...editorial.stories.map((story, index) => ({
|
|
155
|
+
label: `stories[${index}]`,
|
|
156
|
+
story,
|
|
157
|
+
lead: false,
|
|
158
|
+
})),
|
|
159
|
+
];
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const story = entry.story;
|
|
162
|
+
if (!story || typeof story !== "object" || Array.isArray(story)) {
|
|
163
|
+
throw validationError(`${entry.label} must be an object.`);
|
|
164
|
+
}
|
|
165
|
+
const eventKey = String(story.eventKey || "").trim();
|
|
166
|
+
const event = eventByKey.get(eventKey);
|
|
167
|
+
if (!event) throw validationError(`${entry.label}.eventKey is not in the claim.`);
|
|
168
|
+
if (usedKeys.has(eventKey)) {
|
|
169
|
+
throw validationError(`${eventKey} is cited or covered more than once.`);
|
|
170
|
+
}
|
|
171
|
+
usedKeys.add(eventKey);
|
|
172
|
+
if (entry.lead && String(event.section || "") !== "front") {
|
|
173
|
+
throw validationError("The lead must cite a front-section event.");
|
|
174
|
+
}
|
|
175
|
+
requireEditorialText(story.headline, `${entry.label}.headline`);
|
|
176
|
+
requireEditorialText(story.summary, `${entry.label}.summary`);
|
|
177
|
+
const quote = typeof story.sourceQuote === "string" ? story.sourceQuote : "";
|
|
178
|
+
if (String(event.section || "") === "front") {
|
|
179
|
+
const canonicalSummary = String(event.summary || "");
|
|
180
|
+
const quoteIsValid = canonicalSummary
|
|
181
|
+
? Boolean(
|
|
182
|
+
quote &&
|
|
183
|
+
quote.length <= maximum &&
|
|
184
|
+
canonicalSummary.includes(quote),
|
|
185
|
+
)
|
|
186
|
+
: quote === "";
|
|
187
|
+
if (!quoteIsValid) {
|
|
188
|
+
throw validationError(
|
|
189
|
+
`${entry.label}.sourceQuote must be an exact contiguous claim-summary passage no longer than ${maximum} characters.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
} else if (quote !== "") {
|
|
193
|
+
throw validationError(`${entry.label}.sourceQuote must be empty outside the front section.`);
|
|
194
|
+
}
|
|
195
|
+
const covered = story.coveredEventKeys ?? [];
|
|
196
|
+
if (!Array.isArray(covered)) {
|
|
197
|
+
throw validationError(`${entry.label}.coveredEventKeys must be an array.`);
|
|
198
|
+
}
|
|
199
|
+
for (const rawCoveredKey of covered) {
|
|
200
|
+
const coveredKey = String(rawCoveredKey || "").trim();
|
|
201
|
+
if (!eventByKey.has(coveredKey)) {
|
|
202
|
+
throw validationError(`${entry.label} covers an eventKey not in the claim.`);
|
|
203
|
+
}
|
|
204
|
+
if (coveredKey === eventKey || usedKeys.has(coveredKey)) {
|
|
205
|
+
throw validationError(`${coveredKey} is cited or covered more than once.`);
|
|
206
|
+
}
|
|
207
|
+
usedKeys.add(coveredKey);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
valid: true,
|
|
212
|
+
editionId: claim.editionId,
|
|
213
|
+
citedEventCount: entries.length,
|
|
214
|
+
coveredEventCount: usedKeys.size - entries.length,
|
|
215
|
+
availableEventCount: claim.events.length,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function writeNewsClaimArtifacts({ result, outputPath, scaffoldPath }) {
|
|
220
|
+
const claim = result?.data?.claim || null;
|
|
221
|
+
const artifacts = { claimFile: null, scaffoldFile: null };
|
|
222
|
+
if (outputPath) {
|
|
223
|
+
artifacts.claimFile = writeAdminJsonFile(
|
|
224
|
+
outputPath,
|
|
225
|
+
{ schemaVersion: 1, savedAt: new Date().toISOString(), claim },
|
|
226
|
+
{ privateFile: true },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (scaffoldPath && claim) {
|
|
230
|
+
artifacts.scaffoldFile = writeAdminJsonFile(
|
|
231
|
+
scaffoldPath,
|
|
232
|
+
createNewsEditorialScaffold(claim),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return artifacts;
|
|
236
|
+
}
|