dev-orchestrator-connector 0.1.0 → 0.1.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 +8 -3
- package/bin/dev-orchestrator-connector.js +2 -2
- package/dist/activity.js +22 -0
- package/dist/api.js +24 -0
- package/dist/cli.js +90 -0
- package/dist/codex.js +202 -0
- package/dist/config.js +69 -0
- package/dist/git-credential.js +28 -0
- package/dist/journal.js +51 -0
- package/dist/process.js +54 -0
- package/dist/publisher.js +182 -0
- package/dist/repository.js +310 -0
- package/dist/types.js +1 -0
- package/dist/worker.js +192 -0
- package/dist/workspace.js +2 -0
- package/package.json +13 -6
- package/src/activity.ts +0 -33
- package/src/api.ts +0 -23
- package/src/cli.ts +0 -55
- package/src/codex.ts +0 -160
- package/src/config.ts +0 -46
- package/src/git-credential.ts +0 -29
- package/src/journal.ts +0 -36
- package/src/process.ts +0 -40
- package/src/publisher.ts +0 -152
- package/src/repository.ts +0 -261
- package/src/types.ts +0 -6
- package/src/worker.ts +0 -164
- package/src/workspace.ts +0 -2
package/src/process.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
export const cleanGitEnv = (): NodeJS.ProcessEnv => ({
|
|
4
|
-
PATH: process.env.PATH, HOME: process.env.HOME,
|
|
5
|
-
LANG: "C", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null",
|
|
6
|
-
GIT_TERMINAL_PROMPT: "0", GIT_NO_REPLACE_OBJECTS: "1"
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
export async function readAuthor(source:string,env:NodeJS.ProcessEnv={PATH:process.env.PATH,HOME:process.env.HOME,XDG_CONFIG_HOME:process.env.XDG_CONFIG_HOME,GIT_CONFIG_NOSYSTEM:"1"}):Promise<{name:string;email:string}> {
|
|
10
|
-
// Reading these two values does not execute hooks or credential helpers.
|
|
11
|
-
const name=(await command("git",["-C",source,"config","--get","user.name"],{env})).output;
|
|
12
|
-
const email=(await command("git",["-C",source,"config","--get","user.email"],{env})).output;
|
|
13
|
-
if(!name || name.length>200 || !email || email.length>254 || /[\r\n]/.test(name+email))throw new Error("Configure a Git author on the customer host before running tasks.");
|
|
14
|
-
return {name,email};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function stopProcess(child: ChildProcess): void {
|
|
18
|
-
if (!child.pid || child.exitCode !== null) return;
|
|
19
|
-
try { process.kill(-child.pid,"SIGKILL"); } catch { child.kill("SIGKILL"); }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function command(binary: string, args: string[], opts: { cwd?: string; env?: NodeJS.ProcessEnv; signal?: AbortSignal; timeout?: number; allowExitOne?: boolean } = {}): Promise<{ output: string; code: number }> {
|
|
23
|
-
return new Promise((resolve,reject)=>{
|
|
24
|
-
const child=spawn(binary,args,{cwd:opts.cwd,env:opts.env ?? cleanGitEnv(),detached:true,stdio:["ignore","pipe","ignore"]});
|
|
25
|
-
let output="",settled=false;
|
|
26
|
-
const fail=()=>{if(!settled){settled=true;stopProcess(child);reject(new Error("Local command failed."));}};
|
|
27
|
-
const timer=setTimeout(fail,opts.timeout ?? 30_000);
|
|
28
|
-
const abort=()=>fail();
|
|
29
|
-
opts.signal?.addEventListener("abort",abort,{once:true});
|
|
30
|
-
if(opts.signal?.aborted) fail();
|
|
31
|
-
child.stdout.on("data",chunk=>{output+=chunk.toString();if(output.length>1_048_576)fail();});
|
|
32
|
-
child.on("error",fail);
|
|
33
|
-
child.on("close",code=>{
|
|
34
|
-
clearTimeout(timer);opts.signal?.removeEventListener("abort",abort);
|
|
35
|
-
if(settled)return;settled=true;
|
|
36
|
-
if(code===0 || (code===1 && opts.allowExitOne))resolve({output:output.trim(),code:code!});
|
|
37
|
-
else reject(new Error("Local command failed."));
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
}
|
package/src/publisher.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, readFile, writeFile, rm, realpath, lstat, access, mkdir } from "node:fs/promises";
|
|
2
|
-
import { join, resolve, sep } from "node:path";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { command, cleanGitEnv } from "./process.ts";
|
|
5
|
-
import { withGitHubCredential, validateGitHubCredential } from "./git-credential.ts";
|
|
6
|
-
import type { GitHubCredential } from "./types.ts";
|
|
7
|
-
import { validBranch, validRepository } from "./workspace.ts";
|
|
8
|
-
|
|
9
|
-
export type PublicationContext={workspace:string;repository:string;baseBranch:string;branch:string;baseline:string};
|
|
10
|
-
type Dependencies={runCommand?:typeof command;apiFetch?:typeof fetch;remoteURL?:(repository:string)=>string};
|
|
11
|
-
const validSHA=(sha:string)=>/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(sha);
|
|
12
|
-
|
|
13
|
-
export class Publisher {
|
|
14
|
-
readonly context:PublicationContext;
|
|
15
|
-
private readonly runCommand:typeof command;
|
|
16
|
-
private readonly apiFetch:typeof fetch;
|
|
17
|
-
private readonly remoteURL:(repository:string)=>string;
|
|
18
|
-
|
|
19
|
-
constructor(context:PublicationContext,dependencies:Dependencies={}) {
|
|
20
|
-
if(!validRepository(context.repository) || !validBranch(context.branch) || !validBranch(context.baseBranch) || context.branch===context.baseBranch || !context.branch.startsWith("task/") || !validSHA(context.baseline))throw new Error("Invalid publication context.");
|
|
21
|
-
this.context=context;
|
|
22
|
-
this.runCommand=dependencies.runCommand??command;
|
|
23
|
-
this.apiFetch=dependencies.apiFetch??fetch;
|
|
24
|
-
this.remoteURL=dependencies.remoteURL??(repository=>`https://github.com/${repository}.git`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async status(signal?:AbortSignal):Promise<{changed:boolean;commit:string}> {
|
|
28
|
-
return this.trusted(async(git,sha)=>{
|
|
29
|
-
const uncommitted=await git(["diff","--no-ext-diff","--no-textconv","--quiet",sha,"--"],true);
|
|
30
|
-
const untracked=await git(["ls-files","--others","--exclude-standard"]);
|
|
31
|
-
if(uncommitted.code!==0 || untracked.output)throw new Error("Uncommitted changes remain. Codex must commit the task before publication.");
|
|
32
|
-
const diff=await git(["diff","--no-ext-diff","--no-textconv","--quiet",this.context.baseline,sha,"--"],true);
|
|
33
|
-
return {changed:diff.code===1,commit:sha};
|
|
34
|
-
},signal);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async publish(title:string,body:string,credential:GitHubCredential,signal?:AbortSignal):Promise<Record<string,string>> {
|
|
38
|
-
if(!title.trim() || title.length>240 || body.length>64_000)throw new Error("Invalid PR metadata.");
|
|
39
|
-
validateGitHubCredential(credential);
|
|
40
|
-
const status=await this.status(signal);
|
|
41
|
-
if(!status.changed)throw new Error("No committed changes to publish.");
|
|
42
|
-
return this.trusted(async(git,sha,temp,objects)=>{
|
|
43
|
-
await withGitHubCredential(temp,credential,env=>git(["push","--no-verify",this.remoteURL(this.context.repository),`${sha}:refs/heads/${this.context.branch}`],false,{...env,GIT_OBJECT_DIRECTORY:objects}));
|
|
44
|
-
let pr=await this.findOpenPullRequest(credential,signal);
|
|
45
|
-
if(!pr) {
|
|
46
|
-
const [owner]=this.context.repository.split("/");
|
|
47
|
-
const created=await this.githubRequest("POST",`/repos/${this.context.repository}/pulls`,credential,{
|
|
48
|
-
title,
|
|
49
|
-
head:`${owner}:${this.context.branch}`,
|
|
50
|
-
base:this.context.baseBranch,
|
|
51
|
-
body,
|
|
52
|
-
draft:false
|
|
53
|
-
},signal);
|
|
54
|
-
if(created.status===422)pr=await this.findOpenPullRequest(credential,signal);
|
|
55
|
-
else if(created.ok)pr=created.body;
|
|
56
|
-
else throw new Error("GitHub could not create a pull request for this branch.");
|
|
57
|
-
}
|
|
58
|
-
if(!this.matchesPullRequest(pr,sha))throw new Error("GitHub pull request does not match the task commit.");
|
|
59
|
-
return {repository:this.context.repository,branch:this.context.branch,base_branch:this.context.baseBranch,commit:sha,pr_url:pr.html_url};
|
|
60
|
-
},signal);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private async findOpenPullRequest(credential:GitHubCredential,signal?:AbortSignal):Promise<any|undefined> {
|
|
64
|
-
const [owner]=this.context.repository.split("/");
|
|
65
|
-
const query=new URLSearchParams({state:"open",head:`${owner}:${this.context.branch}`,per_page:"100"});
|
|
66
|
-
const response=await this.githubRequest("GET",`/repos/${this.context.repository}/pulls?${query}`,credential,undefined,signal);
|
|
67
|
-
if(!response.ok || !Array.isArray(response.body))throw new Error("GitHub pull request lookup failed.");
|
|
68
|
-
const sameBranch=response.body.filter((pr:any)=>pr?.head?.ref===this.context.branch && pr?.head?.repo?.full_name===this.context.repository);
|
|
69
|
-
const exact=sameBranch.find((pr:any)=>pr?.base?.ref===this.context.baseBranch && pr?.base?.repo?.full_name===this.context.repository);
|
|
70
|
-
if(sameBranch.length && !exact)throw new Error("An open pull request exists for this task branch with a different base repository or branch.");
|
|
71
|
-
return exact;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
private matchesPullRequest(pr:any,sha:string):boolean {
|
|
75
|
-
const escaped=this.context.repository.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");
|
|
76
|
-
return pr?.state==="open" && pr?.head?.sha===sha && pr?.head?.ref===this.context.branch &&
|
|
77
|
-
pr?.head?.repo?.full_name===this.context.repository && pr?.base?.ref===this.context.baseBranch &&
|
|
78
|
-
pr?.base?.repo?.full_name===this.context.repository &&
|
|
79
|
-
new RegExp(`^https://github\\.com/${escaped}/pull/[0-9]+$`).test(pr?.html_url??"");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private async githubRequest(method:"GET"|"POST",path:string,credential:GitHubCredential,body?:Record<string,string|boolean>,signal?:AbortSignal):Promise<{ok:boolean;status:number;body:any}> {
|
|
83
|
-
const timeout=AbortSignal.timeout(15_000);
|
|
84
|
-
const requestSignal=signal?AbortSignal.any([signal,timeout]):timeout;
|
|
85
|
-
let response:Response;
|
|
86
|
-
try {
|
|
87
|
-
response=await this.apiFetch("https://api.github.com"+path,{
|
|
88
|
-
method,redirect:"error",signal:requestSignal,
|
|
89
|
-
headers:{authorization:`Bearer ${credential.token}`,accept:"application/vnd.github+json","x-github-api-version":"2022-11-28","content-type":"application/json","user-agent":"dev-orchestrator"},
|
|
90
|
-
...(body?{body:JSON.stringify(body)}:{})
|
|
91
|
-
});
|
|
92
|
-
} catch {throw new Error("GitHub pull request service is temporarily unavailable.");}
|
|
93
|
-
let payload:any;
|
|
94
|
-
try {payload=await response.json();} catch {throw new Error("GitHub returned an invalid pull request response.");}
|
|
95
|
-
return {ok:response.ok,status:response.status,body:payload};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private async trusted<T>(fn:(git:(args:string[],allowExitOne?:boolean,commandEnv?:NodeJS.ProcessEnv)=>Promise<{output:string;code:number}>,sha:string,temp:string,objects:string)=>Promise<T>,signal?:AbortSignal):Promise<T> {
|
|
99
|
-
const workspace=await realpath(this.context.workspace);
|
|
100
|
-
const pointer=join(workspace,".git");
|
|
101
|
-
const stat=await lstat(pointer);
|
|
102
|
-
if(stat.isSymbolicLink() || (!stat.isDirectory() && !stat.isFile()))throw new Error("Publication needs valid Git metadata.");
|
|
103
|
-
let gitDir:string;
|
|
104
|
-
let commonDir:string;
|
|
105
|
-
if(stat.isDirectory()) {
|
|
106
|
-
gitDir=workspace+sep+".git";
|
|
107
|
-
commonDir=gitDir;
|
|
108
|
-
} else {
|
|
109
|
-
const content=await readFile(pointer,"utf8");
|
|
110
|
-
const match=content.match(/^gitdir: (.+)\r?\n?$/);
|
|
111
|
-
if(!match)throw new Error("Publication needs valid Git worktree metadata.");
|
|
112
|
-
gitDir=await realpath(resolve(workspace,match[1]));
|
|
113
|
-
commonDir=await realpath(join(gitDir,"..",".."));
|
|
114
|
-
const worktrees=await realpath(join(commonDir,"worktrees"));
|
|
115
|
-
if(!gitDir.startsWith(worktrees+sep) || gitDir.slice(worktrees.length+1).includes(sep))throw new Error("Publication worktree metadata escaped its repository.");
|
|
116
|
-
}
|
|
117
|
-
const objects=await realpath(join(commonDir,"objects"));
|
|
118
|
-
if(!objects.startsWith(commonDir+sep))throw new Error("Publication needs an internal Git object directory.");
|
|
119
|
-
for(const forbidden of ["info/alternates","info/http-alternates"]) {
|
|
120
|
-
try {await access(join(objects,forbidden));throw new Error("External Git object stores are not supported.");}
|
|
121
|
-
catch(error) {if((error as NodeJS.ErrnoException).code!=="ENOENT")throw error;}
|
|
122
|
-
}
|
|
123
|
-
const head=await readFile(join(gitDir,"HEAD"),"utf8");
|
|
124
|
-
if(head.trim()!==`ref: refs/heads/${this.context.branch}`)throw new Error("Task branch changed unexpectedly.");
|
|
125
|
-
const ref=join(commonDir,"refs","heads",this.context.branch);
|
|
126
|
-
const resolved=await realpath(ref);
|
|
127
|
-
if(!resolved.startsWith(commonDir+sep))throw new Error("Unsafe Git reference.");
|
|
128
|
-
const sha=(await readFile(ref,"utf8")).trim();
|
|
129
|
-
if(!validSHA(sha))throw new Error("Invalid task commit.");
|
|
130
|
-
const temporary=await mkdtemp(join(tmpdir(),"dev-orchestrator-publish-"));
|
|
131
|
-
try {
|
|
132
|
-
const bare=join(temporary,"repo.git");
|
|
133
|
-
await this.runCommand("git",["init","--bare","--template=",bare],{signal});
|
|
134
|
-
await mkdir(join(bare,"info"),{mode:0o700});
|
|
135
|
-
const excludes=join(commonDir,"info","exclude");
|
|
136
|
-
const excludesInfo=await lstat(excludes).catch(error=>{if((error as NodeJS.ErrnoException).code==="ENOENT")return undefined;throw error;});
|
|
137
|
-
if(excludesInfo) {
|
|
138
|
-
if(!excludesInfo.isFile() || excludesInfo.isSymbolicLink() || excludesInfo.size>1_048_576)throw new Error("Unsafe Git exclude metadata.");
|
|
139
|
-
await writeFile(join(bare,"info","exclude"),await readFile(excludes,"utf8"),{mode:0o600});
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
const shallow=await readFile(join(commonDir,"shallow"),"utf8");
|
|
143
|
-
if(!shallow.trim().split("\n").every(validSHA))throw new Error("Invalid shallow boundary.");
|
|
144
|
-
await writeFile(join(bare,"shallow"),shallow,{mode:0o600});
|
|
145
|
-
} catch(error) {if((error as NodeJS.ErrnoException).code!=="ENOENT")throw error;}
|
|
146
|
-
const env={...cleanGitEnv(),GIT_OBJECT_DIRECTORY:objects};
|
|
147
|
-
const git=(args:string[],allowExitOne=false,commandEnv:NodeJS.ProcessEnv=env)=>this.runCommand("git",["--git-dir",bare,"--work-tree",workspace,"-c","core.bare=false","-c","core.hooksPath=/dev/null",...args],{cwd:temporary,env:commandEnv,signal,allowExitOne});
|
|
148
|
-
await git(["read-tree",sha]);
|
|
149
|
-
return await fn(git,sha,temporary,objects);
|
|
150
|
-
} finally {await rm(temporary,{recursive:true,force:true});}
|
|
151
|
-
}
|
|
152
|
-
}
|
package/src/repository.ts
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { command } from "./process.ts";
|
|
5
|
-
import { withGitHubCredential } from "./git-credential.ts";
|
|
6
|
-
import type { GitHubCredential, Lease } from "./types.ts";
|
|
7
|
-
import { validBranch, validRepository } from "./workspace.ts";
|
|
8
|
-
|
|
9
|
-
type Marker={run:string;lifecycle:number;project:string;repository:string;base_branch:string;baseline:string;branch:string};
|
|
10
|
-
type Options={remoteURL?:(repository:string)=>string;runCommand?:typeof command};
|
|
11
|
-
const validId=(value:unknown):value is string=>typeof value==="string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value);
|
|
12
|
-
const validSHA=(value:unknown):value is string=>typeof value==="string" && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value);
|
|
13
|
-
|
|
14
|
-
async function ensureDirectory(path:string):Promise<string> {
|
|
15
|
-
await mkdir(path,{recursive:true,mode:0o700});
|
|
16
|
-
const info=await lstat(path);
|
|
17
|
-
const canonical=await realpath(path);
|
|
18
|
-
if(!info.isDirectory() || info.isSymbolicLink() || canonical!==resolve(path))throw new Error("Managed repository storage must be a canonical directory without symlinks.");
|
|
19
|
-
await chmod(path,0o700);
|
|
20
|
-
return canonical;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function acquireLock(path:string):Promise<()=>Promise<void>> {
|
|
24
|
-
const deadline=Date.now()+30_000;
|
|
25
|
-
while(Date.now()<deadline) {
|
|
26
|
-
try {
|
|
27
|
-
const file=await open(path,"wx",0o600);
|
|
28
|
-
await file.writeFile(String(process.pid));await file.sync();await file.close();
|
|
29
|
-
return async()=>{await unlink(path).catch(error=>{if((error as NodeJS.ErrnoException).code!=="ENOENT")throw error;});};
|
|
30
|
-
} catch(error) {
|
|
31
|
-
const code=(error as NodeJS.ErrnoException).code;
|
|
32
|
-
if(code!=="EEXIST")throw error;
|
|
33
|
-
const info=await lstat(path).catch(()=>undefined);
|
|
34
|
-
if(info?.isSymbolicLink() || (info && !info.isFile()))throw new Error("Unsafe repository cache lock.");
|
|
35
|
-
const value=info?await readFile(path,"utf8").catch(()=>""):"";
|
|
36
|
-
const pid=Number(value.trim());
|
|
37
|
-
if(Number.isInteger(pid) && pid>0 && pid!==process.pid) {
|
|
38
|
-
try {process.kill(pid,0);} catch(e) {
|
|
39
|
-
if((e as NodeJS.ErrnoException).code==="ESRCH") {await unlink(path).catch(()=>{});continue;}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
await new Promise(resolve=>setTimeout(resolve,100));
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
throw new Error("Repository cache is busy. Retry after the active repository operation finishes.");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function worktreeGitDirectory(workspace:string,cache:string):Promise<string> {
|
|
49
|
-
const pointer=join(workspace,".git");
|
|
50
|
-
const info=await lstat(pointer);
|
|
51
|
-
if(!info.isFile() || info.isSymbolicLink())throw new Error("Task worktree Git pointer is invalid.");
|
|
52
|
-
const contents=await readFile(pointer,"utf8");
|
|
53
|
-
const match=contents.match(/^gitdir: (.+)\r?\n?$/);
|
|
54
|
-
if(!match)throw new Error("Task worktree Git pointer is invalid.");
|
|
55
|
-
const gitdir=await realpath(resolve(workspace,match[1]));
|
|
56
|
-
const worktrees=await realpath(join(cache,"worktrees"));
|
|
57
|
-
if(!gitdir.startsWith(worktrees+sep) || relative(worktrees,gitdir).split(sep).length!==1)throw new Error("Task worktree metadata escaped its managed repository.");
|
|
58
|
-
const gitInfo=await lstat(gitdir);
|
|
59
|
-
if(!gitInfo.isDirectory() || gitInfo.isSymbolicLink())throw new Error("Task worktree metadata is invalid.");
|
|
60
|
-
return gitdir;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function readMarker(workspace:string,cache:string):Promise<Marker> {
|
|
64
|
-
const gitdir=await worktreeGitDirectory(workspace,cache);
|
|
65
|
-
const path=join(gitdir,"orchestrator.json");
|
|
66
|
-
const info=await lstat(path);
|
|
67
|
-
if(!info.isFile() || info.isSymbolicLink() || (info.mode&0o077)!==0)throw new Error("Task worktree marker is unsafe.");
|
|
68
|
-
const marker=JSON.parse(await readFile(path,"utf8")) as Marker;
|
|
69
|
-
if(!validId(marker.run) || !validId(marker.project) || !Number.isInteger(marker.lifecycle) || marker.lifecycle<1 || !validRepository(marker.repository) || !validBranch(marker.base_branch) || !validBranch(marker.branch) || !validSHA(marker.baseline))throw new Error("Task worktree marker is invalid.");
|
|
70
|
-
return marker;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function workspaceCache(workspace:string):Promise<string> {
|
|
74
|
-
const pointer=await readFile(join(workspace,".git"),"utf8");
|
|
75
|
-
const match=pointer.match(/^gitdir: (.+)\r?\n?$/);
|
|
76
|
-
if(!match)throw new Error("Task worktree Git pointer is invalid.");
|
|
77
|
-
const gitdir=await realpath(resolve(workspace,match[1]));
|
|
78
|
-
return realpath(join(gitdir,"..",".."));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export class RepositoryWorkspace {
|
|
82
|
-
private readonly remoteURL:(repository:string)=>string;
|
|
83
|
-
private readonly runCommand:typeof command;
|
|
84
|
-
|
|
85
|
-
constructor(options:Options={}) {
|
|
86
|
-
this.remoteURL=options.remoteURL??(repository=>`https://github.com/${repository}.git`);
|
|
87
|
-
this.runCommand=options.runCommand??command;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async prepare(root:string,lease:Lease,credential:GitHubCredential):Promise<{path:string;branch:string;baseline:string}> {
|
|
91
|
-
const projectId=lease.project_id;
|
|
92
|
-
const repository=lease.snapshot?.repository;
|
|
93
|
-
const baseBranch=lease.snapshot?.base_branch;
|
|
94
|
-
if(!isAbsolute(root) || !validId(lease.id) || !validId(projectId) || !Number.isInteger(lease.lifecycle) || lease.lifecycle<1 || !validRepository(repository) || !validBranch(baseBranch))throw new Error("Task repository snapshot is invalid.");
|
|
95
|
-
const dataRoot=await ensureDirectory(root);
|
|
96
|
-
const repositories=await ensureDirectory(join(dataRoot,"repositories"));
|
|
97
|
-
const workspaces=await ensureDirectory(join(dataRoot,"workspaces"));
|
|
98
|
-
const cacheKey=createHash("sha256").update(repository).digest("hex").slice(0,16);
|
|
99
|
-
const cache=join(repositories,`${projectId}-${cacheKey}`);
|
|
100
|
-
const workspace=join(workspaces,`${lease.id}-${lease.lifecycle}`);
|
|
101
|
-
const branch=`task/${lease.id}`;
|
|
102
|
-
const remote=this.remoteURL(repository);
|
|
103
|
-
const release=await acquireLock(cache+".lock");
|
|
104
|
-
try {
|
|
105
|
-
return await withGitHubCredential(dataRoot,credential,async env=>{
|
|
106
|
-
const git=(args:string[],cwd?:string)=>this.runCommand("git",args,{cwd,env,timeout:120_000});
|
|
107
|
-
let cacheInfo=await lstat(cache).catch(error=>{if((error as NodeJS.ErrnoException).code==="ENOENT")return undefined;throw error;});
|
|
108
|
-
if(cacheInfo) {
|
|
109
|
-
if(!cacheInfo.isDirectory() || cacheInfo.isSymbolicLink() || await realpath(cache)!==cache)throw new Error("Managed repository cache is unsafe.");
|
|
110
|
-
if((await git(["--git-dir",cache,"rev-parse","--is-bare-repository"])).output!=="true")throw new Error("Managed repository cache is invalid.");
|
|
111
|
-
const existing=(await git(["--git-dir",cache,"remote","get-url","origin"])).output;
|
|
112
|
-
if(existing!==remote)throw new Error("Managed repository cache does not match the selected repository.");
|
|
113
|
-
} else {
|
|
114
|
-
await git(["-c","core.hooksPath=/dev/null","clone","--bare","--template=","--",remote,cache]);
|
|
115
|
-
cacheInfo=await lstat(cache);
|
|
116
|
-
if(!cacheInfo.isDirectory() || cacheInfo.isSymbolicLink())throw new Error("Repository checkout did not create a safe cache.");
|
|
117
|
-
await chmod(cache,0o700);
|
|
118
|
-
}
|
|
119
|
-
await git(["--git-dir",cache,"-c","core.hooksPath=/dev/null","fetch","--prune","origin",`+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`]);
|
|
120
|
-
const baseline=(await git(["--git-dir",cache,"rev-parse",`refs/remotes/origin/${baseBranch}^{commit}`])).output;
|
|
121
|
-
if(!validSHA(baseline))throw new Error("Selected base branch does not resolve to a commit.");
|
|
122
|
-
|
|
123
|
-
const existingWorkspace=await lstat(workspace).catch(error=>{if((error as NodeJS.ErrnoException).code==="ENOENT")return undefined;throw error;});
|
|
124
|
-
if(existingWorkspace) {
|
|
125
|
-
if(!existingWorkspace.isDirectory() || existingWorkspace.isSymbolicLink() || await realpath(workspace)!==workspace)throw new Error("Existing task worktree is unsafe.");
|
|
126
|
-
const marker=await readMarker(workspace,cache);
|
|
127
|
-
if(marker.run!==lease.id || marker.lifecycle!==lease.lifecycle || marker.project!==projectId || marker.repository!==repository || marker.base_branch!==baseBranch || marker.branch!==branch || !validSHA(marker.baseline))throw new Error("Existing task worktree identity does not match this lease.");
|
|
128
|
-
return {path:workspace,branch,baseline:marker.baseline};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
await git(["--git-dir",cache,"worktree","add","--force","--detach",workspace,baseline]);
|
|
132
|
-
await git(["-C",workspace,"-c","core.hooksPath=/dev/null","switch","-C",branch,"--ignore-other-worktrees",baseline]);
|
|
133
|
-
await git(["-C",workspace,"config","user.name",credential.author_name]);
|
|
134
|
-
await git(["-C",workspace,"config","user.email",credential.author_email]);
|
|
135
|
-
await git(["-C",workspace,"config","core.hooksPath","/dev/null"]);
|
|
136
|
-
const gitdir=await worktreeGitDirectory(workspace,cache);
|
|
137
|
-
await writeFile(join(gitdir,"orchestrator.json"),JSON.stringify({run:lease.id,lifecycle:lease.lifecycle,project:projectId,repository,base_branch:baseBranch,baseline,branch} satisfies Marker),{mode:0o600,flag:"wx"});
|
|
138
|
-
return {path:workspace,branch,baseline};
|
|
139
|
-
});
|
|
140
|
-
} catch(error) {
|
|
141
|
-
throw error instanceof Error && /Task repository|Managed repository|Selected base|Existing task|Task worktree/.test(error.message)
|
|
142
|
-
? error
|
|
143
|
-
: new Error("Repository setup failed. Check GitHub App access, repository selection, base branch, and local storage.",{cause:error});
|
|
144
|
-
} finally {
|
|
145
|
-
await release();
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async writeProjectInstructions(workspace:string,instructions:unknown):Promise<string|undefined> {
|
|
150
|
-
if(typeof instructions!=="string" || Buffer.byteLength(instructions,"utf8")>32_000)throw new Error("Project instructions snapshot is invalid.");
|
|
151
|
-
const canonical=await realpath(workspace);
|
|
152
|
-
if(!isAbsolute(workspace) || canonical!==resolve(workspace))throw new Error("Task worktree path must be canonical.");
|
|
153
|
-
const pointer=await readFile(join(canonical,".git"),"utf8");
|
|
154
|
-
const match=pointer.match(/^gitdir: (.+)\r?\n?$/);
|
|
155
|
-
if(!match)throw new Error("Task worktree Git pointer is invalid.");
|
|
156
|
-
const gitdir=await realpath(resolve(canonical,match[1]));
|
|
157
|
-
const cache=dirname(dirname(gitdir));
|
|
158
|
-
const marker=await readMarker(canonical,cache);
|
|
159
|
-
const target=join(canonical,`.dev-orchestrator-instructions-${marker.run}-${marker.lifecycle}.md`);
|
|
160
|
-
const release=await acquireLock(cache+".lock");
|
|
161
|
-
try {
|
|
162
|
-
const existing=await lstat(target).catch(error=>{if((error as NodeJS.ErrnoException).code==="ENOENT")return undefined;throw error;});
|
|
163
|
-
if(existing?.isSymbolicLink() || (existing && !existing.isFile()))throw new Error("Project instructions target is unsafe.");
|
|
164
|
-
if(instructions==="") {
|
|
165
|
-
if(existing)await unlink(target);
|
|
166
|
-
return undefined;
|
|
167
|
-
}
|
|
168
|
-
if(existing) {
|
|
169
|
-
if(await readFile(target,"utf8")!==instructions)throw new Error("Project instructions target already exists with different content.");
|
|
170
|
-
return target;
|
|
171
|
-
}
|
|
172
|
-
const excludeDirPath=join(cache,"info");
|
|
173
|
-
await mkdir(excludeDirPath,{recursive:true,mode:0o700});
|
|
174
|
-
const excludeDir=await realpath(excludeDirPath);
|
|
175
|
-
if(!excludeDir.startsWith(cache+sep))throw new Error("Worktree ignore metadata escaped its Git directory.");
|
|
176
|
-
const excludeDirInfo=await lstat(excludeDirPath);
|
|
177
|
-
if(!excludeDirInfo.isDirectory() || excludeDirInfo.isSymbolicLink())throw new Error("Worktree ignore metadata is unsafe.");
|
|
178
|
-
const excludePath=join(excludeDir,"exclude");
|
|
179
|
-
await writeFile(excludePath,"",{encoding:"utf8",mode:0o600,flag:"wx"}).catch(error=>{
|
|
180
|
-
if((error as NodeJS.ErrnoException).code!=="EEXIST")throw error;
|
|
181
|
-
});
|
|
182
|
-
const excludeInfo=await lstat(excludePath);
|
|
183
|
-
if(!excludeInfo.isFile() || excludeInfo.isSymbolicLink())throw new Error("Worktree ignore metadata is unsafe.");
|
|
184
|
-
const exclude=await readFile(excludePath,"utf8");
|
|
185
|
-
const rule=`/${basename(target)}`;
|
|
186
|
-
if(!exclude.split(/\r?\n/).includes(rule)) {
|
|
187
|
-
const excludeTemp=join(excludeDir,`.exclude-${randomUUID()}`);
|
|
188
|
-
try {
|
|
189
|
-
await writeFile(excludeTemp,exclude+(exclude.endsWith("\n")||exclude===""?"":"\n")+rule+"\n",{encoding:"utf8",mode:0o600,flag:"wx"});
|
|
190
|
-
await rename(excludeTemp,excludePath);
|
|
191
|
-
} catch(error) {
|
|
192
|
-
await unlink(excludeTemp).catch(()=>{});
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
const temporary=join(canonical,`.instructions-${randomUUID()}`);
|
|
197
|
-
try {
|
|
198
|
-
await writeFile(temporary,instructions,{encoding:"utf8",mode:0o600,flag:"wx"});
|
|
199
|
-
await chmod(temporary,0o600);
|
|
200
|
-
await rename(temporary,target);
|
|
201
|
-
return target;
|
|
202
|
-
} catch(error) {
|
|
203
|
-
await unlink(temporary).catch(()=>{});
|
|
204
|
-
throw error;
|
|
205
|
-
}
|
|
206
|
-
} finally {await release();}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async markTerminal(workspace:string,outcome:"success"|"failure"):Promise<void> {
|
|
210
|
-
const canonical=await realpath(workspace);
|
|
211
|
-
if(!isAbsolute(workspace) || canonical!==resolve(workspace))throw new Error("Task worktree path must be canonical.");
|
|
212
|
-
const cache=await workspaceCache(canonical);
|
|
213
|
-
const gitdir=await worktreeGitDirectory(canonical,cache);
|
|
214
|
-
await readMarker(canonical,cache);
|
|
215
|
-
const terminalPath=join(gitdir,"terminal.json");
|
|
216
|
-
const existing=await lstat(terminalPath).catch(error=>{if((error as NodeJS.ErrnoException).code==="ENOENT")return undefined;throw error;});
|
|
217
|
-
if(existing) {
|
|
218
|
-
if(!existing.isFile() || existing.isSymbolicLink() || (existing.mode&0o077)!==0)throw new Error("Task terminal marker is unsafe.");
|
|
219
|
-
const previous=JSON.parse(await readFile(terminalPath,"utf8")) as {outcome?:unknown};
|
|
220
|
-
if(previous.outcome!==outcome)throw new Error("Task terminal outcome changed unexpectedly.");
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
await writeFile(terminalPath,JSON.stringify({outcome,recorded_at:Date.now()}),{mode:0o600,flag:"wx"});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async cleanupExpired(root:string,protectedWorkspace?:string,now=Date.now()):Promise<number> {
|
|
227
|
-
const canonicalRoot=await ensureDirectory(root);
|
|
228
|
-
const workspacesRoot=join(canonicalRoot,"workspaces");
|
|
229
|
-
const entries=await readdir(workspacesRoot,{withFileTypes:true}).catch(error=>{
|
|
230
|
-
if((error as NodeJS.ErrnoException).code==="ENOENT")return [];
|
|
231
|
-
throw error;
|
|
232
|
-
});
|
|
233
|
-
let removed=0;
|
|
234
|
-
for(const entry of entries) {
|
|
235
|
-
if(!entry.isDirectory() || entry.isSymbolicLink())continue;
|
|
236
|
-
const workspace=join(workspacesRoot,entry.name);
|
|
237
|
-
if(workspace===protectedWorkspace)continue;
|
|
238
|
-
try {
|
|
239
|
-
if(await realpath(workspace)!==workspace)continue;
|
|
240
|
-
const cache=await workspaceCache(workspace);
|
|
241
|
-
const marker=await readMarker(workspace,cache);
|
|
242
|
-
if(entry.name!==`${marker.run}-${marker.lifecycle}`)continue;
|
|
243
|
-
const terminalPath=join(await worktreeGitDirectory(workspace,cache),"terminal.json");
|
|
244
|
-
const terminalInfo=await lstat(terminalPath);
|
|
245
|
-
if(!terminalInfo.isFile() || terminalInfo.isSymbolicLink() || (terminalInfo.mode&0o077)!==0)continue;
|
|
246
|
-
const terminal=JSON.parse(await readFile(terminalPath,"utf8")) as {outcome?:unknown;recorded_at?:unknown};
|
|
247
|
-
if(!["success","failure"].includes(String(terminal.outcome)) || typeof terminal.recorded_at!=="number")continue;
|
|
248
|
-
const retention=terminal.outcome==="success"?24*60*60_000:7*24*60*60_000;
|
|
249
|
-
if(now-terminal.recorded_at<retention)continue;
|
|
250
|
-
if(await realpath(cache)!==cache)continue;
|
|
251
|
-
const release=await acquireLock(cache+".lock");
|
|
252
|
-
try {
|
|
253
|
-
await this.runCommand("git",["--git-dir",cache,"-c","core.hooksPath=/dev/null","worktree","remove","--force",workspace],{env:{PATH:process.env.PATH,HOME:process.env.HOME,GIT_CONFIG_NOSYSTEM:"1",GIT_CONFIG_GLOBAL:"/dev/null"},timeout:30_000});
|
|
254
|
-
await rm(workspace,{recursive:true,force:true});
|
|
255
|
-
removed++;
|
|
256
|
-
} finally {await release();}
|
|
257
|
-
} catch { /* Unknown or unsafe worktrees are retained for manual recovery. */ }
|
|
258
|
-
}
|
|
259
|
-
return removed;
|
|
260
|
-
}
|
|
261
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export type Config = { version: 2; server: string; token: string; codexBinary?: string };
|
|
2
|
-
export type TokenPurpose = "checkout" | "publish";
|
|
3
|
-
export type GitHubCredential = { token: string; expires_at: string; author_name: string; author_email: string };
|
|
4
|
-
export type Lease = { id: string; generation: number; lifecycle: number; project_id?: string; title?: string; prompt?: string; source_key?: string; snapshot?: Record<string, unknown>; expires_in_ms?: number; sequence?: number };
|
|
5
|
-
export type RunEvent = { sequence: number; kind: string; data: Record<string, unknown> };
|
|
6
|
-
export type JournalState = { lease: Lease; sequence: number; pending_event?: RunEvent; workspace?: string; baseline?: string };
|
package/src/worker.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { Journal } from "./journal.ts";
|
|
4
|
-
import { API, APIError } from "./api.ts";
|
|
5
|
-
import { CodexSession, RepeatedFailureError, CodexBlockedError } from "./codex.ts";
|
|
6
|
-
import { RepositoryWorkspace } from "./repository.ts";
|
|
7
|
-
import { Publisher, type PublicationContext } from "./publisher.ts";
|
|
8
|
-
import type { Config, JournalState } from "./types.ts";
|
|
9
|
-
|
|
10
|
-
type Transport={post:(path:string,payload:unknown)=>Promise<any>};
|
|
11
|
-
type ExecutionDependencies={dataRoot?:string;repositories?:RepositoryWorkspace;publisherFactory?:(context:PublicationContext)=>Publisher};
|
|
12
|
-
|
|
13
|
-
export async function emitDurably(api:Transport,journal:Journal,state:JournalState,kind:string,data:Record<string,unknown>):Promise<any> {
|
|
14
|
-
if(state.pending_event)throw new Error("A previous event still needs acknowledgement.");
|
|
15
|
-
state.sequence++;
|
|
16
|
-
state.pending_event={sequence:state.sequence,kind,data};
|
|
17
|
-
await journal.save(state);
|
|
18
|
-
const result=await api.post("/executor/events",{lease:state.lease,event:state.pending_event});
|
|
19
|
-
delete state.pending_event;
|
|
20
|
-
await journal.save(state);
|
|
21
|
-
return result;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function recover(api:Transport,journal:Journal,repositories?:RepositoryWorkspace,dataRoot?:string):Promise<void> {
|
|
25
|
-
const state=await journal.load();
|
|
26
|
-
if(!state)return;
|
|
27
|
-
const result=await api.post("/executor/reconcile",{lease:state.lease,journal:{pending_event:state.pending_event??null}});
|
|
28
|
-
let terminalState=result.state;
|
|
29
|
-
if(result.action==="replay" && state.pending_event) {
|
|
30
|
-
const kind=state.pending_event.kind;
|
|
31
|
-
const acknowledgement=await api.post("/executor/events",{lease:state.lease,event:state.pending_event});
|
|
32
|
-
if(["succeeded","blocked","failed","cancelled"].includes(acknowledgement?.state))terminalState=acknowledgement.state;
|
|
33
|
-
if(["started","progress"].includes(kind))terminalState=(await api.post("/executor/reconcile",{lease:state.lease,journal:{pending_event:null}})).state;
|
|
34
|
-
} else if(result.action!=="acknowledge")throw new Error("Task reconciliation needs operator attention.");
|
|
35
|
-
if(state.workspace && repositories && dataRoot && ["succeeded","blocked","failed","cancelled"].includes(terminalState)) {
|
|
36
|
-
await repositories.markTerminal(state.workspace,terminalState==="succeeded"?"success":"failure");
|
|
37
|
-
}
|
|
38
|
-
await journal.clear();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function runLoop(config:Config,root:string,signal:AbortSignal):Promise<void> {
|
|
42
|
-
const api=new API(config.server,config.token);
|
|
43
|
-
const journal=new Journal(root);
|
|
44
|
-
const repositories=new RepositoryWorkspace();
|
|
45
|
-
await recover(api,journal,repositories,dirname(root));
|
|
46
|
-
const health=CodexSession.start(config.codexBinary??"codex",root);
|
|
47
|
-
let authenticated=false;
|
|
48
|
-
try {await health.initialize();authenticated=await health.authenticated();}finally{health.stop();}
|
|
49
|
-
await api.post("/executor/heartbeat",{protocol:1,codex_authenticated:authenticated,platform:process.platform});
|
|
50
|
-
if(!authenticated)throw new Error("Sign in to the official Codex CLI with your own ChatGPT account before starting this connector.");
|
|
51
|
-
while(!signal.aborted) {
|
|
52
|
-
try {
|
|
53
|
-
await recover(api,journal,repositories,dirname(root));
|
|
54
|
-
await api.post("/executor/heartbeat",{protocol:1,codex_authenticated:true,platform:process.platform});
|
|
55
|
-
const claimStarted=performance.now();
|
|
56
|
-
const lease=await api.post("/executor/claim",{});
|
|
57
|
-
if(lease)await execute(config,api,journal,{lease,sequence:lease.sequence??0},claimStarted,signal,{dataRoot:dirname(root)});
|
|
58
|
-
} catch(error) {
|
|
59
|
-
if(signal.aborted)break;
|
|
60
|
-
if(error instanceof APIError && error.status===401)throw new Error("Executor access was revoked. Pair again with an authorized owner.");
|
|
61
|
-
console.error("Control-plane operation interrupted. Preserved task state; reconnecting without rerunning unacknowledged work.");
|
|
62
|
-
}
|
|
63
|
-
await new Promise(resolve=>setTimeout(resolve,5_000));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function execute(config:Config,api:Transport,journal:Journal,state:JournalState,claimStarted:number,signal:AbortSignal,dependencies:ExecutionDependencies={}):Promise<void> {
|
|
68
|
-
const controller=new AbortController();
|
|
69
|
-
let session:CodexSession|undefined;
|
|
70
|
-
let phase="github_installation_required";
|
|
71
|
-
let deadline=claimStarted+Math.min(state.lease.expires_in_ms??90_000,90_000)-5_000;
|
|
72
|
-
const abort=()=>{controller.abort();session?.stop();};
|
|
73
|
-
let workspacePath:string|undefined;
|
|
74
|
-
signal.addEventListener("abort",abort,{once:true});
|
|
75
|
-
const safety=setInterval(()=>{if(performance.now()>=deadline)abort();},250);
|
|
76
|
-
const renewal=setInterval(async()=>{
|
|
77
|
-
const started=performance.now();
|
|
78
|
-
try {
|
|
79
|
-
const result=await api.post("/executor/renew",state.lease);
|
|
80
|
-
if(!controller.signal.aborted)deadline=started+Math.min(result.expires_in_ms,90_000)-5_000;
|
|
81
|
-
} catch(error) {
|
|
82
|
-
if(error instanceof APIError && [401,409].includes(error.status))abort();
|
|
83
|
-
// A transport failure does not extend the last acknowledged deadline.
|
|
84
|
-
}
|
|
85
|
-
},20_000);
|
|
86
|
-
const timeBudget=setTimeout(abort,20*60_000);
|
|
87
|
-
try {
|
|
88
|
-
await journal.save(state);
|
|
89
|
-
await emitDurably(api,journal,state,"started",{});
|
|
90
|
-
await emitDurably(api,journal,state,"progress",{stage:"repository_preparing"});
|
|
91
|
-
const dataRoot=dependencies.dataRoot??dirname(journal.root);
|
|
92
|
-
const repositories=dependencies.repositories??new RepositoryWorkspace();
|
|
93
|
-
await repositories.cleanupExpired(dataRoot);
|
|
94
|
-
const credential=await api.post("/executor/github-token",{lease:state.lease,purpose:"checkout"});
|
|
95
|
-
phase="workspace_setup_failed";
|
|
96
|
-
const workspace=await repositories.prepare(dataRoot,state.lease,credential);
|
|
97
|
-
workspacePath=workspace.path;
|
|
98
|
-
const snapshot=state.lease.snapshot??{};
|
|
99
|
-
const instructionsPath=await repositories.writeProjectInstructions(workspace.path,snapshot.instructions??"");
|
|
100
|
-
state.workspace=workspace.path;state.baseline=workspace.baseline;await journal.save(state);
|
|
101
|
-
if(controller.signal.aborted)throw new Error("Lease ended.");
|
|
102
|
-
await emitDurably(api,journal,state,"progress",{stage:"workspace_ready"});
|
|
103
|
-
phase="authentication_required";
|
|
104
|
-
session=CodexSession.start(config.codexBinary??"codex",workspace.path);
|
|
105
|
-
await session.initialize();
|
|
106
|
-
if(!await session.authenticated())throw new Error("Local account is not signed in.");
|
|
107
|
-
phase="execution_failed";
|
|
108
|
-
const prompt=[
|
|
109
|
-
"Implement this development task end to end in this isolated checkout.",
|
|
110
|
-
"Read and respect the repository's AGENTS.md and project-scoped skills. Do not change global skills or configuration.",
|
|
111
|
-
"Validate changes and create a Conventional Commit on the existing issue branch. Do not push, create a PR, merge, or deploy: the trusted connector handles publication after your turn.",
|
|
112
|
-
"Do not invent external access or credentials. If essential context is missing, report a real blocker. Network access is disabled in this evaluation sandbox.",
|
|
113
|
-
instructionsPath
|
|
114
|
-
? "Project instructions: read and follow the file at "+instructionsPath+" before making changes."
|
|
115
|
-
: "Project instructions:\n"+String(snapshot.instructions??""),
|
|
116
|
-
"Task: "+String(state.lease.title??""),String(state.lease.prompt??"")
|
|
117
|
-
].join("\n\n");
|
|
118
|
-
await session.run(
|
|
119
|
-
workspace.path,
|
|
120
|
-
prompt,
|
|
121
|
-
typeof snapshot.model==="string"?snapshot.model:undefined,
|
|
122
|
-
controller.signal,
|
|
123
|
-
stage=>emitDurably(api,journal,state,"progress",{stage})
|
|
124
|
-
);
|
|
125
|
-
session.stop();session=undefined;
|
|
126
|
-
phase="publication_failed";
|
|
127
|
-
await api.post("/executor/renew",state.lease);
|
|
128
|
-
const publicationContext={workspace:workspace.path,repository:String(snapshot.repository??""),baseBranch:String(snapshot.base_branch??""),branch:workspace.branch,baseline:workspace.baseline};
|
|
129
|
-
const publisher=dependencies.publisherFactory?.(publicationContext)??new Publisher(publicationContext);
|
|
130
|
-
const status=await publisher.status(controller.signal);
|
|
131
|
-
if(status.changed) {
|
|
132
|
-
await emitDurably(api,journal,state,"progress",{stage:"publishing"});
|
|
133
|
-
const publishCredential=await api.post("/executor/github-token",{lease:state.lease,purpose:"publish"});
|
|
134
|
-
const result=await publisher.publish(state.lease.title??"Development task",`## Summary\n\nCompleted by the customer-owned local executor.\n\n## Validation\n\nReview the committed changes and repository checks before merging.\n\nRun: ${state.lease.id}`,publishCredential,controller.signal);
|
|
135
|
-
if(controller.signal.aborted)throw new Error("Lease ended.");
|
|
136
|
-
await emitDurably(api,journal,state,"progress",{stage:"pr_verifying"});
|
|
137
|
-
const acknowledgement=await emitDurably(api,journal,state,"candidate",result);
|
|
138
|
-
if(acknowledgement?.state==="succeeded")await repositories.markTerminal(workspace.path,"success").catch(()=>{});
|
|
139
|
-
} else {
|
|
140
|
-
const acknowledgement=await emitDurably(api,journal,state,"no_changes",{});
|
|
141
|
-
if(acknowledgement?.state==="succeeded")await repositories.markTerminal(workspace.path,"success").catch(()=>{});
|
|
142
|
-
}
|
|
143
|
-
await journal.clear();
|
|
144
|
-
} catch(error) {
|
|
145
|
-
session?.stop();
|
|
146
|
-
if(controller.signal.aborted || state.pending_event)throw error;
|
|
147
|
-
if(error instanceof APIError && ["github_installation_required","github_permission_denied","base_branch_missing","github_unavailable","local_policy_denied"].includes(error.code))phase=error.code;
|
|
148
|
-
else if(error instanceof APIError && error.code==="provider_authentication")phase="github_permission_denied";
|
|
149
|
-
else if(error instanceof APIError && ["provider_rejected","provider_rate_limit","provider_unavailable"].includes(error.code))phase="github_unavailable";
|
|
150
|
-
else if(error instanceof Error && /Selected base branch does not resolve to a commit/i.test(error.message))phase="base_branch_missing";
|
|
151
|
-
else if(error instanceof Error && /Repository setup failed|worktree|managed repository cache|Task repository snapshot/i.test(error.message))phase="workspace_setup_failed";
|
|
152
|
-
if(error instanceof CodexBlockedError)phase="missing_context";
|
|
153
|
-
const fingerprint=error instanceof RepeatedFailureError?error.fingerprint:createHash("sha256").update(phase+":"+(error instanceof Error?error.message:"unknown")).digest("hex");
|
|
154
|
-
const kind=["authentication_required","local_policy_denied","github_installation_required","github_permission_denied","base_branch_missing","missing_context","workspace_setup_failed"].includes(phase)?"blocked":"failure";
|
|
155
|
-
const acknowledgement=await emitDurably(api,journal,state,kind,{category:phase,fingerprint,occurrences:error instanceof RepeatedFailureError?3:1});
|
|
156
|
-
if(workspacePath && ["blocked","cancelled","failed"].includes(acknowledgement?.state)) {
|
|
157
|
-
await (dependencies.repositories??new RepositoryWorkspace()).markTerminal(workspacePath,"failure").catch(()=>{});
|
|
158
|
-
}
|
|
159
|
-
await journal.clear();
|
|
160
|
-
} finally {
|
|
161
|
-
clearInterval(safety);clearInterval(renewal);clearTimeout(timeBudget);
|
|
162
|
-
signal.removeEventListener("abort",abort);session?.stop();
|
|
163
|
-
}
|
|
164
|
-
}
|
package/src/workspace.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
export const validBranch=(value:unknown):value is string=>typeof value==="string" && value.length<=200 && /^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(value) && !value.includes("..") && !value.includes("//") && value.split("/").every(part=>part && !part.startsWith(".") && !part.endsWith(".") && !part.endsWith(".lock"));
|
|
2
|
-
export const validRepository=(value:unknown):value is string=>typeof value==="string" && value.length<=200 && /^[A-Za-z0-9][A-Za-z0-9_-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) && !value.includes("..") && !value.endsWith(".git");
|