@shapeshift-labs/frontier-swarm-codex 0.2.0 → 0.3.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/README.md +33 -5
- package/benchmarks/package-bench.mjs +19 -2
- package/dist/cli.js +37 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +174 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +573 -23
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { FRONTIER_SWARM_DEFAULT_MODEL, FRONTIER_SWARM_DEFAULT_REASONING_EFFORT, checkSwarmOwnership, completeSwarmJob, createSwarmLeases, createSwarmPlan, createSwarmProof, createSwarmRun, createSwarmSchedule, recordSwarmEvent } from '@shapeshift-labs/frontier-swarm';
|
|
4
|
+
import { FRONTIER_SWARM_DEFAULT_MODEL, FRONTIER_SWARM_DEFAULT_REASONING_EFFORT, checkSwarmOwnership, completeSwarmJob, createSwarmMergeBundle, createSwarmEventStream, createSwarmLeases, createSwarmPlan, createSwarmProof, createSwarmRun, createSwarmSchedule, recordSwarmEvent, routeSwarmEventToMailboxes } from '@shapeshift-labs/frontier-swarm';
|
|
5
5
|
export const FRONTIER_SWARM_CODEX_DEFAULT_MODEL = FRONTIER_SWARM_DEFAULT_MODEL;
|
|
6
6
|
export const FRONTIER_SWARM_CODEX_DEFAULT_REASONING_EFFORT = FRONTIER_SWARM_DEFAULT_REASONING_EFFORT;
|
|
7
|
+
export const FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_KIND = 'frontier.swarm-codex.workspace-manifest';
|
|
8
|
+
export const FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_VERSION = 1;
|
|
9
|
+
export const FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_KIND = 'frontier.swarm-codex.workspace-proof';
|
|
10
|
+
export const FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_VERSION = 1;
|
|
11
|
+
export const FRONTIER_SWARM_CODEX_PID_MANIFEST_KIND = 'frontier.swarm-codex.pid-manifest';
|
|
12
|
+
export const FRONTIER_SWARM_CODEX_PID_MANIFEST_VERSION = 1;
|
|
13
|
+
export const FRONTIER_SWARM_CODEX_COLLECTION_KIND = 'frontier.swarm-codex.collection';
|
|
14
|
+
export const FRONTIER_SWARM_CODEX_COLLECTION_VERSION = 1;
|
|
7
15
|
const DEFAULT_WORKSPACE_INCLUDES = ['AGENTS.md', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'config'];
|
|
8
16
|
const DEFAULT_WORKSPACE_EXCLUDES = [
|
|
9
17
|
'.git',
|
|
@@ -78,11 +86,16 @@ export function coerceCodexSwarmTasksInput(value) {
|
|
|
78
86
|
sourceRefs: readStringArray(task.sourceRefs).concat(readStringArray(task.legacySourcePaths)),
|
|
79
87
|
targetRefs: readStringArray(task.targetRefs).concat(readStringArray(task.ownedFiles), readStringArray(task.files)),
|
|
80
88
|
allowedWrites: readStringArray(task.allowedWrites).concat(readStringArray(task.ownedFiles), readStringArray(task.files)),
|
|
89
|
+
ownershipRegions: Array.isArray(task.ownershipRegions) ? task.ownershipRegions : [],
|
|
90
|
+
ownedRegions: readStringArray(task.ownedRegions),
|
|
91
|
+
changedRegions: readStringArray(task.changedRegions),
|
|
81
92
|
acceptance: readStringArray(task.acceptance),
|
|
82
93
|
acceptanceChecks: Array.isArray(task.acceptanceChecks) ? task.acceptanceChecks : undefined,
|
|
83
94
|
verification: Array.isArray(task.verification) ? task.verification : undefined,
|
|
84
95
|
evidenceCommand: typeof task.evidenceCommand === 'string' ? task.evidenceCommand : undefined,
|
|
85
96
|
shardCommand: typeof task.shardCommand === 'string' ? task.shardCommand : undefined,
|
|
97
|
+
capabilities: readStringArray(task.capabilities),
|
|
98
|
+
resourceRequirements: isObject(task.resourceRequirements) ? task.resourceRequirements : undefined,
|
|
86
99
|
tags: readStringArray(task.tags),
|
|
87
100
|
metadata: { source: task }
|
|
88
101
|
};
|
|
@@ -92,26 +105,87 @@ export async function runCodexSwarm(plan, options) {
|
|
|
92
105
|
const outDir = path.resolve(options.cwd ?? process.cwd(), options.outDir);
|
|
93
106
|
await fs.mkdir(outDir, { recursive: true });
|
|
94
107
|
await fs.writeFile(path.join(outDir, 'swarm-plan.json'), JSON.stringify(plan, null, 2) + '\n');
|
|
108
|
+
const eventStream = options.eventStream ?? createSwarmEventStream({
|
|
109
|
+
runId: plan.runId,
|
|
110
|
+
root: path.join(outDir, 'streams'),
|
|
111
|
+
lanes: Array.from(new Set(plan.jobs.map((job) => job.lane)))
|
|
112
|
+
});
|
|
113
|
+
await initFileSwarmEventStream(eventStream);
|
|
114
|
+
const pidManifestPath = path.resolve(options.cwd ?? process.cwd(), options.pidManifestPath ?? path.join(outDir, 'pids.json'));
|
|
115
|
+
await appendCodexPidManifest(pidManifestPath, { pid: process.pid, role: 'parent', runId: plan.runId, startedAt: Date.now() }, plan.runId);
|
|
95
116
|
let run = createSwarmRun({ plan, status: 'running', startedAt: Date.now() });
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
const startedEvent = { type: 'swarm.started', runId: run.id, at: run.startedAt, data: { jobCount: plan.jobs.length } };
|
|
118
|
+
run = recordSwarmEvent(run, startedEvent);
|
|
119
|
+
await appendFileSwarmEvent(eventStream, startedEvent);
|
|
120
|
+
const runOptions = { ...options, eventStream, pidManifestPath };
|
|
121
|
+
const results = await runScheduledJobPool(plan, Math.max(1, options.maxConcurrency ?? 1), (job, lease) => runCodexJob(job, runOptions, outDir, lease));
|
|
122
|
+
for (const result of results) {
|
|
123
|
+
const job = plan.jobs.find((entry) => entry.id === result.jobId);
|
|
124
|
+
if (job) {
|
|
125
|
+
await options.onJobFinished?.({ job, result });
|
|
126
|
+
await appendFileSwarmEvent(eventStream, {
|
|
127
|
+
type: 'agent.finished',
|
|
128
|
+
runId: run.id,
|
|
129
|
+
jobId: job.id,
|
|
130
|
+
taskId: job.taskId,
|
|
131
|
+
lane: job.lane,
|
|
132
|
+
data: { status: result.status, mergeReadiness: result.mergeReadiness, changedPathCount: result.changedPaths?.length ?? 0 }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
98
136
|
for (const result of results)
|
|
99
137
|
run = completeSwarmJob(run, result);
|
|
100
138
|
const proof = createSwarmProof(run, { validation: plan.validation });
|
|
101
139
|
const ok = run.summary.failedCount === 0 && run.summary.blockedCount === 0 && run.summary.ownershipViolationCount === 0;
|
|
140
|
+
await appendFileSwarmEvent(eventStream, {
|
|
141
|
+
type: 'swarm.finished',
|
|
142
|
+
runId: run.id,
|
|
143
|
+
data: { ok, summary: run.summary }
|
|
144
|
+
});
|
|
102
145
|
await fs.writeFile(path.join(outDir, 'swarm-results.json'), JSON.stringify({ ok, outDir, run, proof }, null, 2) + '\n');
|
|
103
|
-
|
|
146
|
+
await writeSwarmCoordinatorSnapshot(options.coordinatorSnapshotPath ? path.resolve(options.cwd ?? process.cwd(), options.coordinatorSnapshotPath) : path.join(outDir, 'coordinator-dashboard.json'), {
|
|
147
|
+
ok,
|
|
148
|
+
outDir,
|
|
149
|
+
plan,
|
|
150
|
+
run,
|
|
151
|
+
proof,
|
|
152
|
+
eventStream,
|
|
153
|
+
pidManifestPath
|
|
154
|
+
});
|
|
155
|
+
const result = { ok, outDir, plan, run, proof };
|
|
156
|
+
await options.onSwarmFinished?.({ result });
|
|
157
|
+
return result;
|
|
104
158
|
}
|
|
105
159
|
export async function runCodexJob(job, options, outDir, lease) {
|
|
106
|
-
const paths = await createJobPaths(outDir, job);
|
|
160
|
+
const paths = await createJobPaths(outDir, job, options);
|
|
107
161
|
const workspace = await prepareCodexWorkspace(job, options);
|
|
108
162
|
const workspacePlan = createCodexWorkspacePlan(job, options);
|
|
163
|
+
const hookInput = {
|
|
164
|
+
job,
|
|
165
|
+
cwd: options.cwd ?? process.cwd(),
|
|
166
|
+
outDir,
|
|
167
|
+
workspacePath: workspace,
|
|
168
|
+
workspacePlan,
|
|
169
|
+
paths
|
|
170
|
+
};
|
|
171
|
+
await options.prepareJobWorkspace?.(hookInput);
|
|
109
172
|
const fileSnapshot = shouldSnapshotWorkspaceChanges(workspacePlan, options)
|
|
110
173
|
? await snapshotWorkspaceFiles(workspace)
|
|
111
174
|
: undefined;
|
|
112
|
-
const
|
|
175
|
+
const basePrompt = renderCodexPrompt(job, { workspacePath: workspace, paths });
|
|
176
|
+
const prompt = options.renderJobPrompt
|
|
177
|
+
? await options.renderJobPrompt({ ...hookInput, prompt: basePrompt })
|
|
178
|
+
: basePrompt;
|
|
113
179
|
await fs.writeFile(paths.promptPath, prompt);
|
|
114
180
|
const args = buildCodexArgs(job, { ...options, workspacePath: workspace, paths });
|
|
181
|
+
await options.onJobStarted?.({ ...hookInput, prompt, args });
|
|
182
|
+
await appendFileSwarmEvent(options.eventStream, {
|
|
183
|
+
type: 'agent.scheduled',
|
|
184
|
+
jobId: job.id,
|
|
185
|
+
taskId: job.taskId,
|
|
186
|
+
lane: job.lane,
|
|
187
|
+
data: { workspace: workspacePlan.path, capabilities: job.capabilities, resourceRequirements: job.resourceRequirements }
|
|
188
|
+
});
|
|
115
189
|
const startedAt = Date.now();
|
|
116
190
|
const execution = options.dryRun
|
|
117
191
|
? { exitCode: 0, changedPaths: [] }
|
|
@@ -125,12 +199,27 @@ export async function runCodexJob(job, options, outDir, lease) {
|
|
|
125
199
|
paths,
|
|
126
200
|
timeoutMs: job.compute.timeoutMs ?? options.jobTimeoutMs ?? 7200000
|
|
127
201
|
});
|
|
128
|
-
const
|
|
202
|
+
const collected = execution.changedPaths
|
|
203
|
+
? filterWorkspaceChangedPaths(execution.changedPaths, workspacePlan)
|
|
204
|
+
: options.collectGitStatus === false
|
|
205
|
+
? { changedPaths: [], ignoredChangedPaths: [] }
|
|
206
|
+
: await collectChangedPaths(workspace, fileSnapshot, workspacePlan);
|
|
207
|
+
const rawChangedPaths = collected.changedPaths;
|
|
208
|
+
const changedPaths = options.changedPathFilter ? [...options.changedPathFilter(rawChangedPaths, hookInput)] : rawChangedPaths;
|
|
209
|
+
const workspaceProof = await createSwarmWorkspaceProof(workspacePlan, { ignoredChangedPaths: collected.ignoredChangedPaths });
|
|
210
|
+
await fs.writeFile(paths.workspaceProofPath, JSON.stringify(workspaceProof, null, 2) + '\n');
|
|
129
211
|
const ownership = checkSwarmOwnership(job, changedPaths);
|
|
130
212
|
const verification = options.runVerification ? await runVerification(job.verification, workspace) : [];
|
|
131
213
|
const failedVerification = verification.some((entry) => entry.required !== false && entry.status !== 0);
|
|
132
214
|
const status = ownership.ok && execution.exitCode === 0 && !failedVerification ? 'completed' : 'failed';
|
|
133
|
-
|
|
215
|
+
const patchPath = await writeCodexPatchFile({
|
|
216
|
+
workspace,
|
|
217
|
+
sourceRoot: path.resolve(options.cwd ?? process.cwd()),
|
|
218
|
+
paths,
|
|
219
|
+
workspacePlan,
|
|
220
|
+
changedPaths
|
|
221
|
+
});
|
|
222
|
+
const result = {
|
|
134
223
|
jobId: job.id,
|
|
135
224
|
status,
|
|
136
225
|
startedAt,
|
|
@@ -138,17 +227,30 @@ export async function runCodexJob(job, options, outDir, lease) {
|
|
|
138
227
|
exitCode: execution.exitCode,
|
|
139
228
|
signal: execution.signal,
|
|
140
229
|
changedPaths,
|
|
230
|
+
changedRegions: job.changedRegions,
|
|
141
231
|
ownershipViolations: ownership.violations,
|
|
142
|
-
evidencePaths: [paths.evidenceDir],
|
|
232
|
+
evidencePaths: [paths.evidenceDir, paths.workspaceProofPath, paths.mergeBundlePath, ...(patchPath ? [patchPath] : [])],
|
|
233
|
+
...(patchPath ? { patchPath } : {}),
|
|
234
|
+
queueItemIds: [job.taskId],
|
|
143
235
|
verification,
|
|
144
236
|
lastMessage: execution.lastMessage,
|
|
145
237
|
error: execution.error,
|
|
146
238
|
metadata: lease ? { leaseId: lease.id, leaseToken: lease.token, fencingToken: lease.fencingToken } : undefined
|
|
147
239
|
};
|
|
240
|
+
const mergeBundle = createSwarmMergeBundle({
|
|
241
|
+
runId: options.eventStream?.runId,
|
|
242
|
+
job,
|
|
243
|
+
result,
|
|
244
|
+
...(patchPath ? { patchPath } : {}),
|
|
245
|
+
evidencePaths: [paths.evidenceDir, paths.workspaceProofPath],
|
|
246
|
+
queueItemIds: [job.taskId]
|
|
247
|
+
});
|
|
248
|
+
await fs.writeFile(paths.mergeBundlePath, JSON.stringify(mergeBundle, null, 2) + '\n');
|
|
249
|
+
return result;
|
|
148
250
|
}
|
|
149
251
|
export function buildCodexArgs(job, input) {
|
|
150
|
-
const model = job
|
|
151
|
-
const effort = job
|
|
252
|
+
const model = resolveCodexModelFlag(job, input);
|
|
253
|
+
const effort = resolveCodexReasoningEffort(job, input);
|
|
152
254
|
const sandbox = job.compute.sandbox ?? input.sandbox ?? 'workspace-write';
|
|
153
255
|
const args = [
|
|
154
256
|
'exec',
|
|
@@ -160,12 +262,15 @@ export function buildCodexArgs(job, input) {
|
|
|
160
262
|
sandbox,
|
|
161
263
|
'--json',
|
|
162
264
|
'--output-last-message',
|
|
163
|
-
input.paths.lastMessagePath
|
|
164
|
-
'--model',
|
|
165
|
-
model,
|
|
166
|
-
'-c',
|
|
167
|
-
`model_reasoning_effort="${effort}"`
|
|
265
|
+
input.paths.lastMessagePath
|
|
168
266
|
];
|
|
267
|
+
if (model)
|
|
268
|
+
args.push('--model', model);
|
|
269
|
+
if (effort)
|
|
270
|
+
args.push('-c', `model_reasoning_effort="${effort}"`);
|
|
271
|
+
const approval = normalizeCodexApprovalPolicy(input.approval);
|
|
272
|
+
if (approval)
|
|
273
|
+
args.push('--ask-for-approval', approval);
|
|
169
274
|
if (shouldSkipGitRepoCheck(input))
|
|
170
275
|
args.push('--skip-git-repo-check');
|
|
171
276
|
for (const dir of input.addDirs ?? [])
|
|
@@ -178,6 +283,56 @@ export function buildCodexArgs(job, input) {
|
|
|
178
283
|
args.push('-');
|
|
179
284
|
return args;
|
|
180
285
|
}
|
|
286
|
+
export function normalizeCodexModelFlag(model) {
|
|
287
|
+
if (model === false || model == null)
|
|
288
|
+
return undefined;
|
|
289
|
+
const value = String(model).trim();
|
|
290
|
+
if (!value)
|
|
291
|
+
return undefined;
|
|
292
|
+
const normalized = value.toLowerCase();
|
|
293
|
+
if (normalized === 'auto' || normalized === 'default' || normalized === 'config' || normalized === 'config-default') {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
export function normalizeCodexApprovalPolicy(approval) {
|
|
299
|
+
if (approval === false || approval == null)
|
|
300
|
+
return undefined;
|
|
301
|
+
const value = String(approval).trim().toLowerCase().replaceAll('_', '-');
|
|
302
|
+
if (!value || value === 'default' || value === 'config-default')
|
|
303
|
+
return undefined;
|
|
304
|
+
if (value === 'never' || value === 'none' || value === 'off' || value === 'false' || value === 'full-auto')
|
|
305
|
+
return 'never';
|
|
306
|
+
if (value === 'untrusted')
|
|
307
|
+
return 'untrusted';
|
|
308
|
+
if (value === 'on-failure')
|
|
309
|
+
return 'on-failure';
|
|
310
|
+
if (value === 'on-request' || value === 'request' || value === 'manual')
|
|
311
|
+
return 'on-request';
|
|
312
|
+
throw new Error(`unsupported Codex approval policy "${approval}"; expected untrusted, on-request, on-failure, never, full-auto, none, or default`);
|
|
313
|
+
}
|
|
314
|
+
function resolveCodexModelFlag(job, input) {
|
|
315
|
+
const explicit = normalizeCodexModelFlag(input.model);
|
|
316
|
+
if (explicit || input.model === false)
|
|
317
|
+
return explicit;
|
|
318
|
+
const policy = input.modelPolicy ?? (input.forwardPlanModel ? 'plan' : 'config-default');
|
|
319
|
+
if (policy === 'plan')
|
|
320
|
+
return normalizeCodexModelFlag(job.compute.model ?? FRONTIER_SWARM_CODEX_DEFAULT_MODEL);
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
function resolveCodexReasoningEffort(job, input) {
|
|
324
|
+
if (input.reasoningEffort === false)
|
|
325
|
+
return undefined;
|
|
326
|
+
if (typeof input.reasoningEffort === 'string') {
|
|
327
|
+
const explicit = input.reasoningEffort.trim();
|
|
328
|
+
return explicit && explicit !== 'default' && explicit !== 'config-default' ? explicit : undefined;
|
|
329
|
+
}
|
|
330
|
+
const policy = input.modelPolicy ?? (input.forwardPlanModel || input.forwardPlanReasoningEffort ? 'plan' : 'config-default');
|
|
331
|
+
if (policy !== 'plan')
|
|
332
|
+
return undefined;
|
|
333
|
+
const effort = job.compute.reasoningEffort ?? FRONTIER_SWARM_CODEX_DEFAULT_REASONING_EFFORT;
|
|
334
|
+
return effort ? String(effort).trim() : undefined;
|
|
335
|
+
}
|
|
181
336
|
export function renderCodexPrompt(job, input) {
|
|
182
337
|
return [
|
|
183
338
|
'# Frontier Swarm Codex Job',
|
|
@@ -237,6 +392,15 @@ export async function spawnCodexExecutor(input) {
|
|
|
237
392
|
await fs.writeFile(input.paths.stderrPath, '');
|
|
238
393
|
return new Promise((resolve) => {
|
|
239
394
|
const child = spawn(input.codexPath, input.args, { cwd: input.cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
395
|
+
if (child.pid) {
|
|
396
|
+
appendCodexPidManifest(input.paths.pidManifestPath, {
|
|
397
|
+
pid: child.pid,
|
|
398
|
+
role: 'codex',
|
|
399
|
+
jobId: input.job.id,
|
|
400
|
+
startedAt: Date.now(),
|
|
401
|
+
command: [input.codexPath, ...input.args]
|
|
402
|
+
}).catch(() => { });
|
|
403
|
+
}
|
|
240
404
|
const timer = setTimeout(() => child.kill('SIGTERM'), input.timeoutMs);
|
|
241
405
|
child.stdout.on('data', (chunk) => fs.appendFile(input.paths.eventsPath, chunk).catch(() => { }));
|
|
242
406
|
child.stderr.on('data', (chunk) => fs.appendFile(input.paths.stderrPath, chunk).catch(() => { }));
|
|
@@ -255,7 +419,7 @@ export async function spawnCodexExecutor(input) {
|
|
|
255
419
|
});
|
|
256
420
|
});
|
|
257
421
|
}
|
|
258
|
-
async function createJobPaths(outDir, job) {
|
|
422
|
+
async function createJobPaths(outDir, job, options) {
|
|
259
423
|
const jobDir = path.join(outDir, job.id);
|
|
260
424
|
const paths = {
|
|
261
425
|
jobDir,
|
|
@@ -263,7 +427,11 @@ async function createJobPaths(outDir, job) {
|
|
|
263
427
|
eventsPath: path.join(jobDir, 'codex-events.jsonl'),
|
|
264
428
|
stderrPath: path.join(jobDir, 'codex-stderr.log'),
|
|
265
429
|
lastMessagePath: path.join(jobDir, 'last-message.md'),
|
|
266
|
-
evidenceDir: path.join(jobDir, 'evidence')
|
|
430
|
+
evidenceDir: path.join(jobDir, 'evidence'),
|
|
431
|
+
workspaceProofPath: path.join(jobDir, 'evidence', 'workspace-proof.json'),
|
|
432
|
+
patchPath: path.join(jobDir, 'evidence', 'changes.patch'),
|
|
433
|
+
mergeBundlePath: path.join(jobDir, 'evidence', 'merge.json'),
|
|
434
|
+
pidManifestPath: path.resolve(options.cwd ?? process.cwd(), options.pidManifestPath ?? path.join(outDir, 'pids.json'))
|
|
267
435
|
};
|
|
268
436
|
await fs.mkdir(paths.evidenceDir, { recursive: true });
|
|
269
437
|
return paths;
|
|
@@ -285,6 +453,7 @@ export async function prepareCodexWorkspace(job, options) {
|
|
|
285
453
|
if (await pathExists(plan.path)) {
|
|
286
454
|
if (!plan.replace)
|
|
287
455
|
return plan.path;
|
|
456
|
+
assertGeneratedWorkspacePath(plan);
|
|
288
457
|
await fs.rm(plan.path, { recursive: true, force: true });
|
|
289
458
|
}
|
|
290
459
|
await fs.mkdir(plan.path, { recursive: true });
|
|
@@ -314,6 +483,10 @@ export function createCodexWorkspacePlan(job, options) {
|
|
|
314
483
|
excludes: [],
|
|
315
484
|
artifactIncludes: [],
|
|
316
485
|
linkPaths: [],
|
|
486
|
+
requiredIncludes: [],
|
|
487
|
+
optionalIncludes: [],
|
|
488
|
+
strategy: workspace.strategy ?? 'fs-cp',
|
|
489
|
+
...(workspace.guardRoot ? { guardRoot: path.resolve(cwd, workspace.guardRoot) } : {}),
|
|
317
490
|
linkNodeModules: false,
|
|
318
491
|
replace: false,
|
|
319
492
|
skipGitRepoCheck: workspace.skipGitRepoCheck ?? false
|
|
@@ -341,6 +514,16 @@ export function createCodexWorkspacePlan(job, options) {
|
|
|
341
514
|
...readStringArray(rawTask.snapshotLinkPaths),
|
|
342
515
|
...readStringArray(rawTask.linkPaths)
|
|
343
516
|
]);
|
|
517
|
+
const requiredIncludes = uniqueWorkspacePaths([
|
|
518
|
+
...readStringArray(workspace.requiredIncludes),
|
|
519
|
+
...readStringArray(rawTask.requiredIncludes),
|
|
520
|
+
...readStringArray(rawTask.snapshotRequiredIncludes)
|
|
521
|
+
]);
|
|
522
|
+
const optionalIncludes = uniqueWorkspacePaths([
|
|
523
|
+
...readStringArray(workspace.optionalIncludes),
|
|
524
|
+
...readStringArray(rawTask.optionalIncludes),
|
|
525
|
+
...readStringArray(rawTask.snapshotOptionalIncludes)
|
|
526
|
+
]);
|
|
344
527
|
return {
|
|
345
528
|
mode,
|
|
346
529
|
root,
|
|
@@ -349,11 +532,231 @@ export function createCodexWorkspacePlan(job, options) {
|
|
|
349
532
|
excludes,
|
|
350
533
|
artifactIncludes,
|
|
351
534
|
linkPaths,
|
|
535
|
+
requiredIncludes,
|
|
536
|
+
optionalIncludes,
|
|
537
|
+
strategy: workspace.strategy ?? 'fs-cp',
|
|
538
|
+
guardRoot: path.resolve(cwd, workspace.guardRoot ?? workspace.root ?? path.join('agent-worktrees', 'frontier-swarm-codex')),
|
|
352
539
|
linkNodeModules: workspace.linkNodeModules ?? (mode !== 'git-worktree'),
|
|
353
540
|
replace: workspace.replace ?? false,
|
|
354
541
|
skipGitRepoCheck: workspace.skipGitRepoCheck ?? (mode === 'copy' || mode === 'snapshot')
|
|
355
542
|
};
|
|
356
543
|
}
|
|
544
|
+
export function createSwarmWorkspaceManifest(plan) {
|
|
545
|
+
return {
|
|
546
|
+
kind: FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_KIND,
|
|
547
|
+
version: FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_VERSION,
|
|
548
|
+
id: 'codex-workspace:' + stableHash([plan.mode, plan.root, plan.path, plan.includes, plan.linkPaths]),
|
|
549
|
+
mode: plan.mode,
|
|
550
|
+
root: plan.root,
|
|
551
|
+
path: plan.path,
|
|
552
|
+
includes: [...plan.includes],
|
|
553
|
+
excludes: [...plan.excludes],
|
|
554
|
+
artifactIncludes: [...plan.artifactIncludes],
|
|
555
|
+
linkPaths: [...plan.linkPaths],
|
|
556
|
+
requiredIncludes: [...plan.requiredIncludes],
|
|
557
|
+
optionalIncludes: [...plan.optionalIncludes],
|
|
558
|
+
strategy: plan.strategy,
|
|
559
|
+
...(plan.guardRoot ? { guardRoot: plan.guardRoot } : {}),
|
|
560
|
+
linkNodeModules: plan.linkNodeModules,
|
|
561
|
+
skipGitRepoCheck: plan.skipGitRepoCheck
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
export async function createSwarmWorkspaceProof(plan, input = {}) {
|
|
565
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
566
|
+
const manifest = createSwarmWorkspaceManifest(plan);
|
|
567
|
+
const copiedCandidates = uniqueWorkspacePaths([...plan.includes, ...plan.artifactIncludes, ...plan.requiredIncludes]);
|
|
568
|
+
const optionalCandidates = uniqueWorkspacePaths(plan.optionalIncludes);
|
|
569
|
+
const copiedPaths = [];
|
|
570
|
+
const missingRequired = [];
|
|
571
|
+
const missingOptional = [];
|
|
572
|
+
for (const include of copiedCandidates) {
|
|
573
|
+
if (await pathExists(path.join(plan.path, include)))
|
|
574
|
+
copiedPaths.push(include);
|
|
575
|
+
else if (plan.requiredIncludes.includes(include))
|
|
576
|
+
missingRequired.push(include);
|
|
577
|
+
}
|
|
578
|
+
for (const include of optionalCandidates) {
|
|
579
|
+
if (await pathExists(path.join(plan.path, include)))
|
|
580
|
+
copiedPaths.push(include);
|
|
581
|
+
else
|
|
582
|
+
missingOptional.push(include);
|
|
583
|
+
}
|
|
584
|
+
const linkedPaths = [];
|
|
585
|
+
for (const linkPath of uniqueWorkspacePaths([...plan.linkPaths, ...(plan.linkNodeModules ? ['node_modules'] : [])])) {
|
|
586
|
+
const stat = await fs.lstat(path.join(plan.path, linkPath)).catch(() => undefined);
|
|
587
|
+
if (stat?.isSymbolicLink())
|
|
588
|
+
linkedPaths.push(linkPath);
|
|
589
|
+
}
|
|
590
|
+
const ignoredChangedPaths = uniqueWorkspacePaths(input.ignoredChangedPaths ?? []);
|
|
591
|
+
return {
|
|
592
|
+
kind: FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_KIND,
|
|
593
|
+
version: FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_VERSION,
|
|
594
|
+
id: 'codex-workspace-proof:' + stableHash([manifest.id, copiedPaths, linkedPaths, missingRequired, missingOptional, generatedAt]),
|
|
595
|
+
generatedAt,
|
|
596
|
+
manifest,
|
|
597
|
+
copiedPaths: uniqueWorkspacePaths(copiedPaths),
|
|
598
|
+
linkedPaths,
|
|
599
|
+
missingRequired,
|
|
600
|
+
missingOptional,
|
|
601
|
+
ignoredChangedPaths,
|
|
602
|
+
summary: {
|
|
603
|
+
copiedCount: uniqueWorkspacePaths(copiedPaths).length,
|
|
604
|
+
linkedCount: linkedPaths.length,
|
|
605
|
+
missingRequiredCount: missingRequired.length,
|
|
606
|
+
missingOptionalCount: missingOptional.length,
|
|
607
|
+
ignoredChangedPathCount: ignoredChangedPaths.length
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
export async function initFileSwarmEventStream(stream) {
|
|
612
|
+
if (!stream)
|
|
613
|
+
return;
|
|
614
|
+
const mailboxes = [stream.global, ...Object.values(stream.lanes)];
|
|
615
|
+
await Promise.all(mailboxes.map(async (mailbox) => {
|
|
616
|
+
if (!mailbox.path)
|
|
617
|
+
return;
|
|
618
|
+
await fs.mkdir(path.dirname(mailbox.path), { recursive: true });
|
|
619
|
+
await fs.writeFile(mailbox.path, '');
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
622
|
+
export async function appendFileSwarmEvent(stream, event) {
|
|
623
|
+
if (!stream)
|
|
624
|
+
return;
|
|
625
|
+
const line = JSON.stringify({ at: Date.now(), ...event }) + '\n';
|
|
626
|
+
const paths = routeSwarmEventToMailboxes(stream, event)
|
|
627
|
+
.map((mailbox) => mailbox.path)
|
|
628
|
+
.filter((mailboxPath) => !!mailboxPath);
|
|
629
|
+
await Promise.all(paths.map(async (mailboxPath) => {
|
|
630
|
+
await fs.mkdir(path.dirname(mailboxPath), { recursive: true });
|
|
631
|
+
await fs.appendFile(mailboxPath, line);
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
export async function writeSwarmCoordinatorSnapshot(file, input) {
|
|
635
|
+
const byLane = input.run.jobs.reduce((acc, job) => {
|
|
636
|
+
const current = acc[job.lane] ?? { total: 0, completed: 0, failed: 0, blocked: 0 };
|
|
637
|
+
current.total += 1;
|
|
638
|
+
const result = input.run.results.find((entry) => entry.jobId === job.id);
|
|
639
|
+
if (result?.status === 'completed' || result?.status === 'verified')
|
|
640
|
+
current.completed += 1;
|
|
641
|
+
else if (result?.status === 'failed')
|
|
642
|
+
current.failed += 1;
|
|
643
|
+
else if (result?.status === 'blocked')
|
|
644
|
+
current.blocked += 1;
|
|
645
|
+
acc[job.lane] = current;
|
|
646
|
+
return acc;
|
|
647
|
+
}, {});
|
|
648
|
+
const mergeReadiness = input.run.results.reduce((acc, result) => {
|
|
649
|
+
acc[result.mergeReadiness] = (acc[result.mergeReadiness] ?? 0) + 1;
|
|
650
|
+
return acc;
|
|
651
|
+
}, {});
|
|
652
|
+
const dashboard = {
|
|
653
|
+
kind: 'frontier.swarm-codex.coordinator-dashboard',
|
|
654
|
+
version: 1,
|
|
655
|
+
generatedAt: new Date().toISOString(),
|
|
656
|
+
ok: input.ok,
|
|
657
|
+
outDir: input.outDir,
|
|
658
|
+
runId: input.run.id,
|
|
659
|
+
planId: input.plan.id,
|
|
660
|
+
summary: input.run.summary,
|
|
661
|
+
byLane,
|
|
662
|
+
mergeReadiness,
|
|
663
|
+
eventStream: input.eventStream ?? null,
|
|
664
|
+
pidManifestPath: input.pidManifestPath ?? null,
|
|
665
|
+
proof: input.proof
|
|
666
|
+
};
|
|
667
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
668
|
+
await fs.writeFile(file, JSON.stringify(dashboard, null, 2) + '\n');
|
|
669
|
+
}
|
|
670
|
+
export async function appendCodexPidManifest(file, entry, runId) {
|
|
671
|
+
const manifest = await readCodexPidManifest(file).catch(() => ({
|
|
672
|
+
kind: FRONTIER_SWARM_CODEX_PID_MANIFEST_KIND,
|
|
673
|
+
version: FRONTIER_SWARM_CODEX_PID_MANIFEST_VERSION,
|
|
674
|
+
...(runId ? { runId } : {}),
|
|
675
|
+
entries: []
|
|
676
|
+
}));
|
|
677
|
+
const entries = manifest.entries.filter((existing) => existing.pid !== entry.pid || existing.jobId !== entry.jobId);
|
|
678
|
+
entries.push(entry);
|
|
679
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
680
|
+
await fs.writeFile(file, JSON.stringify({ ...manifest, ...(runId ? { runId } : {}), entries }, null, 2) + '\n');
|
|
681
|
+
}
|
|
682
|
+
export async function readCodexPidManifest(file) {
|
|
683
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
684
|
+
}
|
|
685
|
+
export async function stopCodexSwarmRun(input) {
|
|
686
|
+
const signal = input.signal ?? 'SIGTERM';
|
|
687
|
+
const pidManifestPath = await resolvePidManifestPath(input.run);
|
|
688
|
+
const manifest = await readCodexPidManifest(pidManifestPath);
|
|
689
|
+
const stopped = [];
|
|
690
|
+
const missing = [];
|
|
691
|
+
const errors = [];
|
|
692
|
+
for (const entry of manifest.entries.filter((item) => item.pid !== process.pid).sort((left, right) => right.startedAt - left.startedAt)) {
|
|
693
|
+
try {
|
|
694
|
+
process.kill(entry.pid, signal);
|
|
695
|
+
stopped.push(entry.pid);
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
const code = typeof error === 'object' && error && 'code' in error ? String(error.code) : '';
|
|
699
|
+
if (code === 'ESRCH')
|
|
700
|
+
missing.push(entry.pid);
|
|
701
|
+
else
|
|
702
|
+
errors.push({ pid: entry.pid, error: error instanceof Error ? error.message : String(error) });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return { ok: errors.length === 0, pidManifestPath, signal, stopped, missing, errors };
|
|
706
|
+
}
|
|
707
|
+
export async function collectCodexSwarmRun(input) {
|
|
708
|
+
const generatedAt = Date.now();
|
|
709
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
710
|
+
const runDir = await resolveRunDirectory(input.run);
|
|
711
|
+
const outDir = path.resolve(cwd, input.outDir ?? path.join(runDir, 'collected'));
|
|
712
|
+
const buckets = {
|
|
713
|
+
'ready-to-apply': [],
|
|
714
|
+
'needs-human-port': [],
|
|
715
|
+
'failed-evidence': [],
|
|
716
|
+
'stale-against-head': []
|
|
717
|
+
};
|
|
718
|
+
const mergePaths = await findFilesByName(runDir, 'merge.json');
|
|
719
|
+
for (const mergePath of mergePaths.sort()) {
|
|
720
|
+
const bundle = JSON.parse(await fs.readFile(mergePath, 'utf8'));
|
|
721
|
+
const staleAgainstHead = input.checkStale === false ? false : await bundlePatchIsStale(bundle, mergePath, cwd);
|
|
722
|
+
const bucket = classifyCodexCollectBucket(bundle, staleAgainstHead);
|
|
723
|
+
const branchName = input.branchPrefix ? `${input.branchPrefix}/${slug(bundle.jobId)}` : bundle.branchName;
|
|
724
|
+
const nextBundle = {
|
|
725
|
+
...bundle,
|
|
726
|
+
...(branchName ? { branchName } : {}),
|
|
727
|
+
staleAgainstHead: bundle.staleAgainstHead || staleAgainstHead,
|
|
728
|
+
disposition: staleAgainstHead ? 'stale-against-head' : bundle.disposition,
|
|
729
|
+
autoMergeable: bucket === 'ready-to-apply' && bundle.autoMergeable
|
|
730
|
+
};
|
|
731
|
+
const outputDir = path.join(outDir, bucket, slug(bundle.jobId));
|
|
732
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
733
|
+
await fs.writeFile(path.join(outputDir, 'merge.json'), JSON.stringify(nextBundle, null, 2) + '\n');
|
|
734
|
+
const patchPath = resolveBundlePatchPath(nextBundle, mergePath);
|
|
735
|
+
if (patchPath && await pathExists(patchPath))
|
|
736
|
+
await fs.copyFile(patchPath, path.join(outputDir, 'changes.patch')).catch(() => { });
|
|
737
|
+
buckets[bucket].push({ bucket, jobId: bundle.jobId, mergePath, outputDir, bundle: nextBundle });
|
|
738
|
+
}
|
|
739
|
+
const summary = {
|
|
740
|
+
total: mergePaths.length,
|
|
741
|
+
'ready-to-apply': buckets['ready-to-apply'].length,
|
|
742
|
+
'needs-human-port': buckets['needs-human-port'].length,
|
|
743
|
+
'failed-evidence': buckets['failed-evidence'].length,
|
|
744
|
+
'stale-against-head': buckets['stale-against-head'].length
|
|
745
|
+
};
|
|
746
|
+
const result = {
|
|
747
|
+
kind: FRONTIER_SWARM_CODEX_COLLECTION_KIND,
|
|
748
|
+
version: FRONTIER_SWARM_CODEX_COLLECTION_VERSION,
|
|
749
|
+
ok: summary['failed-evidence'] === 0 && summary['stale-against-head'] === 0,
|
|
750
|
+
runDir,
|
|
751
|
+
outDir,
|
|
752
|
+
generatedAt,
|
|
753
|
+
buckets,
|
|
754
|
+
summary
|
|
755
|
+
};
|
|
756
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
757
|
+
await fs.writeFile(path.join(outDir, 'collection.json'), JSON.stringify(result, null, 2) + '\n');
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
357
760
|
async function copyWorkspacePath(cwd, workspacePath, include, excludes) {
|
|
358
761
|
const relative = normalizeWorkspacePath(include);
|
|
359
762
|
if (!relative)
|
|
@@ -392,6 +795,12 @@ function shouldSkipGitRepoCheck(input) {
|
|
|
392
795
|
return workspace.skipGitRepoCheck;
|
|
393
796
|
return workspace.mode === 'copy' || workspace.mode === 'snapshot';
|
|
394
797
|
}
|
|
798
|
+
function assertGeneratedWorkspacePath(plan) {
|
|
799
|
+
const relative = path.relative(plan.guardRoot ?? plan.root, plan.path);
|
|
800
|
+
if (relative.startsWith('..') || path.isAbsolute(relative) || relative === '') {
|
|
801
|
+
throw new Error(`Refusing to replace workspace outside generated root: ${plan.path}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
395
804
|
function readRawTask(job) {
|
|
396
805
|
const metadata = isObject(job.task.metadata) ? job.task.metadata : {};
|
|
397
806
|
return isObject(metadata.source) ? metadata.source : {};
|
|
@@ -426,12 +835,71 @@ async function gitChangedPaths(cwd) {
|
|
|
426
835
|
return value.includes(' -> ') ? value.split(' -> ') : [value];
|
|
427
836
|
});
|
|
428
837
|
}
|
|
429
|
-
async function collectChangedPaths(cwd, baseline) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return gitPaths;
|
|
838
|
+
async function collectChangedPaths(cwd, baseline, plan) {
|
|
839
|
+
if (!baseline)
|
|
840
|
+
return filterWorkspaceChangedPaths(await gitChangedPaths(cwd), plan);
|
|
433
841
|
const after = await snapshotWorkspaceFiles(cwd);
|
|
434
|
-
return diffWorkspaceFiles(baseline, after);
|
|
842
|
+
return filterWorkspaceChangedPaths(diffWorkspaceFiles(baseline, after), plan);
|
|
843
|
+
}
|
|
844
|
+
async function writeCodexPatchFile(input) {
|
|
845
|
+
await fs.mkdir(path.dirname(input.paths.patchPath), { recursive: true });
|
|
846
|
+
const changedPaths = uniqueWorkspacePaths(input.changedPaths);
|
|
847
|
+
if (changedPaths.length === 0) {
|
|
848
|
+
await fs.writeFile(input.paths.patchPath, '');
|
|
849
|
+
return undefined;
|
|
850
|
+
}
|
|
851
|
+
const diff = input.workspacePlan.mode === 'current' || input.workspacePlan.mode === 'git-worktree'
|
|
852
|
+
? await gitDiffPatch(input.workspace, changedPaths)
|
|
853
|
+
: await noIndexWorkspacePatch(input.sourceRoot, input.workspace, changedPaths);
|
|
854
|
+
await fs.writeFile(input.paths.patchPath, diff);
|
|
855
|
+
return diff.trim().length ? input.paths.patchPath : undefined;
|
|
856
|
+
}
|
|
857
|
+
async function gitDiffPatch(workspace, changedPaths) {
|
|
858
|
+
const result = await runProcess('git', ['diff', '--', ...changedPaths], { cwd: workspace, allowFailure: true });
|
|
859
|
+
return result.stdout;
|
|
860
|
+
}
|
|
861
|
+
async function noIndexWorkspacePatch(sourceRoot, workspace, changedPaths) {
|
|
862
|
+
const chunks = [];
|
|
863
|
+
for (const file of changedPaths) {
|
|
864
|
+
const source = path.join(sourceRoot, file);
|
|
865
|
+
const target = path.join(workspace, file);
|
|
866
|
+
const sourceExists = await pathExists(source);
|
|
867
|
+
const targetExists = await pathExists(target);
|
|
868
|
+
if (!sourceExists && !targetExists)
|
|
869
|
+
continue;
|
|
870
|
+
const left = sourceExists ? source : '/dev/null';
|
|
871
|
+
const right = targetExists ? target : '/dev/null';
|
|
872
|
+
const result = await runProcess('git', ['diff', '--no-index', '--', left, right], { cwd: sourceRoot, allowFailure: true });
|
|
873
|
+
if (result.stdout.trim())
|
|
874
|
+
chunks.push(result.stdout);
|
|
875
|
+
}
|
|
876
|
+
return chunks.join('\n');
|
|
877
|
+
}
|
|
878
|
+
function filterWorkspaceChangedPaths(paths, plan) {
|
|
879
|
+
const changedPaths = [];
|
|
880
|
+
const ignoredChangedPaths = [];
|
|
881
|
+
for (const file of uniqueWorkspacePaths(paths)) {
|
|
882
|
+
if (isIgnoredWorkspaceChangedPath(file, plan))
|
|
883
|
+
ignoredChangedPaths.push(file);
|
|
884
|
+
else
|
|
885
|
+
changedPaths.push(file);
|
|
886
|
+
}
|
|
887
|
+
return { changedPaths, ignoredChangedPaths };
|
|
888
|
+
}
|
|
889
|
+
function isIgnoredWorkspaceChangedPath(file, plan) {
|
|
890
|
+
if (plan.mode !== 'copy' && plan.mode !== 'snapshot')
|
|
891
|
+
return false;
|
|
892
|
+
const ignored = [
|
|
893
|
+
...plan.excludes,
|
|
894
|
+
...plan.artifactIncludes,
|
|
895
|
+
...plan.linkPaths,
|
|
896
|
+
...(plan.linkNodeModules ? ['node_modules'] : []),
|
|
897
|
+
'agent-runs',
|
|
898
|
+
'.frontier-framework',
|
|
899
|
+
'dist',
|
|
900
|
+
'coverage'
|
|
901
|
+
];
|
|
902
|
+
return ignored.some((entry) => file === entry || file.startsWith(entry.replace(/\/$/, '') + '/'));
|
|
435
903
|
}
|
|
436
904
|
async function snapshotWorkspaceFiles(root) {
|
|
437
905
|
const snapshot = new Map();
|
|
@@ -625,6 +1093,68 @@ async function pathExists(file) {
|
|
|
625
1093
|
return false;
|
|
626
1094
|
}
|
|
627
1095
|
}
|
|
1096
|
+
async function resolvePidManifestPath(runPath) {
|
|
1097
|
+
const absolute = path.resolve(runPath);
|
|
1098
|
+
const stat = await fs.lstat(absolute).catch(() => undefined);
|
|
1099
|
+
if (stat?.isDirectory())
|
|
1100
|
+
return path.join(absolute, 'pids.json');
|
|
1101
|
+
if (path.basename(absolute) === 'swarm-results.json')
|
|
1102
|
+
return path.join(path.dirname(absolute), 'pids.json');
|
|
1103
|
+
return absolute;
|
|
1104
|
+
}
|
|
1105
|
+
async function resolveRunDirectory(runPath) {
|
|
1106
|
+
const absolute = path.resolve(runPath);
|
|
1107
|
+
const stat = await fs.lstat(absolute).catch(() => undefined);
|
|
1108
|
+
if (stat?.isDirectory())
|
|
1109
|
+
return absolute;
|
|
1110
|
+
if (path.basename(absolute) === 'swarm-results.json' || path.basename(absolute) === 'pids.json')
|
|
1111
|
+
return path.dirname(absolute);
|
|
1112
|
+
return path.dirname(absolute);
|
|
1113
|
+
}
|
|
1114
|
+
async function findFilesByName(root, name) {
|
|
1115
|
+
const out = [];
|
|
1116
|
+
async function walk(current) {
|
|
1117
|
+
const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
1118
|
+
for (const entry of entries) {
|
|
1119
|
+
const absolute = path.join(current, entry.name);
|
|
1120
|
+
if (entry.isDirectory()) {
|
|
1121
|
+
if (entry.name === 'collected' || entry.name === 'node_modules' || entry.name === '.git')
|
|
1122
|
+
continue;
|
|
1123
|
+
await walk(absolute);
|
|
1124
|
+
}
|
|
1125
|
+
else if (entry.isFile() && entry.name === name) {
|
|
1126
|
+
out.push(absolute);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
await walk(root);
|
|
1131
|
+
return out;
|
|
1132
|
+
}
|
|
1133
|
+
async function bundlePatchIsStale(bundle, mergePath, cwd) {
|
|
1134
|
+
const patchPath = resolveBundlePatchPath(bundle, mergePath);
|
|
1135
|
+
if (!patchPath || !await pathExists(patchPath))
|
|
1136
|
+
return false;
|
|
1137
|
+
const patch = await fs.readFile(patchPath, 'utf8').catch(() => '');
|
|
1138
|
+
if (!patch.trim())
|
|
1139
|
+
return false;
|
|
1140
|
+
const result = await runProcess('git', ['apply', '--check', patchPath], { cwd, allowFailure: true });
|
|
1141
|
+
return result.status !== 0;
|
|
1142
|
+
}
|
|
1143
|
+
function resolveBundlePatchPath(bundle, mergePath) {
|
|
1144
|
+
if (!bundle.patchPath)
|
|
1145
|
+
return undefined;
|
|
1146
|
+
return path.isAbsolute(bundle.patchPath) ? bundle.patchPath : path.resolve(path.dirname(mergePath), bundle.patchPath);
|
|
1147
|
+
}
|
|
1148
|
+
function classifyCodexCollectBucket(bundle, staleAgainstHead) {
|
|
1149
|
+
if (staleAgainstHead || bundle.staleAgainstHead || bundle.disposition === 'stale-against-head')
|
|
1150
|
+
return 'stale-against-head';
|
|
1151
|
+
if (bundle.disposition === 'rejected' || bundle.disposition === 'blocked' || bundle.commandsFailed.length > 0 || bundle.status === 'failed') {
|
|
1152
|
+
return 'failed-evidence';
|
|
1153
|
+
}
|
|
1154
|
+
if (bundle.disposition === 'auto-mergeable' && bundle.autoMergeable)
|
|
1155
|
+
return 'ready-to-apply';
|
|
1156
|
+
return 'needs-human-port';
|
|
1157
|
+
}
|
|
628
1158
|
async function readOptionalText(file) {
|
|
629
1159
|
try {
|
|
630
1160
|
return await fs.readFile(file, 'utf8');
|
|
@@ -662,4 +1192,24 @@ async function runProcess(command, args, options) {
|
|
|
662
1192
|
function tail(text, maxLines = 24) {
|
|
663
1193
|
return text.trim().split(/\r?\n/).filter(Boolean).slice(-maxLines);
|
|
664
1194
|
}
|
|
1195
|
+
function stableHash(value) {
|
|
1196
|
+
const text = stableStringify(value);
|
|
1197
|
+
let hash = 2166136261;
|
|
1198
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1199
|
+
hash ^= text.charCodeAt(index);
|
|
1200
|
+
hash = Math.imul(hash, 16777619);
|
|
1201
|
+
}
|
|
1202
|
+
return 'fnv1a32:' + (hash >>> 0).toString(16).padStart(8, '0');
|
|
1203
|
+
}
|
|
1204
|
+
function slug(value) {
|
|
1205
|
+
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'item';
|
|
1206
|
+
}
|
|
1207
|
+
function stableStringify(value) {
|
|
1208
|
+
if (value === null || typeof value !== 'object')
|
|
1209
|
+
return JSON.stringify(value);
|
|
1210
|
+
if (Array.isArray(value))
|
|
1211
|
+
return '[' + value.map(stableStringify).join(',') + ']';
|
|
1212
|
+
const object = value;
|
|
1213
|
+
return '{' + Object.keys(object).sort().map((key) => JSON.stringify(key) + ':' + stableStringify(object[key])).join(',') + '}';
|
|
1214
|
+
}
|
|
665
1215
|
//# sourceMappingURL=index.js.map
|