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.
Files changed (51) hide show
  1. package/README.md +272 -456
  2. package/package.json +8 -3
  3. package/shared/openshell/headlesscode-openrouter.yaml +21 -0
  4. package/shared/openshell/headlesscode-policy.yaml +21 -0
  5. package/src/cli.ts +77 -18
  6. package/src/cloud/openshell-preflight.ts +16 -0
  7. package/src/cloud/openshell-provider.ts +642 -0
  8. package/src/cloud/openshell-session.ts +119 -0
  9. package/src/cloud/openshell-subsession.ts +63 -0
  10. package/src/cloud/openshell-worker.ts +124 -0
  11. package/src/engine/events.ts +3 -0
  12. package/src/engine/loop.ts +143 -0
  13. package/src/engine/types.ts +42 -0
  14. package/src/llm/ollama.ts +25 -19
  15. package/src/monitoring/controller.ts +73 -0
  16. package/src/monitoring/features.ts +103 -0
  17. package/src/monitoring/index.ts +4 -0
  18. package/src/monitoring/predictor.ts +246 -0
  19. package/src/monitoring/types.ts +192 -0
  20. package/src/orchestrator/cli.ts +24 -0
  21. package/src/orchestrator/reviewer.ts +11 -0
  22. package/src/project-store.ts +14 -0
  23. package/src/qa/qa.ts +12 -0
  24. package/src/rsi/adaptive.ts +49 -0
  25. package/src/rsi/adversarial.ts +106 -0
  26. package/src/rsi/archive.ts +6 -5
  27. package/src/rsi/artifact-store.ts +158 -0
  28. package/src/rsi/config.ts +50 -2
  29. package/src/rsi/controller.ts +561 -42
  30. package/src/rsi/curriculum.ts +135 -16
  31. package/src/rsi/evaluator.ts +6 -37
  32. package/src/rsi/fitness.ts +39 -4
  33. package/src/rsi/index.ts +1 -0
  34. package/src/rsi/migrations/001_postgres_fleet_queue.sql +65 -0
  35. package/src/rsi/migrations/002_external_artifacts_and_job_leases.sql +39 -0
  36. package/src/rsi/migrations/003_model_training_jobs.sql +6 -0
  37. package/src/rsi/model-training.ts +256 -0
  38. package/src/rsi/mutation.ts +1 -77
  39. package/src/rsi/openshell.ts +639 -0
  40. package/src/rsi/postgres-queue.ts +424 -0
  41. package/src/rsi/promote-curriculum.ts +21 -0
  42. package/src/rsi/reports.ts +29 -2
  43. package/src/rsi/roles.ts +13 -3
  44. package/src/rsi/selection.ts +7 -1
  45. package/src/rsi/training-data.ts +103 -0
  46. package/src/rsi/trajectory.ts +1 -1
  47. package/src/rsi/types.ts +115 -2
  48. package/src/rsi/worker.ts +264 -0
  49. package/src/rsi/workspace.ts +14 -3
  50. package/src/watcher/cli.ts +18 -0
  51. package/src/watcher/watch.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "headlesscode",
3
- "version": "1.2.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": ">=18"
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 { improveMain } from "./rsi/controller.js"
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: API key required ────────────────────────────────────────────
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
+ }