@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/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((resolve, reject) => {
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
- resolve({
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
- return statusOutput.split("\n").map((line) => line.replace(/\r$/, "")).filter(Boolean).map((line) => ({
43
- status: line.slice(0, 2),
44
- path: line.slice(3).trim()
45
- })).filter((entry) => entry.path.length > 0);
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", ["status", "--porcelain"], { cwd: input.workspacePath });
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", ["status", "--porcelain"], { cwd: input.workspacePath });
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/codex.ts
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.kind}: ${pointer.uri}`).join("\n");
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
- "Work autonomously but keep the change narrow. Run relevant verification if you modify files. End with a concise summary."
437
+ ...executorPolicyPromptLines()
99
438
  ].join("\n");
100
439
  }
101
- function createCodexExecutor(options = {}) {
440
+ function createClaudeCodeExecutor(options = {}) {
102
441
  const runner = options.runner ?? nodeCommandRunner;
103
- const codexCommand = options.codexCommand ?? "codex";
442
+ const claudeCommand = options.claudeCommand ?? "claude";
104
443
  return {
105
- id: "codex",
106
- displayName: "Codex Executor",
444
+ id: "claude-code",
445
+ displayName: "Claude Code Executor",
107
446
  async canRun(input) {
108
- const codexVersion = await runner.run(codexCommand, ["--version"], { cwd: input.workspacePath });
109
- if (codexVersion.exitCode !== 0) {
110
- return { ready: false, reason: `Codex CLI is not available: ${codexVersion.stderr || codexVersion.stdout}` };
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 Codex executor." };
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({ runner, workspacePath: input.workspacePath, branchName });
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 codex exec",
479
+ message: "Starting claude --print",
132
480
  at: (/* @__PURE__ */ new Date()).toISOString()
133
481
  });
134
482
  const args = [
135
- "exec",
136
- "--cd",
137
- input.workspacePath,
138
- "--full-auto",
139
- "--ephemeral",
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 codexResult = await runner.run(codexCommand, args, {
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(codexResult, "codex exec");
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: `Codex executor completed with ${files.length} changed file(s)`,
514
+ message: `Claude Code executor completed with ${files.length} changed file(s)`,
164
515
  at: (/* @__PURE__ */ new Date()).toISOString()
165
516
  });
166
- const output = codexResult.stdout.trim() || codexResult.stderr.trim() || "Codex completed without textual output.";
167
- return {
168
- conclusion: "success",
169
- summary: output.slice(-4e3),
170
- changedFiles: files,
171
- artifacts: [{ title: "Run branch", uri: branchName }],
172
- verification: [
173
- {
174
- command: "codex exec",
175
- outcome: "passed",
176
- excerpt: output.slice(-1e3)
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
- nextAction: files.length > 0 ? "Review the local branch and create a pull request." : "No file changes were detected."
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