@glrs-dev/cli 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/{chunk-SB3MLROC.js → chunk-MIWZLETC.js} +7 -2
  3. package/dist/cli.js +1 -1
  4. package/dist/lib/auto-update.js +1 -1
  5. package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +34 -4
  6. package/dist/vendor/harness-opencode/dist/agents/prompts/build.open.md +18 -4
  7. package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer-thorough.md +77 -0
  8. package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer.md +80 -0
  9. package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer.open.md +68 -0
  10. package/dist/vendor/harness-opencode/dist/agents/prompts/debriefer.md +55 -0
  11. package/dist/vendor/harness-opencode/dist/agents/prompts/gap-analyzer.md +2 -0
  12. package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +5 -1
  13. package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +119 -10
  14. package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +149 -88
  15. package/dist/vendor/harness-opencode/dist/agents/prompts/research-auto.md +1 -1
  16. package/dist/vendor/harness-opencode/dist/agents/prompts/research-local.md +1 -1
  17. package/dist/vendor/harness-opencode/dist/agents/prompts/research-web.md +1 -1
  18. package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +2 -0
  19. package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +129 -0
  20. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +53 -0
  21. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +56 -0
  22. package/dist/vendor/harness-opencode/dist/agents/shared/index.ts +1 -0
  23. package/dist/vendor/harness-opencode/dist/agents/shared/ui-evaluation-ladder.md +50 -0
  24. package/dist/vendor/harness-opencode/dist/agents/shared/workflow-mechanics.md +5 -5
  25. package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +104 -0
  26. package/dist/vendor/harness-opencode/dist/chunk-GCWHRUOK.js +259 -0
  27. package/dist/vendor/harness-opencode/dist/chunk-MJSMBY2Y.js +87 -0
  28. package/dist/vendor/harness-opencode/dist/chunk-NIFAVPNN.js +544 -0
  29. package/dist/vendor/harness-opencode/dist/{chunk-VJUETC6A.js → chunk-PDMXYZM4.js} +53 -1
  30. package/dist/vendor/harness-opencode/dist/cli.js +1596 -1964
  31. package/dist/vendor/harness-opencode/dist/commands/prompts/fresh.md +27 -24
  32. package/dist/vendor/harness-opencode/dist/commands/prompts/review.md +3 -3
  33. package/dist/vendor/harness-opencode/dist/commands/prompts/ship.md +2 -0
  34. package/dist/vendor/harness-opencode/dist/index.js +188 -633
  35. package/dist/vendor/harness-opencode/dist/loop-session-J35NILUZ.js +30 -0
  36. package/dist/vendor/harness-opencode/dist/opencode-server-KPCDFYAX.js +22 -0
  37. package/dist/vendor/harness-opencode/dist/plan-parser-TMHEKT22.js +6 -0
  38. package/dist/vendor/harness-opencode/dist/plan-session-7VS32P52.js +117 -0
  39. package/dist/vendor/harness-opencode/dist/scoper-S77SOK7X.js +326 -0
  40. package/dist/vendor/harness-opencode/dist/skills/adversarial-review-rubric/SKILL.md +47 -0
  41. package/dist/vendor/harness-opencode/dist/skills/code-quality/SKILL.md +1 -1
  42. package/dist/vendor/harness-opencode/dist/skills/root-cause-diagnosis/SKILL.md +24 -0
  43. package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +167 -0
  44. package/dist/vendor/harness-opencode/package.json +1 -1
  45. package/package.json +3 -1
  46. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-assessor.md +0 -77
  47. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +0 -40
  48. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +0 -56
  49. package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-scoper.md +0 -58
  50. package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.md +0 -68
  51. package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.open.md +0 -58
  52. package/dist/vendor/harness-opencode/dist/agents/prompts/qa-thorough.md +0 -63
  53. package/dist/vendor/harness-opencode/dist/bin/plan-check.sh +0 -255
  54. package/dist/vendor/harness-opencode/dist/chunk-6CZPRUMJ.js +0 -869
  55. package/dist/vendor/harness-opencode/dist/chunk-DZG4D3OH.js +0 -54
  56. package/dist/vendor/harness-opencode/dist/chunk-OYRKOEXK.js +0 -88
  57. package/dist/vendor/harness-opencode/dist/commands/prompts/autopilot.md +0 -96
  58. package/dist/vendor/harness-opencode/dist/install-6775ZBDG.js +0 -13
  59. package/dist/vendor/harness-opencode/dist/paths-WZ23ZQOV.js +0 -18
@@ -1,2082 +1,1763 @@
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";
8
+ } from "./chunk-PDMXYZM4.js";
5
9
  import {
6
- install
7
- } from "./chunk-6CZPRUMJ.js";
8
- import "./chunk-VJUETC6A.js";
10
+ MAX_ITERATIONS,
11
+ TIMEOUT_MS,
12
+ runRalphLoop
13
+ } from "./chunk-NIFAVPNN.js";
14
+ import "./chunk-MJSMBY2Y.js";
9
15
  import {
10
- getCurrentScopePath,
11
- getPilotConfigPath,
12
- getPilotDir,
13
- getPlanArtifactPath,
14
- getScopeArtifactPath,
15
- getStateDbPath
16
- } from "./chunk-OYRKOEXK.js";
16
+ createSession,
17
+ getLastAssistantMessage,
18
+ sendAndWait
19
+ } from "./chunk-GCWHRUOK.js";
17
20
 
18
21
  // src/cli.ts
19
22
  import {
20
23
  binary,
21
- command as command6,
24
+ command as command3,
22
25
  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,
26
+ positional as positional2,
27
+ subcommands,
29
28
  run
30
29
  } from "cmd-ts";
31
30
 
32
- // src/cli/uninstall.ts
31
+ // src/cli/install.ts
32
+ import * as fs3 from "fs";
33
+ import * as path3 from "path";
34
+ import * as os2 from "os";
35
+ import { fileURLToPath } from "url";
36
+
37
+ // src/cli/merge-config.ts
33
38
  import * as fs from "fs";
34
39
  import * as path from "path";
35
- import * as os from "os";
36
- var PLUGIN_NAME = "@glrs-dev/harness-plugin-opencode";
37
- function getOpencodeConfigPath() {
38
- const configHome = process.env["XDG_CONFIG_HOME"] ?? path.join(os.homedir(), ".config");
39
- return path.join(configHome, "opencode", "opencode.json");
40
- }
41
- function uninstall(opts = {}) {
42
- const { dryRun = false } = opts;
43
- const configPath = getOpencodeConfigPath();
44
- const c2 = {
45
- reset: "\x1B[0m",
46
- green: "\x1B[32m",
47
- yellow: "\x1B[33m",
48
- blue: "\x1B[34m"
49
- };
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}`);
53
- console.log(`
54
- ${c2.blue}Uninstalling ${PLUGIN_NAME}${c2.reset}
55
- `);
56
- if (!fs.existsSync(configPath)) {
57
- warn(`No opencode.json found at ${configPath} \u2014 nothing to do`);
58
- return;
40
+ var UNION_ALLOWLIST = /* @__PURE__ */ new Set(["plugin"]);
41
+ function isPlainObject(v) {
42
+ return typeof v === "object" && v !== null && !Array.isArray(v) && Object.prototype.toString.call(v) === "[object Object]";
43
+ }
44
+ function deepClone(v) {
45
+ if (v === null || typeof v !== "object") return v;
46
+ if (Array.isArray(v)) return v.map(deepClone);
47
+ const out = {};
48
+ for (const k of Object.keys(v)) {
49
+ out[k] = deepClone(v[k]);
50
+ }
51
+ return out;
52
+ }
53
+ function fmtPath(parts) {
54
+ return parts.map((p) => /^[A-Za-z_$][\w$]*$/.test(p) ? p : `["${p.replace(/"/g, '\\"')}"]`).reduce((acc, part) => {
55
+ if (acc === "") return part;
56
+ if (part.startsWith("[")) return acc + part;
57
+ return acc + "." + part;
58
+ }, "");
59
+ }
60
+ function pluginName(entry) {
61
+ if (typeof entry === "string") {
62
+ const atIdx = entry.indexOf("@", 1);
63
+ return atIdx > 0 ? entry.slice(0, atIdx) : entry;
64
+ }
65
+ if (Array.isArray(entry) && typeof entry[0] === "string") {
66
+ const name = entry[0];
67
+ const atIdx = name.indexOf("@", 1);
68
+ return atIdx > 0 ? name.slice(0, atIdx) : name;
69
+ }
70
+ return null;
71
+ }
72
+ function mergeWalk(src, dst, pathParts, additions, warnings) {
73
+ for (const key of Object.keys(src)) {
74
+ const sv = src[key];
75
+ const newPath = pathParts.concat([key]);
76
+ const pathStr = fmtPath(newPath);
77
+ if (!Object.prototype.hasOwnProperty.call(dst, key)) {
78
+ dst[key] = deepClone(sv);
79
+ additions.push(`added: ${pathStr}`);
80
+ continue;
81
+ }
82
+ const dv = dst[key];
83
+ if (isPlainObject(sv) && isPlainObject(dv)) {
84
+ mergeWalk(sv, dv, newPath, additions, warnings);
85
+ continue;
86
+ }
87
+ if (isPlainObject(sv) && !isPlainObject(dv)) {
88
+ warnings.push(
89
+ `WARN: scalar-vs-object: user has non-object at ${pathStr} where we ship an object; not migrating. To adopt: ${JSON.stringify(sv)}`
90
+ );
91
+ continue;
92
+ }
93
+ if (Array.isArray(sv)) {
94
+ if (!Array.isArray(dv)) {
95
+ warnings.push(
96
+ `WARN: scalar-vs-array: user has non-array at ${pathStr} where we ship an array; not migrating. To adopt: ${JSON.stringify(sv)}`
97
+ );
98
+ continue;
99
+ }
100
+ const joined = newPath.join(".");
101
+ if (UNION_ALLOWLIST.has(joined)) {
102
+ for (const item of sv) {
103
+ const srcName = pluginName(item);
104
+ if (srcName) {
105
+ const dstIdx = dv.findIndex(
106
+ (x) => pluginName(x) === srcName
107
+ );
108
+ if (dstIdx >= 0) {
109
+ const srcIsTuple = Array.isArray(item) && item.length >= 2;
110
+ const dstIsTuple = Array.isArray(dv[dstIdx]) && dv[dstIdx].length >= 2;
111
+ if (srcIsTuple && !dstIsTuple) {
112
+ dv[dstIdx] = deepClone(item);
113
+ additions.push(`upgraded: ${pathStr}[${JSON.stringify(srcName)}] to tuple form`);
114
+ }
115
+ } else {
116
+ dv.push(deepClone(item));
117
+ additions.push(`appended: ${pathStr}[${JSON.stringify(item)}]`);
118
+ }
119
+ } else {
120
+ const needle = JSON.stringify(item);
121
+ const alreadyPresent = dv.some(
122
+ (x) => JSON.stringify(x) === needle
123
+ );
124
+ if (!alreadyPresent) {
125
+ dv.push(deepClone(item));
126
+ additions.push(`appended: ${pathStr}[${JSON.stringify(item)}]`);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ continue;
132
+ }
59
133
  }
60
- let raw;
134
+ }
135
+ function mergeConfig(srcJson, dstPath, dryRun = false) {
136
+ let dstText;
61
137
  try {
62
- raw = fs.readFileSync(configPath, "utf8");
138
+ dstText = fs.readFileSync(dstPath, "utf8");
63
139
  } catch (e) {
64
- console.error(`\x1B[31m\u2717\x1B[0m Failed to read ${configPath}: ${e.message}`);
65
- process.exit(1);
140
+ throw new Error(`Failed to read dst ${dstPath}: ${e.message}`);
66
141
  }
67
- let config;
142
+ let dst;
68
143
  try {
69
- config = JSON.parse(raw);
144
+ dst = JSON.parse(dstText);
70
145
  } catch (e) {
71
- console.error(`\x1B[31m\u2717\x1B[0m Invalid JSON in ${configPath}: ${e.message}`);
72
- process.exit(1);
146
+ throw new Error(
147
+ `User config at ${dstPath} has invalid JSON: ${e.message}. Not touching the file.`
148
+ );
73
149
  }
74
- const plugins = Array.isArray(config.plugin) ? config.plugin : [];
75
- const filtered = plugins.filter((p) => {
76
- const name = typeof p === "string" ? p : Array.isArray(p) ? p[0] : null;
77
- return name !== PLUGIN_NAME && !String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
78
- });
79
- if (filtered.length === plugins.length) {
80
- warn(`"${PLUGIN_NAME}" not found in plugin array \u2014 nothing to remove`);
81
- return;
150
+ if (!isPlainObject(dst)) {
151
+ throw new Error(
152
+ `User config at ${dstPath} is not a JSON object at the top level.`
153
+ );
154
+ }
155
+ const additions = [];
156
+ const warnings = [];
157
+ mergeWalk(srcJson, dst, [], additions, warnings);
158
+ if (additions.length === 0) {
159
+ return { changed: false, warnings };
82
160
  }
83
161
  if (dryRun) {
84
- info(`[dry-run] Would remove "${PLUGIN_NAME}" from plugin array in ${configPath}`);
85
- return;
162
+ return { changed: true, bakPath: "(dry-run)", additions, warnings };
86
163
  }
87
- const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
164
+ const suffix = `${Date.now()}-${process.pid}`;
165
+ const bakPath = `${dstPath}.bak.${suffix}`;
166
+ const tmpPath = `${dstPath}.merge.tmp.${suffix}`;
88
167
  try {
89
- fs.copyFileSync(configPath, bakPath);
168
+ fs.copyFileSync(dstPath, bakPath);
90
169
  } catch (e) {
91
- console.error(`\x1B[31m\u2717\x1B[0m Failed to write backup: ${e.message}`);
92
- process.exit(1);
170
+ throw new Error(`Failed to write backup ${bakPath}: ${e.message}`);
171
+ }
172
+ const serialized = JSON.stringify(dst, null, 2) + "\n";
173
+ try {
174
+ fs.writeFileSync(tmpPath, serialized);
175
+ } catch (e) {
176
+ try {
177
+ fs.unlinkSync(bakPath);
178
+ } catch {
179
+ }
180
+ throw new Error(`Failed to write tempfile ${tmpPath}: ${e.message}`);
93
181
  }
94
- config.plugin = filtered;
95
- const tmpPath = `${configPath}.uninstall.tmp.${Date.now()}-${process.pid}`;
96
182
  try {
97
- fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
98
- fs.renameSync(tmpPath, configPath);
183
+ fs.renameSync(tmpPath, dstPath);
99
184
  } catch (e) {
100
185
  try {
101
186
  fs.unlinkSync(tmpPath);
102
187
  } catch {
103
188
  }
104
- console.error(`\x1B[31m\u2717\x1B[0m Failed to write config: ${e.message}`);
105
- process.exit(1);
189
+ try {
190
+ fs.unlinkSync(bakPath);
191
+ } catch {
192
+ }
193
+ throw new Error(`Failed to rename ${tmpPath} \u2192 ${dstPath}: ${e.message}`);
106
194
  }
107
- ok(`Removed "${PLUGIN_NAME}" from ${configPath}`);
108
- info(`Backup: ${bakPath}`);
109
- console.log(`
110
- To fully remove the package: bun remove @glrs-dev/harness-plugin-opencode
111
- `);
195
+ return { changed: true, bakPath, additions, warnings };
196
+ }
197
+ function seedConfig(srcJson, dstPath) {
198
+ fs.mkdirSync(path.dirname(dstPath), { recursive: true });
199
+ fs.writeFileSync(dstPath, JSON.stringify(srcJson, null, 2) + "\n");
112
200
  }
113
201
 
114
- // src/cli/doctor.ts
202
+ // src/cli/plugin-check.ts
115
203
  import * as fs2 from "fs";
116
204
  import * as path2 from "path";
117
- import * as os2 from "os";
118
- 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");
205
+ import * as os from "os";
206
+ import { select, checkbox, confirm } from "@inquirer/prompts";
207
+ async function promptChoice(question, choices, defaultIndex = 0) {
208
+ if (!process.stdin.isTTY) return defaultIndex;
209
+ const answer = await select({
210
+ message: question,
211
+ choices: choices.map((label, i) => ({
212
+ name: label,
213
+ value: i
214
+ })),
215
+ default: defaultIndex
216
+ });
217
+ return answer;
123
218
  }
124
- function cmd(command7) {
125
- try {
126
- return execSync(command7, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
127
- } catch {
128
- return null;
129
- }
219
+ async function promptMulti(question, choices) {
220
+ if (!process.stdin.isTTY) {
221
+ const defaults = /* @__PURE__ */ new Set();
222
+ choices.forEach((c3, i) => {
223
+ if (c3.defaultOn) defaults.add(i);
224
+ });
225
+ return defaults;
226
+ }
227
+ const answers = await checkbox({
228
+ message: question,
229
+ choices: choices.map((c3, i) => ({
230
+ name: c3.label,
231
+ value: i,
232
+ checked: c3.defaultOn
233
+ }))
234
+ });
235
+ return new Set(answers);
130
236
  }
131
- function which(bin) {
132
- return cmd(`which ${bin}`) !== null;
237
+
238
+ // src/cli/models-dev.ts
239
+ var MODELS_DEV_URL = "https://models.dev/api.json";
240
+ var FETCH_TIMEOUT_MS = 5e3;
241
+ function combinedCost(m) {
242
+ const input = m.cost?.input ?? 0;
243
+ const output = m.cost?.output ?? 0;
244
+ return input + output;
245
+ }
246
+ function suggestTiersFromModelsDev(provider) {
247
+ const models = Object.values(provider.models).sort(
248
+ (a, b) => combinedCost(b) - combinedCost(a)
249
+ );
250
+ if (models.length === 0) {
251
+ throw new Error(`Provider "${provider.id}" has no models`);
252
+ }
253
+ const deep = models[0];
254
+ const fast = models[models.length - 1];
255
+ let mid;
256
+ if (models.length <= 2) {
257
+ mid = models.length === 1 ? deep : fast;
258
+ } else {
259
+ const midCost = (combinedCost(deep) + combinedCost(fast)) / 2;
260
+ const candidates = models.filter(
261
+ (m) => m.id !== deep.id && m.id !== fast.id
262
+ );
263
+ mid = candidates.reduce(
264
+ (best, m) => Math.abs(combinedCost(m) - midCost) < Math.abs(combinedCost(best) - midCost) ? m : best
265
+ );
266
+ }
267
+ const ref = (m) => `${provider.id}/${m.id}`;
268
+ return {
269
+ deep: ref(deep),
270
+ mid: ref(mid),
271
+ fast: ref(fast)
272
+ };
133
273
  }
134
- function doctor() {
135
- const c2 = {
136
- reset: "\x1B[0m",
137
- green: "\x1B[32m",
138
- yellow: "\x1B[33m",
139
- red: "\x1B[31m",
140
- bold: "\x1B[1m"
274
+ function pickBedrockTierIds(provider) {
275
+ const models = Object.values(provider.models);
276
+ const mostRecent = (candidates) => {
277
+ if (candidates.length === 0) return null;
278
+ return [...candidates].sort((a, b) => {
279
+ const aDate = a.last_updated ?? "";
280
+ const bDate = b.last_updated ?? "";
281
+ if (aDate !== bDate) return bDate.localeCompare(aDate);
282
+ return b.id.localeCompare(a.id);
283
+ })[0];
141
284
  };
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}`);
145
- console.log(`
146
- ${c2.bold}Doctor \u2014 ${PLUGIN_NAME2}${c2.reset}
147
- `);
148
- const ocVersion = cmd("opencode --version 2>/dev/null | head -1");
149
- if (ocVersion) {
150
- ok(`opencode ${ocVersion}`);
151
- } else {
152
- fail("opencode CLI not found \u2014 install from https://opencode.ai");
285
+ const pickFamily = (familyKeyword) => {
286
+ const globalCandidates = models.filter(
287
+ (m) => m.id.startsWith(`global.anthropic.claude-${familyKeyword}-`)
288
+ );
289
+ const globalPick = mostRecent(globalCandidates);
290
+ if (globalPick) return globalPick;
291
+ const nonPrefixedCandidates = models.filter(
292
+ (m) => m.id.startsWith(`anthropic.claude-${familyKeyword}-`)
293
+ );
294
+ return mostRecent(nonPrefixedCandidates);
295
+ };
296
+ const opus = pickFamily("opus");
297
+ const sonnet = pickFamily("sonnet");
298
+ const haiku = pickFamily("haiku");
299
+ if (!opus || !sonnet || !haiku) {
300
+ return suggestTiersFromModelsDev(provider);
153
301
  }
154
- const configPath = getOpencodeConfigPath2();
155
- if (fs2.existsSync(configPath)) {
156
- try {
157
- const config = JSON.parse(fs2.readFileSync(configPath, "utf8"));
158
- const plugins = Array.isArray(config.plugin) ? config.plugin : [];
159
- let pluginOptions = null;
160
- const hasPlugin = plugins.some((p) => {
161
- if (typeof p === "string") {
162
- return p === PLUGIN_NAME2 || p.startsWith(`${PLUGIN_NAME2}@`);
163
- }
164
- if (Array.isArray(p)) {
165
- const [name, opts] = p;
166
- const match = name === PLUGIN_NAME2 || String(name ?? "").startsWith(`${PLUGIN_NAME2}@`);
167
- if (match && opts && typeof opts === "object") {
168
- pluginOptions = opts;
169
- }
170
- return match;
171
- }
172
- return false;
173
- });
174
- if (hasPlugin) {
175
- ok(`"${PLUGIN_NAME2}" present in opencode.json plugin array`);
176
- } else {
177
- warn(`"${PLUGIN_NAME2}" NOT in opencode.json plugin array \u2014 run: bunx ${PLUGIN_NAME2} install`);
178
- }
179
- const modelSources = [];
180
- if (pluginOptions && typeof pluginOptions.models === "object") {
181
- modelSources.push({
182
- label: "plugin options.models",
183
- block: pluginOptions.models
184
- });
185
- }
186
- const legacyHarness = config.harness;
187
- if (legacyHarness && typeof legacyHarness.models === "object") {
188
- modelSources.push({
189
- label: "harness.models (legacy)",
190
- block: legacyHarness.models
191
- });
192
- }
193
- if (modelSources.length > 0) {
194
- const invalid = [];
195
- for (const { label, block } of modelSources) {
196
- if (!block || typeof block !== "object") continue;
197
- for (const [key, rawValue] of Object.entries(block)) {
198
- const candidate = Array.isArray(rawValue) ? rawValue[0] : rawValue;
199
- if (typeof candidate !== "string") continue;
200
- const result = validateModelOverride(candidate);
201
- if (!result.valid) {
202
- invalid.push({
203
- keyPath: `${label}.${key}`,
204
- value: candidate,
205
- suggestion: result.suggestion,
206
- reason: result.reason
207
- });
208
- }
209
- }
210
- }
211
- if (invalid.length === 0) {
212
- ok("model overrides look valid");
213
- } else {
214
- for (const entry of invalid) {
215
- fail(`invalid model override at ${entry.keyPath}: "${entry.value}"`);
216
- if (entry.reason) {
217
- console.log(` ${c2.yellow}reason:${c2.reset} ${entry.reason}`);
218
- }
219
- if (entry.suggestion) {
220
- console.log(
221
- ` ${c2.yellow}fix:${c2.reset} remove this key, or replace with \`${entry.suggestion}\``
222
- );
223
- } else {
224
- console.log(
225
- ` ${c2.yellow}fix:${c2.reset} remove this key, or run \`bunx ${PLUGIN_NAME2} install\` to pick a current preset`
226
- );
227
- }
228
- }
229
- }
230
- }
231
- } catch {
232
- fail(`opencode.json at ${configPath} has invalid JSON`);
302
+ const ref = (m) => `${provider.id}/${m.id}`;
303
+ return {
304
+ deep: ref(opus),
305
+ mid: ref(sonnet),
306
+ fast: ref(haiku)
307
+ };
308
+ }
309
+ async function fetchModelsDevProviders() {
310
+ const controller = new AbortController();
311
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
312
+ try {
313
+ const res = await fetch(MODELS_DEV_URL, { signal: controller.signal });
314
+ if (!res.ok) return null;
315
+ const data = await res.json();
316
+ if (!data || typeof data !== "object" || Array.isArray(data)) return null;
317
+ const providers = [];
318
+ for (const [key, rawValue] of Object.entries(
319
+ data
320
+ )) {
321
+ if (!rawValue || typeof rawValue !== "object") continue;
322
+ const value = rawValue;
323
+ if (typeof value.id !== "string" || value.id !== key) continue;
324
+ if (typeof value.name !== "string") continue;
325
+ if (!value.models || typeof value.models !== "object") continue;
326
+ providers.push(value);
233
327
  }
234
- } else {
235
- warn(`No opencode.json at ${configPath} \u2014 run: bunx ${PLUGIN_NAME2} install`);
236
- }
237
- if (which("uvx")) {
238
- ok("uvx (serena + git MCPs)");
239
- } else {
240
- warn("uvx not found \u2014 serena and git MCPs won't work. Install: brew install uv");
241
- }
242
- if (which("node") && which("npx")) {
243
- ok(`node ${cmd("node --version") ?? ""} + npx (memory MCP)`);
244
- } else {
245
- warn("node/npx not found \u2014 memory MCP won't work");
246
- }
247
- const planCheckResult = cmd(`bunx ${PLUGIN_NAME2} plan-check --help 2>/dev/null`);
248
- if (planCheckResult !== null) {
249
- ok("plan-check CLI invokable");
250
- } else {
251
- warn("plan-check CLI not invokable \u2014 try: bun install");
252
- }
253
- if (which("bun")) {
254
- ok(`bun ${cmd("bun --version") ?? ""}`);
255
- } else if (which("npm")) {
256
- ok(`npm ${cmd("npm --version") ?? ""} (install bun for faster installs)`);
257
- } else {
258
- fail("Neither bun nor npm found \u2014 cannot install plugins");
328
+ if (providers.length === 0) return null;
329
+ return providers;
330
+ } catch {
331
+ return null;
332
+ } finally {
333
+ clearTimeout(timer);
259
334
  }
260
- 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");
335
+ }
336
+
337
+ // src/cli/install.ts
338
+ var PLUGIN_NAME = "@glrs-dev/harness-plugin-opencode";
339
+ var c = {
340
+ reset: "\x1B[0m",
341
+ green: "\x1B[32m",
342
+ yellow: "\x1B[33m",
343
+ blue: "\x1B[34m",
344
+ dim: "\x1B[2m",
345
+ bold: "\x1B[1m"
346
+ };
347
+ var ok = (msg) => console.log(`${c.green}\u2713${c.reset} ${msg}`);
348
+ var info = (msg) => console.log(`${c.blue}\u2022${c.reset} ${msg}`);
349
+ var warn = (msg) => console.log(`${c.yellow}!${c.reset} ${msg}`);
350
+ var MODEL_PRESETS = [
351
+ {
352
+ label: "Anthropic API (direct)",
353
+ providerId: "anthropic",
354
+ deep: "anthropic/claude-opus-4-7",
355
+ mid: "anthropic/claude-sonnet-4-6",
356
+ fast: "anthropic/claude-haiku-4-5-20251001"
357
+ },
358
+ {
359
+ label: "AWS Bedrock",
360
+ providerId: "amazon-bedrock",
361
+ deep: "amazon-bedrock/global.anthropic.claude-opus-4-7",
362
+ mid: "amazon-bedrock/global.anthropic.claude-sonnet-4-6",
363
+ fast: "amazon-bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0"
364
+ },
365
+ {
366
+ label: "Google Vertex AI (Claude)",
367
+ providerId: "google-vertex-anthropic",
368
+ deep: "google-vertex-anthropic/claude-opus-4-7@default",
369
+ mid: "google-vertex-anthropic/claude-sonnet-4-6@default",
370
+ fast: "google-vertex-anthropic/claude-haiku-4-5@20251001"
267
371
  }
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`");
372
+ ];
373
+ var MCP_TOGGLES = [
374
+ { name: "playwright", label: "Playwright \u2014 browser automation + visual UI verification (requires Chromium)", defaultOn: false },
375
+ { name: "linear", label: "Linear \u2014 issue tracker integration", defaultOn: false }
376
+ ];
377
+ var PLUGIN_TOGGLES = [
378
+ {
379
+ name: "opencode-snip",
380
+ label: "Token reduction \u2014 opencode-snip (requires Go snip binary)",
381
+ defaultOn: false
272
382
  }
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
- }
383
+ ];
384
+ function extractPluginOptions(config) {
385
+ if (!config) return null;
386
+ const plugins = config.plugin;
387
+ if (!Array.isArray(plugins)) return null;
388
+ for (const entry of plugins) {
389
+ if (Array.isArray(entry) && entry.length >= 2 && (entry[0] === PLUGIN_NAME || String(entry[0]).startsWith(`${PLUGIN_NAME}@`))) {
390
+ return entry[1];
283
391
  }
284
- } else {
285
- warn(
286
- "could not run `opencode agent list` \u2014 skipping pilot agent registration check"
287
- );
288
392
  }
289
- console.log();
393
+ return null;
290
394
  }
291
-
292
- // src/bin/plan-check.ts
293
- import { execFileSync } from "child_process";
294
- import { fileURLToPath } from "url";
295
- import { dirname, join as join3 } from "path";
296
- function planCheck(args) {
297
- const here = dirname(fileURLToPath(import.meta.url));
395
+ function readPackageVersion() {
396
+ const here = path3.dirname(fileURLToPath(import.meta.url));
298
397
  const candidates = [
299
- join3(here, "plan-check.sh"),
300
- // dev: src/bin/plan-check.sh
301
- join3(here, "bin", "plan-check.sh")
302
- // dist: dist/ → dist/bin/plan-check.sh
398
+ path3.join(here, "..", "package.json"),
399
+ path3.join(here, "..", "..", "package.json")
303
400
  ];
304
- let scriptPath;
305
- for (const p of candidates) {
401
+ for (const candidate of candidates) {
306
402
  try {
307
- execFileSync("test", ["-f", p]);
308
- scriptPath = p;
309
- break;
403
+ const raw = fs3.readFileSync(candidate, "utf8");
404
+ const parsed = JSON.parse(raw);
405
+ if (parsed.name === PLUGIN_NAME && typeof parsed.version === "string") {
406
+ return parsed.version;
407
+ }
310
408
  } catch {
311
409
  }
312
410
  }
313
- if (!scriptPath) {
314
- console.error("plan-check: could not find plan-check.sh");
315
- process.exit(2);
411
+ throw new Error(
412
+ `Could not locate ${PLUGIN_NAME}'s package.json to read version`
413
+ );
414
+ }
415
+ function getOpencodeConfigPath() {
416
+ const configHome = process.env["XDG_CONFIG_HOME"] ?? path3.join(os2.homedir(), ".config");
417
+ return path3.join(configHome, "opencode", "opencode.json");
418
+ }
419
+ async function refreshPluginCacheIfStale() {
420
+ try {
421
+ const cacheDir = getOpenCodeCachePackageDir();
422
+ const pin = await inspectCachePin(cacheDir);
423
+ if (pin.kind !== "exact") return;
424
+ const ourVersion = readOurPackageVersion(import.meta.url);
425
+ if (pin.version === ourVersion) return;
426
+ const result = await refreshPluginCache(pin.version, ourVersion);
427
+ if (result.outcome === "refreshed") {
428
+ ok(`Plugin cache updated: ${result.fromVersion} \u2192 ${result.toVersion}`);
429
+ }
430
+ } catch {
316
431
  }
432
+ }
433
+ function readExistingConfig(configPath) {
434
+ if (!fs3.existsSync(configPath)) return null;
317
435
  try {
318
- execFileSync("bash", [scriptPath, ...args], {
319
- stdio: "inherit",
320
- encoding: "utf8"
321
- });
322
- } catch (e) {
323
- process.exit(e.status ?? 1);
436
+ return JSON.parse(fs3.readFileSync(configPath, "utf8"));
437
+ } catch {
438
+ return null;
324
439
  }
325
440
  }
326
-
327
- // src/plan-paths.ts
328
- import { execFile } from "child_process";
329
- import * as fs3 from "fs/promises";
330
- import * as os3 from "os";
331
- import * as path3 from "path";
332
- function execFileP(file, args, opts = {}) {
333
- const { cwd, timeoutMs = 5e3 } = opts;
334
- return new Promise((resolve2, reject) => {
335
- const controller = new AbortController();
336
- const timer = setTimeout(() => controller.abort(), timeoutMs);
337
- execFile(
338
- file,
339
- args,
340
- { signal: controller.signal, cwd, encoding: "utf8" },
341
- (err, stdout) => {
342
- clearTimeout(timer);
343
- if (err) {
344
- reject(err);
345
- return;
346
- }
347
- resolve2(stdout ?? "");
348
- }
349
- );
350
- });
351
- }
352
- function expandTilde(p) {
353
- if (p === "~") return os3.homedir();
354
- if (p.startsWith("~/")) return path3.join(os3.homedir(), p.slice(2));
355
- return p;
356
- }
357
- async function getRepoFolder(worktreeDir) {
358
- let stdout;
359
- try {
360
- stdout = await execFileP(
361
- "git",
362
- ["rev-parse", "--git-common-dir"],
363
- { cwd: worktreeDir }
364
- );
365
- } catch (err) {
366
- const msg = err instanceof Error ? err.message : "unknown error running `git rev-parse --git-common-dir`";
367
- throw new Error(
368
- `getRepoFolder: failed to resolve git-common-dir for ${worktreeDir}: ${msg}`
369
- );
370
- }
371
- const gitCommonDir = stdout.trim();
372
- if (!gitCommonDir) {
373
- throw new Error(
374
- `getRepoFolder: \`git rev-parse --git-common-dir\` returned empty for ${worktreeDir}`
375
- );
441
+ function detectModelProvider(existing) {
442
+ const opts = extractPluginOptions(existing);
443
+ const models = opts?.models ?? existing?.harness?.models;
444
+ if (!models) return null;
445
+ const deep = Array.isArray(models.deep) ? models.deep[0] : models.deep;
446
+ if (typeof deep !== "string") return null;
447
+ for (const preset of MODEL_PRESETS) {
448
+ if (deep === preset.deep) return preset.label;
449
+ }
450
+ return `custom (${deep})`;
451
+ }
452
+ function detectEnabledMcps(existing) {
453
+ const enabled = /* @__PURE__ */ new Set();
454
+ const mcp = existing?.mcp;
455
+ if (!mcp || typeof mcp !== "object") return enabled;
456
+ for (const toggle of MCP_TOGGLES) {
457
+ if (mcp[toggle.name]?.enabled === true) {
458
+ enabled.add(toggle.name);
459
+ }
376
460
  }
377
- const absCommonDir = path3.isAbsolute(gitCommonDir) ? gitCommonDir : path3.resolve(worktreeDir, gitCommonDir);
378
- const repoRoot = path3.dirname(absCommonDir);
379
- return path3.basename(repoRoot);
380
- }
381
- async function getPlanDir(worktreeDir) {
382
- const override = process.env.GLORIOUS_PLAN_DIR;
383
- const base = override ? expandTilde(override) : path3.join(os3.homedir(), ".glorious", "opencode");
384
- const repoFolder = await getRepoFolder(worktreeDir);
385
- const planDir = path3.join(base, repoFolder, "plans");
386
- await fs3.mkdir(planDir, { recursive: true });
387
- return planDir;
461
+ return enabled;
388
462
  }
389
- async function migratePlans(worktreeDir, planDir) {
390
- const oldDir = path3.join(worktreeDir, ".agent", "plans");
391
- const marker = path3.join(oldDir, ".migrated");
392
- try {
393
- await fs3.stat(oldDir);
394
- } catch {
395
- return;
396
- }
397
- try {
398
- await fs3.stat(marker);
399
- return;
400
- } catch {
401
- }
402
- let entries;
403
- try {
404
- entries = await fs3.readdir(oldDir);
405
- } catch {
406
- return;
407
- }
408
- const planFiles = entries.filter(
409
- (name) => name.endsWith(".md") && !name.startsWith(".")
410
- );
411
- await fs3.mkdir(planDir, { recursive: true });
412
- for (const name of planFiles) {
413
- const src = path3.join(oldDir, name);
414
- const dst = path3.join(planDir, name);
415
- let dstExists = false;
416
- try {
417
- await fs3.stat(dst);
418
- dstExists = true;
419
- } catch {
420
- dstExists = false;
421
- }
422
- if (!dstExists) {
423
- await fs3.rename(src, dst);
424
- continue;
425
- }
426
- 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;
463
+ function detectEnabledPluginToggles(existing) {
464
+ const enabled = /* @__PURE__ */ new Set();
465
+ const plugins = Array.isArray(existing?.plugin) ? existing.plugin : [];
466
+ const toggleNames = new Set(PLUGIN_TOGGLES.map((t) => t.name));
467
+ for (const entry of plugins) {
468
+ const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
469
+ if (typeof name === "string" && toggleNames.has(name)) {
470
+ enabled.add(name);
433
471
  }
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
472
  }
439
- await fs3.writeFile(marker, "");
473
+ return enabled;
440
474
  }
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
+ function migrateHarnessKeyToPluginOptions(configPath) {
475
476
  try {
476
- raw = JSON.parse(fs4.readFileSync(configPath, "utf8"));
477
+ if (!fs3.existsSync(configPath)) return;
478
+ const raw = fs3.readFileSync(configPath, "utf8");
479
+ const config = JSON.parse(raw);
480
+ if (!config.harness || typeof config.harness !== "object") return;
481
+ const plugins = Array.isArray(config.plugin) ? config.plugin : [];
482
+ const pluginIdx = plugins.findIndex((entry) => {
483
+ const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
484
+ return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
485
+ });
486
+ if (pluginIdx < 0) return;
487
+ const current = plugins[pluginIdx];
488
+ const existingName = typeof current === "string" ? current : Array.isArray(current) ? current[0] : PLUGIN_NAME;
489
+ const existingOpts = Array.isArray(current) && current.length >= 2 ? current[1] : {};
490
+ const merged = { ...config.harness, ...existingOpts };
491
+ plugins[pluginIdx] = [existingName, merged];
492
+ delete config.harness;
493
+ const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
494
+ fs3.copyFileSync(configPath, bakPath);
495
+ fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
496
+ ok("Migrated legacy `harness` config into plugin options");
497
+ info(`Backup: ${bakPath}`);
477
498
  } 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
499
  }
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
500
  }
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 });
501
+ function deepEqual(a, b) {
502
+ if (a === b) return true;
503
+ if (typeof a !== typeof b) return false;
504
+ if (a === null || b === null) return a === b;
505
+ if (typeof a !== "object") return false;
506
+ const aObj = a;
507
+ const bObj = b;
508
+ const aKeys = Object.keys(aObj);
509
+ const bKeys = Object.keys(bObj);
510
+ if (aKeys.length !== bKeys.length) return false;
511
+ for (const key of aKeys) {
512
+ if (!bKeys.includes(key)) return false;
513
+ if (!deepEqual(aObj[key], bObj[key])) return false;
537
514
  }
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
- });
515
+ return true;
574
516
  }
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);
517
+ function writePluginOption(configPath, subKey, value, opts) {
518
+ try {
519
+ if (!fs3.existsSync(configPath)) {
520
+ return { changed: false };
521
+ }
522
+ const raw = fs3.readFileSync(configPath, "utf8");
523
+ const config = JSON.parse(raw);
524
+ if (!Array.isArray(config.plugin)) {
525
+ return { changed: false };
595
526
  }
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
527
+ const pluginIdx = config.plugin.findIndex((entry) => {
528
+ const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
529
+ return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
618
530
  });
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
- });
531
+ if (pluginIdx < 0) {
532
+ return { changed: false };
625
533
  }
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");
534
+ const current = config.plugin[pluginIdx];
535
+ const existingName = typeof current === "string" ? current : Array.isArray(current) ? current[0] : PLUGIN_NAME;
536
+ const existingOpts = Array.isArray(current) && current.length >= 2 ? current[1] : {};
537
+ if (deepEqual(existingOpts[subKey], value)) {
538
+ return { changed: false };
689
539
  }
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();
540
+ const newOpts = { ...existingOpts, [subKey]: value };
541
+ if (opts.dryRun) {
542
+ info(`[dry-run] Would reconfigure ${subKey} in plugin options`);
543
+ return { changed: true };
544
+ }
545
+ const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
546
+ fs3.copyFileSync(configPath, bakPath);
547
+ config.plugin[pluginIdx] = [existingName, newOpts];
548
+ fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
549
+ ok(`Reconfigured ${subKey}`);
550
+ info(`Backup: ${bakPath}`);
551
+ return { changed: true, bakPath };
813
552
  } 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 });
553
+ return { changed: false };
845
554
  }
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
555
  }
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();
556
+ function writeMcpToggles(configPath, enabledSet, opts) {
889
557
  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 }
558
+ if (!fs3.existsSync(configPath)) {
559
+ return { changed: false };
560
+ }
561
+ const raw = fs3.readFileSync(configPath, "utf8");
562
+ const config = JSON.parse(raw);
563
+ const toggleNames = new Set(MCP_TOGGLES.map((t) => t.name));
564
+ const existingMcp = config.mcp && typeof config.mcp === "object" ? { ...config.mcp } : {};
565
+ const newMcp = {};
566
+ let hasChanges = false;
567
+ for (const [key, val] of Object.entries(existingMcp)) {
568
+ if (!toggleNames.has(key)) {
569
+ newMcp[key] = val;
900
570
  }
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
571
  }
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
- };
572
+ for (const toggleName of toggleNames) {
573
+ if (enabledSet.has(toggleName)) {
574
+ newMcp[toggleName] = { enabled: true };
575
+ if (!deepEqual(existingMcp[toggleName], { enabled: true })) {
576
+ hasChanges = true;
577
+ }
578
+ } else {
579
+ if (existingMcp[toggleName] !== void 0) {
580
+ hasChanges = true;
581
+ }
582
+ }
932
583
  }
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` };
584
+ if (!hasChanges && Object.keys(newMcp).length === Object.keys(existingMcp).length) {
585
+ const allKeysMatch = Object.keys(newMcp).every(
586
+ (k) => deepEqual(newMcp[k], existingMcp[k])
587
+ );
588
+ if (allKeysMatch) {
589
+ return { changed: false };
590
+ }
940
591
  }
941
- if (!artifact) {
942
- closeDb2();
943
- return { ok: false, reason: `scope.json at ${scopePath} has invalid schema` };
592
+ if (opts.dryRun) {
593
+ info(`[dry-run] Would reconfigure MCP toggles`);
594
+ return { changed: true };
944
595
  }
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();
596
+ const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
597
+ fs3.copyFileSync(configPath, bakPath);
598
+ if (Object.keys(newMcp).length > 0) {
599
+ config.mcp = newMcp;
600
+ } else {
601
+ delete config.mcp;
602
+ }
603
+ fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
604
+ ok("Reconfigured MCPs");
605
+ info(`Backup: ${bakPath}`);
606
+ return { changed: true, bakPath };
607
+ } catch {
608
+ return { changed: false };
965
609
  }
966
610
  }
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);
611
+ function writePluginToggles(configPath, enabledSet, opts) {
612
+ try {
613
+ if (!fs3.existsSync(configPath)) {
614
+ return { changed: false };
615
+ }
616
+ const raw = fs3.readFileSync(configPath, "utf8");
617
+ const config = JSON.parse(raw);
618
+ const toggleNames = new Set(PLUGIN_TOGGLES.map((t) => t.name));
619
+ const existingPlugins = Array.isArray(config.plugin) ? config.plugin : [];
620
+ const currentlyPresent = /* @__PURE__ */ new Set();
621
+ for (const entry of existingPlugins) {
622
+ const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
623
+ if (typeof name === "string" && toggleNames.has(name)) {
624
+ currentlyPresent.add(name);
1012
625
  }
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);
626
+ }
627
+ const toAdd = [];
628
+ const toRemove = /* @__PURE__ */ new Set();
629
+ for (const toggleName of toggleNames) {
630
+ if (enabledSet.has(toggleName) && !currentlyPresent.has(toggleName)) {
631
+ toAdd.push(toggleName);
632
+ } else if (!enabledSet.has(toggleName) && currentlyPresent.has(toggleName)) {
633
+ toRemove.add(toggleName);
1019
634
  }
1020
635
  }
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);
636
+ if (toAdd.length === 0 && toRemove.size === 0) {
637
+ return { changed: false };
1034
638
  }
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);
1045
- }
1046
- });
1047
-
1048
- // src/pilot/cli/go.ts
1049
- import { command as command3, option, string as stringType, optional } from "cmd-ts";
1050
-
1051
- // src/pilot/orchestrator.ts
1052
- import * as fs8 from "fs";
1053
-
1054
- // src/pilot/server.ts
1055
- import { execFile as execFile3 } from "child_process";
1056
- import { promisify as promisify2 } from "util";
1057
- import {
1058
- createOpencodeServer,
1059
- createOpencodeClient
1060
- } from "@opencode-ai/sdk";
1061
- var execFileP3 = promisify2(execFile3);
1062
- var DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
1063
- async function ensureOpencodeOnPath() {
1064
- try {
1065
- await execFileP3("opencode", ["--version"]);
639
+ if (opts.dryRun) {
640
+ info(`[dry-run] Would reconfigure plugin toggles`);
641
+ return { changed: true };
642
+ }
643
+ const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
644
+ fs3.copyFileSync(configPath, bakPath);
645
+ const newPlugins = existingPlugins.filter((entry) => {
646
+ const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
647
+ return !(typeof name === "string" && toRemove.has(name));
648
+ });
649
+ for (const name of toAdd) {
650
+ newPlugins.push(name);
651
+ }
652
+ config.plugin = newPlugins;
653
+ fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
654
+ ok("Reconfigured plugin add-ons");
655
+ info(`Backup: ${bakPath}`);
656
+ return { changed: true, bakPath };
1066
657
  } catch {
1067
- throw new Error(
1068
- "opencode CLI not found on PATH.\n Install: https://opencode.ai\n Or: bunx opencode upgrade"
1069
- );
658
+ return { changed: false };
1070
659
  }
1071
660
  }
1072
- async function startServer(opts) {
1073
- await ensureOpencodeOnPath();
1074
- const timeoutMs = opts.timeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
1075
- const port = opts.port ?? 0;
1076
- const server = await createOpencodeServer({
1077
- port,
1078
- timeout: timeoutMs
1079
- });
1080
- const client = createOpencodeClient({ url: server.url });
1081
- let shutdownCalled = false;
1082
- const shutdown = async () => {
1083
- if (shutdownCalled) return;
1084
- shutdownCalled = true;
1085
- try {
1086
- await server.close();
1087
- } catch {
661
+ async function install(opts = {}) {
662
+ const { dryRun = false, pin = false, nonInteractive = false } = opts;
663
+ const configPath = getOpencodeConfigPath();
664
+ const pluginEntry = pin ? `${PLUGIN_NAME}@${readPackageVersion()}` : PLUGIN_NAME;
665
+ const interactive = !nonInteractive && process.stdin.isTTY === true;
666
+ const existing = readExistingConfig(configPath);
667
+ const hasPlugin = existing ? (Array.isArray(existing.plugin) ? existing.plugin : []).some(
668
+ (p) => {
669
+ const name = typeof p === "string" ? p : Array.isArray(p) ? p[0] : null;
670
+ return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
1088
671
  }
1089
- };
1090
- return { url: server.url, client, shutdown };
1091
- }
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.`
672
+ ) : false;
673
+ const existingProvider = detectModelProvider(existing);
674
+ const existingMcps = detectEnabledMcps(existing);
675
+ const existingPluginToggles = detectEnabledPluginToggles(existing);
676
+ const existingOpts = extractPluginOptions(existing);
677
+ let hasModels = !!(existingOpts?.models ?? existing?.harness?.models);
678
+ console.log(`
679
+ ${c.bold}${c.blue}@glrs-dev/harness-plugin-opencode${c.reset} setup
680
+ `);
681
+ if (hasPlugin) {
682
+ ok("Plugin already registered");
683
+ }
684
+ if (existingProvider) {
685
+ ok(`Models: ${existingProvider}`);
686
+ }
687
+ if (existingMcps.size > 0) {
688
+ ok(`MCPs: ${[...existingMcps].join(", ")} enabled`);
689
+ }
690
+ let reconfigureModels = false;
691
+ let reconfigureMcps = false;
692
+ let reconfigurePluginToggles = false;
693
+ let newModelsValue = null;
694
+ let newMcpEnabledSet = /* @__PURE__ */ new Set();
695
+ let newPluginToggleEnabledSet = new Set(existingPluginToggles);
696
+ if (hasPlugin && (existingProvider || hasModels)) {
697
+ const unconfiguredMcps = MCP_TOGGLES.filter(
698
+ (t) => !existingMcps.has(t.name) && !existing?.mcp?.[t.name]
1100
699
  );
1101
- }
1102
- }
1103
- async function createSession(client, opts) {
1104
- const session = await client.session.create({
1105
- body: {
1106
- directory: opts.cwd,
1107
- ...opts.agentName ? { agentID: opts.agentName } : {}
1108
- }
1109
- });
1110
- return session.id;
1111
- }
1112
- async function sendAndWait(client, opts) {
1113
- const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
1114
- await client.session.chat({
1115
- sessionID: opts.sessionId,
1116
- body: { content: [{ type: "text", text: opts.message }] }
1117
- });
1118
- return waitForIdle(client, {
1119
- sessionId: opts.sessionId,
1120
- stallMs,
1121
- abortSignal: opts.abortSignal
1122
- });
1123
- }
1124
- async function waitForIdle(client, opts) {
1125
- const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
1126
- return new Promise((resolve2) => {
1127
- let stallTimer = null;
1128
- let unsubscribe = null;
1129
- let settled = false;
1130
- const settle = (result) => {
1131
- if (settled) return;
1132
- settled = true;
1133
- if (stallTimer) clearTimeout(stallTimer);
1134
- if (unsubscribe) unsubscribe();
1135
- resolve2(result);
1136
- };
1137
- const resetStall = () => {
1138
- if (stallTimer) clearTimeout(stallTimer);
1139
- stallTimer = setTimeout(() => settle({ kind: "stall", stallMs }), stallMs);
1140
- };
1141
- if (opts.abortSignal) {
1142
- if (opts.abortSignal.aborted) {
1143
- settle({ kind: "abort" });
1144
- return;
700
+ if (interactive) {
701
+ const reconfigure = await promptChoice(
702
+ " Reconfigure models?",
703
+ ["No, keep current config", "Yes, reconfigure models"],
704
+ 0
705
+ );
706
+ if (reconfigure === 1) {
707
+ reconfigureModels = true;
708
+ hasModels = false;
1145
709
  }
1146
- opts.abortSignal.addEventListener("abort", () => settle({ kind: "abort" }), { once: true });
1147
- }
1148
- resetStall();
1149
- const stream = client.event.subscribe();
1150
- let streamDone = false;
1151
- (async () => {
1152
- try {
1153
- for await (const event of stream) {
1154
- if (settled) break;
1155
- const props = event.properties ?? {};
1156
- const eventSessionId = props["sessionID"];
1157
- if (eventSessionId !== opts.sessionId) continue;
1158
- resetStall();
1159
- const type = event.type ?? "";
1160
- if (type === "session.idle") {
1161
- settle({ kind: "idle" });
1162
- break;
1163
- }
1164
- if (type === "session.error") {
1165
- const msg = props["message"] ?? "session error";
1166
- settle({ kind: "error", message: msg });
1167
- break;
1168
- }
1169
- }
1170
- } catch (err) {
1171
- if (!settled) {
1172
- settle({ kind: "error", message: err instanceof Error ? err.message : String(err) });
710
+ if (existingMcps.size > 0) {
711
+ const reconfigureMcpChoice = await promptChoice(
712
+ " Reconfigure MCPs?",
713
+ ["No, keep current config", "Yes, reconfigure MCPs"],
714
+ 0
715
+ );
716
+ if (reconfigureMcpChoice === 1) {
717
+ reconfigureMcps = true;
1173
718
  }
1174
- } finally {
1175
- streamDone = true;
1176
719
  }
1177
- })();
1178
- unsubscribe = () => {
1179
- };
1180
- });
1181
- }
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
- });
720
+ const reconfigurePluginToggleChoice = await promptChoice(
721
+ " Reconfigure plugin add-ons?",
722
+ ["No, keep current config", "Yes, reconfigure plugin add-ons"],
723
+ 0
724
+ );
725
+ if (reconfigurePluginToggleChoice === 1) {
726
+ reconfigurePluginToggles = true;
727
+ }
728
+ if (!reconfigureModels && !reconfigureMcps && !reconfigurePluginToggles && unconfiguredMcps.length === 0) {
729
+ console.log(`
730
+ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
731
+ `);
732
+ return;
733
+ }
734
+ } else if (unconfiguredMcps.length === 0) {
735
+ console.log(`
736
+ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
737
+ `);
738
+ return;
739
+ }
1204
740
  }
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}` };
741
+ const pluginOpts = {};
742
+ if (interactive && !hasModels) {
743
+ console.log();
744
+ console.log(`${c.dim}Models${c.reset}`);
745
+ info("Fetching available providers\u2026");
746
+ const modelsDevProviders = await fetchModelsDevProviders();
747
+ let preset = null;
748
+ if (modelsDevProviders && modelsDevProviders.length > 0) {
749
+ const providerChoices = modelsDevProviders.map((p) => p.name);
750
+ providerChoices.push("Keep defaults (no model config)");
751
+ providerChoices.push("Custom (enter model IDs manually)");
752
+ const keepDefaultsIdx = providerChoices.length - 2;
753
+ const providerIdx = await promptChoice(
754
+ " Which model provider?",
755
+ providerChoices,
756
+ keepDefaultsIdx
757
+ );
758
+ if (providerIdx < modelsDevProviders.length) {
759
+ const provider = modelsDevProviders[providerIdx];
760
+ ok(`Provider: ${provider.name}`);
761
+ const suggested = provider.id === "amazon-bedrock" ? pickBedrockTierIds(provider) : suggestTiersFromModelsDev(provider);
762
+ const modelChoices = Object.keys(provider.models).map(
763
+ (modelId) => `${provider.id}/${modelId}`
764
+ );
765
+ const tiers = [
766
+ { tier: "deep", suggested: suggested.deep },
767
+ { tier: "mid", suggested: suggested.mid },
768
+ { tier: "fast", suggested: suggested.fast }
769
+ ];
770
+ const picked = {};
771
+ for (const { tier, suggested: suggestedModel } of tiers) {
772
+ const defaultIdx = modelChoices.indexOf(suggestedModel);
773
+ const idx = await promptChoice(
774
+ ` ${tier} model?`,
775
+ modelChoices,
776
+ defaultIdx >= 0 ? defaultIdx : 0
777
+ );
778
+ picked[tier] = modelChoices[idx];
779
+ info(` ${tier} \u2192 ${picked[tier]}`);
780
+ }
781
+ preset = {
782
+ label: provider.name,
783
+ providerId: provider.id,
784
+ deep: picked["deep"],
785
+ mid: picked["mid"],
786
+ fast: picked["fast"]
787
+ };
788
+ } else if (providerIdx === modelsDevProviders.length) {
789
+ ok("Models: OpenCode defaults");
790
+ pluginOpts._skipModels = true;
791
+ }
792
+ } else {
793
+ warn("Could not reach Models.dev API \u2014 using built-in presets");
794
+ const presetLabels = [...MODEL_PRESETS.map((p) => p.label), "Keep defaults (no model config)", "Custom (enter model IDs manually)"];
795
+ const keepDefaultsOfflineIdx = presetLabels.length - 2;
796
+ const choice = await promptChoice(
797
+ " Which model provider?",
798
+ presetLabels,
799
+ keepDefaultsOfflineIdx
800
+ );
801
+ if (choice < MODEL_PRESETS.length) {
802
+ preset = MODEL_PRESETS[choice];
803
+ ok(`Provider: ${preset.label}`);
804
+ } else if (choice === MODEL_PRESETS.length) {
805
+ ok("Models: OpenCode defaults");
806
+ pluginOpts._skipModels = true;
807
+ }
1246
808
  }
1247
- if (!fs6.existsSync(planPath)) {
1248
- return { ok: false, reason: `Planner did not produce plan.json at ${planPath}` };
809
+ if (preset) {
810
+ pluginOpts.models = {
811
+ deep: [preset.deep],
812
+ mid: [preset.mid],
813
+ fast: [preset.fast]
814
+ };
815
+ newModelsValue = {
816
+ deep: [preset.deep],
817
+ mid: [preset.mid],
818
+ fast: [preset.fast]
819
+ };
820
+ ok(`Models configured`);
821
+ const midExecIdx = await promptChoice(
822
+ " Use a strict executor for build agents? (recommended for Kimi/Qwen/DeepSeek)",
823
+ ["No (use mid model as reasoning builder)", "Yes (configure mid-execute model)"],
824
+ 0
825
+ );
826
+ if (midExecIdx === 1) {
827
+ const { input } = await import("@inquirer/prompts");
828
+ const midExecModel = await input({
829
+ message: " mid-execute model ID:",
830
+ default: preset.mid
831
+ });
832
+ if (midExecModel) {
833
+ pluginOpts.models["mid-execute"] = [midExecModel];
834
+ newModelsValue["mid-execute"] = [midExecModel];
835
+ info(` mid-execute \u2192 ${midExecModel} (strict executor prompts)`);
836
+ }
837
+ } else {
838
+ info(` mid-execute: skipped (build agents use mid model with reasoning prompts)`);
839
+ }
840
+ } else if (!pluginOpts._skipModels) {
841
+ info("Enter model IDs in <provider>/<model-id> format (e.g. amazon-bedrock/global.anthropic.claude-opus-4-7)");
842
+ const { input } = await import("@inquirer/prompts");
843
+ const deepModel = await input({ message: " deep (most capable):" });
844
+ const midModel = await input({ message: " mid (balanced):" });
845
+ const fastModel = await input({ message: " fast (cheapest):" });
846
+ if (deepModel) {
847
+ const resolvedMid = midModel || deepModel;
848
+ pluginOpts.models = {
849
+ deep: [deepModel],
850
+ mid: [resolvedMid],
851
+ fast: [fastModel || midModel || deepModel]
852
+ };
853
+ newModelsValue = {
854
+ deep: [deepModel],
855
+ mid: [resolvedMid],
856
+ fast: [fastModel || midModel || deepModel]
857
+ };
858
+ ok("Models: custom");
859
+ const midExecModel = await input({ message: " mid-execute (optional strict executor, press Enter to skip):" });
860
+ if (midExecModel) {
861
+ pluginOpts.models["mid-execute"] = [midExecModel];
862
+ newModelsValue["mid-execute"] = [midExecModel];
863
+ info(` mid-execute \u2192 ${midExecModel} (strict executor prompts)`);
864
+ } else {
865
+ info(` mid-execute: skipped (build agents use mid model with reasoning prompts)`);
866
+ }
867
+ } else {
868
+ ok("Models: OpenCode defaults");
869
+ }
1249
870
  }
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` };
871
+ delete pluginOpts._skipModels;
872
+ console.log();
873
+ }
874
+ if (interactive && reconfigureMcps) {
875
+ console.log(`${c.dim}Reconfigure MCP servers${c.reset}`);
876
+ const currentEnabled = new Set(existingMcps);
877
+ const selected = await promptMulti(
878
+ " Select MCPs to enable:",
879
+ MCP_TOGGLES.map((t) => ({ label: t.label, defaultOn: currentEnabled.has(t.name) }))
880
+ );
881
+ newMcpEnabledSet = new Set([...selected].map((i) => MCP_TOGGLES[i].name));
882
+ const names = [...newMcpEnabledSet].join(", ");
883
+ if (newMcpEnabledSet.size > 0) {
884
+ ok(`MCPs to enable: ${names}`);
885
+ } else {
886
+ ok("MCPs: all disabled");
1256
887
  }
1257
- if (!artifact) {
1258
- return { ok: false, reason: `plan.json at ${planPath} has invalid schema` };
888
+ console.log();
889
+ }
890
+ if (interactive && reconfigurePluginToggles) {
891
+ console.log(`${c.dim}Plugin add-ons${c.reset}`);
892
+ const currentEnabled = new Set(existingPluginToggles);
893
+ const selected = await promptMulti(
894
+ " Enable plugin add-ons?",
895
+ PLUGIN_TOGGLES.map((t) => ({ label: t.label, defaultOn: currentEnabled.has(t.name) }))
896
+ );
897
+ newPluginToggleEnabledSet = new Set([...selected].map((i) => PLUGIN_TOGGLES[i].name));
898
+ const names = [...newPluginToggleEnabledSet].join(", ");
899
+ if (newPluginToggleEnabledSet.size > 0) {
900
+ ok(`Plugin add-ons enabled: ${names}`);
901
+ } else {
902
+ ok("Plugin add-ons: none");
1259
903
  }
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();
904
+ console.log();
1271
905
  }
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"]
906
+ const pluginValue = Object.keys(pluginOpts).length > 0 ? [pluginEntry, pluginOpts] : pluginEntry;
907
+ const config = {
908
+ $schema: "https://opencode.ai/config.json",
909
+ plugin: [pluginValue]
910
+ };
911
+ if (interactive) {
912
+ const unconfigured = MCP_TOGGLES.filter(
913
+ (t) => !existingMcps.has(t.name) && !existing?.mcp?.[t.name]
914
+ );
915
+ if (unconfigured.length > 0) {
916
+ console.log(`${c.dim}Optional MCP servers (serena, memory, git are always on)${c.reset}`);
917
+ const selected = await promptMulti(
918
+ " Enable additional MCPs?",
919
+ unconfigured.map((t) => ({ label: t.label, defaultOn: t.defaultOn }))
920
+ );
921
+ if (selected.size > 0) {
922
+ const mcp = {};
923
+ for (const idx of selected) {
924
+ const toggle = unconfigured[idx];
925
+ mcp[toggle.name] = { enabled: true };
926
+ }
927
+ config.mcp = mcp;
928
+ const names = [...selected].map((i) => unconfigured[i].name).join(", ");
929
+ ok(`MCPs enabled: ${names}`);
930
+ } else {
931
+ ok("MCPs: defaults only");
932
+ }
933
+ console.log();
1307
934
  }
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 };
935
+ const unconfiguredPluginToggles = PLUGIN_TOGGLES.filter(
936
+ (t) => !existingPluginToggles.has(t.name)
937
+ );
938
+ if (unconfiguredPluginToggles.length > 0) {
939
+ console.log(`${c.dim}Plugin add-ons${c.reset}`);
940
+ const selected = await promptMulti(
941
+ " Enable plugin add-ons?",
942
+ unconfiguredPluginToggles.map((t) => ({ label: t.label, defaultOn: t.defaultOn }))
943
+ );
944
+ if (selected.size > 0) {
945
+ for (const idx of selected) {
946
+ const toggle = unconfiguredPluginToggles[idx];
947
+ config.plugin.push(toggle.name);
948
+ newPluginToggleEnabledSet.add(toggle.name);
949
+ }
950
+ const names = [...selected].map((i) => unconfiguredPluginToggles[i].name).join(", ");
951
+ ok(`Plugin add-ons enabled: ${names}`);
952
+ } else {
953
+ ok("Plugin add-ons: none");
1365
954
  }
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
- });
955
+ console.log();
1373
956
  }
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
957
  }
958
+ if (reconfigureModels && newModelsValue) {
959
+ writePluginOption(configPath, "models", newModelsValue, { dryRun });
960
+ }
961
+ if (reconfigureMcps) {
962
+ writeMcpToggles(configPath, newMcpEnabledSet, { dryRun });
963
+ }
964
+ if (reconfigurePluginToggles) {
965
+ writePluginToggles(configPath, newPluginToggleEnabledSet, { dryRun });
966
+ }
967
+ if (!fs3.existsSync(configPath)) {
968
+ if (dryRun) {
969
+ info(`[dry-run] Would create ${configPath}`);
970
+ info(`[dry-run] Config: ${JSON.stringify(config, null, 2)}`);
971
+ } else {
972
+ seedConfig(config, configPath);
973
+ ok(`Created ${configPath}`);
974
+ }
975
+ } else {
976
+ try {
977
+ const result = mergeConfig(config, configPath, dryRun);
978
+ if (!result.changed) {
979
+ ok("opencode.json is up to date");
980
+ for (const w of result.warnings) warn(w);
981
+ } else {
982
+ if (dryRun) {
983
+ info(`[dry-run] Would merge into ${configPath}:`);
984
+ for (const a of result.additions) info(` ${a}`);
985
+ } else {
986
+ ok(`Updated ${configPath}`);
987
+ info(`Backup: ${result.bakPath}`);
988
+ for (const a of result.additions) info(` ${a}`);
989
+ }
990
+ for (const w of result.warnings) warn(w);
991
+ }
992
+ } catch (e) {
993
+ console.error(`\x1B[31m\u2717\x1B[0m ${e.message}`);
994
+ process.exit(1);
995
+ }
996
+ }
997
+ if (!dryRun) {
998
+ migrateHarnessKeyToPluginOptions(configPath);
999
+ }
1000
+ if (!dryRun) {
1001
+ await refreshPluginCacheIfStale();
1002
+ }
1003
+ if (newPluginToggleEnabledSet.has("opencode-snip")) {
1004
+ warn("opencode-snip requires the Go snip binary. Install: brew install vhardouin/opencode-snip/snip");
1005
+ }
1006
+ console.log(`
1007
+ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
1008
+ `);
1384
1009
  }
1385
- async function runOneTask(opts) {
1386
- const { workflowId, task, taskNum, scope, cwd, server, db } = opts;
1387
- let headBefore;
1010
+
1011
+ // src/cli/uninstall.ts
1012
+ import * as fs4 from "fs";
1013
+ import * as path4 from "path";
1014
+ import * as os3 from "os";
1015
+ var PLUGIN_NAME2 = "@glrs-dev/harness-plugin-opencode";
1016
+ function getOpencodeConfigPath2() {
1017
+ const configHome = process.env["XDG_CONFIG_HOME"] ?? path4.join(os3.homedir(), ".config");
1018
+ return path4.join(configHome, "opencode", "opencode.json");
1019
+ }
1020
+ function uninstall(opts = {}) {
1021
+ const { dryRun = false } = opts;
1022
+ const configPath = getOpencodeConfigPath2();
1023
+ const c3 = {
1024
+ reset: "\x1B[0m",
1025
+ green: "\x1B[32m",
1026
+ yellow: "\x1B[33m",
1027
+ blue: "\x1B[34m"
1028
+ };
1029
+ const ok2 = (msg) => console.log(`${c3.green}\u2713${c3.reset} ${msg}`);
1030
+ const info2 = (msg) => console.log(`${c3.blue}\u2022${c3.reset} ${msg}`);
1031
+ const warn2 = (msg) => console.log(`${c3.yellow}!${c3.reset} ${msg}`);
1032
+ console.log(`
1033
+ ${c3.blue}Uninstalling ${PLUGIN_NAME2}${c3.reset}
1034
+ `);
1035
+ if (!fs4.existsSync(configPath)) {
1036
+ warn2(`No opencode.json found at ${configPath} \u2014 nothing to do`);
1037
+ return;
1038
+ }
1039
+ let raw;
1388
1040
  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" };
1041
+ raw = fs4.readFileSync(configPath, "utf8");
1042
+ } catch (e) {
1043
+ console.error(`\x1B[31m\u2717\x1B[0m Failed to read ${configPath}: ${e.message}`);
1044
+ process.exit(1);
1393
1045
  }
1394
- let sessionId;
1046
+ let config;
1395
1047
  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
- };
1048
+ config = JSON.parse(raw);
1049
+ } catch (e) {
1050
+ console.error(`\x1B[31m\u2717\x1B[0m Invalid JSON in ${configPath}: ${e.message}`);
1051
+ process.exit(1);
1406
1052
  }
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
1053
+ const plugins = Array.isArray(config.plugin) ? config.plugin : [];
1054
+ const filtered = plugins.filter((p) => {
1055
+ const name = typeof p === "string" ? p : Array.isArray(p) ? p[0] : null;
1056
+ return name !== PLUGIN_NAME2 && !String(name ?? "").startsWith(`${PLUGIN_NAME2}@`);
1413
1057
  });
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
- };
1058
+ if (filtered.length === plugins.length) {
1059
+ warn2(`"${PLUGIN_NAME2}" not found in plugin array \u2014 nothing to remove`);
1060
+ return;
1421
1061
  }
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
- };
1062
+ if (dryRun) {
1063
+ info2(`[dry-run] Would remove "${PLUGIN_NAME2}" from plugin array in ${configPath}`);
1064
+ return;
1430
1065
  }
1431
- let commitSha;
1066
+ const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
1432
1067
  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
- };
1068
+ fs4.copyFileSync(configPath, bakPath);
1069
+ } catch (e) {
1070
+ console.error(`\x1B[31m\u2717\x1B[0m Failed to write backup: ${e.message}`);
1071
+ process.exit(1);
1455
1072
  }
1456
- return { ok: true, taskId: task.id, commitSha };
1457
- }
1458
- async function runVerifyCommands(commands, cwd) {
1459
- for (const cmd2 of commands) {
1073
+ config.plugin = filtered;
1074
+ const tmpPath = `${configPath}.uninstall.tmp.${Date.now()}-${process.pid}`;
1075
+ try {
1076
+ fs4.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
1077
+ fs4.renameSync(tmpPath, configPath);
1078
+ } catch (e) {
1460
1079
  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}` };
1080
+ fs4.unlinkSync(tmpPath);
1081
+ } catch {
1465
1082
  }
1083
+ console.error(`\x1B[31m\u2717\x1B[0m Failed to write config: ${e.message}`);
1084
+ process.exit(1);
1466
1085
  }
1467
- return { ok: true };
1086
+ ok2(`Removed "${PLUGIN_NAME2}" from ${configPath}`);
1087
+ info2(`Backup: ${bakPath}`);
1088
+ console.log(`
1089
+ To fully remove the package: bun remove @glrs-dev/harness-plugin-opencode
1090
+ `);
1468
1091
  }
1469
- async function cleanWorkingTree(cwd) {
1092
+
1093
+ // src/cli/doctor.ts
1094
+ import * as fs5 from "fs";
1095
+ import * as path5 from "path";
1096
+ import * as os4 from "os";
1097
+ import { execSync } from "child_process";
1098
+ var PLUGIN_NAME3 = "@glrs-dev/harness-plugin-opencode";
1099
+ function getOpencodeConfigPath3() {
1100
+ const configHome = process.env["XDG_CONFIG_HOME"] ?? path5.join(os4.homedir(), ".config");
1101
+ return path5.join(configHome, "opencode", "opencode.json");
1102
+ }
1103
+ function cmd(command4) {
1470
1104
  try {
1471
- await execFileP4("git", ["reset", "--hard", "HEAD"], { cwd });
1472
- await execFileP4("git", ["clean", "-fd"], { cwd });
1105
+ return execSync(command4, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1473
1106
  } catch {
1107
+ return null;
1108
+ }
1109
+ }
1110
+ function which(bin) {
1111
+ return cmd(`which ${bin}`) !== null;
1112
+ }
1113
+ function doctor() {
1114
+ const c3 = {
1115
+ reset: "\x1B[0m",
1116
+ green: "\x1B[32m",
1117
+ yellow: "\x1B[33m",
1118
+ red: "\x1B[31m",
1119
+ bold: "\x1B[1m"
1120
+ };
1121
+ const ok2 = (msg) => console.log(`${c3.green}\u2713${c3.reset} ${msg}`);
1122
+ const warn2 = (msg) => console.log(`${c3.yellow}!${c3.reset} ${msg}`);
1123
+ const fail = (msg) => console.log(`${c3.red}\u2717${c3.reset} ${msg}`);
1124
+ console.log(`
1125
+ ${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
1126
+ `);
1127
+ const ocVersion = cmd("opencode --version 2>/dev/null | head -1");
1128
+ if (ocVersion) {
1129
+ ok2(`opencode ${ocVersion}`);
1130
+ } else {
1131
+ fail("opencode CLI not found \u2014 install from https://opencode.ai");
1132
+ }
1133
+ const configPath = getOpencodeConfigPath3();
1134
+ if (fs5.existsSync(configPath)) {
1135
+ try {
1136
+ const config = JSON.parse(fs5.readFileSync(configPath, "utf8"));
1137
+ const plugins = Array.isArray(config.plugin) ? config.plugin : [];
1138
+ let pluginOptions = null;
1139
+ const hasPlugin = plugins.some((p) => {
1140
+ if (typeof p === "string") {
1141
+ return p === PLUGIN_NAME3 || p.startsWith(`${PLUGIN_NAME3}@`);
1142
+ }
1143
+ if (Array.isArray(p)) {
1144
+ const [name, opts] = p;
1145
+ const match = name === PLUGIN_NAME3 || String(name ?? "").startsWith(`${PLUGIN_NAME3}@`);
1146
+ if (match && opts && typeof opts === "object") {
1147
+ pluginOptions = opts;
1148
+ }
1149
+ return match;
1150
+ }
1151
+ return false;
1152
+ });
1153
+ if (hasPlugin) {
1154
+ ok2(`"${PLUGIN_NAME3}" present in opencode.json plugin array`);
1155
+ } else {
1156
+ warn2(`"${PLUGIN_NAME3}" NOT in opencode.json plugin array \u2014 run: bunx ${PLUGIN_NAME3} install`);
1157
+ }
1158
+ const modelSources = [];
1159
+ if (pluginOptions && typeof pluginOptions.models === "object") {
1160
+ modelSources.push({
1161
+ label: "plugin options.models",
1162
+ block: pluginOptions.models
1163
+ });
1164
+ }
1165
+ const legacyHarness = config.harness;
1166
+ if (legacyHarness && typeof legacyHarness.models === "object") {
1167
+ modelSources.push({
1168
+ label: "harness.models (legacy)",
1169
+ block: legacyHarness.models
1170
+ });
1171
+ }
1172
+ if (modelSources.length > 0) {
1173
+ const invalid = [];
1174
+ for (const { label, block } of modelSources) {
1175
+ if (!block || typeof block !== "object") continue;
1176
+ for (const [key, rawValue] of Object.entries(block)) {
1177
+ const candidate = Array.isArray(rawValue) ? rawValue[0] : rawValue;
1178
+ if (typeof candidate !== "string") continue;
1179
+ const result = validateModelOverride(candidate);
1180
+ if (!result.valid) {
1181
+ invalid.push({
1182
+ keyPath: `${label}.${key}`,
1183
+ value: candidate,
1184
+ suggestion: result.suggestion,
1185
+ reason: result.reason
1186
+ });
1187
+ }
1188
+ }
1189
+ }
1190
+ if (invalid.length === 0) {
1191
+ ok2("model overrides look valid");
1192
+ } else {
1193
+ for (const entry of invalid) {
1194
+ fail(`invalid model override at ${entry.keyPath}: "${entry.value}"`);
1195
+ if (entry.reason) {
1196
+ console.log(` ${c3.yellow}reason:${c3.reset} ${entry.reason}`);
1197
+ }
1198
+ if (entry.suggestion) {
1199
+ console.log(
1200
+ ` ${c3.yellow}fix:${c3.reset} remove this key, or replace with \`${entry.suggestion}\``
1201
+ );
1202
+ } else {
1203
+ console.log(
1204
+ ` ${c3.yellow}fix:${c3.reset} remove this key, or run \`bunx ${PLUGIN_NAME3} install\` to pick a current preset`
1205
+ );
1206
+ }
1207
+ }
1208
+ }
1209
+ }
1210
+ } catch {
1211
+ fail(`opencode.json at ${configPath} has invalid JSON`);
1212
+ }
1213
+ } else {
1214
+ warn2(`No opencode.json at ${configPath} \u2014 run: bunx ${PLUGIN_NAME3} install`);
1215
+ }
1216
+ if (which("uvx")) {
1217
+ ok2("uvx (serena + git MCPs)");
1218
+ } else {
1219
+ warn2("uvx not found \u2014 serena and git MCPs won't work. Install: brew install uv");
1220
+ }
1221
+ if (which("node") && which("npx")) {
1222
+ ok2(`node ${cmd("node --version") ?? ""} + npx (memory MCP)`);
1223
+ } else {
1224
+ warn2("node/npx not found \u2014 memory MCP won't work");
1474
1225
  }
1226
+ if (which("bun")) {
1227
+ ok2(`bun ${cmd("bun --version") ?? ""}`);
1228
+ } else if (which("npm")) {
1229
+ ok2(`npm ${cmd("npm --version") ?? ""} (install bun for faster installs)`);
1230
+ } else {
1231
+ fail("Neither bun nor npm found \u2014 cannot install plugins");
1232
+ }
1233
+ console.log();
1475
1234
  }
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
-
1486
- ${task.prompt}
1487
1235
 
1488
- Verify commands (run these after making changes):
1489
- ${verifyText}
1236
+ // src/autopilot/cli.ts
1237
+ import { command, option, positional, string as stringType, optional, number as numberType, flag } from "cmd-ts";
1490
1238
 
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>`;
1497
- }
1498
-
1499
- // src/pilot/assess.ts
1500
- 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
- });
1239
+ // src/autopilot/debrief.ts
1240
+ function shouldRunDebrief(opts) {
1241
+ if (opts.noDebrief) return false;
1242
+ const envVal = opts.env["GLRS_AUTOPILOT_DEBRIEF"];
1243
+ if (envVal !== void 0 && envVal.toLowerCase() === "off") return false;
1244
+ return true;
1245
+ }
1246
+ async function defaultExecGitDiffStat(cwd) {
1247
+ const { execFile: execFileCb } = await import("child_process");
1248
+ const { promisify } = await import("util");
1249
+ const execFile2 = promisify(execFileCb);
1250
+ try {
1251
+ const { stdout } = await execFile2("git", ["diff", "--stat", "HEAD~1", "HEAD"], { cwd });
1252
+ return stdout.trim();
1253
+ } catch {
1254
+ try {
1255
+ const { stdout } = await execFile2("git", ["diff", "--stat"], { cwd });
1256
+ return stdout.trim() || "(no uncommitted changes)";
1257
+ } catch {
1258
+ return "(git diff unavailable)";
1535
1259
  }
1536
1260
  }
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
1261
  }
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
- });
1262
+ function buildContextMessage(loopResult, prompt, gitDiffStat) {
1263
+ const cost = loopResult.cumulativeCostUsd !== void 0 ? `$${loopResult.cumulativeCostUsd.toFixed(4)}` : "not available";
1264
+ const sessionId = loopResult.sessionId ?? "not available";
1265
+ return [
1266
+ "## Autopilot session context",
1267
+ "",
1268
+ `**Exit reason:** ${loopResult.exitReason}`,
1269
+ `**Iterations completed:** ${loopResult.iterations}`,
1270
+ `**Exit message:** ${loopResult.message}`,
1271
+ `**Cumulative cost:** ${cost}`,
1272
+ `**Session ID:** ${sessionId}`,
1273
+ "",
1274
+ "## Original prompt",
1275
+ "",
1276
+ prompt,
1277
+ "",
1278
+ "## Git diff stat (last commit vs HEAD~1)",
1279
+ "",
1280
+ gitDiffStat || "(no changes)",
1281
+ "",
1282
+ "---",
1283
+ "",
1284
+ "Please produce the five-section debrief as instructed in your system prompt."
1285
+ ].join("\n");
1286
+ }
1287
+ async function runDebrief(opts) {
1288
+ const _createSession = opts._deps?.createSession ?? createSession;
1289
+ const _sendAndWait = opts._deps?.sendAndWait ?? sendAndWait;
1290
+ const _getLastAssistantMessage = opts._deps?.getLastAssistantMessage ?? getLastAssistantMessage;
1291
+ const _execGitDiffStat = opts._deps?.execGitDiffStat ?? defaultExecGitDiffStat;
1557
1292
  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
1293
+ const gitDiffStat = await _execGitDiffStat(opts.cwd).catch(() => "(git diff unavailable)");
1294
+ const contextMessage = buildContextMessage(opts.loopResult, opts.prompt, gitDiffStat);
1295
+ const sessionId = await _createSession(opts.server.client, {
1296
+ cwd: opts.cwd,
1297
+ agentName: "debriefer"
1568
1298
  });
1569
- const assessorPrompt = buildAssessorPrompt({ workflowId, scope, plan, assessPath, cycle });
1570
- const result = await sendAndWait(server.client, {
1299
+ await _sendAndWait(opts.server.client, {
1571
1300
  sessionId,
1572
- message: assessorPrompt,
1573
- stallMs: 10 * 60 * 1e3
1574
- // 10 min
1301
+ message: contextMessage,
1302
+ stallMs: 5 * 60 * 1e3
1303
+ // 5 min stall timeout for debrief
1575
1304
  });
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
- });
1305
+ const debriefOutput = await _getLastAssistantMessage(opts.server.client, sessionId);
1306
+ if (debriefOutput) {
1307
+ process.stdout.write("\n\x1B[1m\u2500\u2500\u2500 Autopilot Debrief \u2500\u2500\u2500\x1B[0m\n\n");
1308
+ process.stdout.write(debriefOutput);
1309
+ process.stdout.write("\n\n");
1612
1310
  }
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 };
1632
- } 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
- };
1647
- }
1648
- } finally {
1649
- closeDb();
1311
+ } catch (err) {
1312
+ const msg = err instanceof Error ? err.message : String(err);
1313
+ process.stderr.write(`\x1B[33m\u26A0 Debrief failed (non-fatal): ${msg}\x1B[0m
1314
+ `);
1650
1315
  }
1651
1316
  }
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
1317
 
1663
- Workflow: ${workflowId}
1664
- Goal: ${scope.goal}
1665
- Assessment cycle: ${cycle}
1318
+ // src/autopilot/cli.ts
1319
+ var loopCmd = command({
1320
+ name: "loop",
1321
+ description: "Run the Ralph loop: send a prompt to PRIME repeatedly until it emits <autopilot-done> or a budget is exhausted.",
1322
+ args: {
1323
+ prompt: positional({
1324
+ type: stringType,
1325
+ displayName: "prompt",
1326
+ description: "The prompt to send to PRIME each iteration (e.g. a Linear issue ref or free-form task)."
1327
+ }),
1328
+ maxIterations: option({
1329
+ long: "max-iterations",
1330
+ type: optional(numberType),
1331
+ description: `Maximum number of loop iterations (default: ${MAX_ITERATIONS}).`
1332
+ }),
1333
+ timeout: option({
1334
+ long: "timeout",
1335
+ type: optional(numberType),
1336
+ description: `Total wall-clock timeout in milliseconds (default: ${TIMEOUT_MS} = 4 hours).`
1337
+ }),
1338
+ noDebrief: flag({
1339
+ long: "no-debrief",
1340
+ description: "Skip the post-run debrief session."
1341
+ })
1342
+ },
1343
+ handler: async ({ prompt, maxIterations, timeout, noDebrief }) => {
1344
+ const cwd = process.cwd();
1345
+ process.stdout.write("\n\x1B[1mAutopilot \u2014 Ralph loop\x1B[0m\n");
1346
+ process.stdout.write(`Prompt: ${prompt.slice(0, 80)}${prompt.length > 80 ? "..." : ""}
1347
+ `);
1348
+ process.stdout.write(`Max iterations: ${maxIterations ?? MAX_ITERATIONS}
1349
+ `);
1350
+ process.stdout.write(`Timeout: ${((timeout ?? TIMEOUT_MS) / 36e5).toFixed(1)}h
1666
1351
 
1667
- Acceptance criteria to evaluate:
1668
- ${acsText}
1352
+ `);
1353
+ const result = await runRalphLoop({
1354
+ prompt,
1355
+ cwd,
1356
+ maxIterations: maxIterations ?? void 0,
1357
+ timeoutMs: timeout ?? void 0
1358
+ });
1359
+ const icon = result.exitReason === "sentinel" ? "\x1B[32m\u2713\x1B[0m" : result.exitReason === "kill-switch" ? "\x1B[33m\u2298\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
1360
+ process.stdout.write(`
1361
+ ${icon} ${result.message}
1362
+ `);
1363
+ process.stdout.write(` Iterations: ${result.iterations}
1669
1364
 
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?
1365
+ `);
1366
+ if (shouldRunDebrief({ noDebrief, env: process.env })) {
1367
+ const { startServer } = await import("./opencode-server-KPCDFYAX.js");
1368
+ let debriefServer;
1369
+ try {
1370
+ debriefServer = await startServer({ cwd });
1371
+ await runDebrief({
1372
+ server: debriefServer,
1373
+ loopResult: result,
1374
+ prompt,
1375
+ cwd
1376
+ });
1377
+ } catch {
1378
+ process.stderr.write("\x1B[33m\u26A0 Debrief server failed to start (non-fatal)\x1B[0m\n");
1379
+ } finally {
1380
+ await debriefServer?.shutdown().catch(() => {
1381
+ });
1382
+ }
1383
+ }
1384
+ if (result.exitReason !== "sentinel" && result.exitReason !== "kill-switch") {
1385
+ process.exit(1);
1386
+ }
1387
+ process.exit(0);
1388
+ }
1389
+ });
1675
1390
 
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.
1391
+ // src/autopilot/autopilot-cmd.ts
1392
+ import { command as command2, option as option2, optional as optional2, string as stringType2 } from "cmd-ts";
1681
1393
 
1682
- 3. Write your assessment to: ${assessPath}
1394
+ // src/autopilot/interactive.ts
1395
+ import * as fs7 from "fs";
1396
+ import * as path7 from "path";
1683
1397
 
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"
1398
+ // src/plan-paths.ts
1399
+ import { execFile } from "child_process";
1400
+ import * as fs6 from "fs/promises";
1401
+ import * as os5 from "os";
1402
+ import * as path6 from "path";
1403
+ function execFileP(file, args, opts = {}) {
1404
+ const { cwd, timeoutMs = 5e3 } = opts;
1405
+ return new Promise((resolve2, reject) => {
1406
+ const controller = new AbortController();
1407
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1408
+ execFile(
1409
+ file,
1410
+ args,
1411
+ { signal: controller.signal, cwd, encoding: "utf8" },
1412
+ (err, stdout) => {
1413
+ clearTimeout(timer);
1414
+ if (err) {
1415
+ reject(err);
1416
+ return;
1417
+ }
1418
+ resolve2(stdout ?? "");
1419
+ }
1420
+ );
1421
+ });
1695
1422
  }
1696
-
1697
- Verdict is "pass" only if ALL ACs are "met" AND no high-severity actionable risks exist.`;
1423
+ function expandTilde(p) {
1424
+ if (p === "~") return os5.homedir();
1425
+ if (p.startsWith("~/")) return path6.join(os5.homedir(), p.slice(2));
1426
+ return p;
1427
+ }
1428
+ async function getRepoFolder(worktreeDir) {
1429
+ let stdout;
1430
+ try {
1431
+ stdout = await execFileP(
1432
+ "git",
1433
+ ["rev-parse", "--git-common-dir"],
1434
+ { cwd: worktreeDir }
1435
+ );
1436
+ } catch (err) {
1437
+ const msg = err instanceof Error ? err.message : "unknown error running `git rev-parse --git-common-dir`";
1438
+ throw new Error(
1439
+ `getRepoFolder: failed to resolve git-common-dir for ${worktreeDir}: ${msg}`
1440
+ );
1441
+ }
1442
+ const gitCommonDir = stdout.trim();
1443
+ if (!gitCommonDir) {
1444
+ throw new Error(
1445
+ `getRepoFolder: \`git rev-parse --git-common-dir\` returned empty for ${worktreeDir}`
1446
+ );
1447
+ }
1448
+ const absCommonDir = path6.isAbsolute(gitCommonDir) ? gitCommonDir : path6.resolve(worktreeDir, gitCommonDir);
1449
+ const repoRoot = path6.dirname(absCommonDir);
1450
+ return path6.basename(repoRoot);
1451
+ }
1452
+ async function getPlanDir(worktreeDir) {
1453
+ const override = process.env.GLORIOUS_PLAN_DIR;
1454
+ const base = override ? expandTilde(override) : path6.join(os5.homedir(), ".glorious", "opencode");
1455
+ const repoFolder = await getRepoFolder(worktreeDir);
1456
+ const planDir = path6.join(base, repoFolder, "plans");
1457
+ await fs6.mkdir(planDir, { recursive: true });
1458
+ return planDir;
1698
1459
  }
1699
1460
 
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: {}
1461
+ // src/autopilot/interactive.ts
1462
+ function defaultBanner(message) {
1463
+ process.stdout.write(`
1464
+ ${message}
1465
+ `);
1466
+ }
1467
+ async function orchestrateAutopilot(opts, deps) {
1468
+ const banner = deps.onBanner ?? defaultBanner;
1469
+ const cwd = opts.cwd ?? process.cwd();
1470
+ banner("\u2192 Phase 1/3: Scoping (interactive)...");
1471
+ const scoperResult = await deps.runScoper({
1472
+ planDir: opts.planDir,
1473
+ slug: opts.slug,
1474
+ initialGoal: opts.initialGoal
1710
1475
  });
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
- });
1719
- }
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 }
1476
+ banner(`\u2713 Scope captured at ${scoperResult.scopePath}`);
1477
+ const actualSlug = path7.basename(path7.dirname(scoperResult.scopePath));
1478
+ banner("\u2192 Phase 2/3: Planning (headless)...");
1479
+ const planResult = await deps.runPlan({
1480
+ scopePath: scoperResult.scopePath,
1481
+ planDir: opts.planDir,
1482
+ slug: actualSlug || opts.slug
1727
1483
  });
1728
- logEvent(db, {
1729
- workflowId,
1730
- phase: "resolve",
1731
- kind: "workflow.completed",
1732
- payload: {
1733
- duration: `${Math.round(durationMs / 1e3)}s`
1734
- }
1484
+ banner(`\u2713 Plan written at ${planResult.planPath}`);
1485
+ banner("\u2192 Phase 3/3: Executing (headless loop)...");
1486
+ const loopResult = await deps.runLoop({
1487
+ planPath: planResult.planPath,
1488
+ cwd
1735
1489
  });
1736
- closeDb();
1737
1490
  return {
1738
- workflowId,
1739
- goal: scope.goal,
1740
- durationMs,
1741
- acknowledgedRisks
1491
+ scopePath: scoperResult.scopePath,
1492
+ planPath: planResult.planPath,
1493
+ loopResult
1742
1494
  };
1743
1495
  }
1496
+ function deriveSlug(goal) {
1497
+ const slug = goal.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
1498
+ return slug.length > 0 ? slug : `feature-${Date.now()}`;
1499
+ }
1500
+ async function browsePlansDir(planDir, _readdirSync) {
1501
+ const { select: select2 } = await import("@inquirer/prompts");
1502
+ const readdir2 = _readdirSync ?? ((p, o) => fs7.readdirSync(p, o));
1503
+ let currentDir = planDir;
1504
+ while (true) {
1505
+ const entries = readdir2(currentDir, { withFileTypes: true });
1506
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
1507
+ const files = entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name).sort();
1508
+ if (dirs.length === 0 && files.length === 0) {
1509
+ process.stderr.write(`
1510
+ No plans found in ${currentDir}
1744
1511
 
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)) {
1757
- 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
- );
1512
+ `);
1513
+ return null;
1514
+ }
1515
+ const choices = [];
1516
+ for (const d of dirs) {
1517
+ const dirPath = path7.join(currentDir, d);
1518
+ const hasMain = fs7.existsSync(path7.join(dirPath, "main.md"));
1519
+ const fileCount = readdir2(dirPath, { withFileTypes: true }).filter((e) => e.isFile()).length;
1520
+ choices.push({
1521
+ name: hasMain ? `${d}/ (multi-file plan \u2014 ${fileCount} files)` : `${d}/ (${fileCount} files)`,
1522
+ value: `dir:${dirPath}`
1523
+ });
1524
+ }
1525
+ for (const f of files) {
1526
+ choices.push({
1527
+ name: `${f}`,
1528
+ value: `file:${path7.join(currentDir, f)}`
1529
+ });
1530
+ }
1531
+ if (currentDir !== planDir) {
1532
+ choices.push({ name: "\u21A9 Back", value: "back" });
1533
+ }
1534
+ choices.push({ name: "\u2715 Cancel (scope a new feature instead)", value: "cancel" });
1535
+ const answer = await select2({
1536
+ message: "Select a plan:",
1537
+ choices
1538
+ });
1539
+ if (answer === "cancel") return null;
1540
+ if (answer === "back") {
1541
+ currentDir = path7.dirname(currentDir);
1542
+ continue;
1543
+ }
1544
+ if (answer.startsWith("file:")) {
1545
+ return answer.slice("file:".length);
1546
+ }
1547
+ if (answer.startsWith("dir:")) {
1548
+ const dirPath = answer.slice("dir:".length);
1549
+ const hasMain = fs7.existsSync(path7.join(dirPath, "main.md"));
1550
+ if (hasMain) {
1551
+ const dirAction = await select2({
1552
+ message: `${path7.basename(dirPath)}/ has a main.md. What do you want?`,
1553
+ choices: [
1554
+ { name: "Select this as a multi-file plan", value: "select" },
1555
+ { name: "Browse files inside", value: "browse" },
1556
+ { name: "\u21A9 Back", value: "back" }
1557
+ ]
1558
+ });
1559
+ if (dirAction === "select") return dirPath;
1560
+ if (dirAction === "browse") {
1561
+ currentDir = dirPath;
1562
+ continue;
1563
+ }
1564
+ continue;
1763
1565
  }
1764
- } catch {
1566
+ currentDir = dirPath;
1567
+ continue;
1765
1568
  }
1766
1569
  }
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
- };
1570
+ }
1571
+ async function runInteractiveAutopilot(cwd, _deps) {
1572
+ const _getPlanDir = _deps?.getPlanDir ?? getPlanDir;
1573
+ const planDir = await _getPlanDir(cwd);
1574
+ let hasExistingPlan;
1575
+ if (_deps?.promptExistingPlan) {
1576
+ hasExistingPlan = await _deps.promptExistingPlan();
1577
+ } else {
1578
+ const { confirm: confirm2 } = await import("@inquirer/prompts");
1579
+ hasExistingPlan = await confirm2({
1580
+ message: "Do you have an existing plan?",
1581
+ default: false
1582
+ });
1773
1583
  }
1774
- let scope;
1775
- 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` };
1584
+ if (hasExistingPlan) {
1585
+ const repoLocalPlansDir = path7.join(cwd, "plans");
1586
+ const hasRepoLocal = fs7.existsSync(repoLocalPlansDir) && fs7.statSync(repoLocalPlansDir).isDirectory();
1587
+ const hasShared = fs7.existsSync(planDir) && fs7.statSync(planDir).isDirectory();
1588
+ let browseRoot;
1589
+ if (hasRepoLocal && hasShared) {
1590
+ const { select: select2 } = await import("@inquirer/prompts");
1591
+ const which2 = await select2({
1592
+ message: "Where are your plans?",
1593
+ choices: [
1594
+ { name: `./plans/ (repo-local)`, value: repoLocalPlansDir },
1595
+ { name: `${planDir} (harness-shared)`, value: planDir }
1596
+ ]
1597
+ });
1598
+ browseRoot = which2;
1599
+ } else if (hasRepoLocal) {
1600
+ browseRoot = repoLocalPlansDir;
1601
+ } else {
1602
+ browseRoot = planDir;
1780
1603
  }
1781
- scope = parsed;
1782
- } 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
- };
1805
- }
1806
- 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 };
1604
+ let selectedPlan;
1605
+ if (_deps?.browsePlans) {
1606
+ selectedPlan = await _deps.browsePlans(browseRoot);
1607
+ } else {
1608
+ selectedPlan = await browsePlansDir(browseRoot, _deps?.readdirSync);
1810
1609
  }
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 };
1823
- }
1824
- const assessResult = await runAssessPhase({
1825
- workflowId,
1826
- scope,
1827
- plan: currentPlan,
1828
- cwd,
1829
- cycle,
1830
- server
1831
- });
1832
- if (!assessResult.ok) {
1833
- return { ok: false, reason: `Assess phase failed: ${assessResult.reason}`, workflowId };
1834
- }
1835
- if (assessResult.verdict === "pass") {
1836
- const resolveResult = await runResolvePhase({
1837
- workflowId,
1838
- scope,
1839
- assessment: assessResult.artifact,
1840
- cwd,
1841
- startedAt
1610
+ if (!selectedPlan) {
1611
+ process.stderr.write("\n No plan selected. Starting new feature scoping.\n\n");
1612
+ } else {
1613
+ const isDir = fs7.statSync(selectedPlan).isDirectory();
1614
+ const planPath = isDir ? selectedPlan : selectedPlan;
1615
+ const { parsePlanState } = await import("./plan-parser-TMHEKT22.js");
1616
+ const planState = parsePlanState(planPath);
1617
+ if (planState.totalItems > 0 && planState.checkedItems === planState.totalItems) {
1618
+ const { select: selectAction } = await import("@inquirer/prompts");
1619
+ const action = await selectAction({
1620
+ message: `All ${planState.totalItems} items in this plan are already checked. What do you want to do?`,
1621
+ choices: [
1622
+ { name: "Uncheck all items and run from scratch", value: "uncheck" },
1623
+ { name: "Run anyway (agent will verify/audit the checked items)", value: "run" },
1624
+ { name: "Cancel and pick a different plan", value: "cancel" }
1625
+ ]
1842
1626
  });
1843
- return {
1844
- ok: true,
1845
- workflowId,
1846
- goal: scope.goal,
1847
- durationMs: resolveResult.durationMs,
1848
- acknowledgedRisks: resolveResult.acknowledgedRisks
1849
- };
1850
- }
1851
- if (cycle < maxCycles) {
1852
- const elapsedSec = Math.round((Date.now() - startedAt) / 1e3);
1627
+ if (action === "cancel") {
1628
+ process.stderr.write("\n Cancelled. Starting new feature scoping.\n\n");
1629
+ } else {
1630
+ if (action === "uncheck") {
1631
+ const uncheckFiles = isDir ? fs7.readdirSync(planPath).filter((f) => f.endsWith(".md")).map((f) => path7.join(planPath, f)) : [planPath];
1632
+ for (const file of uncheckFiles) {
1633
+ const content = fs7.readFileSync(file, "utf-8");
1634
+ const unchecked = content.replace(/- \[x\]/g, "- [ ]");
1635
+ fs7.writeFileSync(file, unchecked);
1636
+ }
1637
+ process.stderr.write(`
1638
+ \u2713 Unchecked all items in ${uncheckFiles.length} file(s).
1639
+
1640
+ `);
1641
+ }
1642
+ const banner = _deps?.onBanner ?? ((msg) => process.stdout.write(`
1643
+ ${msg}
1644
+ `));
1645
+ banner(`\u2192 Running loop against plan: ${planPath}`);
1646
+ const { runLoopSession: runLoopSession2 } = await import("./loop-session-J35NILUZ.js");
1647
+ const _runLoop = _deps?.runLoop ?? runLoopSession2;
1648
+ const loopResult = await _runLoop({ planPath, cwd });
1649
+ return {
1650
+ scopePath: "",
1651
+ planPath,
1652
+ loopResult
1653
+ };
1654
+ }
1655
+ } else {
1656
+ const unchecked = planState.totalItems - planState.checkedItems;
1853
1657
  process.stderr.write(
1854
1658
  `
1855
- [pilot] \u26A0\uFE0F Assess cycle ${cycle}/${maxCycles} failed. Re-planning...
1856
- Gap: ${assessResult.replanGuidance}
1857
- Elapsed: ${elapsedSec}s
1659
+ Plan: ${planState.totalItems} items, ${unchecked} remaining.
1858
1660
 
1859
1661
  `
1860
1662
  );
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;
1663
+ const banner = _deps?.onBanner ?? ((msg) => process.stdout.write(`
1664
+ ${msg}
1665
+ `));
1666
+ banner(`\u2192 Running loop against plan: ${planPath}`);
1667
+ const { runLoopSession: runLoopSession2 } = await import("./loop-session-J35NILUZ.js");
1668
+ const _runLoop = _deps?.runLoop ?? runLoopSession2;
1669
+ const loopResult = await _runLoop({ planPath, cwd });
1670
+ return {
1671
+ scopePath: "",
1672
+ planPath,
1673
+ loopResult
1674
+ };
1885
1675
  }
1886
1676
  }
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 }
1677
+ }
1678
+ let goal;
1679
+ let ticketRef;
1680
+ if (_deps?.promptGoal) {
1681
+ goal = await _deps.promptGoal();
1682
+ } else {
1683
+ const { input } = await import("@inquirer/prompts");
1684
+ goal = await input({
1685
+ message: "What do you want to build? (one sentence, free-form)",
1686
+ validate: (v) => v.trim().length > 0 ? true : "Please describe what you want to build."
1894
1687
  });
1895
- closeFailDb();
1896
- return {
1897
- ok: false,
1898
- reason: `Assess failed after ${maxCycles} cycles. Manual intervention required.`,
1899
- workflowId
1900
- };
1901
- } finally {
1902
- await server.shutdown();
1903
1688
  }
1904
- }
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;
1689
+ if (_deps?.promptTicketRef) {
1690
+ ticketRef = await _deps.promptTicketRef();
1691
+ } else {
1692
+ const { input } = await import("@inquirer/prompts");
1693
+ ticketRef = await input({
1694
+ message: "Optional ticket or issue ref (Linear ID, GitHub issue URL, etc.)",
1695
+ default: ""
1696
+ });
1913
1697
  }
1914
- }
1915
-
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`.",
1920
- 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`)"
1925
- })
1926
- },
1927
- handler: async ({ scope }) => {
1928
- 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}
1935
- `);
1936
- if (result.workflowId) {
1937
- process.stderr.write(` Workflow: ${result.workflowId}
1938
- `);
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
- }
1698
+ const slug = deriveSlug(goal);
1699
+ const seedDir = path7.join(planDir, slug);
1700
+ const seedPath = path7.join(seedDir, "scope-seed.md");
1701
+ const _mkdirSync = _deps?.mkdirSync ?? ((p, o) => fs7.mkdirSync(p, o));
1702
+ const _writeFileSync = _deps?.writeFileSync ?? fs7.writeFileSync;
1703
+ _mkdirSync(seedDir, { recursive: true });
1704
+ const seedContent = [
1705
+ `# Scope Seed: ${slug}`,
1706
+ "",
1707
+ `## Goal`,
1708
+ "",
1709
+ goal,
1710
+ "",
1711
+ ...ticketRef.trim() ? [`## Ticket / Issue Ref`, "", ticketRef.trim(), ""] : []
1712
+ ].join("\n");
1713
+ _writeFileSync(seedPath, seedContent);
1714
+ const { runScoperSession } = await import("./scoper-S77SOK7X.js");
1715
+ const { runPlanSession } = await import("./plan-session-7VS32P52.js");
1716
+ const { runLoopSession } = await import("./loop-session-J35NILUZ.js");
1717
+ return orchestrateAutopilot(
1718
+ { slug, planDir, cwd, initialGoal: goal },
1719
+ {
1720
+ runScoper: _deps?.runScoper ?? runScoperSession,
1721
+ runPlan: _deps?.runPlan ?? runPlanSession,
1722
+ runLoop: _deps?.runLoop ?? runLoopSession,
1723
+ onBanner: _deps?.onBanner
1954
1724
  }
1955
- console.log();
1956
- process.exit(0);
1957
- }
1958
- });
1725
+ );
1726
+ }
1959
1727
 
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.",
1728
+ // src/autopilot/autopilot-cmd.ts
1729
+ var autopilotInteractiveCmd = command2({
1730
+ name: "autopilot",
1731
+ description: "Interactive three-phase autopilot: scope with @scoper, plan with @plan, then execute with the Ralph loop. Produces a structured plan before running.",
1965
1732
  args: {
1966
- workflow: option2({
1967
- long: "workflow",
1733
+ slug: option2({
1734
+ long: "slug",
1968
1735
  type: optional2(stringType2),
1969
- description: "Workflow ID (defaults to the latest)"
1970
- }),
1971
- json: flag({
1972
- long: "json",
1973
- description: "Output JSON"
1736
+ description: "Plan slug (kebab-case, \u22645 words). If omitted, you will be prompted during the scoping session."
1974
1737
  })
1975
1738
  },
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.
2034
-
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
2040
-
1739
+ handler: async ({ slug: _slug }) => {
1740
+ const result = await runInteractiveAutopilot(process.cwd());
1741
+ process.stdout.write(
1742
+ `
1743
+ \x1B[1m\u2713 Autopilot complete\x1B[0m
1744
+ Scope: ${result.scopePath}
1745
+ Plan: ${result.planPath}
1746
+ Loop: ${result.loopResult.exitReason} after ${result.loopResult.iterations} iteration(s)
2041
1747
  `
2042
- );
2043
- process.exit(1);
2044
- }
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
1748
+ );
2068
1749
  }
2069
1750
  });
2070
1751
 
2071
1752
  // src/cli/cli-update.ts
2072
- import * as fs9 from "fs";
2073
- import * as path5 from "path";
2074
- import * as os4 from "os";
1753
+ import * as fs8 from "fs";
1754
+ import * as path8 from "path";
1755
+ import * as os6 from "os";
2075
1756
  import { spawn } from "child_process";
2076
1757
  import { fileURLToPath as fileURLToPath2 } from "url";
2077
1758
  var PACKAGE_NAME = "@glrs-dev/harness-plugin-opencode";
2078
1759
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2079
- var c = {
1760
+ var c2 = {
2080
1761
  reset: "\x1B[0m",
2081
1762
  green: "\x1B[32m",
2082
1763
  yellow: "\x1B[33m",
@@ -2098,12 +1779,12 @@ function isMajorBump(current, latest) {
2098
1779
  return latest.major > current.major;
2099
1780
  }
2100
1781
  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");
1782
+ const cacheHome = process.env["XDG_CACHE_HOME"] ?? path8.join(os6.homedir(), ".cache");
1783
+ return path8.join(cacheHome, "harness-opencode", "cli-update.json");
2103
1784
  }
2104
1785
  function readState() {
2105
1786
  try {
2106
- const raw = fs9.readFileSync(getStateFilePath(), "utf8");
1787
+ const raw = fs8.readFileSync(getStateFilePath(), "utf8");
2107
1788
  return JSON.parse(raw);
2108
1789
  } catch {
2109
1790
  return null;
@@ -2112,21 +1793,21 @@ function readState() {
2112
1793
  function writeState(state) {
2113
1794
  try {
2114
1795
  const statePath = getStateFilePath();
2115
- fs9.mkdirSync(path5.dirname(statePath), { recursive: true });
2116
- fs9.writeFileSync(statePath, JSON.stringify(state));
1796
+ fs8.mkdirSync(path8.dirname(statePath), { recursive: true });
1797
+ fs8.writeFileSync(statePath, JSON.stringify(state));
2117
1798
  } catch {
2118
1799
  }
2119
1800
  }
2120
1801
  function readInstalledVersion() {
2121
- const here = path5.dirname(fileURLToPath2(import.meta.url));
1802
+ const here = path8.dirname(fileURLToPath2(import.meta.url));
2122
1803
  const candidates = [
2123
- path5.join(here, "..", "package.json"),
2124
- path5.join(here, "..", "..", "package.json"),
2125
- path5.join(here, "package.json")
1804
+ path8.join(here, "..", "package.json"),
1805
+ path8.join(here, "..", "..", "package.json"),
1806
+ path8.join(here, "package.json")
2126
1807
  ];
2127
1808
  for (const candidate of candidates) {
2128
1809
  try {
2129
- const raw = fs9.readFileSync(candidate, "utf8");
1810
+ const raw = fs8.readFileSync(candidate, "utf8");
2130
1811
  const parsed = JSON.parse(raw);
2131
1812
  if (parsed.name === PACKAGE_NAME && typeof parsed.version === "string") {
2132
1813
  return parsed.version;
@@ -2197,7 +1878,7 @@ function startUpdateCheck() {
2197
1878
  action = () => {
2198
1879
  process.stderr.write(
2199
1880
  `
2200
- ${c.blue}\u2022${c.reset} Updating ${PACKAGE_NAME} ${c.dim}${currentVersionStr}${c.reset} \u2192 ${c.green}${latestStr}${c.reset} in the background...
1881
+ ${c2.blue}\u2022${c2.reset} Updating ${PACKAGE_NAME} ${c2.dim}${currentVersionStr}${c2.reset} \u2192 ${c2.green}${latestStr}${c2.reset} in the background...
2201
1882
  `
2202
1883
  );
2203
1884
  spawnBackgroundUpdate();
@@ -2212,8 +1893,8 @@ ${c.blue}\u2022${c.reset} Updating ${PACKAGE_NAME} ${c.dim}${currentVersionStr}$
2212
1893
  function printMajorNotice(current, latest) {
2213
1894
  process.stderr.write(
2214
1895
  `
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}
1896
+ ${c2.yellow}${c2.bold}Major update available:${c2.reset} ${current} \u2192 ${c2.green}${latest}${c2.reset}
1897
+ ${c2.dim}Review the changelog before upgrading:${c2.reset}
2217
1898
  bun update -g ${PACKAGE_NAME}
2218
1899
  `
2219
1900
  );
@@ -2252,7 +1933,7 @@ Upgrade Node or run via a compatible Bun runtime. See the "engines" field in pac
2252
1933
  }
2253
1934
  }
2254
1935
  var VERSION = "0.1.0";
2255
- var installCmd = command6({
1936
+ var installCmd = command3({
2256
1937
  name: "install",
2257
1938
  description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
2258
1939
  args: {
@@ -2269,7 +1950,7 @@ var installCmd = command6({
2269
1950
  await install({ dryRun, pin });
2270
1951
  }
2271
1952
  });
2272
- var uninstallCmd = command6({
1953
+ var uninstallCmd = command3({
2273
1954
  name: "uninstall",
2274
1955
  description: 'Remove "@glrs-dev/harness-plugin-opencode" from your opencode.json plugin array.',
2275
1956
  args: {
@@ -2282,7 +1963,7 @@ var uninstallCmd = command6({
2282
1963
  uninstall({ dryRun });
2283
1964
  }
2284
1965
  });
2285
- var doctorCmd = command6({
1966
+ var doctorCmd = command3({
2286
1967
  name: "doctor",
2287
1968
  description: "Check installation health (OpenCode CLI, plugin registration, MCP backends).",
2288
1969
  args: {},
@@ -2290,58 +1971,7 @@ var doctorCmd = command6({
2290
1971
  doctor();
2291
1972
  }
2292
1973
  });
2293
- var planCheckCmd = command6({
2294
- name: "plan-check",
2295
- description: "Parse a plan file's plan-state fence (legacy markdown plans).",
2296
- args: {
2297
- run: option3({
2298
- long: "run",
2299
- type: optional3(string2),
2300
- description: "Print verify commands for pending items, one per line."
2301
- }),
2302
- check: option3({
2303
- long: "check",
2304
- type: optional3(string2),
2305
- description: "Structural validation; exits 1 if any item is invalid."
2306
- }),
2307
- rest: restPositionals2({
2308
- type: string2,
2309
- displayName: "plan-path",
2310
- description: "Path to a plan markdown file. Required unless --run / --check is given."
2311
- })
2312
- },
2313
- handler: ({ run: run2, check, rest }) => {
2314
- const legacy = [];
2315
- if (run2 !== void 0) {
2316
- legacy.push("--run", run2);
2317
- } else if (check !== void 0) {
2318
- legacy.push("--check", check);
2319
- } else {
2320
- legacy.push(...rest);
2321
- }
2322
- planCheck(legacy);
2323
- }
2324
- });
2325
- var planDirCmd = command6({
2326
- name: "plan-dir",
2327
- description: "Print the repo-shared plan directory for the current worktree (resolves + creates + migrates legacy).",
2328
- args: {},
2329
- handler: async () => {
2330
- try {
2331
- const cwd = process.cwd();
2332
- const planDir = await getPlanDir(cwd);
2333
- await migratePlans(cwd, planDir);
2334
- process.stdout.write(planDir + "\n");
2335
- process.exit(0);
2336
- } catch (err) {
2337
- const msg = err instanceof Error ? err.message : String(err);
2338
- process.stderr.write(`plan-dir: ${msg}
2339
- `);
2340
- process.exit(1);
2341
- }
2342
- }
2343
- });
2344
- var installPluginCmd = command6({
1974
+ var installPluginCmd = command3({
2345
1975
  name: "install-plugin",
2346
1976
  description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
2347
1977
  args: {
@@ -2358,7 +1988,7 @@ var installPluginCmd = command6({
2358
1988
  await install({ dryRun, pin });
2359
1989
  }
2360
1990
  });
2361
- var cli = subcommands2({
1991
+ var cli = subcommands({
2362
1992
  name: "glrs-oc",
2363
1993
  description: "OpenCode agent harness CLI.",
2364
1994
  version: VERSION,
@@ -2367,9 +1997,11 @@ var cli = subcommands2({
2367
1997
  install: installCmd,
2368
1998
  uninstall: uninstallCmd,
2369
1999
  doctor: doctorCmd,
2370
- "plan-check": planCheckCmd,
2371
- "plan-dir": planDirCmd,
2372
- pilot: pilotSubcommand
2000
+ // `loop` is the raw-prompt Ralph loop runner.
2001
+ // `autopilot` is the interactive three-phase orchestrator (scope → plan → loop).
2002
+ // PR 3 diverged them: they are now separate subcommands.
2003
+ loop: loopCmd,
2004
+ autopilot: autopilotInteractiveCmd
2373
2005
  }
2374
2006
  });
2375
2007
  var printUpdate = startUpdateCheck();