@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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { executeExternalAgentTool } from "../api.js";
|
|
5
|
+
import {
|
|
6
|
+
collectWorkspaceTextFilesLenient,
|
|
7
|
+
resolveLocalProjectFilePath,
|
|
8
|
+
writeProjectFiles,
|
|
9
|
+
} from "../workspace.js";
|
|
10
|
+
import { appendAgentTrace } from "./trace.js";
|
|
11
|
+
|
|
12
|
+
const EXTERNAL_AGENT_MAX_TOOL_CALLS_PER_HOSTED_ROUND = 8;
|
|
13
|
+
|
|
14
|
+
export async function createExternalAgentToolSession({
|
|
15
|
+
options,
|
|
16
|
+
auth,
|
|
17
|
+
buildId,
|
|
18
|
+
baseFilesHash,
|
|
19
|
+
dir,
|
|
20
|
+
runtime,
|
|
21
|
+
traceFile,
|
|
22
|
+
provider,
|
|
23
|
+
initialFiles,
|
|
24
|
+
}) {
|
|
25
|
+
const projectFiles = Array.isArray(initialFiles)
|
|
26
|
+
? normalizeProjectFiles(initialFiles)
|
|
27
|
+
: normalizeProjectFiles(await collectWorkspaceTextFilesLenient(dir));
|
|
28
|
+
return new ExternalAgentToolSession({
|
|
29
|
+
options,
|
|
30
|
+
auth,
|
|
31
|
+
buildId,
|
|
32
|
+
baseFilesHash,
|
|
33
|
+
dir,
|
|
34
|
+
runtime,
|
|
35
|
+
traceFile,
|
|
36
|
+
provider,
|
|
37
|
+
projectFiles,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ExternalAgentToolSession {
|
|
42
|
+
constructor({
|
|
43
|
+
options,
|
|
44
|
+
auth,
|
|
45
|
+
buildId,
|
|
46
|
+
baseFilesHash,
|
|
47
|
+
dir,
|
|
48
|
+
runtime,
|
|
49
|
+
traceFile,
|
|
50
|
+
provider,
|
|
51
|
+
projectFiles,
|
|
52
|
+
}) {
|
|
53
|
+
this.options = options;
|
|
54
|
+
this.auth = auth;
|
|
55
|
+
this.buildId = buildId;
|
|
56
|
+
this.baseFilesHash = baseFilesHash;
|
|
57
|
+
this.dir = dir;
|
|
58
|
+
this.runtime = runtime;
|
|
59
|
+
this.traceFile = traceFile;
|
|
60
|
+
this.provider = provider;
|
|
61
|
+
this.projectFiles = projectFiles;
|
|
62
|
+
this.readPaths = [];
|
|
63
|
+
this.sequence = 0;
|
|
64
|
+
this.queue = Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
call(name, toolArguments) {
|
|
68
|
+
const pending = this.queue.then(() => this.#call(name, toolArguments));
|
|
69
|
+
this.queue = pending.catch(() => undefined);
|
|
70
|
+
return pending;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async #call(name, toolArguments) {
|
|
74
|
+
this.sequence += 1;
|
|
75
|
+
const sequence = this.sequence;
|
|
76
|
+
const maxToolRounds = Math.max(
|
|
77
|
+
1,
|
|
78
|
+
Number(this.runtime?.maxToolRounds || 16),
|
|
79
|
+
);
|
|
80
|
+
// Hosted Lumine limits provider response rounds, and one response may
|
|
81
|
+
// contain several tool calls. App-server/MCP adapters expose calls rather
|
|
82
|
+
// than provider-round boundaries, so keep an equivalent safety ceiling
|
|
83
|
+
// without incorrectly treating every call as a whole hosted round.
|
|
84
|
+
const maxToolCalls =
|
|
85
|
+
maxToolRounds * EXTERNAL_AGENT_MAX_TOOL_CALLS_PER_HOSTED_ROUND;
|
|
86
|
+
if (sequence > maxToolCalls) {
|
|
87
|
+
const output = {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: `Lumine stopped after ${maxToolCalls} tool calls in this pass. Finish with a short summary.`,
|
|
90
|
+
};
|
|
91
|
+
await appendAgentTrace(this.traceFile, {
|
|
92
|
+
type: "tool_result",
|
|
93
|
+
provider: this.provider,
|
|
94
|
+
phase: this.runtime?.phase || null,
|
|
95
|
+
sequence,
|
|
96
|
+
tool: String(name || ""),
|
|
97
|
+
durationMs: 0,
|
|
98
|
+
ok: false,
|
|
99
|
+
result: output.error,
|
|
100
|
+
});
|
|
101
|
+
return output;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const toolName = String(name || "").trim();
|
|
105
|
+
await appendAgentTrace(this.traceFile, {
|
|
106
|
+
type: "tool_call",
|
|
107
|
+
provider: this.provider,
|
|
108
|
+
phase: this.runtime?.phase || null,
|
|
109
|
+
sequence,
|
|
110
|
+
tool: toolName,
|
|
111
|
+
argumentKeys:
|
|
112
|
+
toolArguments && typeof toolArguments === "object"
|
|
113
|
+
? Object.keys(toolArguments).sort()
|
|
114
|
+
: [],
|
|
115
|
+
});
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
const previousFiles = this.projectFiles;
|
|
118
|
+
try {
|
|
119
|
+
const result = await executeExternalAgentTool({
|
|
120
|
+
options: this.options,
|
|
121
|
+
auth: this.auth,
|
|
122
|
+
buildId: this.buildId,
|
|
123
|
+
baseFilesHash: this.baseFilesHash,
|
|
124
|
+
projectFiles: previousFiles,
|
|
125
|
+
readPaths: this.readPaths,
|
|
126
|
+
name: toolName,
|
|
127
|
+
arguments: toolArguments,
|
|
128
|
+
});
|
|
129
|
+
const nextFiles = normalizeProjectFiles(result.projectFiles);
|
|
130
|
+
const changedPaths = listChangedProjectPaths(previousFiles, nextFiles);
|
|
131
|
+
if (changedPaths.length > 0) {
|
|
132
|
+
await replaceWorkspaceProjectFiles({
|
|
133
|
+
dir: this.dir,
|
|
134
|
+
previousFiles,
|
|
135
|
+
nextFiles,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
this.projectFiles = nextFiles;
|
|
139
|
+
this.readPaths = Array.isArray(result.readPaths)
|
|
140
|
+
? result.readPaths.map(String)
|
|
141
|
+
: this.readPaths;
|
|
142
|
+
const output = result.output ?? {};
|
|
143
|
+
const ok = output?.ok !== false && !output?.error;
|
|
144
|
+
await appendAgentTrace(this.traceFile, {
|
|
145
|
+
type: "tool_result",
|
|
146
|
+
provider: this.provider,
|
|
147
|
+
phase: this.runtime?.phase || null,
|
|
148
|
+
sequence,
|
|
149
|
+
tool: toolName,
|
|
150
|
+
durationMs: Date.now() - startedAt,
|
|
151
|
+
ok,
|
|
152
|
+
changedPaths,
|
|
153
|
+
result: summarizeToolResult(output),
|
|
154
|
+
});
|
|
155
|
+
return output;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await appendAgentTrace(this.traceFile, {
|
|
158
|
+
type: "tool_result",
|
|
159
|
+
provider: this.provider,
|
|
160
|
+
phase: this.runtime?.phase || null,
|
|
161
|
+
sequence,
|
|
162
|
+
tool: toolName,
|
|
163
|
+
durationMs: Date.now() - startedAt,
|
|
164
|
+
ok: false,
|
|
165
|
+
error: String(error?.message || error).slice(0, 500),
|
|
166
|
+
});
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function normalizeProjectFiles(files) {
|
|
173
|
+
const byPath = new Map();
|
|
174
|
+
for (const file of Array.isArray(files) ? files : []) {
|
|
175
|
+
const filePath = String(file?.path || "").trim();
|
|
176
|
+
if (!filePath || byPath.has(filePath)) continue;
|
|
177
|
+
byPath.set(filePath, String(file?.content || ""));
|
|
178
|
+
}
|
|
179
|
+
return Array.from(byPath, ([filePath, content]) => ({
|
|
180
|
+
path: filePath,
|
|
181
|
+
content,
|
|
182
|
+
})).sort((left, right) => left.path.localeCompare(right.path));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function listChangedProjectPaths(beforeFiles, afterFiles) {
|
|
186
|
+
const before = new Map(
|
|
187
|
+
normalizeProjectFiles(beforeFiles).map((file) => [file.path, file.content]),
|
|
188
|
+
);
|
|
189
|
+
const after = new Map(
|
|
190
|
+
normalizeProjectFiles(afterFiles).map((file) => [file.path, file.content]),
|
|
191
|
+
);
|
|
192
|
+
return Array.from(new Set([...before.keys(), ...after.keys()]))
|
|
193
|
+
.filter((filePath) => before.get(filePath) !== after.get(filePath))
|
|
194
|
+
.sort();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function replaceWorkspaceProjectFiles({
|
|
198
|
+
dir,
|
|
199
|
+
previousFiles,
|
|
200
|
+
nextFiles,
|
|
201
|
+
}) {
|
|
202
|
+
const rootDir = path.resolve(dir);
|
|
203
|
+
const nextPaths = new Set(nextFiles.map((file) => file.path));
|
|
204
|
+
const deletedPaths = previousFiles
|
|
205
|
+
.map((file) => file.path)
|
|
206
|
+
.filter((filePath) => !nextPaths.has(filePath))
|
|
207
|
+
.sort((left, right) => right.length - left.length);
|
|
208
|
+
|
|
209
|
+
for (const projectPath of new Set([
|
|
210
|
+
...deletedPaths,
|
|
211
|
+
...nextFiles.map((file) => file.path),
|
|
212
|
+
])) {
|
|
213
|
+
await assertProjectPathDoesNotTraverseSymlink({ rootDir, projectPath });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const projectPath of deletedPaths) {
|
|
217
|
+
const filePath = resolveLocalProjectFilePath({ rootDir, projectPath });
|
|
218
|
+
await fs.unlink(filePath).catch((error) => {
|
|
219
|
+
if (error.code !== "ENOENT") throw error;
|
|
220
|
+
});
|
|
221
|
+
await removeEmptyProjectParents({ rootDir, filePath: path.dirname(filePath) });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const file of nextFiles) {
|
|
225
|
+
const filePath = resolveLocalProjectFilePath({
|
|
226
|
+
rootDir,
|
|
227
|
+
projectPath: file.path,
|
|
228
|
+
});
|
|
229
|
+
const stat = await fs.lstat(filePath).catch((error) => {
|
|
230
|
+
if (error.code === "ENOENT") return null;
|
|
231
|
+
throw error;
|
|
232
|
+
});
|
|
233
|
+
if (stat?.isDirectory()) {
|
|
234
|
+
await fs.rmdir(filePath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
await writeProjectFiles({ dir: rootDir, files: nextFiles });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function assertProjectPathDoesNotTraverseSymlink({
|
|
241
|
+
rootDir,
|
|
242
|
+
projectPath,
|
|
243
|
+
}) {
|
|
244
|
+
let current = resolveLocalProjectFilePath({ rootDir, projectPath });
|
|
245
|
+
while (current !== rootDir && current.startsWith(`${rootDir}${path.sep}`)) {
|
|
246
|
+
const stat = await fs.lstat(current).catch((error) => {
|
|
247
|
+
if (error.code === "ENOENT") return null;
|
|
248
|
+
throw error;
|
|
249
|
+
});
|
|
250
|
+
if (stat?.isSymbolicLink()) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Refusing to write ${projectPath}: its local path traverses a symbolic link.`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
current = path.dirname(current);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function removeEmptyProjectParents({ rootDir, filePath }) {
|
|
260
|
+
let current = path.resolve(filePath);
|
|
261
|
+
const metadataRoot = path.join(rootDir, ".twinkle");
|
|
262
|
+
while (
|
|
263
|
+
current !== rootDir &&
|
|
264
|
+
current.startsWith(`${rootDir}${path.sep}`) &&
|
|
265
|
+
current !== metadataRoot
|
|
266
|
+
) {
|
|
267
|
+
try {
|
|
268
|
+
await fs.rmdir(current);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error.code === "ENOENT") {
|
|
271
|
+
current = path.dirname(current);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (error.code === "ENOTEMPTY" || error.code === "EEXIST") return;
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
current = path.dirname(current);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function summarizeToolResult(output) {
|
|
282
|
+
if (!output || typeof output !== "object") {
|
|
283
|
+
return String(output || "").slice(0, 500);
|
|
284
|
+
}
|
|
285
|
+
if (output.error) return String(output.error).slice(0, 500);
|
|
286
|
+
if (typeof output.message === "string") return output.message.slice(0, 500);
|
|
287
|
+
const summary = {};
|
|
288
|
+
for (const key of [
|
|
289
|
+
"ok",
|
|
290
|
+
"path",
|
|
291
|
+
"prefix",
|
|
292
|
+
"matchingPathCount",
|
|
293
|
+
"pathsIncluded",
|
|
294
|
+
"pathsOmitted",
|
|
295
|
+
"matchCount",
|
|
296
|
+
"filesRead",
|
|
297
|
+
]) {
|
|
298
|
+
if (output[key] !== undefined) summary[key] = output[key];
|
|
299
|
+
}
|
|
300
|
+
return Object.keys(summary).length > 0 ? summary : "completed";
|
|
301
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export async function createAgentRunArtifacts({
|
|
6
|
+
dir,
|
|
7
|
+
provider,
|
|
8
|
+
userMessage,
|
|
9
|
+
runtime,
|
|
10
|
+
}) {
|
|
11
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
12
|
+
const suffix = crypto.randomBytes(4).toString("hex");
|
|
13
|
+
const runId = `${timestamp}-${suffix}`;
|
|
14
|
+
const runDir = path.join(dir, ".twinkle", "agent-runs", runId);
|
|
15
|
+
const runtimeFile = path.join(runDir, "runtime.json");
|
|
16
|
+
const traceFile = path.join(runDir, "trace.jsonl");
|
|
17
|
+
const feedbackFile = path.join(runDir, "feedback.json");
|
|
18
|
+
await fs.mkdir(runDir, { recursive: true, mode: 0o700 });
|
|
19
|
+
await writeAgentRuntimeFile({
|
|
20
|
+
runtimeFile,
|
|
21
|
+
runtime,
|
|
22
|
+
runId,
|
|
23
|
+
provider,
|
|
24
|
+
userMessage,
|
|
25
|
+
});
|
|
26
|
+
await appendAgentTrace(traceFile, {
|
|
27
|
+
type: "run_started",
|
|
28
|
+
runId,
|
|
29
|
+
provider,
|
|
30
|
+
runtimeVersion: runtime?.version || null,
|
|
31
|
+
phase: runtime?.phase || "implementation",
|
|
32
|
+
at: new Date().toISOString(),
|
|
33
|
+
});
|
|
34
|
+
return { runId, runDir, runtimeFile, traceFile, feedbackFile };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function writeAgentRuntimeFile({
|
|
38
|
+
runtimeFile,
|
|
39
|
+
runtime,
|
|
40
|
+
runId,
|
|
41
|
+
provider,
|
|
42
|
+
userMessage,
|
|
43
|
+
}) {
|
|
44
|
+
await fs.writeFile(
|
|
45
|
+
runtimeFile,
|
|
46
|
+
JSON.stringify(
|
|
47
|
+
{
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
runId,
|
|
50
|
+
provider,
|
|
51
|
+
userMessage,
|
|
52
|
+
runtime,
|
|
53
|
+
},
|
|
54
|
+
null,
|
|
55
|
+
2,
|
|
56
|
+
),
|
|
57
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
58
|
+
);
|
|
59
|
+
await fs.chmod(runtimeFile, 0o600);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function readAgentRuntimeFile(runtimeFile) {
|
|
63
|
+
const parsed = JSON.parse(await fs.readFile(runtimeFile, "utf8"));
|
|
64
|
+
if (!parsed?.runtime || !parsed?.runId) {
|
|
65
|
+
throw new Error("Invalid Lumine external-agent runtime file.");
|
|
66
|
+
}
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function appendAgentTrace(traceFile, event) {
|
|
71
|
+
const normalized = {
|
|
72
|
+
...event,
|
|
73
|
+
at: event?.at || new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
await fs.appendFile(traceFile, `${JSON.stringify(normalized)}\n`, {
|
|
76
|
+
encoding: "utf8",
|
|
77
|
+
mode: 0o600,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function readAgentTrace(traceFile) {
|
|
82
|
+
const text = await fs.readFile(traceFile, "utf8").catch((error) => {
|
|
83
|
+
if (error.code === "ENOENT") return "";
|
|
84
|
+
throw error;
|
|
85
|
+
});
|
|
86
|
+
return text
|
|
87
|
+
.split("\n")
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.map((line) => {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(line);
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function summarizeAgentTraceForReview(events) {
|
|
100
|
+
return (Array.isArray(events) ? events : [])
|
|
101
|
+
.filter((event) =>
|
|
102
|
+
[
|
|
103
|
+
"run_started",
|
|
104
|
+
"provider_completed",
|
|
105
|
+
"tool_call",
|
|
106
|
+
"tool_result",
|
|
107
|
+
"validation_failed",
|
|
108
|
+
"validation_passed",
|
|
109
|
+
"save_completed",
|
|
110
|
+
"run_failed",
|
|
111
|
+
].includes(event?.type),
|
|
112
|
+
)
|
|
113
|
+
.map((event) => {
|
|
114
|
+
const summary = {
|
|
115
|
+
type: event.type,
|
|
116
|
+
phase: event.phase || null,
|
|
117
|
+
sequence: event.sequence || null,
|
|
118
|
+
tool: event.tool || null,
|
|
119
|
+
durationMs: Number(event.durationMs || 0) || null,
|
|
120
|
+
ok: typeof event.ok === "boolean" ? event.ok : null,
|
|
121
|
+
changedPaths: Array.isArray(event.changedPaths)
|
|
122
|
+
? event.changedPaths.slice(0, 20)
|
|
123
|
+
: [],
|
|
124
|
+
result: event.result || null,
|
|
125
|
+
reason: event.reason || null,
|
|
126
|
+
error: event.error || null,
|
|
127
|
+
};
|
|
128
|
+
return summary;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function writeAgentFeedback(feedbackFile, feedback) {
|
|
133
|
+
await fs.writeFile(feedbackFile, `${JSON.stringify(feedback, null, 2)}\n`, {
|
|
134
|
+
encoding: "utf8",
|
|
135
|
+
mode: 0o600,
|
|
136
|
+
});
|
|
137
|
+
await fs.chmod(feedbackFile, 0o600);
|
|
138
|
+
}
|