headlesscode 1.0.3 → 1.2.1
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 +100 -56
- package/package.json +1 -1
- package/src/cli.ts +115 -4
- package/src/engine/claims.ts +503 -0
- package/src/engine/events.ts +32 -0
- package/src/engine/lazy-tools.ts +30 -9
- package/src/engine/loop.ts +473 -11
- package/src/engine/prompt.ts +5 -1
- package/src/engine/types.ts +36 -0
- package/src/rsi/archive.ts +129 -0
- package/src/rsi/config.ts +312 -0
- package/src/rsi/controller.ts +268 -0
- package/src/rsi/curriculum.ts +68 -0
- package/src/rsi/evaluator.ts +106 -0
- package/src/rsi/fitness.ts +64 -0
- package/src/rsi/index.ts +16 -0
- package/src/rsi/models.ts +89 -0
- package/src/rsi/mutation.ts +77 -0
- package/src/rsi/reports.ts +47 -0
- package/src/rsi/roles.ts +37 -0
- package/src/rsi/sandbox.ts +10 -0
- package/src/rsi/search.ts +32 -0
- package/src/rsi/selection.ts +132 -0
- package/src/rsi/trajectory.ts +143 -0
- package/src/rsi/types.ts +317 -0
- package/src/rsi/workspace.ts +96 -0
- package/src/tools/executor.ts +79 -3
package/src/engine/prompt.ts
CHANGED
|
@@ -660,7 +660,11 @@ export function buildHeadlessConventions(mode: string, customModes: ModeConfig[]
|
|
|
660
660
|
- Commit before finishing: real, working changes must be committed via \`git add\` + \`git commit\` BEFORE calling attempt_completion. Use one commit per logical change with a descriptive message, matching this repo's normal style (run \`git log --oneline\` for examples). If you genuinely have no changes to commit (e.g. a read-only investigation), finish without committing.
|
|
661
661
|
- Test selection during iterative work: after editing a file, run the SPECIFIC test file(s) for what you changed (the \`run_tests\` tool, or \`npx tsx <test-file>\` directly) instead of the full suite on every edit. The full \`npm test\` is still required once, right before attempt_completion, for real confidence.
|
|
662
662
|
- Batch same-file diffs: when several changes target the SAME file, include them as separate SEARCH/REPLACE blocks in ONE apply_diff call — and after any successful edit, re-read the file before composing further diffs, because its content has changed.
|
|
663
|
-
- Waiting on an external check (CI run, registry, live service) is legitimate verification work, but every wait call still counts against your iteration budget: prefer ONE long-running command with an explicit \`timeout\` — e.g. \`gh run watch <id> --interval 30 --exit-status\` — over repeated \`sleep N && gh run list\` polls. (Issue #119: polling burns iterations with no token spend.)
|
|
663
|
+
- Waiting on an external check (CI run, registry, live service) is legitimate verification work, but every wait call still counts against your iteration budget: prefer ONE long-running command with an explicit \`timeout\` — e.g. \`gh run watch <id> --interval 30 --exit-status\` — over repeated \`sleep N && gh run list\` polls. (Issue #119: polling burns iterations with no token spend.)
|
|
664
|
+
- Check the real workspace before assuming a path exists: when a task resembles something you've worked on before, use \`list_files\`/\`read_file\` on THIS workspace first rather than guessing a path from a similar prior task — a plausible-looking guess against the wrong repo/scratch layout still fails, and repeating the same wrong guess wastes the iteration budget without new information.
|
|
665
|
+
- If the same command fails twice in a row, change what you do next — read the real error, try something different, or (if there's genuinely no path forward) report the honest state via \`attempt_completion\` rather than retrying the identical call or repeating the same diagnosis in prose without acting on it. A real, honest "this doesn't work and here's why" is always a valid, complete answer; a stalled loop is not.
|
|
666
|
+
- When \`attempt_completion\` is deferred (rejected because a check hasn't actually passed yet, an edit hasn't succeeded, or a claim isn't backed by real evidence), your VERY NEXT action must be the real fix the deferral message describes — a new command, a corrected edit, or gathering the missing evidence — never another \`attempt_completion\` call on its own. Retrying completion without first taking that corrective action just repeats the same rejection and burns the iteration budget with no new information; it will keep being deferred for the identical reason until you actually address it.
|
|
667
|
+
- Starting a long-running process (a dev server, a background listener) and then checking it — do this as TWO separate \`execute_command\` calls, not one. First call: run the command DIRECTLY, with no trailing \`&\` and no \`bash -c '... &'\` wrapper — e.g. \`python3 server.py\` with \`timeout: 2\`. The tool's own contract already keeps it running in the background past the timeout (you get the output captured so far, not an error); it does NOT need you to background it yourself. Second, separate call: the actual check (\`curl ...\`, \`nc -z ...\`). Manually backgrounding with \`&\` inside a \`bash -c\` (or any subshell) is a real, observed failure mode — the process gets backgrounded INSIDE that subshell, which then exits once the \`&\` returns, and the process can be reaped along with it before your next command in the same chain ever reaches it. If a check right after starting something fails, don't keep editing the file you just started — first suspect the START command's shape.`
|
|
664
668
|
}
|
|
665
669
|
|
|
666
670
|
// ─── Tool selection ──────────────────────────────────────────────────────────
|
package/src/engine/types.ts
CHANGED
|
@@ -189,6 +189,15 @@ export interface ToolContext {
|
|
|
189
189
|
* executors (tests, reviewer/QA).
|
|
190
190
|
*/
|
|
191
191
|
guardLargeOverwrites?: boolean
|
|
192
|
+
/**
|
|
193
|
+
* See HeadlessSessionConfig.disableReadFileCache (loop.ts) for the full
|
|
194
|
+
* writeup. When true, read_file/list_files always serve real content —
|
|
195
|
+
* the session-scoped "[cache] unchanged, re-read the earlier result"
|
|
196
|
+
* short-circuit (src/tools/executor.ts) never fires. Absent/false for
|
|
197
|
+
* cloud sessions and bare executors (tests, reviewer/QA), where that
|
|
198
|
+
* short-circuit's real token-cost savings still apply.
|
|
199
|
+
*/
|
|
200
|
+
disableReadFileCache?: boolean
|
|
192
201
|
/**
|
|
193
202
|
* Decision escalation (ask_followup_question, see src/tools/executor.ts):
|
|
194
203
|
* how long to block waiting for `.harness.decision-answer` before falling
|
|
@@ -269,6 +278,24 @@ export interface SessionBudgetUsage {
|
|
|
269
278
|
model: string
|
|
270
279
|
}
|
|
271
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Evidence-gated completion (fabrication fix, 2026-09-01): the outcome of
|
|
283
|
+
* the claim-verification pass that ran against the final attempt_completion
|
|
284
|
+
* result when evidenceRequiredCompletion was on (see src/engine/claims.ts).
|
|
285
|
+
* Present on a successful SessionResult ONLY when the gate actually ran and
|
|
286
|
+
* found at least one machine-checkable claim — its presence is the caller's
|
|
287
|
+
* signal that "success" means "claims independently verified against ground
|
|
288
|
+
* truth", not "the model said so". Absent when the gate didn't run.
|
|
289
|
+
*/
|
|
290
|
+
export interface SessionCompletionVerification {
|
|
291
|
+
/** Number of machine-checkable claims extracted from the result text. */
|
|
292
|
+
claimsChecked: number
|
|
293
|
+
/** Number of claims independently confirmed against the real world. */
|
|
294
|
+
claimsPassed: number
|
|
295
|
+
/** Number of claims that could not be confirmed (0 on an accepted completion). */
|
|
296
|
+
claimsUnverified: number
|
|
297
|
+
}
|
|
298
|
+
|
|
272
299
|
export interface SessionResult {
|
|
273
300
|
status: SessionStatus
|
|
274
301
|
result?: string
|
|
@@ -277,6 +304,15 @@ export interface SessionResult {
|
|
|
277
304
|
reason?: string
|
|
278
305
|
iterations: number
|
|
279
306
|
toolCalls: number
|
|
307
|
+
/**
|
|
308
|
+
* Evidence-gated completion (fabrication fix): present when the
|
|
309
|
+
* completion's claims were independently verified against ground truth
|
|
310
|
+
* (file existence, real command re-runs, serial logs, git history) — see
|
|
311
|
+
* SessionCompletionVerification. Absent when the gate didn't run (flag
|
|
312
|
+
* off, no claims in the result) — callers must NOT treat that as
|
|
313
|
+
* "verified", only as "not checked".
|
|
314
|
+
*/
|
|
315
|
+
verification?: SessionCompletionVerification
|
|
280
316
|
/**
|
|
281
317
|
* Issue #34: absolute path to the session's complete final report
|
|
282
318
|
* (`<workspaceRoot>/.headlesscode/reports/<sessionId>.md`), present when
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises"
|
|
2
|
+
import * as path from "node:path"
|
|
3
|
+
import type { CandidateRecord, RsiArchive, RsiRunRecord } from "./types.js"
|
|
4
|
+
|
|
5
|
+
export function archivePath(archiveDir: string): string {
|
|
6
|
+
return path.join(archiveDir, "archive.json")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function emptyArchive(): RsiArchive {
|
|
10
|
+
return {
|
|
11
|
+
schemaVersion: 2,
|
|
12
|
+
updatedAt: new Date(0).toISOString(),
|
|
13
|
+
runs: [],
|
|
14
|
+
activeRuns: [],
|
|
15
|
+
candidates: [],
|
|
16
|
+
modelCandidates: [],
|
|
17
|
+
combinations: [],
|
|
18
|
+
jobs: [],
|
|
19
|
+
trajectoryRefs: [],
|
|
20
|
+
curriculumTasks: [],
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeRun(run: RsiRunRecord): RsiRunRecord {
|
|
25
|
+
return {
|
|
26
|
+
...run,
|
|
27
|
+
candidates: Array.isArray(run.candidates) ? run.candidates : [],
|
|
28
|
+
reports: Array.isArray(run.reports) ? run.reports : [],
|
|
29
|
+
selectedCandidates: run.selectedCandidates ?? (run.selected ? [run.selected] : []),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function migrate(raw: Partial<RsiArchive> & { schemaVersion?: number }): RsiArchive {
|
|
34
|
+
const archive = emptyArchive()
|
|
35
|
+
archive.updatedAt = raw.updatedAt ?? archive.updatedAt
|
|
36
|
+
archive.runs = (Array.isArray(raw.runs) ? raw.runs : []).map((run) => normalizeRun(run))
|
|
37
|
+
archive.activeRuns = (Array.isArray(raw.activeRuns) ? raw.activeRuns : []).map((run) => normalizeRun(run))
|
|
38
|
+
archive.candidates = Array.isArray(raw.candidates) ? raw.candidates : archive.runs.flatMap((run) => run.candidates)
|
|
39
|
+
archive.modelCandidates = Array.isArray(raw.modelCandidates) ? raw.modelCandidates : []
|
|
40
|
+
archive.combinations = Array.isArray(raw.combinations) ? raw.combinations : []
|
|
41
|
+
archive.jobs = Array.isArray(raw.jobs) ? raw.jobs : []
|
|
42
|
+
archive.trajectoryRefs = Array.isArray(raw.trajectoryRefs) ? raw.trajectoryRefs : []
|
|
43
|
+
archive.curriculumTasks = Array.isArray(raw.curriculumTasks) ? raw.curriculumTasks : []
|
|
44
|
+
return archive
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function readArchive(archiveDir: string): Promise<RsiArchive> {
|
|
48
|
+
try {
|
|
49
|
+
const raw = await fs.readFile(archivePath(archiveDir), "utf8")
|
|
50
|
+
const parsed = JSON.parse(raw) as Record<string, unknown> & { schemaVersion?: number }
|
|
51
|
+
if ((parsed.schemaVersion === 1 || parsed.schemaVersion === 2) && Array.isArray(parsed.runs)) {
|
|
52
|
+
return migrate(parsed as Partial<RsiArchive> & { schemaVersion?: number })
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// A missing or malformed archive starts a new durable history. The next
|
|
56
|
+
// write makes the state explicit and inspectable.
|
|
57
|
+
}
|
|
58
|
+
return emptyArchive()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function writeArchive(archiveDir: string, archive: RsiArchive): Promise<void> {
|
|
62
|
+
await fs.mkdir(archiveDir, { recursive: true })
|
|
63
|
+
const destination = archivePath(archiveDir)
|
|
64
|
+
const temporary = `${destination}.tmp-${process.pid}`
|
|
65
|
+
await fs.writeFile(temporary, `${JSON.stringify(archive, null, 2)}\n`, "utf8")
|
|
66
|
+
await fs.rename(temporary, destination)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function appendRun(archiveDir: string, run: RsiRunRecord): Promise<RsiArchive> {
|
|
70
|
+
const archive = await readArchive(archiveDir)
|
|
71
|
+
archive.activeRuns = archive.activeRuns.filter((entry) => entry.runId !== run.runId)
|
|
72
|
+
archive.runs = [...archive.runs.filter((entry) => entry.runId !== run.runId), normalizeRun(run)]
|
|
73
|
+
for (const candidate of run.candidates) {
|
|
74
|
+
const index = archive.candidates.findIndex((entry) => entry.id === candidate.id)
|
|
75
|
+
if (index >= 0) archive.candidates[index] = candidate
|
|
76
|
+
else archive.candidates.push(candidate)
|
|
77
|
+
}
|
|
78
|
+
for (const model of run.modelCandidates ?? []) {
|
|
79
|
+
if (!archive.modelCandidates.some((entry) => entry.id === model.id)) archive.modelCandidates.push(model)
|
|
80
|
+
}
|
|
81
|
+
for (const combination of run.combinations ?? []) {
|
|
82
|
+
const index = archive.combinations.findIndex((entry) => entry.id === combination.id)
|
|
83
|
+
if (index >= 0) archive.combinations[index] = combination
|
|
84
|
+
else archive.combinations.push(combination)
|
|
85
|
+
}
|
|
86
|
+
for (const job of run.jobs ?? []) {
|
|
87
|
+
const index = archive.jobs.findIndex((entry) => entry.id === job.id)
|
|
88
|
+
if (index >= 0) archive.jobs[index] = job
|
|
89
|
+
else archive.jobs.push(job)
|
|
90
|
+
}
|
|
91
|
+
for (const task of run.curriculumTasks ?? []) {
|
|
92
|
+
if (!archive.curriculumTasks.some((entry) => entry.id === task.id)) archive.curriculumTasks.push(task)
|
|
93
|
+
}
|
|
94
|
+
archive.trajectoryRefs = [...new Set([...archive.trajectoryRefs, ...(run.trajectoryRefs ?? [])])]
|
|
95
|
+
archive.updatedAt = new Date().toISOString()
|
|
96
|
+
await writeArchive(archiveDir, archive)
|
|
97
|
+
return archive
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function checkpointRun(archiveDir: string, run: RsiRunRecord): Promise<RsiArchive> {
|
|
101
|
+
const archive = await readArchive(archiveDir)
|
|
102
|
+
const normalized = normalizeRun(run)
|
|
103
|
+
archive.activeRuns = [...archive.activeRuns.filter((entry) => entry.runId !== run.runId), normalized]
|
|
104
|
+
for (const candidate of run.candidates) {
|
|
105
|
+
const index = archive.candidates.findIndex((entry) => entry.id === candidate.id)
|
|
106
|
+
if (index >= 0) archive.candidates[index] = candidate
|
|
107
|
+
else archive.candidates.push(candidate)
|
|
108
|
+
}
|
|
109
|
+
for (const job of run.jobs ?? []) {
|
|
110
|
+
const index = archive.jobs.findIndex((entry) => entry.id === job.id)
|
|
111
|
+
if (index >= 0) archive.jobs[index] = job
|
|
112
|
+
else archive.jobs.push(job)
|
|
113
|
+
}
|
|
114
|
+
archive.updatedAt = new Date().toISOString()
|
|
115
|
+
await writeArchive(archiveDir, archive)
|
|
116
|
+
return archive
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function findActiveRun(archiveDir: string, runId: string): Promise<RsiRunRecord | undefined> {
|
|
120
|
+
return (await readArchive(archiveDir)).activeRuns.find((run) => run.runId === runId)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function archiveCandidate(archive: RsiArchive, candidate: CandidateRecord): RsiArchive {
|
|
124
|
+
const index = archive.candidates.findIndex((entry) => entry.id === candidate.id)
|
|
125
|
+
if (index >= 0) archive.candidates[index] = candidate
|
|
126
|
+
else archive.candidates.push(candidate)
|
|
127
|
+
archive.updatedAt = new Date().toISOString()
|
|
128
|
+
return archive
|
|
129
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import * as path from "node:path"
|
|
2
|
+
import { DEFAULT_PROTECTED_FILES } from "../permissions/protected-files.js"
|
|
3
|
+
import type { ComputePolicy, MutationHypothesis, MutationKind, ParentSelectionPolicy, RsiConfig } from "./types.js"
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_RSI_MODEL = "wxrq-qwen3.5-9b:latest"
|
|
6
|
+
export const DEFAULT_VISIBLE_EVALS = ["npm test -- --filter rsi"]
|
|
7
|
+
export const DEFAULT_PROTECTED_PATHS = [
|
|
8
|
+
...DEFAULT_PROTECTED_FILES,
|
|
9
|
+
"src/rsi/",
|
|
10
|
+
"scripts/",
|
|
11
|
+
"**/*.test.ts",
|
|
12
|
+
"package.json",
|
|
13
|
+
"package-lock.json",
|
|
14
|
+
".gitignore",
|
|
15
|
+
"scripts/eval-suite/",
|
|
16
|
+
".headlesscode/",
|
|
17
|
+
".worktrees/",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export interface ParsedRsiArgs {
|
|
21
|
+
config?: RsiConfig
|
|
22
|
+
help?: boolean
|
|
23
|
+
error?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function positiveInteger(value: string, name: string): number {
|
|
27
|
+
const parsed = Number(value)
|
|
28
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
29
|
+
throw new Error(`${name} must be a positive integer`)
|
|
30
|
+
}
|
|
31
|
+
return parsed
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function splitCommands(value: string): string[] {
|
|
35
|
+
return value
|
|
36
|
+
.split(";;")
|
|
37
|
+
.map((command) => command.trim())
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function rsiHelp(): string {
|
|
42
|
+
return `headlesscode improve — bounded recursive self-improvement
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
headlesscode improve --repo <path> [options]
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--model <id> worker model (default: ${DEFAULT_RSI_MODEL})
|
|
49
|
+
--population <n> candidates per generation (default: 2)
|
|
50
|
+
--generations <n> bounded generations (default: 1)
|
|
51
|
+
--parent-policy <name> champion-specialist-novelty | pareto-front | all-eligible
|
|
52
|
+
--mutation-kind <name> corrective | architectural | search-policy | curriculum | model-adaptation
|
|
53
|
+
--hypothesis <text> recorded reason for the mutation
|
|
54
|
+
--expected-effect <text> measurable benefit to test
|
|
55
|
+
--potential-downside <text> recorded cost or regression risk
|
|
56
|
+
--compute-policy <name> single | independent | planner-executors | critic-retry
|
|
57
|
+
--eval <command> visible command; repeat or separate with ;;
|
|
58
|
+
--hidden-eval <command> supervisor-only hidden command
|
|
59
|
+
--mutation-task <text> improvement objective
|
|
60
|
+
--archive-dir <path> durable archive directory
|
|
61
|
+
--worktree-dir <path> candidate worktree directory
|
|
62
|
+
--trajectory-dir <path> structured trajectory output directory
|
|
63
|
+
--curriculum-dir <path> generated curriculum proposal directory
|
|
64
|
+
--resume <run-id> resume a checkpointed active run
|
|
65
|
+
--model-candidate-id <id> model identity used for combination tracking
|
|
66
|
+
--base-ref <ref> git ref to branch from (default: HEAD)
|
|
67
|
+
--dry-run print the planned loop without changing files
|
|
68
|
+
--keep-worktrees retain candidate worktrees after evaluation
|
|
69
|
+
--help show this help
|
|
70
|
+
|
|
71
|
+
The evaluator and scoring code are protected from candidates. The first run
|
|
72
|
+
uses the local Qwen worker unless --model overrides it.
|
|
73
|
+
`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseRsiArgs(argv: string[], cwd = process.cwd()): ParsedRsiArgs {
|
|
77
|
+
let repoRoot = cwd
|
|
78
|
+
let model = DEFAULT_RSI_MODEL
|
|
79
|
+
let population = 2
|
|
80
|
+
let generations = 1
|
|
81
|
+
let maxConcurrent = 1
|
|
82
|
+
let mutationTask = "Improve the headlesscode agent loop while preserving all existing behavior and tests."
|
|
83
|
+
let parentSelectionPolicy: ParentSelectionPolicy = "champion-specialist-novelty"
|
|
84
|
+
let mutationKind: MutationKind = "corrective"
|
|
85
|
+
let hypothesis: MutationHypothesis = {
|
|
86
|
+
statement: mutationTask,
|
|
87
|
+
expectedEffect: "higher verified task success without regressions",
|
|
88
|
+
potentialDownside: "additional complexity or inference cost",
|
|
89
|
+
}
|
|
90
|
+
let computePolicy: ComputePolicy = "single"
|
|
91
|
+
let evalCommands = [...DEFAULT_VISIBLE_EVALS]
|
|
92
|
+
let hiddenEvalCommands: string[] = []
|
|
93
|
+
let archiveDir: string | undefined
|
|
94
|
+
let worktreeDir: string | undefined
|
|
95
|
+
let baseRef = "HEAD"
|
|
96
|
+
let seed = "headlesscode-rsi"
|
|
97
|
+
let dryRun = false
|
|
98
|
+
let keepWorktrees = false
|
|
99
|
+
let maxIterations = 40
|
|
100
|
+
let commandTimeoutMs = 15 * 60_000
|
|
101
|
+
let trajectoryDir: string | undefined
|
|
102
|
+
let curriculumDir: string | undefined
|
|
103
|
+
let resumeRunId: string | undefined
|
|
104
|
+
let modelCandidateId: string | undefined
|
|
105
|
+
|
|
106
|
+
const take = (index: number, name: string): [string, number] => {
|
|
107
|
+
const value = argv[index + 1]
|
|
108
|
+
if (!value || value.startsWith("--")) {
|
|
109
|
+
throw new Error(`${name} requires a value`)
|
|
110
|
+
}
|
|
111
|
+
return [value, index + 1]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
for (let index = 0; index < argv.length; index++) {
|
|
116
|
+
const arg = argv[index]
|
|
117
|
+
switch (arg) {
|
|
118
|
+
case "--help":
|
|
119
|
+
case "-h":
|
|
120
|
+
return { help: true }
|
|
121
|
+
case "--repo": {
|
|
122
|
+
const [value, next] = take(index, arg)
|
|
123
|
+
repoRoot = path.resolve(value)
|
|
124
|
+
index = next
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
case "--model": {
|
|
128
|
+
const [value, next] = take(index, arg)
|
|
129
|
+
model = value
|
|
130
|
+
index = next
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
case "--population": {
|
|
134
|
+
const [value, next] = take(index, arg)
|
|
135
|
+
population = positiveInteger(value, arg)
|
|
136
|
+
index = next
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
case "--generations": {
|
|
140
|
+
const [value, next] = take(index, arg)
|
|
141
|
+
generations = positiveInteger(value, arg)
|
|
142
|
+
index = next
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
case "--parent-policy": {
|
|
146
|
+
const [value, next] = take(index, arg)
|
|
147
|
+
if (!["champion-specialist-novelty", "pareto-front", "all-eligible"].includes(value)) throw new Error(`${arg} has an invalid policy`)
|
|
148
|
+
parentSelectionPolicy = value as ParentSelectionPolicy
|
|
149
|
+
index = next
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
case "--mutation-kind": {
|
|
153
|
+
const [value, next] = take(index, arg)
|
|
154
|
+
if (!["corrective", "architectural", "search-policy", "curriculum", "model-adaptation"].includes(value)) throw new Error(`${arg} has an invalid mutation kind`)
|
|
155
|
+
mutationKind = value as MutationKind
|
|
156
|
+
hypothesis.statement = mutationTask
|
|
157
|
+
index = next
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
case "--hypothesis": {
|
|
161
|
+
const [value, next] = take(index, arg)
|
|
162
|
+
hypothesis.statement = value
|
|
163
|
+
index = next
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
case "--expected-effect": {
|
|
167
|
+
const [value, next] = take(index, arg)
|
|
168
|
+
hypothesis.expectedEffect = value
|
|
169
|
+
index = next
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
case "--potential-downside": {
|
|
173
|
+
const [value, next] = take(index, arg)
|
|
174
|
+
hypothesis.potentialDownside = value
|
|
175
|
+
index = next
|
|
176
|
+
break
|
|
177
|
+
}
|
|
178
|
+
case "--compute-policy": {
|
|
179
|
+
const [value, next] = take(index, arg)
|
|
180
|
+
if (!["single", "independent", "planner-executors", "critic-retry"].includes(value)) throw new Error(`${arg} has an invalid policy`)
|
|
181
|
+
computePolicy = value as ComputePolicy
|
|
182
|
+
index = next
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
case "--max-concurrent": {
|
|
186
|
+
const [value, next] = take(index, arg)
|
|
187
|
+
maxConcurrent = positiveInteger(value, arg)
|
|
188
|
+
index = next
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
case "--eval": {
|
|
192
|
+
const [value, next] = take(index, arg)
|
|
193
|
+
evalCommands = splitCommands(value)
|
|
194
|
+
index = next
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
case "--hidden-eval": {
|
|
198
|
+
const [value, next] = take(index, arg)
|
|
199
|
+
hiddenEvalCommands.push(...splitCommands(value))
|
|
200
|
+
index = next
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
case "--mutation-task": {
|
|
204
|
+
const [value, next] = take(index, arg)
|
|
205
|
+
mutationTask = value
|
|
206
|
+
index = next
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
case "--archive-dir": {
|
|
210
|
+
const [value, next] = take(index, arg)
|
|
211
|
+
archiveDir = path.resolve(repoRoot, value)
|
|
212
|
+
index = next
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
case "--worktree-dir": {
|
|
216
|
+
const [value, next] = take(index, arg)
|
|
217
|
+
worktreeDir = path.resolve(repoRoot, value)
|
|
218
|
+
index = next
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
case "--trajectory-dir": {
|
|
222
|
+
const [value, next] = take(index, arg)
|
|
223
|
+
trajectoryDir = path.resolve(repoRoot, value)
|
|
224
|
+
index = next
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
case "--curriculum-dir": {
|
|
228
|
+
const [value, next] = take(index, arg)
|
|
229
|
+
curriculumDir = path.resolve(repoRoot, value)
|
|
230
|
+
index = next
|
|
231
|
+
break
|
|
232
|
+
}
|
|
233
|
+
case "--resume": {
|
|
234
|
+
const [value, next] = take(index, arg)
|
|
235
|
+
resumeRunId = value
|
|
236
|
+
index = next
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
case "--model-candidate-id": {
|
|
240
|
+
const [value, next] = take(index, arg)
|
|
241
|
+
modelCandidateId = value
|
|
242
|
+
index = next
|
|
243
|
+
break
|
|
244
|
+
}
|
|
245
|
+
case "--base-ref": {
|
|
246
|
+
const [value, next] = take(index, arg)
|
|
247
|
+
baseRef = value
|
|
248
|
+
index = next
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
case "--seed": {
|
|
252
|
+
const [value, next] = take(index, arg)
|
|
253
|
+
seed = value
|
|
254
|
+
index = next
|
|
255
|
+
break
|
|
256
|
+
}
|
|
257
|
+
case "--max-iterations": {
|
|
258
|
+
const [value, next] = take(index, arg)
|
|
259
|
+
maxIterations = positiveInteger(value, arg)
|
|
260
|
+
index = next
|
|
261
|
+
break
|
|
262
|
+
}
|
|
263
|
+
case "--timeout-ms": {
|
|
264
|
+
const [value, next] = take(index, arg)
|
|
265
|
+
commandTimeoutMs = positiveInteger(value, arg)
|
|
266
|
+
index = next
|
|
267
|
+
break
|
|
268
|
+
}
|
|
269
|
+
case "--dry-run":
|
|
270
|
+
dryRun = true
|
|
271
|
+
break
|
|
272
|
+
case "--keep-worktrees":
|
|
273
|
+
keepWorktrees = true
|
|
274
|
+
break
|
|
275
|
+
default:
|
|
276
|
+
throw new Error(`unknown improve argument: ${arg}`)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const resolvedRepo = path.resolve(repoRoot)
|
|
280
|
+
return {
|
|
281
|
+
config: {
|
|
282
|
+
repoRoot: resolvedRepo,
|
|
283
|
+
model,
|
|
284
|
+
population,
|
|
285
|
+
generations,
|
|
286
|
+
maxConcurrent,
|
|
287
|
+
mutationTask,
|
|
288
|
+
parentSelectionPolicy,
|
|
289
|
+
mutationKind,
|
|
290
|
+
hypothesis: { ...hypothesis },
|
|
291
|
+
computePolicy,
|
|
292
|
+
evalCommands,
|
|
293
|
+
hiddenEvalCommands,
|
|
294
|
+
archiveDir: archiveDir ?? path.join(resolvedRepo, ".headlesscode", "rsi"),
|
|
295
|
+
worktreeDir: worktreeDir ?? path.join(resolvedRepo, ".worktrees", "rsi"),
|
|
296
|
+
trajectoryDir: trajectoryDir ?? path.join(archiveDir ?? path.join(resolvedRepo, ".headlesscode", "rsi"), "trajectories"),
|
|
297
|
+
curriculumDir: curriculumDir ?? path.join(archiveDir ?? path.join(resolvedRepo, ".headlesscode", "rsi"), "curriculum"),
|
|
298
|
+
baseRef,
|
|
299
|
+
seed,
|
|
300
|
+
dryRun,
|
|
301
|
+
keepWorktrees,
|
|
302
|
+
maxIterations,
|
|
303
|
+
protectedPaths: [...DEFAULT_PROTECTED_PATHS],
|
|
304
|
+
commandTimeoutMs,
|
|
305
|
+
resumeRunId,
|
|
306
|
+
modelCandidateId,
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return { error: error instanceof Error ? error.message : String(error) }
|
|
311
|
+
}
|
|
312
|
+
}
|