@glrs-dev/harness-plugin-opencode 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/README.md +42 -106
- package/SECURITY.md +1 -1
- package/dist/agents/prompts/build.md +34 -4
- package/dist/agents/prompts/build.open.md +18 -4
- package/dist/agents/prompts/code-reviewer-thorough.md +77 -0
- package/dist/agents/prompts/code-reviewer.md +80 -0
- package/dist/agents/prompts/code-reviewer.open.md +68 -0
- package/dist/agents/prompts/debriefer.md +55 -0
- package/dist/agents/prompts/gap-analyzer.md +2 -0
- package/dist/agents/prompts/plan-reviewer.md +5 -1
- package/dist/agents/prompts/plan.md +119 -10
- package/dist/agents/prompts/prime.md +149 -88
- package/dist/agents/prompts/research-auto.md +1 -1
- package/dist/agents/prompts/research-local.md +1 -1
- package/dist/agents/prompts/research-web.md +1 -1
- package/dist/agents/prompts/research.md +2 -0
- package/dist/agents/prompts/scoper.md +129 -0
- package/dist/agents/prompts/spec-reviewer.md +53 -0
- package/dist/agents/prompts/spec-reviewer.open.md +56 -0
- package/dist/agents/shared/index.ts +1 -0
- package/dist/agents/shared/ui-evaluation-ladder.md +50 -0
- package/dist/agents/shared/workflow-mechanics.md +5 -5
- package/dist/autopilot/prompt-template.md +104 -0
- package/dist/chunk-GCWHRUOK.js +259 -0
- package/dist/chunk-MJSMBY2Y.js +87 -0
- package/dist/chunk-NIFAVPNN.js +544 -0
- package/dist/{chunk-VJUETC6A.js → chunk-PDMXYZM4.js} +53 -1
- package/dist/cli.js +1596 -1964
- package/dist/commands/prompts/fresh.md +27 -24
- package/dist/commands/prompts/review.md +3 -3
- package/dist/commands/prompts/ship.md +2 -0
- package/dist/index.js +188 -633
- package/dist/loop-session-J35NILUZ.js +30 -0
- package/dist/opencode-server-KPCDFYAX.js +22 -0
- package/dist/plan-parser-TMHEKT22.js +6 -0
- package/dist/plan-session-7VS32P52.js +117 -0
- package/dist/scoper-S77SOK7X.js +326 -0
- package/dist/skills/adversarial-review-rubric/SKILL.md +47 -0
- package/dist/skills/code-quality/SKILL.md +1 -1
- package/dist/skills/root-cause-diagnosis/SKILL.md +24 -0
- package/dist/skills/spear-protocol/SKILL.md +167 -0
- package/package.json +3 -1
- package/dist/agents/prompts/pilot-assessor.md +0 -77
- package/dist/agents/prompts/pilot-builder.md +0 -40
- package/dist/agents/prompts/pilot-planner.md +0 -56
- package/dist/agents/prompts/pilot-scoper.md +0 -58
- package/dist/agents/prompts/qa-reviewer.md +0 -68
- package/dist/agents/prompts/qa-reviewer.open.md +0 -58
- package/dist/agents/prompts/qa-thorough.md +0 -63
- package/dist/bin/plan-check.sh +0 -255
- package/dist/chunk-6CZPRUMJ.js +0 -869
- package/dist/chunk-DZG4D3OH.js +0 -54
- package/dist/chunk-OYRKOEXK.js +0 -88
- package/dist/commands/prompts/autopilot.md +0 -96
- package/dist/install-6775ZBDG.js +0 -13
- package/dist/paths-WZ23ZQOV.js +0 -18
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parsePlanState
|
|
3
|
+
} from "./chunk-MJSMBY2Y.js";
|
|
4
|
+
import {
|
|
5
|
+
createSession,
|
|
6
|
+
getLastAssistantMessage,
|
|
7
|
+
getSessionCost,
|
|
8
|
+
sendAndWait,
|
|
9
|
+
startServer
|
|
10
|
+
} from "./chunk-GCWHRUOK.js";
|
|
11
|
+
|
|
12
|
+
// src/autopilot/loop.ts
|
|
13
|
+
import { execFile as execFileCb } from "child_process";
|
|
14
|
+
import { promisify } from "util";
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
import { join as join3 } from "path";
|
|
17
|
+
|
|
18
|
+
// src/lib/logger.ts
|
|
19
|
+
import pino from "pino";
|
|
20
|
+
import PinoPretty from "pino-pretty";
|
|
21
|
+
import { existsSync, mkdirSync } from "fs";
|
|
22
|
+
import { dirname, join } from "path";
|
|
23
|
+
function resolveStderrLevel() {
|
|
24
|
+
const env = process.env["GLRS_LOG_LEVEL"];
|
|
25
|
+
if (env && ["fatal", "error", "warn", "info", "debug", "trace", "silent"].includes(env)) {
|
|
26
|
+
return env;
|
|
27
|
+
}
|
|
28
|
+
return "info";
|
|
29
|
+
}
|
|
30
|
+
function shouldPrettyPrint() {
|
|
31
|
+
if (process.env["GLRS_LOG_FORMAT"] === "json") return false;
|
|
32
|
+
if (process.env["GLRS_LOG_FORMAT"] === "pretty") return true;
|
|
33
|
+
return process.stderr.isTTY ?? false;
|
|
34
|
+
}
|
|
35
|
+
function resolveLogFilePath(cwd) {
|
|
36
|
+
const env = process.env["GLRS_AUTOPILOT_LOG_FILE"];
|
|
37
|
+
if (env === "off") return null;
|
|
38
|
+
if (env) return env;
|
|
39
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
40
|
+
return join(cwd, ".agent", "autopilot-logs", `${timestamp}.log`);
|
|
41
|
+
}
|
|
42
|
+
function buildStderrStream(level) {
|
|
43
|
+
if (shouldPrettyPrint()) {
|
|
44
|
+
const pretty = PinoPretty({
|
|
45
|
+
colorize: true,
|
|
46
|
+
translateTime: "HH:MM:ss.l",
|
|
47
|
+
ignore: "pid,hostname,component",
|
|
48
|
+
messageFormat: "[{component}] {msg}",
|
|
49
|
+
destination: 2
|
|
50
|
+
// stderr
|
|
51
|
+
});
|
|
52
|
+
return { level, stream: pretty };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
level,
|
|
56
|
+
stream: pino.destination({ fd: 2, sync: false })
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function buildFileStream(cwd) {
|
|
60
|
+
const filePath = resolveLogFilePath(cwd);
|
|
61
|
+
if (!filePath) return null;
|
|
62
|
+
const parent = dirname(filePath);
|
|
63
|
+
if (!existsSync(parent)) {
|
|
64
|
+
mkdirSync(parent, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
path: filePath,
|
|
68
|
+
entry: {
|
|
69
|
+
level: "trace",
|
|
70
|
+
// sync: true gives deterministic flushSync semantics so the file
|
|
71
|
+
// log is safe to read immediately after flush() returns (critical
|
|
72
|
+
// for tests and for reliable postmortem after a crash). Autopilot
|
|
73
|
+
// runs are long-lived enough that sync writes aren't a bottleneck.
|
|
74
|
+
stream: pino.destination({ dest: filePath, sync: true, mkdir: true })
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function createAutopilotLogger(opts) {
|
|
79
|
+
const stderrLevel = resolveStderrLevel();
|
|
80
|
+
const fileSink = buildFileStream(opts.cwd);
|
|
81
|
+
const streams = [buildStderrStream(stderrLevel)];
|
|
82
|
+
if (fileSink) streams.push(fileSink.entry);
|
|
83
|
+
const ms = pino.multistream(streams);
|
|
84
|
+
const root = pino(
|
|
85
|
+
{
|
|
86
|
+
level: "trace",
|
|
87
|
+
// router gate; individual streams apply their own levels
|
|
88
|
+
timestamp: pino.stdTimeFunctions.isoTime
|
|
89
|
+
},
|
|
90
|
+
ms
|
|
91
|
+
);
|
|
92
|
+
const flush = async () => {
|
|
93
|
+
try {
|
|
94
|
+
ms.flushSync();
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
root,
|
|
100
|
+
logFilePath: fileSink?.path ?? null,
|
|
101
|
+
flush
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function childLogger(root, component) {
|
|
105
|
+
return root.child({ component });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/autopilot/status.ts
|
|
109
|
+
function formatElapsed(ms) {
|
|
110
|
+
const totalSeconds = Math.floor(ms / 1e3);
|
|
111
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
112
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
113
|
+
const seconds = totalSeconds % 60;
|
|
114
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
115
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
116
|
+
return `${seconds}s`;
|
|
117
|
+
}
|
|
118
|
+
function formatCost(usd) {
|
|
119
|
+
if (usd === 0) return "$0.00";
|
|
120
|
+
return `$${usd.toFixed(3)}`;
|
|
121
|
+
}
|
|
122
|
+
function composeStatusMessage(state, now) {
|
|
123
|
+
const elapsed = formatElapsed(now - state.startedAt);
|
|
124
|
+
const cost = formatCost(state.cumulativeCostUsd);
|
|
125
|
+
const iterNote = state.iterationsCompleted === 0 ? "iteration 1 in flight" : `${state.iterationsCompleted} iteration${state.iterationsCompleted === 1 ? "" : "s"} complete`;
|
|
126
|
+
let planNote = "";
|
|
127
|
+
if (state.phaseCount !== void 0 && state.phasesCompleted !== void 0 && state.mainCheckboxesTotal !== void 0 && state.mainCheckboxesCompleted !== void 0) {
|
|
128
|
+
planNote = `, phase ${state.phasesCompleted}/${state.phaseCount}, ${state.mainCheckboxesCompleted}/${state.mainCheckboxesTotal} boxes`;
|
|
129
|
+
}
|
|
130
|
+
if (state.lastIterationErrored) {
|
|
131
|
+
return `working (${iterNote}, ${elapsed} elapsed, ${cost} used${planNote}) \u2014 last iteration errored`;
|
|
132
|
+
}
|
|
133
|
+
return `working (${iterNote}, ${elapsed} elapsed, ${cost} used${planNote})`;
|
|
134
|
+
}
|
|
135
|
+
function createStatusHeartbeat(opts) {
|
|
136
|
+
const now = opts._deps?.now ?? (() => Date.now());
|
|
137
|
+
const setIntervalFn = opts._deps?.setInterval ?? setInterval;
|
|
138
|
+
const clearIntervalFn = opts._deps?.clearInterval ?? clearInterval;
|
|
139
|
+
const state = {
|
|
140
|
+
startedAt: now(),
|
|
141
|
+
iterationsCompleted: 0,
|
|
142
|
+
cumulativeCostUsd: 0,
|
|
143
|
+
lastIterationProgress: false,
|
|
144
|
+
lastIterationErrored: false
|
|
145
|
+
};
|
|
146
|
+
let timerId = null;
|
|
147
|
+
const tick = () => {
|
|
148
|
+
if (opts.pollCost) {
|
|
149
|
+
opts.pollCost().then((cost) => {
|
|
150
|
+
if (cost > 0) state.cumulativeCostUsd = cost;
|
|
151
|
+
}).catch(() => {
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const message = composeStatusMessage(state, now());
|
|
155
|
+
opts.logger.info(
|
|
156
|
+
{
|
|
157
|
+
elapsedMs: now() - state.startedAt,
|
|
158
|
+
iterationsCompleted: state.iterationsCompleted,
|
|
159
|
+
cumulativeCostUsd: state.cumulativeCostUsd,
|
|
160
|
+
lastIterationProgress: state.lastIterationProgress,
|
|
161
|
+
lastIterationErrored: state.lastIterationErrored
|
|
162
|
+
},
|
|
163
|
+
message
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
start() {
|
|
168
|
+
if (timerId !== null) return;
|
|
169
|
+
timerId = setIntervalFn(tick, opts.intervalMs);
|
|
170
|
+
},
|
|
171
|
+
stop() {
|
|
172
|
+
if (timerId === null) return;
|
|
173
|
+
clearIntervalFn(timerId);
|
|
174
|
+
timerId = null;
|
|
175
|
+
},
|
|
176
|
+
update(patch) {
|
|
177
|
+
Object.assign(state, patch);
|
|
178
|
+
},
|
|
179
|
+
getState() {
|
|
180
|
+
return { ...state };
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/autopilot/config.ts
|
|
186
|
+
var MAX_ITERATIONS = 50;
|
|
187
|
+
var STRUGGLE_THRESHOLD = 3;
|
|
188
|
+
var TIMEOUT_MS = 4 * 60 * 60 * 1e3;
|
|
189
|
+
var STALL_MS = 60 * 60 * 1e3;
|
|
190
|
+
var KILL_SWITCH_PATH = ".agent/autopilot-disable";
|
|
191
|
+
var SENTINEL_TAG = "<autopilot-done>";
|
|
192
|
+
var STATUS_INTERVAL_MS = (() => {
|
|
193
|
+
const env = process.env["GLRS_AUTOPILOT_STATUS_INTERVAL_MS"];
|
|
194
|
+
if (!env) return 5 * 60 * 1e3;
|
|
195
|
+
const n = Number.parseInt(env, 10);
|
|
196
|
+
if (Number.isNaN(n) || n < 1e3 || n > 60 * 60 * 1e3) return 5 * 60 * 1e3;
|
|
197
|
+
return n;
|
|
198
|
+
})();
|
|
199
|
+
|
|
200
|
+
// src/autopilot/sentinel.ts
|
|
201
|
+
function detectSentinel(text) {
|
|
202
|
+
if (!text.includes(SENTINEL_TAG)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
const withoutFences = text.replace(/```[\s\S]*?```/g, "");
|
|
206
|
+
const withoutInline = withoutFences.replace(/`[^`\n]*`/g, "");
|
|
207
|
+
return withoutInline.includes(SENTINEL_TAG);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/autopilot/struggle.ts
|
|
211
|
+
import * as fs from "fs";
|
|
212
|
+
import * as path from "path";
|
|
213
|
+
var StruggleDetector = class {
|
|
214
|
+
_consecutiveStalls = 0;
|
|
215
|
+
_threshold;
|
|
216
|
+
constructor(threshold) {
|
|
217
|
+
this._threshold = threshold;
|
|
218
|
+
}
|
|
219
|
+
/** Number of consecutive stall iterations recorded so far. */
|
|
220
|
+
get consecutiveStalls() {
|
|
221
|
+
return this._consecutiveStalls;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Record the result of one iteration.
|
|
225
|
+
* @param madeProgress - true if the agent made filesystem changes this iteration.
|
|
226
|
+
*/
|
|
227
|
+
record(madeProgress) {
|
|
228
|
+
if (madeProgress) {
|
|
229
|
+
this._consecutiveStalls = 0;
|
|
230
|
+
} else {
|
|
231
|
+
this._consecutiveStalls++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Returns true if the agent has stalled for `threshold` consecutive
|
|
236
|
+
* iterations without making progress.
|
|
237
|
+
*/
|
|
238
|
+
isStruggling() {
|
|
239
|
+
return this._consecutiveStalls >= this._threshold;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
function checkKillSwitch(cwd) {
|
|
243
|
+
const killSwitchFile = path.join(cwd, KILL_SWITCH_PATH);
|
|
244
|
+
return fs.existsSync(killSwitchFile);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/autopilot/loop.ts
|
|
248
|
+
var execFile = promisify(execFileCb);
|
|
249
|
+
function buildFullPrompt(userPrompt) {
|
|
250
|
+
const candidates = [
|
|
251
|
+
join3(import.meta.dir, "prompt-template.md"),
|
|
252
|
+
join3(import.meta.dir, "..", "..", "src", "autopilot", "prompt-template.md")
|
|
253
|
+
];
|
|
254
|
+
let template = "";
|
|
255
|
+
for (const candidate of candidates) {
|
|
256
|
+
try {
|
|
257
|
+
const raw = readFileSync(candidate, "utf8");
|
|
258
|
+
template = raw.replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
259
|
+
break;
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const withArgs = template.replace("$ARGUMENTS", userPrompt);
|
|
264
|
+
return withArgs || userPrompt;
|
|
265
|
+
}
|
|
266
|
+
async function checkProgress(cwd, baseRef) {
|
|
267
|
+
try {
|
|
268
|
+
const { stdout } = await execFile("git", ["diff", "--stat", baseRef], { cwd });
|
|
269
|
+
return stdout.trim().length > 0;
|
|
270
|
+
} catch {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function getHeadSha(cwd) {
|
|
275
|
+
try {
|
|
276
|
+
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], { cwd });
|
|
277
|
+
return stdout.trim();
|
|
278
|
+
} catch {
|
|
279
|
+
return "HEAD";
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function runRalphLoop(opts) {
|
|
283
|
+
const maxIterations = opts.maxIterations ?? MAX_ITERATIONS;
|
|
284
|
+
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
285
|
+
const stallMs = opts.stallMs ?? STALL_MS;
|
|
286
|
+
const struggleThreshold = opts.struggleThreshold ?? STRUGGLE_THRESHOLD;
|
|
287
|
+
const _startServer = opts._deps?.startServer ?? startServer;
|
|
288
|
+
const _createSession = opts._deps?.createSession ?? createSession;
|
|
289
|
+
const _sendAndWait = opts._deps?.sendAndWait ?? sendAndWait;
|
|
290
|
+
const _getLastAssistantMessage = opts._deps?.getLastAssistantMessage ?? getLastAssistantMessage;
|
|
291
|
+
const fullPrompt = buildFullPrompt(opts.prompt);
|
|
292
|
+
const struggle = new StruggleDetector(struggleThreshold);
|
|
293
|
+
const startTime = Date.now();
|
|
294
|
+
const autopilotLog = createAutopilotLogger({ cwd: opts.cwd });
|
|
295
|
+
const log = childLogger(autopilotLog.root, "autopilot.loop");
|
|
296
|
+
const toolLog = childLogger(autopilotLog.root, "autopilot.tool");
|
|
297
|
+
const streamLog = childLogger(autopilotLog.root, "autopilot.stream");
|
|
298
|
+
const statusLog = childLogger(autopilotLog.root, "autopilot.status");
|
|
299
|
+
let heartbeat = null;
|
|
300
|
+
if (autopilotLog.logFilePath) {
|
|
301
|
+
log.info({ file: autopilotLog.logFilePath }, `Logging to ${autopilotLog.logFilePath}`);
|
|
302
|
+
}
|
|
303
|
+
log.info({ cwd: opts.cwd, maxIterations, timeoutMs }, "Starting OpenCode server");
|
|
304
|
+
const server = await _startServer({ cwd: opts.cwd });
|
|
305
|
+
log.info({ url: server.url }, "Server ready");
|
|
306
|
+
const abort = new AbortController();
|
|
307
|
+
const timeoutHandle = setTimeout(() => {
|
|
308
|
+
abort.abort();
|
|
309
|
+
}, timeoutMs);
|
|
310
|
+
let sessionId;
|
|
311
|
+
try {
|
|
312
|
+
sessionId = await _createSession(server.client, {
|
|
313
|
+
cwd: opts.cwd,
|
|
314
|
+
agentName: "autopilot-prime"
|
|
315
|
+
});
|
|
316
|
+
log.info({ sessionId }, "Session created with autopilot-prime");
|
|
317
|
+
heartbeat = createStatusHeartbeat({
|
|
318
|
+
logger: statusLog,
|
|
319
|
+
intervalMs: STATUS_INTERVAL_MS,
|
|
320
|
+
pollCost: async () => getSessionCost(server.client, sessionId)
|
|
321
|
+
});
|
|
322
|
+
heartbeat.start();
|
|
323
|
+
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
324
|
+
if (checkKillSwitch(opts.cwd)) {
|
|
325
|
+
log.warn({ iteration: iteration - 1 }, "Kill switch active \u2014 stopping");
|
|
326
|
+
return {
|
|
327
|
+
exitReason: "kill-switch",
|
|
328
|
+
iterations: iteration - 1,
|
|
329
|
+
message: `Kill switch active (.agent/autopilot-disable exists). Stopping after ${iteration - 1} iteration(s).`,
|
|
330
|
+
sessionId
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
334
|
+
log.warn({ iteration: iteration - 1, timeoutMs }, "Total timeout exceeded");
|
|
335
|
+
return {
|
|
336
|
+
exitReason: "timeout",
|
|
337
|
+
iterations: iteration - 1,
|
|
338
|
+
message: `Total timeout (${timeoutMs}ms) exceeded after ${iteration - 1} iteration(s).`,
|
|
339
|
+
sessionId
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const headBefore = await getHeadSha(opts.cwd);
|
|
343
|
+
const iterationBaseCost = heartbeat.getState().cumulativeCostUsd;
|
|
344
|
+
const iterStart = Date.now();
|
|
345
|
+
log.debug({ iteration, maxIterations }, `Iteration ${iteration}/${maxIterations} \u2014 sending prompt`);
|
|
346
|
+
let streamDeltaCount = 0;
|
|
347
|
+
let streamCharCount = 0;
|
|
348
|
+
let lastStreamLogAt = 0;
|
|
349
|
+
let lastToolOrStreamLogAt = Date.now();
|
|
350
|
+
const DEBUG_STREAM_INTERVAL_MS = 15e3;
|
|
351
|
+
const INFO_STREAM_INTERVAL_MS = 6e4;
|
|
352
|
+
const result = await _sendAndWait(server.client, {
|
|
353
|
+
sessionId,
|
|
354
|
+
message: fullPrompt,
|
|
355
|
+
stallMs,
|
|
356
|
+
abortSignal: abort.signal,
|
|
357
|
+
// Autopilot is lights-out: auto-reject every permission prompt
|
|
358
|
+
// (question tool, edit gates, bash confirmations) so the agent
|
|
359
|
+
// can't deadlock waiting on a human response.
|
|
360
|
+
autoRejectPermissions: true,
|
|
361
|
+
serverUrl: server.url,
|
|
362
|
+
onPermissionRejected: (perm) => {
|
|
363
|
+
log.warn(
|
|
364
|
+
{ iteration, permissionId: perm.id, permissionType: perm.type, title: perm.title },
|
|
365
|
+
`Auto-rejected permission: ${perm.type} \u2014 "${perm.title}"`
|
|
366
|
+
);
|
|
367
|
+
},
|
|
368
|
+
onToolCall: (toolName) => {
|
|
369
|
+
toolLog.debug({ iteration, tool: toolName }, toolName);
|
|
370
|
+
lastToolOrStreamLogAt = Date.now();
|
|
371
|
+
streamDeltaCount = 0;
|
|
372
|
+
streamCharCount = 0;
|
|
373
|
+
lastStreamLogAt = Date.now();
|
|
374
|
+
},
|
|
375
|
+
onCostUpdate: (cost, tokens) => {
|
|
376
|
+
heartbeat.update({
|
|
377
|
+
cumulativeCostUsd: iterationBaseCost + cost
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
onTextDelta: (charCount) => {
|
|
381
|
+
streamDeltaCount += 1;
|
|
382
|
+
streamCharCount += charCount;
|
|
383
|
+
const now = Date.now();
|
|
384
|
+
if (now - lastStreamLogAt >= DEBUG_STREAM_INTERVAL_MS) {
|
|
385
|
+
streamLog.debug(
|
|
386
|
+
{ iteration, deltas: streamDeltaCount, chars: streamCharCount },
|
|
387
|
+
`streaming (${streamDeltaCount} deltas, ${streamCharCount} chars)`
|
|
388
|
+
);
|
|
389
|
+
lastStreamLogAt = now;
|
|
390
|
+
}
|
|
391
|
+
const silenceSinceLastTool = now - lastToolOrStreamLogAt;
|
|
392
|
+
if (silenceSinceLastTool >= INFO_STREAM_INTERVAL_MS) {
|
|
393
|
+
streamLog.info(
|
|
394
|
+
{
|
|
395
|
+
iteration,
|
|
396
|
+
deltas: streamDeltaCount,
|
|
397
|
+
chars: streamCharCount,
|
|
398
|
+
silenceMs: silenceSinceLastTool
|
|
399
|
+
},
|
|
400
|
+
`still streaming (${streamDeltaCount} deltas, ${streamCharCount} chars, ${Math.round(silenceSinceLastTool / 1e3)}s since last tool)`
|
|
401
|
+
);
|
|
402
|
+
lastToolOrStreamLogAt = now;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
const iterDurationMs = Date.now() - iterStart;
|
|
407
|
+
if (result.kind === "abort") {
|
|
408
|
+
log.warn({ iteration, iterDurationMs }, "Iteration aborted (total timeout)");
|
|
409
|
+
return {
|
|
410
|
+
exitReason: "timeout",
|
|
411
|
+
iterations: iteration,
|
|
412
|
+
message: `Aborted after ${iteration} iteration(s) (total timeout exceeded).`,
|
|
413
|
+
sessionId
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (result.kind === "stall") {
|
|
417
|
+
log.warn({ iteration, stallMs: result.stallMs }, "Iteration stalled");
|
|
418
|
+
return {
|
|
419
|
+
exitReason: "stall",
|
|
420
|
+
iterations: iteration,
|
|
421
|
+
message: `Iteration ${iteration} stalled for ${result.stallMs}ms with no idle signal.`,
|
|
422
|
+
sessionId
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (result.kind === "error") {
|
|
426
|
+
log.error({ iteration, err: result.message }, "Iteration errored");
|
|
427
|
+
heartbeat.update({
|
|
428
|
+
iterationsCompleted: iteration,
|
|
429
|
+
lastIterationErrored: true
|
|
430
|
+
});
|
|
431
|
+
return {
|
|
432
|
+
exitReason: "error",
|
|
433
|
+
iterations: iteration,
|
|
434
|
+
message: `Error in iteration ${iteration}: ${result.message}`,
|
|
435
|
+
sessionId
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (result.kind === "question_rejected") {
|
|
439
|
+
log.warn(
|
|
440
|
+
{ iteration, questionTitle: result.title },
|
|
441
|
+
`Question rejected \u2014 re-sending prompt with reminder (iteration ${iteration})`
|
|
442
|
+
);
|
|
443
|
+
const reminderResult = await _sendAndWait(server.client, {
|
|
444
|
+
sessionId,
|
|
445
|
+
message: fullPrompt + "\n\nIMPORTANT: Your previous attempt to use the question tool was rejected. The question tool is not available in autopilot mode. You must solve this without asking the user. Pick a sensible default, document the decision in the plan's ## Open questions, and continue working.",
|
|
446
|
+
stallMs,
|
|
447
|
+
abortSignal: abort.signal,
|
|
448
|
+
autoRejectPermissions: true,
|
|
449
|
+
serverUrl: server.url,
|
|
450
|
+
onPermissionRejected: (perm) => {
|
|
451
|
+
log.warn(
|
|
452
|
+
{ iteration, permissionId: perm.id, permissionType: perm.type },
|
|
453
|
+
`Auto-rejected permission on retry: ${perm.type}`
|
|
454
|
+
);
|
|
455
|
+
},
|
|
456
|
+
onToolCall: (toolName) => {
|
|
457
|
+
toolLog.debug({ iteration, tool: toolName }, toolName);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
if (reminderResult.kind === "question_rejected") {
|
|
461
|
+
log.error(
|
|
462
|
+
{ iteration },
|
|
463
|
+
"Agent invoked question tool twice in same iteration \u2014 giving up on this iteration"
|
|
464
|
+
);
|
|
465
|
+
} else if (reminderResult.kind !== "idle") {
|
|
466
|
+
log.warn(
|
|
467
|
+
{ iteration, kind: reminderResult.kind },
|
|
468
|
+
`Retry after question rejection returned ${reminderResult.kind}`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const lastMessage = await _getLastAssistantMessage(server.client, sessionId);
|
|
473
|
+
if (detectSentinel(lastMessage)) {
|
|
474
|
+
log.info({ iteration, iterDurationMs }, "Sentinel detected \u2014 autopilot done");
|
|
475
|
+
return {
|
|
476
|
+
exitReason: "sentinel",
|
|
477
|
+
iterations: iteration,
|
|
478
|
+
message: `Agent emitted <autopilot-done> at iteration ${iteration}.`,
|
|
479
|
+
sessionId
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const madeProgress = await checkProgress(opts.cwd, headBefore);
|
|
483
|
+
struggle.record(madeProgress);
|
|
484
|
+
const cumulativeCostUsd = await getSessionCost(server.client, sessionId);
|
|
485
|
+
const planPathMatch = opts.prompt.match(/plans\/([^/\s]+(?:\/[^/\s]+)?)/);
|
|
486
|
+
let planProgressPatch = {};
|
|
487
|
+
if (planPathMatch) {
|
|
488
|
+
try {
|
|
489
|
+
const planPath = planPathMatch[0];
|
|
490
|
+
const planState = parsePlanState(planPath);
|
|
491
|
+
if (planState.type === "multi") {
|
|
492
|
+
planProgressPatch = {
|
|
493
|
+
phaseCount: planState.phaseCount,
|
|
494
|
+
phasesCompleted: planState.phasesCompleted,
|
|
495
|
+
mainCheckboxesTotal: planState.totalItems,
|
|
496
|
+
mainCheckboxesCompleted: planState.checkedItems
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
} catch (err) {
|
|
500
|
+
log.debug({ err }, "plan-parser error \u2014 falling back to plan-blind heartbeat");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
heartbeat.update({
|
|
504
|
+
iterationsCompleted: iteration,
|
|
505
|
+
cumulativeCostUsd,
|
|
506
|
+
lastIterationProgress: madeProgress,
|
|
507
|
+
lastIterationErrored: false,
|
|
508
|
+
...planProgressPatch
|
|
509
|
+
});
|
|
510
|
+
log.debug(
|
|
511
|
+
{ iteration, iterDurationMs, madeProgress, cumulativeCostUsd },
|
|
512
|
+
`Iteration ${iteration} idle (${(iterDurationMs / 1e3).toFixed(1)}s, ${madeProgress ? "progress" : "no progress"})`
|
|
513
|
+
);
|
|
514
|
+
if (struggle.isStruggling()) {
|
|
515
|
+
log.warn({ iteration, struggleThreshold }, "Struggle detected \u2014 stopping");
|
|
516
|
+
return {
|
|
517
|
+
exitReason: "struggle",
|
|
518
|
+
iterations: iteration,
|
|
519
|
+
message: `Agent made no filesystem progress for ${struggleThreshold} consecutive iteration(s). Stopping at iteration ${iteration}.`,
|
|
520
|
+
sessionId
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
log.warn({ maxIterations }, "Reached max iterations");
|
|
525
|
+
return {
|
|
526
|
+
exitReason: "max-iterations",
|
|
527
|
+
iterations: maxIterations,
|
|
528
|
+
message: `Reached maximum iterations (${maxIterations}). Stopping.`,
|
|
529
|
+
sessionId
|
|
530
|
+
};
|
|
531
|
+
} finally {
|
|
532
|
+
clearTimeout(timeoutHandle);
|
|
533
|
+
heartbeat?.stop();
|
|
534
|
+
log.info({}, "Shutting down server");
|
|
535
|
+
await server.shutdown();
|
|
536
|
+
await autopilotLog.flush();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export {
|
|
541
|
+
MAX_ITERATIONS,
|
|
542
|
+
TIMEOUT_MS,
|
|
543
|
+
runRalphLoop
|
|
544
|
+
};
|
|
@@ -196,10 +196,62 @@ function readOurPackageVersion(fromFileUrl) {
|
|
|
196
196
|
return "0.0.0";
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
// src/model-validator.ts
|
|
200
|
+
var CATWALK_PROVIDER_PATTERN = /^(?:bedrock|vertex|vertexai)\//;
|
|
201
|
+
var LEGACY_PRE_100_PATTERN = /^(bedrock|vertex|vertexai)\/claude-(opus|sonnet|haiku)(-\d+)?$/;
|
|
202
|
+
var LEGACY_TO_MODELS_DEV = {
|
|
203
|
+
// --- Pre-PR-#100 Bedrock (no subpath) ---
|
|
204
|
+
"bedrock/claude-opus": "amazon-bedrock/global.anthropic.claude-opus-4-7",
|
|
205
|
+
"bedrock/claude-opus-4": "amazon-bedrock/global.anthropic.claude-opus-4-7",
|
|
206
|
+
"bedrock/claude-sonnet": "amazon-bedrock/global.anthropic.claude-sonnet-4-6",
|
|
207
|
+
"bedrock/claude-sonnet-4": "amazon-bedrock/global.anthropic.claude-sonnet-4-6",
|
|
208
|
+
"bedrock/claude-haiku": "amazon-bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
209
|
+
"bedrock/claude-haiku-4": "amazon-bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
210
|
+
// --- Pre-Models.dev Bedrock (had subpath, but wrong provider prefix) ---
|
|
211
|
+
"bedrock/anthropic.claude-opus-4-6": "amazon-bedrock/global.anthropic.claude-opus-4-7",
|
|
212
|
+
"bedrock/anthropic.claude-opus-4-7": "amazon-bedrock/global.anthropic.claude-opus-4-7",
|
|
213
|
+
"bedrock/anthropic.claude-sonnet-4-6": "amazon-bedrock/global.anthropic.claude-sonnet-4-6",
|
|
214
|
+
"bedrock/anthropic.claude-haiku-4-5-20251001-v1:0": "amazon-bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
215
|
+
// --- Pre-PR-#100 Vertex (no @date suffix) ---
|
|
216
|
+
"vertex/claude-opus": "google-vertex-anthropic/claude-opus-4-7@default",
|
|
217
|
+
"vertex/claude-opus-4": "google-vertex-anthropic/claude-opus-4-7@default",
|
|
218
|
+
"vertex/claude-sonnet": "google-vertex-anthropic/claude-sonnet-4-6@default",
|
|
219
|
+
"vertex/claude-sonnet-4": "google-vertex-anthropic/claude-sonnet-4-6@default",
|
|
220
|
+
"vertex/claude-haiku": "google-vertex-anthropic/claude-haiku-4-5@20251001",
|
|
221
|
+
"vertex/claude-haiku-4": "google-vertex-anthropic/claude-haiku-4-5@20251001",
|
|
222
|
+
"vertexai/claude-opus": "google-vertex-anthropic/claude-opus-4-7@default",
|
|
223
|
+
"vertexai/claude-opus-4": "google-vertex-anthropic/claude-opus-4-7@default",
|
|
224
|
+
"vertexai/claude-sonnet": "google-vertex-anthropic/claude-sonnet-4-6@default",
|
|
225
|
+
"vertexai/claude-sonnet-4": "google-vertex-anthropic/claude-sonnet-4-6@default",
|
|
226
|
+
"vertexai/claude-haiku": "google-vertex-anthropic/claude-haiku-4-5@20251001",
|
|
227
|
+
"vertexai/claude-haiku-4": "google-vertex-anthropic/claude-haiku-4-5@20251001",
|
|
228
|
+
// --- Pre-Models.dev Vertex (had @date suffix, wrong provider prefix) ---
|
|
229
|
+
"vertexai/claude-opus-4-6@20250610": "google-vertex-anthropic/claude-opus-4-6@default",
|
|
230
|
+
"vertexai/claude-opus-4-7@20250610": "google-vertex-anthropic/claude-opus-4-7@default",
|
|
231
|
+
"vertexai/claude-sonnet-4-6@20250725": "google-vertex-anthropic/claude-sonnet-4-6@default",
|
|
232
|
+
"vertexai/claude-haiku-4-5@20251001": "google-vertex-anthropic/claude-haiku-4-5@20251001"
|
|
233
|
+
};
|
|
234
|
+
function validateModelOverride(id) {
|
|
235
|
+
if (typeof id !== "string") return { valid: true };
|
|
236
|
+
if (id.length === 0) return { valid: true };
|
|
237
|
+
if (CATWALK_PROVIDER_PATTERN.test(id)) {
|
|
238
|
+
const suggestion = LEGACY_TO_MODELS_DEV[id] ?? "run `bunx @glrs-dev/harness-plugin-opencode install` to pick a current preset";
|
|
239
|
+
const reason = LEGACY_PRE_100_PATTERN.test(id) ? `"${id}" is a pre-PR-#100 model ID format that does not resolve in OpenCode. Bedrock IDs need the \`amazon-bedrock\` provider prefix (not \`bedrock\`); Vertex Claude IDs need the \`google-vertex-anthropic\` provider prefix (not \`vertex\` / \`vertexai\`).` : `"${id}" uses a provider prefix (\`${id.split("/")[0]}\`) that does not exist in OpenCode's runtime. AWS Bedrock's provider ID is \`amazon-bedrock\`; Vertex Claude's is \`google-vertex-anthropic\`.`;
|
|
240
|
+
return { valid: false, reason, suggestion };
|
|
241
|
+
}
|
|
242
|
+
return { valid: true };
|
|
243
|
+
}
|
|
244
|
+
function formatModelOverrideWarning(id, source, suggestion) {
|
|
245
|
+
const suggestionText = suggestion ? ` Suggested replacement: \`${suggestion}\`.` : "";
|
|
246
|
+
return `[@glrs-dev/harness-plugin-opencode] Warning: invalid model override "${id}" (from ${source}).${suggestionText} Run \`bunx @glrs-dev/harness-plugin-opencode doctor\` for details.`;
|
|
247
|
+
}
|
|
248
|
+
|
|
199
249
|
export {
|
|
200
250
|
PACKAGE_NAME,
|
|
201
251
|
getOpenCodeCachePackageDir,
|
|
202
252
|
inspectCachePin,
|
|
203
253
|
refreshPluginCache,
|
|
204
|
-
readOurPackageVersion
|
|
254
|
+
readOurPackageVersion,
|
|
255
|
+
validateModelOverride,
|
|
256
|
+
formatModelOverrideWarning
|
|
205
257
|
};
|