@opentag/runner 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,6 +23,8 @@ var nodeCommandRunner = {
23
23
  stderr: Buffer.concat(stderr).toString("utf8")
24
24
  });
25
25
  });
26
+ child.stdin.on("error", () => {
27
+ });
26
28
  if (options.input) {
27
29
  child.stdin.write(options.input);
28
30
  }
@@ -36,6 +38,143 @@ async function assertCommandSucceeded(result, label) {
36
38
  }
37
39
  }
38
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
+
39
178
  // src/executor.ts
40
179
  import { contextPointerLabel } from "@opentag/core";
41
180
  function renderContextPacketForPrompt(packet) {
@@ -75,11 +214,23 @@ function branchNameForRun(runId) {
75
214
  const safeRunId = runId.replace(/[^a-zA-Z0-9._-]/g, "-");
76
215
  return `opentag/${safeRunId}`;
77
216
  }
217
+ var STATUS_PORCELAIN_Z_ARGS = ["-c", "core.quotePath=false", "status", "--porcelain", "-z"];
78
218
  function parseStatusEntries(statusOutput) {
79
- return statusOutput.split("\n").map((line) => line.replace(/\r$/, "")).filter(Boolean).map((line) => ({
80
- status: line.slice(0, 2),
81
- path: line.slice(3).trim()
82
- })).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;
83
234
  }
84
235
  function isInternalArtifactPath(path) {
85
236
  return INTERNAL_ARTIFACT_ROOTS.some((root) => path === root || path.startsWith(`${root}/`));
@@ -118,12 +269,12 @@ async function deleteRunBranch(input) {
118
269
  await assertCommandSucceeded(result, "delete empty run branch");
119
270
  }
120
271
  async function changedFiles(input) {
121
- 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 });
122
273
  await assertCommandSucceeded(result, "read changed files");
123
274
  return parseChangedFiles(result.stdout);
124
275
  }
125
276
  async function cleanupInternalArtifacts(input) {
126
- 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 });
127
278
  await assertCommandSucceeded(statusResult, "scan internal artifacts");
128
279
  const untrackedRoots = Array.from(
129
280
  new Set(
@@ -163,9 +314,44 @@ async function pushBranch(input) {
163
314
  }
164
315
 
165
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
+ }
166
351
  function createExecutorRunResult(input) {
167
352
  const proposalId = `proposal_${input.runId}`;
168
- const summary = input.output.slice(-4e3);
353
+ const report = parseExecutorReport(input.output);
354
+ const summary = report ? renderExecutorReportSummary({ ...input, report }) : cleanOrFallbackExecutorSummary(input);
169
355
  const suggestedChanges = input.changedFiles.length > 0 ? [
170
356
  {
171
357
  proposalId,
@@ -248,8 +434,7 @@ function buildPrompt(input) {
248
434
  "Context pointers:",
249
435
  contextLines(input.context),
250
436
  "",
251
- "Work autonomously but keep the change narrow. Run relevant verification if you modify files.",
252
- "End with a concise summary of what changed, what was verified, and the recommended next action."
437
+ ...executorPolicyPromptLines()
253
438
  ].join("\n");
254
439
  }
255
440
  function createClaudeCodeExecutor(options = {}) {
@@ -258,6 +443,40 @@ function createClaudeCodeExecutor(options = {}) {
258
443
  return {
259
444
  id: "claude-code",
260
445
  displayName: "Claude Code Executor",
446
+ capability: {
447
+ id: "claude-code",
448
+ invocation: "spawn",
449
+ supportsProfile: false,
450
+ supportsStreaming: false,
451
+ supportsCancel: false,
452
+ supportsHookCompletion: false,
453
+ progressEvents: "audit",
454
+ approvalMode: "opentag_policy",
455
+ contextAccess: ["context_packet", "context_pointers", "workspace"],
456
+ promptAssembly: "executor_adapter",
457
+ writeAccess: "workspace",
458
+ conversationAccess: "request",
459
+ promptMutation: "none",
460
+ rawContextAccess: false,
461
+ writeActionAccess: "none",
462
+ workspaceIsolation: "branch",
463
+ requiredSecrets: [
464
+ {
465
+ id: "anthropic_api_key",
466
+ label: "Anthropic API key",
467
+ required: false,
468
+ env: "ANTHROPIC_API_KEY",
469
+ description: "Needed when the local Claude Code CLI is configured to authenticate from environment."
470
+ }
471
+ ],
472
+ completionSignals: [
473
+ {
474
+ type: "process_exit",
475
+ required: true,
476
+ description: "OpenTag treats a successful `claude --print` process exit as the normal completion signal."
477
+ }
478
+ ]
479
+ },
261
480
  async canRun(input) {
262
481
  try {
263
482
  const claudeVersion = await runner.run(claudeCommand, ["--version"], { cwd: input.workspacePath });
@@ -561,7 +780,7 @@ function buildPrompt2(input) {
561
780
  "Context pointers:",
562
781
  contextLines2(input.context),
563
782
  "",
564
- "Work autonomously but keep the change narrow. Run relevant verification if you modify files. End with a concise summary."
783
+ ...executorPolicyPromptLines()
565
784
  ].join("\n");
566
785
  }
567
786
  function createCodexExecutor(options = {}) {
@@ -570,6 +789,40 @@ function createCodexExecutor(options = {}) {
570
789
  return {
571
790
  id: "codex",
572
791
  displayName: "Codex Executor",
792
+ capability: {
793
+ id: "codex",
794
+ invocation: "spawn",
795
+ supportsProfile: false,
796
+ supportsStreaming: false,
797
+ supportsCancel: false,
798
+ supportsHookCompletion: false,
799
+ progressEvents: "audit",
800
+ approvalMode: "opentag_policy",
801
+ contextAccess: ["context_packet", "context_pointers", "workspace"],
802
+ promptAssembly: "executor_adapter",
803
+ writeAccess: "workspace",
804
+ conversationAccess: "request",
805
+ promptMutation: "none",
806
+ rawContextAccess: false,
807
+ writeActionAccess: "none",
808
+ workspaceIsolation: "worktree",
809
+ requiredSecrets: [
810
+ {
811
+ id: "openai_api_key",
812
+ label: "OpenAI API key",
813
+ required: false,
814
+ env: "OPENAI_API_KEY",
815
+ description: "Needed when the local Codex CLI is configured to authenticate from environment."
816
+ }
817
+ ],
818
+ completionSignals: [
819
+ {
820
+ type: "process_exit",
821
+ required: true,
822
+ description: "OpenTag treats a successful `codex exec` process exit as the normal completion signal."
823
+ }
824
+ ]
825
+ },
573
826
  async canRun(input) {
574
827
  const codexVersion = await runner.run(codexCommand, ["--version"], { cwd: input.workspacePath });
575
828
  if (codexVersion.exitCode !== 0) {
@@ -731,6 +984,32 @@ function createEchoExecutor() {
731
984
  return {
732
985
  id: "echo",
733
986
  displayName: "Echo Executor",
987
+ capability: {
988
+ id: "echo",
989
+ invocation: "spawn",
990
+ supportsProfile: false,
991
+ supportsStreaming: false,
992
+ supportsCancel: false,
993
+ supportsHookCompletion: false,
994
+ progressEvents: "audit",
995
+ approvalMode: "opentag_policy",
996
+ contextAccess: ["context_packet", "context_pointers"],
997
+ promptAssembly: "opentag",
998
+ writeAccess: "none",
999
+ conversationAccess: "request",
1000
+ promptMutation: "none",
1001
+ rawContextAccess: false,
1002
+ writeActionAccess: "none",
1003
+ workspaceIsolation: "none",
1004
+ requiredSecrets: [],
1005
+ completionSignals: [
1006
+ {
1007
+ type: "process_exit",
1008
+ required: true,
1009
+ description: "The in-process echo executor returns a structured result immediately."
1010
+ }
1011
+ ]
1012
+ },
734
1013
  async canRun() {
735
1014
  return { ready: true };
736
1015
  },
@@ -766,8 +1045,254 @@ function createEchoExecutor() {
766
1045
  }
767
1046
  };
768
1047
  }
1048
+
1049
+ // src/session-profile.ts
1050
+ import { formatProjectTargetRef, projectTargetRefFromEvent } from "@opentag/core";
1051
+ var DEFAULT_AGENT_SESSION_PROFILE_TEMPLATE = "opentag-{provider}-{accountId}-{conversationId}-{owner}-{repo}-{actorId}";
1052
+ function metadataString(metadata, key) {
1053
+ const value = metadata?.[key];
1054
+ if (typeof value === "string") return value;
1055
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1056
+ return "";
1057
+ }
1058
+ function sanitizeAgentSessionProfileId(profile) {
1059
+ return profile.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1060
+ }
1061
+ function profileTemplateValue(input) {
1062
+ if (input.key === "runId") return input.runId;
1063
+ if (input.key === "provider" || input.key === "sourceProvider") return input.sourceProvider;
1064
+ if (input.key === "projectTarget") return input.projectTargetRef ? formatProjectTargetRef(input.projectTargetRef) : "";
1065
+ if (input.key === "repoProvider") return input.projectTargetRef?.provider ?? metadataString(input.metadata, "repoProvider");
1066
+ if (input.key === "owner") return input.projectTargetRef?.owner ?? metadataString(input.metadata, "owner");
1067
+ if (input.key === "repo") return input.projectTargetRef?.repo ?? metadataString(input.metadata, "repo");
1068
+ if (input.key === "actorId") return input.actorId ?? "";
1069
+ return metadataString(input.metadata, input.key);
1070
+ }
1071
+ function createAgentSessionProfile(input) {
1072
+ const template = input.template ?? DEFAULT_AGENT_SESSION_PROFILE_TEMPLATE;
1073
+ const metadata = input.metadata ?? {};
1074
+ const projectTargetRef = input.projectTargetRef ?? void 0;
1075
+ const profile = template.replace(
1076
+ /\{([^}]+)\}/g,
1077
+ (_match, key) => profileTemplateValue({
1078
+ key,
1079
+ runId: input.runId,
1080
+ sourceProvider: input.sourceProvider,
1081
+ metadata,
1082
+ ...projectTargetRef ? { projectTargetRef } : {},
1083
+ ...input.actorId ? { actorId: input.actorId } : {}
1084
+ })
1085
+ );
1086
+ const id = sanitizeAgentSessionProfileId(profile) || (input.fallbackId ? sanitizeAgentSessionProfileId(input.fallbackId) : "");
1087
+ if (!id) return void 0;
1088
+ const accountId = metadataString(metadata, "accountId");
1089
+ const conversationId = metadataString(metadata, "conversationId");
1090
+ return {
1091
+ id,
1092
+ template,
1093
+ sourceProvider: input.sourceProvider,
1094
+ ...projectTargetRef ? { projectTarget: formatProjectTargetRef(projectTargetRef) } : {},
1095
+ ...accountId ? { accountId } : {},
1096
+ ...conversationId ? { conversationId } : {},
1097
+ ...input.actorId ? { actorId: input.actorId } : {}
1098
+ };
1099
+ }
1100
+ function createAgentSessionProfileForEvent(input) {
1101
+ return createAgentSessionProfile({
1102
+ runId: input.runId,
1103
+ sourceProvider: input.event.source,
1104
+ metadata: input.metadata ?? input.event.metadata,
1105
+ projectTargetRef: projectTargetRefFromEvent(input.event),
1106
+ actorId: input.event.actor.providerUserId,
1107
+ ...input.template ? { template: input.template } : {},
1108
+ ...input.fallbackId ? { fallbackId: input.fallbackId } : {}
1109
+ });
1110
+ }
1111
+ function resolveAgentSessionProfile(input) {
1112
+ if (input.profile) {
1113
+ const id = sanitizeAgentSessionProfileId(input.profile);
1114
+ const metadata = input.metadata ?? {};
1115
+ const accountId = metadataString(metadata, "accountId") || input.fallback?.accountId;
1116
+ const conversationId = metadataString(metadata, "conversationId") || input.fallback?.conversationId;
1117
+ const actorId = input.actorId ?? input.fallback?.actorId;
1118
+ const projectTarget = input.projectTargetRef ? formatProjectTargetRef(input.projectTargetRef) : input.fallback?.projectTarget;
1119
+ return id ? {
1120
+ id,
1121
+ template: input.profile,
1122
+ sourceProvider: input.fallback?.sourceProvider || metadataString(input.metadata, "provider") || "unknown",
1123
+ ...projectTarget ? { projectTarget } : {},
1124
+ ...accountId ? { accountId } : {},
1125
+ ...conversationId ? { conversationId } : {},
1126
+ ...actorId ? { actorId } : {}
1127
+ } : input.fallback;
1128
+ }
1129
+ if (input.profileTemplate) {
1130
+ return createAgentSessionProfile({
1131
+ runId: metadataString(input.metadata, "runId") || input.fallback?.id || "run",
1132
+ sourceProvider: metadataString(input.metadata, "provider") || input.fallback?.sourceProvider || "unknown",
1133
+ ...input.metadata ? { metadata: input.metadata } : {},
1134
+ ...input.projectTargetRef ? { projectTargetRef: input.projectTargetRef } : {},
1135
+ ...input.actorId ? { actorId: input.actorId } : {},
1136
+ template: input.profileTemplate,
1137
+ ...input.fallback?.id ? { fallbackId: input.fallback.id } : {}
1138
+ });
1139
+ }
1140
+ return input.fallback;
1141
+ }
1142
+
1143
+ // src/hermes.ts
1144
+ import { contextPointerLabel as contextPointerLabel4 } from "@opentag/core";
1145
+ function contextLines3(context) {
1146
+ if (!context.length) return "No additional context pointers were provided.";
1147
+ return context.map((pointer) => `- ${contextPointerLabel4(pointer)}: ${pointer.uri}`).join("\n");
1148
+ }
1149
+ function buildPrompt3(input) {
1150
+ return [
1151
+ "You are executing an OpenTag run in a local checkout.",
1152
+ `Run ID: ${input.runId}`,
1153
+ "",
1154
+ "User request:",
1155
+ input.rawText,
1156
+ "",
1157
+ ...renderContextPacketForPrompt(input.contextPacket),
1158
+ ...input.contextPacket ? [""] : [],
1159
+ "Context pointers:",
1160
+ contextLines3(input.context),
1161
+ "",
1162
+ "Use only the selected Hermes profile for tools, skills, memory, and session behavior.",
1163
+ "Work autonomously but keep the change narrow. Run relevant verification if you modify files.",
1164
+ "End with a concise summary of what changed, what was verified, and the recommended next action."
1165
+ ].join("\n");
1166
+ }
1167
+ function createHermesExecutor(options = {}) {
1168
+ const runner = options.runner ?? nodeCommandRunner;
1169
+ const hermesCommand = options.hermesCommand ?? "hermes";
1170
+ return {
1171
+ id: "hermes",
1172
+ displayName: "Hermes Executor",
1173
+ capability: {
1174
+ id: "hermes",
1175
+ invocation: "spawn",
1176
+ supportsProfile: true,
1177
+ supportsStreaming: false,
1178
+ supportsCancel: false,
1179
+ supportsHookCompletion: false,
1180
+ progressEvents: "audit",
1181
+ approvalMode: "opentag_policy",
1182
+ contextAccess: ["context_packet", "context_pointers", "workspace"],
1183
+ promptAssembly: "executor_adapter",
1184
+ writeAccess: "workspace",
1185
+ conversationAccess: "request",
1186
+ promptMutation: "none",
1187
+ rawContextAccess: false,
1188
+ writeActionAccess: "none",
1189
+ workspaceIsolation: "branch",
1190
+ requiredSecrets: [],
1191
+ completionSignals: [
1192
+ {
1193
+ type: "process_exit",
1194
+ required: true,
1195
+ description: "OpenTag treats a successful `hermes -z` process exit as the normal completion signal."
1196
+ }
1197
+ ]
1198
+ },
1199
+ async canRun(input) {
1200
+ try {
1201
+ const hermesVersion = await runner.run(hermesCommand, ["--version"], { cwd: input.workspacePath });
1202
+ if (hermesVersion.exitCode !== 0) {
1203
+ return { ready: false, reason: `Hermes CLI is not available: ${hermesVersion.stderr || hermesVersion.stdout}` };
1204
+ }
1205
+ } catch (error) {
1206
+ return { ready: false, reason: `Hermes CLI is not available: ${error instanceof Error ? error.message : String(error)}` };
1207
+ }
1208
+ let gitStatus;
1209
+ try {
1210
+ gitStatus = await runner.run("git", ["status", "--porcelain"], { cwd: input.workspacePath });
1211
+ } catch (error) {
1212
+ return { ready: false, reason: `Workspace is not a git checkout: ${error instanceof Error ? error.message : String(error)}` };
1213
+ }
1214
+ if (gitStatus.exitCode !== 0) {
1215
+ return { ready: false, reason: `Workspace is not a git checkout: ${gitStatus.stderr || gitStatus.stdout}` };
1216
+ }
1217
+ if (gitStatus.stdout.trim().length > 0) {
1218
+ return { ready: false, reason: "Workspace has uncommitted changes; refusing to run Hermes executor." };
1219
+ }
1220
+ return { ready: true };
1221
+ },
1222
+ async run(input, sink) {
1223
+ const branchName = branchNameForRun(input.runId);
1224
+ await sink.emit({
1225
+ type: "executor.started",
1226
+ message: `Creating isolated branch ${branchName}`,
1227
+ at: (/* @__PURE__ */ new Date()).toISOString()
1228
+ });
1229
+ await createRunBranch({
1230
+ runner,
1231
+ workspacePath: input.workspacePath,
1232
+ branchName,
1233
+ ...input.baseBranch ? { startPoint: input.baseBranch } : {}
1234
+ });
1235
+ await sink.emit({
1236
+ type: "executor.progress",
1237
+ message: "Starting hermes -z",
1238
+ at: (/* @__PURE__ */ new Date()).toISOString()
1239
+ });
1240
+ const prompt = buildPrompt3({
1241
+ runId: input.runId,
1242
+ rawText: input.command.rawText,
1243
+ context: input.context,
1244
+ contextPacket: input.contextPacket
1245
+ });
1246
+ const profile = resolveAgentSessionProfile({
1247
+ ...options.profile ? { profile: options.profile } : {},
1248
+ ...options.profileTemplate ? { profileTemplate: options.profileTemplate } : {},
1249
+ metadata: {
1250
+ ...input.metadata ?? {},
1251
+ runId: input.runId
1252
+ },
1253
+ ...input.sessionProfile ? { fallback: input.sessionProfile } : {}
1254
+ });
1255
+ const args = [...profile ? ["-p", profile.id] : [], "-z", prompt];
1256
+ let hermesResult;
1257
+ try {
1258
+ hermesResult = await runner.run(hermesCommand, args, { cwd: input.workspacePath });
1259
+ await assertCommandSucceeded(hermesResult, "hermes -z");
1260
+ } finally {
1261
+ const cleanedArtifacts = await cleanupInternalArtifacts({ runner, workspacePath: input.workspacePath });
1262
+ if (cleanedArtifacts.length > 0) {
1263
+ await sink.emit({
1264
+ type: "executor.progress",
1265
+ message: `Cleaned internal artifacts: ${cleanedArtifacts.join(", ")}`,
1266
+ at: (/* @__PURE__ */ new Date()).toISOString()
1267
+ });
1268
+ }
1269
+ }
1270
+ if (!hermesResult) throw new Error("Hermes did not return a result.");
1271
+ const files = await changedFiles({ runner, workspacePath: input.workspacePath });
1272
+ await sink.emit({
1273
+ type: "executor.completed",
1274
+ message: `Hermes executor completed with ${files.length} changed file(s)`,
1275
+ at: (/* @__PURE__ */ new Date()).toISOString()
1276
+ });
1277
+ const output = hermesResult.stdout.trim() || hermesResult.stderr.trim() || "Hermes completed without textual output.";
1278
+ return createExecutorRunResult({
1279
+ executorName: "Hermes",
1280
+ runId: input.runId,
1281
+ branchName,
1282
+ ...input.baseBranch ? { baseBranch: input.baseBranch } : {},
1283
+ output,
1284
+ changedFiles: files
1285
+ });
1286
+ },
1287
+ async cancel() {
1288
+ return;
1289
+ }
1290
+ };
1291
+ }
769
1292
  export {
1293
+ DEFAULT_AGENT_SESSION_PROFILE_TEMPLATE,
770
1294
  DEFAULT_SAFE_ENV_NAMES,
1295
+ STATUS_PORCELAIN_Z_ARGS,
771
1296
  assertCommandSucceeded,
772
1297
  assessRunnerSecurity,
773
1298
  branchNameForRun,
@@ -775,10 +1300,13 @@ export {
775
1300
  cleanupInternalArtifacts,
776
1301
  commitChangedFiles,
777
1302
  commitRunChanges,
1303
+ createAgentSessionProfile,
1304
+ createAgentSessionProfileForEvent,
778
1305
  createClaudeCodeExecutor,
779
1306
  createCodexExecutor,
780
1307
  createEchoExecutor,
781
1308
  createExecutorRunResult,
1309
+ createHermesExecutor,
782
1310
  createRunBranch,
783
1311
  createRunWorktree,
784
1312
  deleteRunBranch,
@@ -790,6 +1318,8 @@ export {
790
1318
  pushBranch,
791
1319
  removeRunWorktree,
792
1320
  renderContextPacketForPrompt,
1321
+ resolveAgentSessionProfile,
1322
+ sanitizeAgentSessionProfileId,
793
1323
  scrubEnvironment,
794
1324
  worktreePathForRun
795
1325
  };