@stage5/lumine 0.2.39 → 0.2.41
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 +419 -0
- package/lib/admin.js +495 -44
- 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 +145 -11
- package/lib/constants.js +15 -0
- package/lib/forum.js +375 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +26 -3
- package/sdk/LUMINE_ADMIN.md +95 -20
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ npx @stage5/lumine@latest describe --no-description --target 123
|
|
|
14
14
|
npx @stage5/lumine@latest upgrade https://www.twin-kle.com/app/123
|
|
15
15
|
npx @stage5/lumine@latest projects
|
|
16
16
|
npx @stage5/lumine@latest branches 884
|
|
17
|
+
npx @stage5/lumine@latest forum 884 --json
|
|
18
|
+
npx @stage5/lumine@latest forum listen 884 --json
|
|
17
19
|
npx @stage5/lumine@latest suggestions 884
|
|
18
20
|
npx @stage5/lumine@latest explore --sort forks
|
|
19
21
|
npx @stage5/lumine@latest reference https://www.twin-kle.com/app/123
|
|
@@ -58,6 +60,16 @@ Use `lumine branches <build-url-or-id>` to list the contribution branches you
|
|
|
58
60
|
can review, including each contributor, branch number, status, and URL. Then use
|
|
59
61
|
`lumine diff <branch-url>` to inspect one branch.
|
|
60
62
|
|
|
63
|
+
Use `lumine forum [build-url-or-id]` to read the complete canonical Team Forum
|
|
64
|
+
history visible to the current workspace. A project owner reading Main receives
|
|
65
|
+
Main plus every branch's posts and replies. A branch contributor receives that
|
|
66
|
+
branch plus every project-owner post and reply on Main, including older Main
|
|
67
|
+
threads that were not separately broadcast. `--json` returns one complete
|
|
68
|
+
snapshot. `lumine forum listen --json` then polls from the last server-confirmed
|
|
69
|
+
sequence and emits newline-delimited update batches without advancing through a
|
|
70
|
+
partial or failed read. Pass `--cursor <sequence>` only when intentionally
|
|
71
|
+
resuming a previously confirmed cursor; the default starts at the beginning.
|
|
72
|
+
|
|
61
73
|
Branch contributors can nudge the project owner from their pulled branch with
|
|
62
74
|
`lumine suggest branch [message]` or `lumine suggest thumbnail`. The thumbnail
|
|
63
75
|
command offers the thumbnail currently saved on that branch. Project owners can
|
|
@@ -152,7 +164,35 @@ version state in `.twinkle/lumine-project.json` so local agents can tell when
|
|
|
152
164
|
they should rerun with `npx @stage5/lumine@latest`. Use `--no-update-check` to
|
|
153
165
|
skip that advisory network check.
|
|
154
166
|
|
|
155
|
-
|
|
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:
|
|
156
196
|
|
|
157
197
|
```bash
|
|
158
198
|
codex "Read AGENTS.md, then make the requested change."
|
|
@@ -175,10 +215,13 @@ events.
|
|
|
175
215
|
```bash
|
|
176
216
|
lumine admin identity list --json
|
|
177
217
|
lumine admin daily-run start --identity auto --comment-mode off --json
|
|
178
|
-
lumine admin recommendations list --
|
|
218
|
+
lumine admin recommendations list --all --checkpoint recommendations.json --json
|
|
219
|
+
lumine admin recommendations list --after 2026-08-14T00:00:00Z --all --json
|
|
220
|
+
lumine admin recommendations list --include-legacy --all --json
|
|
179
221
|
lumine admin subjects candidates --after 2026-08-01T00:00:00Z --json
|
|
180
222
|
lumine admin subjects candidates --effort unassigned --json
|
|
181
|
-
lumine admin builds candidates --
|
|
223
|
+
lumine admin builds candidates --all --json
|
|
224
|
+
lumine admin builds review build:884 --output-dir ./build-review --json
|
|
182
225
|
lumine admin subject get 123 --include-comments --json
|
|
183
226
|
lumine admin post get https://www.twin-kle.com/ai-stories/88 --json
|
|
184
227
|
lumine admin post comments dailyReflection:99 --json
|
|
@@ -194,9 +237,16 @@ lumine admin brief --days 3 --json
|
|
|
194
237
|
lumine admin notable add Minecrarft_guy --note "Created 8 thoughtful subjects and helped peers in 23 comments this window." --json
|
|
195
238
|
lumine admin post recommend comment:456 --anyone-can-reward --reward-twinkles 3 --json
|
|
196
239
|
lumine admin post reward comment:456 --twinkles 3 --json
|
|
240
|
+
lumine admin post skip-batch --target-file skipped.json --checkpoint skip-progress.json --json
|
|
197
241
|
lumine admin comment draft build:884 --file comment.md \
|
|
198
|
-
--
|
|
242
|
+
--review-receipt /path/from-build-review/review.json --json
|
|
199
243
|
lumine admin comment post --draft-id 77 --json
|
|
244
|
+
lumine admin news claim --output claim.json --scaffold editorial.json --json
|
|
245
|
+
lumine admin news validate --claim claim.json --file editorial.json --json
|
|
246
|
+
lumine admin news submit --claim claim.json --file editorial.json --json
|
|
247
|
+
lumine admin daily-run escalation add --target subject:123 \
|
|
248
|
+
--note "Concrete privacy issue requiring owner review" --json
|
|
249
|
+
lumine admin daily-run report --json
|
|
200
250
|
lumine admin daily-run complete --json
|
|
201
251
|
```
|
|
202
252
|
|
|
@@ -209,11 +259,15 @@ reply is handled by Twinkle's existing autonomous Zero/Ciel responder without
|
|
|
209
259
|
Lumine remaining active.
|
|
210
260
|
|
|
211
261
|
Management agents also inspect recent public Build candidates during each run.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
262
|
+
`builds review` opens one published app in an isolated temporary Chromium
|
|
263
|
+
profile, captures a screenshot and console evidence, verifies that the
|
|
264
|
+
published version stayed fixed, and writes a review receipt in a unique output
|
|
265
|
+
subdirectory. A direct Build comment is never server-generated: review the
|
|
266
|
+
runtime (or pull and read an
|
|
267
|
+
open-source app), compose with `--file`, and attach the receipt. The server
|
|
268
|
+
rejects missing or stale review evidence and any app/thread change before
|
|
269
|
+
publication. Manual `--reviewed-version` / `--reviewed-via` evidence remains
|
|
270
|
+
available for genuine code reviews.
|
|
217
271
|
|
|
218
272
|
Every operation is noninteractive when its required arguments are present.
|
|
219
273
|
`--json` prints exactly one uncolored JSON value and returns a nonzero status
|
|
@@ -223,10 +277,21 @@ server also enforces canonical no-duplicate invariants. Failed mutation JSON
|
|
|
223
277
|
includes `error.details.retryIdempotencyKey` so a partial attempt can be
|
|
224
278
|
resumed with the exact generated key.
|
|
225
279
|
|
|
226
|
-
Subject and
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
280
|
+
Subject and queue listings use opaque, stable snapshot cursors. `--all`
|
|
281
|
+
follows them automatically, saves a checkpoint after every canonical page,
|
|
282
|
+
and records completed queue coverage in the run audit; `--resume` continues
|
|
283
|
+
the exact same request. Recommendation scans default to the previous completed
|
|
284
|
+
run's start boundary for at-least-once coverage. Use `--after` for an explicit
|
|
285
|
+
timestamp or `--include-legacy`
|
|
286
|
+
for an intentional all-history scan. Subject `--after` is inclusive and
|
|
287
|
+
cursors are bound to the original date and effort filters.
|
|
288
|
+
|
|
289
|
+
`news claim` can write both the canonical leased digest and an editable
|
|
290
|
+
editorial scaffold. `news validate` is local and checks every citation and
|
|
291
|
+
quote before submission; `news submit --claim` reads the lease identity from
|
|
292
|
+
the claim file. `daily-run report` summarizes confirmed mutations, completed
|
|
293
|
+
queue coverage, explicitly recorded escalations, and the run brief before the
|
|
294
|
+
run is completed.
|
|
230
295
|
|
|
231
296
|
The complete run lifecycle, command contracts, nullable fields, Karma approval
|
|
232
297
|
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
|
+
}
|