glab-agent 0.1.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/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/glab-agent.mjs +18 -0
- package/package.json +59 -0
- package/src/local-agent/agent-config.ts +315 -0
- package/src/local-agent/agent-provider.ts +59 -0
- package/src/local-agent/agent-runner.ts +244 -0
- package/src/local-agent/claude-runner.ts +136 -0
- package/src/local-agent/cli.ts +1497 -0
- package/src/local-agent/codex-runner.ts +153 -0
- package/src/local-agent/gitlab-glab-client.ts +722 -0
- package/src/local-agent/health-server.ts +56 -0
- package/src/local-agent/heartbeat.ts +33 -0
- package/src/local-agent/log-rotate.ts +56 -0
- package/src/local-agent/logger.ts +92 -0
- package/src/local-agent/metrics.ts +51 -0
- package/src/local-agent/mr-actions.ts +121 -0
- package/src/local-agent/notifier.ts +190 -0
- package/src/local-agent/process-manager.ts +193 -0
- package/src/local-agent/reply-runner.ts +111 -0
- package/src/local-agent/repo-cache.ts +144 -0
- package/src/local-agent/report.ts +183 -0
- package/src/local-agent/skill-import.ts +344 -0
- package/src/local-agent/skill-inject.ts +109 -0
- package/src/local-agent/skill-parse.ts +47 -0
- package/src/local-agent/smoke-test.ts +443 -0
- package/src/local-agent/state-store.ts +186 -0
- package/src/local-agent/token-check.ts +37 -0
- package/src/local-agent/watcher.ts +1226 -0
- package/src/local-agent/wiki-sync.ts +290 -0
- package/src/local-agent/worktree-manager.ts +141 -0
- package/src/text.ts +16 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
import type { GitlabIssue } from "./gitlab-glab-client.js";
|
|
9
|
+
import type { WorktreeInfo } from "./worktree-manager.js";
|
|
10
|
+
import {
|
|
11
|
+
AgentRunnerError,
|
|
12
|
+
type AgentRunContext,
|
|
13
|
+
type AgentRunResult,
|
|
14
|
+
type AgentRunner,
|
|
15
|
+
type AgentRunnerOptions,
|
|
16
|
+
buildAgentPrompt,
|
|
17
|
+
createOutputPath,
|
|
18
|
+
isSmokeIssue,
|
|
19
|
+
readSummary,
|
|
20
|
+
runSmokeTestChecks
|
|
21
|
+
} from "./agent-runner.js";
|
|
22
|
+
import { createLogger, type Logger } from "./logger.js";
|
|
23
|
+
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
25
|
+
|
|
26
|
+
export class CodexRunner implements AgentRunner {
|
|
27
|
+
private readonly execFileImpl: typeof execFileAsync;
|
|
28
|
+
|
|
29
|
+
private readonly env: NodeJS.ProcessEnv;
|
|
30
|
+
|
|
31
|
+
private readonly repoPath?: string;
|
|
32
|
+
|
|
33
|
+
private readonly gitlabHost: string;
|
|
34
|
+
|
|
35
|
+
private readonly gitlabProjectId: number;
|
|
36
|
+
|
|
37
|
+
private readonly agentDefinition?: import("./agent-config.js").AgentDefinition;
|
|
38
|
+
|
|
39
|
+
private readonly logger: Logger;
|
|
40
|
+
|
|
41
|
+
private readonly timeoutSeconds?: number;
|
|
42
|
+
|
|
43
|
+
constructor(options: AgentRunnerOptions = {}) {
|
|
44
|
+
this.execFileImpl = options.execFileImpl ?? execFileAsync;
|
|
45
|
+
this.env = options.env ?? process.env;
|
|
46
|
+
this.repoPath = options.repoPath;
|
|
47
|
+
this.gitlabHost = options.gitlabHost ?? "";
|
|
48
|
+
this.gitlabProjectId = options.gitlabProjectId ?? 0;
|
|
49
|
+
this.agentDefinition = options.agentDefinition;
|
|
50
|
+
this.logger = options.logger ?? createLogger("runner", options.agentDefinition?.name);
|
|
51
|
+
this.timeoutSeconds = options.timeoutSeconds ?? options.agentDefinition?.timeout_seconds;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async run(issue: GitlabIssue, worktree: WorktreeInfo, context?: AgentRunContext): Promise<AgentRunResult> {
|
|
55
|
+
const signal = context?.signal;
|
|
56
|
+
const outputPath = await createOutputPath("codex", issue.iid);
|
|
57
|
+
|
|
58
|
+
if (isSmokeIssue(issue)) {
|
|
59
|
+
return runSmokeTestChecks(issue, worktree, outputPath, this.execFileImpl, this.env, this.repoPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const prompt = buildAgentPrompt({
|
|
63
|
+
issueIid: issue.iid,
|
|
64
|
+
issueTitle: issue.title,
|
|
65
|
+
gitlabHost: this.gitlabHost,
|
|
66
|
+
gitlabProjectId: this.gitlabProjectId,
|
|
67
|
+
worktreePath: worktree.worktreePath,
|
|
68
|
+
branch: worktree.branch,
|
|
69
|
+
provider: "codex",
|
|
70
|
+
todoId: context?.todoId,
|
|
71
|
+
preamble: this.agentDefinition?.prompt.preamble,
|
|
72
|
+
append: this.agentDefinition?.prompt.append,
|
|
73
|
+
// skills injected via .agent_context/skills/*.md files, not in prompt
|
|
74
|
+
});
|
|
75
|
+
const codexCommand = resolveCodexCommand(this.env);
|
|
76
|
+
|
|
77
|
+
this.logger.info(`Executing: ${codexCommand} exec --full-auto -C ${worktree.worktreePath}`);
|
|
78
|
+
this.logger.info(`Prompt (${prompt.length} chars): issue #${issue.iid} "${issue.title}"`);
|
|
79
|
+
|
|
80
|
+
const codexLogPath = outputPath.replace("last-message.txt", "codex-output.log");
|
|
81
|
+
const timeoutMs = this.timeoutSeconds ? this.timeoutSeconds * 1000 : 0;
|
|
82
|
+
if (this.timeoutSeconds) {
|
|
83
|
+
this.logger.info(`Timeout: ${this.timeoutSeconds}s`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const codexArgs = ["exec", "--full-auto", "-C", worktree.worktreePath, "--output-last-message", outputPath];
|
|
88
|
+
if (this.agentDefinition?.model) {
|
|
89
|
+
codexArgs.push("--model", this.agentDefinition.model);
|
|
90
|
+
}
|
|
91
|
+
codexArgs.push(prompt);
|
|
92
|
+
|
|
93
|
+
const { stdout, stderr } = await this.execFileImpl(
|
|
94
|
+
codexCommand,
|
|
95
|
+
codexArgs,
|
|
96
|
+
{
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
99
|
+
env: this.env,
|
|
100
|
+
...(timeoutMs > 0 ? { timeout: timeoutMs, killSignal: "SIGTERM" } : {}),
|
|
101
|
+
...(signal ? { signal } : {})
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const output = [stdout, stderr].filter(Boolean).join("\n--- stderr ---\n");
|
|
106
|
+
await writeFile(codexLogPath, output || "(no output)", "utf8");
|
|
107
|
+
if (output) {
|
|
108
|
+
this.logger.info(`Codex output (last 300): ${output.slice(-300)}`);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const execError = error as { stdout?: string; stderr?: string; killed?: boolean };
|
|
112
|
+
const isTimeout = execError.killed === true && timeoutMs > 0;
|
|
113
|
+
const output = [execError.stdout, execError.stderr].filter(Boolean).join("\n--- stderr ---\n");
|
|
114
|
+
await writeFile(codexLogPath, output || String(error), "utf8").catch(() => undefined);
|
|
115
|
+
if (isTimeout) {
|
|
116
|
+
this.logger.error(`Timed out after ${this.timeoutSeconds}s.`);
|
|
117
|
+
throw new AgentRunnerError(
|
|
118
|
+
`Codex timed out after ${this.timeoutSeconds}s.`,
|
|
119
|
+
`Timed out after ${this.timeoutSeconds}s`,
|
|
120
|
+
outputPath
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (execError.stderr) {
|
|
124
|
+
this.logger.error(`Stderr: ${execError.stderr.slice(0, 500)}`);
|
|
125
|
+
}
|
|
126
|
+
this.logger.error(`Failed: ${String(error).slice(0, 300)}`);
|
|
127
|
+
const summary = await readSummary(outputPath, `Codex execution failed: ${String(error)}`);
|
|
128
|
+
throw new AgentRunnerError("Codex execution failed.", summary, outputPath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.logger.info(`Finished. Output: ${outputPath} | Log: ${codexLogPath}`);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
summary: await readSummary(outputPath, "Codex finished without a final summary."),
|
|
135
|
+
outputPath
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resolveCodexCommand(env: NodeJS.ProcessEnv = process.env): string {
|
|
141
|
+
const explicit = env.CODEX_BIN?.trim();
|
|
142
|
+
|
|
143
|
+
if (explicit) {
|
|
144
|
+
return explicit;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const homeCodex = path.join(homedir(), ".local", "bin", "codex");
|
|
148
|
+
if (existsSync(homeCodex)) {
|
|
149
|
+
return homeCodex;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return "codex";
|
|
153
|
+
}
|