@bbigbang/agent-node 0.1.0
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/dist/agentHost.js +483 -0
- package/dist/appVersion.js +14 -0
- package/dist/assetCachePaths.js +35 -0
- package/dist/attachmentInput.js +588 -0
- package/dist/attachmentMaterializer.js +230 -0
- package/dist/bigbangCli.js +17 -0
- package/dist/bigbangMessageSendDetection.js +284 -0
- package/dist/builtinSkillRoots.js +54 -0
- package/dist/claudeConfig.js +32 -0
- package/dist/claudeDirectRuntime.js +1960 -0
- package/dist/claudeSessionControls.js +78 -0
- package/dist/claudeTranscriptFs.js +147 -0
- package/dist/codexAppServerClient.js +188 -0
- package/dist/codexAppServerEnv.js +14 -0
- package/dist/codexAppServerRpc.js +273 -0
- package/dist/codexAppServerRuntime.js +3495 -0
- package/dist/codexBuiltinPrompt.js +117 -0
- package/dist/codexConversationSummarizer.js +76 -0
- package/dist/codexTranscriptFs.js +145 -0
- package/dist/config.js +129 -0
- package/dist/connection.js +151 -0
- package/dist/dispatchQueueStore.js +39 -0
- package/dist/dreamEnv.js +1 -0
- package/dist/dreamMemoryFallback.js +118 -0
- package/dist/dreamToolPolicy.js +293 -0
- package/dist/droidMissionRunner.js +808 -0
- package/dist/executor.js +1078 -0
- package/dist/hostRuntime.js +1 -0
- package/dist/libraryAuthorityFs.js +74 -0
- package/dist/libraryMirror.js +183 -0
- package/dist/main.js +1659 -0
- package/dist/native-worker/native-worker.mjs +475 -0
- package/dist/nativeMissionAgentDispatch.js +463 -0
- package/dist/nativeMissionRunner.js +461 -0
- package/dist/nativeSkillMounts.js +204 -0
- package/dist/nativeWorkerHost.js +142 -0
- package/dist/nodeSink.js +142 -0
- package/dist/panelHttpFetch.js +334 -0
- package/dist/runtimeDrivers.js +62 -0
- package/dist/skillFs.js +229 -0
- package/dist/soloHost.js +165 -0
- package/dist/soloNodeSink.js +138 -0
- package/dist/terminalManager.js +254 -0
- package/dist/workspaceFs.js +1020 -0
- package/dist/workspaceGit.js +694 -0
- package/dist/workspaceInspect.js +22 -0
- package/package.json +49 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { log } from '@bbigbang/runtime-acp';
|
|
6
|
+
import { NativeWorkerHost } from './nativeWorkerHost.js';
|
|
7
|
+
import { NativeMissionAgentDispatch, } from './nativeMissionAgentDispatch.js';
|
|
8
|
+
import { resolveExistingMissionDir, resolveMissionDir } from './droidMissionRunner.js';
|
|
9
|
+
const OUTPUT_LIMIT = 8_000;
|
|
10
|
+
const NATIVE_OUTPUT_DIR = 'native-output';
|
|
11
|
+
export function runNativeMission(msg, send, hooks, executor) {
|
|
12
|
+
if (!msg.featureId) {
|
|
13
|
+
handleNativeMissionHandshake(msg, send);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
runNativeFeatureWorker(msg, send, hooks, executor);
|
|
17
|
+
}
|
|
18
|
+
function handleNativeMissionHandshake(msg, send) {
|
|
19
|
+
const workspaceRoot = resolveExistingDirectory(msg.workspaceRoot);
|
|
20
|
+
const missionDir = resolveExistingMissionDir(workspaceRoot, msg.missionDir);
|
|
21
|
+
send({
|
|
22
|
+
type: 'mission.run.accepted',
|
|
23
|
+
requestId: msg.requestId,
|
|
24
|
+
missionId: msg.missionId,
|
|
25
|
+
missionDir,
|
|
26
|
+
runtimeProvider: 'bigbang_native',
|
|
27
|
+
pid: null,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function runNativeFeatureWorker(msg, send, hooks, executor) {
|
|
31
|
+
const featureId = msg.featureId;
|
|
32
|
+
if (!msg.featureDescription) {
|
|
33
|
+
throw new Error('Native mission run request is missing featureDescription.');
|
|
34
|
+
}
|
|
35
|
+
if (!msg.milestone) {
|
|
36
|
+
throw new Error('Native mission run request is missing milestone.');
|
|
37
|
+
}
|
|
38
|
+
const workspaceRoot = resolveExistingDirectory(msg.workspaceRoot);
|
|
39
|
+
const missionDir = resolveMissionDir(workspaceRoot, msg.missionDir);
|
|
40
|
+
const outputDir = path.join(missionDir, NATIVE_OUTPUT_DIR);
|
|
41
|
+
mkdirSync(outputDir, { recursive: true });
|
|
42
|
+
const workerScriptPath = resolveNativeWorkerScriptPath();
|
|
43
|
+
const context = {
|
|
44
|
+
featureId,
|
|
45
|
+
featureDescription: msg.featureDescription,
|
|
46
|
+
milestone: msg.milestone,
|
|
47
|
+
expectedBehavior: msg.expectedBehavior,
|
|
48
|
+
title: msg.title,
|
|
49
|
+
prompt: msg.prompt,
|
|
50
|
+
missionId: msg.missionId,
|
|
51
|
+
workspaceRoot,
|
|
52
|
+
missionDir,
|
|
53
|
+
modelMode: msg.modelMode,
|
|
54
|
+
orchestratorModel: msg.orchestratorModel,
|
|
55
|
+
workerModel: msg.workerModel,
|
|
56
|
+
validatorModel: msg.validatorModel,
|
|
57
|
+
priorHandoffs: msg.priorHandoffs,
|
|
58
|
+
preconditions: msg.preconditions,
|
|
59
|
+
validationContract: msg.validationContract,
|
|
60
|
+
workspaceFiles: msg.workspaceFiles,
|
|
61
|
+
useAgentDispatch: msg.useAgentDispatch,
|
|
62
|
+
};
|
|
63
|
+
if (msg.useAgentDispatch && executor) {
|
|
64
|
+
log.info('[agent-node] dispatching native feature via platform agent', {
|
|
65
|
+
requestId: msg.requestId,
|
|
66
|
+
missionId: msg.missionId,
|
|
67
|
+
featureId,
|
|
68
|
+
modelMode: msg.modelMode,
|
|
69
|
+
workerModel: msg.workerModel,
|
|
70
|
+
});
|
|
71
|
+
const dispatch = new NativeMissionAgentDispatch(context, send, executor, msg.requestId);
|
|
72
|
+
hooks?.onDispatchCreated?.(dispatch);
|
|
73
|
+
void dispatch
|
|
74
|
+
.dispatch()
|
|
75
|
+
.catch((error) => {
|
|
76
|
+
log.warn('[agent-node] native agent dispatch failed', {
|
|
77
|
+
requestId: msg.requestId,
|
|
78
|
+
missionId: msg.missionId,
|
|
79
|
+
featureId,
|
|
80
|
+
error: String(error?.message ?? error),
|
|
81
|
+
});
|
|
82
|
+
})
|
|
83
|
+
.finally(() => {
|
|
84
|
+
hooks?.onDispatchFinished?.(dispatch);
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const runtimeConfigForLog = {
|
|
89
|
+
modelMode: msg.modelMode,
|
|
90
|
+
orchestratorModel: msg.orchestratorModel ?? null,
|
|
91
|
+
workerModel: msg.workerModel ?? null,
|
|
92
|
+
validatorModel: msg.validatorModel ?? null,
|
|
93
|
+
providerBaseUrl: redactUrlCredentials(process.env.OPENAI_API_BASE ?? 'https://api.openai.com/v1'),
|
|
94
|
+
apiKeyConfigured: Boolean(process.env.OPENAI_API_KEY),
|
|
95
|
+
workerTimeoutMs: Number(process.env.NATIVE_WORKER_TIMEOUT_MS ?? 300_000),
|
|
96
|
+
workerGracePeriodMs: Number(process.env.NATIVE_WORKER_GRACE_PERIOD_MS ?? 5_000),
|
|
97
|
+
};
|
|
98
|
+
log.info('[agent-node] spawning native worker', {
|
|
99
|
+
requestId: msg.requestId,
|
|
100
|
+
missionId: msg.missionId,
|
|
101
|
+
featureId,
|
|
102
|
+
workerScriptPath,
|
|
103
|
+
...runtimeConfigForLog,
|
|
104
|
+
});
|
|
105
|
+
const host = new NativeWorkerHost({
|
|
106
|
+
command: process.execPath,
|
|
107
|
+
args: [workerScriptPath],
|
|
108
|
+
cwd: missionDir,
|
|
109
|
+
env: {
|
|
110
|
+
...process.env,
|
|
111
|
+
NATIVE_WORKER_CONTEXT: JSON.stringify(context),
|
|
112
|
+
NATIVE_WORKER_FEATURE_ID: featureId,
|
|
113
|
+
NATIVE_WORKER_MISSION_DIR: missionDir,
|
|
114
|
+
NATIVE_WORKER_WORKSPACE_ROOT: workspaceRoot,
|
|
115
|
+
},
|
|
116
|
+
timeoutMs: Number(process.env.NATIVE_WORKER_TIMEOUT_MS ?? 300_000),
|
|
117
|
+
gracePeriodMs: Number(process.env.NATIVE_WORKER_GRACE_PERIOD_MS ?? 5_000),
|
|
118
|
+
onStdout: (text) => {
|
|
119
|
+
sendOutputEvent(send, msg.requestId, msg.missionId, featureId, 'stdout', text);
|
|
120
|
+
},
|
|
121
|
+
onStderr: (text) => {
|
|
122
|
+
sendOutputEvent(send, msg.requestId, msg.missionId, featureId, 'stderr', text);
|
|
123
|
+
},
|
|
124
|
+
onExit: (result) => {
|
|
125
|
+
hooks?.onHostExit?.();
|
|
126
|
+
const validation = readNativeWorkerOutput(outputDir, featureId);
|
|
127
|
+
const output = validation.output;
|
|
128
|
+
const outputError = validation.ok ? undefined : validation.error;
|
|
129
|
+
const outputReason = validation.ok ? undefined : validation.reason;
|
|
130
|
+
const outputFailed = output.status === 'failed' || outputError || Boolean(result.error);
|
|
131
|
+
const endExitCode = result.exitCode || (outputFailed ? 1 : 0);
|
|
132
|
+
const endError = result.error ?? outputError;
|
|
133
|
+
// A crash (segfault, OOM, external kill) is indicated by a non-null
|
|
134
|
+
// signal and no host-initiated timeout/cancel error. Host-initiated
|
|
135
|
+
// kills already carry their own descriptive error.
|
|
136
|
+
const crashed = result.signal != null && !result.error;
|
|
137
|
+
const reason = crashed ? 'process_crashed' : outputReason;
|
|
138
|
+
const endMsg = {
|
|
139
|
+
type: 'mission.run.end',
|
|
140
|
+
requestId: msg.requestId,
|
|
141
|
+
missionId: msg.missionId,
|
|
142
|
+
missionDir,
|
|
143
|
+
runtimeProvider: 'bigbang_native',
|
|
144
|
+
featureId,
|
|
145
|
+
exitCode: endExitCode,
|
|
146
|
+
signal: result.signal,
|
|
147
|
+
reason,
|
|
148
|
+
output,
|
|
149
|
+
handoff: output.handoff,
|
|
150
|
+
...(endError ? { error: endError } : {}),
|
|
151
|
+
};
|
|
152
|
+
send(endMsg);
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
hooks?.onHostCreated?.(host);
|
|
156
|
+
host.spawn();
|
|
157
|
+
send({
|
|
158
|
+
type: 'mission.run.accepted',
|
|
159
|
+
requestId: msg.requestId,
|
|
160
|
+
missionId: msg.missionId,
|
|
161
|
+
missionDir,
|
|
162
|
+
runtimeProvider: 'bigbang_native',
|
|
163
|
+
featureId,
|
|
164
|
+
pid: host.pid,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function resolveExistingDirectory(value) {
|
|
168
|
+
const resolved = path.resolve(value);
|
|
169
|
+
if (!existsSync(resolved)) {
|
|
170
|
+
throw new Error(`Directory does not exist: ${value}`);
|
|
171
|
+
}
|
|
172
|
+
return realpathSync(resolved);
|
|
173
|
+
}
|
|
174
|
+
function resolveNativeWorkerScriptPath() {
|
|
175
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
176
|
+
const candidates = [
|
|
177
|
+
path.join(moduleDir, 'native-worker', 'native-worker.mjs'),
|
|
178
|
+
path.join(moduleDir, 'native-worker', 'native-worker.js'),
|
|
179
|
+
];
|
|
180
|
+
for (const candidate of candidates) {
|
|
181
|
+
if (existsSync(candidate))
|
|
182
|
+
return candidate;
|
|
183
|
+
}
|
|
184
|
+
throw new Error('Native worker script not found.');
|
|
185
|
+
}
|
|
186
|
+
function redactUrlCredentials(value) {
|
|
187
|
+
if (!value)
|
|
188
|
+
return '';
|
|
189
|
+
try {
|
|
190
|
+
const url = new URL(value);
|
|
191
|
+
url.username = '';
|
|
192
|
+
url.password = '';
|
|
193
|
+
return url.toString().replace(/\/$/, '');
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function buildDiscoveredIssues(messages) {
|
|
200
|
+
return messages.map((description) => ({
|
|
201
|
+
id: randomUUID(),
|
|
202
|
+
severity: 'non-blocking',
|
|
203
|
+
description,
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
function buildFailedHandoff(summary, issueDescriptions) {
|
|
207
|
+
return {
|
|
208
|
+
schemaVersion: '2.0.0',
|
|
209
|
+
salientSummary: summary,
|
|
210
|
+
whatWasImplemented: [],
|
|
211
|
+
whatWasLeftUndone: '',
|
|
212
|
+
verification: { commandsRun: [], interactiveChecks: [] },
|
|
213
|
+
tests: { added: [] },
|
|
214
|
+
discoveredIssues: buildDiscoveredIssues(issueDescriptions),
|
|
215
|
+
successState: 'failure',
|
|
216
|
+
returnToOrchestrator: true,
|
|
217
|
+
workerSessionId: 'unknown',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function isStringArray(value) {
|
|
221
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
222
|
+
}
|
|
223
|
+
function isCommandRunEntry(value) {
|
|
224
|
+
if (!isRecord(value))
|
|
225
|
+
return false;
|
|
226
|
+
return (typeof value.command === 'string' &&
|
|
227
|
+
typeof value.exitCode === 'number' &&
|
|
228
|
+
typeof value.observation === 'string');
|
|
229
|
+
}
|
|
230
|
+
function isDiscoveredIssue(value) {
|
|
231
|
+
if (!isRecord(value))
|
|
232
|
+
return false;
|
|
233
|
+
return (typeof value.id === 'string' &&
|
|
234
|
+
(value.severity === 'blocking' || value.severity === 'non-blocking') &&
|
|
235
|
+
typeof value.description === 'string' &&
|
|
236
|
+
(value.suggestion === undefined || typeof value.suggestion === 'string'));
|
|
237
|
+
}
|
|
238
|
+
function validateNativeMissionHandoff(value) {
|
|
239
|
+
if (!isRecord(value))
|
|
240
|
+
return null;
|
|
241
|
+
if (value.schemaVersion !== '2.0.0')
|
|
242
|
+
return null;
|
|
243
|
+
if (typeof value.salientSummary !== 'string')
|
|
244
|
+
return null;
|
|
245
|
+
if (!isStringArray(value.whatWasImplemented))
|
|
246
|
+
return null;
|
|
247
|
+
if (typeof value.whatWasLeftUndone !== 'string')
|
|
248
|
+
return null;
|
|
249
|
+
const verification = value.verification;
|
|
250
|
+
if (!isRecord(verification))
|
|
251
|
+
return null;
|
|
252
|
+
const commandsRun = verification.commandsRun;
|
|
253
|
+
const interactiveChecks = verification.interactiveChecks;
|
|
254
|
+
if (!Array.isArray(commandsRun) || !commandsRun.every(isCommandRunEntry))
|
|
255
|
+
return null;
|
|
256
|
+
if (!Array.isArray(interactiveChecks))
|
|
257
|
+
return null;
|
|
258
|
+
if (interactiveChecks.some((item) => !isRecord(item) || typeof item.action !== 'string' || typeof item.observed !== 'string')) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const tests = value.tests;
|
|
262
|
+
if (!isRecord(tests))
|
|
263
|
+
return null;
|
|
264
|
+
const added = tests.added;
|
|
265
|
+
if (!Array.isArray(added))
|
|
266
|
+
return null;
|
|
267
|
+
for (const entry of added) {
|
|
268
|
+
if (!isRecord(entry))
|
|
269
|
+
return null;
|
|
270
|
+
if (typeof entry.file !== 'string')
|
|
271
|
+
return null;
|
|
272
|
+
if (!Array.isArray(entry.cases))
|
|
273
|
+
return null;
|
|
274
|
+
for (const c of entry.cases) {
|
|
275
|
+
if (!isRecord(c) || typeof c.name !== 'string' || typeof c.description !== 'string')
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const discoveredIssues = value.discoveredIssues;
|
|
280
|
+
if (!Array.isArray(discoveredIssues) || !discoveredIssues.every(isDiscoveredIssue))
|
|
281
|
+
return null;
|
|
282
|
+
if (value.successState !== 'success' &&
|
|
283
|
+
value.successState !== 'partial' &&
|
|
284
|
+
value.successState !== 'failure') {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
if (typeof value.returnToOrchestrator !== 'boolean')
|
|
288
|
+
return null;
|
|
289
|
+
if (typeof value.workerSessionId !== 'string')
|
|
290
|
+
return null;
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
const VALID_OUTPUT_STATUSES = ['completed', 'failed', 'skipped'];
|
|
294
|
+
function readNativeWorkerOutput(outputDir, featureId) {
|
|
295
|
+
const outputPath = path.join(outputDir, `${featureId}.json`);
|
|
296
|
+
if (!existsSync(outputPath)) {
|
|
297
|
+
const error = `Native worker did not write output file: ${outputPath}`;
|
|
298
|
+
const output = {
|
|
299
|
+
schemaVersion: 'unknown',
|
|
300
|
+
featureId,
|
|
301
|
+
status: 'failed',
|
|
302
|
+
exitCode: 1,
|
|
303
|
+
implementedFiles: [],
|
|
304
|
+
fileOperations: [],
|
|
305
|
+
runtimeConfig: buildPlaceholderRuntimeConfig(),
|
|
306
|
+
handoff: buildFailedHandoff('Native worker did not produce a structured output file.', [
|
|
307
|
+
`Output file not found: ${outputPath}`,
|
|
308
|
+
]),
|
|
309
|
+
error,
|
|
310
|
+
};
|
|
311
|
+
return { ok: false, output, error, reason: 'invalid_output' };
|
|
312
|
+
}
|
|
313
|
+
let raw;
|
|
314
|
+
try {
|
|
315
|
+
raw = readFileSync(outputPath, 'utf8');
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
const error = String(err?.message ?? err);
|
|
319
|
+
const output = {
|
|
320
|
+
schemaVersion: 'unknown',
|
|
321
|
+
featureId,
|
|
322
|
+
status: 'failed',
|
|
323
|
+
exitCode: 1,
|
|
324
|
+
implementedFiles: [],
|
|
325
|
+
fileOperations: [],
|
|
326
|
+
runtimeConfig: buildPlaceholderRuntimeConfig(),
|
|
327
|
+
handoff: buildFailedHandoff('Native worker output file could not be read.', [error]),
|
|
328
|
+
error,
|
|
329
|
+
};
|
|
330
|
+
return { ok: false, output, error };
|
|
331
|
+
}
|
|
332
|
+
let parsed;
|
|
333
|
+
try {
|
|
334
|
+
parsed = JSON.parse(raw);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
const error = `Invalid worker output JSON: ${String(err?.message ?? err)}`;
|
|
338
|
+
const output = {
|
|
339
|
+
schemaVersion: 'unknown',
|
|
340
|
+
featureId,
|
|
341
|
+
status: 'failed',
|
|
342
|
+
exitCode: 1,
|
|
343
|
+
implementedFiles: [],
|
|
344
|
+
fileOperations: [],
|
|
345
|
+
runtimeConfig: buildPlaceholderRuntimeConfig(),
|
|
346
|
+
handoff: buildFailedHandoff('Native worker output could not be parsed as JSON.', [
|
|
347
|
+
String(err?.message ?? err),
|
|
348
|
+
]),
|
|
349
|
+
stdout: raw.slice(0, OUTPUT_LIMIT),
|
|
350
|
+
rawOutput: raw,
|
|
351
|
+
error,
|
|
352
|
+
};
|
|
353
|
+
return { ok: false, output, error, reason: 'invalid_output' };
|
|
354
|
+
}
|
|
355
|
+
if (!isRecord(parsed) || typeof parsed.featureId !== 'string' || parsed.featureId.trim() !== featureId) {
|
|
356
|
+
const error = 'Worker output featureId mismatch or missing';
|
|
357
|
+
const output = {
|
|
358
|
+
schemaVersion: 'unknown',
|
|
359
|
+
featureId,
|
|
360
|
+
status: 'failed',
|
|
361
|
+
exitCode: 1,
|
|
362
|
+
implementedFiles: [],
|
|
363
|
+
fileOperations: [],
|
|
364
|
+
runtimeConfig: buildPlaceholderRuntimeConfig(),
|
|
365
|
+
handoff: buildFailedHandoff('Native worker output has an invalid or missing featureId.', [
|
|
366
|
+
`Expected featureId "${featureId}", got ${isRecord(parsed) ? JSON.stringify(parsed.featureId) : typeof parsed}`,
|
|
367
|
+
]),
|
|
368
|
+
stdout: raw.slice(0, OUTPUT_LIMIT),
|
|
369
|
+
rawOutput: raw,
|
|
370
|
+
error,
|
|
371
|
+
};
|
|
372
|
+
return { ok: false, output, error, reason: 'invalid_output' };
|
|
373
|
+
}
|
|
374
|
+
const status = typeof parsed.status === 'string' ? parsed.status : undefined;
|
|
375
|
+
const statusValid = status !== undefined && VALID_OUTPUT_STATUSES.includes(status);
|
|
376
|
+
const implementedFiles = Array.isArray(parsed.implementedFiles) && parsed.implementedFiles.every((item) => typeof item === 'string')
|
|
377
|
+
? parsed.implementedFiles
|
|
378
|
+
: undefined;
|
|
379
|
+
const handoff = validateNativeMissionHandoff(parsed.handoff);
|
|
380
|
+
const fileOperations = Array.isArray(parsed.fileOperations) ? parsed.fileOperations : undefined;
|
|
381
|
+
const runtimeConfig = isRecord(parsed.runtimeConfig)
|
|
382
|
+
? parsed.runtimeConfig
|
|
383
|
+
: undefined;
|
|
384
|
+
const missingFields = [];
|
|
385
|
+
if (!statusValid)
|
|
386
|
+
missingFields.push('status');
|
|
387
|
+
if (implementedFiles === undefined)
|
|
388
|
+
missingFields.push('implementedFiles');
|
|
389
|
+
if (handoff === null)
|
|
390
|
+
missingFields.push('handoff');
|
|
391
|
+
if (missingFields.length > 0) {
|
|
392
|
+
const error = `Worker output missing required fields: ${missingFields.join(', ')}`;
|
|
393
|
+
const output = {
|
|
394
|
+
schemaVersion: typeof parsed.schemaVersion === 'string' ? parsed.schemaVersion : 'unknown',
|
|
395
|
+
featureId,
|
|
396
|
+
status: 'failed',
|
|
397
|
+
exitCode: typeof parsed.exitCode === 'number' ? parsed.exitCode : 1,
|
|
398
|
+
implementedFiles: implementedFiles ?? [],
|
|
399
|
+
fileOperations: fileOperations ?? [],
|
|
400
|
+
runtimeConfig: runtimeConfig ?? buildPlaceholderRuntimeConfig(),
|
|
401
|
+
handoff: handoff ??
|
|
402
|
+
buildFailedHandoff('Native worker output is missing required fields.', [
|
|
403
|
+
`Missing or invalid fields: ${missingFields.join(', ')}`,
|
|
404
|
+
]),
|
|
405
|
+
stdout: typeof parsed.stdout === 'string' ? parsed.stdout : raw.slice(0, OUTPUT_LIMIT),
|
|
406
|
+
rawOutput: raw,
|
|
407
|
+
error,
|
|
408
|
+
};
|
|
409
|
+
return { ok: false, output, error, reason: 'invalid_output' };
|
|
410
|
+
}
|
|
411
|
+
const output = {
|
|
412
|
+
schemaVersion: typeof parsed.schemaVersion === 'string' ? parsed.schemaVersion : 'unknown',
|
|
413
|
+
featureId,
|
|
414
|
+
status: status,
|
|
415
|
+
implementedFiles: implementedFiles,
|
|
416
|
+
fileOperations: fileOperations ?? [],
|
|
417
|
+
runtimeConfig: runtimeConfig ?? buildPlaceholderRuntimeConfig(),
|
|
418
|
+
handoff: handoff,
|
|
419
|
+
stdout: typeof parsed.stdout === 'string' ? parsed.stdout : undefined,
|
|
420
|
+
stderr: typeof parsed.stderr === 'string' ? parsed.stderr : undefined,
|
|
421
|
+
exitCode: typeof parsed.exitCode === 'number' ? parsed.exitCode : 0,
|
|
422
|
+
errorClassification: isRecord(parsed.errorClassification)
|
|
423
|
+
? parsed.errorClassification
|
|
424
|
+
: undefined,
|
|
425
|
+
};
|
|
426
|
+
return { ok: true, output };
|
|
427
|
+
}
|
|
428
|
+
function buildPlaceholderRuntimeConfig() {
|
|
429
|
+
return {
|
|
430
|
+
model: 'unknown',
|
|
431
|
+
modelMode: 'unknown',
|
|
432
|
+
orchestratorModel: null,
|
|
433
|
+
workerModel: null,
|
|
434
|
+
validatorModel: null,
|
|
435
|
+
providerBaseUrl: 'unknown',
|
|
436
|
+
apiKeyConfigured: false,
|
|
437
|
+
workerTimeoutMs: 0,
|
|
438
|
+
workerGracePeriodMs: 0,
|
|
439
|
+
platform: process.platform,
|
|
440
|
+
nodeVersion: process.version,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function sendOutputEvent(send, requestId, missionId, featureId, stream, text) {
|
|
444
|
+
send({
|
|
445
|
+
type: 'mission.run.event',
|
|
446
|
+
requestId,
|
|
447
|
+
missionId,
|
|
448
|
+
runtimeProvider: 'bigbang_native',
|
|
449
|
+
eventType: `mission_${stream}`,
|
|
450
|
+
source: 'bigbang_native',
|
|
451
|
+
eventTime: Date.now(),
|
|
452
|
+
featureId,
|
|
453
|
+
payload: {
|
|
454
|
+
stream,
|
|
455
|
+
text: text.length > OUTPUT_LIMIT ? `${text.slice(0, OUTPUT_LIMIT)}\n[truncated]` : text,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function isRecord(value) {
|
|
460
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
461
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { BUILTIN_LIBRARY_DOCUMENTS_SKILL_ROOT_SENTINEL, BUILTIN_UI_PANEL_SKILL_ROOT_SENTINEL, BUILTIN_WORKSPACE_TOOL_SKILL_ROOT_SENTINEL, } from '@bbigbang/protocol';
|
|
4
|
+
import { expandSkillRoot } from './builtinSkillRoots.js';
|
|
5
|
+
const MANIFEST_FILENAME = '.bigbang-managed-links.json';
|
|
6
|
+
const BUILTIN_SKILL_ROOT_SENTINELS = new Map([
|
|
7
|
+
[BUILTIN_LIBRARY_DOCUMENTS_SKILL_ROOT_SENTINEL, 'library-documents'],
|
|
8
|
+
[BUILTIN_UI_PANEL_SKILL_ROOT_SENTINEL, 'ui-panel'],
|
|
9
|
+
[BUILTIN_WORKSPACE_TOOL_SKILL_ROOT_SENTINEL, 'workspace-tool-builder'],
|
|
10
|
+
]);
|
|
11
|
+
export function ensureNativeSkillMounts(params) {
|
|
12
|
+
const mountRoot = resolveMountRoot(params);
|
|
13
|
+
if (!mountRoot)
|
|
14
|
+
return;
|
|
15
|
+
const normalizedEnabledSkillPaths = normalizeEnabledSkillPaths(params.enabledSkillPaths ?? []);
|
|
16
|
+
const normalizedRoots = normalizeRoots(params.skillRoots ?? [], normalizedEnabledSkillPaths.length > 0);
|
|
17
|
+
fs.mkdirSync(mountRoot, { recursive: true });
|
|
18
|
+
const manifestPath = path.join(mountRoot, MANIFEST_FILENAME);
|
|
19
|
+
const previousEntries = readManifestEntries(manifestPath);
|
|
20
|
+
for (const entryName of previousEntries) {
|
|
21
|
+
removeManagedEntry(path.join(mountRoot, entryName));
|
|
22
|
+
}
|
|
23
|
+
const managedEntries = [];
|
|
24
|
+
for (const candidate of collectSkillCandidates(normalizedRoots, normalizedEnabledSkillPaths)) {
|
|
25
|
+
const destinationPath = path.join(mountRoot, candidate.name);
|
|
26
|
+
if (fs.existsSync(destinationPath)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
fs.symlinkSync(candidate.targetPath, destinationPath, 'dir');
|
|
30
|
+
managedEntries.push(candidate.name);
|
|
31
|
+
}
|
|
32
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify({ entries: managedEntries }, null, 2)}\n`, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
function resolveMountRoot(params) {
|
|
35
|
+
if (params.agentType === 'codex_acp') {
|
|
36
|
+
return path.join(path.resolve(params.workspaceRoot), '.agents', 'skills');
|
|
37
|
+
}
|
|
38
|
+
if (params.agentType === 'codex_app_server') {
|
|
39
|
+
return path.join(path.resolve(params.workspaceRoot), '.agents', 'skills');
|
|
40
|
+
}
|
|
41
|
+
if (params.agentType === 'claude_acp' || params.agentType === 'claude_sdk') {
|
|
42
|
+
return path.join(path.resolve(params.workspaceRoot), '.claude', 'skills');
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function normalizeRoots(skillRoots, explicitSelectionMode) {
|
|
47
|
+
const roots = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
for (const rawValue of skillRoots) {
|
|
50
|
+
const trimmed = rawValue.trim();
|
|
51
|
+
if (!trimmed)
|
|
52
|
+
continue;
|
|
53
|
+
const managedSkillName = BUILTIN_SKILL_ROOT_SENTINELS.get(trimmed) ?? null;
|
|
54
|
+
const rootPath = expandSkillRoot(trimmed);
|
|
55
|
+
if (seen.has(rootPath))
|
|
56
|
+
continue;
|
|
57
|
+
if (!(fs.statSync(rootPath, { throwIfNoEntry: false })?.isDirectory() ?? false))
|
|
58
|
+
continue;
|
|
59
|
+
const managedSkillPath = managedSkillName ? path.join(rootPath, managedSkillName) : null;
|
|
60
|
+
const managedSkillRealPath = managedSkillPath && fs.statSync(managedSkillPath, { throwIfNoEntry: false })?.isDirectory()
|
|
61
|
+
? fs.realpathSync(managedSkillPath)
|
|
62
|
+
: null;
|
|
63
|
+
roots.push({
|
|
64
|
+
path: rootPath,
|
|
65
|
+
realPath: fs.realpathSync(rootPath),
|
|
66
|
+
autoMount: !explicitSelectionMode || Boolean(managedSkillName),
|
|
67
|
+
managedSkillName,
|
|
68
|
+
managedSkillRealPath,
|
|
69
|
+
});
|
|
70
|
+
seen.add(rootPath);
|
|
71
|
+
}
|
|
72
|
+
return roots;
|
|
73
|
+
}
|
|
74
|
+
function normalizeEnabledSkillPaths(enabledSkillPaths) {
|
|
75
|
+
return enabledSkillPaths
|
|
76
|
+
.map((value) => path.resolve(value.trim()))
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.filter((value, index, list) => list.indexOf(value) === index)
|
|
79
|
+
.filter((value) => path.basename(value) === 'SKILL.md');
|
|
80
|
+
}
|
|
81
|
+
function collectSkillCandidates(skillRoots, enabledSkillPaths) {
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
const results = [];
|
|
84
|
+
for (const root of skillRoots) {
|
|
85
|
+
if (!root.autoMount)
|
|
86
|
+
continue;
|
|
87
|
+
const entries = safeReadDir(root);
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (root.managedSkillName && entry.name !== root.managedSkillName)
|
|
90
|
+
continue;
|
|
91
|
+
if (!entry.isDirectory() && !(root.managedSkillName && entry.isSymbolicLink()))
|
|
92
|
+
continue;
|
|
93
|
+
if (seen.has(entry.name))
|
|
94
|
+
continue;
|
|
95
|
+
const targetPath = path.join(root.path, entry.name);
|
|
96
|
+
const skillFile = path.join(targetPath, 'SKILL.md');
|
|
97
|
+
if (!isMountableSkillFile(skillFile, root))
|
|
98
|
+
continue;
|
|
99
|
+
seen.add(entry.name);
|
|
100
|
+
results.push({ name: entry.name, targetPath });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const skillPath of enabledSkillPaths) {
|
|
104
|
+
if (!isMountableExplicitSkillFile(skillPath, skillRoots))
|
|
105
|
+
continue;
|
|
106
|
+
const targetPath = path.dirname(skillPath);
|
|
107
|
+
const name = path.basename(targetPath);
|
|
108
|
+
if (!name || seen.has(name))
|
|
109
|
+
continue;
|
|
110
|
+
seen.add(name);
|
|
111
|
+
results.push({ name, targetPath });
|
|
112
|
+
}
|
|
113
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
114
|
+
}
|
|
115
|
+
function safeReadDir(root) {
|
|
116
|
+
try {
|
|
117
|
+
return fs.readdirSync(root.path, { withFileTypes: true });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function isMountableSkillFile(skillPath, root) {
|
|
124
|
+
const stat = fs.lstatSync(skillPath, { throwIfNoEntry: false });
|
|
125
|
+
if (!stat)
|
|
126
|
+
return false;
|
|
127
|
+
if (!root.managedSkillName && stat.isSymbolicLink())
|
|
128
|
+
return false;
|
|
129
|
+
if (!(fs.statSync(skillPath, { throwIfNoEntry: false })?.isFile() ?? false))
|
|
130
|
+
return false;
|
|
131
|
+
if (root.managedSkillName) {
|
|
132
|
+
if (!root.managedSkillRealPath)
|
|
133
|
+
return false;
|
|
134
|
+
const skillDirectoryRealPath = fs.realpathSync(path.dirname(skillPath));
|
|
135
|
+
return skillDirectoryRealPath === root.managedSkillRealPath;
|
|
136
|
+
}
|
|
137
|
+
const realPath = fs.realpathSync(skillPath);
|
|
138
|
+
const relative = path.relative(root.realPath, realPath);
|
|
139
|
+
return relative === 'SKILL.md' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
140
|
+
}
|
|
141
|
+
function isMountableExplicitSkillFile(skillPath, roots) {
|
|
142
|
+
const stat = fs.lstatSync(skillPath, { throwIfNoEntry: false });
|
|
143
|
+
if (!stat || stat.isSymbolicLink())
|
|
144
|
+
return false;
|
|
145
|
+
if (!(fs.statSync(skillPath, { throwIfNoEntry: false })?.isFile() ?? false))
|
|
146
|
+
return false;
|
|
147
|
+
const realPath = fs.realpathSync(skillPath);
|
|
148
|
+
if (roots.length === 0) {
|
|
149
|
+
return !pathHasSymlinkAncestor(skillPath);
|
|
150
|
+
}
|
|
151
|
+
return roots.some((root) => {
|
|
152
|
+
const relative = path.relative(root.realPath, realPath);
|
|
153
|
+
return (relative === 'SKILL.md' || (!relative.startsWith('..') && !path.isAbsolute(relative)))
|
|
154
|
+
&& !pathHasSymlinkDescendant(root.path, skillPath);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function pathHasSymlinkAncestor(filePath) {
|
|
158
|
+
const parsed = path.parse(filePath);
|
|
159
|
+
let current = parsed.root;
|
|
160
|
+
const parts = path.relative(parsed.root, filePath).split(path.sep).filter(Boolean);
|
|
161
|
+
for (const part of parts) {
|
|
162
|
+
current = path.join(current, part);
|
|
163
|
+
const stat = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
164
|
+
if (stat?.isSymbolicLink())
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
function pathHasSymlinkDescendant(rootPath, filePath) {
|
|
170
|
+
const relative = path.relative(rootPath, filePath);
|
|
171
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
172
|
+
return false;
|
|
173
|
+
let current = rootPath;
|
|
174
|
+
const parts = relative.split(path.sep).filter(Boolean);
|
|
175
|
+
for (const part of parts.slice(0, -1)) {
|
|
176
|
+
current = path.join(current, part);
|
|
177
|
+
const stat = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
178
|
+
if (stat?.isSymbolicLink())
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
function readManifestEntries(manifestPath) {
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
186
|
+
if (!Array.isArray(parsed.entries))
|
|
187
|
+
return [];
|
|
188
|
+
return parsed.entries.filter((value) => typeof value === 'string');
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function removeManagedEntry(entryPath) {
|
|
195
|
+
try {
|
|
196
|
+
const stat = fs.lstatSync(entryPath);
|
|
197
|
+
if (stat.isSymbolicLink()) {
|
|
198
|
+
fs.unlinkSync(entryPath);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Ignore stale manifest entries and continue rebuilding managed links.
|
|
203
|
+
}
|
|
204
|
+
}
|