@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/README.md +27 -0
- package/dist/claude-code.d.ts.map +1 -1
- package/dist/codex.d.ts.map +1 -1
- 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 +44 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/git.d.ts +1 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/hermes.d.ts +10 -0
- package/dist/hermes.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +540 -10
- package/dist/index.js.map +1 -1
- package/dist/result.d.ts.map +1 -1
- package/dist/session-profile.d.ts +37 -0
- package/dist/session-profile.d.ts.map +1 -0
- package/package.json +2 -2
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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",
|
|
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",
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|