@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +18 -4
  3. package/dist/vendor/harness-opencode/dist/agents/prompts/build.open.md +18 -4
  4. package/dist/vendor/harness-opencode/dist/agents/prompts/{qa-thorough.md → code-reviewer-thorough.md} +34 -19
  5. package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer.md +80 -0
  6. package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer.open.md +68 -0
  7. package/dist/vendor/harness-opencode/dist/agents/prompts/gap-analyzer.md +2 -0
  8. package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +3 -0
  9. package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +23 -4
  10. package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +146 -87
  11. package/dist/vendor/harness-opencode/dist/agents/prompts/research-auto.md +1 -1
  12. package/dist/vendor/harness-opencode/dist/agents/prompts/research-local.md +1 -1
  13. package/dist/vendor/harness-opencode/dist/agents/prompts/research-web.md +1 -1
  14. package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +2 -0
  15. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +54 -0
  16. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +57 -0
  17. package/dist/vendor/harness-opencode/dist/agents/shared/index.ts +1 -0
  18. package/dist/vendor/harness-opencode/dist/agents/shared/ui-evaluation-ladder.md +50 -0
  19. package/dist/vendor/harness-opencode/dist/agents/shared/workflow-mechanics.md +5 -5
  20. package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +80 -0
  21. package/dist/vendor/harness-opencode/dist/{chunk-VJUETC6A.js → chunk-PDMXYZM4.js} +53 -1
  22. package/dist/vendor/harness-opencode/dist/cli.js +1333 -1646
  23. package/dist/vendor/harness-opencode/dist/commands/prompts/fresh.md +27 -24
  24. package/dist/vendor/harness-opencode/dist/commands/prompts/review.md +3 -3
  25. package/dist/vendor/harness-opencode/dist/commands/prompts/ship.md +2 -0
  26. package/dist/vendor/harness-opencode/dist/index.js +106 -627
  27. package/dist/vendor/harness-opencode/dist/skills/adversarial-review-rubric/SKILL.md +47 -0
  28. package/dist/vendor/harness-opencode/dist/skills/code-quality/SKILL.md +1 -1
  29. package/dist/vendor/harness-opencode/dist/skills/root-cause-diagnosis/SKILL.md +24 -0
  30. package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +166 -0
  31. package/dist/vendor/harness-opencode/package.json +1 -1
  32. package/package.json +1 -1
  33. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-assessor.md +0 -77
  34. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +0 -40
  35. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +0 -56
  36. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-scoper.md +0 -58
  37. package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.md +0 -68
  38. package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.open.md +0 -58
  39. package/dist/vendor/harness-opencode/dist/chunk-6CZPRUMJ.js +0 -869
  40. package/dist/vendor/harness-opencode/dist/chunk-DZG4D3OH.js +0 -54
  41. package/dist/vendor/harness-opencode/dist/chunk-OYRKOEXK.js +0 -88
  42. package/dist/vendor/harness-opencode/dist/commands/prompts/autopilot.md +0 -96
  43. package/dist/vendor/harness-opencode/dist/install-6775ZBDG.js +0 -13
  44. 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-DZG4D3OH.js";
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 command6,
22
- flag as flag2,
23
- option as option3,
24
- optional as optional3,
25
- positional,
26
- restPositionals as restPositionals2,
27
- string as string2,
28
- subcommands as subcommands2,
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/uninstall.ts
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"] ?? path.join(os.homedir(), ".config");
39
- return path.join(configHome, "opencode", "opencode.json");
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 = getOpencodeConfigPath();
44
- const c2 = {
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 ok = (msg) => console.log(`${c2.green}\u2713${c2.reset} ${msg}`);
51
- const info = (msg) => console.log(`${c2.blue}\u2022${c2.reset} ${msg}`);
52
- const warn = (msg) => console.log(`${c2.yellow}!${c2.reset} ${msg}`);
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
- ${c2.blue}Uninstalling ${PLUGIN_NAME}${c2.reset}
1026
+ ${c3.blue}Uninstalling ${PLUGIN_NAME2}${c3.reset}
55
1027
  `);
56
- if (!fs.existsSync(configPath)) {
57
- warn(`No opencode.json found at ${configPath} \u2014 nothing to do`);
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 = fs.readFileSync(configPath, "utf8");
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 !== PLUGIN_NAME && !String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
1049
+ return name !== PLUGIN_NAME2 && !String(name ?? "").startsWith(`${PLUGIN_NAME2}@`);
78
1050
  });
79
1051
  if (filtered.length === plugins.length) {
80
- warn(`"${PLUGIN_NAME}" not found in plugin array \u2014 nothing to remove`);
1052
+ warn2(`"${PLUGIN_NAME2}" not found in plugin array \u2014 nothing to remove`);
81
1053
  return;
82
1054
  }
83
1055
  if (dryRun) {
84
- info(`[dry-run] Would remove "${PLUGIN_NAME}" from plugin array in ${configPath}`);
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
- fs.copyFileSync(configPath, bakPath);
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
- fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
98
- fs.renameSync(tmpPath, configPath);
1069
+ fs4.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
1070
+ fs4.renameSync(tmpPath, configPath);
99
1071
  } catch (e) {
100
1072
  try {
101
- fs.unlinkSync(tmpPath);
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
- ok(`Removed "${PLUGIN_NAME}" from ${configPath}`);
108
- info(`Backup: ${bakPath}`);
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 fs2 from "fs";
116
- import * as path2 from "path";
117
- import * as os2 from "os";
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 PLUGIN_NAME2 = "@glrs-dev/harness-plugin-opencode";
120
- function getOpencodeConfigPath2() {
121
- const configHome = process.env["XDG_CONFIG_HOME"] ?? path2.join(os2.homedir(), ".config");
122
- return path2.join(configHome, "opencode", "opencode.json");
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(command7) {
1096
+ function cmd(command3) {
125
1097
  try {
126
- return execSync(command7, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
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 c2 = {
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 ok = (msg) => console.log(`${c2.green}\u2713${c2.reset} ${msg}`);
143
- const warn = (msg) => console.log(`${c2.yellow}!${c2.reset} ${msg}`);
144
- const fail = (msg) => console.log(`${c2.red}\u2717${c2.reset} ${msg}`);
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
- ${c2.bold}Doctor \u2014 ${PLUGIN_NAME2}${c2.reset}
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
- ok(`opencode ${ocVersion}`);
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 = getOpencodeConfigPath2();
155
- if (fs2.existsSync(configPath)) {
1126
+ const configPath = getOpencodeConfigPath3();
1127
+ if (fs5.existsSync(configPath)) {
156
1128
  try {
157
- const config = JSON.parse(fs2.readFileSync(configPath, "utf8"));
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 === PLUGIN_NAME2 || p.startsWith(`${PLUGIN_NAME2}@`);
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 === PLUGIN_NAME2 || String(name ?? "").startsWith(`${PLUGIN_NAME2}@`);
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
- ok(`"${PLUGIN_NAME2}" present in opencode.json plugin array`);
1147
+ ok2(`"${PLUGIN_NAME3}" present in opencode.json plugin array`);
176
1148
  } else {
177
- warn(`"${PLUGIN_NAME2}" NOT in opencode.json plugin array \u2014 run: bunx ${PLUGIN_NAME2} install`);
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
- ok("model overrides look valid");
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(` ${c2.yellow}reason:${c2.reset} ${entry.reason}`);
1189
+ console.log(` ${c3.yellow}reason:${c3.reset} ${entry.reason}`);
218
1190
  }
219
1191
  if (entry.suggestion) {
220
1192
  console.log(
221
- ` ${c2.yellow}fix:${c2.reset} remove this key, or replace with \`${entry.suggestion}\``
1193
+ ` ${c3.yellow}fix:${c3.reset} remove this key, or replace with \`${entry.suggestion}\``
222
1194
  );
223
1195
  } else {
224
1196
  console.log(
225
- ` ${c2.yellow}fix:${c2.reset} remove this key, or run \`bunx ${PLUGIN_NAME2} install\` to pick a current preset`
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
- warn(`No opencode.json at ${configPath} \u2014 run: bunx ${PLUGIN_NAME2} install`);
1207
+ warn2(`No opencode.json at ${configPath} \u2014 run: bunx ${PLUGIN_NAME3} install`);
236
1208
  }
237
1209
  if (which("uvx")) {
238
- ok("uvx (serena + git MCPs)");
1210
+ ok2("uvx (serena + git MCPs)");
239
1211
  } else {
240
- warn("uvx not found \u2014 serena and git MCPs won't work. Install: brew install uv");
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
- ok(`node ${cmd("node --version") ?? ""} + npx (memory MCP)`);
1215
+ ok2(`node ${cmd("node --version") ?? ""} + npx (memory MCP)`);
244
1216
  } else {
245
- warn("node/npx not found \u2014 memory MCP won't work");
1217
+ warn2("node/npx not found \u2014 memory MCP won't work");
246
1218
  }
247
- const planCheckResult = cmd(`bunx ${PLUGIN_NAME2} plan-check --help 2>/dev/null`);
1219
+ const planCheckResult = cmd(`bunx ${PLUGIN_NAME3} plan-check --help 2>/dev/null`);
248
1220
  if (planCheckResult !== null) {
249
- ok("plan-check CLI invokable");
1221
+ ok2("plan-check CLI invokable");
250
1222
  } else {
251
- warn("plan-check CLI not invokable \u2014 try: bun install");
1223
+ warn2("plan-check CLI not invokable \u2014 try: bun install");
252
1224
  }
253
1225
  if (which("bun")) {
254
- ok(`bun ${cmd("bun --version") ?? ""}`);
1226
+ ok2(`bun ${cmd("bun --version") ?? ""}`);
255
1227
  } else if (which("npm")) {
256
- ok(`npm ${cmd("npm --version") ?? ""} (install bun for faster installs)`);
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 join3 } from "path";
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 = dirname(fileURLToPath(import.meta.url));
1240
+ const here = dirname3(fileURLToPath2(import.meta.url));
298
1241
  const candidates = [
299
- join3(here, "plan-check.sh"),
1242
+ join5(here, "plan-check.sh"),
300
1243
  // dev: src/bin/plan-check.sh
301
- join3(here, "bin", "plan-check.sh")
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 fs3 from "fs/promises";
330
- import * as os3 from "os";
331
- import * as path3 from "path";
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 os3.homedir();
354
- if (p.startsWith("~/")) return path3.join(os3.homedir(), p.slice(2));
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 = path3.isAbsolute(gitCommonDir) ? gitCommonDir : path3.resolve(worktreeDir, gitCommonDir);
378
- const repoRoot = path3.dirname(absCommonDir);
379
- return path3.basename(repoRoot);
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) : path3.join(os3.homedir(), ".glorious", "opencode");
1326
+ const base = override ? expandTilde(override) : path6.join(os5.homedir(), ".glorious", "opencode");
384
1327
  const repoFolder = await getRepoFolder(worktreeDir);
385
- const planDir = path3.join(base, repoFolder, "plans");
386
- await fs3.mkdir(planDir, { recursive: true });
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 = path3.join(worktreeDir, ".agent", "plans");
391
- const marker = path3.join(oldDir, ".migrated");
1333
+ const oldDir = path6.join(worktreeDir, ".agent", "plans");
1334
+ const marker = path6.join(oldDir, ".migrated");
392
1335
  try {
393
- await fs3.stat(oldDir);
1336
+ await fs6.stat(oldDir);
394
1337
  } catch {
395
1338
  return;
396
1339
  }
397
1340
  try {
398
- await fs3.stat(marker);
1341
+ await fs6.stat(marker);
399
1342
  return;
400
1343
  } catch {
401
1344
  }
402
1345
  let entries;
403
1346
  try {
404
- entries = await fs3.readdir(oldDir);
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 fs3.mkdir(planDir, { recursive: true });
1354
+ await fs6.mkdir(planDir, { recursive: true });
412
1355
  for (const name of planFiles) {
413
- const src = path3.join(oldDir, name);
414
- const dst = path3.join(planDir, name);
1356
+ const src = path6.join(oldDir, name);
1357
+ const dst = path6.join(planDir, name);
415
1358
  let dstExists = false;
416
1359
  try {
417
- await fs3.stat(dst);
1360
+ await fs6.stat(dst);
418
1361
  dstExists = true;
419
1362
  } catch {
420
1363
  dstExists = false;
421
1364
  }
422
1365
  if (!dstExists) {
423
- await fs3.rename(src, dst);
1366
+ await fs6.rename(src, dst);
424
1367
  continue;
425
1368
  }
426
1369
  const [srcBuf, dstBuf] = await Promise.all([
427
- fs3.readFile(src),
428
- fs3.readFile(dst)
429
- ]);
430
- if (srcBuf.equals(dstBuf)) {
431
- await fs3.unlink(src);
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
- console.log(`
1036
- \x1B[32m\u2713\x1B[0m Scope complete`);
1037
- console.log(` Workflow: ${result.workflowId}`);
1038
- console.log(` Goal: ${result.artifact.goal}`);
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/pilot/orchestrator.ts
1052
- import * as fs8 from "fs";
1385
+ // src/autopilot/cli.ts
1386
+ import { command, option, positional, string as stringType, optional, number as numberType } from "cmd-ts";
1053
1387
 
1054
- // src/pilot/server.ts
1055
- import { execFile as execFile3 } from "child_process";
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 execFileP3 = promisify2(execFile3);
1401
+ var execFileP2 = promisify(execFile2);
1062
1402
  var DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
1063
1403
  async function ensureOpencodeOnPath() {
1064
1404
  try {
1065
- await execFileP3("opencode", ["--version"]);
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 execFileP4("git", ["reset", "--hard", "HEAD"], { cwd });
1472
- await execFileP4("git", ["clean", "-fd"], { cwd });
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
- ${task.prompt}
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
- Verify commands (run these after making changes):
1489
- ${verifyText}
1490
-
1491
- Rules:
1492
- - DO NOT commit. The orchestrator commits after verify passes.
1493
- - DO NOT push.
1494
- - DO NOT ask questions.
1495
- - If verify fails, fix the issue and re-run.
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/pilot/assess.ts
1541
+ // src/autopilot/struggle.ts
1500
1542
  import * as fs7 from "fs";
1501
- import * as path4 from "path";
1502
- function parseAssessmentArtifact(raw) {
1503
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
1504
- const obj = raw;
1505
- if (typeof obj["workflow_id"] !== "string") return null;
1506
- if (obj["verdict"] !== "pass" && obj["verdict"] !== "fail") return null;
1507
- if (!Array.isArray(obj["ac_results"])) return null;
1508
- const acResults = [];
1509
- for (const r of obj["ac_results"]) {
1510
- if (!r || typeof r !== "object") return null;
1511
- const result = r;
1512
- if (typeof result["id"] !== "string") return null;
1513
- if (!["met", "unmet", "partial"].includes(result["status"])) return null;
1514
- if (typeof result["evidence"] !== "string") return null;
1515
- acResults.push({
1516
- id: result["id"],
1517
- status: result["status"],
1518
- evidence: result["evidence"],
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
- const unmetAcs = artifact.ac_results.filter((r) => r.status !== "met").map((r) => r.id);
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
- async function getAssessArtifactPath(cwd, workflowId, cycle) {
1653
- const base = await getPilotDir(cwd);
1654
- const dir = path4.join(base, "scopes", workflowId);
1655
- fs7.mkdirSync(dir, { recursive: true });
1656
- return path4.join(dir, `assessment-cycle-${cycle}.json`);
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
- const durationMs = Date.now() - startedAt;
1721
- updateWorkflowStatus(db, workflowId, "completed");
1722
- logEvent(db, {
1723
- workflowId,
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/pilot/orchestrator.ts
1746
- async function runOrchestrator(opts) {
1747
- const { cwd } = opts;
1748
- const startedAt = Date.now();
1749
- const safety = await checkSafety(cwd);
1750
- if (!safety.ok) {
1751
- return { ok: false, reason: safety.reason };
1752
- }
1753
- const config = loadPilotConfig(cwd);
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 = JSON.parse(fs8.readFileSync(configPath, "utf8"));
1759
- if (raw && typeof raw === "object" && ("baseline" in raw || "after_each" in raw)) {
1760
- process.stderr.write(
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 scopePath = opts.scopePath ?? await findCurrentScope(cwd);
1768
- if (!scopePath) {
1769
- return {
1770
- ok: false,
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 raw = JSON.parse(fs8.readFileSync(scopePath, "utf8"));
1777
- const parsed = parseScopeArtifact(raw);
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 { ok: false, reason: `Could not read scope.json at ${scopePath}` };
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 planResult = await runPlanPhase({ workflowId, scope, cwd, server });
1808
- if (!planResult.ok) {
1809
- return { ok: false, reason: `Plan phase failed: ${planResult.reason}`, workflowId };
1810
- }
1811
- let currentPlan = planResult.artifact;
1812
- const maxCycles = config.max_assess_cycles;
1813
- for (let cycle = 1; cycle <= maxCycles; cycle++) {
1814
- const executeResult = await runExecutePhase({
1815
- workflowId,
1816
- scope,
1817
- plan: currentPlan,
1818
- cwd,
1819
- server
1820
- });
1821
- if (!executeResult.ok) {
1822
- return { ok: false, reason: `Execute phase failed: ${executeResult.reason}`, workflowId };
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 assessResult = await runAssessPhase({
1825
- workflowId,
1826
- scope,
1827
- plan: currentPlan,
1828
- cwd,
1829
- cycle,
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 (!assessResult.ok) {
1833
- return { ok: false, reason: `Assess phase failed: ${assessResult.reason}`, workflowId };
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 (assessResult.verdict === "pass") {
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
- ok: true,
1845
- workflowId,
1846
- goal: scope.goal,
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 (cycle < maxCycles) {
1852
- const elapsedSec = Math.round((Date.now() - startedAt) / 1e3);
1853
- process.stderr.write(
1854
- `
1855
- [pilot] \u26A0\uFE0F Assess cycle ${cycle}/${maxCycles} failed. Re-planning...
1856
- Gap: ${assessResult.replanGuidance}
1857
- Elapsed: ${elapsedSec}s
1858
-
1859
- `
1860
- );
1861
- const { db: replanDb, close: closeReplanDb } = openStateDb(dbPath);
1862
- logEvent(replanDb, {
1863
- workflowId,
1864
- phase: "plan",
1865
- kind: "task.plan.replan",
1866
- payload: { gap: assessResult.replanGuidance, cycle }
1867
- });
1868
- closeReplanDb();
1869
- const replanResult = await runPlanPhase({
1870
- workflowId,
1871
- scope: {
1872
- ...scope,
1873
- context: `${scope.context ?? ""}
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
- ok: false,
1898
- reason: `Assess failed after ${maxCycles} cycles. Manual intervention required.`,
1899
- workflowId
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/pilot/cli/go.ts
1917
- var goCmd = command3({
1918
- name: "go",
1919
- description: "Run the autonomous SPEAR loop (Plan \u2192 Execute \u2192 Assess \u2192 Resolve). Requires a scope from `pilot scope`.",
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
- scope: option({
1922
- long: "scope",
1923
- type: optional(stringType),
1924
- description: "Path to scope.json (defaults to the current scope from `pilot scope`)"
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 ({ scope }) => {
1728
+ handler: async ({ prompt, maxIterations, timeout }) => {
1928
1729
  const cwd = process.cwd();
1929
- console.log("\n\x1B[1mPilot v2 \u2014 Autonomous execution\x1B[0m");
1930
- console.log("Running: Plan \u2192 Execute \u2192 Assess \u2192 Resolve\n");
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
- if (result.workflowId) {
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
- Migration guide:
2036
- pilot scope "<goal>" \u2014 interactive scoping (replaces pilot plan)
2037
- pilot go \u2014 autonomous execution (replaces pilot build)
2038
- pilot configure \u2014 set up models and verify commands
2039
- pilot status \u2014 check workflow status
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 fs9 from "fs";
2073
- import * as path5 from "path";
2074
- import * as os4 from "os";
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 fileURLToPath2 } from "url";
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 c = {
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"] ?? path5.join(os4.homedir(), ".cache");
2102
- return path5.join(cacheHome, "harness-opencode", "cli-update.json");
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 = fs9.readFileSync(getStateFilePath(), "utf8");
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
- fs9.mkdirSync(path5.dirname(statePath), { recursive: true });
2116
- fs9.writeFileSync(statePath, JSON.stringify(state));
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 = path5.dirname(fileURLToPath2(import.meta.url));
1808
+ const here = path8.dirname(fileURLToPath3(import.meta.url));
2122
1809
  const candidates = [
2123
- path5.join(here, "..", "package.json"),
2124
- path5.join(here, "..", "..", "package.json"),
2125
- path5.join(here, "package.json")
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 = fs9.readFileSync(candidate, "utf8");
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
- ${c.blue}\u2022${c.reset} Updating ${PACKAGE_NAME} ${c.dim}${currentVersionStr}${c.reset} \u2192 ${c.green}${latestStr}${c.reset} in the background...
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
- ${c.yellow}${c.bold}Major update available:${c.reset} ${current} \u2192 ${c.green}${latest}${c.reset}
2216
- ${c.dim}Review the changelog before upgrading:${c.reset}
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 = command6({
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: flag2({
1946
+ dryRun: flag({
2260
1947
  long: "dry-run",
2261
1948
  description: "Preview changes without writing."
2262
1949
  }),
2263
- pin: flag2({
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 = command6({
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: flag2({
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 = command6({
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 = command6({
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: option3({
1984
+ run: option2({
2298
1985
  long: "run",
2299
- type: optional3(string2),
1986
+ type: optional2(string),
2300
1987
  description: "Print verify commands for pending items, one per line."
2301
1988
  }),
2302
- check: option3({
1989
+ check: option2({
2303
1990
  long: "check",
2304
- type: optional3(string2),
1991
+ type: optional2(string),
2305
1992
  description: "Structural validation; exits 1 if any item is invalid."
2306
1993
  }),
2307
- rest: restPositionals2({
2308
- type: string2,
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 = command6({
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 = command6({
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: flag2({
2035
+ dryRun: flag({
2349
2036
  long: "dry-run",
2350
2037
  description: "Preview changes without writing."
2351
2038
  }),
2352
- pin: flag2({
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 = subcommands2({
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
- pilot: pilotSubcommand
2059
+ autopilot: autopilotCmd
2373
2060
  }
2374
2061
  });
2375
2062
  var printUpdate = startUpdateCheck();