headlesscode 1.2.1 → 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/README.md +272 -456
- package/package.json +8 -3
- package/shared/openshell/headlesscode-openrouter.yaml +21 -0
- package/shared/openshell/headlesscode-policy.yaml +21 -0
- package/src/cli.ts +77 -18
- package/src/cloud/openshell-preflight.ts +16 -0
- package/src/cloud/openshell-provider.ts +642 -0
- package/src/cloud/openshell-session.ts +119 -0
- package/src/cloud/openshell-subsession.ts +63 -0
- package/src/cloud/openshell-worker.ts +124 -0
- package/src/engine/events.ts +3 -0
- package/src/engine/loop.ts +143 -0
- package/src/engine/types.ts +42 -0
- package/src/llm/ollama.ts +25 -19
- package/src/monitoring/controller.ts +73 -0
- package/src/monitoring/features.ts +103 -0
- package/src/monitoring/index.ts +4 -0
- package/src/monitoring/predictor.ts +246 -0
- package/src/monitoring/types.ts +192 -0
- package/src/orchestrator/cli.ts +24 -0
- package/src/orchestrator/reviewer.ts +11 -0
- package/src/project-store.ts +14 -0
- package/src/qa/qa.ts +12 -0
- package/src/rsi/adaptive.ts +49 -0
- package/src/rsi/adversarial.ts +106 -0
- package/src/rsi/archive.ts +6 -5
- package/src/rsi/artifact-store.ts +158 -0
- package/src/rsi/config.ts +50 -2
- package/src/rsi/controller.ts +561 -42
- package/src/rsi/curriculum.ts +135 -16
- package/src/rsi/evaluator.ts +6 -37
- package/src/rsi/fitness.ts +39 -4
- package/src/rsi/index.ts +1 -0
- package/src/rsi/migrations/001_postgres_fleet_queue.sql +65 -0
- package/src/rsi/migrations/002_external_artifacts_and_job_leases.sql +39 -0
- package/src/rsi/migrations/003_model_training_jobs.sql +6 -0
- package/src/rsi/model-training.ts +256 -0
- package/src/rsi/mutation.ts +1 -77
- package/src/rsi/openshell.ts +639 -0
- package/src/rsi/postgres-queue.ts +424 -0
- package/src/rsi/promote-curriculum.ts +21 -0
- package/src/rsi/reports.ts +29 -2
- package/src/rsi/roles.ts +13 -3
- package/src/rsi/selection.ts +7 -1
- package/src/rsi/training-data.ts +103 -0
- package/src/rsi/trajectory.ts +1 -1
- package/src/rsi/types.ts +115 -2
- package/src/rsi/worker.ts +264 -0
- package/src/rsi/workspace.ts +14 -3
- package/src/watcher/cli.ts +18 -0
- package/src/watcher/watch.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "headlesscode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Standalone headless coding-agent harness: runs the Zoo Code agent loop (prompts, tools, modes) without a VS Code UI, driven by CLI, HTTP, and parallel worktree orchestration.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"type": "module",
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
20
|
+
"node": ">=22.19"
|
|
21
21
|
},
|
|
22
22
|
"bin": {
|
|
23
23
|
"headlesscode": "bin/headlesscode.mjs"
|
|
@@ -41,14 +41,18 @@
|
|
|
41
41
|
"dev": "tsx src/index.ts",
|
|
42
42
|
"start": "tsx src/cli.ts",
|
|
43
43
|
"cli": "tsx src/cli.ts",
|
|
44
|
+
"monitor:pilot": "tsx scripts/monitor-pilot/run.ts",
|
|
45
|
+
"rsi:promote-curriculum": "tsx src/rsi/promote-curriculum.ts",
|
|
44
46
|
"test": "node scripts/run-tests.mjs",
|
|
45
47
|
"prepublishOnly": "npm ci && npm test"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
50
|
+
"@aws-sdk/client-s3": "^3.1142.0",
|
|
48
51
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
52
|
"@octokit/auth-app": "^8.2.0",
|
|
50
53
|
"fastest-levenshtein": "^1.0.16",
|
|
51
54
|
"p-wait-for": "^5.0.2",
|
|
55
|
+
"pg": "^8.23.0",
|
|
52
56
|
"playwright": "^1.62.1",
|
|
53
57
|
"simple-git": "^3.36.0",
|
|
54
58
|
"tsx": "^4.19.0",
|
|
@@ -58,6 +62,7 @@
|
|
|
58
62
|
"zod": "^3.25.76"
|
|
59
63
|
},
|
|
60
64
|
"devDependencies": {
|
|
61
|
-
"@types/node": "^22.10.0"
|
|
65
|
+
"@types/node": "^22.10.0",
|
|
66
|
+
"@types/pg": "^8.23.1"
|
|
62
67
|
}
|
|
63
68
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
id: headlesscode-openrouter
|
|
2
|
+
display_name: HeadlessCode OpenRouter
|
|
3
|
+
description: OpenRouter inference access for HeadlessCode workers running in Node.js
|
|
4
|
+
category: inference
|
|
5
|
+
inference_capable: true
|
|
6
|
+
credentials:
|
|
7
|
+
- name: api_key
|
|
8
|
+
description: OpenRouter API key
|
|
9
|
+
env_vars: [HEADLESSCODE_OPENROUTER_API_KEY]
|
|
10
|
+
required: true
|
|
11
|
+
auth_style: bearer
|
|
12
|
+
header_name: authorization
|
|
13
|
+
discovery:
|
|
14
|
+
credentials: [api_key]
|
|
15
|
+
endpoints:
|
|
16
|
+
- host: openrouter.ai
|
|
17
|
+
port: 443
|
|
18
|
+
protocol: rest
|
|
19
|
+
access: read-write
|
|
20
|
+
enforcement: enforce
|
|
21
|
+
binaries: [/usr/local/bin/node]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
filesystem_policy:
|
|
3
|
+
include_workdir: true
|
|
4
|
+
read_only:
|
|
5
|
+
- /usr
|
|
6
|
+
- /lib
|
|
7
|
+
- /lib64
|
|
8
|
+
- /etc
|
|
9
|
+
- /proc
|
|
10
|
+
- /dev/urandom
|
|
11
|
+
- /var/log
|
|
12
|
+
read_write:
|
|
13
|
+
- /sandbox
|
|
14
|
+
- /tmp
|
|
15
|
+
- /dev/null
|
|
16
|
+
landlock:
|
|
17
|
+
compatibility: hard_requirement
|
|
18
|
+
process:
|
|
19
|
+
run_as_user: sandbox
|
|
20
|
+
run_as_group: sandbox
|
|
21
|
+
network_policies: {}
|
package/src/cli.ts
CHANGED
|
@@ -43,7 +43,7 @@ import { analyzeCliMain } from "./orchestrator/analyze-cli.js"
|
|
|
43
43
|
import { costHistoryCliMain } from "./orchestrator/cost-history-cli.js"
|
|
44
44
|
import { resolvePermissions, type PermissionsConfig } from "./permissions/config.js"
|
|
45
45
|
import { resolveModelForMode, resolveReasoningEffortForMode } from "./config/mode-models.js"
|
|
46
|
-
import {
|
|
46
|
+
import { openShellSessionMain } from "./cloud/openshell-session.js"
|
|
47
47
|
|
|
48
48
|
const VERSION = "0.1.0"
|
|
49
49
|
|
|
@@ -187,6 +187,9 @@ Usage:
|
|
|
187
187
|
headlesscode --dry-run [options] # build system prompt + validate config, no LLM call
|
|
188
188
|
|
|
189
189
|
Subcommands:
|
|
190
|
+
headlesscode openshell-session --repo <path> --task <text> --policy <path>
|
|
191
|
+
Run one task in an OpenShell sandbox and keep its Git worktree
|
|
192
|
+
for review. See \`openshell-session --help\` for options.
|
|
190
193
|
headlesscode improve --repo <path> [--model <id>] [--population <n>] [--dry-run]
|
|
191
194
|
Run a bounded recursive self-improvement generation. Candidates
|
|
192
195
|
use isolated git worktrees; evaluator/scoring paths are protected.
|
|
@@ -721,10 +724,73 @@ export function parseArgs(argv: string[]): { options: CliOptions; error?: string
|
|
|
721
724
|
}
|
|
722
725
|
|
|
723
726
|
export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
|
|
727
|
+
if (argv[0] === "openshell-review-session" || argv[0] === "openshell-qa-session") {
|
|
728
|
+
let payload: Record<string, unknown>
|
|
729
|
+
try {
|
|
730
|
+
payload = JSON.parse(Buffer.from(argv[1] ?? "", "base64url").toString("utf8")) as Record<string, unknown>
|
|
731
|
+
} catch {
|
|
732
|
+
process.stderr.write("headlesscode: invalid OpenShell session payload\n")
|
|
733
|
+
return 2
|
|
734
|
+
}
|
|
735
|
+
if (argv[0] === "openshell-review-session") {
|
|
736
|
+
const { runReview } = await import("./orchestrator/reviewer.js")
|
|
737
|
+
const result = await runReview(payload as unknown as Parameters<typeof runReview>[0])
|
|
738
|
+
process.stdout.write(`HEADLESSCODE_RESULT_JSON:${JSON.stringify(result)}\n`)
|
|
739
|
+
return 0
|
|
740
|
+
}
|
|
741
|
+
const { runQa } = await import("./qa/qa.js")
|
|
742
|
+
const result = await runQa(payload as unknown as Parameters<typeof runQa>[0])
|
|
743
|
+
process.stdout.write(`HEADLESSCODE_RESULT_JSON:${JSON.stringify(result)}\n`)
|
|
744
|
+
return 0
|
|
745
|
+
}
|
|
746
|
+
if (argv[0] === "openshell-worker") {
|
|
747
|
+
const { openShellWorkerMain } = await import("./cloud/openshell-worker.js")
|
|
748
|
+
return openShellWorkerMain(argv.slice(1))
|
|
749
|
+
}
|
|
750
|
+
if (argv[0] === "openshell-session") {
|
|
751
|
+
return openShellSessionMain(argv.slice(1))
|
|
752
|
+
}
|
|
753
|
+
if (argv[0] === "rsi-worker") {
|
|
754
|
+
const { rsiWorkerMain } = await import("./rsi/worker.js")
|
|
755
|
+
return rsiWorkerMain()
|
|
756
|
+
}
|
|
757
|
+
if (argv[0] === "rsi-train-model") {
|
|
758
|
+
const { parseRsiArgs } = await import("./rsi/config.js")
|
|
759
|
+
const { readArchive } = await import("./rsi/archive.js")
|
|
760
|
+
const { PostgresRsiJobQueue, fleetQueuePolicy } = await import("./rsi/postgres-queue.js")
|
|
761
|
+
const { runBoundedModelCandidate } = await import("./rsi/model-training.js")
|
|
762
|
+
const parsed = parseRsiArgs(argv.slice(1))
|
|
763
|
+
if (parsed.help) {
|
|
764
|
+
process.stdout.write("Usage: headlesscode rsi-train-model --repo <path> --archive-dir <path> --resume <run-id> --model-candidate-id <id>\nThe registered worker must use an OpenShell training image and a local Hugging Face checkpoint.\n")
|
|
765
|
+
return 0
|
|
766
|
+
}
|
|
767
|
+
if (parsed.error || !parsed.config || !parsed.config.resumeRunId || !parsed.config.modelCandidateId || parsed.config.dryRun) {
|
|
768
|
+
process.stderr.write(`headlesscode rsi-train-model: ${parsed.error ?? "--resume and --model-candidate-id are required; dry-run is unsupported"}\n`)
|
|
769
|
+
return 2
|
|
770
|
+
}
|
|
771
|
+
const config = parsed.config
|
|
772
|
+
const archive = await readArchive(config.archiveDir)
|
|
773
|
+
const run = archive.activeRuns.find((entry) => entry.runId === config.resumeRunId) ?? archive.runs.find((entry) => entry.runId === config.resumeRunId)
|
|
774
|
+
if (!run) {
|
|
775
|
+
process.stderr.write(`headlesscode rsi-train-model: RSI run not found in ${config.archiveDir}\n`)
|
|
776
|
+
return 2
|
|
777
|
+
}
|
|
778
|
+
const policy = fleetQueuePolicy(process.env.HEADLESSCODE_RSI_QUEUE_NAME?.trim() || "rsi-default", config.maxConcurrent, process.env)
|
|
779
|
+
const queue = PostgresRsiJobQueue.fromEnvironment(policy, process.env)
|
|
780
|
+
try {
|
|
781
|
+
await queue.migrate()
|
|
782
|
+
const model = await runBoundedModelCandidate(config, run, queue, config.modelCandidateId!)
|
|
783
|
+
process.stdout.write(`[rsi-model] ${model.id}: ${model.status}; eligible=${model.eligibleForSelection === true}\n`)
|
|
784
|
+
return model.eligibleForSelection ? 0 : 1
|
|
785
|
+
} finally {
|
|
786
|
+
await queue.close()
|
|
787
|
+
}
|
|
788
|
+
}
|
|
724
789
|
// Bounded recursive self-improvement: the supervisor owns the evaluator,
|
|
725
790
|
// archive, and selection logic while each candidate runs in its own
|
|
726
791
|
// worktree. See docs/recursive-self-improvement.md.
|
|
727
792
|
if (argv[0] === "improve") {
|
|
793
|
+
const { improveMain } = await import("./rsi/controller.js")
|
|
728
794
|
return improveMain(argv.slice(1))
|
|
729
795
|
}
|
|
730
796
|
|
|
@@ -1019,9 +1085,17 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
1019
1085
|
}
|
|
1020
1086
|
}
|
|
1021
1087
|
|
|
1022
|
-
// ── real run:
|
|
1088
|
+
// ── real run: OpenRouter key required unless this mode is local ────────────
|
|
1089
|
+
const codeModeBackend = process.env.HEADLESSCODE_CODE_MODE_BACKEND ?? "openrouter"
|
|
1090
|
+
const localBackendModes = new Set(
|
|
1091
|
+
(process.env.HEADLESSCODE_LOCAL_BACKEND_MODES ?? "code")
|
|
1092
|
+
.split(",")
|
|
1093
|
+
.map((s) => s.trim())
|
|
1094
|
+
.filter(Boolean),
|
|
1095
|
+
)
|
|
1096
|
+
const useLocalCodeBackend = localBackendModes.has(options.mode) && codeModeBackend === "ollama"
|
|
1023
1097
|
const apiKey = process.env.HEADLESSCODE_OPENROUTER_API_KEY
|
|
1024
|
-
if (!apiKey) {
|
|
1098
|
+
if (!apiKey && !useLocalCodeBackend) {
|
|
1025
1099
|
process.stderr.write(
|
|
1026
1100
|
"headlesscode: HEADLESSCODE_OPENROUTER_API_KEY is not set.\n" +
|
|
1027
1101
|
" Export it (e.g. export HEADLESSCODE_OPENROUTER_API_KEY=sk-or-...) or use --dry-run to\n" +
|
|
@@ -1068,21 +1142,6 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
1068
1142
|
env: process.env,
|
|
1069
1143
|
})
|
|
1070
1144
|
|
|
1071
|
-
// Generalized 2026-08-21 (issue #142) beyond the original `code`-only
|
|
1072
|
-
// scope (plans/local-dual-model-code-agent.md, D2) — real need: two
|
|
1073
|
-
// separate local daemons on two different GPUs (a coder model and a
|
|
1074
|
-
// review model), each needing its own mode -> URL/model mapping.
|
|
1075
|
-
// HEADLESSCODE_LOCAL_BACKEND_MODES defaults to "code" alone, so
|
|
1076
|
-
// nobody's existing setup changes behavior unless they opt in.
|
|
1077
|
-
const codeModeBackend = process.env.HEADLESSCODE_CODE_MODE_BACKEND ?? "openrouter"
|
|
1078
|
-
const localBackendModes = new Set(
|
|
1079
|
-
(process.env.HEADLESSCODE_LOCAL_BACKEND_MODES ?? "code")
|
|
1080
|
-
.split(",")
|
|
1081
|
-
.map((s) => s.trim())
|
|
1082
|
-
.filter(Boolean),
|
|
1083
|
-
)
|
|
1084
|
-
const useLocalCodeBackend = localBackendModes.has(options.mode) && codeModeBackend === "ollama"
|
|
1085
|
-
|
|
1086
1145
|
// The local daemon's own proxy (ollama_shim.py) deliberately runs a 600s
|
|
1087
1146
|
// upstream request timeout — its own comment documents why: a shorter
|
|
1088
1147
|
// shim timeout was once found to cut off calls before the harness's own
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
import * as fs from "node:fs"
|
|
3
|
+
|
|
4
|
+
export function openshellPreflight(options: { requireOpenRouterCredential?: boolean } = {}): string | undefined {
|
|
5
|
+
const policy = process.env.HEADLESSCODE_OPENSHELL_POLICY
|
|
6
|
+
if (!policy || !fs.existsSync(policy)) return "set HEADLESSCODE_OPENSHELL_POLICY to an existing base policy YAML"
|
|
7
|
+
if (options.requireOpenRouterCredential !== false && !process.env.HEADLESSCODE_OPENROUTER_API_KEY) return "set HEADLESSCODE_OPENROUTER_API_KEY so OpenShell can provide the imported credential profile"
|
|
8
|
+
const version = spawnSync("openshell", ["--version"], { encoding: "utf8", timeout: 5000 })
|
|
9
|
+
if (version.error || version.status !== 0) return "OpenShell CLI is unavailable"
|
|
10
|
+
const gateway = spawnSync("openshell", ["gateway", "info"], { encoding: "utf8", timeout: 10000 })
|
|
11
|
+
const output = `${gateway.stdout ?? ""}${gateway.stderr ?? ""}`
|
|
12
|
+
if (gateway.error || gateway.status !== 0 || !/Status:\s+healthy/i.test(output)) {
|
|
13
|
+
return `OpenShell gateway is not healthy${output.trim() ? ": " + output.trim() : ""}`
|
|
14
|
+
}
|
|
15
|
+
return undefined
|
|
16
|
+
}
|