@glrs-dev/cli 2.1.0 → 2.2.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 +2 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +18 -4
- package/dist/vendor/harness-opencode/dist/agents/prompts/build.open.md +18 -4
- package/dist/vendor/harness-opencode/dist/agents/prompts/{qa-thorough.md → code-reviewer-thorough.md} +34 -19
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer.md +80 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer.open.md +68 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/gap-analyzer.md +2 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +3 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +23 -4
- package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +146 -87
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-auto.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-local.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/research-web.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +2 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +54 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +57 -0
- package/dist/vendor/harness-opencode/dist/agents/shared/index.ts +1 -0
- package/dist/vendor/harness-opencode/dist/agents/shared/ui-evaluation-ladder.md +50 -0
- package/dist/vendor/harness-opencode/dist/agents/shared/workflow-mechanics.md +5 -5
- package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +80 -0
- package/dist/vendor/harness-opencode/dist/{chunk-VJUETC6A.js → chunk-PDMXYZM4.js} +53 -1
- package/dist/vendor/harness-opencode/dist/cli.js +1333 -1646
- package/dist/vendor/harness-opencode/dist/commands/prompts/fresh.md +27 -24
- package/dist/vendor/harness-opencode/dist/commands/prompts/review.md +3 -3
- package/dist/vendor/harness-opencode/dist/commands/prompts/ship.md +2 -0
- package/dist/vendor/harness-opencode/dist/index.js +106 -627
- package/dist/vendor/harness-opencode/dist/skills/adversarial-review-rubric/SKILL.md +47 -0
- package/dist/vendor/harness-opencode/dist/skills/code-quality/SKILL.md +1 -1
- package/dist/vendor/harness-opencode/dist/skills/root-cause-diagnosis/SKILL.md +24 -0
- package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +166 -0
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-assessor.md +0 -77
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +0 -40
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +0 -56
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-scoper.md +0 -58
- package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.md +0 -68
- package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.open.md +0 -58
- package/dist/vendor/harness-opencode/dist/chunk-6CZPRUMJ.js +0 -869
- package/dist/vendor/harness-opencode/dist/chunk-DZG4D3OH.js +0 -54
- package/dist/vendor/harness-opencode/dist/chunk-OYRKOEXK.js +0 -88
- package/dist/vendor/harness-opencode/dist/commands/prompts/autopilot.md +0 -96
- package/dist/vendor/harness-opencode/dist/install-6775ZBDG.js +0 -13
- package/dist/vendor/harness-opencode/dist/paths-WZ23ZQOV.js +0 -18
|
@@ -1,65 +1,1037 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import {
|
|
3
|
+
getOpenCodeCachePackageDir,
|
|
4
|
+
inspectCachePin,
|
|
5
|
+
readOurPackageVersion,
|
|
6
|
+
refreshPluginCache,
|
|
3
7
|
validateModelOverride
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
install
|
|
7
|
-
} from "./chunk-6CZPRUMJ.js";
|
|
8
|
-
import "./chunk-VJUETC6A.js";
|
|
9
|
-
import {
|
|
10
|
-
getCurrentScopePath,
|
|
11
|
-
getPilotConfigPath,
|
|
12
|
-
getPilotDir,
|
|
13
|
-
getPlanArtifactPath,
|
|
14
|
-
getScopeArtifactPath,
|
|
15
|
-
getStateDbPath
|
|
16
|
-
} from "./chunk-OYRKOEXK.js";
|
|
8
|
+
} from "./chunk-PDMXYZM4.js";
|
|
17
9
|
|
|
18
10
|
// src/cli.ts
|
|
19
11
|
import {
|
|
20
12
|
binary,
|
|
21
|
-
command as
|
|
22
|
-
flag
|
|
23
|
-
option as
|
|
24
|
-
optional as
|
|
25
|
-
positional,
|
|
26
|
-
restPositionals
|
|
27
|
-
string
|
|
28
|
-
subcommands
|
|
13
|
+
command as command2,
|
|
14
|
+
flag,
|
|
15
|
+
option as option2,
|
|
16
|
+
optional as optional2,
|
|
17
|
+
positional as positional2,
|
|
18
|
+
restPositionals,
|
|
19
|
+
string,
|
|
20
|
+
subcommands,
|
|
29
21
|
run
|
|
30
22
|
} from "cmd-ts";
|
|
31
23
|
|
|
32
|
-
// src/cli/
|
|
24
|
+
// src/cli/install.ts
|
|
25
|
+
import * as fs3 from "fs";
|
|
26
|
+
import * as path3 from "path";
|
|
27
|
+
import * as os2 from "os";
|
|
28
|
+
import { fileURLToPath } from "url";
|
|
29
|
+
|
|
30
|
+
// src/cli/merge-config.ts
|
|
33
31
|
import * as fs from "fs";
|
|
34
32
|
import * as path from "path";
|
|
33
|
+
var UNION_ALLOWLIST = /* @__PURE__ */ new Set(["plugin"]);
|
|
34
|
+
function isPlainObject(v) {
|
|
35
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.prototype.toString.call(v) === "[object Object]";
|
|
36
|
+
}
|
|
37
|
+
function deepClone(v) {
|
|
38
|
+
if (v === null || typeof v !== "object") return v;
|
|
39
|
+
if (Array.isArray(v)) return v.map(deepClone);
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const k of Object.keys(v)) {
|
|
42
|
+
out[k] = deepClone(v[k]);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function fmtPath(parts) {
|
|
47
|
+
return parts.map((p) => /^[A-Za-z_$][\w$]*$/.test(p) ? p : `["${p.replace(/"/g, '\\"')}"]`).reduce((acc, part) => {
|
|
48
|
+
if (acc === "") return part;
|
|
49
|
+
if (part.startsWith("[")) return acc + part;
|
|
50
|
+
return acc + "." + part;
|
|
51
|
+
}, "");
|
|
52
|
+
}
|
|
53
|
+
function pluginName(entry) {
|
|
54
|
+
if (typeof entry === "string") {
|
|
55
|
+
const atIdx = entry.indexOf("@", 1);
|
|
56
|
+
return atIdx > 0 ? entry.slice(0, atIdx) : entry;
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(entry) && typeof entry[0] === "string") {
|
|
59
|
+
const name = entry[0];
|
|
60
|
+
const atIdx = name.indexOf("@", 1);
|
|
61
|
+
return atIdx > 0 ? name.slice(0, atIdx) : name;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function mergeWalk(src, dst, pathParts, additions, warnings) {
|
|
66
|
+
for (const key of Object.keys(src)) {
|
|
67
|
+
const sv = src[key];
|
|
68
|
+
const newPath = pathParts.concat([key]);
|
|
69
|
+
const pathStr = fmtPath(newPath);
|
|
70
|
+
if (!Object.prototype.hasOwnProperty.call(dst, key)) {
|
|
71
|
+
dst[key] = deepClone(sv);
|
|
72
|
+
additions.push(`added: ${pathStr}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const dv = dst[key];
|
|
76
|
+
if (isPlainObject(sv) && isPlainObject(dv)) {
|
|
77
|
+
mergeWalk(sv, dv, newPath, additions, warnings);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (isPlainObject(sv) && !isPlainObject(dv)) {
|
|
81
|
+
warnings.push(
|
|
82
|
+
`WARN: scalar-vs-object: user has non-object at ${pathStr} where we ship an object; not migrating. To adopt: ${JSON.stringify(sv)}`
|
|
83
|
+
);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(sv)) {
|
|
87
|
+
if (!Array.isArray(dv)) {
|
|
88
|
+
warnings.push(
|
|
89
|
+
`WARN: scalar-vs-array: user has non-array at ${pathStr} where we ship an array; not migrating. To adopt: ${JSON.stringify(sv)}`
|
|
90
|
+
);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const joined = newPath.join(".");
|
|
94
|
+
if (UNION_ALLOWLIST.has(joined)) {
|
|
95
|
+
for (const item of sv) {
|
|
96
|
+
const srcName = pluginName(item);
|
|
97
|
+
if (srcName) {
|
|
98
|
+
const dstIdx = dv.findIndex(
|
|
99
|
+
(x) => pluginName(x) === srcName
|
|
100
|
+
);
|
|
101
|
+
if (dstIdx >= 0) {
|
|
102
|
+
const srcIsTuple = Array.isArray(item) && item.length >= 2;
|
|
103
|
+
const dstIsTuple = Array.isArray(dv[dstIdx]) && dv[dstIdx].length >= 2;
|
|
104
|
+
if (srcIsTuple && !dstIsTuple) {
|
|
105
|
+
dv[dstIdx] = deepClone(item);
|
|
106
|
+
additions.push(`upgraded: ${pathStr}[${JSON.stringify(srcName)}] to tuple form`);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
dv.push(deepClone(item));
|
|
110
|
+
additions.push(`appended: ${pathStr}[${JSON.stringify(item)}]`);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
const needle = JSON.stringify(item);
|
|
114
|
+
const alreadyPresent = dv.some(
|
|
115
|
+
(x) => JSON.stringify(x) === needle
|
|
116
|
+
);
|
|
117
|
+
if (!alreadyPresent) {
|
|
118
|
+
dv.push(deepClone(item));
|
|
119
|
+
additions.push(`appended: ${pathStr}[${JSON.stringify(item)}]`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function mergeConfig(srcJson, dstPath, dryRun = false) {
|
|
129
|
+
let dstText;
|
|
130
|
+
try {
|
|
131
|
+
dstText = fs.readFileSync(dstPath, "utf8");
|
|
132
|
+
} catch (e) {
|
|
133
|
+
throw new Error(`Failed to read dst ${dstPath}: ${e.message}`);
|
|
134
|
+
}
|
|
135
|
+
let dst;
|
|
136
|
+
try {
|
|
137
|
+
dst = JSON.parse(dstText);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`User config at ${dstPath} has invalid JSON: ${e.message}. Not touching the file.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!isPlainObject(dst)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`User config at ${dstPath} is not a JSON object at the top level.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const additions = [];
|
|
149
|
+
const warnings = [];
|
|
150
|
+
mergeWalk(srcJson, dst, [], additions, warnings);
|
|
151
|
+
if (additions.length === 0) {
|
|
152
|
+
return { changed: false, warnings };
|
|
153
|
+
}
|
|
154
|
+
if (dryRun) {
|
|
155
|
+
return { changed: true, bakPath: "(dry-run)", additions, warnings };
|
|
156
|
+
}
|
|
157
|
+
const suffix = `${Date.now()}-${process.pid}`;
|
|
158
|
+
const bakPath = `${dstPath}.bak.${suffix}`;
|
|
159
|
+
const tmpPath = `${dstPath}.merge.tmp.${suffix}`;
|
|
160
|
+
try {
|
|
161
|
+
fs.copyFileSync(dstPath, bakPath);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
throw new Error(`Failed to write backup ${bakPath}: ${e.message}`);
|
|
164
|
+
}
|
|
165
|
+
const serialized = JSON.stringify(dst, null, 2) + "\n";
|
|
166
|
+
try {
|
|
167
|
+
fs.writeFileSync(tmpPath, serialized);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
try {
|
|
170
|
+
fs.unlinkSync(bakPath);
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
throw new Error(`Failed to write tempfile ${tmpPath}: ${e.message}`);
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
fs.renameSync(tmpPath, dstPath);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
try {
|
|
179
|
+
fs.unlinkSync(tmpPath);
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
fs.unlinkSync(bakPath);
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`Failed to rename ${tmpPath} \u2192 ${dstPath}: ${e.message}`);
|
|
187
|
+
}
|
|
188
|
+
return { changed: true, bakPath, additions, warnings };
|
|
189
|
+
}
|
|
190
|
+
function seedConfig(srcJson, dstPath) {
|
|
191
|
+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
|
|
192
|
+
fs.writeFileSync(dstPath, JSON.stringify(srcJson, null, 2) + "\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/cli/plugin-check.ts
|
|
196
|
+
import * as fs2 from "fs";
|
|
197
|
+
import * as path2 from "path";
|
|
35
198
|
import * as os from "os";
|
|
199
|
+
import { select, checkbox, confirm } from "@inquirer/prompts";
|
|
200
|
+
async function promptChoice(question, choices, defaultIndex = 0) {
|
|
201
|
+
if (!process.stdin.isTTY) return defaultIndex;
|
|
202
|
+
const answer = await select({
|
|
203
|
+
message: question,
|
|
204
|
+
choices: choices.map((label, i) => ({
|
|
205
|
+
name: label,
|
|
206
|
+
value: i
|
|
207
|
+
})),
|
|
208
|
+
default: defaultIndex
|
|
209
|
+
});
|
|
210
|
+
return answer;
|
|
211
|
+
}
|
|
212
|
+
async function promptMulti(question, choices) {
|
|
213
|
+
if (!process.stdin.isTTY) {
|
|
214
|
+
const defaults = /* @__PURE__ */ new Set();
|
|
215
|
+
choices.forEach((c3, i) => {
|
|
216
|
+
if (c3.defaultOn) defaults.add(i);
|
|
217
|
+
});
|
|
218
|
+
return defaults;
|
|
219
|
+
}
|
|
220
|
+
const answers = await checkbox({
|
|
221
|
+
message: question,
|
|
222
|
+
choices: choices.map((c3, i) => ({
|
|
223
|
+
name: c3.label,
|
|
224
|
+
value: i,
|
|
225
|
+
checked: c3.defaultOn
|
|
226
|
+
}))
|
|
227
|
+
});
|
|
228
|
+
return new Set(answers);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/cli/models-dev.ts
|
|
232
|
+
var MODELS_DEV_URL = "https://models.dev/api.json";
|
|
233
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
234
|
+
function combinedCost(m) {
|
|
235
|
+
const input = m.cost?.input ?? 0;
|
|
236
|
+
const output = m.cost?.output ?? 0;
|
|
237
|
+
return input + output;
|
|
238
|
+
}
|
|
239
|
+
function suggestTiersFromModelsDev(provider) {
|
|
240
|
+
const models = Object.values(provider.models).sort(
|
|
241
|
+
(a, b) => combinedCost(b) - combinedCost(a)
|
|
242
|
+
);
|
|
243
|
+
if (models.length === 0) {
|
|
244
|
+
throw new Error(`Provider "${provider.id}" has no models`);
|
|
245
|
+
}
|
|
246
|
+
const deep = models[0];
|
|
247
|
+
const fast = models[models.length - 1];
|
|
248
|
+
let mid;
|
|
249
|
+
if (models.length <= 2) {
|
|
250
|
+
mid = models.length === 1 ? deep : fast;
|
|
251
|
+
} else {
|
|
252
|
+
const midCost = (combinedCost(deep) + combinedCost(fast)) / 2;
|
|
253
|
+
const candidates = models.filter(
|
|
254
|
+
(m) => m.id !== deep.id && m.id !== fast.id
|
|
255
|
+
);
|
|
256
|
+
mid = candidates.reduce(
|
|
257
|
+
(best, m) => Math.abs(combinedCost(m) - midCost) < Math.abs(combinedCost(best) - midCost) ? m : best
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const ref = (m) => `${provider.id}/${m.id}`;
|
|
261
|
+
return {
|
|
262
|
+
deep: ref(deep),
|
|
263
|
+
mid: ref(mid),
|
|
264
|
+
fast: ref(fast)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function pickBedrockTierIds(provider) {
|
|
268
|
+
const models = Object.values(provider.models);
|
|
269
|
+
const mostRecent = (candidates) => {
|
|
270
|
+
if (candidates.length === 0) return null;
|
|
271
|
+
return [...candidates].sort((a, b) => {
|
|
272
|
+
const aDate = a.last_updated ?? "";
|
|
273
|
+
const bDate = b.last_updated ?? "";
|
|
274
|
+
if (aDate !== bDate) return bDate.localeCompare(aDate);
|
|
275
|
+
return b.id.localeCompare(a.id);
|
|
276
|
+
})[0];
|
|
277
|
+
};
|
|
278
|
+
const pickFamily = (familyKeyword) => {
|
|
279
|
+
const globalCandidates = models.filter(
|
|
280
|
+
(m) => m.id.startsWith(`global.anthropic.claude-${familyKeyword}-`)
|
|
281
|
+
);
|
|
282
|
+
const globalPick = mostRecent(globalCandidates);
|
|
283
|
+
if (globalPick) return globalPick;
|
|
284
|
+
const nonPrefixedCandidates = models.filter(
|
|
285
|
+
(m) => m.id.startsWith(`anthropic.claude-${familyKeyword}-`)
|
|
286
|
+
);
|
|
287
|
+
return mostRecent(nonPrefixedCandidates);
|
|
288
|
+
};
|
|
289
|
+
const opus = pickFamily("opus");
|
|
290
|
+
const sonnet = pickFamily("sonnet");
|
|
291
|
+
const haiku = pickFamily("haiku");
|
|
292
|
+
if (!opus || !sonnet || !haiku) {
|
|
293
|
+
return suggestTiersFromModelsDev(provider);
|
|
294
|
+
}
|
|
295
|
+
const ref = (m) => `${provider.id}/${m.id}`;
|
|
296
|
+
return {
|
|
297
|
+
deep: ref(opus),
|
|
298
|
+
mid: ref(sonnet),
|
|
299
|
+
fast: ref(haiku)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function fetchModelsDevProviders() {
|
|
303
|
+
const controller = new AbortController();
|
|
304
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
305
|
+
try {
|
|
306
|
+
const res = await fetch(MODELS_DEV_URL, { signal: controller.signal });
|
|
307
|
+
if (!res.ok) return null;
|
|
308
|
+
const data = await res.json();
|
|
309
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
310
|
+
const providers = [];
|
|
311
|
+
for (const [key, rawValue] of Object.entries(
|
|
312
|
+
data
|
|
313
|
+
)) {
|
|
314
|
+
if (!rawValue || typeof rawValue !== "object") continue;
|
|
315
|
+
const value = rawValue;
|
|
316
|
+
if (typeof value.id !== "string" || value.id !== key) continue;
|
|
317
|
+
if (typeof value.name !== "string") continue;
|
|
318
|
+
if (!value.models || typeof value.models !== "object") continue;
|
|
319
|
+
providers.push(value);
|
|
320
|
+
}
|
|
321
|
+
if (providers.length === 0) return null;
|
|
322
|
+
return providers;
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
} finally {
|
|
326
|
+
clearTimeout(timer);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/cli/install.ts
|
|
36
331
|
var PLUGIN_NAME = "@glrs-dev/harness-plugin-opencode";
|
|
332
|
+
var c = {
|
|
333
|
+
reset: "\x1B[0m",
|
|
334
|
+
green: "\x1B[32m",
|
|
335
|
+
yellow: "\x1B[33m",
|
|
336
|
+
blue: "\x1B[34m",
|
|
337
|
+
dim: "\x1B[2m",
|
|
338
|
+
bold: "\x1B[1m"
|
|
339
|
+
};
|
|
340
|
+
var ok = (msg) => console.log(`${c.green}\u2713${c.reset} ${msg}`);
|
|
341
|
+
var info = (msg) => console.log(`${c.blue}\u2022${c.reset} ${msg}`);
|
|
342
|
+
var warn = (msg) => console.log(`${c.yellow}!${c.reset} ${msg}`);
|
|
343
|
+
var MODEL_PRESETS = [
|
|
344
|
+
{
|
|
345
|
+
label: "Anthropic API (direct)",
|
|
346
|
+
providerId: "anthropic",
|
|
347
|
+
deep: "anthropic/claude-opus-4-7",
|
|
348
|
+
mid: "anthropic/claude-sonnet-4-6",
|
|
349
|
+
fast: "anthropic/claude-haiku-4-5-20251001"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
label: "AWS Bedrock",
|
|
353
|
+
providerId: "amazon-bedrock",
|
|
354
|
+
deep: "amazon-bedrock/global.anthropic.claude-opus-4-7",
|
|
355
|
+
mid: "amazon-bedrock/global.anthropic.claude-sonnet-4-6",
|
|
356
|
+
fast: "amazon-bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
label: "Google Vertex AI (Claude)",
|
|
360
|
+
providerId: "google-vertex-anthropic",
|
|
361
|
+
deep: "google-vertex-anthropic/claude-opus-4-7@default",
|
|
362
|
+
mid: "google-vertex-anthropic/claude-sonnet-4-6@default",
|
|
363
|
+
fast: "google-vertex-anthropic/claude-haiku-4-5@20251001"
|
|
364
|
+
}
|
|
365
|
+
];
|
|
366
|
+
var MCP_TOGGLES = [
|
|
367
|
+
{ name: "playwright", label: "Playwright \u2014 browser automation + visual UI verification (requires Chromium)", defaultOn: false },
|
|
368
|
+
{ name: "linear", label: "Linear \u2014 issue tracker integration", defaultOn: false }
|
|
369
|
+
];
|
|
370
|
+
var PLUGIN_TOGGLES = [
|
|
371
|
+
{
|
|
372
|
+
name: "opencode-snip",
|
|
373
|
+
label: "Token reduction \u2014 opencode-snip (requires Go snip binary)",
|
|
374
|
+
defaultOn: false
|
|
375
|
+
}
|
|
376
|
+
];
|
|
377
|
+
function extractPluginOptions(config) {
|
|
378
|
+
if (!config) return null;
|
|
379
|
+
const plugins = config.plugin;
|
|
380
|
+
if (!Array.isArray(plugins)) return null;
|
|
381
|
+
for (const entry of plugins) {
|
|
382
|
+
if (Array.isArray(entry) && entry.length >= 2 && (entry[0] === PLUGIN_NAME || String(entry[0]).startsWith(`${PLUGIN_NAME}@`))) {
|
|
383
|
+
return entry[1];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
function readPackageVersion() {
|
|
389
|
+
const here = path3.dirname(fileURLToPath(import.meta.url));
|
|
390
|
+
const candidates = [
|
|
391
|
+
path3.join(here, "..", "package.json"),
|
|
392
|
+
path3.join(here, "..", "..", "package.json")
|
|
393
|
+
];
|
|
394
|
+
for (const candidate of candidates) {
|
|
395
|
+
try {
|
|
396
|
+
const raw = fs3.readFileSync(candidate, "utf8");
|
|
397
|
+
const parsed = JSON.parse(raw);
|
|
398
|
+
if (parsed.name === PLUGIN_NAME && typeof parsed.version === "string") {
|
|
399
|
+
return parsed.version;
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Could not locate ${PLUGIN_NAME}'s package.json to read version`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
37
408
|
function getOpencodeConfigPath() {
|
|
38
|
-
const configHome = process.env["XDG_CONFIG_HOME"] ??
|
|
39
|
-
return
|
|
409
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? path3.join(os2.homedir(), ".config");
|
|
410
|
+
return path3.join(configHome, "opencode", "opencode.json");
|
|
411
|
+
}
|
|
412
|
+
async function refreshPluginCacheIfStale() {
|
|
413
|
+
try {
|
|
414
|
+
const cacheDir = getOpenCodeCachePackageDir();
|
|
415
|
+
const pin = await inspectCachePin(cacheDir);
|
|
416
|
+
if (pin.kind !== "exact") return;
|
|
417
|
+
const ourVersion = readOurPackageVersion(import.meta.url);
|
|
418
|
+
if (pin.version === ourVersion) return;
|
|
419
|
+
const result = await refreshPluginCache(pin.version, ourVersion);
|
|
420
|
+
if (result.outcome === "refreshed") {
|
|
421
|
+
ok(`Plugin cache updated: ${result.fromVersion} \u2192 ${result.toVersion}`);
|
|
422
|
+
}
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function readExistingConfig(configPath) {
|
|
427
|
+
if (!fs3.existsSync(configPath)) return null;
|
|
428
|
+
try {
|
|
429
|
+
return JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
430
|
+
} catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function detectModelProvider(existing) {
|
|
435
|
+
const opts = extractPluginOptions(existing);
|
|
436
|
+
const models = opts?.models ?? existing?.harness?.models;
|
|
437
|
+
if (!models) return null;
|
|
438
|
+
const deep = Array.isArray(models.deep) ? models.deep[0] : models.deep;
|
|
439
|
+
if (typeof deep !== "string") return null;
|
|
440
|
+
for (const preset of MODEL_PRESETS) {
|
|
441
|
+
if (deep === preset.deep) return preset.label;
|
|
442
|
+
}
|
|
443
|
+
return `custom (${deep})`;
|
|
444
|
+
}
|
|
445
|
+
function detectEnabledMcps(existing) {
|
|
446
|
+
const enabled = /* @__PURE__ */ new Set();
|
|
447
|
+
const mcp = existing?.mcp;
|
|
448
|
+
if (!mcp || typeof mcp !== "object") return enabled;
|
|
449
|
+
for (const toggle of MCP_TOGGLES) {
|
|
450
|
+
if (mcp[toggle.name]?.enabled === true) {
|
|
451
|
+
enabled.add(toggle.name);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return enabled;
|
|
455
|
+
}
|
|
456
|
+
function detectEnabledPluginToggles(existing) {
|
|
457
|
+
const enabled = /* @__PURE__ */ new Set();
|
|
458
|
+
const plugins = Array.isArray(existing?.plugin) ? existing.plugin : [];
|
|
459
|
+
const toggleNames = new Set(PLUGIN_TOGGLES.map((t) => t.name));
|
|
460
|
+
for (const entry of plugins) {
|
|
461
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
462
|
+
if (typeof name === "string" && toggleNames.has(name)) {
|
|
463
|
+
enabled.add(name);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return enabled;
|
|
467
|
+
}
|
|
468
|
+
function migrateHarnessKeyToPluginOptions(configPath) {
|
|
469
|
+
try {
|
|
470
|
+
if (!fs3.existsSync(configPath)) return;
|
|
471
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
472
|
+
const config = JSON.parse(raw);
|
|
473
|
+
if (!config.harness || typeof config.harness !== "object") return;
|
|
474
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
475
|
+
const pluginIdx = plugins.findIndex((entry) => {
|
|
476
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
477
|
+
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
|
|
478
|
+
});
|
|
479
|
+
if (pluginIdx < 0) return;
|
|
480
|
+
const current = plugins[pluginIdx];
|
|
481
|
+
const existingName = typeof current === "string" ? current : Array.isArray(current) ? current[0] : PLUGIN_NAME;
|
|
482
|
+
const existingOpts = Array.isArray(current) && current.length >= 2 ? current[1] : {};
|
|
483
|
+
const merged = { ...config.harness, ...existingOpts };
|
|
484
|
+
plugins[pluginIdx] = [existingName, merged];
|
|
485
|
+
delete config.harness;
|
|
486
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
487
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
488
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
489
|
+
ok("Migrated legacy `harness` config into plugin options");
|
|
490
|
+
info(`Backup: ${bakPath}`);
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function deepEqual(a, b) {
|
|
495
|
+
if (a === b) return true;
|
|
496
|
+
if (typeof a !== typeof b) return false;
|
|
497
|
+
if (a === null || b === null) return a === b;
|
|
498
|
+
if (typeof a !== "object") return false;
|
|
499
|
+
const aObj = a;
|
|
500
|
+
const bObj = b;
|
|
501
|
+
const aKeys = Object.keys(aObj);
|
|
502
|
+
const bKeys = Object.keys(bObj);
|
|
503
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
504
|
+
for (const key of aKeys) {
|
|
505
|
+
if (!bKeys.includes(key)) return false;
|
|
506
|
+
if (!deepEqual(aObj[key], bObj[key])) return false;
|
|
507
|
+
}
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
function writePluginOption(configPath, subKey, value, opts) {
|
|
511
|
+
try {
|
|
512
|
+
if (!fs3.existsSync(configPath)) {
|
|
513
|
+
return { changed: false };
|
|
514
|
+
}
|
|
515
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
516
|
+
const config = JSON.parse(raw);
|
|
517
|
+
if (!Array.isArray(config.plugin)) {
|
|
518
|
+
return { changed: false };
|
|
519
|
+
}
|
|
520
|
+
const pluginIdx = config.plugin.findIndex((entry) => {
|
|
521
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
522
|
+
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
|
|
523
|
+
});
|
|
524
|
+
if (pluginIdx < 0) {
|
|
525
|
+
return { changed: false };
|
|
526
|
+
}
|
|
527
|
+
const current = config.plugin[pluginIdx];
|
|
528
|
+
const existingName = typeof current === "string" ? current : Array.isArray(current) ? current[0] : PLUGIN_NAME;
|
|
529
|
+
const existingOpts = Array.isArray(current) && current.length >= 2 ? current[1] : {};
|
|
530
|
+
if (deepEqual(existingOpts[subKey], value)) {
|
|
531
|
+
return { changed: false };
|
|
532
|
+
}
|
|
533
|
+
const newOpts = { ...existingOpts, [subKey]: value };
|
|
534
|
+
if (opts.dryRun) {
|
|
535
|
+
info(`[dry-run] Would reconfigure ${subKey} in plugin options`);
|
|
536
|
+
return { changed: true };
|
|
537
|
+
}
|
|
538
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
539
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
540
|
+
config.plugin[pluginIdx] = [existingName, newOpts];
|
|
541
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
542
|
+
ok(`Reconfigured ${subKey}`);
|
|
543
|
+
info(`Backup: ${bakPath}`);
|
|
544
|
+
return { changed: true, bakPath };
|
|
545
|
+
} catch {
|
|
546
|
+
return { changed: false };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function writeMcpToggles(configPath, enabledSet, opts) {
|
|
550
|
+
try {
|
|
551
|
+
if (!fs3.existsSync(configPath)) {
|
|
552
|
+
return { changed: false };
|
|
553
|
+
}
|
|
554
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
555
|
+
const config = JSON.parse(raw);
|
|
556
|
+
const toggleNames = new Set(MCP_TOGGLES.map((t) => t.name));
|
|
557
|
+
const existingMcp = config.mcp && typeof config.mcp === "object" ? { ...config.mcp } : {};
|
|
558
|
+
const newMcp = {};
|
|
559
|
+
let hasChanges = false;
|
|
560
|
+
for (const [key, val] of Object.entries(existingMcp)) {
|
|
561
|
+
if (!toggleNames.has(key)) {
|
|
562
|
+
newMcp[key] = val;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
for (const toggleName of toggleNames) {
|
|
566
|
+
if (enabledSet.has(toggleName)) {
|
|
567
|
+
newMcp[toggleName] = { enabled: true };
|
|
568
|
+
if (!deepEqual(existingMcp[toggleName], { enabled: true })) {
|
|
569
|
+
hasChanges = true;
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
if (existingMcp[toggleName] !== void 0) {
|
|
573
|
+
hasChanges = true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (!hasChanges && Object.keys(newMcp).length === Object.keys(existingMcp).length) {
|
|
578
|
+
const allKeysMatch = Object.keys(newMcp).every(
|
|
579
|
+
(k) => deepEqual(newMcp[k], existingMcp[k])
|
|
580
|
+
);
|
|
581
|
+
if (allKeysMatch) {
|
|
582
|
+
return { changed: false };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (opts.dryRun) {
|
|
586
|
+
info(`[dry-run] Would reconfigure MCP toggles`);
|
|
587
|
+
return { changed: true };
|
|
588
|
+
}
|
|
589
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
590
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
591
|
+
if (Object.keys(newMcp).length > 0) {
|
|
592
|
+
config.mcp = newMcp;
|
|
593
|
+
} else {
|
|
594
|
+
delete config.mcp;
|
|
595
|
+
}
|
|
596
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
597
|
+
ok("Reconfigured MCPs");
|
|
598
|
+
info(`Backup: ${bakPath}`);
|
|
599
|
+
return { changed: true, bakPath };
|
|
600
|
+
} catch {
|
|
601
|
+
return { changed: false };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function writePluginToggles(configPath, enabledSet, opts) {
|
|
605
|
+
try {
|
|
606
|
+
if (!fs3.existsSync(configPath)) {
|
|
607
|
+
return { changed: false };
|
|
608
|
+
}
|
|
609
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
610
|
+
const config = JSON.parse(raw);
|
|
611
|
+
const toggleNames = new Set(PLUGIN_TOGGLES.map((t) => t.name));
|
|
612
|
+
const existingPlugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
613
|
+
const currentlyPresent = /* @__PURE__ */ new Set();
|
|
614
|
+
for (const entry of existingPlugins) {
|
|
615
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
616
|
+
if (typeof name === "string" && toggleNames.has(name)) {
|
|
617
|
+
currentlyPresent.add(name);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const toAdd = [];
|
|
621
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
622
|
+
for (const toggleName of toggleNames) {
|
|
623
|
+
if (enabledSet.has(toggleName) && !currentlyPresent.has(toggleName)) {
|
|
624
|
+
toAdd.push(toggleName);
|
|
625
|
+
} else if (!enabledSet.has(toggleName) && currentlyPresent.has(toggleName)) {
|
|
626
|
+
toRemove.add(toggleName);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (toAdd.length === 0 && toRemove.size === 0) {
|
|
630
|
+
return { changed: false };
|
|
631
|
+
}
|
|
632
|
+
if (opts.dryRun) {
|
|
633
|
+
info(`[dry-run] Would reconfigure plugin toggles`);
|
|
634
|
+
return { changed: true };
|
|
635
|
+
}
|
|
636
|
+
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
637
|
+
fs3.copyFileSync(configPath, bakPath);
|
|
638
|
+
const newPlugins = existingPlugins.filter((entry) => {
|
|
639
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
|
|
640
|
+
return !(typeof name === "string" && toRemove.has(name));
|
|
641
|
+
});
|
|
642
|
+
for (const name of toAdd) {
|
|
643
|
+
newPlugins.push(name);
|
|
644
|
+
}
|
|
645
|
+
config.plugin = newPlugins;
|
|
646
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
647
|
+
ok("Reconfigured plugin add-ons");
|
|
648
|
+
info(`Backup: ${bakPath}`);
|
|
649
|
+
return { changed: true, bakPath };
|
|
650
|
+
} catch {
|
|
651
|
+
return { changed: false };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function install(opts = {}) {
|
|
655
|
+
const { dryRun = false, pin = false, nonInteractive = false } = opts;
|
|
656
|
+
const configPath = getOpencodeConfigPath();
|
|
657
|
+
const pluginEntry = pin ? `${PLUGIN_NAME}@${readPackageVersion()}` : PLUGIN_NAME;
|
|
658
|
+
const interactive = !nonInteractive && process.stdin.isTTY === true;
|
|
659
|
+
const existing = readExistingConfig(configPath);
|
|
660
|
+
const hasPlugin = existing ? (Array.isArray(existing.plugin) ? existing.plugin : []).some(
|
|
661
|
+
(p) => {
|
|
662
|
+
const name = typeof p === "string" ? p : Array.isArray(p) ? p[0] : null;
|
|
663
|
+
return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
|
|
664
|
+
}
|
|
665
|
+
) : false;
|
|
666
|
+
const existingProvider = detectModelProvider(existing);
|
|
667
|
+
const existingMcps = detectEnabledMcps(existing);
|
|
668
|
+
const existingPluginToggles = detectEnabledPluginToggles(existing);
|
|
669
|
+
const existingOpts = extractPluginOptions(existing);
|
|
670
|
+
let hasModels = !!(existingOpts?.models ?? existing?.harness?.models);
|
|
671
|
+
console.log(`
|
|
672
|
+
${c.bold}${c.blue}@glrs-dev/harness-plugin-opencode${c.reset} setup
|
|
673
|
+
`);
|
|
674
|
+
if (hasPlugin) {
|
|
675
|
+
ok("Plugin already registered");
|
|
676
|
+
}
|
|
677
|
+
if (existingProvider) {
|
|
678
|
+
ok(`Models: ${existingProvider}`);
|
|
679
|
+
}
|
|
680
|
+
if (existingMcps.size > 0) {
|
|
681
|
+
ok(`MCPs: ${[...existingMcps].join(", ")} enabled`);
|
|
682
|
+
}
|
|
683
|
+
let reconfigureModels = false;
|
|
684
|
+
let reconfigureMcps = false;
|
|
685
|
+
let reconfigurePluginToggles = false;
|
|
686
|
+
let newModelsValue = null;
|
|
687
|
+
let newMcpEnabledSet = /* @__PURE__ */ new Set();
|
|
688
|
+
let newPluginToggleEnabledSet = new Set(existingPluginToggles);
|
|
689
|
+
if (hasPlugin && (existingProvider || hasModels)) {
|
|
690
|
+
const unconfiguredMcps = MCP_TOGGLES.filter(
|
|
691
|
+
(t) => !existingMcps.has(t.name) && !existing?.mcp?.[t.name]
|
|
692
|
+
);
|
|
693
|
+
if (interactive) {
|
|
694
|
+
const reconfigure = await promptChoice(
|
|
695
|
+
" Reconfigure models?",
|
|
696
|
+
["No, keep current config", "Yes, reconfigure models"],
|
|
697
|
+
0
|
|
698
|
+
);
|
|
699
|
+
if (reconfigure === 1) {
|
|
700
|
+
reconfigureModels = true;
|
|
701
|
+
hasModels = false;
|
|
702
|
+
}
|
|
703
|
+
if (existingMcps.size > 0) {
|
|
704
|
+
const reconfigureMcpChoice = await promptChoice(
|
|
705
|
+
" Reconfigure MCPs?",
|
|
706
|
+
["No, keep current config", "Yes, reconfigure MCPs"],
|
|
707
|
+
0
|
|
708
|
+
);
|
|
709
|
+
if (reconfigureMcpChoice === 1) {
|
|
710
|
+
reconfigureMcps = true;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const reconfigurePluginToggleChoice = await promptChoice(
|
|
714
|
+
" Reconfigure plugin add-ons?",
|
|
715
|
+
["No, keep current config", "Yes, reconfigure plugin add-ons"],
|
|
716
|
+
0
|
|
717
|
+
);
|
|
718
|
+
if (reconfigurePluginToggleChoice === 1) {
|
|
719
|
+
reconfigurePluginToggles = true;
|
|
720
|
+
}
|
|
721
|
+
if (!reconfigureModels && !reconfigureMcps && !reconfigurePluginToggles && unconfiguredMcps.length === 0) {
|
|
722
|
+
console.log(`
|
|
723
|
+
${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
724
|
+
`);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
} else if (unconfiguredMcps.length === 0) {
|
|
728
|
+
console.log(`
|
|
729
|
+
${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
730
|
+
`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const pluginOpts = {};
|
|
735
|
+
if (interactive && !hasModels) {
|
|
736
|
+
console.log();
|
|
737
|
+
console.log(`${c.dim}Models${c.reset}`);
|
|
738
|
+
info("Fetching available providers\u2026");
|
|
739
|
+
const modelsDevProviders = await fetchModelsDevProviders();
|
|
740
|
+
let preset = null;
|
|
741
|
+
if (modelsDevProviders && modelsDevProviders.length > 0) {
|
|
742
|
+
const providerChoices = modelsDevProviders.map((p) => p.name);
|
|
743
|
+
providerChoices.push("Keep defaults (no model config)");
|
|
744
|
+
providerChoices.push("Custom (enter model IDs manually)");
|
|
745
|
+
const keepDefaultsIdx = providerChoices.length - 2;
|
|
746
|
+
const providerIdx = await promptChoice(
|
|
747
|
+
" Which model provider?",
|
|
748
|
+
providerChoices,
|
|
749
|
+
keepDefaultsIdx
|
|
750
|
+
);
|
|
751
|
+
if (providerIdx < modelsDevProviders.length) {
|
|
752
|
+
const provider = modelsDevProviders[providerIdx];
|
|
753
|
+
ok(`Provider: ${provider.name}`);
|
|
754
|
+
const suggested = provider.id === "amazon-bedrock" ? pickBedrockTierIds(provider) : suggestTiersFromModelsDev(provider);
|
|
755
|
+
const modelChoices = Object.keys(provider.models).map(
|
|
756
|
+
(modelId) => `${provider.id}/${modelId}`
|
|
757
|
+
);
|
|
758
|
+
const tiers = [
|
|
759
|
+
{ tier: "deep", suggested: suggested.deep },
|
|
760
|
+
{ tier: "mid", suggested: suggested.mid },
|
|
761
|
+
{ tier: "fast", suggested: suggested.fast }
|
|
762
|
+
];
|
|
763
|
+
const picked = {};
|
|
764
|
+
for (const { tier, suggested: suggestedModel } of tiers) {
|
|
765
|
+
const defaultIdx = modelChoices.indexOf(suggestedModel);
|
|
766
|
+
const idx = await promptChoice(
|
|
767
|
+
` ${tier} model?`,
|
|
768
|
+
modelChoices,
|
|
769
|
+
defaultIdx >= 0 ? defaultIdx : 0
|
|
770
|
+
);
|
|
771
|
+
picked[tier] = modelChoices[idx];
|
|
772
|
+
info(` ${tier} \u2192 ${picked[tier]}`);
|
|
773
|
+
}
|
|
774
|
+
preset = {
|
|
775
|
+
label: provider.name,
|
|
776
|
+
providerId: provider.id,
|
|
777
|
+
deep: picked["deep"],
|
|
778
|
+
mid: picked["mid"],
|
|
779
|
+
fast: picked["fast"]
|
|
780
|
+
};
|
|
781
|
+
} else if (providerIdx === modelsDevProviders.length) {
|
|
782
|
+
ok("Models: OpenCode defaults");
|
|
783
|
+
pluginOpts._skipModels = true;
|
|
784
|
+
}
|
|
785
|
+
} else {
|
|
786
|
+
warn("Could not reach Models.dev API \u2014 using built-in presets");
|
|
787
|
+
const presetLabels = [...MODEL_PRESETS.map((p) => p.label), "Keep defaults (no model config)", "Custom (enter model IDs manually)"];
|
|
788
|
+
const keepDefaultsOfflineIdx = presetLabels.length - 2;
|
|
789
|
+
const choice = await promptChoice(
|
|
790
|
+
" Which model provider?",
|
|
791
|
+
presetLabels,
|
|
792
|
+
keepDefaultsOfflineIdx
|
|
793
|
+
);
|
|
794
|
+
if (choice < MODEL_PRESETS.length) {
|
|
795
|
+
preset = MODEL_PRESETS[choice];
|
|
796
|
+
ok(`Provider: ${preset.label}`);
|
|
797
|
+
} else if (choice === MODEL_PRESETS.length) {
|
|
798
|
+
ok("Models: OpenCode defaults");
|
|
799
|
+
pluginOpts._skipModels = true;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (preset) {
|
|
803
|
+
pluginOpts.models = {
|
|
804
|
+
deep: [preset.deep],
|
|
805
|
+
mid: [preset.mid],
|
|
806
|
+
fast: [preset.fast]
|
|
807
|
+
};
|
|
808
|
+
newModelsValue = {
|
|
809
|
+
deep: [preset.deep],
|
|
810
|
+
mid: [preset.mid],
|
|
811
|
+
fast: [preset.fast]
|
|
812
|
+
};
|
|
813
|
+
ok(`Models configured`);
|
|
814
|
+
const midExecIdx = await promptChoice(
|
|
815
|
+
" Use a strict executor for build agents? (recommended for Kimi/Qwen/DeepSeek)",
|
|
816
|
+
["No (use mid model as reasoning builder)", "Yes (configure mid-execute model)"],
|
|
817
|
+
0
|
|
818
|
+
);
|
|
819
|
+
if (midExecIdx === 1) {
|
|
820
|
+
const { input } = await import("@inquirer/prompts");
|
|
821
|
+
const midExecModel = await input({
|
|
822
|
+
message: " mid-execute model ID:",
|
|
823
|
+
default: preset.mid
|
|
824
|
+
});
|
|
825
|
+
if (midExecModel) {
|
|
826
|
+
pluginOpts.models["mid-execute"] = [midExecModel];
|
|
827
|
+
newModelsValue["mid-execute"] = [midExecModel];
|
|
828
|
+
info(` mid-execute \u2192 ${midExecModel} (strict executor prompts)`);
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
info(` mid-execute: skipped (build agents use mid model with reasoning prompts)`);
|
|
832
|
+
}
|
|
833
|
+
} else if (!pluginOpts._skipModels) {
|
|
834
|
+
info("Enter model IDs in <provider>/<model-id> format (e.g. amazon-bedrock/global.anthropic.claude-opus-4-7)");
|
|
835
|
+
const { input } = await import("@inquirer/prompts");
|
|
836
|
+
const deepModel = await input({ message: " deep (most capable):" });
|
|
837
|
+
const midModel = await input({ message: " mid (balanced):" });
|
|
838
|
+
const fastModel = await input({ message: " fast (cheapest):" });
|
|
839
|
+
if (deepModel) {
|
|
840
|
+
const resolvedMid = midModel || deepModel;
|
|
841
|
+
pluginOpts.models = {
|
|
842
|
+
deep: [deepModel],
|
|
843
|
+
mid: [resolvedMid],
|
|
844
|
+
fast: [fastModel || midModel || deepModel]
|
|
845
|
+
};
|
|
846
|
+
newModelsValue = {
|
|
847
|
+
deep: [deepModel],
|
|
848
|
+
mid: [resolvedMid],
|
|
849
|
+
fast: [fastModel || midModel || deepModel]
|
|
850
|
+
};
|
|
851
|
+
ok("Models: custom");
|
|
852
|
+
const midExecModel = await input({ message: " mid-execute (optional strict executor, press Enter to skip):" });
|
|
853
|
+
if (midExecModel) {
|
|
854
|
+
pluginOpts.models["mid-execute"] = [midExecModel];
|
|
855
|
+
newModelsValue["mid-execute"] = [midExecModel];
|
|
856
|
+
info(` mid-execute \u2192 ${midExecModel} (strict executor prompts)`);
|
|
857
|
+
} else {
|
|
858
|
+
info(` mid-execute: skipped (build agents use mid model with reasoning prompts)`);
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
ok("Models: OpenCode defaults");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
delete pluginOpts._skipModels;
|
|
865
|
+
console.log();
|
|
866
|
+
}
|
|
867
|
+
if (interactive && reconfigureMcps) {
|
|
868
|
+
console.log(`${c.dim}Reconfigure MCP servers${c.reset}`);
|
|
869
|
+
const currentEnabled = new Set(existingMcps);
|
|
870
|
+
const selected = await promptMulti(
|
|
871
|
+
" Select MCPs to enable:",
|
|
872
|
+
MCP_TOGGLES.map((t) => ({ label: t.label, defaultOn: currentEnabled.has(t.name) }))
|
|
873
|
+
);
|
|
874
|
+
newMcpEnabledSet = new Set([...selected].map((i) => MCP_TOGGLES[i].name));
|
|
875
|
+
const names = [...newMcpEnabledSet].join(", ");
|
|
876
|
+
if (newMcpEnabledSet.size > 0) {
|
|
877
|
+
ok(`MCPs to enable: ${names}`);
|
|
878
|
+
} else {
|
|
879
|
+
ok("MCPs: all disabled");
|
|
880
|
+
}
|
|
881
|
+
console.log();
|
|
882
|
+
}
|
|
883
|
+
if (interactive && reconfigurePluginToggles) {
|
|
884
|
+
console.log(`${c.dim}Plugin add-ons${c.reset}`);
|
|
885
|
+
const currentEnabled = new Set(existingPluginToggles);
|
|
886
|
+
const selected = await promptMulti(
|
|
887
|
+
" Enable plugin add-ons?",
|
|
888
|
+
PLUGIN_TOGGLES.map((t) => ({ label: t.label, defaultOn: currentEnabled.has(t.name) }))
|
|
889
|
+
);
|
|
890
|
+
newPluginToggleEnabledSet = new Set([...selected].map((i) => PLUGIN_TOGGLES[i].name));
|
|
891
|
+
const names = [...newPluginToggleEnabledSet].join(", ");
|
|
892
|
+
if (newPluginToggleEnabledSet.size > 0) {
|
|
893
|
+
ok(`Plugin add-ons enabled: ${names}`);
|
|
894
|
+
} else {
|
|
895
|
+
ok("Plugin add-ons: none");
|
|
896
|
+
}
|
|
897
|
+
console.log();
|
|
898
|
+
}
|
|
899
|
+
const pluginValue = Object.keys(pluginOpts).length > 0 ? [pluginEntry, pluginOpts] : pluginEntry;
|
|
900
|
+
const config = {
|
|
901
|
+
$schema: "https://opencode.ai/config.json",
|
|
902
|
+
plugin: [pluginValue]
|
|
903
|
+
};
|
|
904
|
+
if (interactive) {
|
|
905
|
+
const unconfigured = MCP_TOGGLES.filter(
|
|
906
|
+
(t) => !existingMcps.has(t.name) && !existing?.mcp?.[t.name]
|
|
907
|
+
);
|
|
908
|
+
if (unconfigured.length > 0) {
|
|
909
|
+
console.log(`${c.dim}Optional MCP servers (serena, memory, git are always on)${c.reset}`);
|
|
910
|
+
const selected = await promptMulti(
|
|
911
|
+
" Enable additional MCPs?",
|
|
912
|
+
unconfigured.map((t) => ({ label: t.label, defaultOn: t.defaultOn }))
|
|
913
|
+
);
|
|
914
|
+
if (selected.size > 0) {
|
|
915
|
+
const mcp = {};
|
|
916
|
+
for (const idx of selected) {
|
|
917
|
+
const toggle = unconfigured[idx];
|
|
918
|
+
mcp[toggle.name] = { enabled: true };
|
|
919
|
+
}
|
|
920
|
+
config.mcp = mcp;
|
|
921
|
+
const names = [...selected].map((i) => unconfigured[i].name).join(", ");
|
|
922
|
+
ok(`MCPs enabled: ${names}`);
|
|
923
|
+
} else {
|
|
924
|
+
ok("MCPs: defaults only");
|
|
925
|
+
}
|
|
926
|
+
console.log();
|
|
927
|
+
}
|
|
928
|
+
const unconfiguredPluginToggles = PLUGIN_TOGGLES.filter(
|
|
929
|
+
(t) => !existingPluginToggles.has(t.name)
|
|
930
|
+
);
|
|
931
|
+
if (unconfiguredPluginToggles.length > 0) {
|
|
932
|
+
console.log(`${c.dim}Plugin add-ons${c.reset}`);
|
|
933
|
+
const selected = await promptMulti(
|
|
934
|
+
" Enable plugin add-ons?",
|
|
935
|
+
unconfiguredPluginToggles.map((t) => ({ label: t.label, defaultOn: t.defaultOn }))
|
|
936
|
+
);
|
|
937
|
+
if (selected.size > 0) {
|
|
938
|
+
for (const idx of selected) {
|
|
939
|
+
const toggle = unconfiguredPluginToggles[idx];
|
|
940
|
+
config.plugin.push(toggle.name);
|
|
941
|
+
newPluginToggleEnabledSet.add(toggle.name);
|
|
942
|
+
}
|
|
943
|
+
const names = [...selected].map((i) => unconfiguredPluginToggles[i].name).join(", ");
|
|
944
|
+
ok(`Plugin add-ons enabled: ${names}`);
|
|
945
|
+
} else {
|
|
946
|
+
ok("Plugin add-ons: none");
|
|
947
|
+
}
|
|
948
|
+
console.log();
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (reconfigureModels && newModelsValue) {
|
|
952
|
+
writePluginOption(configPath, "models", newModelsValue, { dryRun });
|
|
953
|
+
}
|
|
954
|
+
if (reconfigureMcps) {
|
|
955
|
+
writeMcpToggles(configPath, newMcpEnabledSet, { dryRun });
|
|
956
|
+
}
|
|
957
|
+
if (reconfigurePluginToggles) {
|
|
958
|
+
writePluginToggles(configPath, newPluginToggleEnabledSet, { dryRun });
|
|
959
|
+
}
|
|
960
|
+
if (!fs3.existsSync(configPath)) {
|
|
961
|
+
if (dryRun) {
|
|
962
|
+
info(`[dry-run] Would create ${configPath}`);
|
|
963
|
+
info(`[dry-run] Config: ${JSON.stringify(config, null, 2)}`);
|
|
964
|
+
} else {
|
|
965
|
+
seedConfig(config, configPath);
|
|
966
|
+
ok(`Created ${configPath}`);
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
try {
|
|
970
|
+
const result = mergeConfig(config, configPath, dryRun);
|
|
971
|
+
if (!result.changed) {
|
|
972
|
+
ok("opencode.json is up to date");
|
|
973
|
+
for (const w of result.warnings) warn(w);
|
|
974
|
+
} else {
|
|
975
|
+
if (dryRun) {
|
|
976
|
+
info(`[dry-run] Would merge into ${configPath}:`);
|
|
977
|
+
for (const a of result.additions) info(` ${a}`);
|
|
978
|
+
} else {
|
|
979
|
+
ok(`Updated ${configPath}`);
|
|
980
|
+
info(`Backup: ${result.bakPath}`);
|
|
981
|
+
for (const a of result.additions) info(` ${a}`);
|
|
982
|
+
}
|
|
983
|
+
for (const w of result.warnings) warn(w);
|
|
984
|
+
}
|
|
985
|
+
} catch (e) {
|
|
986
|
+
console.error(`\x1B[31m\u2717\x1B[0m ${e.message}`);
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (!dryRun) {
|
|
991
|
+
migrateHarnessKeyToPluginOptions(configPath);
|
|
992
|
+
}
|
|
993
|
+
if (!dryRun) {
|
|
994
|
+
await refreshPluginCacheIfStale();
|
|
995
|
+
}
|
|
996
|
+
if (newPluginToggleEnabledSet.has("opencode-snip")) {
|
|
997
|
+
warn("opencode-snip requires the Go snip binary. Install: brew install vhardouin/opencode-snip/snip");
|
|
998
|
+
}
|
|
999
|
+
console.log(`
|
|
1000
|
+
${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
|
|
1001
|
+
`);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/cli/uninstall.ts
|
|
1005
|
+
import * as fs4 from "fs";
|
|
1006
|
+
import * as path4 from "path";
|
|
1007
|
+
import * as os3 from "os";
|
|
1008
|
+
var PLUGIN_NAME2 = "@glrs-dev/harness-plugin-opencode";
|
|
1009
|
+
function getOpencodeConfigPath2() {
|
|
1010
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? path4.join(os3.homedir(), ".config");
|
|
1011
|
+
return path4.join(configHome, "opencode", "opencode.json");
|
|
40
1012
|
}
|
|
41
1013
|
function uninstall(opts = {}) {
|
|
42
1014
|
const { dryRun = false } = opts;
|
|
43
|
-
const configPath =
|
|
44
|
-
const
|
|
1015
|
+
const configPath = getOpencodeConfigPath2();
|
|
1016
|
+
const c3 = {
|
|
45
1017
|
reset: "\x1B[0m",
|
|
46
1018
|
green: "\x1B[32m",
|
|
47
1019
|
yellow: "\x1B[33m",
|
|
48
1020
|
blue: "\x1B[34m"
|
|
49
1021
|
};
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
1022
|
+
const ok2 = (msg) => console.log(`${c3.green}\u2713${c3.reset} ${msg}`);
|
|
1023
|
+
const info2 = (msg) => console.log(`${c3.blue}\u2022${c3.reset} ${msg}`);
|
|
1024
|
+
const warn2 = (msg) => console.log(`${c3.yellow}!${c3.reset} ${msg}`);
|
|
53
1025
|
console.log(`
|
|
54
|
-
${
|
|
1026
|
+
${c3.blue}Uninstalling ${PLUGIN_NAME2}${c3.reset}
|
|
55
1027
|
`);
|
|
56
|
-
if (!
|
|
57
|
-
|
|
1028
|
+
if (!fs4.existsSync(configPath)) {
|
|
1029
|
+
warn2(`No opencode.json found at ${configPath} \u2014 nothing to do`);
|
|
58
1030
|
return;
|
|
59
1031
|
}
|
|
60
1032
|
let raw;
|
|
61
1033
|
try {
|
|
62
|
-
raw =
|
|
1034
|
+
raw = fs4.readFileSync(configPath, "utf8");
|
|
63
1035
|
} catch (e) {
|
|
64
1036
|
console.error(`\x1B[31m\u2717\x1B[0m Failed to read ${configPath}: ${e.message}`);
|
|
65
1037
|
process.exit(1);
|
|
@@ -74,19 +1046,19 @@ ${c2.blue}Uninstalling ${PLUGIN_NAME}${c2.reset}
|
|
|
74
1046
|
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
75
1047
|
const filtered = plugins.filter((p) => {
|
|
76
1048
|
const name = typeof p === "string" ? p : Array.isArray(p) ? p[0] : null;
|
|
77
|
-
return name !==
|
|
1049
|
+
return name !== PLUGIN_NAME2 && !String(name ?? "").startsWith(`${PLUGIN_NAME2}@`);
|
|
78
1050
|
});
|
|
79
1051
|
if (filtered.length === plugins.length) {
|
|
80
|
-
|
|
1052
|
+
warn2(`"${PLUGIN_NAME2}" not found in plugin array \u2014 nothing to remove`);
|
|
81
1053
|
return;
|
|
82
1054
|
}
|
|
83
1055
|
if (dryRun) {
|
|
84
|
-
|
|
1056
|
+
info2(`[dry-run] Would remove "${PLUGIN_NAME2}" from plugin array in ${configPath}`);
|
|
85
1057
|
return;
|
|
86
1058
|
}
|
|
87
1059
|
const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
|
|
88
1060
|
try {
|
|
89
|
-
|
|
1061
|
+
fs4.copyFileSync(configPath, bakPath);
|
|
90
1062
|
} catch (e) {
|
|
91
1063
|
console.error(`\x1B[31m\u2717\x1B[0m Failed to write backup: ${e.message}`);
|
|
92
1064
|
process.exit(1);
|
|
@@ -94,36 +1066,36 @@ ${c2.blue}Uninstalling ${PLUGIN_NAME}${c2.reset}
|
|
|
94
1066
|
config.plugin = filtered;
|
|
95
1067
|
const tmpPath = `${configPath}.uninstall.tmp.${Date.now()}-${process.pid}`;
|
|
96
1068
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
1069
|
+
fs4.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1070
|
+
fs4.renameSync(tmpPath, configPath);
|
|
99
1071
|
} catch (e) {
|
|
100
1072
|
try {
|
|
101
|
-
|
|
1073
|
+
fs4.unlinkSync(tmpPath);
|
|
102
1074
|
} catch {
|
|
103
1075
|
}
|
|
104
1076
|
console.error(`\x1B[31m\u2717\x1B[0m Failed to write config: ${e.message}`);
|
|
105
1077
|
process.exit(1);
|
|
106
1078
|
}
|
|
107
|
-
|
|
108
|
-
|
|
1079
|
+
ok2(`Removed "${PLUGIN_NAME2}" from ${configPath}`);
|
|
1080
|
+
info2(`Backup: ${bakPath}`);
|
|
109
1081
|
console.log(`
|
|
110
1082
|
To fully remove the package: bun remove @glrs-dev/harness-plugin-opencode
|
|
111
1083
|
`);
|
|
112
1084
|
}
|
|
113
1085
|
|
|
114
1086
|
// src/cli/doctor.ts
|
|
115
|
-
import * as
|
|
116
|
-
import * as
|
|
117
|
-
import * as
|
|
1087
|
+
import * as fs5 from "fs";
|
|
1088
|
+
import * as path5 from "path";
|
|
1089
|
+
import * as os4 from "os";
|
|
118
1090
|
import { execSync } from "child_process";
|
|
119
|
-
var
|
|
120
|
-
function
|
|
121
|
-
const configHome = process.env["XDG_CONFIG_HOME"] ??
|
|
122
|
-
return
|
|
1091
|
+
var PLUGIN_NAME3 = "@glrs-dev/harness-plugin-opencode";
|
|
1092
|
+
function getOpencodeConfigPath3() {
|
|
1093
|
+
const configHome = process.env["XDG_CONFIG_HOME"] ?? path5.join(os4.homedir(), ".config");
|
|
1094
|
+
return path5.join(configHome, "opencode", "opencode.json");
|
|
123
1095
|
}
|
|
124
|
-
function cmd(
|
|
1096
|
+
function cmd(command3) {
|
|
125
1097
|
try {
|
|
126
|
-
return execSync(
|
|
1098
|
+
return execSync(command3, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
127
1099
|
} catch {
|
|
128
1100
|
return null;
|
|
129
1101
|
}
|
|
@@ -132,38 +1104,38 @@ function which(bin) {
|
|
|
132
1104
|
return cmd(`which ${bin}`) !== null;
|
|
133
1105
|
}
|
|
134
1106
|
function doctor() {
|
|
135
|
-
const
|
|
1107
|
+
const c3 = {
|
|
136
1108
|
reset: "\x1B[0m",
|
|
137
1109
|
green: "\x1B[32m",
|
|
138
1110
|
yellow: "\x1B[33m",
|
|
139
1111
|
red: "\x1B[31m",
|
|
140
1112
|
bold: "\x1B[1m"
|
|
141
1113
|
};
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const fail = (msg) => console.log(`${
|
|
1114
|
+
const ok2 = (msg) => console.log(`${c3.green}\u2713${c3.reset} ${msg}`);
|
|
1115
|
+
const warn2 = (msg) => console.log(`${c3.yellow}!${c3.reset} ${msg}`);
|
|
1116
|
+
const fail = (msg) => console.log(`${c3.red}\u2717${c3.reset} ${msg}`);
|
|
145
1117
|
console.log(`
|
|
146
|
-
${
|
|
1118
|
+
${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
|
|
147
1119
|
`);
|
|
148
1120
|
const ocVersion = cmd("opencode --version 2>/dev/null | head -1");
|
|
149
1121
|
if (ocVersion) {
|
|
150
|
-
|
|
1122
|
+
ok2(`opencode ${ocVersion}`);
|
|
151
1123
|
} else {
|
|
152
1124
|
fail("opencode CLI not found \u2014 install from https://opencode.ai");
|
|
153
1125
|
}
|
|
154
|
-
const configPath =
|
|
155
|
-
if (
|
|
1126
|
+
const configPath = getOpencodeConfigPath3();
|
|
1127
|
+
if (fs5.existsSync(configPath)) {
|
|
156
1128
|
try {
|
|
157
|
-
const config = JSON.parse(
|
|
1129
|
+
const config = JSON.parse(fs5.readFileSync(configPath, "utf8"));
|
|
158
1130
|
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
159
1131
|
let pluginOptions = null;
|
|
160
1132
|
const hasPlugin = plugins.some((p) => {
|
|
161
1133
|
if (typeof p === "string") {
|
|
162
|
-
return p ===
|
|
1134
|
+
return p === PLUGIN_NAME3 || p.startsWith(`${PLUGIN_NAME3}@`);
|
|
163
1135
|
}
|
|
164
1136
|
if (Array.isArray(p)) {
|
|
165
1137
|
const [name, opts] = p;
|
|
166
|
-
const match = name ===
|
|
1138
|
+
const match = name === PLUGIN_NAME3 || String(name ?? "").startsWith(`${PLUGIN_NAME3}@`);
|
|
167
1139
|
if (match && opts && typeof opts === "object") {
|
|
168
1140
|
pluginOptions = opts;
|
|
169
1141
|
}
|
|
@@ -172,9 +1144,9 @@ ${c2.bold}Doctor \u2014 ${PLUGIN_NAME2}${c2.reset}
|
|
|
172
1144
|
return false;
|
|
173
1145
|
});
|
|
174
1146
|
if (hasPlugin) {
|
|
175
|
-
|
|
1147
|
+
ok2(`"${PLUGIN_NAME3}" present in opencode.json plugin array`);
|
|
176
1148
|
} else {
|
|
177
|
-
|
|
1149
|
+
warn2(`"${PLUGIN_NAME3}" NOT in opencode.json plugin array \u2014 run: bunx ${PLUGIN_NAME3} install`);
|
|
178
1150
|
}
|
|
179
1151
|
const modelSources = [];
|
|
180
1152
|
if (pluginOptions && typeof pluginOptions.models === "object") {
|
|
@@ -209,20 +1181,20 @@ ${c2.bold}Doctor \u2014 ${PLUGIN_NAME2}${c2.reset}
|
|
|
209
1181
|
}
|
|
210
1182
|
}
|
|
211
1183
|
if (invalid.length === 0) {
|
|
212
|
-
|
|
1184
|
+
ok2("model overrides look valid");
|
|
213
1185
|
} else {
|
|
214
1186
|
for (const entry of invalid) {
|
|
215
1187
|
fail(`invalid model override at ${entry.keyPath}: "${entry.value}"`);
|
|
216
1188
|
if (entry.reason) {
|
|
217
|
-
console.log(` ${
|
|
1189
|
+
console.log(` ${c3.yellow}reason:${c3.reset} ${entry.reason}`);
|
|
218
1190
|
}
|
|
219
1191
|
if (entry.suggestion) {
|
|
220
1192
|
console.log(
|
|
221
|
-
` ${
|
|
1193
|
+
` ${c3.yellow}fix:${c3.reset} remove this key, or replace with \`${entry.suggestion}\``
|
|
222
1194
|
);
|
|
223
1195
|
} else {
|
|
224
1196
|
console.log(
|
|
225
|
-
` ${
|
|
1197
|
+
` ${c3.yellow}fix:${c3.reset} remove this key, or run \`bunx ${PLUGIN_NAME3} install\` to pick a current preset`
|
|
226
1198
|
);
|
|
227
1199
|
}
|
|
228
1200
|
}
|
|
@@ -232,73 +1204,44 @@ ${c2.bold}Doctor \u2014 ${PLUGIN_NAME2}${c2.reset}
|
|
|
232
1204
|
fail(`opencode.json at ${configPath} has invalid JSON`);
|
|
233
1205
|
}
|
|
234
1206
|
} else {
|
|
235
|
-
|
|
1207
|
+
warn2(`No opencode.json at ${configPath} \u2014 run: bunx ${PLUGIN_NAME3} install`);
|
|
236
1208
|
}
|
|
237
1209
|
if (which("uvx")) {
|
|
238
|
-
|
|
1210
|
+
ok2("uvx (serena + git MCPs)");
|
|
239
1211
|
} else {
|
|
240
|
-
|
|
1212
|
+
warn2("uvx not found \u2014 serena and git MCPs won't work. Install: brew install uv");
|
|
241
1213
|
}
|
|
242
1214
|
if (which("node") && which("npx")) {
|
|
243
|
-
|
|
1215
|
+
ok2(`node ${cmd("node --version") ?? ""} + npx (memory MCP)`);
|
|
244
1216
|
} else {
|
|
245
|
-
|
|
1217
|
+
warn2("node/npx not found \u2014 memory MCP won't work");
|
|
246
1218
|
}
|
|
247
|
-
const planCheckResult = cmd(`bunx ${
|
|
1219
|
+
const planCheckResult = cmd(`bunx ${PLUGIN_NAME3} plan-check --help 2>/dev/null`);
|
|
248
1220
|
if (planCheckResult !== null) {
|
|
249
|
-
|
|
1221
|
+
ok2("plan-check CLI invokable");
|
|
250
1222
|
} else {
|
|
251
|
-
|
|
1223
|
+
warn2("plan-check CLI not invokable \u2014 try: bun install");
|
|
252
1224
|
}
|
|
253
1225
|
if (which("bun")) {
|
|
254
|
-
|
|
1226
|
+
ok2(`bun ${cmd("bun --version") ?? ""}`);
|
|
255
1227
|
} else if (which("npm")) {
|
|
256
|
-
|
|
1228
|
+
ok2(`npm ${cmd("npm --version") ?? ""} (install bun for faster installs)`);
|
|
257
1229
|
} else {
|
|
258
1230
|
fail("Neither bun nor npm found \u2014 cannot install plugins");
|
|
259
1231
|
}
|
|
260
1232
|
console.log();
|
|
261
|
-
console.log(`${c2.bold}Pilot subsystem${c2.reset}`);
|
|
262
|
-
if (which("git")) {
|
|
263
|
-
const gitVer = cmd("git --version") ?? "";
|
|
264
|
-
ok(`git ${gitVer}`);
|
|
265
|
-
} else {
|
|
266
|
-
fail("git not found \u2014 pilot subsystem requires git");
|
|
267
|
-
}
|
|
268
|
-
if (which("bash")) {
|
|
269
|
-
ok("bash (verify-runner shell)");
|
|
270
|
-
} else {
|
|
271
|
-
fail("bash not found \u2014 pilot's verify commands run via `bash -c`");
|
|
272
|
-
}
|
|
273
|
-
const agentList = cmd("opencode agent list 2>/dev/null");
|
|
274
|
-
if (agentList !== null) {
|
|
275
|
-
for (const agentName of ["pilot-scoper", "pilot-planner", "pilot-builder", "pilot-assessor"]) {
|
|
276
|
-
if (agentList.includes(agentName)) {
|
|
277
|
-
ok(`${agentName} agent registered`);
|
|
278
|
-
} else {
|
|
279
|
-
warn(
|
|
280
|
-
`${agentName} agent NOT in \`opencode agent list\` \u2014 plugin may not be loaded; run: bunx ` + PLUGIN_NAME2 + " install"
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
} else {
|
|
285
|
-
warn(
|
|
286
|
-
"could not run `opencode agent list` \u2014 skipping pilot agent registration check"
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
console.log();
|
|
290
1233
|
}
|
|
291
1234
|
|
|
292
1235
|
// src/bin/plan-check.ts
|
|
293
1236
|
import { execFileSync } from "child_process";
|
|
294
|
-
import { fileURLToPath } from "url";
|
|
295
|
-
import { dirname, join as
|
|
1237
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1238
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
296
1239
|
function planCheck(args) {
|
|
297
|
-
const here =
|
|
1240
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
298
1241
|
const candidates = [
|
|
299
|
-
|
|
1242
|
+
join5(here, "plan-check.sh"),
|
|
300
1243
|
// dev: src/bin/plan-check.sh
|
|
301
|
-
|
|
1244
|
+
join5(here, "bin", "plan-check.sh")
|
|
302
1245
|
// dist: dist/ → dist/bin/plan-check.sh
|
|
303
1246
|
];
|
|
304
1247
|
let scriptPath;
|
|
@@ -326,9 +1269,9 @@ function planCheck(args) {
|
|
|
326
1269
|
|
|
327
1270
|
// src/plan-paths.ts
|
|
328
1271
|
import { execFile } from "child_process";
|
|
329
|
-
import * as
|
|
330
|
-
import * as
|
|
331
|
-
import * as
|
|
1272
|
+
import * as fs6 from "fs/promises";
|
|
1273
|
+
import * as os5 from "os";
|
|
1274
|
+
import * as path6 from "path";
|
|
332
1275
|
function execFileP(file, args, opts = {}) {
|
|
333
1276
|
const { cwd, timeoutMs = 5e3 } = opts;
|
|
334
1277
|
return new Promise((resolve2, reject) => {
|
|
@@ -350,8 +1293,8 @@ function execFileP(file, args, opts = {}) {
|
|
|
350
1293
|
});
|
|
351
1294
|
}
|
|
352
1295
|
function expandTilde(p) {
|
|
353
|
-
if (p === "~") return
|
|
354
|
-
if (p.startsWith("~/")) return
|
|
1296
|
+
if (p === "~") return os5.homedir();
|
|
1297
|
+
if (p.startsWith("~/")) return path6.join(os5.homedir(), p.slice(2));
|
|
355
1298
|
return p;
|
|
356
1299
|
}
|
|
357
1300
|
async function getRepoFolder(worktreeDir) {
|
|
@@ -374,695 +1317,92 @@ async function getRepoFolder(worktreeDir) {
|
|
|
374
1317
|
`getRepoFolder: \`git rev-parse --git-common-dir\` returned empty for ${worktreeDir}`
|
|
375
1318
|
);
|
|
376
1319
|
}
|
|
377
|
-
const absCommonDir =
|
|
378
|
-
const repoRoot =
|
|
379
|
-
return
|
|
1320
|
+
const absCommonDir = path6.isAbsolute(gitCommonDir) ? gitCommonDir : path6.resolve(worktreeDir, gitCommonDir);
|
|
1321
|
+
const repoRoot = path6.dirname(absCommonDir);
|
|
1322
|
+
return path6.basename(repoRoot);
|
|
380
1323
|
}
|
|
381
1324
|
async function getPlanDir(worktreeDir) {
|
|
382
1325
|
const override = process.env.GLORIOUS_PLAN_DIR;
|
|
383
|
-
const base = override ? expandTilde(override) :
|
|
1326
|
+
const base = override ? expandTilde(override) : path6.join(os5.homedir(), ".glorious", "opencode");
|
|
384
1327
|
const repoFolder = await getRepoFolder(worktreeDir);
|
|
385
|
-
const planDir =
|
|
386
|
-
await
|
|
1328
|
+
const planDir = path6.join(base, repoFolder, "plans");
|
|
1329
|
+
await fs6.mkdir(planDir, { recursive: true });
|
|
387
1330
|
return planDir;
|
|
388
1331
|
}
|
|
389
1332
|
async function migratePlans(worktreeDir, planDir) {
|
|
390
|
-
const oldDir =
|
|
391
|
-
const marker =
|
|
1333
|
+
const oldDir = path6.join(worktreeDir, ".agent", "plans");
|
|
1334
|
+
const marker = path6.join(oldDir, ".migrated");
|
|
392
1335
|
try {
|
|
393
|
-
await
|
|
1336
|
+
await fs6.stat(oldDir);
|
|
394
1337
|
} catch {
|
|
395
1338
|
return;
|
|
396
1339
|
}
|
|
397
1340
|
try {
|
|
398
|
-
await
|
|
1341
|
+
await fs6.stat(marker);
|
|
399
1342
|
return;
|
|
400
1343
|
} catch {
|
|
401
1344
|
}
|
|
402
1345
|
let entries;
|
|
403
1346
|
try {
|
|
404
|
-
entries = await
|
|
1347
|
+
entries = await fs6.readdir(oldDir);
|
|
405
1348
|
} catch {
|
|
406
1349
|
return;
|
|
407
1350
|
}
|
|
408
1351
|
const planFiles = entries.filter(
|
|
409
1352
|
(name) => name.endsWith(".md") && !name.startsWith(".")
|
|
410
1353
|
);
|
|
411
|
-
await
|
|
1354
|
+
await fs6.mkdir(planDir, { recursive: true });
|
|
412
1355
|
for (const name of planFiles) {
|
|
413
|
-
const src =
|
|
414
|
-
const dst =
|
|
1356
|
+
const src = path6.join(oldDir, name);
|
|
1357
|
+
const dst = path6.join(planDir, name);
|
|
415
1358
|
let dstExists = false;
|
|
416
1359
|
try {
|
|
417
|
-
await
|
|
1360
|
+
await fs6.stat(dst);
|
|
418
1361
|
dstExists = true;
|
|
419
1362
|
} catch {
|
|
420
1363
|
dstExists = false;
|
|
421
1364
|
}
|
|
422
1365
|
if (!dstExists) {
|
|
423
|
-
await
|
|
1366
|
+
await fs6.rename(src, dst);
|
|
424
1367
|
continue;
|
|
425
1368
|
}
|
|
426
1369
|
const [srcBuf, dstBuf] = await Promise.all([
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
]);
|
|
430
|
-
if (srcBuf.equals(dstBuf)) {
|
|
431
|
-
await
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
process.stderr.write(
|
|
435
|
-
`[harness-opencode] migratePlans: conflict on ${name} \u2014 destination ${dst} exists with different content; leaving source ${src} in place. Resolve manually.
|
|
436
|
-
`
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
await fs3.writeFile(marker, "");
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// src/pilot/cli/index.ts
|
|
443
|
-
import { subcommands } from "cmd-ts";
|
|
444
|
-
|
|
445
|
-
// src/pilot/cli/configure.ts
|
|
446
|
-
import { command } from "cmd-ts";
|
|
447
|
-
import { input, select, confirm, number } from "@inquirer/prompts";
|
|
448
|
-
|
|
449
|
-
// src/pilot/config.ts
|
|
450
|
-
import * as fs4 from "fs";
|
|
451
|
-
var DEFAULT_MODEL = "anthropic/claude-sonnet-4-6";
|
|
452
|
-
var DEFAULT_CONFIG = {
|
|
453
|
-
models: {
|
|
454
|
-
scope: DEFAULT_MODEL,
|
|
455
|
-
plan: DEFAULT_MODEL,
|
|
456
|
-
execute: DEFAULT_MODEL,
|
|
457
|
-
assess: DEFAULT_MODEL
|
|
458
|
-
},
|
|
459
|
-
verify: {
|
|
460
|
-
baseline: [],
|
|
461
|
-
after_each: []
|
|
462
|
-
},
|
|
463
|
-
max_assess_cycles: 3,
|
|
464
|
-
playwright: {
|
|
465
|
-
enabled: false,
|
|
466
|
-
base_url: "http://localhost:3000"
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
function loadPilotConfig(cwd) {
|
|
470
|
-
const configPath = getPilotConfigPath(cwd);
|
|
471
|
-
if (!fs4.existsSync(configPath)) {
|
|
472
|
-
return { ...DEFAULT_CONFIG };
|
|
473
|
-
}
|
|
474
|
-
let raw;
|
|
475
|
-
try {
|
|
476
|
-
raw = JSON.parse(fs4.readFileSync(configPath, "utf8"));
|
|
477
|
-
} catch {
|
|
478
|
-
process.stderr.write(
|
|
479
|
-
`[pilot] Warning: .glrs/pilot.json has invalid JSON \u2014 using defaults
|
|
480
|
-
`
|
|
481
|
-
);
|
|
482
|
-
return { ...DEFAULT_CONFIG };
|
|
483
|
-
}
|
|
484
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
485
|
-
return { ...DEFAULT_CONFIG };
|
|
486
|
-
}
|
|
487
|
-
const obj = raw;
|
|
488
|
-
if ("baseline" in obj || "after_each" in obj) {
|
|
489
|
-
process.stderr.write(
|
|
490
|
-
`[pilot] Warning: .glrs/pilot.json appears to be in the old pilot v1 format.
|
|
491
|
-
Run \`pilot configure\` to set up the new v2 configuration.
|
|
492
|
-
Using defaults for now.
|
|
493
|
-
`
|
|
494
|
-
);
|
|
495
|
-
return { ...DEFAULT_CONFIG };
|
|
496
|
-
}
|
|
497
|
-
const models = mergeModels(obj["models"]);
|
|
498
|
-
const verify = mergeVerify(obj["verify"]);
|
|
499
|
-
const playwright = mergePlaywright(obj["playwright"]);
|
|
500
|
-
const max_assess_cycles = typeof obj["max_assess_cycles"] === "number" && obj["max_assess_cycles"] > 0 ? obj["max_assess_cycles"] : DEFAULT_CONFIG.max_assess_cycles;
|
|
501
|
-
return { models, verify, max_assess_cycles, playwright };
|
|
502
|
-
}
|
|
503
|
-
function mergeModels(raw) {
|
|
504
|
-
const d = DEFAULT_CONFIG.models;
|
|
505
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { ...d };
|
|
506
|
-
const obj = raw;
|
|
507
|
-
return {
|
|
508
|
-
scope: typeof obj["scope"] === "string" ? obj["scope"] : d.scope,
|
|
509
|
-
plan: typeof obj["plan"] === "string" ? obj["plan"] : d.plan,
|
|
510
|
-
execute: typeof obj["execute"] === "string" ? obj["execute"] : d.execute,
|
|
511
|
-
assess: typeof obj["assess"] === "string" ? obj["assess"] : d.assess
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
function mergeVerify(raw) {
|
|
515
|
-
const d = DEFAULT_CONFIG.verify;
|
|
516
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { ...d };
|
|
517
|
-
const obj = raw;
|
|
518
|
-
return {
|
|
519
|
-
baseline: Array.isArray(obj["baseline"]) ? obj["baseline"].filter((x) => typeof x === "string") : d.baseline,
|
|
520
|
-
after_each: Array.isArray(obj["after_each"]) ? obj["after_each"].filter((x) => typeof x === "string") : d.after_each
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
function mergePlaywright(raw) {
|
|
524
|
-
const d = DEFAULT_CONFIG.playwright;
|
|
525
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { ...d };
|
|
526
|
-
const obj = raw;
|
|
527
|
-
return {
|
|
528
|
-
enabled: typeof obj["enabled"] === "boolean" ? obj["enabled"] : d.enabled,
|
|
529
|
-
base_url: typeof obj["base_url"] === "string" ? obj["base_url"] : d.base_url
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
function writePilotConfig(cwd, config) {
|
|
533
|
-
const configPath = getPilotConfigPath(cwd);
|
|
534
|
-
const dir = configPath.slice(0, configPath.lastIndexOf("/"));
|
|
535
|
-
if (!fs4.existsSync(dir)) {
|
|
536
|
-
fs4.mkdirSync(dir, { recursive: true });
|
|
537
|
-
}
|
|
538
|
-
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// src/pilot/cli/configure.ts
|
|
542
|
-
var MODEL_SUGGESTIONS = [
|
|
543
|
-
// Anthropic
|
|
544
|
-
"anthropic/claude-opus-4-7",
|
|
545
|
-
"anthropic/claude-sonnet-4-6",
|
|
546
|
-
"anthropic/claude-haiku-4-5",
|
|
547
|
-
// Amazon Bedrock
|
|
548
|
-
"amazon-bedrock/global.anthropic.claude-opus-4-7",
|
|
549
|
-
"amazon-bedrock/global.anthropic.claude-sonnet-4-6",
|
|
550
|
-
"amazon-bedrock/global.anthropic.claude-haiku-4-5",
|
|
551
|
-
// OpenAI
|
|
552
|
-
"openai/gpt-4o",
|
|
553
|
-
"openai/gpt-4o-mini",
|
|
554
|
-
"openai/o3",
|
|
555
|
-
"openai/o4-mini",
|
|
556
|
-
// Google
|
|
557
|
-
"google/gemini-2.5-pro",
|
|
558
|
-
"google/gemini-2.5-flash",
|
|
559
|
-
// DeepSeek
|
|
560
|
-
"deepseek/deepseek-chat",
|
|
561
|
-
// Qwen
|
|
562
|
-
"qwen/qwen3-coder"
|
|
563
|
-
];
|
|
564
|
-
async function promptModel(phase, current) {
|
|
565
|
-
const choices = MODEL_SUGGESTIONS.includes(current) ? MODEL_SUGGESTIONS.map((m) => ({ name: m, value: m })) : [
|
|
566
|
-
{ name: `${current} (current)`, value: current },
|
|
567
|
-
...MODEL_SUGGESTIONS.map((m) => ({ name: m, value: m }))
|
|
568
|
-
];
|
|
569
|
-
return select({
|
|
570
|
-
message: `Model for ${phase} phase:`,
|
|
571
|
-
choices,
|
|
572
|
-
default: current
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
async function promptVerifyCommands(label, current) {
|
|
576
|
-
const currentStr = current.join(", ");
|
|
577
|
-
const raw = await input({
|
|
578
|
-
message: `${label} commands (comma-separated, empty to clear):`,
|
|
579
|
-
default: currentStr
|
|
580
|
-
});
|
|
581
|
-
if (!raw.trim()) return [];
|
|
582
|
-
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
583
|
-
}
|
|
584
|
-
var configureCmd = command({
|
|
585
|
-
name: "configure",
|
|
586
|
-
description: "Interactively configure pilot v2 for this repo (.glrs/pilot.json).",
|
|
587
|
-
args: {},
|
|
588
|
-
handler: async () => {
|
|
589
|
-
const cwd = process.cwd();
|
|
590
|
-
if (!process.stdin.isTTY) {
|
|
591
|
-
process.stderr.write(
|
|
592
|
-
"pilot configure: requires an interactive terminal (TTY).\n Edit .glrs/pilot.json directly for non-interactive configuration.\n"
|
|
593
|
-
);
|
|
594
|
-
process.exit(1);
|
|
595
|
-
}
|
|
596
|
-
const current = loadPilotConfig(cwd);
|
|
597
|
-
console.log("\n\x1B[1mPilot v2 Configuration\x1B[0m");
|
|
598
|
-
console.log("Configure per-phase models, verify commands, and behavior.\n");
|
|
599
|
-
console.log("\x1B[2m\u2500\u2500 Models \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
|
|
600
|
-
const scopeModel = await promptModel("scope", current.models.scope);
|
|
601
|
-
const planModel = await promptModel("plan", current.models.plan);
|
|
602
|
-
const executeModel = await promptModel("execute", current.models.execute);
|
|
603
|
-
const assessModel = await promptModel("assess", current.models.assess);
|
|
604
|
-
console.log("\n\x1B[2m\u2500\u2500 Verify commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
|
|
605
|
-
const baseline = await promptVerifyCommands("Baseline (run before execution)", current.verify.baseline);
|
|
606
|
-
const after_each = await promptVerifyCommands("After-each (run after each task)", current.verify.after_each);
|
|
607
|
-
console.log("\n\x1B[2m\u2500\u2500 Assess loop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
|
|
608
|
-
const max_assess_cycles = await number({
|
|
609
|
-
message: "Max assess cycles (how many times to re-plan on failure):",
|
|
610
|
-
default: current.max_assess_cycles,
|
|
611
|
-
min: 1,
|
|
612
|
-
max: 10
|
|
613
|
-
}) ?? current.max_assess_cycles;
|
|
614
|
-
console.log("\n\x1B[2m\u2500\u2500 Playwright (optional visual testing) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m");
|
|
615
|
-
const playwrightEnabled = await confirm({
|
|
616
|
-
message: "Enable Playwright MCP for visual verification in Assess?",
|
|
617
|
-
default: current.playwright.enabled
|
|
618
|
-
});
|
|
619
|
-
let playwrightBaseUrl = current.playwright.base_url;
|
|
620
|
-
if (playwrightEnabled) {
|
|
621
|
-
playwrightBaseUrl = await input({
|
|
622
|
-
message: "Playwright base URL:",
|
|
623
|
-
default: current.playwright.base_url
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
const config = {
|
|
627
|
-
models: {
|
|
628
|
-
scope: scopeModel,
|
|
629
|
-
plan: planModel,
|
|
630
|
-
execute: executeModel,
|
|
631
|
-
assess: assessModel
|
|
632
|
-
},
|
|
633
|
-
verify: { baseline, after_each },
|
|
634
|
-
max_assess_cycles,
|
|
635
|
-
playwright: { enabled: playwrightEnabled, base_url: playwrightBaseUrl }
|
|
636
|
-
};
|
|
637
|
-
writePilotConfig(cwd, config);
|
|
638
|
-
console.log("\n\x1B[32m\u2713\x1B[0m Configuration saved to .glrs/pilot.json");
|
|
639
|
-
console.log(' Run \x1B[1mpilot scope "<goal>"\x1B[0m to start a new workflow.\n');
|
|
640
|
-
process.exit(0);
|
|
641
|
-
}
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
// src/pilot/cli/scope.ts
|
|
645
|
-
import { command as command2, restPositionals, string } from "cmd-ts";
|
|
646
|
-
import { input as input2 } from "@inquirer/prompts";
|
|
647
|
-
|
|
648
|
-
// src/pilot/scope.ts
|
|
649
|
-
import * as fs5 from "fs";
|
|
650
|
-
|
|
651
|
-
// src/pilot/state.ts
|
|
652
|
-
import { Database } from "bun:sqlite";
|
|
653
|
-
import { ulid } from "ulid";
|
|
654
|
-
var SCHEMA_SQL = `
|
|
655
|
-
CREATE TABLE IF NOT EXISTS workflows (
|
|
656
|
-
id TEXT NOT NULL PRIMARY KEY,
|
|
657
|
-
goal TEXT NOT NULL,
|
|
658
|
-
scope_path TEXT,
|
|
659
|
-
plan_path TEXT,
|
|
660
|
-
status TEXT NOT NULL CHECK (status IN (
|
|
661
|
-
'pending','scoped','planned','executing','assessing','completed','failed'
|
|
662
|
-
)),
|
|
663
|
-
started_at INTEGER NOT NULL,
|
|
664
|
-
finished_at INTEGER,
|
|
665
|
-
config TEXT
|
|
666
|
-
);
|
|
667
|
-
|
|
668
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
669
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
670
|
-
workflow_id TEXT NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
671
|
-
ts INTEGER NOT NULL,
|
|
672
|
-
phase TEXT NOT NULL,
|
|
673
|
-
kind TEXT NOT NULL,
|
|
674
|
-
task_id TEXT,
|
|
675
|
-
payload TEXT NOT NULL,
|
|
676
|
-
session_id TEXT
|
|
677
|
-
);
|
|
678
|
-
|
|
679
|
-
CREATE INDEX IF NOT EXISTS idx_events_workflow ON events(workflow_id, id);
|
|
680
|
-
CREATE INDEX IF NOT EXISTS idx_events_workflow_phase ON events(workflow_id, phase, id);
|
|
681
|
-
`.trim();
|
|
682
|
-
function openStateDb(dbPath) {
|
|
683
|
-
const db = new Database(dbPath, { create: true });
|
|
684
|
-
try {
|
|
685
|
-
db.run("PRAGMA foreign_keys = ON");
|
|
686
|
-
if (dbPath !== ":memory:") {
|
|
687
|
-
db.run("PRAGMA journal_mode = WAL");
|
|
688
|
-
db.run("PRAGMA synchronous = NORMAL");
|
|
689
|
-
}
|
|
690
|
-
} catch (err) {
|
|
691
|
-
db.close();
|
|
692
|
-
throw new Error(
|
|
693
|
-
`openStateDb: failed to set PRAGMAs on ${JSON.stringify(dbPath)}: ${err instanceof Error ? err.message : String(err)}`
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
try {
|
|
697
|
-
db.exec(SCHEMA_SQL);
|
|
698
|
-
} catch (err) {
|
|
699
|
-
db.close();
|
|
700
|
-
throw err;
|
|
701
|
-
}
|
|
702
|
-
return { db, close: () => db.close() };
|
|
703
|
-
}
|
|
704
|
-
function createWorkflow(db, opts) {
|
|
705
|
-
const id = ulid();
|
|
706
|
-
const now = opts.now ?? Date.now();
|
|
707
|
-
db.prepare(
|
|
708
|
-
`INSERT INTO workflows (id, goal, status, started_at, config)
|
|
709
|
-
VALUES (?, ?, 'pending', ?, ?)`
|
|
710
|
-
).run(id, opts.goal, now, opts.config ?? null);
|
|
711
|
-
return id;
|
|
712
|
-
}
|
|
713
|
-
function getWorkflow(db, id) {
|
|
714
|
-
return db.prepare(
|
|
715
|
-
`SELECT * FROM workflows WHERE id = ?`
|
|
716
|
-
).get(id);
|
|
717
|
-
}
|
|
718
|
-
function latestWorkflow(db) {
|
|
719
|
-
return db.prepare(
|
|
720
|
-
`SELECT * FROM workflows ORDER BY started_at DESC LIMIT 1`
|
|
721
|
-
).get();
|
|
722
|
-
}
|
|
723
|
-
function updateWorkflowStatus(db, id, status, opts = {}) {
|
|
724
|
-
const now = opts.now ?? Date.now();
|
|
725
|
-
const terminal = status === "completed" || status === "failed";
|
|
726
|
-
db.prepare(
|
|
727
|
-
`UPDATE workflows
|
|
728
|
-
SET status = ?,
|
|
729
|
-
scope_path = COALESCE(?, scope_path),
|
|
730
|
-
plan_path = COALESCE(?, plan_path),
|
|
731
|
-
finished_at = CASE WHEN ? THEN ? ELSE finished_at END
|
|
732
|
-
WHERE id = ?`
|
|
733
|
-
).run(
|
|
734
|
-
status,
|
|
735
|
-
opts.scopePath ?? null,
|
|
736
|
-
opts.planPath ?? null,
|
|
737
|
-
terminal ? 1 : 0,
|
|
738
|
-
terminal ? now : null,
|
|
739
|
-
id
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
function appendEvent(db, opts) {
|
|
743
|
-
const ts = opts.now ?? Date.now();
|
|
744
|
-
let payloadStr;
|
|
745
|
-
try {
|
|
746
|
-
payloadStr = JSON.stringify(opts.payload);
|
|
747
|
-
} catch {
|
|
748
|
-
payloadStr = JSON.stringify({ _serializationError: true });
|
|
749
|
-
}
|
|
750
|
-
db.prepare(
|
|
751
|
-
`INSERT INTO events (workflow_id, ts, phase, kind, task_id, payload, session_id)
|
|
752
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
753
|
-
).run(
|
|
754
|
-
opts.workflowId,
|
|
755
|
-
ts,
|
|
756
|
-
opts.phase,
|
|
757
|
-
opts.kind,
|
|
758
|
-
opts.taskId ?? null,
|
|
759
|
-
payloadStr,
|
|
760
|
-
opts.sessionId ?? null
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
function readEvents(db, opts) {
|
|
764
|
-
if (opts.phase) {
|
|
765
|
-
return db.prepare(
|
|
766
|
-
`SELECT * FROM events WHERE workflow_id = ? AND phase = ? ORDER BY id LIMIT ?`
|
|
767
|
-
).all(opts.workflowId, opts.phase, opts.limit ?? 1e3);
|
|
768
|
-
}
|
|
769
|
-
return db.prepare(
|
|
770
|
-
`SELECT * FROM events WHERE workflow_id = ? ORDER BY id LIMIT ?`
|
|
771
|
-
).all(opts.workflowId, opts.limit ?? 1e3);
|
|
772
|
-
}
|
|
773
|
-
function logEvent(db, opts) {
|
|
774
|
-
appendEvent(db, opts);
|
|
775
|
-
const indent = " ".repeat(opts.indent ?? 0);
|
|
776
|
-
const kvPairs = Object.entries(opts.payload).map(([k, v]) => {
|
|
777
|
-
const val = typeof v === "string" && v.includes(" ") ? `"${v}"` : String(v);
|
|
778
|
-
return `${k}=${val}`;
|
|
779
|
-
}).join(" ");
|
|
780
|
-
const line = `${indent}[pilot] ${opts.kind.padEnd(32)} ${kvPairs}
|
|
781
|
-
`;
|
|
782
|
-
process.stderr.write(line);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// src/pilot/safety.ts
|
|
786
|
-
import { execFile as execFile2 } from "child_process";
|
|
787
|
-
import { promisify } from "util";
|
|
788
|
-
var execFileP2 = promisify(execFile2);
|
|
789
|
-
var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["main", "master", "develop", "trunk"]);
|
|
790
|
-
async function checkSafety(cwd) {
|
|
791
|
-
try {
|
|
792
|
-
await execFileP2("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
793
|
-
} catch {
|
|
794
|
-
return { ok: false, reason: "Not inside a git repository." };
|
|
795
|
-
}
|
|
796
|
-
let branch;
|
|
797
|
-
try {
|
|
798
|
-
const { stdout } = await execFileP2("git", ["branch", "--show-current"], { cwd });
|
|
799
|
-
branch = stdout.trim();
|
|
800
|
-
} catch {
|
|
801
|
-
return { ok: false, reason: "Could not determine current branch." };
|
|
802
|
-
}
|
|
803
|
-
if (PROTECTED_BRANCHES.has(branch)) {
|
|
804
|
-
return {
|
|
805
|
-
ok: false,
|
|
806
|
-
reason: `Refusing to run pilot on protected branch "${branch}". Create a feature branch first (e.g. git checkout -b feat/my-feature).`
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
let status;
|
|
810
|
-
try {
|
|
811
|
-
const { stdout } = await execFileP2("git", ["status", "--porcelain"], { cwd });
|
|
812
|
-
status = stdout.trim();
|
|
813
|
-
} catch {
|
|
814
|
-
return { ok: false, reason: "Could not check working tree status." };
|
|
815
|
-
}
|
|
816
|
-
if (status.length > 0) {
|
|
817
|
-
const lines = status.split("\n").slice(0, 5);
|
|
818
|
-
const preview = lines.join("\n ");
|
|
819
|
-
return {
|
|
820
|
-
ok: false,
|
|
821
|
-
reason: `Working tree is dirty. Commit or stash changes before running pilot.
|
|
822
|
-
${preview}`
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
return { ok: true };
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// src/pilot/scope.ts
|
|
829
|
-
function parseScopeArtifact(raw) {
|
|
830
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
831
|
-
const obj = raw;
|
|
832
|
-
if (typeof obj["workflow_id"] !== "string") return null;
|
|
833
|
-
if (typeof obj["goal"] !== "string") return null;
|
|
834
|
-
if (typeof obj["framing"] !== "string") return null;
|
|
835
|
-
if (!Array.isArray(obj["acceptance_criteria"])) return null;
|
|
836
|
-
const acs = [];
|
|
837
|
-
for (const ac of obj["acceptance_criteria"]) {
|
|
838
|
-
if (!ac || typeof ac !== "object") return null;
|
|
839
|
-
const a = ac;
|
|
840
|
-
if (typeof a["id"] !== "string") return null;
|
|
841
|
-
if (typeof a["description"] !== "string") return null;
|
|
842
|
-
const verifiable = a["verifiable"];
|
|
843
|
-
if (verifiable !== "shell" && verifiable !== "llm" && verifiable !== "manual") return null;
|
|
844
|
-
acs.push({ id: a["id"], description: a["description"], verifiable });
|
|
845
|
-
}
|
|
846
|
-
return {
|
|
847
|
-
workflow_id: obj["workflow_id"],
|
|
848
|
-
goal: obj["goal"],
|
|
849
|
-
framing: obj["framing"],
|
|
850
|
-
acceptance_criteria: acs,
|
|
851
|
-
non_goals: Array.isArray(obj["non_goals"]) ? obj["non_goals"].filter((x) => typeof x === "string") : [],
|
|
852
|
-
context: typeof obj["context"] === "string" ? obj["context"] : void 0
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
async function runScopePhase(opts) {
|
|
856
|
-
const { goal, cwd } = opts;
|
|
857
|
-
const safety = await checkSafety(cwd);
|
|
858
|
-
if (!safety.ok) {
|
|
859
|
-
return { ok: false, reason: safety.reason };
|
|
860
|
-
}
|
|
861
|
-
const config = loadPilotConfig(cwd);
|
|
862
|
-
const dbPath = await getStateDbPath(cwd);
|
|
863
|
-
const { db, close: closeDb } = openStateDb(dbPath);
|
|
864
|
-
const workflowId = createWorkflow(db, {
|
|
865
|
-
goal,
|
|
866
|
-
config: JSON.stringify(config)
|
|
867
|
-
});
|
|
868
|
-
const scopePath = await getScopeArtifactPath(cwd, workflowId);
|
|
869
|
-
logEvent(db, {
|
|
870
|
-
workflowId,
|
|
871
|
-
phase: "scope",
|
|
872
|
-
kind: "workflow.started",
|
|
873
|
-
payload: { id: workflowId, goal }
|
|
874
|
-
});
|
|
875
|
-
logEvent(db, {
|
|
876
|
-
workflowId,
|
|
877
|
-
phase: "scope",
|
|
878
|
-
kind: "task.scope.started",
|
|
879
|
-
payload: { scopePath }
|
|
880
|
-
});
|
|
881
|
-
const scoperPrompt = buildScopePrompt({ goal, scopePath, workflowId });
|
|
882
|
-
logEvent(db, {
|
|
883
|
-
workflowId,
|
|
884
|
-
phase: "scope",
|
|
885
|
-
kind: "task.scope.tui.spawning",
|
|
886
|
-
payload: { agent: "pilot-scoper" }
|
|
887
|
-
});
|
|
888
|
-
closeDb();
|
|
889
|
-
try {
|
|
890
|
-
const { spawn: spawn2 } = await import("child_process");
|
|
891
|
-
const scoperPrompt2 = buildScopePrompt({ goal, scopePath, workflowId });
|
|
892
|
-
const child = spawn2(
|
|
893
|
-
"opencode",
|
|
894
|
-
["--agent", "pilot-scoper", "--prompt", scoperPrompt2],
|
|
895
|
-
{
|
|
896
|
-
stdio: "inherit",
|
|
897
|
-
// TUI takes over the terminal
|
|
898
|
-
cwd,
|
|
899
|
-
env: { ...process.env }
|
|
900
|
-
}
|
|
901
|
-
);
|
|
902
|
-
const exitCode = await new Promise((resolve2) => {
|
|
903
|
-
child.on("close", (code) => resolve2(code ?? 1));
|
|
904
|
-
child.on("error", () => resolve2(1));
|
|
905
|
-
});
|
|
906
|
-
if (exitCode !== 0) {
|
|
907
|
-
return {
|
|
908
|
-
ok: false,
|
|
909
|
-
reason: `OpenCode TUI exited with code ${exitCode}. Scope session may have been interrupted.`
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
} catch (err) {
|
|
913
|
-
return {
|
|
914
|
-
ok: false,
|
|
915
|
-
reason: `Failed to spawn OpenCode TUI: ${err instanceof Error ? err.message : String(err)}`
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
const { db: db2, close: closeDb2 } = openStateDb(dbPath);
|
|
919
|
-
try {
|
|
920
|
-
if (!fs5.existsSync(scopePath)) {
|
|
921
|
-
logEvent(db2, {
|
|
922
|
-
workflowId,
|
|
923
|
-
phase: "scope",
|
|
924
|
-
kind: "task.scope.failed",
|
|
925
|
-
payload: { reason: "scope.json not produced" }
|
|
926
|
-
});
|
|
927
|
-
closeDb2();
|
|
928
|
-
return {
|
|
929
|
-
ok: false,
|
|
930
|
-
reason: `Scoper did not produce scope.json at ${scopePath}. The session may have ended without completing.`
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
let artifact;
|
|
934
|
-
try {
|
|
935
|
-
const raw = JSON.parse(fs5.readFileSync(scopePath, "utf8"));
|
|
936
|
-
artifact = parseScopeArtifact(raw);
|
|
937
|
-
} catch {
|
|
938
|
-
closeDb2();
|
|
939
|
-
return { ok: false, reason: `scope.json at ${scopePath} has invalid JSON` };
|
|
940
|
-
}
|
|
941
|
-
if (!artifact) {
|
|
942
|
-
closeDb2();
|
|
943
|
-
return { ok: false, reason: `scope.json at ${scopePath} has invalid schema` };
|
|
944
|
-
}
|
|
945
|
-
updateWorkflowStatus(db2, workflowId, "scoped", { scopePath });
|
|
946
|
-
const currentScopePath = await getCurrentScopePath(cwd);
|
|
947
|
-
fs5.writeFileSync(
|
|
948
|
-
currentScopePath,
|
|
949
|
-
JSON.stringify({ workflowId, scopePath }, null, 2) + "\n",
|
|
950
|
-
"utf8"
|
|
951
|
-
);
|
|
952
|
-
logEvent(db2, {
|
|
953
|
-
workflowId,
|
|
954
|
-
phase: "scope",
|
|
955
|
-
kind: "task.scope.completed",
|
|
956
|
-
payload: {
|
|
957
|
-
scopePath,
|
|
958
|
-
goal: artifact.goal,
|
|
959
|
-
ac_count: artifact.acceptance_criteria.length
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
return { ok: true, workflowId, scopePath, artifact };
|
|
963
|
-
} finally {
|
|
964
|
-
closeDb2();
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
function buildScopePrompt(opts) {
|
|
968
|
-
return `You are starting a new pilot workflow.
|
|
969
|
-
|
|
970
|
-
Workflow ID: ${opts.workflowId}
|
|
971
|
-
User's goal: ${opts.goal}
|
|
972
|
-
|
|
973
|
-
Your job:
|
|
974
|
-
1. Understand what the user wants to build through conversation.
|
|
975
|
-
2. Explore the codebase to understand the current state.
|
|
976
|
-
3. Produce a scope.json artifact at: ${opts.scopePath}
|
|
977
|
-
|
|
978
|
-
The scope.json must follow this schema:
|
|
979
|
-
{
|
|
980
|
-
"workflow_id": "${opts.workflowId}",
|
|
981
|
-
"goal": "one sentence",
|
|
982
|
-
"framing": "2-4 sentences: why this matters, what success looks like",
|
|
983
|
-
"acceptance_criteria": [
|
|
984
|
-
{ "id": "AC-001", "description": "behavioral, verifiable statement", "verifiable": "shell|llm|manual" }
|
|
985
|
-
],
|
|
986
|
-
"non_goals": ["what we are NOT doing"],
|
|
987
|
-
"context": "optional: key patterns, constraints, background for the planner"
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
Start by asking the user to tell you more about their goal. Then explore the codebase. Then draft acceptance criteria and confirm with the user before writing scope.json.`;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// src/pilot/cli/scope.ts
|
|
994
|
-
var scopeCmd = command2({
|
|
995
|
-
name: "scope",
|
|
996
|
-
description: "Start a new pilot workflow with interactive scoping. Produces scope.json for `pilot go`.",
|
|
997
|
-
args: {
|
|
998
|
-
goalWords: restPositionals({
|
|
999
|
-
type: string,
|
|
1000
|
-
displayName: "goal",
|
|
1001
|
-
description: "What you want to build (optional \u2014 will prompt if not provided)"
|
|
1002
|
-
})
|
|
1003
|
-
},
|
|
1004
|
-
handler: async ({ goalWords }) => {
|
|
1005
|
-
const cwd = process.cwd();
|
|
1006
|
-
let goal = goalWords.join(" ").trim();
|
|
1007
|
-
if (!goal) {
|
|
1008
|
-
if (!process.stdin.isTTY) {
|
|
1009
|
-
process.stderr.write("pilot scope: no goal provided and not running in a TTY.\n");
|
|
1010
|
-
process.stderr.write(' Usage: pilot scope "<what you want to build>"\n');
|
|
1011
|
-
process.exit(1);
|
|
1012
|
-
}
|
|
1013
|
-
goal = await input2({
|
|
1014
|
-
message: "What do you want to build?"
|
|
1015
|
-
});
|
|
1016
|
-
if (!goal.trim()) {
|
|
1017
|
-
process.stderr.write("pilot scope: goal cannot be empty.\n");
|
|
1018
|
-
process.exit(1);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
console.log(`
|
|
1022
|
-
\x1B[1mPilot v2 \u2014 Scope phase\x1B[0m`);
|
|
1023
|
-
console.log(`Goal: ${goal}
|
|
1024
|
-
`);
|
|
1025
|
-
console.log("Starting interactive scoping session...");
|
|
1026
|
-
console.log("The scoper will interview you and explore the codebase.");
|
|
1027
|
-
console.log("When done, it will produce scope.json for `pilot go`.\n");
|
|
1028
|
-
const result = await runScopePhase({ goal, cwd });
|
|
1029
|
-
if (!result.ok) {
|
|
1030
|
-
process.stderr.write(`
|
|
1031
|
-
\x1B[31m\u2717\x1B[0m Scope phase failed: ${result.reason}
|
|
1032
|
-
`);
|
|
1033
|
-
process.exit(1);
|
|
1370
|
+
fs6.readFile(src),
|
|
1371
|
+
fs6.readFile(dst)
|
|
1372
|
+
]);
|
|
1373
|
+
if (srcBuf.equals(dstBuf)) {
|
|
1374
|
+
await fs6.unlink(src);
|
|
1375
|
+
continue;
|
|
1034
1376
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
console.log(` Acceptance criteria: ${result.artifact.acceptance_criteria.length}`);
|
|
1040
|
-
console.log(` Scope: ${result.scopePath}`);
|
|
1041
|
-
console.log(`
|
|
1042
|
-
Run \x1B[1mpilot go\x1B[0m to start autonomous execution.
|
|
1043
|
-
`);
|
|
1044
|
-
process.exit(0);
|
|
1377
|
+
process.stderr.write(
|
|
1378
|
+
`[harness-opencode] migratePlans: conflict on ${name} \u2014 destination ${dst} exists with different content; leaving source ${src} in place. Resolve manually.
|
|
1379
|
+
`
|
|
1380
|
+
);
|
|
1045
1381
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
// src/pilot/cli/go.ts
|
|
1049
|
-
import { command as command3, option, string as stringType, optional } from "cmd-ts";
|
|
1382
|
+
await fs6.writeFile(marker, "");
|
|
1383
|
+
}
|
|
1050
1384
|
|
|
1051
|
-
// src/
|
|
1052
|
-
import
|
|
1385
|
+
// src/autopilot/cli.ts
|
|
1386
|
+
import { command, option, positional, string as stringType, optional, number as numberType } from "cmd-ts";
|
|
1053
1387
|
|
|
1054
|
-
// src/
|
|
1055
|
-
import { execFile as
|
|
1388
|
+
// src/autopilot/loop.ts
|
|
1389
|
+
import { execFile as execFileCb } from "child_process";
|
|
1056
1390
|
import { promisify as promisify2 } from "util";
|
|
1391
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1392
|
+
import { join as join8 } from "path";
|
|
1393
|
+
|
|
1394
|
+
// src/lib/opencode-server.ts
|
|
1395
|
+
import { execFile as execFile2 } from "child_process";
|
|
1396
|
+
import { promisify } from "util";
|
|
1057
1397
|
import {
|
|
1058
1398
|
createOpencodeServer,
|
|
1059
1399
|
createOpencodeClient
|
|
1060
1400
|
} from "@opencode-ai/sdk";
|
|
1061
|
-
var
|
|
1401
|
+
var execFileP2 = promisify(execFile2);
|
|
1062
1402
|
var DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
|
|
1063
1403
|
async function ensureOpencodeOnPath() {
|
|
1064
1404
|
try {
|
|
1065
|
-
await
|
|
1405
|
+
await execFileP2("opencode", ["--version"]);
|
|
1066
1406
|
} catch {
|
|
1067
1407
|
throw new Error(
|
|
1068
1408
|
"opencode CLI not found on PATH.\n Install: https://opencode.ai\n Or: bunx opencode upgrade"
|
|
@@ -1089,17 +1429,6 @@ async function startServer(opts) {
|
|
|
1089
1429
|
};
|
|
1090
1430
|
return { url: server.url, client, shutdown };
|
|
1091
1431
|
}
|
|
1092
|
-
async function selfTest(client) {
|
|
1093
|
-
try {
|
|
1094
|
-
await client.session.list();
|
|
1095
|
-
} catch (err) {
|
|
1096
|
-
throw new Error(
|
|
1097
|
-
`OpenCode server self-test failed \u2014 the server started but isn't responding to API calls.
|
|
1098
|
-
Error: ${err instanceof Error ? err.message : String(err)}
|
|
1099
|
-
Run \`opencode --version\` to verify your installation.`
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
1432
|
async function createSession(client, opts) {
|
|
1104
1433
|
const session = await client.session.create({
|
|
1105
1434
|
body: {
|
|
@@ -1179,904 +1508,262 @@ async function waitForIdle(client, opts) {
|
|
|
1179
1508
|
};
|
|
1180
1509
|
});
|
|
1181
1510
|
}
|
|
1182
|
-
|
|
1183
|
-
// src/pilot/plan.ts
|
|
1184
|
-
import * as fs6 from "fs";
|
|
1185
|
-
function parsePlanArtifact(raw) {
|
|
1186
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
1187
|
-
const obj = raw;
|
|
1188
|
-
if (typeof obj["workflow_id"] !== "string") return null;
|
|
1189
|
-
if (!Array.isArray(obj["tasks"])) return null;
|
|
1190
|
-
const tasks = [];
|
|
1191
|
-
for (const t of obj["tasks"]) {
|
|
1192
|
-
if (!t || typeof t !== "object") return null;
|
|
1193
|
-
const task = t;
|
|
1194
|
-
if (typeof task["id"] !== "string") return null;
|
|
1195
|
-
if (typeof task["title"] !== "string") return null;
|
|
1196
|
-
if (typeof task["prompt"] !== "string") return null;
|
|
1197
|
-
tasks.push({
|
|
1198
|
-
id: task["id"],
|
|
1199
|
-
title: task["title"],
|
|
1200
|
-
prompt: task["prompt"],
|
|
1201
|
-
addresses: Array.isArray(task["addresses"]) ? task["addresses"].filter((x) => typeof x === "string") : [],
|
|
1202
|
-
verify: Array.isArray(task["verify"]) ? task["verify"].filter((x) => typeof x === "string") : []
|
|
1203
|
-
});
|
|
1204
|
-
}
|
|
1205
|
-
return { workflow_id: obj["workflow_id"], tasks };
|
|
1206
|
-
}
|
|
1207
|
-
async function runPlanPhase(opts) {
|
|
1208
|
-
const { workflowId, scope, cwd, server } = opts;
|
|
1209
|
-
const dbPath = await getStateDbPath(cwd);
|
|
1210
|
-
const { db, close: closeDb } = openStateDb(dbPath);
|
|
1211
|
-
const planPath = await getPlanArtifactPath(cwd, workflowId);
|
|
1212
|
-
logEvent(db, {
|
|
1213
|
-
workflowId,
|
|
1214
|
-
phase: "plan",
|
|
1215
|
-
kind: "task.plan.started",
|
|
1216
|
-
payload: { planPath }
|
|
1217
|
-
});
|
|
1218
|
-
try {
|
|
1219
|
-
const sessionId = await createSession(server.client, {
|
|
1220
|
-
cwd,
|
|
1221
|
-
agentName: "pilot-planner"
|
|
1222
|
-
});
|
|
1223
|
-
logEvent(db, {
|
|
1224
|
-
workflowId,
|
|
1225
|
-
phase: "plan",
|
|
1226
|
-
kind: "task.plan.session.created",
|
|
1227
|
-
payload: { sessionId },
|
|
1228
|
-
sessionId
|
|
1229
|
-
});
|
|
1230
|
-
const plannerPrompt = buildPlannerPrompt({ workflowId, scope, planPath });
|
|
1231
|
-
const result = await sendAndWait(server.client, {
|
|
1232
|
-
sessionId,
|
|
1233
|
-
message: plannerPrompt,
|
|
1234
|
-
stallMs: 10 * 60 * 1e3
|
|
1235
|
-
// 10 min
|
|
1236
|
-
});
|
|
1237
|
-
if (result.kind !== "idle") {
|
|
1238
|
-
logEvent(db, {
|
|
1239
|
-
workflowId,
|
|
1240
|
-
phase: "plan",
|
|
1241
|
-
kind: "task.plan.failed",
|
|
1242
|
-
payload: { reason: result.kind },
|
|
1243
|
-
sessionId
|
|
1244
|
-
});
|
|
1245
|
-
return { ok: false, reason: `Planner session ended unexpectedly: ${result.kind}` };
|
|
1246
|
-
}
|
|
1247
|
-
if (!fs6.existsSync(planPath)) {
|
|
1248
|
-
return { ok: false, reason: `Planner did not produce plan.json at ${planPath}` };
|
|
1249
|
-
}
|
|
1250
|
-
let artifact;
|
|
1251
|
-
try {
|
|
1252
|
-
const raw = JSON.parse(fs6.readFileSync(planPath, "utf8"));
|
|
1253
|
-
artifact = parsePlanArtifact(raw);
|
|
1254
|
-
} catch {
|
|
1255
|
-
return { ok: false, reason: `plan.json at ${planPath} has invalid JSON` };
|
|
1256
|
-
}
|
|
1257
|
-
if (!artifact) {
|
|
1258
|
-
return { ok: false, reason: `plan.json at ${planPath} has invalid schema` };
|
|
1259
|
-
}
|
|
1260
|
-
updateWorkflowStatus(db, workflowId, "planned", { planPath });
|
|
1261
|
-
logEvent(db, {
|
|
1262
|
-
workflowId,
|
|
1263
|
-
phase: "plan",
|
|
1264
|
-
kind: "task.plan.completed",
|
|
1265
|
-
payload: { planPath, task_count: artifact.tasks.length },
|
|
1266
|
-
sessionId
|
|
1267
|
-
});
|
|
1268
|
-
return { ok: true, planPath, artifact };
|
|
1269
|
-
} finally {
|
|
1270
|
-
closeDb();
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
function buildPlannerPrompt(opts) {
|
|
1274
|
-
const { workflowId, scope, planPath } = opts;
|
|
1275
|
-
const acsText = scope.acceptance_criteria.map((ac) => ` - ${ac.id}: ${ac.description} (verifiable: ${ac.verifiable})`).join("\n");
|
|
1276
|
-
const nonGoalsText = scope.non_goals.length > 0 ? scope.non_goals.map((ng) => ` - ${ng}`).join("\n") : " (none specified)";
|
|
1277
|
-
return `You are planning a pilot workflow.
|
|
1278
|
-
|
|
1279
|
-
Workflow ID: ${workflowId}
|
|
1280
|
-
Goal: ${scope.goal}
|
|
1281
|
-
Framing: ${scope.framing}
|
|
1282
|
-
|
|
1283
|
-
Acceptance criteria:
|
|
1284
|
-
${acsText}
|
|
1285
|
-
|
|
1286
|
-
Non-goals:
|
|
1287
|
-
${nonGoalsText}
|
|
1288
|
-
|
|
1289
|
-
${scope.context ? `Context:
|
|
1290
|
-
${scope.context}
|
|
1291
|
-
|
|
1292
|
-
` : ""}Your job:
|
|
1293
|
-
1. Survey the codebase to understand the current state.
|
|
1294
|
-
2. Decompose the work into an ordered list of tasks.
|
|
1295
|
-
3. Write plan.json at: ${planPath}
|
|
1296
|
-
|
|
1297
|
-
The plan.json must follow this schema:
|
|
1298
|
-
{
|
|
1299
|
-
"workflow_id": "${workflowId}",
|
|
1300
|
-
"tasks": [
|
|
1301
|
-
{
|
|
1302
|
-
"id": "TASK-001",
|
|
1303
|
-
"title": "Short title",
|
|
1304
|
-
"prompt": "Detailed self-contained instructions for the builder",
|
|
1305
|
-
"addresses": ["AC-001"],
|
|
1306
|
-
"verify": ["bun test src/specific.test.ts"]
|
|
1307
|
-
}
|
|
1308
|
-
]
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
Rules:
|
|
1312
|
-
- Each task must be independently executable.
|
|
1313
|
-
- Each task's prompt must be self-contained (include relevant context).
|
|
1314
|
-
- Every AC must be addressed by at least one task.
|
|
1315
|
-
- Tasks are executed sequentially \u2014 order matters.
|
|
1316
|
-
- Aim for 3-7 tasks. More than 10 is too many.`;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
// src/pilot/execute.ts
|
|
1320
|
-
import { execFile as execFile4 } from "child_process";
|
|
1321
|
-
import { promisify as promisify3 } from "util";
|
|
1322
|
-
var execFileP4 = promisify3(execFile4);
|
|
1323
|
-
async function runExecutePhase(opts) {
|
|
1324
|
-
const { workflowId, scope, plan, cwd, server } = opts;
|
|
1325
|
-
const dbPath = await getStateDbPath(cwd);
|
|
1326
|
-
const { db, close: closeDb } = openStateDb(dbPath);
|
|
1327
|
-
updateWorkflowStatus(db, workflowId, "executing");
|
|
1328
|
-
logEvent(db, {
|
|
1329
|
-
workflowId,
|
|
1330
|
-
phase: "execute",
|
|
1331
|
-
kind: "task.execute.phase.started",
|
|
1332
|
-
payload: { task_count: plan.tasks.length }
|
|
1333
|
-
});
|
|
1334
|
-
const taskResults = [];
|
|
1335
|
-
try {
|
|
1336
|
-
for (let i = 0; i < plan.tasks.length; i++) {
|
|
1337
|
-
const task = plan.tasks[i];
|
|
1338
|
-
const taskNum = `${i + 1}/${plan.tasks.length}`;
|
|
1339
|
-
logEvent(db, {
|
|
1340
|
-
workflowId,
|
|
1341
|
-
phase: "execute",
|
|
1342
|
-
kind: "task.execute.started",
|
|
1343
|
-
payload: { task: taskNum, id: task.id, title: task.title },
|
|
1344
|
-
taskId: task.id
|
|
1345
|
-
});
|
|
1346
|
-
const result = await runOneTask({
|
|
1347
|
-
workflowId,
|
|
1348
|
-
task,
|
|
1349
|
-
taskNum,
|
|
1350
|
-
scope,
|
|
1351
|
-
cwd,
|
|
1352
|
-
server,
|
|
1353
|
-
db
|
|
1354
|
-
});
|
|
1355
|
-
taskResults.push(result);
|
|
1356
|
-
if (!result.ok) {
|
|
1357
|
-
logEvent(db, {
|
|
1358
|
-
workflowId,
|
|
1359
|
-
phase: "execute",
|
|
1360
|
-
kind: "task.execute.phase.failed",
|
|
1361
|
-
payload: { failed_task: task.id, reason: result.reason },
|
|
1362
|
-
taskId: task.id
|
|
1363
|
-
});
|
|
1364
|
-
return { ok: false, reason: `Task ${task.id} failed: ${result.reason}`, taskResults };
|
|
1365
|
-
}
|
|
1366
|
-
logEvent(db, {
|
|
1367
|
-
workflowId,
|
|
1368
|
-
phase: "execute",
|
|
1369
|
-
kind: "task.execute.completed",
|
|
1370
|
-
payload: { task: taskNum, id: task.id, commit: result.commitSha },
|
|
1371
|
-
taskId: task.id
|
|
1372
|
-
});
|
|
1373
|
-
}
|
|
1374
|
-
logEvent(db, {
|
|
1375
|
-
workflowId,
|
|
1376
|
-
phase: "execute",
|
|
1377
|
-
kind: "task.execute.phase.completed",
|
|
1378
|
-
payload: { task_count: plan.tasks.length }
|
|
1379
|
-
});
|
|
1380
|
-
return { ok: true, taskResults };
|
|
1381
|
-
} finally {
|
|
1382
|
-
closeDb();
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
async function runOneTask(opts) {
|
|
1386
|
-
const { workflowId, task, taskNum, scope, cwd, server, db } = opts;
|
|
1387
|
-
let headBefore;
|
|
1388
|
-
try {
|
|
1389
|
-
const { stdout } = await execFileP4("git", ["rev-parse", "HEAD"], { cwd });
|
|
1390
|
-
headBefore = stdout.trim();
|
|
1391
|
-
} catch {
|
|
1392
|
-
return { ok: false, taskId: task.id, reason: "Could not get HEAD SHA before task" };
|
|
1393
|
-
}
|
|
1394
|
-
let sessionId;
|
|
1395
|
-
try {
|
|
1396
|
-
sessionId = await createSession(server.client, {
|
|
1397
|
-
cwd,
|
|
1398
|
-
agentName: "pilot-builder"
|
|
1399
|
-
});
|
|
1400
|
-
} catch (err) {
|
|
1401
|
-
return {
|
|
1402
|
-
ok: false,
|
|
1403
|
-
taskId: task.id,
|
|
1404
|
-
reason: `Failed to create builder session: ${err instanceof Error ? err.message : String(err)}`
|
|
1405
|
-
};
|
|
1406
|
-
}
|
|
1407
|
-
const taskPrompt = buildTaskPrompt({ task, scope, workflowId });
|
|
1408
|
-
const result = await sendAndWait(server.client, {
|
|
1409
|
-
sessionId,
|
|
1410
|
-
message: taskPrompt,
|
|
1411
|
-
stallMs: 15 * 60 * 1e3
|
|
1412
|
-
// 15 min per task
|
|
1413
|
-
});
|
|
1414
|
-
if (result.kind !== "idle") {
|
|
1415
|
-
await cleanWorkingTree(cwd);
|
|
1416
|
-
return {
|
|
1417
|
-
ok: false,
|
|
1418
|
-
taskId: task.id,
|
|
1419
|
-
reason: `Builder session ended unexpectedly: ${result.kind}`
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
|
-
const verifyResult = await runVerifyCommands(task.verify, cwd);
|
|
1423
|
-
if (!verifyResult.ok) {
|
|
1424
|
-
await cleanWorkingTree(cwd);
|
|
1425
|
-
return {
|
|
1426
|
-
ok: false,
|
|
1427
|
-
taskId: task.id,
|
|
1428
|
-
reason: `Verify failed: ${verifyResult.reason}`
|
|
1429
|
-
};
|
|
1430
|
-
}
|
|
1431
|
-
let commitSha;
|
|
1432
|
-
try {
|
|
1433
|
-
const { stdout: diffStat } = await execFileP4("git", ["diff", "--name-only", "HEAD"], { cwd });
|
|
1434
|
-
const { stdout: untrackedRaw } = await execFileP4("git", ["ls-files", "--others", "--exclude-standard"], { cwd });
|
|
1435
|
-
const modifiedFiles = diffStat.trim().split("\n").filter(Boolean);
|
|
1436
|
-
const untrackedFiles = untrackedRaw.trim().split("\n").filter(Boolean);
|
|
1437
|
-
const allFiles = [...modifiedFiles, ...untrackedFiles];
|
|
1438
|
-
if (allFiles.length > 20) {
|
|
1439
|
-
process.stderr.write(
|
|
1440
|
-
` [pilot] \u26A0\uFE0F Task ${task.id} modified ${allFiles.length} files \u2014 review the commit carefully
|
|
1441
|
-
`
|
|
1442
|
-
);
|
|
1443
|
-
}
|
|
1444
|
-
await execFileP4("git", ["add", "-A"], { cwd });
|
|
1445
|
-
await execFileP4("git", ["commit", "-m", `pilot: ${task.title} (${task.id})`], { cwd });
|
|
1446
|
-
const { stdout } = await execFileP4("git", ["rev-parse", "HEAD"], { cwd });
|
|
1447
|
-
commitSha = stdout.trim();
|
|
1448
|
-
} catch (err) {
|
|
1449
|
-
await cleanWorkingTree(cwd);
|
|
1450
|
-
return {
|
|
1451
|
-
ok: false,
|
|
1452
|
-
taskId: task.id,
|
|
1453
|
-
reason: `Commit failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1454
|
-
};
|
|
1455
|
-
}
|
|
1456
|
-
return { ok: true, taskId: task.id, commitSha };
|
|
1457
|
-
}
|
|
1458
|
-
async function runVerifyCommands(commands, cwd) {
|
|
1459
|
-
for (const cmd2 of commands) {
|
|
1460
|
-
try {
|
|
1461
|
-
await execFileP4("bash", ["-c", cmd2], { cwd });
|
|
1462
|
-
} catch (err) {
|
|
1463
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1464
|
-
return { ok: false, reason: `Command "${cmd2}" failed: ${msg}` };
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
return { ok: true };
|
|
1468
|
-
}
|
|
1469
|
-
async function cleanWorkingTree(cwd) {
|
|
1511
|
+
async function getLastAssistantMessage(client, sessionId) {
|
|
1470
1512
|
try {
|
|
1471
|
-
await
|
|
1472
|
-
|
|
1513
|
+
const messages = await client.session.messages({ path: { id: sessionId } });
|
|
1514
|
+
const assistantMessages = messages.filter((m) => m.info.role === "assistant");
|
|
1515
|
+
if (assistantMessages.length === 0) return "";
|
|
1516
|
+
const last = assistantMessages[assistantMessages.length - 1];
|
|
1517
|
+
return last.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
1473
1518
|
} catch {
|
|
1519
|
+
return "";
|
|
1474
1520
|
}
|
|
1475
1521
|
}
|
|
1476
|
-
function buildTaskPrompt(opts) {
|
|
1477
|
-
const { task, scope, workflowId } = opts;
|
|
1478
|
-
const verifyText = task.verify.length > 0 ? task.verify.map((v) => ` - ${v}`).join("\n") : " (no verify commands \u2014 just make the changes)";
|
|
1479
|
-
const addressesText = task.addresses.length > 0 ? task.addresses.join(", ") : "(none specified)";
|
|
1480
|
-
return `You are executing a pilot task.
|
|
1481
|
-
|
|
1482
|
-
Workflow: ${workflowId}
|
|
1483
|
-
Task: ${task.id} \u2014 ${task.title}
|
|
1484
|
-
Addresses: ${addressesText}
|
|
1485
1522
|
|
|
1486
|
-
|
|
1523
|
+
// src/autopilot/config.ts
|
|
1524
|
+
var MAX_ITERATIONS = 50;
|
|
1525
|
+
var STRUGGLE_THRESHOLD = 3;
|
|
1526
|
+
var TIMEOUT_MS = 4 * 60 * 60 * 1e3;
|
|
1527
|
+
var STALL_MS = 60 * 60 * 1e3;
|
|
1528
|
+
var KILL_SWITCH_PATH = ".agent/autopilot-disable";
|
|
1529
|
+
var SENTINEL_TAG = "<autopilot-done>";
|
|
1487
1530
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
- If you cannot proceed after 3 attempts, output: STOP: <reason>`;
|
|
1531
|
+
// src/autopilot/sentinel.ts
|
|
1532
|
+
function detectSentinel(text) {
|
|
1533
|
+
if (!text.includes(SENTINEL_TAG)) {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
const withoutFences = text.replace(/```[\s\S]*?```/g, "");
|
|
1537
|
+
const withoutInline = withoutFences.replace(/`[^`\n]*`/g, "");
|
|
1538
|
+
return withoutInline.includes(SENTINEL_TAG);
|
|
1497
1539
|
}
|
|
1498
1540
|
|
|
1499
|
-
// src/
|
|
1541
|
+
// src/autopilot/struggle.ts
|
|
1500
1542
|
import * as fs7 from "fs";
|
|
1501
|
-
import * as
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
gap: typeof result["gap"] === "string" ? result["gap"] : void 0
|
|
1520
|
-
});
|
|
1521
|
-
}
|
|
1522
|
-
const risks = [];
|
|
1523
|
-
if (Array.isArray(obj["deployment_risks"])) {
|
|
1524
|
-
for (const r of obj["deployment_risks"]) {
|
|
1525
|
-
if (!r || typeof r !== "object") continue;
|
|
1526
|
-
const risk = r;
|
|
1527
|
-
if (!["high", "medium", "low"].includes(risk["severity"])) continue;
|
|
1528
|
-
if (typeof risk["description"] !== "string") continue;
|
|
1529
|
-
risks.push({
|
|
1530
|
-
severity: risk["severity"],
|
|
1531
|
-
description: risk["description"],
|
|
1532
|
-
actionable: Boolean(risk["actionable"]),
|
|
1533
|
-
suggested_fix: typeof risk["suggested_fix"] === "string" ? risk["suggested_fix"] : void 0
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
return {
|
|
1538
|
-
workflow_id: obj["workflow_id"],
|
|
1539
|
-
verdict: obj["verdict"],
|
|
1540
|
-
ac_results: acResults,
|
|
1541
|
-
deployment_risks: risks,
|
|
1542
|
-
replan_guidance: typeof obj["replan_guidance"] === "string" ? obj["replan_guidance"] : void 0
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
async function runAssessPhase(opts) {
|
|
1546
|
-
const { workflowId, scope, plan, cwd, cycle, server } = opts;
|
|
1547
|
-
const dbPath = await getStateDbPath(cwd);
|
|
1548
|
-
const { db, close: closeDb } = openStateDb(dbPath);
|
|
1549
|
-
updateWorkflowStatus(db, workflowId, "assessing");
|
|
1550
|
-
const assessPath = await getAssessArtifactPath(cwd, workflowId, cycle);
|
|
1551
|
-
logEvent(db, {
|
|
1552
|
-
workflowId,
|
|
1553
|
-
phase: "assess",
|
|
1554
|
-
kind: "task.assess.started",
|
|
1555
|
-
payload: { cycle, assessPath }
|
|
1556
|
-
});
|
|
1557
|
-
try {
|
|
1558
|
-
const sessionId = await createSession(server.client, {
|
|
1559
|
-
cwd,
|
|
1560
|
-
agentName: "pilot-assessor"
|
|
1561
|
-
});
|
|
1562
|
-
logEvent(db, {
|
|
1563
|
-
workflowId,
|
|
1564
|
-
phase: "assess",
|
|
1565
|
-
kind: "task.assess.session.created",
|
|
1566
|
-
payload: { sessionId, cycle },
|
|
1567
|
-
sessionId
|
|
1568
|
-
});
|
|
1569
|
-
const assessorPrompt = buildAssessorPrompt({ workflowId, scope, plan, assessPath, cycle });
|
|
1570
|
-
const result = await sendAndWait(server.client, {
|
|
1571
|
-
sessionId,
|
|
1572
|
-
message: assessorPrompt,
|
|
1573
|
-
stallMs: 10 * 60 * 1e3
|
|
1574
|
-
// 10 min
|
|
1575
|
-
});
|
|
1576
|
-
if (result.kind !== "idle") {
|
|
1577
|
-
logEvent(db, {
|
|
1578
|
-
workflowId,
|
|
1579
|
-
phase: "assess",
|
|
1580
|
-
kind: "task.assess.failed",
|
|
1581
|
-
payload: { reason: result.kind, cycle },
|
|
1582
|
-
sessionId
|
|
1583
|
-
});
|
|
1584
|
-
return { ok: false, reason: `Assessor session ended unexpectedly: ${result.kind}` };
|
|
1585
|
-
}
|
|
1586
|
-
if (!fs7.existsSync(assessPath)) {
|
|
1587
|
-
return { ok: false, reason: `Assessor did not produce assessment report at ${assessPath}` };
|
|
1588
|
-
}
|
|
1589
|
-
let artifact;
|
|
1590
|
-
try {
|
|
1591
|
-
const raw = JSON.parse(fs7.readFileSync(assessPath, "utf8"));
|
|
1592
|
-
artifact = parseAssessmentArtifact(raw);
|
|
1593
|
-
} catch {
|
|
1594
|
-
return { ok: false, reason: `Assessment report has invalid JSON` };
|
|
1595
|
-
}
|
|
1596
|
-
if (!artifact) {
|
|
1597
|
-
return { ok: false, reason: `Assessment report has invalid schema` };
|
|
1598
|
-
}
|
|
1599
|
-
for (const acResult of artifact.ac_results) {
|
|
1600
|
-
const kind = acResult.status === "met" ? "task.assess.gate.passed" : "task.assess.gate.failed";
|
|
1601
|
-
logEvent(db, {
|
|
1602
|
-
workflowId,
|
|
1603
|
-
phase: "assess",
|
|
1604
|
-
kind,
|
|
1605
|
-
payload: {
|
|
1606
|
-
gate: acResult.id,
|
|
1607
|
-
status: acResult.status,
|
|
1608
|
-
...acResult.gap ? { reason: acResult.gap } : {}
|
|
1609
|
-
},
|
|
1610
|
-
sessionId
|
|
1611
|
-
});
|
|
1612
|
-
}
|
|
1613
|
-
const highRisks = artifact.deployment_risks.filter((r) => r.severity === "high" && r.actionable);
|
|
1614
|
-
if (highRisks.length > 0) {
|
|
1615
|
-
logEvent(db, {
|
|
1616
|
-
workflowId,
|
|
1617
|
-
phase: "assess",
|
|
1618
|
-
kind: "task.assess.risk_check",
|
|
1619
|
-
payload: { risks: highRisks.map((r) => r.description) },
|
|
1620
|
-
sessionId
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
if (artifact.verdict === "pass") {
|
|
1624
|
-
logEvent(db, {
|
|
1625
|
-
workflowId,
|
|
1626
|
-
phase: "assess",
|
|
1627
|
-
kind: "task.assess.passed",
|
|
1628
|
-
payload: { all_acs_met: true, cycle },
|
|
1629
|
-
sessionId
|
|
1630
|
-
});
|
|
1631
|
-
return { ok: true, verdict: "pass", artifact };
|
|
1543
|
+
import * as path7 from "path";
|
|
1544
|
+
var StruggleDetector = class {
|
|
1545
|
+
_consecutiveStalls = 0;
|
|
1546
|
+
_threshold;
|
|
1547
|
+
constructor(threshold) {
|
|
1548
|
+
this._threshold = threshold;
|
|
1549
|
+
}
|
|
1550
|
+
/** Number of consecutive stall iterations recorded so far. */
|
|
1551
|
+
get consecutiveStalls() {
|
|
1552
|
+
return this._consecutiveStalls;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Record the result of one iteration.
|
|
1556
|
+
* @param madeProgress - true if the agent made filesystem changes this iteration.
|
|
1557
|
+
*/
|
|
1558
|
+
record(madeProgress) {
|
|
1559
|
+
if (madeProgress) {
|
|
1560
|
+
this._consecutiveStalls = 0;
|
|
1632
1561
|
} else {
|
|
1633
|
-
|
|
1634
|
-
logEvent(db, {
|
|
1635
|
-
workflowId,
|
|
1636
|
-
phase: "assess",
|
|
1637
|
-
kind: "task.assess.failed",
|
|
1638
|
-
payload: { unmet: unmetAcs, cycle },
|
|
1639
|
-
sessionId
|
|
1640
|
-
});
|
|
1641
|
-
return {
|
|
1642
|
-
ok: true,
|
|
1643
|
-
verdict: "fail",
|
|
1644
|
-
artifact,
|
|
1645
|
-
replanGuidance: artifact.replan_guidance ?? `Unmet ACs: ${unmetAcs.join(", ")}`
|
|
1646
|
-
};
|
|
1562
|
+
this._consecutiveStalls++;
|
|
1647
1563
|
}
|
|
1648
|
-
} finally {
|
|
1649
|
-
closeDb();
|
|
1650
1564
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
}
|
|
1658
|
-
function buildAssessorPrompt(opts) {
|
|
1659
|
-
const { workflowId, scope, plan, assessPath, cycle } = opts;
|
|
1660
|
-
const acsText = scope.acceptance_criteria.map((ac) => ` - ${ac.id}: ${ac.description} (verifiable: ${ac.verifiable})`).join("\n");
|
|
1661
|
-
return `You are assessing a pilot workflow.
|
|
1662
|
-
|
|
1663
|
-
Workflow: ${workflowId}
|
|
1664
|
-
Goal: ${scope.goal}
|
|
1665
|
-
Assessment cycle: ${cycle}
|
|
1666
|
-
|
|
1667
|
-
Acceptance criteria to evaluate:
|
|
1668
|
-
${acsText}
|
|
1669
|
-
|
|
1670
|
-
Your job:
|
|
1671
|
-
1. FIRST: Deployment-risk reflection. Ask yourself:
|
|
1672
|
-
- What could break when this deploys?
|
|
1673
|
-
- What unexpected consequences could this change have on existing functionality?
|
|
1674
|
-
- What could go wrong?
|
|
1675
|
-
|
|
1676
|
-
2. THEN: Evaluate each AC against the current state of the codebase.
|
|
1677
|
-
- Run verify commands from the plan.
|
|
1678
|
-
- Check the git diff to see what changed.
|
|
1679
|
-
- For shell-verifiable ACs, run the commands.
|
|
1680
|
-
- For llm-verifiable ACs, use your judgment.
|
|
1681
|
-
|
|
1682
|
-
3. Write your assessment to: ${assessPath}
|
|
1683
|
-
|
|
1684
|
-
The assessment must follow this schema:
|
|
1685
|
-
{
|
|
1686
|
-
"workflow_id": "${workflowId}",
|
|
1687
|
-
"verdict": "pass|fail",
|
|
1688
|
-
"ac_results": [
|
|
1689
|
-
{ "id": "AC-001", "status": "met|unmet|partial", "evidence": "what you observed", "gap": "if unmet: what's missing" }
|
|
1690
|
-
],
|
|
1691
|
-
"deployment_risks": [
|
|
1692
|
-
{ "severity": "high|medium|low", "description": "what could go wrong", "actionable": true, "suggested_fix": "optional" }
|
|
1693
|
-
],
|
|
1694
|
-
"replan_guidance": "if verdict=fail: specific guidance for the re-planner"
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
Verdict is "pass" only if ALL ACs are "met" AND no high-severity actionable risks exist.`;
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
// src/pilot/resolve.ts
|
|
1701
|
-
async function runResolvePhase(opts) {
|
|
1702
|
-
const { workflowId, scope, assessment, cwd, startedAt } = opts;
|
|
1703
|
-
const dbPath = await getStateDbPath(cwd);
|
|
1704
|
-
const { db, close: closeDb } = openStateDb(dbPath);
|
|
1705
|
-
logEvent(db, {
|
|
1706
|
-
workflowId,
|
|
1707
|
-
phase: "resolve",
|
|
1708
|
-
kind: "task.resolve.started",
|
|
1709
|
-
payload: {}
|
|
1710
|
-
});
|
|
1711
|
-
const acknowledgedRisks = assessment.deployment_risks.filter((r) => !r.actionable || r.severity !== "high").map((r) => r.description);
|
|
1712
|
-
if (acknowledgedRisks.length > 0) {
|
|
1713
|
-
logEvent(db, {
|
|
1714
|
-
workflowId,
|
|
1715
|
-
phase: "resolve",
|
|
1716
|
-
kind: "task.resolve.acknowledged_risks",
|
|
1717
|
-
payload: { risks: acknowledgedRisks }
|
|
1718
|
-
});
|
|
1565
|
+
/**
|
|
1566
|
+
* Returns true if the agent has stalled for `threshold` consecutive
|
|
1567
|
+
* iterations without making progress.
|
|
1568
|
+
*/
|
|
1569
|
+
isStruggling() {
|
|
1570
|
+
return this._consecutiveStalls >= this._threshold;
|
|
1719
1571
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
phase: "resolve",
|
|
1725
|
-
kind: "task.resolve.completed",
|
|
1726
|
-
payload: { acknowledged_risks: acknowledgedRisks.length }
|
|
1727
|
-
});
|
|
1728
|
-
logEvent(db, {
|
|
1729
|
-
workflowId,
|
|
1730
|
-
phase: "resolve",
|
|
1731
|
-
kind: "workflow.completed",
|
|
1732
|
-
payload: {
|
|
1733
|
-
duration: `${Math.round(durationMs / 1e3)}s`
|
|
1734
|
-
}
|
|
1735
|
-
});
|
|
1736
|
-
closeDb();
|
|
1737
|
-
return {
|
|
1738
|
-
workflowId,
|
|
1739
|
-
goal: scope.goal,
|
|
1740
|
-
durationMs,
|
|
1741
|
-
acknowledgedRisks
|
|
1742
|
-
};
|
|
1572
|
+
};
|
|
1573
|
+
function checkKillSwitch(cwd) {
|
|
1574
|
+
const killSwitchFile = path7.join(cwd, KILL_SWITCH_PATH);
|
|
1575
|
+
return fs7.existsSync(killSwitchFile);
|
|
1743
1576
|
}
|
|
1744
1577
|
|
|
1745
|
-
// src/
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
const
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
const
|
|
1754
|
-
const { getPilotConfigPath: getPilotConfigPath2 } = await import("./paths-WZ23ZQOV.js");
|
|
1755
|
-
const configPath = getPilotConfigPath2(cwd);
|
|
1756
|
-
if (fs8.existsSync(configPath)) {
|
|
1578
|
+
// src/autopilot/loop.ts
|
|
1579
|
+
var execFile3 = promisify2(execFileCb);
|
|
1580
|
+
function buildFullPrompt(userPrompt) {
|
|
1581
|
+
const candidates = [
|
|
1582
|
+
join8(import.meta.dir, "prompt-template.md"),
|
|
1583
|
+
join8(import.meta.dir, "..", "..", "src", "autopilot", "prompt-template.md")
|
|
1584
|
+
];
|
|
1585
|
+
let template = "";
|
|
1586
|
+
for (const candidate of candidates) {
|
|
1757
1587
|
try {
|
|
1758
|
-
const raw =
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
"\n\x1B[33m\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 \u26A0\uFE0F Old pilot v1 config detected (.glrs/pilot.json) \u2502\n\u2502 Run `pilot configure` to set up v2 configuration. \u2502\n\u2502 Using defaults until then. \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\x1B[0m\n"
|
|
1762
|
-
);
|
|
1763
|
-
}
|
|
1588
|
+
const raw = readFileSync6(candidate, "utf8");
|
|
1589
|
+
template = raw.replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
1590
|
+
break;
|
|
1764
1591
|
} catch {
|
|
1765
1592
|
}
|
|
1766
1593
|
}
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
reason: 'No scope found. Run `pilot scope "<goal>"` first.'
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
let scope;
|
|
1594
|
+
const withArgs = template.replace("$ARGUMENTS", userPrompt);
|
|
1595
|
+
return withArgs || userPrompt;
|
|
1596
|
+
}
|
|
1597
|
+
async function checkProgress(cwd, baseRef) {
|
|
1775
1598
|
try {
|
|
1776
|
-
const
|
|
1777
|
-
|
|
1778
|
-
if (!parsed) {
|
|
1779
|
-
return { ok: false, reason: `scope.json at ${scopePath} has invalid schema` };
|
|
1780
|
-
}
|
|
1781
|
-
scope = parsed;
|
|
1599
|
+
const { stdout } = await execFile3("git", ["diff", "--stat", baseRef], { cwd });
|
|
1600
|
+
return stdout.trim().length > 0;
|
|
1782
1601
|
} catch {
|
|
1783
|
-
return
|
|
1784
|
-
}
|
|
1785
|
-
const workflowId = scope.workflow_id;
|
|
1786
|
-
const dbPath = await getStateDbPath(cwd);
|
|
1787
|
-
const { db, close: closeDb } = openStateDb(dbPath);
|
|
1788
|
-
logEvent(db, {
|
|
1789
|
-
workflowId,
|
|
1790
|
-
phase: "plan",
|
|
1791
|
-
kind: "workflow.go.started",
|
|
1792
|
-
payload: { goal: scope.goal, scopePath }
|
|
1793
|
-
});
|
|
1794
|
-
closeDb();
|
|
1795
|
-
let server;
|
|
1796
|
-
try {
|
|
1797
|
-
server = await startServer({ cwd });
|
|
1798
|
-
await selfTest(server.client);
|
|
1799
|
-
} catch (err) {
|
|
1800
|
-
return {
|
|
1801
|
-
ok: false,
|
|
1802
|
-
reason: `Failed to start OpenCode server: ${err instanceof Error ? err.message : String(err)}`,
|
|
1803
|
-
workflowId
|
|
1804
|
-
};
|
|
1602
|
+
return true;
|
|
1805
1603
|
}
|
|
1604
|
+
}
|
|
1605
|
+
async function getHeadSha(cwd) {
|
|
1806
1606
|
try {
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1607
|
+
const { stdout } = await execFile3("git", ["rev-parse", "HEAD"], { cwd });
|
|
1608
|
+
return stdout.trim();
|
|
1609
|
+
} catch {
|
|
1610
|
+
return "HEAD";
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
async function runRalphLoop(opts) {
|
|
1614
|
+
const maxIterations = opts.maxIterations ?? MAX_ITERATIONS;
|
|
1615
|
+
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
1616
|
+
const stallMs = opts.stallMs ?? STALL_MS;
|
|
1617
|
+
const struggleThreshold = opts.struggleThreshold ?? STRUGGLE_THRESHOLD;
|
|
1618
|
+
const _startServer = opts._deps?.startServer ?? startServer;
|
|
1619
|
+
const _createSession = opts._deps?.createSession ?? createSession;
|
|
1620
|
+
const _sendAndWait = opts._deps?.sendAndWait ?? sendAndWait;
|
|
1621
|
+
const _getLastAssistantMessage = opts._deps?.getLastAssistantMessage ?? getLastAssistantMessage;
|
|
1622
|
+
const fullPrompt = buildFullPrompt(opts.prompt);
|
|
1623
|
+
const struggle = new StruggleDetector(struggleThreshold);
|
|
1624
|
+
const startTime = Date.now();
|
|
1625
|
+
const server = await _startServer({ cwd: opts.cwd });
|
|
1626
|
+
const abort = new AbortController();
|
|
1627
|
+
const timeoutHandle = setTimeout(() => {
|
|
1628
|
+
abort.abort();
|
|
1629
|
+
}, timeoutMs);
|
|
1630
|
+
try {
|
|
1631
|
+
const sessionId = await _createSession(server.client, {
|
|
1632
|
+
cwd: opts.cwd,
|
|
1633
|
+
agentName: "prime"
|
|
1634
|
+
});
|
|
1635
|
+
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
1636
|
+
if (checkKillSwitch(opts.cwd)) {
|
|
1637
|
+
return {
|
|
1638
|
+
exitReason: "kill-switch",
|
|
1639
|
+
iterations: iteration - 1,
|
|
1640
|
+
message: `Kill switch active (.agent/autopilot-disable exists). Stopping after ${iteration - 1} iteration(s).`
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
1644
|
+
return {
|
|
1645
|
+
exitReason: "timeout",
|
|
1646
|
+
iterations: iteration - 1,
|
|
1647
|
+
message: `Total timeout (${timeoutMs}ms) exceeded after ${iteration - 1} iteration(s).`
|
|
1648
|
+
};
|
|
1823
1649
|
}
|
|
1824
|
-
const
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
server
|
|
1650
|
+
const headBefore = await getHeadSha(opts.cwd);
|
|
1651
|
+
const result = await _sendAndWait(server.client, {
|
|
1652
|
+
sessionId,
|
|
1653
|
+
message: fullPrompt,
|
|
1654
|
+
stallMs,
|
|
1655
|
+
abortSignal: abort.signal
|
|
1831
1656
|
});
|
|
1832
|
-
if (
|
|
1833
|
-
return {
|
|
1657
|
+
if (result.kind === "abort") {
|
|
1658
|
+
return {
|
|
1659
|
+
exitReason: "timeout",
|
|
1660
|
+
iterations: iteration,
|
|
1661
|
+
message: `Aborted after ${iteration} iteration(s) (total timeout exceeded).`
|
|
1662
|
+
};
|
|
1834
1663
|
}
|
|
1835
|
-
if (
|
|
1836
|
-
const resolveResult = await runResolvePhase({
|
|
1837
|
-
workflowId,
|
|
1838
|
-
scope,
|
|
1839
|
-
assessment: assessResult.artifact,
|
|
1840
|
-
cwd,
|
|
1841
|
-
startedAt
|
|
1842
|
-
});
|
|
1664
|
+
if (result.kind === "stall") {
|
|
1843
1665
|
return {
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
durationMs: resolveResult.durationMs,
|
|
1848
|
-
acknowledgedRisks: resolveResult.acknowledgedRisks
|
|
1666
|
+
exitReason: "stall",
|
|
1667
|
+
iterations: iteration,
|
|
1668
|
+
message: `Iteration ${iteration} stalled for ${result.stallMs}ms with no idle signal.`
|
|
1849
1669
|
};
|
|
1850
1670
|
}
|
|
1851
|
-
if (
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
Previous attempt failed. Gap to address:
|
|
1876
|
-
${assessResult.replanGuidance}`
|
|
1877
|
-
},
|
|
1878
|
-
cwd,
|
|
1879
|
-
server
|
|
1880
|
-
});
|
|
1881
|
-
if (!replanResult.ok) {
|
|
1882
|
-
return { ok: false, reason: `Re-plan failed: ${replanResult.reason}`, workflowId };
|
|
1883
|
-
}
|
|
1884
|
-
currentPlan = replanResult.artifact;
|
|
1671
|
+
if (result.kind === "error") {
|
|
1672
|
+
return {
|
|
1673
|
+
exitReason: "error",
|
|
1674
|
+
iterations: iteration,
|
|
1675
|
+
message: `Error in iteration ${iteration}: ${result.message}`
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
const lastMessage = await _getLastAssistantMessage(server.client, sessionId);
|
|
1679
|
+
if (detectSentinel(lastMessage)) {
|
|
1680
|
+
return {
|
|
1681
|
+
exitReason: "sentinel",
|
|
1682
|
+
iterations: iteration,
|
|
1683
|
+
message: `Agent emitted <autopilot-done> at iteration ${iteration}.`
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
const madeProgress = await checkProgress(opts.cwd, headBefore);
|
|
1687
|
+
struggle.record(madeProgress);
|
|
1688
|
+
if (struggle.isStruggling()) {
|
|
1689
|
+
return {
|
|
1690
|
+
exitReason: "struggle",
|
|
1691
|
+
iterations: iteration,
|
|
1692
|
+
message: `Agent made no filesystem progress for ${struggleThreshold} consecutive iteration(s). Stopping at iteration ${iteration}.`
|
|
1693
|
+
};
|
|
1885
1694
|
}
|
|
1886
1695
|
}
|
|
1887
|
-
const { db: failDb, close: closeFailDb } = openStateDb(dbPath);
|
|
1888
|
-
updateWorkflowStatus(failDb, workflowId, "failed");
|
|
1889
|
-
logEvent(failDb, {
|
|
1890
|
-
workflowId,
|
|
1891
|
-
phase: "assess",
|
|
1892
|
-
kind: "task.assess.cycles.exhausted",
|
|
1893
|
-
payload: { max_cycles: maxCycles }
|
|
1894
|
-
});
|
|
1895
|
-
closeFailDb();
|
|
1896
1696
|
return {
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1697
|
+
exitReason: "max-iterations",
|
|
1698
|
+
iterations: maxIterations,
|
|
1699
|
+
message: `Reached maximum iterations (${maxIterations}). Stopping.`
|
|
1900
1700
|
};
|
|
1901
1701
|
} finally {
|
|
1702
|
+
clearTimeout(timeoutHandle);
|
|
1902
1703
|
await server.shutdown();
|
|
1903
1704
|
}
|
|
1904
1705
|
}
|
|
1905
|
-
async function findCurrentScope(cwd) {
|
|
1906
|
-
try {
|
|
1907
|
-
const pointerPath = await getCurrentScopePath(cwd);
|
|
1908
|
-
if (!fs8.existsSync(pointerPath)) return null;
|
|
1909
|
-
const pointer = JSON.parse(fs8.readFileSync(pointerPath, "utf8"));
|
|
1910
|
-
return typeof pointer.scopePath === "string" ? pointer.scopePath : null;
|
|
1911
|
-
} catch {
|
|
1912
|
-
return null;
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
1706
|
|
|
1916
|
-
// src/
|
|
1917
|
-
var
|
|
1918
|
-
name: "
|
|
1919
|
-
description: "Run the
|
|
1707
|
+
// src/autopilot/cli.ts
|
|
1708
|
+
var autopilotCmd = command({
|
|
1709
|
+
name: "autopilot",
|
|
1710
|
+
description: "Run the Ralph loop: send a prompt to PRIME repeatedly until it emits <autopilot-done> or a budget is exhausted.",
|
|
1920
1711
|
args: {
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
description: "
|
|
1712
|
+
prompt: positional({
|
|
1713
|
+
type: stringType,
|
|
1714
|
+
displayName: "prompt",
|
|
1715
|
+
description: "The prompt to send to PRIME each iteration (e.g. a Linear issue ref or free-form task)."
|
|
1716
|
+
}),
|
|
1717
|
+
maxIterations: option({
|
|
1718
|
+
long: "max-iterations",
|
|
1719
|
+
type: optional(numberType),
|
|
1720
|
+
description: `Maximum number of loop iterations (default: ${MAX_ITERATIONS}).`
|
|
1721
|
+
}),
|
|
1722
|
+
timeout: option({
|
|
1723
|
+
long: "timeout",
|
|
1724
|
+
type: optional(numberType),
|
|
1725
|
+
description: `Total wall-clock timeout in milliseconds (default: ${TIMEOUT_MS} = 4 hours).`
|
|
1925
1726
|
})
|
|
1926
1727
|
},
|
|
1927
|
-
handler: async ({
|
|
1728
|
+
handler: async ({ prompt, maxIterations, timeout }) => {
|
|
1928
1729
|
const cwd = process.cwd();
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
const result = await runOrchestrator({ cwd, scopePath: scope });
|
|
1932
|
-
if (!result.ok) {
|
|
1933
|
-
process.stderr.write(`
|
|
1934
|
-
\x1B[31m\u2717\x1B[0m Pilot failed: ${result.reason}
|
|
1730
|
+
process.stdout.write("\n\x1B[1mAutopilot \u2014 Ralph loop\x1B[0m\n");
|
|
1731
|
+
process.stdout.write(`Prompt: ${prompt.slice(0, 80)}${prompt.length > 80 ? "..." : ""}
|
|
1935
1732
|
`);
|
|
1936
|
-
|
|
1937
|
-
process.stderr.write(` Workflow: ${result.workflowId}
|
|
1733
|
+
process.stdout.write(`Max iterations: ${maxIterations ?? MAX_ITERATIONS}
|
|
1938
1734
|
`);
|
|
1939
|
-
|
|
1940
|
-
process.exit(1);
|
|
1941
|
-
}
|
|
1942
|
-
const durationSec = Math.round(result.durationMs / 1e3);
|
|
1943
|
-
const durationStr = durationSec >= 60 ? `${Math.floor(durationSec / 60)}m ${durationSec % 60}s` : `${durationSec}s`;
|
|
1944
|
-
console.log(`
|
|
1945
|
-
\x1B[32m\u2713\x1B[0m Workflow complete`);
|
|
1946
|
-
console.log(` Goal: ${result.goal}`);
|
|
1947
|
-
console.log(` Duration: ${durationStr}`);
|
|
1948
|
-
if (result.acknowledgedRisks.length > 0) {
|
|
1949
|
-
console.log(`
|
|
1950
|
-
Acknowledged risks (non-blocking):`);
|
|
1951
|
-
for (const risk of result.acknowledgedRisks) {
|
|
1952
|
-
console.log(` \u2022 ${risk}`);
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
console.log();
|
|
1956
|
-
process.exit(0);
|
|
1957
|
-
}
|
|
1958
|
-
});
|
|
1959
|
-
|
|
1960
|
-
// src/pilot/cli/status.ts
|
|
1961
|
-
import { command as command4, option as option2, string as stringType2, optional as optional2, flag } from "cmd-ts";
|
|
1962
|
-
var statusCmd = command4({
|
|
1963
|
-
name: "status",
|
|
1964
|
-
description: "Show pilot workflow status.",
|
|
1965
|
-
args: {
|
|
1966
|
-
workflow: option2({
|
|
1967
|
-
long: "workflow",
|
|
1968
|
-
type: optional2(stringType2),
|
|
1969
|
-
description: "Workflow ID (defaults to the latest)"
|
|
1970
|
-
}),
|
|
1971
|
-
json: flag({
|
|
1972
|
-
long: "json",
|
|
1973
|
-
description: "Output JSON"
|
|
1974
|
-
})
|
|
1975
|
-
},
|
|
1976
|
-
handler: async ({ workflow, json }) => {
|
|
1977
|
-
const cwd = process.cwd();
|
|
1978
|
-
const dbPath = await getStateDbPath(cwd);
|
|
1979
|
-
const { db, close } = openStateDb(dbPath);
|
|
1980
|
-
try {
|
|
1981
|
-
const wf = workflow ? getWorkflow(db, workflow) : latestWorkflow(db);
|
|
1982
|
-
if (!wf) {
|
|
1983
|
-
process.stderr.write('No workflows found. Run `pilot scope "<goal>"` to start one.\n');
|
|
1984
|
-
process.exit(1);
|
|
1985
|
-
}
|
|
1986
|
-
const events = readEvents(db, { workflowId: wf.id, limit: 100 });
|
|
1987
|
-
if (json) {
|
|
1988
|
-
process.stdout.write(JSON.stringify({ workflow: wf, events }, null, 2) + "\n");
|
|
1989
|
-
process.exit(0);
|
|
1990
|
-
}
|
|
1991
|
-
const started = new Date(wf.started_at).toLocaleString();
|
|
1992
|
-
const finished = wf.finished_at ? new Date(wf.finished_at).toLocaleString() : "--";
|
|
1993
|
-
const statusColor = wf.status === "completed" ? "\x1B[32m" : wf.status === "failed" ? "\x1B[31m" : "\x1B[33m";
|
|
1994
|
-
console.log(`
|
|
1995
|
-
Workflow ${wf.id}`);
|
|
1996
|
-
console.log(` Goal: ${wf.goal}`);
|
|
1997
|
-
console.log(` Status: ${statusColor}${wf.status}\x1B[0m`);
|
|
1998
|
-
console.log(` Started: ${started}`);
|
|
1999
|
-
console.log(` Ended: ${finished}`);
|
|
2000
|
-
console.log(`
|
|
2001
|
-
Recent events (${events.length}):`);
|
|
2002
|
-
for (const event of events.slice(-20)) {
|
|
2003
|
-
const ts = new Date(event.ts).toLocaleTimeString();
|
|
2004
|
-
const payload = (() => {
|
|
2005
|
-
try {
|
|
2006
|
-
const p = JSON.parse(event.payload);
|
|
2007
|
-
return Object.entries(p).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ");
|
|
2008
|
-
} catch {
|
|
2009
|
-
return event.payload;
|
|
2010
|
-
}
|
|
2011
|
-
})();
|
|
2012
|
-
console.log(` ${ts} [${event.phase}] ${event.kind} ${payload}`);
|
|
2013
|
-
}
|
|
2014
|
-
console.log();
|
|
2015
|
-
process.exit(0);
|
|
2016
|
-
} finally {
|
|
2017
|
-
close();
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
});
|
|
2021
|
-
|
|
2022
|
-
// src/pilot/cli/shims.ts
|
|
2023
|
-
import { command as command5 } from "cmd-ts";
|
|
2024
|
-
function removedCommand(name, replacement) {
|
|
2025
|
-
return command5({
|
|
2026
|
-
name,
|
|
2027
|
-
description: `[removed] Use \`${replacement}\` instead.`,
|
|
2028
|
-
args: {},
|
|
2029
|
-
handler: async () => {
|
|
2030
|
-
process.stderr.write(
|
|
2031
|
-
`
|
|
2032
|
-
\x1B[33m!\x1B[0m \`pilot ${name}\` was removed in pilot v2.
|
|
2033
|
-
Use \x1B[1m${replacement}\x1B[0m instead.
|
|
1735
|
+
process.stdout.write(`Timeout: ${((timeout ?? TIMEOUT_MS) / 36e5).toFixed(1)}h
|
|
2034
1736
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
1737
|
+
`);
|
|
1738
|
+
const result = await runRalphLoop({
|
|
1739
|
+
prompt,
|
|
1740
|
+
cwd,
|
|
1741
|
+
maxIterations: maxIterations ?? void 0,
|
|
1742
|
+
timeoutMs: timeout ?? void 0
|
|
1743
|
+
});
|
|
1744
|
+
const icon = result.exitReason === "sentinel" ? "\x1B[32m\u2713\x1B[0m" : result.exitReason === "kill-switch" ? "\x1B[33m\u2298\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
|
|
1745
|
+
process.stdout.write(`
|
|
1746
|
+
${icon} ${result.message}
|
|
1747
|
+
`);
|
|
1748
|
+
process.stdout.write(` Iterations: ${result.iterations}
|
|
2040
1749
|
|
|
2041
|
-
`
|
|
2042
|
-
|
|
1750
|
+
`);
|
|
1751
|
+
if (result.exitReason !== "sentinel" && result.exitReason !== "kill-switch") {
|
|
2043
1752
|
process.exit(1);
|
|
2044
1753
|
}
|
|
2045
|
-
|
|
2046
|
-
}
|
|
2047
|
-
var buildShim = removedCommand("build", "pilot go");
|
|
2048
|
-
var validateShim = removedCommand("validate", "pilot configure");
|
|
2049
|
-
var logsShim = removedCommand("logs", "pilot status --json");
|
|
2050
|
-
var costShim = removedCommand("cost", "pilot status --json");
|
|
2051
|
-
var buildResumeShim = removedCommand("build-resume", "pilot go");
|
|
2052
|
-
|
|
2053
|
-
// src/pilot/cli/index.ts
|
|
2054
|
-
var pilotSubcommand = subcommands({
|
|
2055
|
-
name: "pilot",
|
|
2056
|
-
description: "Pilot v2 \u2014 SPEAR-based autonomous execution (scope \u2192 plan \u2192 execute \u2192 assess \u2192 resolve).",
|
|
2057
|
-
cmds: {
|
|
2058
|
-
scope: scopeCmd,
|
|
2059
|
-
go: goCmd,
|
|
2060
|
-
configure: configureCmd,
|
|
2061
|
-
status: statusCmd,
|
|
2062
|
-
// Shims for removed v1 commands (print migration message)
|
|
2063
|
-
build: buildShim,
|
|
2064
|
-
validate: validateShim,
|
|
2065
|
-
logs: logsShim,
|
|
2066
|
-
cost: costShim,
|
|
2067
|
-
"build-resume": buildResumeShim
|
|
1754
|
+
process.exit(0);
|
|
2068
1755
|
}
|
|
2069
1756
|
});
|
|
2070
1757
|
|
|
2071
1758
|
// src/cli/cli-update.ts
|
|
2072
|
-
import * as
|
|
2073
|
-
import * as
|
|
2074
|
-
import * as
|
|
1759
|
+
import * as fs8 from "fs";
|
|
1760
|
+
import * as path8 from "path";
|
|
1761
|
+
import * as os6 from "os";
|
|
2075
1762
|
import { spawn } from "child_process";
|
|
2076
|
-
import { fileURLToPath as
|
|
1763
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2077
1764
|
var PACKAGE_NAME = "@glrs-dev/harness-plugin-opencode";
|
|
2078
1765
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2079
|
-
var
|
|
1766
|
+
var c2 = {
|
|
2080
1767
|
reset: "\x1B[0m",
|
|
2081
1768
|
green: "\x1B[32m",
|
|
2082
1769
|
yellow: "\x1B[33m",
|
|
@@ -2098,12 +1785,12 @@ function isMajorBump(current, latest) {
|
|
|
2098
1785
|
return latest.major > current.major;
|
|
2099
1786
|
}
|
|
2100
1787
|
function getStateFilePath() {
|
|
2101
|
-
const cacheHome = process.env["XDG_CACHE_HOME"] ??
|
|
2102
|
-
return
|
|
1788
|
+
const cacheHome = process.env["XDG_CACHE_HOME"] ?? path8.join(os6.homedir(), ".cache");
|
|
1789
|
+
return path8.join(cacheHome, "harness-opencode", "cli-update.json");
|
|
2103
1790
|
}
|
|
2104
1791
|
function readState() {
|
|
2105
1792
|
try {
|
|
2106
|
-
const raw =
|
|
1793
|
+
const raw = fs8.readFileSync(getStateFilePath(), "utf8");
|
|
2107
1794
|
return JSON.parse(raw);
|
|
2108
1795
|
} catch {
|
|
2109
1796
|
return null;
|
|
@@ -2112,21 +1799,21 @@ function readState() {
|
|
|
2112
1799
|
function writeState(state) {
|
|
2113
1800
|
try {
|
|
2114
1801
|
const statePath = getStateFilePath();
|
|
2115
|
-
|
|
2116
|
-
|
|
1802
|
+
fs8.mkdirSync(path8.dirname(statePath), { recursive: true });
|
|
1803
|
+
fs8.writeFileSync(statePath, JSON.stringify(state));
|
|
2117
1804
|
} catch {
|
|
2118
1805
|
}
|
|
2119
1806
|
}
|
|
2120
1807
|
function readInstalledVersion() {
|
|
2121
|
-
const here =
|
|
1808
|
+
const here = path8.dirname(fileURLToPath3(import.meta.url));
|
|
2122
1809
|
const candidates = [
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
1810
|
+
path8.join(here, "..", "package.json"),
|
|
1811
|
+
path8.join(here, "..", "..", "package.json"),
|
|
1812
|
+
path8.join(here, "package.json")
|
|
2126
1813
|
];
|
|
2127
1814
|
for (const candidate of candidates) {
|
|
2128
1815
|
try {
|
|
2129
|
-
const raw =
|
|
1816
|
+
const raw = fs8.readFileSync(candidate, "utf8");
|
|
2130
1817
|
const parsed = JSON.parse(raw);
|
|
2131
1818
|
if (parsed.name === PACKAGE_NAME && typeof parsed.version === "string") {
|
|
2132
1819
|
return parsed.version;
|
|
@@ -2197,7 +1884,7 @@ function startUpdateCheck() {
|
|
|
2197
1884
|
action = () => {
|
|
2198
1885
|
process.stderr.write(
|
|
2199
1886
|
`
|
|
2200
|
-
${
|
|
1887
|
+
${c2.blue}\u2022${c2.reset} Updating ${PACKAGE_NAME} ${c2.dim}${currentVersionStr}${c2.reset} \u2192 ${c2.green}${latestStr}${c2.reset} in the background...
|
|
2201
1888
|
`
|
|
2202
1889
|
);
|
|
2203
1890
|
spawnBackgroundUpdate();
|
|
@@ -2212,8 +1899,8 @@ ${c.blue}\u2022${c.reset} Updating ${PACKAGE_NAME} ${c.dim}${currentVersionStr}$
|
|
|
2212
1899
|
function printMajorNotice(current, latest) {
|
|
2213
1900
|
process.stderr.write(
|
|
2214
1901
|
`
|
|
2215
|
-
${
|
|
2216
|
-
${
|
|
1902
|
+
${c2.yellow}${c2.bold}Major update available:${c2.reset} ${current} \u2192 ${c2.green}${latest}${c2.reset}
|
|
1903
|
+
${c2.dim}Review the changelog before upgrading:${c2.reset}
|
|
2217
1904
|
bun update -g ${PACKAGE_NAME}
|
|
2218
1905
|
`
|
|
2219
1906
|
);
|
|
@@ -2252,15 +1939,15 @@ Upgrade Node or run via a compatible Bun runtime. See the "engines" field in pac
|
|
|
2252
1939
|
}
|
|
2253
1940
|
}
|
|
2254
1941
|
var VERSION = "0.1.0";
|
|
2255
|
-
var installCmd =
|
|
1942
|
+
var installCmd = command2({
|
|
2256
1943
|
name: "install",
|
|
2257
1944
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
2258
1945
|
args: {
|
|
2259
|
-
dryRun:
|
|
1946
|
+
dryRun: flag({
|
|
2260
1947
|
long: "dry-run",
|
|
2261
1948
|
description: "Preview changes without writing."
|
|
2262
1949
|
}),
|
|
2263
|
-
pin:
|
|
1950
|
+
pin: flag({
|
|
2264
1951
|
long: "pin",
|
|
2265
1952
|
description: "Pin to the current exact version (e.g. @0.1.0)."
|
|
2266
1953
|
})
|
|
@@ -2269,11 +1956,11 @@ var installCmd = command6({
|
|
|
2269
1956
|
await install({ dryRun, pin });
|
|
2270
1957
|
}
|
|
2271
1958
|
});
|
|
2272
|
-
var uninstallCmd =
|
|
1959
|
+
var uninstallCmd = command2({
|
|
2273
1960
|
name: "uninstall",
|
|
2274
1961
|
description: 'Remove "@glrs-dev/harness-plugin-opencode" from your opencode.json plugin array.',
|
|
2275
1962
|
args: {
|
|
2276
|
-
dryRun:
|
|
1963
|
+
dryRun: flag({
|
|
2277
1964
|
long: "dry-run",
|
|
2278
1965
|
description: "Preview changes without writing."
|
|
2279
1966
|
})
|
|
@@ -2282,7 +1969,7 @@ var uninstallCmd = command6({
|
|
|
2282
1969
|
uninstall({ dryRun });
|
|
2283
1970
|
}
|
|
2284
1971
|
});
|
|
2285
|
-
var doctorCmd =
|
|
1972
|
+
var doctorCmd = command2({
|
|
2286
1973
|
name: "doctor",
|
|
2287
1974
|
description: "Check installation health (OpenCode CLI, plugin registration, MCP backends).",
|
|
2288
1975
|
args: {},
|
|
@@ -2290,22 +1977,22 @@ var doctorCmd = command6({
|
|
|
2290
1977
|
doctor();
|
|
2291
1978
|
}
|
|
2292
1979
|
});
|
|
2293
|
-
var planCheckCmd =
|
|
1980
|
+
var planCheckCmd = command2({
|
|
2294
1981
|
name: "plan-check",
|
|
2295
1982
|
description: "Parse a plan file's plan-state fence (legacy markdown plans).",
|
|
2296
1983
|
args: {
|
|
2297
|
-
run:
|
|
1984
|
+
run: option2({
|
|
2298
1985
|
long: "run",
|
|
2299
|
-
type:
|
|
1986
|
+
type: optional2(string),
|
|
2300
1987
|
description: "Print verify commands for pending items, one per line."
|
|
2301
1988
|
}),
|
|
2302
|
-
check:
|
|
1989
|
+
check: option2({
|
|
2303
1990
|
long: "check",
|
|
2304
|
-
type:
|
|
1991
|
+
type: optional2(string),
|
|
2305
1992
|
description: "Structural validation; exits 1 if any item is invalid."
|
|
2306
1993
|
}),
|
|
2307
|
-
rest:
|
|
2308
|
-
type:
|
|
1994
|
+
rest: restPositionals({
|
|
1995
|
+
type: string,
|
|
2309
1996
|
displayName: "plan-path",
|
|
2310
1997
|
description: "Path to a plan markdown file. Required unless --run / --check is given."
|
|
2311
1998
|
})
|
|
@@ -2322,7 +2009,7 @@ var planCheckCmd = command6({
|
|
|
2322
2009
|
planCheck(legacy);
|
|
2323
2010
|
}
|
|
2324
2011
|
});
|
|
2325
|
-
var planDirCmd =
|
|
2012
|
+
var planDirCmd = command2({
|
|
2326
2013
|
name: "plan-dir",
|
|
2327
2014
|
description: "Print the repo-shared plan directory for the current worktree (resolves + creates + migrates legacy).",
|
|
2328
2015
|
args: {},
|
|
@@ -2341,15 +2028,15 @@ var planDirCmd = command6({
|
|
|
2341
2028
|
}
|
|
2342
2029
|
}
|
|
2343
2030
|
});
|
|
2344
|
-
var installPluginCmd =
|
|
2031
|
+
var installPluginCmd = command2({
|
|
2345
2032
|
name: "install-plugin",
|
|
2346
2033
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
2347
2034
|
args: {
|
|
2348
|
-
dryRun:
|
|
2035
|
+
dryRun: flag({
|
|
2349
2036
|
long: "dry-run",
|
|
2350
2037
|
description: "Preview changes without writing."
|
|
2351
2038
|
}),
|
|
2352
|
-
pin:
|
|
2039
|
+
pin: flag({
|
|
2353
2040
|
long: "pin",
|
|
2354
2041
|
description: "Pin to the current exact version (e.g. @0.1.0)."
|
|
2355
2042
|
})
|
|
@@ -2358,7 +2045,7 @@ var installPluginCmd = command6({
|
|
|
2358
2045
|
await install({ dryRun, pin });
|
|
2359
2046
|
}
|
|
2360
2047
|
});
|
|
2361
|
-
var cli =
|
|
2048
|
+
var cli = subcommands({
|
|
2362
2049
|
name: "glrs-oc",
|
|
2363
2050
|
description: "OpenCode agent harness CLI.",
|
|
2364
2051
|
version: VERSION,
|
|
@@ -2369,7 +2056,7 @@ var cli = subcommands2({
|
|
|
2369
2056
|
doctor: doctorCmd,
|
|
2370
2057
|
"plan-check": planCheckCmd,
|
|
2371
2058
|
"plan-dir": planDirCmd,
|
|
2372
|
-
|
|
2059
|
+
autopilot: autopilotCmd
|
|
2373
2060
|
}
|
|
2374
2061
|
});
|
|
2375
2062
|
var printUpdate = startUpdateCheck();
|