@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.
Files changed (47) hide show
  1. package/dist/agentHost.js +483 -0
  2. package/dist/appVersion.js +14 -0
  3. package/dist/assetCachePaths.js +35 -0
  4. package/dist/attachmentInput.js +588 -0
  5. package/dist/attachmentMaterializer.js +230 -0
  6. package/dist/bigbangCli.js +17 -0
  7. package/dist/bigbangMessageSendDetection.js +284 -0
  8. package/dist/builtinSkillRoots.js +54 -0
  9. package/dist/claudeConfig.js +32 -0
  10. package/dist/claudeDirectRuntime.js +1960 -0
  11. package/dist/claudeSessionControls.js +78 -0
  12. package/dist/claudeTranscriptFs.js +147 -0
  13. package/dist/codexAppServerClient.js +188 -0
  14. package/dist/codexAppServerEnv.js +14 -0
  15. package/dist/codexAppServerRpc.js +273 -0
  16. package/dist/codexAppServerRuntime.js +3495 -0
  17. package/dist/codexBuiltinPrompt.js +117 -0
  18. package/dist/codexConversationSummarizer.js +76 -0
  19. package/dist/codexTranscriptFs.js +145 -0
  20. package/dist/config.js +129 -0
  21. package/dist/connection.js +151 -0
  22. package/dist/dispatchQueueStore.js +39 -0
  23. package/dist/dreamEnv.js +1 -0
  24. package/dist/dreamMemoryFallback.js +118 -0
  25. package/dist/dreamToolPolicy.js +293 -0
  26. package/dist/droidMissionRunner.js +808 -0
  27. package/dist/executor.js +1078 -0
  28. package/dist/hostRuntime.js +1 -0
  29. package/dist/libraryAuthorityFs.js +74 -0
  30. package/dist/libraryMirror.js +183 -0
  31. package/dist/main.js +1659 -0
  32. package/dist/native-worker/native-worker.mjs +475 -0
  33. package/dist/nativeMissionAgentDispatch.js +463 -0
  34. package/dist/nativeMissionRunner.js +461 -0
  35. package/dist/nativeSkillMounts.js +204 -0
  36. package/dist/nativeWorkerHost.js +142 -0
  37. package/dist/nodeSink.js +142 -0
  38. package/dist/panelHttpFetch.js +334 -0
  39. package/dist/runtimeDrivers.js +62 -0
  40. package/dist/skillFs.js +229 -0
  41. package/dist/soloHost.js +165 -0
  42. package/dist/soloNodeSink.js +138 -0
  43. package/dist/terminalManager.js +254 -0
  44. package/dist/workspaceFs.js +1020 -0
  45. package/dist/workspaceGit.js +694 -0
  46. package/dist/workspaceInspect.js +22 -0
  47. package/package.json +49 -0
@@ -0,0 +1,808 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import path from 'node:path';
5
+ const OUTPUT_LIMIT = 8_000;
6
+ const TRANSCRIPT_LIMIT = 40_000;
7
+ const ARTIFACT_POLL_MS = 1_000;
8
+ const FOLLOWUP_MAILBOX_RELATIVE_PATH = path.join('.bigbang', 'mission-followups.jsonl');
9
+ export function runDroidMission(msg, send) {
10
+ const workspaceRoot = resolveExistingDirectory(msg.workspaceRoot);
11
+ const missionDir = msg.interaction || msg.continueSession
12
+ ? resolveExistingMissionDir(workspaceRoot, msg.missionDir)
13
+ : resolveMissionDir(workspaceRoot, msg.missionDir);
14
+ if (msg.interaction) {
15
+ deliverMissionInteraction(msg, missionDir, send);
16
+ return;
17
+ }
18
+ const scaffold = msg.continueSession ? null : createDroidMissionScaffold(missionDir, {
19
+ title: msg.title,
20
+ prompt: msg.prompt,
21
+ runtimeContext: resolveMissionRuntimeContext(),
22
+ });
23
+ ensureMissionFollowupMailbox(missionDir);
24
+ const args = buildDroidArgs({ ...msg, missionDir });
25
+ const child = spawn('droid', args, {
26
+ cwd: missionDir,
27
+ env: process.env,
28
+ stdio: ['ignore', 'pipe', 'pipe'],
29
+ });
30
+ let ended = false;
31
+ const artifactTailer = new MissionArtifactTailer({
32
+ missionId: msg.missionId,
33
+ requestId: msg.requestId,
34
+ missionDir,
35
+ send,
36
+ });
37
+ let artifactInterval = null;
38
+ send({
39
+ type: 'mission.run.accepted',
40
+ requestId: msg.requestId,
41
+ missionId: msg.missionId,
42
+ missionDir,
43
+ pid: child.pid ?? null,
44
+ });
45
+ if (scaffold) {
46
+ send({
47
+ type: 'mission.run.event',
48
+ requestId: msg.requestId,
49
+ missionId: msg.missionId,
50
+ eventType: 'mission_scaffold_prepared',
51
+ source: 'agent_node',
52
+ eventTime: scaffold.preparedAt,
53
+ payload: scaffold,
54
+ });
55
+ }
56
+ artifactInterval = setInterval(() => {
57
+ artifactTailer.poll();
58
+ }, ARTIFACT_POLL_MS);
59
+ artifactTailer.poll();
60
+ child.stdout.on('data', (chunk) => {
61
+ sendOutputEvent(send, msg.requestId, msg.missionId, 'stdout', chunk);
62
+ });
63
+ child.stderr.on('data', (chunk) => {
64
+ sendOutputEvent(send, msg.requestId, msg.missionId, 'stderr', chunk);
65
+ });
66
+ child.on('error', (error) => {
67
+ if (ended)
68
+ return;
69
+ ended = true;
70
+ if (artifactInterval)
71
+ clearInterval(artifactInterval);
72
+ artifactTailer.poll();
73
+ send({
74
+ type: 'mission.run.end',
75
+ requestId: msg.requestId,
76
+ missionId: msg.missionId,
77
+ missionDir,
78
+ ...findLatestFactoryArtifactContext(missionDir),
79
+ error: String(error.message ?? error),
80
+ });
81
+ });
82
+ child.on('close', (exitCode, signal) => {
83
+ if (ended)
84
+ return;
85
+ ended = true;
86
+ if (artifactInterval)
87
+ clearInterval(artifactInterval);
88
+ artifactTailer.poll();
89
+ send({
90
+ type: 'mission.run.end',
91
+ requestId: msg.requestId,
92
+ missionId: msg.missionId,
93
+ missionDir,
94
+ ...findLatestFactoryArtifactContext(missionDir),
95
+ exitCode,
96
+ signal,
97
+ ...(exitCode && exitCode !== 0 ? { error: `droid exited with code ${exitCode}` } : {}),
98
+ });
99
+ });
100
+ }
101
+ export function inspectDroidMissionContinuation(msg) {
102
+ try {
103
+ const workspaceRoot = resolveExistingDirectory(msg.workspaceRoot);
104
+ const missionDir = resolveExistingMissionDir(workspaceRoot, msg.missionDir);
105
+ const rootFactorySessionId = findLatestRootFactorySessionId(missionDir);
106
+ const context = findLatestFactoryArtifactContext(missionDir);
107
+ return {
108
+ type: 'mission.continuation.inspect.response',
109
+ requestId: msg.requestId,
110
+ missionId: msg.missionId,
111
+ missionDir,
112
+ rootFactorySessionId,
113
+ factorySessionId: context.factorySessionId ?? null,
114
+ factoryArtifactDir: context.factoryArtifactDir ?? null,
115
+ error: rootFactorySessionId ? null : 'No prior root Droid session found for this mission directory.',
116
+ };
117
+ }
118
+ catch (error) {
119
+ return {
120
+ type: 'mission.continuation.inspect.response',
121
+ requestId: msg.requestId,
122
+ missionId: msg.missionId,
123
+ missionDir: null,
124
+ rootFactorySessionId: null,
125
+ factorySessionId: null,
126
+ factoryArtifactDir: null,
127
+ error: String(error?.message ?? error),
128
+ };
129
+ }
130
+ }
131
+ function deliverMissionInteraction(msg, missionDir, send) {
132
+ const deliveredAt = Date.now();
133
+ try {
134
+ const mailboxPath = appendMissionFollowup(missionDir, {
135
+ requestId: msg.requestId,
136
+ missionId: msg.missionId,
137
+ prompt: msg.prompt,
138
+ deliveredAt,
139
+ });
140
+ send({
141
+ type: 'mission.run.accepted',
142
+ requestId: msg.requestId,
143
+ missionId: msg.missionId,
144
+ missionDir,
145
+ pid: null,
146
+ });
147
+ send({
148
+ type: 'mission.run.event',
149
+ requestId: msg.requestId,
150
+ missionId: msg.missionId,
151
+ eventType: 'mission_interaction_delivered',
152
+ source: 'agent_node',
153
+ eventTime: deliveredAt,
154
+ payload: {
155
+ mailboxPath,
156
+ deliveredAt,
157
+ },
158
+ });
159
+ send({
160
+ type: 'mission.run.end',
161
+ requestId: msg.requestId,
162
+ missionId: msg.missionId,
163
+ missionDir,
164
+ ...findLatestFactoryArtifactContext(missionDir),
165
+ exitCode: 0,
166
+ signal: null,
167
+ });
168
+ }
169
+ catch (error) {
170
+ send({
171
+ type: 'mission.run.end',
172
+ requestId: msg.requestId,
173
+ missionId: msg.missionId,
174
+ missionDir,
175
+ error: String(error?.message ?? error),
176
+ });
177
+ }
178
+ }
179
+ export class MissionArtifactTailer {
180
+ options;
181
+ files = new Map();
182
+ announcedArtifactDirs = new Set();
183
+ constructor(options) {
184
+ this.options = options;
185
+ }
186
+ poll() {
187
+ const dirs = [this.options.missionDir];
188
+ const context = findLatestFactoryArtifactContext(this.options.missionDir);
189
+ const sessionRoot = findFactorySessionRoot(this.options.missionDir);
190
+ if (sessionRoot) {
191
+ this.tailSessionJsonl(sessionRoot);
192
+ }
193
+ if (context.factoryArtifactDir) {
194
+ dirs.push(context.factoryArtifactDir);
195
+ if (!this.announcedArtifactDirs.has(context.factoryArtifactDir)) {
196
+ this.announcedArtifactDirs.add(context.factoryArtifactDir);
197
+ this.sendEvent('mission_artifact_discovered', 'droid_artifact', Date.now(), {
198
+ factorySessionId: context.factorySessionId ?? null,
199
+ factoryArtifactDir: context.factoryArtifactDir,
200
+ });
201
+ }
202
+ }
203
+ for (const dir of uniqueStrings(dirs)) {
204
+ this.tailJsonl(path.join(dir, 'progress_log.jsonl'), (record, lineNumber) => {
205
+ const payload = buildProgressPayload(dir, lineNumber, record);
206
+ this.sendEvent('mission_progress', 'droid_artifact', payload.eventTime, payload);
207
+ });
208
+ this.tailJsonl(path.join(dir, 'worker-transcripts.jsonl'), (record, lineNumber) => {
209
+ const payload = buildTranscriptPayload(dir, lineNumber, record);
210
+ this.sendEvent('mission_worker_transcript', 'droid_artifact', payload.eventTime, payload);
211
+ });
212
+ }
213
+ }
214
+ tailSessionJsonl(sessionRoot) {
215
+ let entries;
216
+ try {
217
+ entries = readdirSync(sessionRoot).filter((entry) => entry.endsWith('.jsonl'));
218
+ }
219
+ catch {
220
+ return;
221
+ }
222
+ for (const entry of entries) {
223
+ const filePath = path.join(sessionRoot, entry);
224
+ const sessionId = entry.slice(0, -'.jsonl'.length);
225
+ this.tailJsonl(filePath, (record, lineNumber) => {
226
+ const payload = buildSessionPayload(sessionRoot, filePath, sessionId, lineNumber, record);
227
+ this.sendEvent('mission_session_event', 'droid_session', payload.eventTime, payload);
228
+ });
229
+ }
230
+ }
231
+ tailJsonl(filePath, onRecord) {
232
+ if (!existsSync(filePath))
233
+ return;
234
+ let stat;
235
+ try {
236
+ stat = statSync(filePath);
237
+ }
238
+ catch {
239
+ return;
240
+ }
241
+ const state = this.files.get(filePath) ?? { offset: 0, buffered: '', lineNumber: 0 };
242
+ if (stat.size < state.offset) {
243
+ state.offset = 0;
244
+ state.buffered = '';
245
+ state.lineNumber = 0;
246
+ }
247
+ if (stat.size === state.offset) {
248
+ this.files.set(filePath, state);
249
+ return;
250
+ }
251
+ let text;
252
+ try {
253
+ text = readFileSync(filePath).subarray(state.offset).toString('utf8');
254
+ }
255
+ catch {
256
+ this.files.set(filePath, state);
257
+ return;
258
+ }
259
+ state.offset = stat.size;
260
+ const combined = `${state.buffered}${text}`;
261
+ const lines = combined.split(/\r?\n/);
262
+ state.buffered = combined.endsWith('\n') || combined.endsWith('\r') ? '' : lines.pop() ?? '';
263
+ for (const line of lines) {
264
+ if (!line.trim())
265
+ continue;
266
+ state.lineNumber += 1;
267
+ try {
268
+ const parsed = JSON.parse(line);
269
+ if (isRecord(parsed))
270
+ onRecord(redactValue(parsed), state.lineNumber);
271
+ }
272
+ catch {
273
+ this.sendEvent('mission_artifact_parse_error', 'droid_artifact', Date.now(), {
274
+ artifactPath: filePath,
275
+ lineNumber: state.lineNumber,
276
+ error: 'Invalid JSONL record',
277
+ });
278
+ }
279
+ }
280
+ this.files.set(filePath, state);
281
+ }
282
+ sendEvent(eventType, source, eventTime, payload) {
283
+ this.options.send({
284
+ type: 'mission.run.event',
285
+ requestId: this.options.requestId,
286
+ missionId: this.options.missionId,
287
+ eventType,
288
+ source,
289
+ eventTime,
290
+ payload,
291
+ });
292
+ }
293
+ }
294
+ export function buildDroidArgs(msg) {
295
+ const args = ['exec'];
296
+ if (msg.continueSession) {
297
+ const sessionId = findLatestRootFactorySessionId(msg.missionDir);
298
+ if (!sessionId) {
299
+ throw new Error('No prior root Droid session found for this mission directory.');
300
+ }
301
+ args.push('--auto', 'high');
302
+ args.push('--session-id', sessionId);
303
+ appendModelArg(args, '--model', msg.orchestratorModel);
304
+ args.push(withMissionRuntimeContextInstructions(msg.prompt, resolveMissionRuntimeContext()));
305
+ return args;
306
+ }
307
+ args.push('--mission', '--auto', 'high');
308
+ if (msg.modelMode === 'platform_override') {
309
+ appendModelArg(args, '--model', msg.orchestratorModel);
310
+ appendModelArg(args, '--worker-model', msg.workerModel);
311
+ appendModelArg(args, '--validator-model', msg.validatorModel);
312
+ }
313
+ args.push(withMissionFollowupInstructions(msg.prompt, msg.missionDir, resolveMissionRuntimeContext()));
314
+ return args;
315
+ }
316
+ export function missionFollowupMailboxPath(missionDir) {
317
+ return path.join(missionDir, FOLLOWUP_MAILBOX_RELATIVE_PATH);
318
+ }
319
+ export function ensureMissionFollowupMailbox(missionDir) {
320
+ const mailboxPath = missionFollowupMailboxPath(missionDir);
321
+ mkdirSync(path.dirname(mailboxPath), { recursive: true });
322
+ return mailboxPath;
323
+ }
324
+ export function createDroidMissionScaffold(missionDir, input) {
325
+ const created = [];
326
+ const existing = [];
327
+ const conflicts = [];
328
+ const writeMissing = (relativePath, content, options) => {
329
+ const filePath = path.join(missionDir, relativePath);
330
+ const parentPath = path.dirname(filePath);
331
+ const parentRelativePath = path.relative(missionDir, parentPath) || '.';
332
+ if (!ensureScaffoldDirectory(parentPath, parentRelativePath, conflicts))
333
+ return;
334
+ if (existsSync(filePath)) {
335
+ try {
336
+ if (!statSync(filePath).isFile()) {
337
+ conflicts.push(relativePath);
338
+ return;
339
+ }
340
+ }
341
+ catch {
342
+ conflicts.push(relativePath);
343
+ return;
344
+ }
345
+ existing.push(relativePath);
346
+ return;
347
+ }
348
+ writeFileSync(filePath, content, { encoding: 'utf8', mode: options?.executable ? 0o755 : 0o644 });
349
+ if (options?.executable)
350
+ chmodSync(filePath, 0o755);
351
+ created.push(relativePath);
352
+ };
353
+ const title = oneLine(input.title, 160) || 'Droid mission';
354
+ const promptPreview = truncateText(input.prompt, 2_000).text;
355
+ const runtimeContext = input.runtimeContext ?? resolveMissionRuntimeContext();
356
+ writeMissing('AGENTS.md', `# Mission Guidance
357
+
358
+ Mission: ${title}
359
+
360
+ Use this directory as the working directory for Droid mission artifacts. Preserve user files, write durable outputs as ordinary files, and keep final reports readable as Markdown when possible.
361
+
362
+ ## Bigbang Runtime Context
363
+
364
+ Use these current platform endpoints for this Bigbang stack:
365
+
366
+ - Web UI base URL: ${runtimeContext.webUrl ?? 'Not configured'}
367
+ - Core API base URL: ${runtimeContext.coreUrl ?? 'Not configured'}
368
+
369
+ Do not switch to another Bigbang deployment or tmux stack unless the mission prompt explicitly asks for it.
370
+
371
+ Original request preview:
372
+
373
+ ${promptPreview}
374
+ `);
375
+ writeMissing('features.json', `${JSON.stringify({
376
+ features: [],
377
+ note: 'Bigbang scaffold placeholder. Droid may replace or extend this file during mission execution.',
378
+ }, null, 2)}\n`);
379
+ writeMissing('validation-contract.md', `# Validation Contract
380
+
381
+ Use this file to record validation expectations for the mission.
382
+
383
+ - Confirm implemented changes satisfy the mission request.
384
+ - Record smoke checks, commands, or manual verification steps.
385
+ - Preserve failing assertions and follow-up work instead of deleting them.
386
+ `);
387
+ writeMissing('services.yaml', `# Optional service declarations for this Droid mission.
388
+ # Add project-specific services here if workers need them during execution.
389
+ services: []
390
+ `);
391
+ writeMissing('init.sh', `#!/usr/bin/env bash
392
+ set -euo pipefail
393
+
394
+ # Optional mission initialization hook.
395
+ `, { executable: true });
396
+ writeMissing(path.join('skills', 'README.md'), `# Mission Skills
397
+
398
+ Add mission-specific Droid skills in this directory when the task needs reusable worker instructions.
399
+ `);
400
+ return {
401
+ preparedAt: Date.now(),
402
+ created,
403
+ existing,
404
+ conflicts,
405
+ missionDir,
406
+ };
407
+ }
408
+ function ensureScaffoldDirectory(dirPath, relativePath, conflicts) {
409
+ if (!existsSync(dirPath)) {
410
+ mkdirSync(dirPath, { recursive: true });
411
+ return true;
412
+ }
413
+ try {
414
+ if (statSync(dirPath).isDirectory())
415
+ return true;
416
+ }
417
+ catch {
418
+ // Fall through and record a conflict.
419
+ }
420
+ conflicts.push(relativePath);
421
+ return false;
422
+ }
423
+ export function appendMissionFollowup(missionDir, input) {
424
+ const mailboxPath = ensureMissionFollowupMailbox(missionDir);
425
+ const runtimeContext = input.runtimeContext ?? resolveMissionRuntimeContext();
426
+ appendFileSync(mailboxPath, `${JSON.stringify({
427
+ type: 'platform_followup',
428
+ requestId: input.requestId,
429
+ missionId: input.missionId,
430
+ deliveredAt: input.deliveredAt,
431
+ prompt: withMissionRuntimeContextInstructions(input.prompt, runtimeContext),
432
+ runtimeContext,
433
+ })}\n`);
434
+ return mailboxPath;
435
+ }
436
+ export function withMissionFollowupInstructions(prompt, missionDir, runtimeContext = resolveMissionRuntimeContext()) {
437
+ return withMissionRuntimeInstructions(prompt, missionDir, runtimeContext);
438
+ }
439
+ export function withMissionRuntimeInstructions(prompt, missionDir, runtimeContext) {
440
+ const mailboxPath = missionFollowupMailboxPath(missionDir);
441
+ return `${withMissionRuntimeContextInstructions(prompt, runtimeContext)}
442
+
443
+ ---
444
+
445
+ Bigbang live interaction mailbox:
446
+ - During this mission, the platform may append user follow-up instructions to this JSONL file:
447
+ ${mailboxPath}
448
+ - Check this file after each major phase, after any long wait, and immediately before your final report.
449
+ - If it contains entries with prompts you have not handled, incorporate the latest follow-up into your next action or final report and explicitly mention that you handled it.
450
+ - Reading this file is safe and expected. Do not modify or delete it.`;
451
+ }
452
+ export function withMissionRuntimeContextInstructions(prompt, runtimeContext) {
453
+ return `${prompt}
454
+
455
+ ---
456
+
457
+ Bigbang runtime context:
458
+ - Web UI base URL: ${runtimeContext.webUrl ?? 'Not configured'}
459
+ - Core API base URL: ${runtimeContext.coreUrl ?? 'Not configured'}
460
+ - Use these endpoints for this mission's UI/API checks. Do not switch to another Bigbang deployment or tmux stack unless the mission prompt explicitly asks for it.`;
461
+ }
462
+ export function resolveMissionRuntimeContext(env = process.env) {
463
+ return {
464
+ webUrl: normalizeRuntimeUrl(env.BIGBANG_MISSION_WEB_URL
465
+ ?? env.BIGBANG_WEB_URL
466
+ ?? env.BIGBANG_BASE_URL
467
+ ?? null),
468
+ coreUrl: normalizeRuntimeUrl(env.BIGBANG_MISSION_CORE_URL
469
+ ?? env.BIGBANG_CORE_URL
470
+ ?? env.CORE_HTTP_URL
471
+ ?? httpUrlFromWsUrl(env.CORE_URL)
472
+ ?? null),
473
+ };
474
+ }
475
+ function normalizeRuntimeUrl(value) {
476
+ const trimmed = value?.trim();
477
+ if (!trimmed)
478
+ return null;
479
+ try {
480
+ const url = new URL(trimmed);
481
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
482
+ return null;
483
+ url.username = '';
484
+ url.password = '';
485
+ url.search = '';
486
+ url.hash = '';
487
+ return url.toString().replace(/\/+$/, '');
488
+ }
489
+ catch {
490
+ return null;
491
+ }
492
+ }
493
+ function httpUrlFromWsUrl(value) {
494
+ const trimmed = value?.trim();
495
+ if (!trimmed)
496
+ return null;
497
+ if (trimmed.startsWith('ws://'))
498
+ return `http://${trimmed.slice('ws://'.length)}`;
499
+ if (trimmed.startsWith('wss://'))
500
+ return `https://${trimmed.slice('wss://'.length)}`;
501
+ return trimmed.startsWith('http://') || trimmed.startsWith('https://') ? trimmed : null;
502
+ }
503
+ function appendModelArg(args, flag, value) {
504
+ if (typeof value === 'string' && value.trim()) {
505
+ args.push(flag, value.trim());
506
+ }
507
+ }
508
+ function sendOutputEvent(send, requestId, missionId, stream, chunk) {
509
+ const text = String(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk);
510
+ send({
511
+ type: 'mission.run.event',
512
+ requestId,
513
+ missionId,
514
+ eventType: `mission_${stream}`,
515
+ source: 'droid_cli',
516
+ eventTime: Date.now(),
517
+ payload: {
518
+ stream,
519
+ text: text.length > OUTPUT_LIMIT ? `${text.slice(0, OUTPUT_LIMIT)}\n[truncated]` : text,
520
+ },
521
+ });
522
+ }
523
+ function resolveExistingDirectory(value) {
524
+ return realpathSync(path.resolve(value));
525
+ }
526
+ export function resolveMissionDir(workspaceRoot, missionDir) {
527
+ const resolved = path.isAbsolute(missionDir) ? path.resolve(missionDir) : path.resolve(workspaceRoot, missionDir);
528
+ const relative = path.relative(workspaceRoot, resolved);
529
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
530
+ throw new Error('Mission dir must be inside the workspace root.');
531
+ }
532
+ mkdirSync(resolved, { recursive: true });
533
+ const realWorkspaceRoot = realpathSync(workspaceRoot);
534
+ const realMissionDir = realpathSync(resolved);
535
+ const realRelative = path.relative(realWorkspaceRoot, realMissionDir);
536
+ if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) {
537
+ throw new Error('Mission dir must stay inside the real workspace root.');
538
+ }
539
+ return realMissionDir;
540
+ }
541
+ export function resolveExistingMissionDir(workspaceRoot, missionDir) {
542
+ const resolved = path.isAbsolute(missionDir) ? path.resolve(missionDir) : path.resolve(workspaceRoot, missionDir);
543
+ const relative = path.relative(workspaceRoot, resolved);
544
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
545
+ throw new Error('Mission dir must be inside the workspace root.');
546
+ }
547
+ if (!existsSync(resolved)) {
548
+ throw new Error('Mission dir does not exist.');
549
+ }
550
+ const realWorkspaceRoot = realpathSync(workspaceRoot);
551
+ const realMissionDir = realpathSync(resolved);
552
+ const realRelative = path.relative(realWorkspaceRoot, realMissionDir);
553
+ if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) {
554
+ throw new Error('Mission dir must stay inside the real workspace root.');
555
+ }
556
+ return realMissionDir;
557
+ }
558
+ export function findLatestFactorySessionId(missionDir) {
559
+ const sessionRoot = findFactorySessionRoot(missionDir);
560
+ if (!existsSync(sessionRoot))
561
+ return null;
562
+ const candidates = readdirSync(sessionRoot)
563
+ .filter((entry) => entry.endsWith('.jsonl'))
564
+ .map((entry) => {
565
+ const fullPath = path.join(sessionRoot, entry);
566
+ try {
567
+ return {
568
+ sessionId: entry.slice(0, -'.jsonl'.length),
569
+ mtimeMs: statSync(fullPath).mtimeMs,
570
+ };
571
+ }
572
+ catch {
573
+ return null;
574
+ }
575
+ })
576
+ .filter((entry) => Boolean(entry));
577
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
578
+ return candidates[0]?.sessionId ?? null;
579
+ }
580
+ export function findLatestRootFactorySessionId(missionDir) {
581
+ const sessionRoot = findFactorySessionRoot(missionDir);
582
+ if (!existsSync(sessionRoot))
583
+ return null;
584
+ const candidates = readdirSync(sessionRoot)
585
+ .filter((entry) => entry.endsWith('.jsonl'))
586
+ .map((entry) => {
587
+ const fullPath = path.join(sessionRoot, entry);
588
+ try {
589
+ const firstRecord = readFirstJsonlRecord(fullPath);
590
+ if (!firstRecord || stringValue(firstRecord.callingSessionId))
591
+ return null;
592
+ return {
593
+ sessionId: entry.slice(0, -'.jsonl'.length),
594
+ mtimeMs: statSync(fullPath).mtimeMs,
595
+ };
596
+ }
597
+ catch {
598
+ return null;
599
+ }
600
+ })
601
+ .filter((entry) => Boolean(entry));
602
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
603
+ return candidates[0]?.sessionId ?? null;
604
+ }
605
+ function findFactorySessionRoot(missionDir) {
606
+ return path.join(homedir(), '.factory', 'sessions', factorySessionDirName(missionDir));
607
+ }
608
+ function readFirstJsonlRecord(filePath) {
609
+ const text = readFileSync(filePath, 'utf8');
610
+ const line = text.split(/\r?\n/).find((entry) => entry.trim());
611
+ if (!line)
612
+ return null;
613
+ const parsed = JSON.parse(line);
614
+ return isRecord(parsed) ? parsed : null;
615
+ }
616
+ function factorySessionDirName(cwd) {
617
+ return path.resolve(cwd).replace(/[\\/]+/g, '-');
618
+ }
619
+ export function findLatestFactoryArtifactContext(missionDir) {
620
+ const factorySessionId = findLatestFactorySessionId(missionDir);
621
+ if (!factorySessionId)
622
+ return {};
623
+ const factoryArtifactDir = path.join(homedir(), '.factory', 'missions', factorySessionId);
624
+ return {
625
+ factorySessionId,
626
+ factoryArtifactDir: existsSync(factoryArtifactDir) ? factoryArtifactDir : null,
627
+ };
628
+ }
629
+ function buildProgressPayload(artifactDir, lineNumber, record) {
630
+ const type = stringValue(record.type) ?? stringValue(record.eventType) ?? 'progress';
631
+ const eventTime = timeValue(record.timestampMs ?? record.timestamp ?? record.time) ?? Date.now();
632
+ return {
633
+ artifactDir,
634
+ artifactPath: path.join(artifactDir, 'progress_log.jsonl'),
635
+ lineNumber,
636
+ role: classifyMissionRole(type, record),
637
+ type,
638
+ featureId: stringValue(record.featureId),
639
+ milestone: stringValue(record.milestone),
640
+ workerSessionId: stringValue(record.workerSessionId),
641
+ handoffFile: stringValue(record.handoffFile),
642
+ eventTime,
643
+ droidEvent: record,
644
+ };
645
+ }
646
+ function buildTranscriptPayload(artifactDir, lineNumber, record) {
647
+ const skeleton = stringValue(record.skeleton) ?? '';
648
+ const transcript = truncateText(skeleton, TRANSCRIPT_LIMIT);
649
+ const featureId = stringValue(record.featureId);
650
+ const eventTime = timeValue(record.timestampMs ?? record.timestamp ?? record.time) ?? Date.now();
651
+ return {
652
+ artifactDir,
653
+ artifactPath: path.join(artifactDir, 'worker-transcripts.jsonl'),
654
+ lineNumber,
655
+ role: classifyMissionRole('worker_transcript', record),
656
+ featureId,
657
+ milestone: stringValue(record.milestone),
658
+ workerSessionId: stringValue(record.workerSessionId),
659
+ eventTime,
660
+ transcriptPreview: firstMeaningfulLine(skeleton),
661
+ transcriptText: transcript.text,
662
+ transcriptTruncated: transcript.truncated,
663
+ droidEvent: {
664
+ ...record,
665
+ skeleton: transcript.text,
666
+ skeletonTruncated: transcript.truncated,
667
+ },
668
+ };
669
+ }
670
+ function buildSessionPayload(sessionRoot, artifactPath, sessionId, lineNumber, record) {
671
+ const type = stringValue(record.type) ?? 'session_event';
672
+ const eventTime = timeValue(record.timestampMs ?? record.timestamp ?? record.time) ?? Date.now();
673
+ const message = isRecord(record.message) ? record.message : {};
674
+ const messageRole = stringValue(message.role);
675
+ const textPreview = extractSessionTextPreview(record);
676
+ const sessionTitle = stringValue(record.sessionTitle) ?? stringValue(record.title);
677
+ return {
678
+ sessionRoot,
679
+ artifactPath,
680
+ sessionId,
681
+ lineNumber,
682
+ role: classifySessionRole(type, record),
683
+ type,
684
+ sessionTitle,
685
+ callingSessionId: stringValue(record.callingSessionId),
686
+ messageRole,
687
+ eventTime,
688
+ textPreview,
689
+ droidEvent: truncateSessionRecord(record),
690
+ };
691
+ }
692
+ function classifyMissionRole(type, record) {
693
+ const searchable = [
694
+ type,
695
+ stringValue(record.featureId),
696
+ stringValue(record.skillName),
697
+ stringValue(record.workerSessionId),
698
+ ].filter(Boolean).join(' ').toLowerCase();
699
+ if (searchable.includes('validat'))
700
+ return 'validator';
701
+ if (searchable.includes('worker') || type === 'worker_transcript')
702
+ return 'worker';
703
+ if (searchable.includes('orchestrator') || searchable.includes('milestone') || searchable.includes('handoff'))
704
+ return 'orchestrator';
705
+ return 'system';
706
+ }
707
+ function classifySessionRole(type, record) {
708
+ const sessionTitle = stringValue(record.sessionTitle) ?? stringValue(record.title);
709
+ const searchable = [
710
+ type,
711
+ sessionTitle,
712
+ stringValue(record.callingSessionId) ? 'worker' : 'orchestrator',
713
+ ].filter(Boolean).join(' ').toLowerCase();
714
+ if (searchable.includes('validat'))
715
+ return 'validator';
716
+ if (searchable.includes('worker') || stringValue(record.callingSessionId))
717
+ return 'worker';
718
+ if (searchable.includes('orchestrator') || type === 'session_start')
719
+ return 'orchestrator';
720
+ return 'system';
721
+ }
722
+ function extractSessionTextPreview(record) {
723
+ const message = isRecord(record.message) ? record.message : {};
724
+ const role = stringValue(message.role);
725
+ const content = Array.isArray(message.content) ? message.content : [];
726
+ const parts = [];
727
+ for (const item of content) {
728
+ if (!isRecord(item))
729
+ continue;
730
+ const type = stringValue(item.type);
731
+ const text = stringValue(item.text) ?? stringValue(item.thinking);
732
+ const toolName = stringValue(item.name);
733
+ if (text)
734
+ parts.push(text);
735
+ else if (toolName)
736
+ parts.push(`${type ?? 'tool'}: ${toolName}`);
737
+ }
738
+ if (parts.length > 0)
739
+ return truncateText(`${role ? `${role}: ` : ''}${parts.join('\n')}`, 500).text;
740
+ const title = stringValue(record.sessionTitle) ?? stringValue(record.title);
741
+ return title ? truncateText(title, 500).text : `${record.type ?? 'session_event'}`;
742
+ }
743
+ function truncateSessionRecord(record) {
744
+ const redacted = redactValue(record);
745
+ if (!isRecord(redacted))
746
+ return {};
747
+ const json = JSON.stringify(redacted);
748
+ if (json.length <= TRANSCRIPT_LIMIT)
749
+ return redacted;
750
+ return {
751
+ type: redacted.type,
752
+ id: redacted.id,
753
+ timestamp: redacted.timestamp,
754
+ sessionTitle: redacted.sessionTitle,
755
+ callingSessionId: redacted.callingSessionId,
756
+ truncatedJson: `${json.slice(0, TRANSCRIPT_LIMIT)}\n[truncated]`,
757
+ };
758
+ }
759
+ function firstMeaningfulLine(value) {
760
+ const line = value.split(/\r?\n/).map((entry) => entry.trim()).find((entry) => entry && !entry.startsWith('---'));
761
+ return line ? truncateText(line, 240).text : 'Transcript captured';
762
+ }
763
+ function oneLine(value, limit) {
764
+ return truncateText(value.replace(/\s+/g, ' ').trim(), limit).text;
765
+ }
766
+ function truncateText(value, limit) {
767
+ const redacted = redactString(value);
768
+ if (redacted.length <= limit)
769
+ return { text: redacted, truncated: false };
770
+ return { text: `${redacted.slice(0, limit)}\n[truncated]`, truncated: true };
771
+ }
772
+ function redactValue(value) {
773
+ if (typeof value === 'string')
774
+ return redactString(value);
775
+ if (Array.isArray(value))
776
+ return value.map(redactValue);
777
+ if (isRecord(value)) {
778
+ const result = {};
779
+ for (const [key, child] of Object.entries(value)) {
780
+ result[key] = redactValue(child);
781
+ }
782
+ return result;
783
+ }
784
+ return value;
785
+ }
786
+ function redactString(value) {
787
+ return value
788
+ .replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, '[redacted-secret]')
789
+ .replace(/\b(api[_-]?key|token|secret|password)\s*[:=]\s*["']?[^"'\s]+/gi, '$1=[redacted-secret]');
790
+ }
791
+ function timeValue(value) {
792
+ if (typeof value === 'number' && Number.isFinite(value))
793
+ return value;
794
+ if (typeof value === 'string' && value.trim()) {
795
+ const parsed = Date.parse(value);
796
+ return Number.isFinite(parsed) ? parsed : null;
797
+ }
798
+ return null;
799
+ }
800
+ function stringValue(value) {
801
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
802
+ }
803
+ function isRecord(value) {
804
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
805
+ }
806
+ function uniqueStrings(values) {
807
+ return Array.from(new Set(values));
808
+ }