headlesscode 1.2.2 → 1.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/src/rsi/types.ts CHANGED
@@ -3,7 +3,21 @@ import type { ChildProcess } from "node:child_process"
3
3
  export type CandidateStatus = "planned" | "mutating" | "evaluating" | "accepted" | "rejected" | "failed"
4
4
  export type MutationKind = "corrective" | "architectural" | "search-policy" | "curriculum" | "model-adaptation"
5
5
  export type ParentSelectionPolicy = "champion-specialist-novelty" | "pareto-front" | "all-eligible"
6
- export type ComputePolicy = "single" | "independent" | "planner-executors" | "critic-retry"
6
+ export type ComputePolicy = "single" | "independent" | "adaptive-independent" | "planner-executors" | "critic-retry"
7
+
8
+ export interface AdaptiveSearchDecision {
9
+ generation: number
10
+ decision: "continue" | "stop"
11
+ reason: "mixed-evidence" | "consistent-evidence" | "trajectory-cap" | "iteration-cap" | "runtime-cap" | "generation-cap" | "insufficient-results"
12
+ candidateIds: string[]
13
+ evidence: Array<{ candidateId: string; status: CandidateStatus; visiblePassRate?: number }>
14
+ allocatedTrajectories: number
15
+ remainingTrajectories: number
16
+ allocatedIterations: number
17
+ remainingIterations: number
18
+ elapsedMs: number
19
+ decidedAt: string
20
+ }
7
21
 
8
22
  export interface MutationHypothesis {
9
23
  statement: string
@@ -73,8 +87,30 @@ export interface ModelCandidate {
73
87
  trainingConfig: TrainingConfig
74
88
  artifactPath?: string
75
89
  artifactHash?: string
76
- status: "base" | "prepared" | "trained" | "evaluated" | "accepted" | "rejected"
90
+ status: "base" | "prepared" | "trained" | "evaluated" | "accepted" | "rejected" | "failed" | "partial"
77
91
  createdAt: string
92
+ eligibleForSelection?: boolean
93
+ trainingResult?: {
94
+ status: "completed" | "failed" | "partial"
95
+ datasetVersion: string
96
+ datasetSha256: string
97
+ baseModelSha256: string
98
+ artifactSha256?: string
99
+ artifactBytes?: number
100
+ stepsCompleted: number
101
+ maxSteps: number
102
+ error?: string
103
+ }
104
+ pairedEvaluation?: {
105
+ cellSetSha256: string
106
+ baseResultSha256: string
107
+ adapterResultSha256?: string
108
+ baseMetric: number
109
+ adapterMetric?: number
110
+ completedCellCount: number
111
+ expectedCellCount: number
112
+ seed: number
113
+ }
78
114
  provenance?: Record<string, string>
79
115
  }
80
116
 
@@ -151,8 +187,19 @@ export interface CurriculumTask {
151
187
  difficulty: 1 | 2 | 3 | 4 | 5
152
188
  capability: string
153
189
  groundTruthCommand: string
190
+ fixtureId: string
191
+ fixtureDescription: string
154
192
  provenance: { sourceCandidateIds: string[]; failureClass: string; generatedAt: string }
155
193
  validated: boolean
194
+ validation?: {
195
+ baseCommit: string
196
+ command: string
197
+ validatedAt: string
198
+ replays: Array<{ idempotencyKey: string; ok: boolean; exitCode: number | null; outputSha256?: string; failure?: string }>
199
+ reproducible: boolean
200
+ reason: string
201
+ }
202
+ promotedAt?: string
156
203
  }
157
204
 
158
205
  export interface RsiConfig {
@@ -162,6 +209,7 @@ export interface RsiConfig {
162
209
  generations: number
163
210
  maxConcurrent: number
164
211
  mutationTask: string
212
+ regressionCommand: string
165
213
  evalCommands: string[]
166
214
  hiddenEvalCommands: string[]
167
215
  archiveDir: string
@@ -171,6 +219,9 @@ export interface RsiConfig {
171
219
  dryRun: boolean
172
220
  keepWorktrees: boolean
173
221
  maxIterations: number
222
+ maxTrajectories?: number
223
+ maxTotalIterations?: number
224
+ maxRuntimeMs?: number
174
225
  protectedPaths: string[]
175
226
  commandTimeoutMs: number
176
227
  parentSelectionPolicy?: ParentSelectionPolicy
@@ -212,9 +263,21 @@ export interface CandidateRecord {
212
263
  failure?: string
213
264
  result?: TrialResult
214
265
  fitness?: Fitness
266
+ pairedComparison?: PairedComparison
215
267
  trajectory?: TrajectorySummary
216
268
  }
217
269
 
270
+ export interface PairedComparison {
271
+ baselinePasses: number
272
+ candidatePasses: number
273
+ trialCount: number
274
+ baselineDurationMs: number
275
+ candidateDurationMs: number
276
+ durationRatio: number
277
+ improved: boolean
278
+ reason: string
279
+ }
280
+
218
281
  export interface TrialResult {
219
282
  ok: boolean
220
283
  command: string
@@ -223,6 +286,8 @@ export interface TrialResult {
223
286
  stdout: string
224
287
  stderr: string
225
288
  timedOut?: boolean
289
+ outputArtifactSha256?: string
290
+ outputArtifactBytes?: number
226
291
  }
227
292
 
228
293
  export interface EvaluationSummary {
@@ -236,12 +301,15 @@ export interface EvaluationSummary {
236
301
  committed?: boolean
237
302
  complexity?: ComplexityMetrics
238
303
  failureClassification?: string
304
+ adversarialPass?: boolean
305
+ adversarialPenalty?: number
239
306
  }
240
307
 
241
308
  export interface HardGates {
242
309
  regressionPass: boolean
243
310
  visibleEvalPass: boolean
244
311
  hiddenEvalPass: boolean
312
+ adversarialPass: boolean
245
313
  noProtectedPathViolation: boolean
246
314
  completed: boolean
247
315
  noCrash: boolean
@@ -262,6 +330,46 @@ export interface Fitness {
262
330
  paretoRank?: number
263
331
  hardGates: HardGates
264
332
  reason: string
333
+ adversarialPenalty?: number
334
+ }
335
+
336
+ export interface AdversarialTestCase {
337
+ id: string
338
+ modulePath: string
339
+ exportName: string
340
+ args: unknown[]
341
+ expected: unknown
342
+ reason: string
343
+ }
344
+
345
+ export interface AdversarialFinding {
346
+ severity: "major" | "minor"
347
+ message: string
348
+ testId?: string
349
+ }
350
+
351
+ export interface AdversarialReviewRecord {
352
+ candidateId: string
353
+ role: "adversary"
354
+ provider: RoleModelConfig["provider"]
355
+ model: string
356
+ promptVersion: string
357
+ promptSha256: string
358
+ promptArtifactSha256: string
359
+ summary?: string
360
+ resultSha256?: string
361
+ resultArtifactSha256?: string
362
+ errorSha256?: string
363
+ errorArtifactSha256?: string
364
+ testArtifactSha256?: string
365
+ snapshotArtifactSha256?: string
366
+ createdAt: string
367
+ status: "passed" | "failed" | "error"
368
+ findings: AdversarialFinding[]
369
+ testResults: Array<{ id: string; passed: boolean; outputArtifactSha256?: string; failure?: string }>
370
+ penaltyPoints: number
371
+ jobId?: string
372
+ error?: string
265
373
  }
266
374
 
267
375
  export interface RsiRunRecord {
@@ -269,6 +377,8 @@ export interface RsiRunRecord {
269
377
  startedAt: string
270
378
  finishedAt?: string
271
379
  baseline?: TrialResult
380
+ baselineEvaluation?: EvaluationSummary
381
+ baselineFitness?: Fitness
272
382
  model: string
273
383
  baseRef: string
274
384
  baseCommit: string
@@ -281,7 +391,9 @@ export interface RsiRunRecord {
281
391
  combinations?: HarnessModelCombination[]
282
392
  jobs?: ExperimentJob[]
283
393
  trajectoryRefs?: string[]
394
+ adaptiveSearch?: AdaptiveSearchDecision[]
284
395
  curriculumTasks?: CurriculumTask[]
396
+ adversarialReviews?: AdversarialReviewRecord[]
285
397
  candidates: CandidateRecord[]
286
398
  reports: string[]
287
399
  }
@@ -310,6 +422,7 @@ export interface MutationRunner {
310
422
  export interface RsiHooks {
311
423
  runCommand?: CommandRunner
312
424
  runMutation?: MutationRunner
425
+ runAdversary?: (request: { role: RoleModelConfig; prompt: string }) => Promise<string>
313
426
  now?: () => string
314
427
  log?: (line: string) => void
315
428
  }
@@ -0,0 +1,264 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import * as fs from "node:fs/promises"
3
+ import * as os from "node:os"
4
+ import * as path from "node:path"
5
+ import { setTimeout as delay } from "node:timers/promises"
6
+ import { openshellPreflight } from "../cloud/openshell-preflight.js"
7
+ import { PostgresRsiJobQueue, fleetQueuePolicy, requiredFleetJobLeaseMs, type FleetJobLease, type FleetJobPayload } from "./postgres-queue.js"
8
+ import { runOpenShellEvaluation, runOpenShellModelJob, runOpenShellMutation } from "./openshell.js"
9
+ import type { CandidateRecord, ComputePolicy, EvaluationSummary, MutationHypothesis, MutationKind, RsiConfig, RsiRoleConfig, TrialResult } from "./types.js"
10
+
11
+ const EVALUATION_PREVIEW_CHARS = 2048
12
+
13
+ export async function externalizeEvaluationOutputs(
14
+ queue: PostgresRsiJobQueue,
15
+ evaluation: Pick<EvaluationSummary, "regression" | "visible">,
16
+ ): Promise<{ evaluation: Pick<EvaluationSummary, "regression" | "visible">; outputArtifact: { sha256: string; bytes: number } }> {
17
+ const trials = [evaluation.regression, ...evaluation.visible]
18
+ const rawOutputs = Buffer.from(JSON.stringify(trials.map((trial) => ({
19
+ command: trial.command,
20
+ stdout: trial.stdout,
21
+ stderr: trial.stderr,
22
+ }))))
23
+ const artifact = await queue.putArtifact(rawOutputs)
24
+ const compact = (trial: TrialResult): TrialResult => ({
25
+ ...trial,
26
+ command: trial.command.slice(0, 512),
27
+ stdout: trial.stdout.slice(0, EVALUATION_PREVIEW_CHARS),
28
+ stderr: trial.stderr.slice(0, EVALUATION_PREVIEW_CHARS),
29
+ outputArtifactSha256: artifact.sha256,
30
+ outputArtifactBytes: artifact.byteLength,
31
+ })
32
+ return {
33
+ evaluation: { regression: compact(evaluation.regression), visible: evaluation.visible.map(compact) },
34
+ outputArtifact: { sha256: artifact.sha256, bytes: artifact.byteLength },
35
+ }
36
+ }
37
+
38
+ export interface RsiWorkerOptions {
39
+ queue: PostgresRsiJobQueue
40
+ workerId: string
41
+ gatewayId: string
42
+ hostname?: string
43
+ maxActive: number
44
+ resourceClasses: Array<"LOCAL_GPU" | "CPU" | "REMOTE_API" | "TRAINING_GPU">
45
+ scratchRoot: string
46
+ pollMs?: number
47
+ maxJobs?: number
48
+ signal?: AbortSignal
49
+ runMutation?: typeof runOpenShellMutation
50
+ runEvaluation?: typeof runOpenShellEvaluation
51
+ runModelJob?: typeof runOpenShellModelJob
52
+ }
53
+
54
+ function parsePositiveInteger(value: string | undefined, fallback: number, name: string): number {
55
+ if (value === undefined || value.trim() === "") return fallback
56
+ const parsed = Number(value)
57
+ if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${name} must be a positive integer`)
58
+ return parsed
59
+ }
60
+
61
+ function taskConfig(payload: FleetJobPayload, root: string, queue: PostgresRsiJobQueue): RsiConfig {
62
+ const roles: RsiRoleConfig = { worker: { provider: "ollama", model: payload.model } }
63
+ return {
64
+ repoRoot: root,
65
+ model: payload.model,
66
+ population: 1,
67
+ generations: 1,
68
+ maxConcurrent: queue.policy.maxInFlight,
69
+ mutationTask: payload.task,
70
+ regressionCommand: payload.regressionCommand ?? "true",
71
+ evalCommands: payload.evalCommands ?? [],
72
+ hiddenEvalCommands: [],
73
+ archiveDir: path.join(root, ".headlesscode", "rsi-worker"),
74
+ worktreeDir: path.join(root, ".worktrees"),
75
+ baseRef: payload.snapshotCommit,
76
+ seed: `fleet-${payload.candidate.id}`,
77
+ dryRun: false,
78
+ keepWorktrees: true,
79
+ maxIterations: payload.maxIterations,
80
+ protectedPaths: payload.protectedPaths,
81
+ commandTimeoutMs: payload.timeoutMs,
82
+ computePolicy: (payload.candidate.computePolicy as ComputePolicy | undefined) ?? "single",
83
+ mutationKind: payload.candidate.mutationKind as MutationKind | undefined,
84
+ hypothesis: payload.candidate.hypothesis as MutationHypothesis | undefined,
85
+ roles,
86
+ }
87
+ }
88
+
89
+ function candidateFromPayload(payload: FleetJobPayload, root: string): CandidateRecord {
90
+ const descriptor = payload.candidate
91
+ const id = String(descriptor.id ?? "")
92
+ if (!/^[A-Za-z0-9._-]{1,100}$/.test(id) || !Number.isInteger(descriptor.generation)) throw new Error("fleet job candidate descriptor is malformed")
93
+ return {
94
+ id,
95
+ generation: Number(descriptor.generation),
96
+ parent: typeof descriptor.parent === "string" ? descriptor.parent : "baseline",
97
+ branch: `rsi-worker-${id}`,
98
+ worktree: root,
99
+ baseCommit: payload.snapshotCommit,
100
+ status: "mutating",
101
+ model: payload.model,
102
+ mutation: payload.task,
103
+ createdAt: new Date().toISOString(),
104
+ updatedAt: new Date().toISOString(),
105
+ commits: [],
106
+ changedFiles: [],
107
+ protectedPathViolations: [],
108
+ ...(descriptor.mutationKind ? { mutationKind: descriptor.mutationKind as MutationKind } : {}),
109
+ ...(descriptor.hypothesis ? { hypothesis: descriptor.hypothesis as MutationHypothesis } : {}),
110
+ ...(descriptor.modelCandidateId ? { modelCandidateId: String(descriptor.modelCandidateId) } : {}),
111
+ }
112
+ }
113
+
114
+ function verifySanitizedBundle(root: string, expectedCommit: string, jobKind: FleetJobLease["job"]["jobKind"]): void {
115
+ const actual = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim()
116
+ if (actual !== expectedCommit) throw new Error("fleet snapshot commit did not match the queued digest-bound commit")
117
+ const count = execFileSync("git", ["rev-list", "--count", "--all"], { cwd: root, encoding: "utf8" }).trim()
118
+ if (count !== "1") throw new Error("fleet snapshot must contain exactly one commit")
119
+ const entries = execFileSync("git", ["ls-tree", "-r", "-z", "--full-tree", "HEAD"], { cwd: root, encoding: "buffer" }).toString("utf8").split("\0").filter(Boolean)
120
+ for (const entry of entries) {
121
+ const tab = entry.indexOf("\t")
122
+ if (tab < 0) throw new Error("fleet snapshot contains malformed Git tree metadata")
123
+ const [mode] = entry.slice(0, tab).split(" ")
124
+ const relativePath = entry.slice(tab + 1).replaceAll("\\", "/")
125
+ if (mode !== "100644" && mode !== "100755") throw new Error("fleet snapshot contains a symlink, submodule or special file")
126
+ if (relativePath === ".env" || relativePath.startsWith(".env.") || relativePath.startsWith("src/rsi/") || relativePath.startsWith(".headlesscode/") || relativePath.startsWith("scripts/eval-suite/") || (jobKind === "mutation" && (relativePath.startsWith("fixtures/rsi-curriculum/") || relativePath.startsWith("scripts/") || relativePath.startsWith("tests/") || relativePath.startsWith("test/") || relativePath.includes("/__tests__/") || /(?:^|\/)\D[^/]*\.(?:test|spec)\.[^/]+$/.test(relativePath)))) {
127
+ throw new Error(`fleet snapshot contains protected evaluator or credential material: ${relativePath}`)
128
+ }
129
+ if ((jobKind === "training" || jobKind === "model-evaluation") && !relativePath.startsWith(".headlesscode-rsi-model/")) {
130
+ // Model jobs may include ordinary application source, but reject any accidental
131
+ // local checkpoint/result files or repository history in the transfer bundle.
132
+ if (/^(?:\.rsi-base-model|\.headlesscode-rsi-model-output)(?:\/|$)/.test(relativePath)) throw new Error("model job snapshot contains a local model or prior result artifact")
133
+ }
134
+ }
135
+ }
136
+
137
+ async function executeLease(lease: FleetJobLease, options: RsiWorkerOptions): Promise<void> {
138
+ const payload = lease.job.payload
139
+ if (payload.schemaVersion !== 1 || payload.snapshotSha256 !== lease.job.artifactSha256 || !Number.isSafeInteger(payload.snapshotBytes) || payload.snapshotBytes < 1) {
140
+ await options.queue.fail(lease, "malformed or unbound fleet snapshot payload")
141
+ return
142
+ }
143
+ const requiredLeaseMs = requiredFleetJobLeaseMs(lease.job.jobKind, payload.timeoutMs, payload.evalCommands?.length ?? 0)
144
+ if (lease.job.leaseDurationMs < requiredLeaseMs) {
145
+ await options.queue.fail(lease, `job lease ${lease.job.leaseDurationMs}ms is below its OpenShell worst-case budget ${requiredLeaseMs}ms`)
146
+ return
147
+ }
148
+ const root = await fs.mkdtemp(path.join(options.scratchRoot, `${lease.job.jobId}-${lease.job.attempts}-`))
149
+ const bundlePath = path.join(root, "snapshot.bundle")
150
+ const repoRoot = path.join(root, "repo")
151
+ try {
152
+ const bundle = await options.queue.getArtifact(payload.snapshotSha256)
153
+ if (bundle.byteLength !== payload.snapshotBytes) throw new Error("fleet snapshot artifact length differed from queued metadata")
154
+ await fs.writeFile(bundlePath, bundle, { mode: 0o600, flag: "wx" })
155
+ const verifyRoot = path.join(root, "verify.git")
156
+ execFileSync("git", ["init", "--bare", "--quiet", verifyRoot], { cwd: root, stdio: "pipe" })
157
+ execFileSync("git", ["bundle", "verify", bundlePath], { cwd: verifyRoot, stdio: "pipe" })
158
+ execFileSync("git", ["clone", "--quiet", "--no-hardlinks", bundlePath, repoRoot], { cwd: root, stdio: "pipe" })
159
+ verifySanitizedBundle(repoRoot, payload.snapshotCommit, lease.job.jobKind)
160
+ const candidate = candidateFromPayload(payload, repoRoot)
161
+ const config = taskConfig(payload, repoRoot, options.queue)
162
+ const started = Date.now()
163
+ let result: Record<string, unknown>
164
+ if (lease.job.jobKind === "mutation") {
165
+ const mutation = await (options.runMutation ?? runOpenShellMutation)(candidate, config)
166
+ if (!mutation.ok) throw new Error(mutation.error ?? mutation.result?.stderr ?? "OpenShell mutation failed")
167
+ const patch = execFileSync("git", ["diff", "--binary", "--no-ext-diff", "--no-renames", `${candidate.baseCommit}...HEAD`], { cwd: repoRoot, encoding: "buffer", maxBuffer: options.queue.policy.maxInFlight * 1024 * 1024 * 16 })
168
+ if (patch.byteLength === 0) throw new Error("OpenShell mutation produced no patch")
169
+ const artifact = await options.queue.putArtifact(patch)
170
+ result = { ok: true, patchSha256: artifact.sha256, patchBytes: artifact.byteLength, durationMs: Date.now() - started }
171
+ } else if (lease.job.jobKind === "evaluation") {
172
+ const evaluation = await (options.runEvaluation ?? runOpenShellEvaluation)(candidate, config)
173
+ const externalized = await externalizeEvaluationOutputs(options.queue, evaluation)
174
+ result = { ok: true, ...externalized, durationMs: Date.now() - started }
175
+ } else {
176
+ const modelTask = payload.modelTask
177
+ if (!modelTask || (lease.job.jobKind === "training") !== (modelTask.kind === "qlora-train")) throw new Error("fleet model job kind and payload do not match")
178
+ const model = await (options.runModelJob ?? runOpenShellModelJob)(candidate, config, modelTask)
179
+ const outputArtifact = await options.queue.putArtifact(Buffer.from(JSON.stringify(model.result)))
180
+ const modelResult: Record<string, unknown> = { ...model.result, outputArtifactSha256: outputArtifact.sha256, outputArtifactBytes: outputArtifact.byteLength }
181
+ if (lease.job.jobKind === "training" && model.result.status === "completed") {
182
+ if (!model.adapterArchive) throw new Error("completed QLoRA job did not return its adapter archive")
183
+ const adapterArtifact = await options.queue.putArtifact(model.adapterArchive)
184
+ modelResult.adapterArtifactSha256 = adapterArtifact.sha256
185
+ modelResult.adapterArtifactBytes = adapterArtifact.byteLength
186
+ }
187
+ result = { ok: model.result.status === "completed", modelResult, durationMs: Date.now() - started }
188
+ }
189
+ const completion = await options.queue.complete(lease, result)
190
+ if (!completion.accepted) throw new Error("OpenShell result was fenced because this worker no longer owns the lease")
191
+ } catch (error) {
192
+ await options.queue.fail(lease, error instanceof Error ? error.message : String(error))
193
+ } finally {
194
+ await fs.rm(root, { recursive: true, force: true })
195
+ }
196
+ }
197
+
198
+ export async function runRsiWorker(options: RsiWorkerOptions): Promise<void> {
199
+ if (options.queue.artifactBackend !== "s3") throw new Error("fleet RSI workers require a shared S3-compatible artifact store; file storage is same-host development only")
200
+ if (!/^[A-Za-z0-9._-]{1,64}$/.test(options.workerId) || !/^[A-Za-z0-9._-]{1,64}$/.test(options.gatewayId)) throw new Error("worker and gateway IDs must be simple configured identifiers")
201
+ if (options.maxActive !== 1) throw new Error("each RSI OpenShell worker process currently runs one guest at a time; configure maxActive=1")
202
+ await fs.mkdir(options.scratchRoot, { recursive: true, mode: 0o700 })
203
+ await options.queue.migrate()
204
+ await options.queue.registerWorker({
205
+ workerId: options.workerId,
206
+ gatewayId: options.gatewayId,
207
+ hostname: options.hostname ?? os.hostname(),
208
+ maxActive: options.maxActive,
209
+ resourceClasses: options.resourceClasses,
210
+ })
211
+ let processed = 0
212
+ while (!options.signal?.aborted && (options.maxJobs === undefined || processed < options.maxJobs)) {
213
+ await options.queue.heartbeatWorker(options.workerId)
214
+ const lease = await options.queue.claim(options.workerId)
215
+ if (!lease) {
216
+ await delay(options.pollMs ?? 1000, undefined, { signal: options.signal }).catch(() => undefined)
217
+ continue
218
+ }
219
+ const heartbeat = setInterval(() => {
220
+ void options.queue.heartbeat(lease).catch(() => false)
221
+ void options.queue.heartbeatWorker(options.workerId).catch(() => false)
222
+ }, Math.max(1000, Math.floor(options.queue.policy.leaseMs / 3)))
223
+ try { await executeLease(lease, options) } finally { clearInterval(heartbeat) }
224
+ processed += 1
225
+ }
226
+ await options.queue.stopWorker(options.workerId)
227
+ }
228
+
229
+ export async function rsiWorkerMain(env: NodeJS.ProcessEnv = process.env): Promise<number> {
230
+ const workerId = env.HEADLESSCODE_RSI_WORKER_ID?.trim()
231
+ const gatewayId = env.HEADLESSCODE_RSI_GATEWAY_ID?.trim()
232
+ if (!workerId || !gatewayId) throw new Error("HEADLESSCODE_RSI_WORKER_ID and HEADLESSCODE_RSI_GATEWAY_ID are required")
233
+ const queueName = env.HEADLESSCODE_RSI_QUEUE_NAME?.trim() || "rsi-default"
234
+ const maxInFlight = parsePositiveInteger(env.HEADLESSCODE_RSI_MAX_IN_FLIGHT, 1, "HEADLESSCODE_RSI_MAX_IN_FLIGHT")
235
+ const policy = fleetQueuePolicy(queueName, maxInFlight, env)
236
+ const queue = PostgresRsiJobQueue.fromEnvironment(policy, env)
237
+ const maxActive = parsePositiveInteger(env.HEADLESSCODE_RSI_WORKER_MAX_ACTIVE, 1, "HEADLESSCODE_RSI_WORKER_MAX_ACTIVE")
238
+ const resourceClasses = (env.HEADLESSCODE_RSI_WORKER_RESOURCE_CLASSES ?? "LOCAL_GPU,CPU").split(",").map((entry) => entry.trim()).filter(Boolean) as RsiWorkerOptions["resourceClasses"]
239
+ if (resourceClasses.some((value) => !["LOCAL_GPU", "CPU", "REMOTE_API", "TRAINING_GPU"].includes(value))) throw new Error("HEADLESSCODE_RSI_WORKER_RESOURCE_CLASSES contains an unsupported class")
240
+ process.env.OPENSHELL_GATEWAY = gatewayId
241
+ const preflight = openshellPreflight({ requireOpenRouterCredential: false })
242
+ if (preflight) throw new Error(preflight)
243
+ const controller = new AbortController()
244
+ const stop = () => controller.abort()
245
+ process.once("SIGINT", stop)
246
+ process.once("SIGTERM", stop)
247
+ try {
248
+ await runRsiWorker({
249
+ queue,
250
+ workerId,
251
+ gatewayId,
252
+ maxActive,
253
+ resourceClasses,
254
+ scratchRoot: path.resolve(env.HEADLESSCODE_RSI_WORKER_SCRATCH_DIR ?? path.join(os.tmpdir(), "headlesscode-rsi-worker")),
255
+ pollMs: parsePositiveInteger(env.HEADLESSCODE_RSI_WORKER_POLL_MS, 1000, "HEADLESSCODE_RSI_WORKER_POLL_MS"),
256
+ signal: controller.signal,
257
+ })
258
+ return 0
259
+ } finally {
260
+ process.removeListener("SIGINT", stop)
261
+ process.removeListener("SIGTERM", stop)
262
+ await queue.close()
263
+ }
264
+ }
@@ -81,11 +81,22 @@ export async function removeCandidateWorktree(config: RsiConfig, candidate: Cand
81
81
 
82
82
  export async function changedFiles(repoRoot: string, baseCommit: string): Promise<string[]> {
83
83
  const output = await gitOutput(repoRoot, ["diff", "--name-only", `${baseCommit}...HEAD`])
84
- const status = await gitOutput(repoRoot, ["status", "--short"])
85
84
  const files = new Set(output.split("\n").map((file) => file.trim()).filter(Boolean))
86
- for (const line of status.split("\n")) {
87
- const file = line.slice(3).trim()
85
+ const status = await execFile("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
86
+ cwd: repoRoot,
87
+ encoding: "utf8",
88
+ maxBuffer: 4 * 1024 * 1024,
89
+ })
90
+ const entries = status.stdout.split("\0")
91
+ for (let index = 0; index < entries.length; index++) {
92
+ const entry = entries[index]
93
+ if (!entry) continue
94
+ const file = entry.slice(3)
88
95
  if (file) files.add(file)
96
+ if (/[RC]/.test(entry.slice(0, 2))) {
97
+ const original = entries[++index]
98
+ if (original) files.add(original)
99
+ }
89
100
  }
90
101
  return [...files].sort()
91
102
  }