@opentag/runner 0.1.0 → 0.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/LICENSE +21 -0
- package/dist/claude-code.d.ts +11 -0
- package/dist/claude-code.d.ts.map +1 -0
- package/dist/codex.d.ts +3 -1
- package/dist/codex.d.ts.map +1 -1
- package/dist/command.d.ts +2 -0
- package/dist/command.d.ts.map +1 -1
- package/dist/echo.d.ts.map +1 -1
- package/dist/executor-report.d.ts +24 -0
- package/dist/executor-report.d.ts.map +1 -0
- package/dist/executor.d.ts +7 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/git.d.ts +35 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +789 -48
- package/dist/index.js.map +1 -1
- package/dist/result.d.ts +11 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/security.d.ts +32 -0
- package/dist/security.d.ts.map +1 -0
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
// src/claude-code.ts
|
|
2
|
+
import { contextPointerLabel as contextPointerLabel2 } from "@opentag/core";
|
|
3
|
+
|
|
1
4
|
// src/command.ts
|
|
2
5
|
import { spawn } from "child_process";
|
|
3
6
|
var nodeCommandRunner = {
|
|
4
7
|
run(command, args, options = {}) {
|
|
5
|
-
return new Promise((
|
|
8
|
+
return new Promise((resolve2, reject) => {
|
|
6
9
|
const child = spawn(command, args, {
|
|
7
10
|
cwd: options.cwd,
|
|
11
|
+
env: options.env,
|
|
8
12
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9
13
|
});
|
|
10
14
|
const stdout = [];
|
|
@@ -13,12 +17,14 @@ var nodeCommandRunner = {
|
|
|
13
17
|
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
14
18
|
child.on("error", reject);
|
|
15
19
|
child.on("close", (exitCode) => {
|
|
16
|
-
|
|
20
|
+
resolve2({
|
|
17
21
|
exitCode: exitCode ?? 1,
|
|
18
22
|
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
19
23
|
stderr: Buffer.concat(stderr).toString("utf8")
|
|
20
24
|
});
|
|
21
25
|
});
|
|
26
|
+
child.stdin.on("error", () => {
|
|
27
|
+
});
|
|
22
28
|
if (options.input) {
|
|
23
29
|
child.stdin.write(options.input);
|
|
24
30
|
}
|
|
@@ -32,17 +38,199 @@ async function assertCommandSucceeded(result, label) {
|
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
// src/executor-report.ts
|
|
42
|
+
var EXECUTOR_REPORT_START = "OPENTAG_EXECUTOR_REPORT_START";
|
|
43
|
+
var EXECUTOR_REPORT_END = "OPENTAG_EXECUTOR_REPORT_END";
|
|
44
|
+
var REPORT_OUTCOMES = /* @__PURE__ */ new Set(["passed", "failed", "not_run"]);
|
|
45
|
+
var MAX_REPORT_ITEMS = 8;
|
|
46
|
+
var MAX_REPORT_TEXT_LENGTH = 600;
|
|
47
|
+
function executorReportPromptLines() {
|
|
48
|
+
return [
|
|
49
|
+
"End with this exact machine-readable OpenTag executor report block. Put it last.",
|
|
50
|
+
"Replace every example value with the actual result. Use changes: [] if no files changed.",
|
|
51
|
+
"Use verification: [] if no checks ran. Use verification outcome exactly one of: passed, failed, not_run.",
|
|
52
|
+
EXECUTOR_REPORT_START,
|
|
53
|
+
JSON.stringify(
|
|
54
|
+
{
|
|
55
|
+
changes: [{ file: "README.md", summary: "Added one sentence describing the completed change." }],
|
|
56
|
+
verification: [{ command: "corepack pnpm test", outcome: "passed", summary: "Tests passed." }],
|
|
57
|
+
risks: []
|
|
58
|
+
},
|
|
59
|
+
null,
|
|
60
|
+
2
|
|
61
|
+
),
|
|
62
|
+
EXECUTOR_REPORT_END
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
function executorPolicyPromptLines() {
|
|
66
|
+
return [
|
|
67
|
+
"Work autonomously but keep the change narrow. Run relevant verification if you modify files.",
|
|
68
|
+
"OpenTag owns the source-control handoff after you finish.",
|
|
69
|
+
"Do not run, request, or recommend git add, git commit, git push, or gh pr create.",
|
|
70
|
+
"Do not ask the user to approve local source-control commands; summarize file changes and verification only.",
|
|
71
|
+
"OpenTag will publish the run branch and expose pull-request creation as a suggested action.",
|
|
72
|
+
...executorReportPromptLines()
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
function asRecord(value) {
|
|
76
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
77
|
+
}
|
|
78
|
+
function cleanReportText(value) {
|
|
79
|
+
if (typeof value !== "string") return void 0;
|
|
80
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
81
|
+
return text.length > 0 ? text.slice(0, MAX_REPORT_TEXT_LENGTH) : void 0;
|
|
82
|
+
}
|
|
83
|
+
function cleanStringArray(value) {
|
|
84
|
+
if (!Array.isArray(value)) return void 0;
|
|
85
|
+
const items = value.map(cleanReportText).filter((item) => Boolean(item));
|
|
86
|
+
return items.length > 0 ? items.slice(0, MAX_REPORT_ITEMS) : void 0;
|
|
87
|
+
}
|
|
88
|
+
function normalizeReport(value) {
|
|
89
|
+
const record = asRecord(value);
|
|
90
|
+
if (!record || !Array.isArray(record["changes"])) return void 0;
|
|
91
|
+
const changes = record["changes"].map((item) => {
|
|
92
|
+
const change = asRecord(item);
|
|
93
|
+
if (!change) return void 0;
|
|
94
|
+
const summary = cleanReportText(change["summary"]);
|
|
95
|
+
if (!summary) return void 0;
|
|
96
|
+
const file = cleanReportText(change["file"]);
|
|
97
|
+
return file ? { file, summary } : { summary };
|
|
98
|
+
}).filter((item) => Boolean(item)).slice(0, MAX_REPORT_ITEMS);
|
|
99
|
+
if (record["changes"].length > 0 && changes.length === 0) return void 0;
|
|
100
|
+
const verificationRaw = record["verification"];
|
|
101
|
+
const verification = Array.isArray(verificationRaw) ? verificationRaw.map((item) => {
|
|
102
|
+
const check = asRecord(item);
|
|
103
|
+
if (!check || typeof check["outcome"] !== "string" || !REPORT_OUTCOMES.has(check["outcome"])) return void 0;
|
|
104
|
+
const command = cleanReportText(check["command"]);
|
|
105
|
+
const summary = cleanReportText(check["summary"]);
|
|
106
|
+
return {
|
|
107
|
+
...command ? { command } : {},
|
|
108
|
+
outcome: check["outcome"],
|
|
109
|
+
...summary ? { summary } : {}
|
|
110
|
+
};
|
|
111
|
+
}).filter((item) => Boolean(item)).slice(0, MAX_REPORT_ITEMS) : void 0;
|
|
112
|
+
if (Array.isArray(verificationRaw) && verificationRaw.length > 0 && (!verification || verification.length === 0)) {
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
const risks = cleanStringArray(record["risks"]);
|
|
116
|
+
const notes = cleanStringArray(record["notes"]);
|
|
117
|
+
return {
|
|
118
|
+
changes,
|
|
119
|
+
...verification && verification.length > 0 ? { verification } : {},
|
|
120
|
+
...risks ? { risks } : {},
|
|
121
|
+
...notes ? { notes } : {}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function markerCandidate(output) {
|
|
125
|
+
const startIndex = output.lastIndexOf(EXECUTOR_REPORT_START);
|
|
126
|
+
if (startIndex < 0) return void 0;
|
|
127
|
+
const afterStart = output.slice(startIndex + EXECUTOR_REPORT_START.length);
|
|
128
|
+
const endIndex = afterStart.indexOf(EXECUTOR_REPORT_END);
|
|
129
|
+
return (endIndex >= 0 ? afterStart.slice(0, endIndex) : afterStart).trim();
|
|
130
|
+
}
|
|
131
|
+
function parseCandidate(candidate) {
|
|
132
|
+
try {
|
|
133
|
+
return normalizeReport(JSON.parse(candidate));
|
|
134
|
+
} catch {
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function parseExecutorReport(output) {
|
|
139
|
+
const marker = markerCandidate(output);
|
|
140
|
+
if (marker) {
|
|
141
|
+
const parsed = parseCandidate(marker);
|
|
142
|
+
if (parsed) return parsed;
|
|
143
|
+
}
|
|
144
|
+
return parseCandidate(output.trim());
|
|
145
|
+
}
|
|
146
|
+
function deterministicSummary(input) {
|
|
147
|
+
if (input.changedFiles.length === 0) {
|
|
148
|
+
return `${input.executorName} completed without file changes.`;
|
|
149
|
+
}
|
|
150
|
+
return `${input.executorName} changed ${input.changedFiles.length} file(s). Changed files: ${input.changedFiles.join(", ")}.`;
|
|
151
|
+
}
|
|
152
|
+
function renderExecutorReportSummary(input) {
|
|
153
|
+
const lines = [];
|
|
154
|
+
if (input.report.changes.length > 0) {
|
|
155
|
+
lines.push("What changed:");
|
|
156
|
+
for (const change of input.report.changes) {
|
|
157
|
+
lines.push(`- ${change.file ? `\`${change.file}\`: ` : ""}${change.summary}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (input.report.verification?.length) {
|
|
161
|
+
if (lines.length > 0) lines.push("");
|
|
162
|
+
lines.push("Verified:");
|
|
163
|
+
for (const check of input.report.verification) {
|
|
164
|
+
const prefix = check.command ? `\`${check.command}\`: ${check.outcome}` : check.outcome;
|
|
165
|
+
lines.push(`- ${check.summary ? `${prefix} - ${check.summary}` : prefix}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (input.report.risks?.length) {
|
|
169
|
+
if (lines.length > 0) lines.push("");
|
|
170
|
+
lines.push("Risks:");
|
|
171
|
+
for (const risk of input.report.risks) {
|
|
172
|
+
lines.push(`- ${risk}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return lines.length > 0 ? lines.join("\n") : deterministicSummary(input);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/executor.ts
|
|
179
|
+
import { contextPointerLabel } from "@opentag/core";
|
|
180
|
+
function renderContextPacketForPrompt(packet) {
|
|
181
|
+
if (!packet) return [];
|
|
182
|
+
const lines = ["OpenTag context packet:", `- summary: ${packet.summary}`];
|
|
183
|
+
if (packet.intent) {
|
|
184
|
+
lines.push(`- intent: ${packet.intent.normalizedIntent}`);
|
|
185
|
+
lines.push(`- requested by: ${packet.intent.requestedBy.provider}:${packet.intent.requestedBy.providerUserId}`);
|
|
186
|
+
}
|
|
187
|
+
if (packet.sources?.length) {
|
|
188
|
+
lines.push("- selected sources:");
|
|
189
|
+
for (const source of packet.sources) {
|
|
190
|
+
lines.push(` - [${source.role}] ${contextPointerLabel(source.pointer)}: ${source.pointer.uri}`);
|
|
191
|
+
lines.push(` reason: ${source.reason}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (packet.facts?.length) {
|
|
195
|
+
lines.push("- facts:");
|
|
196
|
+
for (const fact of packet.facts) {
|
|
197
|
+
lines.push(` - ${fact.text}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (packet.exclusions?.length) {
|
|
201
|
+
lines.push("- exclusions:");
|
|
202
|
+
for (const exclusion of packet.exclusions) {
|
|
203
|
+
lines.push(` - ${exclusion}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return lines;
|
|
207
|
+
}
|
|
208
|
+
|
|
35
209
|
// src/git.ts
|
|
210
|
+
import { mkdirSync } from "fs";
|
|
211
|
+
import { dirname } from "path";
|
|
36
212
|
var INTERNAL_ARTIFACT_ROOTS = [".omx", ".codex", ".claude"];
|
|
37
213
|
function branchNameForRun(runId) {
|
|
38
214
|
const safeRunId = runId.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
39
215
|
return `opentag/${safeRunId}`;
|
|
40
216
|
}
|
|
217
|
+
var STATUS_PORCELAIN_Z_ARGS = ["-c", "core.quotePath=false", "status", "--porcelain", "-z"];
|
|
41
218
|
function parseStatusEntries(statusOutput) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
219
|
+
const records = statusOutput.split("\0");
|
|
220
|
+
const entries = [];
|
|
221
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
222
|
+
const record = records[index];
|
|
223
|
+
if (!record) continue;
|
|
224
|
+
const status = record.slice(0, 2);
|
|
225
|
+
const path = record.slice(3);
|
|
226
|
+
const isRenameOrCopy = status.includes("R") || status.includes("C");
|
|
227
|
+
if (isRenameOrCopy) {
|
|
228
|
+
index += 1;
|
|
229
|
+
}
|
|
230
|
+
if (path.length === 0) continue;
|
|
231
|
+
entries.push({ status, path });
|
|
232
|
+
}
|
|
233
|
+
return entries;
|
|
46
234
|
}
|
|
47
235
|
function isInternalArtifactPath(path) {
|
|
48
236
|
return INTERNAL_ARTIFACT_ROOTS.some((root) => path === root || path.startsWith(`${root}/`));
|
|
@@ -51,16 +239,42 @@ function parseChangedFiles(statusOutput) {
|
|
|
51
239
|
return parseStatusEntries(statusOutput).map((entry) => entry.path).filter((path) => !isInternalArtifactPath(path));
|
|
52
240
|
}
|
|
53
241
|
async function createRunBranch(input) {
|
|
54
|
-
const result = await input.runner.run("git", ["checkout", "-B", input.branchName], { cwd: input.workspacePath });
|
|
242
|
+
const result = await input.runner.run("git", ["checkout", "-B", input.branchName, ...input.startPoint ? [input.startPoint] : []], { cwd: input.workspacePath });
|
|
55
243
|
await assertCommandSucceeded(result, "create run branch");
|
|
56
244
|
}
|
|
245
|
+
function worktreePathForRun(input) {
|
|
246
|
+
const safeRunId = input.runId.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
247
|
+
const root = input.worktreeRoot ?? `${input.workspacePath.replace(/\/$/, "")}/.worktrees/opentag`;
|
|
248
|
+
return `${root.replace(/\/$/, "")}/${safeRunId}`;
|
|
249
|
+
}
|
|
250
|
+
async function createRunWorktree(input) {
|
|
251
|
+
mkdirSync(dirname(input.worktreePath), { recursive: true });
|
|
252
|
+
const result = await input.runner.run(
|
|
253
|
+
"git",
|
|
254
|
+
["worktree", "add", "-B", input.branchName, input.worktreePath, input.baseBranch],
|
|
255
|
+
{ cwd: input.workspacePath }
|
|
256
|
+
);
|
|
257
|
+
await assertCommandSucceeded(result, "create run worktree");
|
|
258
|
+
}
|
|
259
|
+
async function removeRunWorktree(input) {
|
|
260
|
+
const result = await input.runner.run("git", ["worktree", "remove", "--force", input.worktreePath], {
|
|
261
|
+
cwd: input.workspacePath
|
|
262
|
+
});
|
|
263
|
+
await assertCommandSucceeded(result, "remove run worktree");
|
|
264
|
+
}
|
|
265
|
+
async function deleteRunBranch(input) {
|
|
266
|
+
const result = await input.runner.run("git", ["branch", "-D", input.branchName], {
|
|
267
|
+
cwd: input.workspacePath
|
|
268
|
+
});
|
|
269
|
+
await assertCommandSucceeded(result, "delete empty run branch");
|
|
270
|
+
}
|
|
57
271
|
async function changedFiles(input) {
|
|
58
|
-
const result = await input.runner.run("git",
|
|
272
|
+
const result = await input.runner.run("git", STATUS_PORCELAIN_Z_ARGS, { cwd: input.workspacePath });
|
|
59
273
|
await assertCommandSucceeded(result, "read changed files");
|
|
60
274
|
return parseChangedFiles(result.stdout);
|
|
61
275
|
}
|
|
62
276
|
async function cleanupInternalArtifacts(input) {
|
|
63
|
-
const statusResult = await input.runner.run("git",
|
|
277
|
+
const statusResult = await input.runner.run("git", STATUS_PORCELAIN_Z_ARGS, { cwd: input.workspacePath });
|
|
64
278
|
await assertCommandSucceeded(statusResult, "scan internal artifacts");
|
|
65
279
|
const untrackedRoots = Array.from(
|
|
66
280
|
new Set(
|
|
@@ -74,15 +288,138 @@ async function cleanupInternalArtifacts(input) {
|
|
|
74
288
|
await assertCommandSucceeded(cleanResult, "clean internal artifacts");
|
|
75
289
|
return untrackedRoots;
|
|
76
290
|
}
|
|
291
|
+
async function commitRunChanges(input) {
|
|
292
|
+
const files = await changedFiles({ runner: input.runner, workspacePath: input.workspacePath });
|
|
293
|
+
if (files.length === 0) return false;
|
|
294
|
+
const addResult = await input.runner.run("git", ["add", "--", ...files], {
|
|
295
|
+
cwd: input.workspacePath
|
|
296
|
+
});
|
|
297
|
+
await assertCommandSucceeded(addResult, "stage run changes");
|
|
298
|
+
const commitResult = await input.runner.run("git", ["commit", "-m", input.message], {
|
|
299
|
+
cwd: input.workspacePath
|
|
300
|
+
});
|
|
301
|
+
await assertCommandSucceeded(commitResult, "commit run changes");
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
async function commitChangedFiles(input) {
|
|
305
|
+
if (input.files.length === 0) return;
|
|
306
|
+
const addResult = await input.runner.run("git", ["add", "--", ...input.files], { cwd: input.workspacePath });
|
|
307
|
+
await assertCommandSucceeded(addResult, "stage changed files");
|
|
308
|
+
const commitResult = await input.runner.run("git", ["commit", "-m", input.message], { cwd: input.workspacePath });
|
|
309
|
+
await assertCommandSucceeded(commitResult, "commit changed files");
|
|
310
|
+
}
|
|
77
311
|
async function pushBranch(input) {
|
|
78
312
|
const result = await input.runner.run("git", ["push", "-u", input.remote, input.branchName], { cwd: input.workspacePath });
|
|
79
313
|
await assertCommandSucceeded(result, "push run branch");
|
|
80
314
|
}
|
|
81
315
|
|
|
82
|
-
// src/
|
|
316
|
+
// src/result.ts
|
|
317
|
+
var MAX_EXECUTOR_SUMMARY_LENGTH = 4e3;
|
|
318
|
+
var DIRECT_SOURCE_CONTROL_COMMAND_PATTERN = /^\s*(?:[-*]\s*)?(?:`{1,3})?\s*(?:git\s+(?:add|commit|push|checkout)|gh\s+pr\s+create)\b/i;
|
|
319
|
+
var GIT_HANDOFF_PATTERNS = [
|
|
320
|
+
DIRECT_SOURCE_CONTROL_COMMAND_PATTERN,
|
|
321
|
+
/\b(?:interactive user approval|permission system)\b.*\b(?:git|source-control|commit|push|pull request|pr)\b/i,
|
|
322
|
+
/\b(?:git|source-control|commit|push|pull request|pr)\b.*\b(?:interactive user approval|permission system)\b/i,
|
|
323
|
+
/(?=.*\b(?:git\s+(?:add|commit|push|checkout)|gh\s+pr\s+create|commit|push|pull request|pr)\b)(?=.*\b(?:approval|approve|manual|need|needs|cannot|can't|please|requires?|required|finish|next action|remaining work|blocked|blocker|permission)\b)/i
|
|
324
|
+
];
|
|
325
|
+
var HANDOFF_HEADING_PATTERN = /^(?:#{1,6}\s*)?(?:\*\*)?\s*(?:blocker|recommended next action|next action|remaining work|manual steps|to finish)\b.*\b(?:git|source-control|commit|push|pull request|pr)\b/i;
|
|
326
|
+
function looksLikeGitHandoff(line) {
|
|
327
|
+
return GIT_HANDOFF_PATTERNS.some((pattern) => pattern.test(line));
|
|
328
|
+
}
|
|
329
|
+
function stripGitHandoffTail(line) {
|
|
330
|
+
if (!looksLikeGitHandoff(line)) return line;
|
|
331
|
+
return line.replace(/\s*(?:Blocker|Recommended next action|Next action|Remaining work|Manual steps|To finish)\s*:.*$/i, "").trimEnd();
|
|
332
|
+
}
|
|
333
|
+
function cleanOrFallbackExecutorSummary(input) {
|
|
334
|
+
const rawSummary = input.output.slice(-MAX_EXECUTOR_SUMMARY_LENGTH).replace(/\r\n/g, "\n");
|
|
335
|
+
const filteredLines = [];
|
|
336
|
+
for (const rawLine of rawSummary.split("\n")) {
|
|
337
|
+
const trimmed = rawLine.trim();
|
|
338
|
+
if (HANDOFF_HEADING_PATTERN.test(trimmed)) continue;
|
|
339
|
+
const withoutHandoffTail = stripGitHandoffTail(rawLine);
|
|
340
|
+
if (looksLikeGitHandoff(withoutHandoffTail)) continue;
|
|
341
|
+
if (withoutHandoffTail.trim().length === 0 && trimmed.length > 0) continue;
|
|
342
|
+
filteredLines.push(withoutHandoffTail);
|
|
343
|
+
}
|
|
344
|
+
const summary = filteredLines.join("\n").replace(/(?:^|\n)\s*(?:To finish|Manual steps):\s*\n```[^\n]*\n\s*```/gi, "\n").replace(/```[^\n]*\n\s*```/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
345
|
+
if (summary.length > 0) return summary;
|
|
346
|
+
if (input.changedFiles.length === 0) {
|
|
347
|
+
return `${input.executorName} completed without file changes.`;
|
|
348
|
+
}
|
|
349
|
+
return `${input.executorName} changed ${input.changedFiles.length} file(s). Changed files: ${input.changedFiles.join(", ")}.`;
|
|
350
|
+
}
|
|
351
|
+
function createExecutorRunResult(input) {
|
|
352
|
+
const proposalId = `proposal_${input.runId}`;
|
|
353
|
+
const report = parseExecutorReport(input.output);
|
|
354
|
+
const summary = report ? renderExecutorReportSummary({ ...input, report }) : cleanOrFallbackExecutorSummary(input);
|
|
355
|
+
const suggestedChanges = input.changedFiles.length > 0 ? [
|
|
356
|
+
{
|
|
357
|
+
proposalId,
|
|
358
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
359
|
+
sourceRunId: input.runId,
|
|
360
|
+
summary: `${input.executorName} changed ${input.changedFiles.length} file(s) on branch ${input.branchName}.`,
|
|
361
|
+
intents: [
|
|
362
|
+
{
|
|
363
|
+
intentId: `${proposalId}_create_pr`,
|
|
364
|
+
domain: "pull_request",
|
|
365
|
+
action: "create_pull_request",
|
|
366
|
+
summary: `Create a pull request for branch ${input.branchName}.`,
|
|
367
|
+
params: {
|
|
368
|
+
title: `OpenTag run ${input.runId}`,
|
|
369
|
+
body: [
|
|
370
|
+
"## Summary",
|
|
371
|
+
"",
|
|
372
|
+
summary
|
|
373
|
+
].join("\n"),
|
|
374
|
+
head: input.branchName,
|
|
375
|
+
base: input.baseBranch ?? "main",
|
|
376
|
+
changedFiles: input.changedFiles,
|
|
377
|
+
risks: ["Creates a pull request from the executor-produced branch; review the diff before merging."],
|
|
378
|
+
executorConditions: ["isolated branch exists"]
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
intentId: `${proposalId}_link_branch`,
|
|
383
|
+
domain: "artifact_links",
|
|
384
|
+
action: "link_artifact",
|
|
385
|
+
summary: `Link the run branch ${input.branchName} to the work item.`,
|
|
386
|
+
params: { title: "Run branch", uri: input.branchName }
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
intentId: `${proposalId}_request_review`,
|
|
390
|
+
domain: "review",
|
|
391
|
+
action: "request_review",
|
|
392
|
+
summary: "Request human review of the generated code changes.",
|
|
393
|
+
params: { changedFiles: input.changedFiles }
|
|
394
|
+
}
|
|
395
|
+
],
|
|
396
|
+
preconditions: ["The local branch was generated from the checkout state available to the runner."]
|
|
397
|
+
}
|
|
398
|
+
] : void 0;
|
|
399
|
+
return {
|
|
400
|
+
conclusion: "success",
|
|
401
|
+
summary,
|
|
402
|
+
changedFiles: input.changedFiles,
|
|
403
|
+
artifacts: [
|
|
404
|
+
...input.changedFiles.length > 0 ? [{ kind: "patch", title: "Run branch", uri: input.branchName }] : [],
|
|
405
|
+
...input.extraArtifacts ?? []
|
|
406
|
+
],
|
|
407
|
+
...suggestedChanges ? { suggestedChanges } : {},
|
|
408
|
+
nextAction: input.changedFiles.length > 0 ? {
|
|
409
|
+
summary: "Review the proposed pull request action and reply `apply 1` if the branch should become a PR.",
|
|
410
|
+
hint: {
|
|
411
|
+
kind: "create_pull_request",
|
|
412
|
+
targetId: proposalId,
|
|
413
|
+
selectedIntentIds: [`${proposalId}_create_pr`]
|
|
414
|
+
}
|
|
415
|
+
} : "No file changes were detected."
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/claude-code.ts
|
|
83
420
|
function contextLines(context) {
|
|
84
421
|
if (!context.length) return "No additional context pointers were provided.";
|
|
85
|
-
return context.map((pointer) => `- ${pointer
|
|
422
|
+
return context.map((pointer) => `- ${contextPointerLabel2(pointer)}: ${pointer.uri}`).join("\n");
|
|
86
423
|
}
|
|
87
424
|
function buildPrompt(input) {
|
|
88
425
|
return [
|
|
@@ -92,29 +429,35 @@ function buildPrompt(input) {
|
|
|
92
429
|
"User request:",
|
|
93
430
|
input.rawText,
|
|
94
431
|
"",
|
|
432
|
+
...renderContextPacketForPrompt(input.contextPacket),
|
|
433
|
+
...input.contextPacket ? [""] : [],
|
|
95
434
|
"Context pointers:",
|
|
96
435
|
contextLines(input.context),
|
|
97
436
|
"",
|
|
98
|
-
|
|
437
|
+
...executorPolicyPromptLines()
|
|
99
438
|
].join("\n");
|
|
100
439
|
}
|
|
101
|
-
function
|
|
440
|
+
function createClaudeCodeExecutor(options = {}) {
|
|
102
441
|
const runner = options.runner ?? nodeCommandRunner;
|
|
103
|
-
const
|
|
442
|
+
const claudeCommand = options.claudeCommand ?? "claude";
|
|
104
443
|
return {
|
|
105
|
-
id: "
|
|
106
|
-
displayName: "
|
|
444
|
+
id: "claude-code",
|
|
445
|
+
displayName: "Claude Code Executor",
|
|
107
446
|
async canRun(input) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
447
|
+
try {
|
|
448
|
+
const claudeVersion = await runner.run(claudeCommand, ["--version"], { cwd: input.workspacePath });
|
|
449
|
+
if (claudeVersion.exitCode !== 0) {
|
|
450
|
+
return { ready: false, reason: `Claude Code CLI is not available: ${claudeVersion.stderr || claudeVersion.stdout}` };
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
return { ready: false, reason: `Claude Code CLI is not available: ${error instanceof Error ? error.message : String(error)}` };
|
|
111
454
|
}
|
|
112
455
|
const gitStatus = await runner.run("git", ["status", "--porcelain"], { cwd: input.workspacePath });
|
|
113
456
|
if (gitStatus.exitCode !== 0) {
|
|
114
457
|
return { ready: false, reason: `Workspace is not a git checkout: ${gitStatus.stderr || gitStatus.stdout}` };
|
|
115
458
|
}
|
|
116
459
|
if (gitStatus.stdout.trim().length > 0) {
|
|
117
|
-
return { ready: false, reason: "Workspace has uncommitted changes; refusing to run
|
|
460
|
+
return { ready: false, reason: "Workspace has uncommitted changes; refusing to run Claude Code executor." };
|
|
118
461
|
}
|
|
119
462
|
return { ready: true };
|
|
120
463
|
},
|
|
@@ -125,30 +468,38 @@ function createCodexExecutor(options = {}) {
|
|
|
125
468
|
message: `Creating isolated branch ${branchName}`,
|
|
126
469
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
127
470
|
});
|
|
128
|
-
await createRunBranch({
|
|
471
|
+
await createRunBranch({
|
|
472
|
+
runner,
|
|
473
|
+
workspacePath: input.workspacePath,
|
|
474
|
+
branchName,
|
|
475
|
+
...input.baseBranch ? { startPoint: input.baseBranch } : {}
|
|
476
|
+
});
|
|
129
477
|
await sink.emit({
|
|
130
478
|
type: "executor.progress",
|
|
131
|
-
message: "Starting
|
|
479
|
+
message: "Starting claude --print",
|
|
132
480
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
133
481
|
});
|
|
134
482
|
const args = [
|
|
135
|
-
"
|
|
136
|
-
"--
|
|
137
|
-
|
|
138
|
-
"--
|
|
139
|
-
"
|
|
483
|
+
"--print",
|
|
484
|
+
"--input-format",
|
|
485
|
+
"text",
|
|
486
|
+
"--output-format",
|
|
487
|
+
"text",
|
|
488
|
+
"--no-session-persistence",
|
|
140
489
|
...options.model ? ["--model", options.model] : [],
|
|
141
|
-
"-"
|
|
490
|
+
...options.permissionMode ? ["--permission-mode", options.permissionMode] : [],
|
|
491
|
+
...options.dangerouslySkipPermissions ? ["--dangerously-skip-permissions"] : []
|
|
142
492
|
];
|
|
143
|
-
const
|
|
493
|
+
const claudeResult = await runner.run(claudeCommand, args, {
|
|
144
494
|
cwd: input.workspacePath,
|
|
145
495
|
input: buildPrompt({
|
|
146
496
|
runId: input.runId,
|
|
147
497
|
rawText: input.command.rawText,
|
|
148
|
-
context: input.context
|
|
498
|
+
context: input.context,
|
|
499
|
+
contextPacket: input.contextPacket
|
|
149
500
|
})
|
|
150
501
|
});
|
|
151
|
-
await assertCommandSucceeded(
|
|
502
|
+
await assertCommandSucceeded(claudeResult, "claude --print");
|
|
152
503
|
const cleanedArtifacts = await cleanupInternalArtifacts({ runner, workspacePath: input.workspacePath });
|
|
153
504
|
if (cleanedArtifacts.length > 0) {
|
|
154
505
|
await sink.emit({
|
|
@@ -160,24 +511,396 @@ function createCodexExecutor(options = {}) {
|
|
|
160
511
|
const files = await changedFiles({ runner, workspacePath: input.workspacePath });
|
|
161
512
|
await sink.emit({
|
|
162
513
|
type: "executor.completed",
|
|
163
|
-
message: `
|
|
514
|
+
message: `Claude Code executor completed with ${files.length} changed file(s)`,
|
|
164
515
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
165
516
|
});
|
|
166
|
-
const output =
|
|
167
|
-
return {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
517
|
+
const output = claudeResult.stdout.trim() || claudeResult.stderr.trim() || "Claude Code completed without textual output.";
|
|
518
|
+
return createExecutorRunResult({
|
|
519
|
+
executorName: "Claude Code",
|
|
520
|
+
runId: input.runId,
|
|
521
|
+
branchName,
|
|
522
|
+
...input.baseBranch ? { baseBranch: input.baseBranch } : {},
|
|
523
|
+
output,
|
|
524
|
+
changedFiles: files
|
|
525
|
+
});
|
|
526
|
+
},
|
|
527
|
+
async cancel() {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/codex.ts
|
|
534
|
+
import { contextPointerLabel as contextPointerLabel3 } from "@opentag/core";
|
|
535
|
+
|
|
536
|
+
// src/security.ts
|
|
537
|
+
import { fileURLToPath } from "url";
|
|
538
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
539
|
+
var DEFAULT_SAFE_ENV_NAMES = [
|
|
540
|
+
"CI",
|
|
541
|
+
"COLORTERM",
|
|
542
|
+
"CODEX_HOME",
|
|
543
|
+
"FORCE_COLOR",
|
|
544
|
+
"HOME",
|
|
545
|
+
"LANG",
|
|
546
|
+
"LOGNAME",
|
|
547
|
+
"NO_COLOR",
|
|
548
|
+
"PATH",
|
|
549
|
+
"PWD",
|
|
550
|
+
"SHELL",
|
|
551
|
+
"SSL_CERT_DIR",
|
|
552
|
+
"SSL_CERT_FILE",
|
|
553
|
+
"TERM",
|
|
554
|
+
"TMP",
|
|
555
|
+
"TMPDIR",
|
|
556
|
+
"TEMP",
|
|
557
|
+
"USER",
|
|
558
|
+
"XDG_CACHE_HOME",
|
|
559
|
+
"XDG_CONFIG_HOME",
|
|
560
|
+
"XDG_DATA_HOME"
|
|
561
|
+
];
|
|
562
|
+
var SAFE_ENV_PREFIXES = ["LC_"];
|
|
563
|
+
var SENSITIVE_ENV_PATTERNS = [
|
|
564
|
+
/TOKEN/,
|
|
565
|
+
/SECRET/,
|
|
566
|
+
/PASSWORD/,
|
|
567
|
+
/PASS$/,
|
|
568
|
+
/API[_-]?KEY/,
|
|
569
|
+
/CREDENTIAL/,
|
|
570
|
+
/COOKIE/,
|
|
571
|
+
/SESSION/,
|
|
572
|
+
/AUTH/,
|
|
573
|
+
/^AWS_/,
|
|
574
|
+
/^AZURE_/,
|
|
575
|
+
/^GCP_/,
|
|
576
|
+
/^GOOGLE_/,
|
|
577
|
+
/^OPENAI_/,
|
|
578
|
+
/^ANTHROPIC_/,
|
|
579
|
+
/^SLACK_/,
|
|
580
|
+
/^GITHUB_TOKEN$/,
|
|
581
|
+
/^GH_TOKEN$/,
|
|
582
|
+
/^SSH_/
|
|
583
|
+
];
|
|
584
|
+
var HIGH_RISK_TEXT_PATTERNS = [
|
|
585
|
+
{
|
|
586
|
+
code: "prompt.instruction_override",
|
|
587
|
+
pattern: /\b(ignore|disregard|forget)\s+(all\s+)?(previous|prior|system|developer|safety)\s+instructions\b/i,
|
|
588
|
+
message: "Request contains an instruction override pattern commonly used in prompt injection."
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
code: "prompt.secret_exfiltration",
|
|
592
|
+
pattern: /\b(print|dump|show|reveal|exfiltrate|send|upload|post|copy)\b[\s\S]{0,100}\b(secret|token|password|api[\s_-]?key|credential|environment variables?|env vars?)\b/i,
|
|
593
|
+
message: "Request appears to ask the runner to expose secrets or environment variables."
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
code: "prompt.sensitive_file_access",
|
|
597
|
+
pattern: /\b(cat|open|read|print|dump)\b[\s\S]{0,80}(~\/\.ssh|~\/\.aws|\.env\b|id_rsa|known_hosts|credentials\b)/i,
|
|
598
|
+
message: "Request appears to ask the runner to read sensitive local credential files."
|
|
599
|
+
}
|
|
600
|
+
];
|
|
601
|
+
function securityMode(policy) {
|
|
602
|
+
return policy?.mode ?? "enforce";
|
|
603
|
+
}
|
|
604
|
+
function isPathInside(childPath, parentPath) {
|
|
605
|
+
const child = resolve(childPath);
|
|
606
|
+
const parent = resolve(parentPath);
|
|
607
|
+
const pathFromParent = relative(parent, child);
|
|
608
|
+
return pathFromParent === "" || !pathFromParent.startsWith("..") && !isAbsolute(pathFromParent);
|
|
609
|
+
}
|
|
610
|
+
function fileContextPath(pointer, workspacePath) {
|
|
611
|
+
if (pointer.kind !== "file") return null;
|
|
612
|
+
if (pointer.uri.startsWith("file://")) {
|
|
613
|
+
return fileURLToPath(pointer.uri);
|
|
614
|
+
}
|
|
615
|
+
if (isAbsolute(pointer.uri)) {
|
|
616
|
+
return pointer.uri;
|
|
617
|
+
}
|
|
618
|
+
return resolve(workspacePath, pointer.uri);
|
|
619
|
+
}
|
|
620
|
+
function hasPermission(permissions, scope) {
|
|
621
|
+
return permissions?.some((permission) => permission.scope === scope) ?? false;
|
|
622
|
+
}
|
|
623
|
+
function needsWritePermission(command, executorId) {
|
|
624
|
+
if (executorId === "echo") return false;
|
|
625
|
+
return command.intent === "fix" || command.intent === "run";
|
|
626
|
+
}
|
|
627
|
+
function scanTextForHighRiskPatterns(input) {
|
|
628
|
+
const sources = [
|
|
629
|
+
{ label: "command", text: input.command.rawText },
|
|
630
|
+
...input.context.filter((pointer) => pointer.kind === "text").map((pointer) => ({ label: pointer.title ?? "text context", text: pointer.uri }))
|
|
631
|
+
];
|
|
632
|
+
const findings = [];
|
|
633
|
+
for (const source of sources) {
|
|
634
|
+
for (const rule of HIGH_RISK_TEXT_PATTERNS) {
|
|
635
|
+
if (rule.pattern.test(source.text)) {
|
|
636
|
+
findings.push({
|
|
637
|
+
code: rule.code,
|
|
638
|
+
severity: "block",
|
|
639
|
+
message: `${rule.message} Source: ${source.label}.`
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return findings;
|
|
645
|
+
}
|
|
646
|
+
function assessRunnerSecurity(input) {
|
|
647
|
+
const mode = securityMode(input.policy);
|
|
648
|
+
if (mode === "off") {
|
|
649
|
+
return { allowed: true, mode, findings: [] };
|
|
650
|
+
}
|
|
651
|
+
const findings = [];
|
|
652
|
+
if (!isAbsolute(input.workspacePath)) {
|
|
653
|
+
findings.push({
|
|
654
|
+
code: "workspace.relative_path",
|
|
655
|
+
severity: "block",
|
|
656
|
+
message: "Workspace path must be absolute before a local executor can run."
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
const workspacePath = resolve(input.workspacePath);
|
|
660
|
+
const executionPath = resolve(input.executionPath ?? input.workspacePath);
|
|
661
|
+
if (input.policy?.allowedWorkspaceRoot && !isPathInside(workspacePath, input.policy.allowedWorkspaceRoot)) {
|
|
662
|
+
findings.push({
|
|
663
|
+
code: "workspace.outside_allowed_root",
|
|
664
|
+
severity: "block",
|
|
665
|
+
message: "Workspace path is outside the configured allowed workspace root."
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
if (input.policy?.allowedWorkspaceRoot && !isPathInside(executionPath, input.policy.allowedWorkspaceRoot)) {
|
|
669
|
+
findings.push({
|
|
670
|
+
code: "execution.outside_allowed_root",
|
|
671
|
+
severity: "block",
|
|
672
|
+
message: "Execution path is outside the configured allowed workspace root."
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
for (const pointer of input.context) {
|
|
676
|
+
const filePath = fileContextPath(pointer, workspacePath);
|
|
677
|
+
if (filePath && !isPathInside(filePath, workspacePath)) {
|
|
678
|
+
findings.push({
|
|
679
|
+
code: "context.file_outside_workspace",
|
|
680
|
+
severity: "block",
|
|
681
|
+
message: `File context is outside the mapped workspace: ${pointer.uri}`
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (needsWritePermission(input.command, input.executorId) && !hasPermission(input.permissions, "repo:write")) {
|
|
686
|
+
findings.push({
|
|
687
|
+
code: "permission.repo_write_required",
|
|
688
|
+
severity: "block",
|
|
689
|
+
message: "Write-capable commands require an explicit repo:write permission grant."
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
if (!input.policy?.allowUnsafePrompts) {
|
|
693
|
+
findings.push(...scanTextForHighRiskPatterns({ command: input.command, context: input.context }));
|
|
694
|
+
}
|
|
695
|
+
const hasBlockingFinding = findings.some((finding) => finding.severity === "block");
|
|
696
|
+
return {
|
|
697
|
+
allowed: mode === "audit" || !hasBlockingFinding,
|
|
698
|
+
mode,
|
|
699
|
+
findings
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function isSensitiveEnvName(name) {
|
|
703
|
+
const upperName = name.toUpperCase();
|
|
704
|
+
return SENSITIVE_ENV_PATTERNS.some((pattern) => pattern.test(upperName));
|
|
705
|
+
}
|
|
706
|
+
function isSafeEnvName(name, policy) {
|
|
707
|
+
const upperName = name.toUpperCase();
|
|
708
|
+
const safeNames = new Set([...DEFAULT_SAFE_ENV_NAMES, ...policy?.extraSafeEnv ?? []].map((envName) => envName.toUpperCase()));
|
|
709
|
+
return safeNames.has(upperName) || SAFE_ENV_PREFIXES.some((prefix) => upperName.startsWith(prefix));
|
|
710
|
+
}
|
|
711
|
+
function scrubEnvironment(env = process.env, policy) {
|
|
712
|
+
if (securityMode(policy) === "off") return { ...env };
|
|
713
|
+
const scrubbed = {};
|
|
714
|
+
for (const [name, value] of Object.entries(env)) {
|
|
715
|
+
if (typeof value !== "string") continue;
|
|
716
|
+
if (isSensitiveEnvName(name)) continue;
|
|
717
|
+
if (!isSafeEnvName(name, policy)) continue;
|
|
718
|
+
scrubbed[name] = value;
|
|
719
|
+
}
|
|
720
|
+
return scrubbed;
|
|
721
|
+
}
|
|
722
|
+
function formatSecurityAssessment(assessment) {
|
|
723
|
+
if (assessment.findings.length === 0) {
|
|
724
|
+
return `OpenTag runner security assessment passed in ${assessment.mode} mode.`;
|
|
725
|
+
}
|
|
726
|
+
const prefix = assessment.allowed ? `OpenTag runner security assessment reported ${assessment.findings.length} finding(s) in ${assessment.mode} mode.` : "OpenTag runner security blocked this run.";
|
|
727
|
+
const details = assessment.findings.map((finding) => `${finding.code}: ${finding.message}`).join(" ");
|
|
728
|
+
return `${prefix} ${details}`;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/codex.ts
|
|
732
|
+
function contextLines2(context) {
|
|
733
|
+
if (!context.length) return "No additional context pointers were provided.";
|
|
734
|
+
return context.map((pointer) => `- ${contextPointerLabel3(pointer)}: ${pointer.uri}`).join("\n");
|
|
735
|
+
}
|
|
736
|
+
function buildPrompt2(input) {
|
|
737
|
+
return [
|
|
738
|
+
"You are executing an OpenTag run in a local checkout.",
|
|
739
|
+
`Run ID: ${input.runId}`,
|
|
740
|
+
"",
|
|
741
|
+
"User request:",
|
|
742
|
+
input.rawText,
|
|
743
|
+
"",
|
|
744
|
+
...renderContextPacketForPrompt(input.contextPacket),
|
|
745
|
+
...input.contextPacket ? [""] : [],
|
|
746
|
+
"Context pointers:",
|
|
747
|
+
contextLines2(input.context),
|
|
748
|
+
"",
|
|
749
|
+
...executorPolicyPromptLines()
|
|
750
|
+
].join("\n");
|
|
751
|
+
}
|
|
752
|
+
function createCodexExecutor(options = {}) {
|
|
753
|
+
const runner = options.runner ?? nodeCommandRunner;
|
|
754
|
+
const codexCommand = options.codexCommand ?? "codex";
|
|
755
|
+
return {
|
|
756
|
+
id: "codex",
|
|
757
|
+
displayName: "Codex Executor",
|
|
758
|
+
async canRun(input) {
|
|
759
|
+
const codexVersion = await runner.run(codexCommand, ["--version"], { cwd: input.workspacePath });
|
|
760
|
+
if (codexVersion.exitCode !== 0) {
|
|
761
|
+
return { ready: false, reason: `Codex CLI is not available: ${codexVersion.stderr || codexVersion.stdout}` };
|
|
762
|
+
}
|
|
763
|
+
const gitRepo = await runner.run("git", ["rev-parse", "--show-toplevel"], { cwd: input.workspacePath });
|
|
764
|
+
if (gitRepo.exitCode !== 0) {
|
|
765
|
+
return { ready: false, reason: `Workspace is not a git checkout: ${gitRepo.stderr || gitRepo.stdout}` };
|
|
766
|
+
}
|
|
767
|
+
const baseBranch = input.baseBranch ?? "main";
|
|
768
|
+
const baseRef = await runner.run("git", ["rev-parse", "--verify", `${baseBranch}^{commit}`], {
|
|
769
|
+
cwd: input.workspacePath
|
|
770
|
+
});
|
|
771
|
+
if (baseRef.exitCode !== 0) {
|
|
772
|
+
return { ready: false, reason: `Base branch '${baseBranch}' is not available: ${baseRef.stderr || baseRef.stdout}` };
|
|
773
|
+
}
|
|
774
|
+
return { ready: true };
|
|
775
|
+
},
|
|
776
|
+
async run(input, sink) {
|
|
777
|
+
const security = options.security;
|
|
778
|
+
const worktreePath = worktreePathForRun({
|
|
779
|
+
workspacePath: input.workspacePath,
|
|
780
|
+
runId: input.runId,
|
|
781
|
+
...input.worktreeRoot ? { worktreeRoot: input.worktreeRoot } : {}
|
|
782
|
+
});
|
|
783
|
+
const assessment = assessRunnerSecurity({
|
|
784
|
+
executorId: "codex",
|
|
785
|
+
workspacePath: input.workspacePath,
|
|
786
|
+
executionPath: worktreePath,
|
|
787
|
+
command: input.command,
|
|
788
|
+
context: input.context,
|
|
789
|
+
...input.permissions ? { permissions: input.permissions } : {},
|
|
790
|
+
...security ? { policy: security } : {}
|
|
791
|
+
});
|
|
792
|
+
if (assessment.findings.length > 0) {
|
|
793
|
+
await sink.emit({
|
|
794
|
+
type: assessment.allowed ? "executor.progress" : "executor.failed",
|
|
795
|
+
message: formatSecurityAssessment(assessment),
|
|
796
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
if (!assessment.allowed) {
|
|
800
|
+
return {
|
|
801
|
+
conclusion: "needs_human",
|
|
802
|
+
summary: formatSecurityAssessment(assessment),
|
|
803
|
+
nextAction: "Review the request and rerun with a narrower prompt or an explicit local policy override if appropriate."
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const branchName = branchNameForRun(input.runId);
|
|
807
|
+
const baseBranch = input.baseBranch ?? "main";
|
|
808
|
+
const keepWorktree = input.keepWorktree ?? "on_failure";
|
|
809
|
+
let completed = false;
|
|
810
|
+
let changedFileCount;
|
|
811
|
+
await sink.emit({
|
|
812
|
+
type: "executor.started",
|
|
813
|
+
message: `Creating isolated worktree ${worktreePath} on ${branchName}`,
|
|
814
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
815
|
+
});
|
|
816
|
+
try {
|
|
817
|
+
await createRunWorktree({
|
|
818
|
+
runner,
|
|
819
|
+
workspacePath: input.workspacePath,
|
|
820
|
+
worktreePath,
|
|
821
|
+
branchName,
|
|
822
|
+
baseBranch
|
|
823
|
+
});
|
|
824
|
+
await sink.emit({
|
|
825
|
+
type: "executor.progress",
|
|
826
|
+
message: "Starting codex exec",
|
|
827
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
828
|
+
});
|
|
829
|
+
const args = [
|
|
830
|
+
"exec",
|
|
831
|
+
"--cd",
|
|
832
|
+
worktreePath,
|
|
833
|
+
"--full-auto",
|
|
834
|
+
"--ephemeral",
|
|
835
|
+
...options.model ? ["--model", options.model] : [],
|
|
836
|
+
"-"
|
|
837
|
+
];
|
|
838
|
+
const codexResult = await runner.run(codexCommand, args, {
|
|
839
|
+
cwd: worktreePath,
|
|
840
|
+
env: scrubEnvironment(void 0, security),
|
|
841
|
+
input: buildPrompt2({
|
|
842
|
+
runId: input.runId,
|
|
843
|
+
rawText: input.command.rawText,
|
|
844
|
+
context: input.context,
|
|
845
|
+
contextPacket: input.contextPacket
|
|
846
|
+
})
|
|
847
|
+
});
|
|
848
|
+
await assertCommandSucceeded(codexResult, "codex exec");
|
|
849
|
+
const cleanedArtifacts = await cleanupInternalArtifacts({ runner, workspacePath: worktreePath });
|
|
850
|
+
if (cleanedArtifacts.length > 0) {
|
|
851
|
+
await sink.emit({
|
|
852
|
+
type: "executor.progress",
|
|
853
|
+
message: `Cleaned internal artifacts: ${cleanedArtifacts.join(", ")}`,
|
|
854
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
const files = await changedFiles({ runner, workspacePath: worktreePath });
|
|
858
|
+
changedFileCount = files.length;
|
|
859
|
+
if (files.length > 0) {
|
|
860
|
+
await sink.emit({
|
|
861
|
+
type: "executor.progress",
|
|
862
|
+
message: `Committing ${files.length} changed file(s) to ${branchName}`,
|
|
863
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
864
|
+
});
|
|
865
|
+
await commitRunChanges({
|
|
866
|
+
runner,
|
|
867
|
+
workspacePath: worktreePath,
|
|
868
|
+
message: `OpenTag run ${input.runId}`
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
completed = true;
|
|
872
|
+
await sink.emit({
|
|
873
|
+
type: "executor.completed",
|
|
874
|
+
message: `Codex executor completed with ${files.length} changed file(s)`,
|
|
875
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
876
|
+
});
|
|
877
|
+
const output = codexResult.stdout.trim() || codexResult.stderr.trim() || "Codex completed without textual output.";
|
|
878
|
+
return createExecutorRunResult({
|
|
879
|
+
executorName: "Codex",
|
|
880
|
+
runId: input.runId,
|
|
881
|
+
branchName,
|
|
882
|
+
...input.baseBranch ? { baseBranch: input.baseBranch } : {},
|
|
883
|
+
output,
|
|
884
|
+
changedFiles: files,
|
|
885
|
+
extraArtifacts: keepWorktree === "always" ? [{ title: "Run worktree", uri: worktreePath }] : []
|
|
886
|
+
});
|
|
887
|
+
} finally {
|
|
888
|
+
const shouldRemove = keepWorktree === "never" || keepWorktree === "on_failure" && completed;
|
|
889
|
+
if (shouldRemove) {
|
|
890
|
+
try {
|
|
891
|
+
await removeRunWorktree({ runner, workspacePath: input.workspacePath, worktreePath });
|
|
892
|
+
if (completed && changedFileCount === 0) {
|
|
893
|
+
await deleteRunBranch({ runner, workspacePath: input.workspacePath, branchName });
|
|
894
|
+
}
|
|
895
|
+
} catch (error) {
|
|
896
|
+
await sink.emit({
|
|
897
|
+
type: "executor.progress",
|
|
898
|
+
message: `Could not clean up run worktree or branch for ${worktreePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
899
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
900
|
+
});
|
|
177
901
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
};
|
|
902
|
+
}
|
|
903
|
+
}
|
|
181
904
|
},
|
|
182
905
|
async cancel() {
|
|
183
906
|
return;
|
|
@@ -216,7 +939,11 @@ function createEchoExecutor() {
|
|
|
216
939
|
outcome: "passed",
|
|
217
940
|
excerpt: input.command.rawText
|
|
218
941
|
}
|
|
219
|
-
]
|
|
942
|
+
],
|
|
943
|
+
nextAction: {
|
|
944
|
+
summary: "No external state change is suggested for the echo executor result.",
|
|
945
|
+
hint: { kind: "none" }
|
|
946
|
+
}
|
|
220
947
|
};
|
|
221
948
|
},
|
|
222
949
|
async cancel() {
|
|
@@ -225,17 +952,31 @@ function createEchoExecutor() {
|
|
|
225
952
|
};
|
|
226
953
|
}
|
|
227
954
|
export {
|
|
955
|
+
DEFAULT_SAFE_ENV_NAMES,
|
|
956
|
+
STATUS_PORCELAIN_Z_ARGS,
|
|
228
957
|
assertCommandSucceeded,
|
|
958
|
+
assessRunnerSecurity,
|
|
229
959
|
branchNameForRun,
|
|
230
960
|
changedFiles,
|
|
231
961
|
cleanupInternalArtifacts,
|
|
962
|
+
commitChangedFiles,
|
|
963
|
+
commitRunChanges,
|
|
964
|
+
createClaudeCodeExecutor,
|
|
232
965
|
createCodexExecutor,
|
|
233
966
|
createEchoExecutor,
|
|
967
|
+
createExecutorRunResult,
|
|
234
968
|
createRunBranch,
|
|
969
|
+
createRunWorktree,
|
|
970
|
+
deleteRunBranch,
|
|
971
|
+
formatSecurityAssessment,
|
|
235
972
|
isInternalArtifactPath,
|
|
236
973
|
nodeCommandRunner,
|
|
237
974
|
parseChangedFiles,
|
|
238
975
|
parseStatusEntries,
|
|
239
|
-
pushBranch
|
|
976
|
+
pushBranch,
|
|
977
|
+
removeRunWorktree,
|
|
978
|
+
renderContextPacketForPrompt,
|
|
979
|
+
scrubEnvironment,
|
|
980
|
+
worktreePathForRun
|
|
240
981
|
};
|
|
241
982
|
//# sourceMappingURL=index.js.map
|