headlesscode 1.2.0 → 1.2.2
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 +245 -459
- package/package.json +2 -1
- package/shared/openshell/headlesscode-openrouter.yaml +21 -0
- package/shared/openshell/headlesscode-policy.yaml +21 -0
- package/src/cli.ts +30 -0
- package/src/cloud/openshell-preflight.ts +16 -0
- package/src/cloud/openshell-provider.ts +582 -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 +154 -4
- package/src/engine/types.ts +42 -0
- 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 +11 -0
- package/src/qa/qa.ts +12 -0
- package/src/watcher/cli.ts +18 -0
- package/src/watcher/watch.ts +3 -1
package/src/orchestrator/cli.ts
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
TRIVIAL_DRIFT_AHEAD,
|
|
69
69
|
} from "./git-sync.js"
|
|
70
70
|
import { resolveModelForMode } from "../config/mode-models.js"
|
|
71
|
+
import { openshellPreflight } from "../cloud/openshell-preflight.js"
|
|
71
72
|
import { runPreflight, runLocalPreflight, type PreflightResult, type LocalPreflightResult } from "../llm/preflight.js"
|
|
72
73
|
import { resolvePerModeEnv } from "../cli.js"
|
|
73
74
|
|
|
@@ -110,6 +111,7 @@ Options:
|
|
|
110
111
|
--poll-interval-ms <n> Watcher poll interval (default: 5000)
|
|
111
112
|
--memory-dir <path> Phase 3 memory dir for workers (passed as HEADLESSCODE_MEMORY_DIR,
|
|
112
113
|
which run-worker.sh forwards as --memory-dir to each worker CLI)
|
|
114
|
+
--execution-provider <local|openshell> Worker runtime (default: local)
|
|
113
115
|
--max-concurrent-sessions <n> Phase 6 GLOBAL cap on concurrent sessions across
|
|
114
116
|
processes (default: $HEADLESSCODE_MAX_CONCURRENT_SESSIONS or 3).
|
|
115
117
|
When the cap is already reached this run ABORTS with a clear
|
|
@@ -195,6 +197,7 @@ Environment:
|
|
|
195
197
|
interface OrchestrateOptions {
|
|
196
198
|
repo: string
|
|
197
199
|
issues: number[]
|
|
200
|
+
executionProvider: "local" | "openshell"
|
|
198
201
|
issuesJson?: string
|
|
199
202
|
/**
|
|
200
203
|
* Fix 3: file a REAL GitHub issue for every synthetic --issues-json entry
|
|
@@ -301,6 +304,7 @@ export function parseOrchestrateArgs(argv: string[]): { options: OrchestrateOpti
|
|
|
301
304
|
const options: OrchestrateOptions = {
|
|
302
305
|
repo: "",
|
|
303
306
|
issues: [],
|
|
307
|
+
executionProvider: process.env.HEADLESSCODE_EXECUTION_PROVIDER === "openshell" ? "openshell" : "local",
|
|
304
308
|
mode: process.env.ORCHESTRATOR_MODE ?? "code",
|
|
305
309
|
reviewMode: "deepseek-reviewer",
|
|
306
310
|
review: true,
|
|
@@ -341,6 +345,12 @@ export function parseOrchestrateArgs(argv: string[]): { options: OrchestrateOpti
|
|
|
341
345
|
}
|
|
342
346
|
|
|
343
347
|
switch (flag) {
|
|
348
|
+
case "--execution-provider": {
|
|
349
|
+
const v = next()
|
|
350
|
+
if (v !== "local" && v !== "openshell") return { options, error: "--execution-provider must be local or openshell" }
|
|
351
|
+
options.executionProvider = v
|
|
352
|
+
break
|
|
353
|
+
}
|
|
344
354
|
case "--repo": {
|
|
345
355
|
const v = next()
|
|
346
356
|
if (v === undefined) {
|
|
@@ -1614,6 +1624,7 @@ export interface BuildSpawnEnvOptions {
|
|
|
1614
1624
|
repo: string
|
|
1615
1625
|
/** Harness mode for the workers (e.g. "code"). */
|
|
1616
1626
|
mode: string
|
|
1627
|
+
executionProvider?: "local" | "openshell"
|
|
1617
1628
|
/** An explicit --model flag value, if the caller passed one — always wins (resolveModelForMode rule 1). */
|
|
1618
1629
|
explicitModel?: string
|
|
1619
1630
|
memoryDir?: string
|
|
@@ -1649,6 +1660,7 @@ export function buildSpawnEnv(options: BuildSpawnEnvOptions): { env: NodeJS.Proc
|
|
|
1649
1660
|
...(options.env ?? process.env),
|
|
1650
1661
|
TARGET_REPO: options.repo,
|
|
1651
1662
|
ORCHESTRATOR_MODE: options.mode,
|
|
1663
|
+
HEADLESSCODE_EXECUTION_PROVIDER: options.executionProvider ?? process.env.HEADLESSCODE_EXECUTION_PROVIDER ?? "local",
|
|
1652
1664
|
// The resolved worker model must reach the spawner (and the workers it
|
|
1653
1665
|
// launches) — same pattern as HEADLESSCODE_PROJECT below.
|
|
1654
1666
|
...(workerModel ? { OPENROUTER_MODEL: workerModel } : {}),
|
|
@@ -2545,6 +2557,14 @@ export async function orchestrateMain(argv: string[]): Promise<number> {
|
|
|
2545
2557
|
process.stderr.write(`headlesscode orchestrate: not a git repo: ${repo}\n`)
|
|
2546
2558
|
return 2
|
|
2547
2559
|
}
|
|
2560
|
+
if (!options.dryRun && options.executionProvider === "openshell") {
|
|
2561
|
+
const issue = openshellPreflight()
|
|
2562
|
+
if (issue) {
|
|
2563
|
+
process.stderr.write(`headlesscode orchestrate: OpenShell preflight failed: ${issue}\n`)
|
|
2564
|
+
return 2
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
process.env.HEADLESSCODE_EXECUTION_PROVIDER = options.executionProvider
|
|
2548
2568
|
|
|
2549
2569
|
let issues: SplitIssue[]
|
|
2550
2570
|
try {
|
|
@@ -2852,6 +2872,7 @@ export async function orchestrateMain(argv: string[]): Promise<number> {
|
|
|
2852
2872
|
const { env: spawnEnv, workerModel } = buildSpawnEnv({
|
|
2853
2873
|
repo,
|
|
2854
2874
|
mode: options.mode,
|
|
2875
|
+
executionProvider: options.executionProvider,
|
|
2855
2876
|
explicitModel: options.model,
|
|
2856
2877
|
memoryDir: options.memoryDir,
|
|
2857
2878
|
maxIterations: options.maxIterations,
|
|
@@ -3032,6 +3053,7 @@ export async function orchestrateMain(argv: string[]): Promise<number> {
|
|
|
3032
3053
|
cwd: repo,
|
|
3033
3054
|
env: {
|
|
3034
3055
|
...process.env,
|
|
3056
|
+
HEADLESSCODE_EXECUTION_PROVIDER: options.executionProvider,
|
|
3035
3057
|
HEADLESSCODE_ROOT: HARNESS_ROOT_TS,
|
|
3036
3058
|
...(options.memoryDir ? { HEADLESSCODE_MEMORY_DIR: path.resolve(options.memoryDir) } : {}),
|
|
3037
3059
|
},
|
|
@@ -3221,6 +3243,7 @@ export async function orchestrateMain(argv: string[]): Promise<number> {
|
|
|
3221
3243
|
cwd: repo,
|
|
3222
3244
|
env: {
|
|
3223
3245
|
...process.env,
|
|
3246
|
+
HEADLESSCODE_EXECUTION_PROVIDER: options.executionProvider,
|
|
3224
3247
|
HEADLESSCODE_ROOT: HARNESS_ROOT_TS,
|
|
3225
3248
|
...(options.memoryDir ? { HEADLESSCODE_MEMORY_DIR: path.resolve(options.memoryDir) } : {}),
|
|
3226
3249
|
},
|
|
@@ -3376,6 +3399,7 @@ export async function orchestrateMain(argv: string[]): Promise<number> {
|
|
|
3376
3399
|
cwd: repo,
|
|
3377
3400
|
env: {
|
|
3378
3401
|
...process.env,
|
|
3402
|
+
HEADLESSCODE_EXECUTION_PROVIDER: options.executionProvider,
|
|
3379
3403
|
HEADLESSCODE_ROOT: HARNESS_ROOT_TS,
|
|
3380
3404
|
...(options.memoryDir ? { HEADLESSCODE_MEMORY_DIR: path.resolve(options.memoryDir) } : {}),
|
|
3381
3405
|
},
|
|
@@ -36,6 +36,7 @@ import { getNativeTools } from "../vendor/zoo-code/src/core/prompts/tools/native
|
|
|
36
36
|
import { addCustomInstructions } from "../vendor/zoo-code/src/core/prompts/sections/custom-instructions.js"
|
|
37
37
|
import type { ChatTool, LlmClient, SessionResult } from "../engine/types.js"
|
|
38
38
|
import type { SessionBudget } from "../budget/budget.js"
|
|
39
|
+
import { runOpenShellSubsession } from "../cloud/openshell-subsession.js"
|
|
39
40
|
|
|
40
41
|
/** Default location of the reviewer checklist, relative to the harness repo. */
|
|
41
42
|
export const DEFAULT_REVIEW_PROMPT_PATH = "shared/prompts/review-mode-prompt.md"
|
|
@@ -210,6 +211,16 @@ export async function runReview(options: ReviewOptions): Promise<ReviewResult> {
|
|
|
210
211
|
maxIterations = 200,
|
|
211
212
|
budget,
|
|
212
213
|
} = options
|
|
214
|
+
if (process.env.HEADLESSCODE_EXECUTION_PROVIDER === "openshell" && !llmClient) {
|
|
215
|
+
try {
|
|
216
|
+
return await runOpenShellSubsession<ReviewResult>("review", workspaceRoot, {
|
|
217
|
+
workspaceRoot, mode, model, reviewPromptPath, taskText, issues, maxIterations, budget,
|
|
218
|
+
})
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
221
|
+
return { findings: [`[review session error] ${message}`], verdict: "error", summary: `Review session failed: ${message}` }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
213
224
|
|
|
214
225
|
// 2026-08-27: verified live — a review session couldn't find the `curlee`
|
|
215
226
|
// compiler at all ("curlee runtime is not available in this environment"),
|
package/src/project-store.ts
CHANGED
|
@@ -95,11 +95,22 @@ export interface ProjectMetadata {
|
|
|
95
95
|
*/
|
|
96
96
|
export function resolveProjectIdentity(workspaceRoot: string): ProjectIdentity {
|
|
97
97
|
const root = path.resolve(workspaceRoot)
|
|
98
|
+
// OpenShell runs an independent Git clone so its writable workspace does not
|
|
99
|
+
// expose host repository metadata. Preserve the original project's store key
|
|
100
|
+
// without mounting or reading the original repository path in the sandbox.
|
|
101
|
+
const identityRoot = process.env.HEADLESSCODE_PROJECT_IDENTITY_ROOT?.trim()
|
|
102
|
+
if (identityRoot && path.isAbsolute(identityRoot)) return { keySource: path.resolve(identityRoot), kind: "git" }
|
|
103
|
+
// Sandboxed workers may set these to a private Git directory so ordinary
|
|
104
|
+
// commits cannot touch shared host metadata. Project identity must still
|
|
105
|
+
// resolve through the workspace's real .git pointer and shared common dir.
|
|
106
|
+
const gitEnv = { ...process.env }
|
|
107
|
+
for (const key of ["GIT_DIR", "GIT_COMMON_DIR", "GIT_WORK_TREE", "GIT_NAMESPACE", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_INDEX_FILE"]) delete gitEnv[key]
|
|
98
108
|
// 1. Git repo → `--git-common-dir` (worktree-aware: resolves to the main
|
|
99
109
|
// repo's .git from inside a worktree), parent = identity source.
|
|
100
110
|
try {
|
|
101
111
|
const out = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
102
112
|
cwd: root,
|
|
113
|
+
env: gitEnv,
|
|
103
114
|
encoding: "utf-8",
|
|
104
115
|
timeout: 10_000,
|
|
105
116
|
stdio: ["ignore", "pipe", "ignore"],
|
package/src/qa/qa.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { addCustomInstructions } from "../vendor/zoo-code/src/core/prompts/secti
|
|
|
49
49
|
import type { ChatTool, LlmClient, SessionResult } from "../engine/types.js"
|
|
50
50
|
import type { MemoryStore } from "../memory/types.js"
|
|
51
51
|
import type { SessionBudget } from "../budget/budget.js"
|
|
52
|
+
import { runOpenShellSubsession } from "../cloud/openshell-subsession.js"
|
|
52
53
|
|
|
53
54
|
/** Default mode slug used for QA sessions (the qa-agent mode). */
|
|
54
55
|
export const DEFAULT_QA_MODE = "qa-agent"
|
|
@@ -246,6 +247,17 @@ export async function runQa(options: RunQaOptions): Promise<QaResult> {
|
|
|
246
247
|
memory = null,
|
|
247
248
|
project,
|
|
248
249
|
} = options
|
|
250
|
+
if (process.env.HEADLESSCODE_EXECUTION_PROVIDER === "openshell" && !llmClient) {
|
|
251
|
+
try {
|
|
252
|
+
return await runOpenShellSubsession<QaResult>("qa", workspaceRoot, {
|
|
253
|
+
workspaceRoot, mode, model, baseUrl, maxIterations, budget, project,
|
|
254
|
+
...(options.taskText ? { taskText: options.taskText } : {}),
|
|
255
|
+
})
|
|
256
|
+
} catch (error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
258
|
+
return { verdict: "error", evidence: "", summary: `QA session failed: ${message}` }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
249
261
|
|
|
250
262
|
// Detect whether the target repo actually defines the requested mode. If
|
|
251
263
|
// not, fall back to the generic checklist as a system prompt override.
|
package/src/watcher/cli.ts
CHANGED
|
@@ -32,6 +32,7 @@ import * as path from "node:path"
|
|
|
32
32
|
|
|
33
33
|
import { Logger } from "../engine/logger.js"
|
|
34
34
|
import { watchIssues, type SweepResult } from "./watch.js"
|
|
35
|
+
import { openshellPreflight } from "../cloud/openshell-preflight.js"
|
|
35
36
|
|
|
36
37
|
const WATCH_USAGE = `headlesscode watch — Phase 5 GitHub issue watcher
|
|
37
38
|
|
|
@@ -61,6 +62,7 @@ Options:
|
|
|
61
62
|
<repo>/.worktrees/.watcher-state.json)
|
|
62
63
|
--mode <slug> Harness mode for workers (default: code; forwarded
|
|
63
64
|
to the spawner as ORCHESTRATOR_MODE)
|
|
65
|
+
--execution-provider <local|openshell> Worker runtime (default: local)
|
|
64
66
|
--qa Pass-through: record that a QA round should follow
|
|
65
67
|
each batch (see docs/phase5-issue-watcher.md)
|
|
66
68
|
--deploy Pass-through: record that a deploy gate should follow
|
|
@@ -92,6 +94,7 @@ export interface WatchCliOptions {
|
|
|
92
94
|
maxConcurrentSessions: number
|
|
93
95
|
stateFile?: string
|
|
94
96
|
mode: string
|
|
97
|
+
executionProvider: "local" | "openshell"
|
|
95
98
|
qa: boolean
|
|
96
99
|
deploy: boolean
|
|
97
100
|
memoryDir?: string
|
|
@@ -110,6 +113,7 @@ export function parseWatchArgs(argv: string[]): { options: WatchCliOptions; erro
|
|
|
110
113
|
maxPerSweep: 5,
|
|
111
114
|
maxConcurrentSessions: 3,
|
|
112
115
|
mode: "code",
|
|
116
|
+
executionProvider: process.env.HEADLESSCODE_EXECUTION_PROVIDER === "openshell" ? "openshell" : "local",
|
|
113
117
|
qa: false,
|
|
114
118
|
deploy: false,
|
|
115
119
|
dryRun: false,
|
|
@@ -135,6 +139,12 @@ export function parseWatchArgs(argv: string[]): { options: WatchCliOptions; erro
|
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
switch (flag) {
|
|
142
|
+
case "--execution-provider": {
|
|
143
|
+
const v = next()
|
|
144
|
+
if (v !== "local" && v !== "openshell") return { options, error: "--execution-provider must be local or openshell" }
|
|
145
|
+
options.executionProvider = v
|
|
146
|
+
break
|
|
147
|
+
}
|
|
138
148
|
case "--owner": {
|
|
139
149
|
const v = next()
|
|
140
150
|
if (v === undefined) {
|
|
@@ -300,6 +310,13 @@ export async function watchMain(argv: string[]): Promise<number> {
|
|
|
300
310
|
return 2
|
|
301
311
|
}
|
|
302
312
|
}
|
|
313
|
+
if (!options.dryRun && options.executionProvider === "openshell") {
|
|
314
|
+
const issue = openshellPreflight()
|
|
315
|
+
if (issue) {
|
|
316
|
+
process.stderr.write(`headlesscode watch: OpenShell preflight failed: ${issue}\n`)
|
|
317
|
+
return 2
|
|
318
|
+
}
|
|
319
|
+
}
|
|
303
320
|
|
|
304
321
|
const ghRepo = options.ghRepo ?? path.basename(repoRoot)
|
|
305
322
|
const logger = new Logger({ level: "info" })
|
|
@@ -328,6 +345,7 @@ export async function watchMain(argv: string[]): Promise<number> {
|
|
|
328
345
|
retryFailed: options.retryFailed,
|
|
329
346
|
dryRun: options.dryRun,
|
|
330
347
|
mode: options.mode,
|
|
348
|
+
executionProvider: options.executionProvider,
|
|
331
349
|
memoryDir: options.memoryDir,
|
|
332
350
|
qa: options.qa,
|
|
333
351
|
deploy: options.deploy,
|
package/src/watcher/watch.ts
CHANGED
|
@@ -129,6 +129,7 @@ export interface WatcherConfig {
|
|
|
129
129
|
spawn?: (batch: SpawnBatch) => SpawnResult | Promise<SpawnResult>
|
|
130
130
|
/** Harness mode for workers (forwarded to the spawner as ORCHESTRATOR_MODE). */
|
|
131
131
|
mode?: string
|
|
132
|
+
executionProvider?: "local" | "openshell"
|
|
132
133
|
/** Memory dir for workers (forwarded to the spawner as HEADLESSCODE_MEMORY_DIR). */
|
|
133
134
|
memoryDir?: string
|
|
134
135
|
/** Phase 4 pass-through: recorded on the batch entry (see docs). */
|
|
@@ -480,7 +481,7 @@ function batchId(): string {
|
|
|
480
481
|
* with cwd = the target repo so `git rev-parse --show-toplevel` resolves).
|
|
481
482
|
* No worktree creation happens in TS; the bash script remains the spawner.
|
|
482
483
|
*/
|
|
483
|
-
export function defaultSpawn(batch: SpawnBatch, config: Pick<WatcherConfig, "mode" | "memoryDir">): SpawnResult {
|
|
484
|
+
export function defaultSpawn(batch: SpawnBatch, config: Pick<WatcherConfig, "mode" | "memoryDir" | "executionProvider">): SpawnResult {
|
|
484
485
|
const issues: SplitIssue[] = [
|
|
485
486
|
{ number: batch.issue.number, title: batch.issue.title, body: batch.issue.body },
|
|
486
487
|
]
|
|
@@ -511,6 +512,7 @@ export function defaultSpawn(batch: SpawnBatch, config: Pick<WatcherConfig, "mod
|
|
|
511
512
|
...process.env,
|
|
512
513
|
TARGET_REPO: batch.repoRoot,
|
|
513
514
|
ORCHESTRATOR_MODE: config.mode ?? "code",
|
|
515
|
+
HEADLESSCODE_EXECUTION_PROVIDER: config.executionProvider ?? process.env.HEADLESSCODE_EXECUTION_PROVIDER ?? "local",
|
|
514
516
|
HEADLESSCODE_PROJECT: path.basename(batch.repoRoot),
|
|
515
517
|
...(config.memoryDir ? { HEADLESSCODE_MEMORY_DIR: path.resolve(config.memoryDir) } : {}),
|
|
516
518
|
},
|