@wix/pathgrade 1.0.6 → 1.0.8
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 -6
- package/dist/agents/opencode-contract.d.ts +24 -0
- package/dist/agents/opencode-contract.js +84 -0
- package/dist/agents/opencode.d.ts +27 -0
- package/dist/agents/opencode.js +508 -0
- package/dist/agents/registry.js +2 -0
- package/dist/commands/clean.d.ts +8 -0
- package/dist/commands/clean.js +94 -0
- package/dist/commands/run-changed.js +3 -5
- package/dist/commands/runner-env.d.ts +2 -0
- package/dist/commands/runner-env.js +10 -0
- package/dist/pathgrade.js +13 -5
- package/dist/providers/credentials.js +12 -0
- package/dist/providers/debug-runs.d.ts +23 -0
- package/dist/providers/debug-runs.js +208 -0
- package/dist/sdk/agent-resolution.js +6 -1
- package/dist/sdk/agent.js +58 -19
- package/dist/sdk/converse.js +3 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/managed-session.d.ts +2 -0
- package/dist/sdk/managed-session.js +2 -0
- package/dist/sdk/runtime-policy.js +1 -0
- package/dist/sdk/snapshots.d.ts +2 -2
- package/dist/sdk/snapshots.js +13 -4
- package/dist/sdk/types.d.ts +11 -3
- package/dist/sdk/types.js +1 -0
- package/dist/tool-events.d.ts +1 -1
- package/dist/types.d.ts +4 -0
- package/package.json +3 -2
package/dist/pathgrade.js
CHANGED
|
@@ -18,11 +18,13 @@ import { runPreviewReactions } from './commands/preview-reactions.js';
|
|
|
18
18
|
import { runReport } from './commands/report.js';
|
|
19
19
|
import { runAffected } from './commands/affected.js';
|
|
20
20
|
import { runChanged } from './commands/run-changed.js';
|
|
21
|
+
import { parseCleanArgs, runClean } from './commands/clean.js';
|
|
21
22
|
import { clearSidecar } from './affected/sidecar.js';
|
|
22
23
|
import { resolvePathgradeConfig } from './config/pathgrade.js';
|
|
23
24
|
import { loadRunnerInvocationAdapter } from './runners/adapter-loader.js';
|
|
24
25
|
import { fmt } from './utils/cli.js';
|
|
25
26
|
import { shutdown } from './utils/shutdown.js';
|
|
27
|
+
import { buildRunnerEnv } from './commands/runner-env.js';
|
|
26
28
|
function loadDotenv() {
|
|
27
29
|
const envPath = path.resolve(process.cwd(), '.env');
|
|
28
30
|
if (!fs.existsSync(envPath))
|
|
@@ -100,6 +102,13 @@ async function main() {
|
|
|
100
102
|
await runInit(process.cwd(), { force: hasForce });
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
105
|
+
if (command === 'clean') {
|
|
106
|
+
const result = await runClean(process.cwd(), parseCleanArgs(args.slice(1)));
|
|
107
|
+
const action = result.dryRun ? 'would remove' : 'removed';
|
|
108
|
+
console.log(`pathgrade: ${action} ${result.removed} debug run(s); ` +
|
|
109
|
+
`retained ${result.retained}; active ${result.active}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
103
112
|
if (command === 'preview') {
|
|
104
113
|
const previewArgs = args.slice(1);
|
|
105
114
|
const mode = previewArgs.includes('browser') ? 'browser' : 'cli';
|
|
@@ -164,11 +173,7 @@ async function main() {
|
|
|
164
173
|
// previous `--changed` run so it doesn't leak into this full-suite
|
|
165
174
|
// run (the reporter would otherwise merge old metadata).
|
|
166
175
|
await clearSidecar(process.cwd());
|
|
167
|
-
const env =
|
|
168
|
-
...process.env,
|
|
169
|
-
...(parsed.forceDiagnostics ? { PATHGRADE_DIAGNOSTICS: '1' } : {}),
|
|
170
|
-
...(parsed.forceVerbose ? { PATHGRADE_VERBOSE: '1' } : {}),
|
|
171
|
-
};
|
|
176
|
+
const env = buildRunnerEnv(parsed);
|
|
172
177
|
try {
|
|
173
178
|
const config = await resolvePathgradeConfig({ cwd: process.cwd() });
|
|
174
179
|
const runner = await loadRunnerInvocationAdapter({
|
|
@@ -209,6 +214,9 @@ function printHelp() {
|
|
|
209
214
|
pathgrade analyze [--skill=X] Analyze skills and output JSON
|
|
210
215
|
pathgrade validate <file> Validate an .eval.ts file
|
|
211
216
|
pathgrade validate --affected Strict: every eval must be anchored or have valid __pathgradeMeta
|
|
217
|
+
pathgrade clean --debug Remove completed debug runs
|
|
218
|
+
[--keep=N] Keep the N newest completed debug runs
|
|
219
|
+
[--dry-run] Report removals without changing files
|
|
212
220
|
pathgrade preview [browser] View results (CLI default, or browser)
|
|
213
221
|
[--last=N] Show only the N most recent reports
|
|
214
222
|
[--filter=X] Filter reports by test name (substring)
|
|
@@ -60,10 +60,22 @@ export async function resolveCredentials(agent, userEnv, ports) {
|
|
|
60
60
|
return resolveCodex(userEnv, p);
|
|
61
61
|
case 'cursor':
|
|
62
62
|
return resolveCursor(userEnv, p);
|
|
63
|
+
case 'opencode':
|
|
64
|
+
return resolveOpenCode(userEnv);
|
|
63
65
|
default:
|
|
64
66
|
return EMPTY;
|
|
65
67
|
}
|
|
66
68
|
}
|
|
69
|
+
function resolveOpenCode(userEnv) {
|
|
70
|
+
const env = {};
|
|
71
|
+
if (userEnv.ANTHROPIC_API_KEY) {
|
|
72
|
+
env.ANTHROPIC_API_KEY = userEnv.ANTHROPIC_API_KEY;
|
|
73
|
+
}
|
|
74
|
+
if (userEnv.ANTHROPIC_BASE_URL) {
|
|
75
|
+
env.ANTHROPIC_BASE_URL = userEnv.ANTHROPIC_BASE_URL;
|
|
76
|
+
}
|
|
77
|
+
return { env, setupCommands: [], copyFromHome: [] };
|
|
78
|
+
}
|
|
67
79
|
async function resolveClaude(userEnv, ports) {
|
|
68
80
|
// User explicitly provided API key — trust it, nothing to add
|
|
69
81
|
if (userEnv.ANTHROPIC_API_KEY) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const DEBUG_ROOT_MARKER = ".pathgrade-debug-root.json";
|
|
2
|
+
export declare const DEBUG_RUN_MARKER = ".pathgrade-debug-run.json";
|
|
3
|
+
export declare const DEFAULT_DEBUG_RETAIN_RUNS = 3;
|
|
4
|
+
export declare function resolveDebugRunId(): string;
|
|
5
|
+
export interface CleanDebugRunsResult {
|
|
6
|
+
removed: number;
|
|
7
|
+
retained: number;
|
|
8
|
+
active: number;
|
|
9
|
+
dryRun: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function prepareManagedDebugRun(input: {
|
|
12
|
+
rootDir: string;
|
|
13
|
+
debugName: string;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
destination: string;
|
|
16
|
+
rootDir: string;
|
|
17
|
+
runId: string;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function cleanDebugRuns(input: {
|
|
20
|
+
rootDir: string;
|
|
21
|
+
keep?: number;
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
}): Promise<CleanDebugRunsResult>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const DEBUG_ROOT_MARKER = '.pathgrade-debug-root.json';
|
|
5
|
+
export const DEBUG_RUN_MARKER = '.pathgrade-debug-run.json';
|
|
6
|
+
export const DEFAULT_DEBUG_RETAIN_RUNS = 3;
|
|
7
|
+
let generatedRunId;
|
|
8
|
+
function createRunId() {
|
|
9
|
+
generatedRunId ??= `${new Date().toISOString().replace(/[:.]/g, '-')}-${Math.random().toString(36).slice(2, 8)}`;
|
|
10
|
+
return generatedRunId;
|
|
11
|
+
}
|
|
12
|
+
export function resolveDebugRunId() {
|
|
13
|
+
const configured = process.env.PATHGRADE_DEBUG_RUN_ID;
|
|
14
|
+
if (configured && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(configured))
|
|
15
|
+
return configured;
|
|
16
|
+
return createRunId();
|
|
17
|
+
}
|
|
18
|
+
async function readJsonFile(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
const stat = await fs.lstat(filePath);
|
|
21
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
22
|
+
return undefined;
|
|
23
|
+
return await fs.readJson(filePath);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function isOwnedDebugRoot(rootDir) {
|
|
30
|
+
try {
|
|
31
|
+
const stat = await fs.lstat(rootDir);
|
|
32
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const marker = await readJsonFile(path.join(rootDir, DEBUG_ROOT_MARKER));
|
|
39
|
+
return marker?.version === 1;
|
|
40
|
+
}
|
|
41
|
+
async function ensureOwnedDebugRoot(rootDir) {
|
|
42
|
+
await fs.ensureDir(rootDir);
|
|
43
|
+
const rootStat = await fs.lstat(rootDir);
|
|
44
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
45
|
+
throw new Error(`Pathgrade debug root must be a real directory: ${rootDir}`);
|
|
46
|
+
}
|
|
47
|
+
const markerPath = path.join(rootDir, DEBUG_ROOT_MARKER);
|
|
48
|
+
let markerExists = false;
|
|
49
|
+
try {
|
|
50
|
+
const markerStat = await fs.lstat(markerPath);
|
|
51
|
+
markerExists = true;
|
|
52
|
+
if (!markerStat.isFile() || markerStat.isSymbolicLink()) {
|
|
53
|
+
throw new Error(`Pathgrade debug root has an unsafe ownership marker: ${rootDir}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code !== 'ENOENT')
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (!markerExists) {
|
|
61
|
+
await fs.writeJson(markerPath, { version: 1 }, { spaces: 2 });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const existing = await readJsonFile(markerPath);
|
|
65
|
+
if (existing?.version !== 1) {
|
|
66
|
+
throw new Error(`Pathgrade debug root has an unsupported ownership marker: ${rootDir}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function isRealDirectory(directory) {
|
|
70
|
+
try {
|
|
71
|
+
const stat = await fs.lstat(directory);
|
|
72
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function ensureRealChildDirectory(directory) {
|
|
79
|
+
try {
|
|
80
|
+
await fs.mkdir(directory);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error.code !== 'EEXIST')
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
if (!await isRealDirectory(directory)) {
|
|
87
|
+
throw new Error(`Pathgrade managed debug path must be a real directory: ${directory}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function ensureRunMarker(runDir, runId) {
|
|
91
|
+
const markerPath = path.join(runDir, DEBUG_RUN_MARKER);
|
|
92
|
+
const temporaryMarker = `${markerPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
93
|
+
await fs.writeJson(temporaryMarker, {
|
|
94
|
+
version: 1,
|
|
95
|
+
runId,
|
|
96
|
+
createdAt: new Date().toISOString(),
|
|
97
|
+
}, { spaces: 2 });
|
|
98
|
+
try {
|
|
99
|
+
await fs.link(temporaryMarker, markerPath);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error.code !== 'EEXIST')
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await fs.remove(temporaryMarker);
|
|
107
|
+
}
|
|
108
|
+
const marker = await readJsonFile(markerPath);
|
|
109
|
+
if (marker?.version !== 1 ||
|
|
110
|
+
marker.runId !== runId ||
|
|
111
|
+
typeof marker.createdAt !== 'string' ||
|
|
112
|
+
!Number.isFinite(Date.parse(marker.createdAt))) {
|
|
113
|
+
throw new Error(`Pathgrade debug run has an unsafe ownership marker: ${runDir}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function prepareManagedDebugRun(input) {
|
|
117
|
+
const rootDir = path.resolve(input.rootDir);
|
|
118
|
+
await ensureOwnedDebugRoot(rootDir);
|
|
119
|
+
const runId = resolveDebugRunId();
|
|
120
|
+
const runsDir = path.join(rootDir, 'runs');
|
|
121
|
+
const runDir = path.join(runsDir, runId);
|
|
122
|
+
const activeDir = path.join(runDir, '.pathgrade-active');
|
|
123
|
+
await ensureRealChildDirectory(runsDir);
|
|
124
|
+
await ensureRealChildDirectory(runDir);
|
|
125
|
+
await ensureRealChildDirectory(activeDir);
|
|
126
|
+
await fs.writeFile(path.join(activeDir, String(process.pid)), '');
|
|
127
|
+
await ensureRunMarker(runDir, runId);
|
|
128
|
+
return {
|
|
129
|
+
destination: input.debugName ? path.join(runDir, input.debugName) : runDir,
|
|
130
|
+
rootDir,
|
|
131
|
+
runId,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function readOwnedRun(runDir) {
|
|
135
|
+
const marker = await readJsonFile(path.join(runDir, DEBUG_RUN_MARKER));
|
|
136
|
+
if (marker?.version !== 1 ||
|
|
137
|
+
typeof marker.runId !== 'string' ||
|
|
138
|
+
typeof marker.createdAt !== 'string' ||
|
|
139
|
+
!Number.isFinite(Date.parse(marker.createdAt))) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
if (path.basename(runDir) !== marker.runId)
|
|
143
|
+
return undefined;
|
|
144
|
+
return marker;
|
|
145
|
+
}
|
|
146
|
+
function isProcessAlive(pid) {
|
|
147
|
+
try {
|
|
148
|
+
process.kill(pid, 0);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return error.code !== 'ESRCH';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function isRunActive(runDir) {
|
|
156
|
+
const activeDir = path.join(runDir, '.pathgrade-active');
|
|
157
|
+
const entries = await fs.readdir(activeDir, { withFileTypes: true }).catch(() => []);
|
|
158
|
+
return entries.some(entry => {
|
|
159
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !/^\d+$/.test(entry.name))
|
|
160
|
+
return false;
|
|
161
|
+
const pid = Number(entry.name);
|
|
162
|
+
return Number.isSafeInteger(pid) && pid > 0 && isProcessAlive(pid);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export async function cleanDebugRuns(input) {
|
|
166
|
+
const dryRun = input.dryRun ?? false;
|
|
167
|
+
if (!await isOwnedDebugRoot(input.rootDir)) {
|
|
168
|
+
return { removed: 0, retained: 0, active: 0, dryRun };
|
|
169
|
+
}
|
|
170
|
+
const runsDir = path.join(input.rootDir, 'runs');
|
|
171
|
+
if (!await isRealDirectory(runsDir)) {
|
|
172
|
+
return { removed: 0, retained: 0, active: 0, dryRun };
|
|
173
|
+
}
|
|
174
|
+
const entries = await fs.readdir(runsDir, { withFileTypes: true }).catch(() => []);
|
|
175
|
+
const ownedRuns = [];
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
178
|
+
continue;
|
|
179
|
+
const dir = path.join(runsDir, entry.name);
|
|
180
|
+
const marker = await readOwnedRun(dir);
|
|
181
|
+
if (marker)
|
|
182
|
+
ownedRuns.push({ dir, marker, active: await isRunActive(dir) });
|
|
183
|
+
}
|
|
184
|
+
const activeRuns = ownedRuns.filter(run => run.active);
|
|
185
|
+
const completedRuns = ownedRuns.filter(run => !run.active);
|
|
186
|
+
completedRuns.sort((a, b) => b.marker.createdAt.localeCompare(a.marker.createdAt));
|
|
187
|
+
const keep = input.keep ?? 0;
|
|
188
|
+
const retained = completedRuns.slice(0, keep);
|
|
189
|
+
const removable = completedRuns.slice(keep);
|
|
190
|
+
let removed = removable.length;
|
|
191
|
+
let newlyActive = 0;
|
|
192
|
+
if (!dryRun) {
|
|
193
|
+
const removalResults = await Promise.all(removable.map(async (run) => {
|
|
194
|
+
if (await isRunActive(run.dir))
|
|
195
|
+
return false;
|
|
196
|
+
await fs.remove(run.dir);
|
|
197
|
+
return true;
|
|
198
|
+
}));
|
|
199
|
+
removed = removalResults.filter(Boolean).length;
|
|
200
|
+
newlyActive = removalResults.length - removed;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
removed,
|
|
204
|
+
retained: retained.length,
|
|
205
|
+
active: activeRuns.length + newlyActive,
|
|
206
|
+
dryRun,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -4,8 +4,13 @@ export class InvalidTransportEnvError extends Error {
|
|
|
4
4
|
this.name = 'InvalidTransportEnvError';
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
+
const AGENT_NAMES = ['claude', 'codex', 'cursor', 'opencode'];
|
|
7
8
|
export function resolveAgentName(opts, env) {
|
|
8
|
-
|
|
9
|
+
const value = opts.agent || env.PATHGRADE_AGENT || 'claude';
|
|
10
|
+
if (!AGENT_NAMES.includes(value)) {
|
|
11
|
+
throw new Error(`Unknown agent "${value}". Available agents: ${AGENT_NAMES.join(', ')}`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
9
14
|
}
|
|
10
15
|
export function resolveCodexTransport(opts, env) {
|
|
11
16
|
if (opts.transport)
|
package/dist/sdk/agent.js
CHANGED
|
@@ -14,6 +14,8 @@ import { getCurrentCaseContext } from './case-context.js';
|
|
|
14
14
|
import { createVerboseEmitter } from '../reporters/verbose-emitter.js';
|
|
15
15
|
import fs from 'fs-extra';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
+
import { cleanDebugRuns, DEFAULT_DEBUG_RETAIN_RUNS, prepareManagedDebugRun, } from '../providers/debug-runs.js';
|
|
18
|
+
import { collectOpenCodeMcpToolNames, validateOpenCodeDeclaration } from '../agents/opencode-contract.js';
|
|
17
19
|
/**
|
|
18
20
|
* Test-only injection point: override the sink used by the next emitter
|
|
19
21
|
* built inside `createAgent`. Pass `null` to restore the default (stderr).
|
|
@@ -41,7 +43,10 @@ class AgentImpl {
|
|
|
41
43
|
verbose;
|
|
42
44
|
transport;
|
|
43
45
|
mcpSafety;
|
|
44
|
-
|
|
46
|
+
opencodeExecutable;
|
|
47
|
+
opencodeMcpToolNames;
|
|
48
|
+
activeChatSession;
|
|
49
|
+
constructor(ws, agentName, llm, timeoutSetting, conversationWindow, modelOpt, debugOpt, debugName, debugBaseDir, verbose, transport, mcpSafety, opencodeExecutable, opencodeMcpToolNames) {
|
|
45
50
|
this.ws = ws;
|
|
46
51
|
this.agentName = agentName;
|
|
47
52
|
this.llm = llm;
|
|
@@ -54,6 +59,8 @@ class AgentImpl {
|
|
|
54
59
|
this.verbose = verbose;
|
|
55
60
|
this.transport = transport;
|
|
56
61
|
this.mcpSafety = mcpSafety;
|
|
62
|
+
this.opencodeExecutable = opencodeExecutable;
|
|
63
|
+
this.opencodeMcpToolNames = opencodeMcpToolNames;
|
|
57
64
|
}
|
|
58
65
|
get messages() {
|
|
59
66
|
return this._messages;
|
|
@@ -96,6 +103,8 @@ class AgentImpl {
|
|
|
96
103
|
...(askUserTimeoutMs !== undefined ? { askUserTimeoutMs } : {}),
|
|
97
104
|
...(this.transport !== undefined ? { transport: this.transport } : {}),
|
|
98
105
|
...(this.mcpSafety !== undefined ? { mcpSafety: this.mcpSafety } : {}),
|
|
106
|
+
...(this.opencodeExecutable !== undefined ? { opencodeExecutable: this.opencodeExecutable } : {}),
|
|
107
|
+
...(this.opencodeMcpToolNames !== undefined ? { opencodeMcpToolNames: this.opencodeMcpToolNames } : {}),
|
|
99
108
|
});
|
|
100
109
|
}
|
|
101
110
|
resolveTimeoutSec(mode, maxTurns) {
|
|
@@ -195,6 +204,7 @@ class AgentImpl {
|
|
|
195
204
|
}
|
|
196
205
|
const timeoutSec = this.resolveTimeoutSec('startChat');
|
|
197
206
|
const ms = this.createSession(timeoutSec);
|
|
207
|
+
this.activeChatSession = ms;
|
|
198
208
|
const turnResult = await this.executeLoggedTurnResult(ms, firstMessage, 1, 'agent_start');
|
|
199
209
|
return new ChatSessionImpl(turnResult, {
|
|
200
210
|
messages: this._messages,
|
|
@@ -310,24 +320,46 @@ class AgentImpl {
|
|
|
310
320
|
// Runner-owned agents stay tracked until flush consumes metadata.
|
|
311
321
|
// Manual agents have no runner flush, so dispose releases them.
|
|
312
322
|
lifecycleCore.releaseAgent(this);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
323
|
+
try {
|
|
324
|
+
await this.activeChatSession?.dispose?.();
|
|
325
|
+
this.activeChatSession = undefined;
|
|
326
|
+
if (this.debugOpt) {
|
|
327
|
+
const managedOptions = typeof this.debugOpt === 'object' ? this.debugOpt : undefined;
|
|
328
|
+
const managed = managedOptions
|
|
329
|
+
? await prepareManagedDebugRun({
|
|
330
|
+
rootDir: managedOptions.directory
|
|
331
|
+
? path.resolve(this.debugBaseDir, managedOptions.directory)
|
|
332
|
+
: path.join(this.debugBaseDir, 'pathgrade-debug'),
|
|
333
|
+
debugName: this.debugName,
|
|
334
|
+
})
|
|
335
|
+
: undefined;
|
|
336
|
+
const dest = typeof this.debugOpt === 'string'
|
|
337
|
+
? this.debugOpt
|
|
338
|
+
: managed?.destination ?? path.join(this.debugBaseDir, 'pathgrade-debug', this.debugName);
|
|
339
|
+
await fs.remove(dest);
|
|
340
|
+
await fs.copy(this.ws.path, dest);
|
|
341
|
+
if (this.interactionMode === 'runConversation' && this.lastConversationResult) {
|
|
342
|
+
const snapshot = buildRunSnapshot({
|
|
343
|
+
agent: this.agentName,
|
|
344
|
+
messages: this._messages,
|
|
345
|
+
log: this._log,
|
|
346
|
+
conversationResult: this.lastConversationResult,
|
|
347
|
+
workspace: dest,
|
|
348
|
+
});
|
|
349
|
+
await fs.writeJSON(path.join(dest, 'run-snapshot.json'), snapshot, { spaces: 2 });
|
|
350
|
+
}
|
|
351
|
+
if (managed) {
|
|
352
|
+
const retainRuns = managedOptions.retainRuns ?? DEFAULT_DEBUG_RETAIN_RUNS;
|
|
353
|
+
await cleanDebugRuns({
|
|
354
|
+
rootDir: managed.rootDir,
|
|
355
|
+
keep: Math.max(0, retainRuns - 1),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
328
358
|
}
|
|
329
359
|
}
|
|
330
|
-
|
|
360
|
+
finally {
|
|
361
|
+
await this.ws.dispose();
|
|
362
|
+
}
|
|
331
363
|
}
|
|
332
364
|
}
|
|
333
365
|
/**
|
|
@@ -353,14 +385,21 @@ function resolveCaseDebugContext() {
|
|
|
353
385
|
};
|
|
354
386
|
}
|
|
355
387
|
export async function createAgent(opts) {
|
|
388
|
+
if (typeof opts.debug === 'object') {
|
|
389
|
+
const retainRuns = opts.debug.retainRuns ?? DEFAULT_DEBUG_RETAIN_RUNS;
|
|
390
|
+
if (!Number.isSafeInteger(retainRuns) || retainRuns < 1) {
|
|
391
|
+
throw new Error('Pathgrade debug retainRuns must be a positive integer');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
356
394
|
const agentName = resolveAgentName(opts, process.env);
|
|
395
|
+
validateOpenCodeDeclaration(agentName, opts);
|
|
357
396
|
const transport = agentName === 'codex'
|
|
358
397
|
? resolveCodexTransport(opts, process.env)
|
|
359
398
|
: undefined;
|
|
360
399
|
const timeoutSetting = opts.timeout ?? 300;
|
|
361
400
|
// Capture runner context now; adapters own installation and restoration.
|
|
362
401
|
const testCtx = opts.debug ? resolveCaseDebugContext() : { name: '', dir: '' };
|
|
363
|
-
const { timeout: _, mcpMock, mcpConfigFile, agent: __, debug: ___, model: ____, transport: _____, mcpSafety: ______, ...rest } = opts;
|
|
402
|
+
const { timeout: _, mcpMock, mcpConfigFile, agent: __, debug: ___, model: ____, transport: _____, mcpSafety: ______, opencodeExecutable, ...rest } = opts;
|
|
364
403
|
const workspace = await prepareWorkspace({
|
|
365
404
|
...rest,
|
|
366
405
|
agent: agentName,
|
|
@@ -378,7 +417,7 @@ export async function createAgent(opts) {
|
|
|
378
417
|
sink: verboseSinkOverride ?? undefined,
|
|
379
418
|
testName: testCtx.name || undefined,
|
|
380
419
|
});
|
|
381
|
-
const agent = new AgentImpl(workspace, agentName, llm, timeoutSetting, opts.conversationWindow, opts.model, opts.debug, debugName, debugBaseDir, verbose, transport, opts.mcpSafety);
|
|
420
|
+
const agent = new AgentImpl(workspace, agentName, llm, timeoutSetting, opts.conversationWindow, opts.model, opts.debug, debugName, debugBaseDir, verbose, transport, opts.mcpSafety, opencodeExecutable, agentName === 'opencode' ? collectOpenCodeMcpToolNames(mcpMock) : undefined);
|
|
382
421
|
lifecycleCore.registerAgent(agent);
|
|
383
422
|
return agent;
|
|
384
423
|
}
|
package/dist/sdk/converse.js
CHANGED
|
@@ -175,7 +175,9 @@ export async function runConversation(opts, deps) {
|
|
|
175
175
|
const turnStart = Date.now();
|
|
176
176
|
// Under app-server the child process holds thread state; a crashed
|
|
177
177
|
// turn is not safely replayable, so retries are suppressed.
|
|
178
|
-
const maxRetries = deps.transport === 'app-server'
|
|
178
|
+
const maxRetries = deps.transport === 'app-server' || deps.agentName === 'opencode'
|
|
179
|
+
? 0
|
|
180
|
+
: MAX_TURN_RETRIES;
|
|
179
181
|
let lastError;
|
|
180
182
|
let turnResult;
|
|
181
183
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { emitEvalResult, resetAllResultObserversForTests, resetUserResultObserve
|
|
|
21
21
|
export { getAgentCapabilities } from './types.js';
|
|
22
22
|
export type { AgentTransport, AgentCapabilities, AgentName, McpRunMode, McpSafetyOptions, McpToolPolicy, McpToolPolicyRule, } from './types.js';
|
|
23
23
|
export type { AskBus, AskBatch, AskQuestion, AskOption, AskAnswer, AskResolution, AskBatchSnapshot, AskAnswerSnapshot, AskResolutionSnapshot, AskHandle, AskHandler, AskSource, AskLifecycle, AskAnswerSource, Unsubscribe as AskBusUnsubscribe, } from './ask-bus/types.js';
|
|
24
|
-
export type { Agent, AgentOptions, Message, Scorer, CheckScorer, ScoreScorer, JudgeScorer, ToolUsageScorer, ScorerContext, EvalResult, ScorerResultEntry, ScorerStatus, ChatSession, ConversationResult, ConverseOptions, UntilPredicate, UntilContext, Reaction, TextReaction, AskUserReaction, AskUserQuestion, AskUserOption, ReactionPreviewEntry, TextReactionPreviewEntry, AskUserReactionPreviewEntry, ReactionPreviewResult, ReactionPreviewTurn, StepScorer, Persona, PersonaConfig, ConversationWindowConfig, TurnDetail, ReactionFiredEntry, PathgradePluginOptions, PathgradeMeta, TurnTiming, TokenUsage, EvaluateOptions, ReactionPreviewStatus, ScoreResult, JudgeInput, CodeJudgeToolName, ToolExpectation, SessionArtifactMatchOptions, SessionArtifactContent, SessionArtifacts, RecordedEvalResult, PathgradeTestMeta, EvaluationResultKind, AgentExecutionMetadata, AgentExecutionTransport, AgentInteractionMode, } from './types.js';
|
|
24
|
+
export type { Agent, AgentOptions, DebugOptions, Message, Scorer, CheckScorer, ScoreScorer, JudgeScorer, ToolUsageScorer, ScorerContext, EvalResult, ScorerResultEntry, ScorerStatus, ChatSession, ConversationResult, ConverseOptions, UntilPredicate, UntilContext, Reaction, TextReaction, AskUserReaction, AskUserQuestion, AskUserOption, ReactionPreviewEntry, TextReactionPreviewEntry, AskUserReactionPreviewEntry, ReactionPreviewResult, ReactionPreviewTurn, StepScorer, Persona, PersonaConfig, ConversationWindowConfig, TurnDetail, ReactionFiredEntry, PathgradePluginOptions, PathgradeMeta, TurnTiming, TokenUsage, EvaluateOptions, ReactionPreviewStatus, ScoreResult, JudgeInput, CodeJudgeToolName, ToolExpectation, SessionArtifactMatchOptions, SessionArtifactContent, SessionArtifacts, RecordedEvalResult, PathgradeTestMeta, EvaluationResultKind, AgentExecutionMetadata, AgentExecutionTransport, AgentInteractionMode, } from './types.js';
|
|
25
25
|
export type { ConversationWindow, ConversationWindowOptions } from './conversation-window.js';
|
|
26
26
|
export type { JudgePipelineOptions } from './judge-pipeline.js';
|
|
27
27
|
export type { RunScorerOptions } from './run-scorer.js';
|
|
@@ -31,6 +31,8 @@ export interface ManagedSessionDeps {
|
|
|
31
31
|
transport?: AgentTransport;
|
|
32
32
|
/** Live MCP Server Safety policy to enforce in supported harnesses. */
|
|
33
33
|
mcpSafety?: McpSafetyOptions;
|
|
34
|
+
opencodeExecutable?: string;
|
|
35
|
+
opencodeMcpToolNames?: string[];
|
|
34
36
|
}
|
|
35
37
|
export interface ManagedSession {
|
|
36
38
|
/** Full lifecycle: log start/result, push messages, check exit code. */
|
|
@@ -24,6 +24,8 @@ export function createManagedSession(deps) {
|
|
|
24
24
|
askBus,
|
|
25
25
|
...(transport !== undefined ? { transport } : {}),
|
|
26
26
|
...(deps.mcpSafety !== undefined ? { mcpSafety: deps.mcpSafety } : {}),
|
|
27
|
+
...(deps.opencodeExecutable !== undefined ? { opencodeExecutable: deps.opencodeExecutable } : {}),
|
|
28
|
+
...(deps.opencodeMcpToolNames !== undefined ? { opencodeMcpToolNames: deps.opencodeMcpToolNames } : {}),
|
|
27
29
|
getAbortSignal: () => currentSignal,
|
|
28
30
|
};
|
|
29
31
|
let session = null;
|
|
@@ -7,6 +7,7 @@ const INTERACTIVE_TOOL_NAMES = {
|
|
|
7
7
|
claude: 'AskUserQuestion',
|
|
8
8
|
codex: 'request_user_input',
|
|
9
9
|
cursor: 'AskQuestion',
|
|
10
|
+
opencode: 'question',
|
|
10
11
|
};
|
|
11
12
|
export function planRuntimePolicies(agent, transport) {
|
|
12
13
|
const capabilities = getAgentCapabilities(agent, transport);
|
package/dist/sdk/snapshots.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { LogEntry } from '../types.js';
|
|
2
2
|
import type { ToolEvent } from '../tool-events.js';
|
|
3
3
|
import type { AgentName, ConversationResult, Message, TurnTiming } from './types.js';
|
|
4
|
-
export declare const RUN_SNAPSHOT_VERSION =
|
|
4
|
+
export declare const RUN_SNAPSHOT_VERSION = 2;
|
|
5
5
|
export interface RunSnapshot {
|
|
6
|
-
version: 1;
|
|
6
|
+
version: 1 | 2;
|
|
7
7
|
timestamp: string;
|
|
8
8
|
agent: AgentName;
|
|
9
9
|
messages: Message[];
|
package/dist/sdk/snapshots.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
export const RUN_SNAPSHOT_VERSION =
|
|
2
|
+
export const RUN_SNAPSHOT_VERSION = 2;
|
|
3
3
|
export function buildRunSnapshot(params) {
|
|
4
4
|
const { agent, messages, log, conversationResult, workspace, timestamp } = params;
|
|
5
5
|
const toolEvents = log
|
|
6
6
|
.filter((entry) => entry.type === 'tool_event' && entry.tool_event)
|
|
7
7
|
.map((entry) => entry.tool_event);
|
|
8
8
|
return {
|
|
9
|
-
version:
|
|
9
|
+
version: agent === 'opencode' ? 2 : 1,
|
|
10
10
|
timestamp: timestamp ?? new Date().toISOString(),
|
|
11
11
|
agent,
|
|
12
12
|
messages: [...messages],
|
|
@@ -102,6 +102,9 @@ function validateRunSnapshot(input) {
|
|
|
102
102
|
if (snapshot.version > RUN_SNAPSHOT_VERSION) {
|
|
103
103
|
throw new SnapshotVersionError(snapshot.version);
|
|
104
104
|
}
|
|
105
|
+
if (snapshot.version !== 1 && snapshot.version !== 2) {
|
|
106
|
+
throw new SnapshotVersionError(snapshot.version);
|
|
107
|
+
}
|
|
105
108
|
if (!Array.isArray(snapshot.messages)) {
|
|
106
109
|
throw new SnapshotParseError('Snapshot is missing required field: messages');
|
|
107
110
|
}
|
|
@@ -127,10 +130,16 @@ function validateRunSnapshot(input) {
|
|
|
127
130
|
const turnTimings = Array.isArray(snapshot.turnTimings)
|
|
128
131
|
? snapshot.turnTimings
|
|
129
132
|
: snapshot.conversationResult.turnTimings;
|
|
133
|
+
const legacyAgents = ['claude', 'codex', 'cursor'];
|
|
134
|
+
const currentAgents = [...legacyAgents, 'opencode'];
|
|
135
|
+
const allowedAgents = snapshot.version === 1 ? legacyAgents : currentAgents;
|
|
136
|
+
if (typeof snapshot.agent !== 'string' || !allowedAgents.includes(snapshot.agent)) {
|
|
137
|
+
throw new SnapshotParseError(`Snapshot version ${snapshot.version} has invalid agent: ${String(snapshot.agent)}`);
|
|
138
|
+
}
|
|
130
139
|
return {
|
|
131
|
-
version:
|
|
140
|
+
version: snapshot.version,
|
|
132
141
|
timestamp: typeof snapshot.timestamp === 'string' ? snapshot.timestamp : new Date(0).toISOString(),
|
|
133
|
-
agent: snapshot.agent
|
|
142
|
+
agent: snapshot.agent,
|
|
134
143
|
messages: snapshot.messages,
|
|
135
144
|
log: snapshot.log,
|
|
136
145
|
toolEvents: snapshot.toolEvents,
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { TrialResult } from '../types.js';
|
|
|
5
5
|
import type { DiagnosticsReport } from './diagnostics.js';
|
|
6
6
|
import type { LLMPort } from '../utils/llm-types.js';
|
|
7
7
|
import type { McpSafetyOptions } from './mcp-safety.js';
|
|
8
|
-
export type AgentName = 'claude' | 'codex' | 'cursor';
|
|
8
|
+
export type AgentName = 'claude' | 'codex' | 'cursor' | 'opencode';
|
|
9
9
|
export type AgentInteractionMode = 'prompt' | 'start_chat' | 'conversation';
|
|
10
10
|
/** Runtime channel that actually executed the agent. */
|
|
11
11
|
export type AgentExecutionTransport = AgentTransport | 'claude-agent-sdk' | 'cursor-agent';
|
|
@@ -19,6 +19,8 @@ export interface AgentExecutionMetadata {
|
|
|
19
19
|
}
|
|
20
20
|
export interface AgentOptions {
|
|
21
21
|
agent?: AgentName;
|
|
22
|
+
/** Absolute path to the pinned OpenCode executable. Valid only for `agent: 'opencode'`. */
|
|
23
|
+
opencodeExecutable?: string;
|
|
22
24
|
model?: string;
|
|
23
25
|
timeout?: number | 'auto';
|
|
24
26
|
workspace?: string;
|
|
@@ -29,8 +31,8 @@ export interface AgentOptions {
|
|
|
29
31
|
mcpConfigFile?: string;
|
|
30
32
|
/** Configure the conversation window for transcript-based agents. Set false to disable. */
|
|
31
33
|
conversationWindow?: ConversationWindowConfig | false;
|
|
32
|
-
/**
|
|
33
|
-
debug?: boolean | string;
|
|
34
|
+
/** Preserve the workspace. true uses the legacy path; strings are exact paths; objects enable managed retention. */
|
|
35
|
+
debug?: boolean | string | DebugOptions;
|
|
34
36
|
/**
|
|
35
37
|
* Glob patterns to ignore when copying workspace and skill directories.
|
|
36
38
|
* Replaces the default ignore list entirely. Pass `[]` to disable filtering.
|
|
@@ -51,6 +53,12 @@ export interface AgentOptions {
|
|
|
51
53
|
*/
|
|
52
54
|
mcpSafety?: McpSafetyOptions;
|
|
53
55
|
}
|
|
56
|
+
export interface DebugOptions {
|
|
57
|
+
/** Managed debug root. Relative paths resolve next to the eval file. */
|
|
58
|
+
directory?: string;
|
|
59
|
+
/** Maximum managed runs to retain, including the current run. Default: 3. */
|
|
60
|
+
retainRuns?: number;
|
|
61
|
+
}
|
|
54
62
|
export type { McpRunMode, McpSafetyOptions, McpToolPolicy, McpToolPolicyRule, } from './mcp-safety.js';
|
|
55
63
|
export interface ConversationWindowConfig {
|
|
56
64
|
/** Number of recent messages to keep verbatim. Default: 4 */
|
package/dist/sdk/types.js
CHANGED
|
@@ -2,6 +2,7 @@ const BASE_CAPABILITIES = {
|
|
|
2
2
|
claude: { mcp: true, nativeSession: true, interactiveQuestionTransport: 'reliable' },
|
|
3
3
|
codex: { mcp: false, nativeSession: true, interactiveQuestionTransport: 'noninteractive' },
|
|
4
4
|
cursor: { mcp: true, nativeSession: true, interactiveQuestionTransport: 'noninteractive' },
|
|
5
|
+
opencode: { mcp: true, nativeSession: true, interactiveQuestionTransport: 'noninteractive' },
|
|
5
6
|
};
|
|
6
7
|
export function getAgentCapabilities(agent, transport) {
|
|
7
8
|
const base = BASE_CAPABILITIES[agent];
|
package/dist/tool-events.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type ToolAction = 'run_shell' | 'read_file' | 'write_file' | 'edit_file' | 'search_code' | 'list_files' | 'ask_user' | 'web_fetch' | 'use_skill' | 'update_todos' | 'mcp_tool_call' | 'unknown';
|
|
2
2
|
export interface ToolEvent {
|
|
3
3
|
action: ToolAction;
|
|
4
|
-
provider: 'claude' | 'codex' | 'cursor';
|
|
4
|
+
provider: 'claude' | 'codex' | 'cursor' | 'opencode';
|
|
5
5
|
providerToolName: string;
|
|
6
6
|
turnNumber?: number;
|
|
7
7
|
arguments?: Record<string, unknown>;
|
package/dist/types.d.ts
CHANGED
|
@@ -379,6 +379,10 @@ export interface AgentSessionOptions {
|
|
|
379
379
|
abortSignal?: AbortSignal;
|
|
380
380
|
/** Supplies the current timeout/cancellation signal for reused sessions. */
|
|
381
381
|
getAbortSignal?: () => AbortSignal | undefined;
|
|
382
|
+
/** Absolute pinned runtime path for the OpenCode adapter. */
|
|
383
|
+
opencodeExecutable?: string;
|
|
384
|
+
/** Exact generated MCP tool names accepted by the OpenCode event normalizer. */
|
|
385
|
+
opencodeMcpToolNames?: string[];
|
|
382
386
|
}
|
|
383
387
|
export declare abstract class BaseAgent {
|
|
384
388
|
createSession(runtime: EnvironmentHandle, runCommand: AgentCommandRunner, options?: AgentSessionOptions): Promise<AgentSession>;
|