@glrs-dev/cli 2.2.0 → 2.4.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 (65) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/{chunk-EM4MJBOD.js → chunk-2AZKRWC6.js} +4 -4
  3. package/dist/{chunk-UXBOTMDY.js → chunk-2P3ETOT2.js} +2 -2
  4. package/dist/chunk-2VMFXAJH.js +795 -0
  5. package/dist/chunk-5ZVUFNCP.js +140 -0
  6. package/dist/{chunk-W37UX3U2.js → chunk-6Y27RQQL.js} +2 -2
  7. package/dist/{chunk-RZWOWTKF.js → chunk-EKNRKZWR.js} +4 -4
  8. package/dist/{chunk-YGNDPKIW.js → chunk-HQUCVJ4G.js} +3 -1
  9. package/dist/{chunk-OABVEBWW.js → chunk-MBEVC327.js} +1 -1
  10. package/dist/{chunk-SB3MLROC.js → chunk-MCM47HH4.js} +8 -3
  11. package/dist/{chunk-F3AFRUT2.js → chunk-PTIO556V.js} +2 -2
  12. package/dist/{chunk-E2UNZIZT.js → chunk-R2WXQ54P.js} +1 -1
  13. package/dist/{chunk-I2KUXY3I.js → chunk-SMDIOB5B.js} +2 -2
  14. package/dist/{chunk-SPULDN7P.js → chunk-YY7EWHMA.js} +5 -3
  15. package/dist/cli.js +31 -20
  16. package/dist/commands/autopilot-interactive.d.ts +89 -0
  17. package/dist/commands/autopilot-interactive.js +248 -0
  18. package/dist/commands/autopilot-raw.d.ts +1 -0
  19. package/dist/commands/autopilot-raw.js +368 -0
  20. package/dist/commands/autopilot-tui.d.ts +7 -0
  21. package/dist/commands/autopilot-tui.js +7 -0
  22. package/dist/commands/autopilot.d.ts +39 -0
  23. package/dist/commands/autopilot.js +395 -0
  24. package/dist/commands/cleanup.js +3 -3
  25. package/dist/commands/create.js +4 -4
  26. package/dist/commands/dashboard.d.ts +3 -0
  27. package/dist/commands/dashboard.js +1549 -0
  28. package/dist/commands/debrief.d.ts +57 -0
  29. package/dist/commands/debrief.js +9 -0
  30. package/dist/commands/delete.js +3 -3
  31. package/dist/commands/go.js +2 -2
  32. package/dist/commands/list.js +3 -3
  33. package/dist/commands/loop.d.ts +42 -0
  34. package/dist/commands/loop.js +133 -0
  35. package/dist/commands/plan-picker.d.ts +15 -0
  36. package/dist/commands/plan-picker.js +76 -0
  37. package/dist/commands/scoper.d.ts +54 -0
  38. package/dist/commands/scoper.js +341 -0
  39. package/dist/commands/switch.js +3 -3
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +1 -1
  42. package/dist/lib/auto-update.js +1 -1
  43. package/dist/lib/config.d.ts +3 -2
  44. package/dist/lib/config.js +1 -1
  45. package/dist/lib/registry.d.ts +2 -0
  46. package/dist/lib/registry.js +1 -1
  47. package/dist/lib/worktree.js +3 -3
  48. package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +16 -0
  49. package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer-thorough.md +6 -7
  50. package/dist/vendor/harness-opencode/dist/agents/prompts/debriefer.md +55 -0
  51. package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +2 -1
  52. package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +104 -7
  53. package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +4 -2
  54. package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +129 -0
  55. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +0 -1
  56. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +0 -1
  57. package/dist/vendor/harness-opencode/dist/chunk-GILWWWMB.js +66 -0
  58. package/dist/vendor/harness-opencode/dist/cli.js +328 -687
  59. package/dist/vendor/harness-opencode/dist/index.js +123 -20
  60. package/dist/vendor/harness-opencode/dist/plugin-check-GJRD2OK6.js +14 -0
  61. package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +2 -1
  62. package/dist/vendor/harness-opencode/package.json +1 -1
  63. package/package.json +10 -2
  64. package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +0 -80
  65. package/dist/vendor/harness-opencode/dist/bin/plan-check.sh +0 -255
@@ -6,25 +6,25 @@ import {
6
6
  refreshPluginCache,
7
7
  validateModelOverride
8
8
  } from "./chunk-PDMXYZM4.js";
9
+ import {
10
+ promptChoice,
11
+ promptMulti
12
+ } from "./chunk-GILWWWMB.js";
9
13
 
10
14
  // src/cli.ts
11
15
  import {
12
16
  binary,
13
17
  command as command2,
14
18
  flag,
15
- option as option2,
16
- optional as optional2,
17
- positional as positional2,
18
- restPositionals,
19
- string,
19
+ positional,
20
20
  subcommands,
21
21
  run
22
22
  } from "cmd-ts";
23
23
 
24
24
  // src/cli/install.ts
25
- import * as fs3 from "fs";
26
- import * as path3 from "path";
27
- import * as os2 from "os";
25
+ import * as fs2 from "fs";
26
+ import * as path2 from "path";
27
+ import * as os from "os";
28
28
  import { fileURLToPath } from "url";
29
29
 
30
30
  // src/cli/merge-config.ts
@@ -192,42 +192,6 @@ function seedConfig(srcJson, dstPath) {
192
192
  fs.writeFileSync(dstPath, JSON.stringify(srcJson, null, 2) + "\n");
193
193
  }
194
194
 
195
- // src/cli/plugin-check.ts
196
- import * as fs2 from "fs";
197
- import * as path2 from "path";
198
- import * as os from "os";
199
- import { select, checkbox, confirm } from "@inquirer/prompts";
200
- async function promptChoice(question, choices, defaultIndex = 0) {
201
- if (!process.stdin.isTTY) return defaultIndex;
202
- const answer = await select({
203
- message: question,
204
- choices: choices.map((label, i) => ({
205
- name: label,
206
- value: i
207
- })),
208
- default: defaultIndex
209
- });
210
- return answer;
211
- }
212
- async function promptMulti(question, choices) {
213
- if (!process.stdin.isTTY) {
214
- const defaults = /* @__PURE__ */ new Set();
215
- choices.forEach((c3, i) => {
216
- if (c3.defaultOn) defaults.add(i);
217
- });
218
- return defaults;
219
- }
220
- const answers = await checkbox({
221
- message: question,
222
- choices: choices.map((c3, i) => ({
223
- name: c3.label,
224
- value: i,
225
- checked: c3.defaultOn
226
- }))
227
- });
228
- return new Set(answers);
229
- }
230
-
231
195
  // src/cli/models-dev.ts
232
196
  var MODELS_DEV_URL = "https://models.dev/api.json";
233
197
  var FETCH_TIMEOUT_MS = 5e3;
@@ -386,14 +350,14 @@ function extractPluginOptions(config) {
386
350
  return null;
387
351
  }
388
352
  function readPackageVersion() {
389
- const here = path3.dirname(fileURLToPath(import.meta.url));
353
+ const here = path2.dirname(fileURLToPath(import.meta.url));
390
354
  const candidates = [
391
- path3.join(here, "..", "package.json"),
392
- path3.join(here, "..", "..", "package.json")
355
+ path2.join(here, "..", "package.json"),
356
+ path2.join(here, "..", "..", "package.json")
393
357
  ];
394
358
  for (const candidate of candidates) {
395
359
  try {
396
- const raw = fs3.readFileSync(candidate, "utf8");
360
+ const raw = fs2.readFileSync(candidate, "utf8");
397
361
  const parsed = JSON.parse(raw);
398
362
  if (parsed.name === PLUGIN_NAME && typeof parsed.version === "string") {
399
363
  return parsed.version;
@@ -406,8 +370,8 @@ function readPackageVersion() {
406
370
  );
407
371
  }
408
372
  function getOpencodeConfigPath() {
409
- const configHome = process.env["XDG_CONFIG_HOME"] ?? path3.join(os2.homedir(), ".config");
410
- return path3.join(configHome, "opencode", "opencode.json");
373
+ const configHome = process.env["XDG_CONFIG_HOME"] ?? path2.join(os.homedir(), ".config");
374
+ return path2.join(configHome, "opencode", "opencode.json");
411
375
  }
412
376
  async function refreshPluginCacheIfStale() {
413
377
  try {
@@ -424,9 +388,9 @@ async function refreshPluginCacheIfStale() {
424
388
  }
425
389
  }
426
390
  function readExistingConfig(configPath) {
427
- if (!fs3.existsSync(configPath)) return null;
391
+ if (!fs2.existsSync(configPath)) return null;
428
392
  try {
429
- return JSON.parse(fs3.readFileSync(configPath, "utf8"));
393
+ return JSON.parse(fs2.readFileSync(configPath, "utf8"));
430
394
  } catch {
431
395
  return null;
432
396
  }
@@ -467,14 +431,14 @@ function detectEnabledPluginToggles(existing) {
467
431
  }
468
432
  function migrateHarnessKeyToPluginOptions(configPath) {
469
433
  try {
470
- if (!fs3.existsSync(configPath)) return;
471
- const raw = fs3.readFileSync(configPath, "utf8");
434
+ if (!fs2.existsSync(configPath)) return;
435
+ const raw = fs2.readFileSync(configPath, "utf8");
472
436
  const config = JSON.parse(raw);
473
437
  if (!config.harness || typeof config.harness !== "object") return;
474
438
  const plugins = Array.isArray(config.plugin) ? config.plugin : [];
475
439
  const pluginIdx = plugins.findIndex((entry) => {
476
440
  const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
477
- return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
441
+ return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`) || String(name ?? "").includes("harness-opencode");
478
442
  });
479
443
  if (pluginIdx < 0) return;
480
444
  const current = plugins[pluginIdx];
@@ -484,8 +448,8 @@ function migrateHarnessKeyToPluginOptions(configPath) {
484
448
  plugins[pluginIdx] = [existingName, merged];
485
449
  delete config.harness;
486
450
  const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
487
- fs3.copyFileSync(configPath, bakPath);
488
- fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
451
+ fs2.copyFileSync(configPath, bakPath);
452
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
489
453
  ok("Migrated legacy `harness` config into plugin options");
490
454
  info(`Backup: ${bakPath}`);
491
455
  } catch {
@@ -509,17 +473,17 @@ function deepEqual(a, b) {
509
473
  }
510
474
  function writePluginOption(configPath, subKey, value, opts) {
511
475
  try {
512
- if (!fs3.existsSync(configPath)) {
476
+ if (!fs2.existsSync(configPath)) {
513
477
  return { changed: false };
514
478
  }
515
- const raw = fs3.readFileSync(configPath, "utf8");
479
+ const raw = fs2.readFileSync(configPath, "utf8");
516
480
  const config = JSON.parse(raw);
517
481
  if (!Array.isArray(config.plugin)) {
518
482
  return { changed: false };
519
483
  }
520
484
  const pluginIdx = config.plugin.findIndex((entry) => {
521
485
  const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
522
- return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
486
+ return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`) || String(name ?? "").includes("harness-opencode");
523
487
  });
524
488
  if (pluginIdx < 0) {
525
489
  return { changed: false };
@@ -536,9 +500,9 @@ function writePluginOption(configPath, subKey, value, opts) {
536
500
  return { changed: true };
537
501
  }
538
502
  const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
539
- fs3.copyFileSync(configPath, bakPath);
503
+ fs2.copyFileSync(configPath, bakPath);
540
504
  config.plugin[pluginIdx] = [existingName, newOpts];
541
- fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
505
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
542
506
  ok(`Reconfigured ${subKey}`);
543
507
  info(`Backup: ${bakPath}`);
544
508
  return { changed: true, bakPath };
@@ -548,10 +512,10 @@ function writePluginOption(configPath, subKey, value, opts) {
548
512
  }
549
513
  function writeMcpToggles(configPath, enabledSet, opts) {
550
514
  try {
551
- if (!fs3.existsSync(configPath)) {
515
+ if (!fs2.existsSync(configPath)) {
552
516
  return { changed: false };
553
517
  }
554
- const raw = fs3.readFileSync(configPath, "utf8");
518
+ const raw = fs2.readFileSync(configPath, "utf8");
555
519
  const config = JSON.parse(raw);
556
520
  const toggleNames = new Set(MCP_TOGGLES.map((t) => t.name));
557
521
  const existingMcp = config.mcp && typeof config.mcp === "object" ? { ...config.mcp } : {};
@@ -587,13 +551,13 @@ function writeMcpToggles(configPath, enabledSet, opts) {
587
551
  return { changed: true };
588
552
  }
589
553
  const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
590
- fs3.copyFileSync(configPath, bakPath);
554
+ fs2.copyFileSync(configPath, bakPath);
591
555
  if (Object.keys(newMcp).length > 0) {
592
556
  config.mcp = newMcp;
593
557
  } else {
594
558
  delete config.mcp;
595
559
  }
596
- fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
560
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
597
561
  ok("Reconfigured MCPs");
598
562
  info(`Backup: ${bakPath}`);
599
563
  return { changed: true, bakPath };
@@ -603,10 +567,10 @@ function writeMcpToggles(configPath, enabledSet, opts) {
603
567
  }
604
568
  function writePluginToggles(configPath, enabledSet, opts) {
605
569
  try {
606
- if (!fs3.existsSync(configPath)) {
570
+ if (!fs2.existsSync(configPath)) {
607
571
  return { changed: false };
608
572
  }
609
- const raw = fs3.readFileSync(configPath, "utf8");
573
+ const raw = fs2.readFileSync(configPath, "utf8");
610
574
  const config = JSON.parse(raw);
611
575
  const toggleNames = new Set(PLUGIN_TOGGLES.map((t) => t.name));
612
576
  const existingPlugins = Array.isArray(config.plugin) ? config.plugin : [];
@@ -634,7 +598,7 @@ function writePluginToggles(configPath, enabledSet, opts) {
634
598
  return { changed: true };
635
599
  }
636
600
  const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
637
- fs3.copyFileSync(configPath, bakPath);
601
+ fs2.copyFileSync(configPath, bakPath);
638
602
  const newPlugins = existingPlugins.filter((entry) => {
639
603
  const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
640
604
  return !(typeof name === "string" && toRemove.has(name));
@@ -643,7 +607,7 @@ function writePluginToggles(configPath, enabledSet, opts) {
643
607
  newPlugins.push(name);
644
608
  }
645
609
  config.plugin = newPlugins;
646
- fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
610
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
647
611
  ok("Reconfigured plugin add-ons");
648
612
  info(`Backup: ${bakPath}`);
649
613
  return { changed: true, bakPath };
@@ -660,7 +624,7 @@ async function install(opts = {}) {
660
624
  const hasPlugin = existing ? (Array.isArray(existing.plugin) ? existing.plugin : []).some(
661
625
  (p) => {
662
626
  const name = typeof p === "string" ? p : Array.isArray(p) ? p[0] : null;
663
- return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`);
627
+ return name === PLUGIN_NAME || String(name ?? "").startsWith(`${PLUGIN_NAME}@`) || String(name ?? "").includes("harness-opencode");
664
628
  }
665
629
  ) : false;
666
630
  const existingProvider = detectModelProvider(existing);
@@ -957,7 +921,7 @@ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
957
921
  if (reconfigurePluginToggles) {
958
922
  writePluginToggles(configPath, newPluginToggleEnabledSet, { dryRun });
959
923
  }
960
- if (!fs3.existsSync(configPath)) {
924
+ if (!fs2.existsSync(configPath)) {
961
925
  if (dryRun) {
962
926
  info(`[dry-run] Would create ${configPath}`);
963
927
  info(`[dry-run] Config: ${JSON.stringify(config, null, 2)}`);
@@ -1002,36 +966,36 @@ ${c.bold}Ready.${c.reset} Run ${c.green}opencode${c.reset} to start.
1002
966
  }
1003
967
 
1004
968
  // src/cli/uninstall.ts
1005
- import * as fs4 from "fs";
1006
- import * as path4 from "path";
1007
- import * as os3 from "os";
969
+ import * as fs3 from "fs";
970
+ import * as path3 from "path";
971
+ import * as os2 from "os";
1008
972
  var PLUGIN_NAME2 = "@glrs-dev/harness-plugin-opencode";
1009
973
  function getOpencodeConfigPath2() {
1010
- const configHome = process.env["XDG_CONFIG_HOME"] ?? path4.join(os3.homedir(), ".config");
1011
- return path4.join(configHome, "opencode", "opencode.json");
974
+ const configHome = process.env["XDG_CONFIG_HOME"] ?? path3.join(os2.homedir(), ".config");
975
+ return path3.join(configHome, "opencode", "opencode.json");
1012
976
  }
1013
977
  function uninstall(opts = {}) {
1014
978
  const { dryRun = false } = opts;
1015
979
  const configPath = getOpencodeConfigPath2();
1016
- const c3 = {
980
+ const c4 = {
1017
981
  reset: "\x1B[0m",
1018
982
  green: "\x1B[32m",
1019
983
  yellow: "\x1B[33m",
1020
984
  blue: "\x1B[34m"
1021
985
  };
1022
- const ok2 = (msg) => console.log(`${c3.green}\u2713${c3.reset} ${msg}`);
1023
- const info2 = (msg) => console.log(`${c3.blue}\u2022${c3.reset} ${msg}`);
1024
- const warn2 = (msg) => console.log(`${c3.yellow}!${c3.reset} ${msg}`);
986
+ const ok3 = (msg) => console.log(`${c4.green}\u2713${c4.reset} ${msg}`);
987
+ const info3 = (msg) => console.log(`${c4.blue}\u2022${c4.reset} ${msg}`);
988
+ const warn2 = (msg) => console.log(`${c4.yellow}!${c4.reset} ${msg}`);
1025
989
  console.log(`
1026
- ${c3.blue}Uninstalling ${PLUGIN_NAME2}${c3.reset}
990
+ ${c4.blue}Uninstalling ${PLUGIN_NAME2}${c4.reset}
1027
991
  `);
1028
- if (!fs4.existsSync(configPath)) {
992
+ if (!fs3.existsSync(configPath)) {
1029
993
  warn2(`No opencode.json found at ${configPath} \u2014 nothing to do`);
1030
994
  return;
1031
995
  }
1032
996
  let raw;
1033
997
  try {
1034
- raw = fs4.readFileSync(configPath, "utf8");
998
+ raw = fs3.readFileSync(configPath, "utf8");
1035
999
  } catch (e) {
1036
1000
  console.error(`\x1B[31m\u2717\x1B[0m Failed to read ${configPath}: ${e.message}`);
1037
1001
  process.exit(1);
@@ -1053,12 +1017,12 @@ ${c3.blue}Uninstalling ${PLUGIN_NAME2}${c3.reset}
1053
1017
  return;
1054
1018
  }
1055
1019
  if (dryRun) {
1056
- info2(`[dry-run] Would remove "${PLUGIN_NAME2}" from plugin array in ${configPath}`);
1020
+ info3(`[dry-run] Would remove "${PLUGIN_NAME2}" from plugin array in ${configPath}`);
1057
1021
  return;
1058
1022
  }
1059
1023
  const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
1060
1024
  try {
1061
- fs4.copyFileSync(configPath, bakPath);
1025
+ fs3.copyFileSync(configPath, bakPath);
1062
1026
  } catch (e) {
1063
1027
  console.error(`\x1B[31m\u2717\x1B[0m Failed to write backup: ${e.message}`);
1064
1028
  process.exit(1);
@@ -1066,32 +1030,32 @@ ${c3.blue}Uninstalling ${PLUGIN_NAME2}${c3.reset}
1066
1030
  config.plugin = filtered;
1067
1031
  const tmpPath = `${configPath}.uninstall.tmp.${Date.now()}-${process.pid}`;
1068
1032
  try {
1069
- fs4.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
1070
- fs4.renameSync(tmpPath, configPath);
1033
+ fs3.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
1034
+ fs3.renameSync(tmpPath, configPath);
1071
1035
  } catch (e) {
1072
1036
  try {
1073
- fs4.unlinkSync(tmpPath);
1037
+ fs3.unlinkSync(tmpPath);
1074
1038
  } catch {
1075
1039
  }
1076
1040
  console.error(`\x1B[31m\u2717\x1B[0m Failed to write config: ${e.message}`);
1077
1041
  process.exit(1);
1078
1042
  }
1079
- ok2(`Removed "${PLUGIN_NAME2}" from ${configPath}`);
1080
- info2(`Backup: ${bakPath}`);
1043
+ ok3(`Removed "${PLUGIN_NAME2}" from ${configPath}`);
1044
+ info3(`Backup: ${bakPath}`);
1081
1045
  console.log(`
1082
1046
  To fully remove the package: bun remove @glrs-dev/harness-plugin-opencode
1083
1047
  `);
1084
1048
  }
1085
1049
 
1086
1050
  // src/cli/doctor.ts
1087
- import * as fs5 from "fs";
1088
- import * as path5 from "path";
1089
- import * as os4 from "os";
1051
+ import * as fs4 from "fs";
1052
+ import * as path4 from "path";
1053
+ import * as os3 from "os";
1090
1054
  import { execSync } from "child_process";
1091
1055
  var PLUGIN_NAME3 = "@glrs-dev/harness-plugin-opencode";
1092
1056
  function getOpencodeConfigPath3() {
1093
- const configHome = process.env["XDG_CONFIG_HOME"] ?? path5.join(os4.homedir(), ".config");
1094
- return path5.join(configHome, "opencode", "opencode.json");
1057
+ const configHome = process.env["XDG_CONFIG_HOME"] ?? path4.join(os3.homedir(), ".config");
1058
+ return path4.join(configHome, "opencode", "opencode.json");
1095
1059
  }
1096
1060
  function cmd(command3) {
1097
1061
  try {
@@ -1104,29 +1068,29 @@ function which(bin) {
1104
1068
  return cmd(`which ${bin}`) !== null;
1105
1069
  }
1106
1070
  function doctor() {
1107
- const c3 = {
1071
+ const c4 = {
1108
1072
  reset: "\x1B[0m",
1109
1073
  green: "\x1B[32m",
1110
1074
  yellow: "\x1B[33m",
1111
1075
  red: "\x1B[31m",
1112
1076
  bold: "\x1B[1m"
1113
1077
  };
1114
- const ok2 = (msg) => console.log(`${c3.green}\u2713${c3.reset} ${msg}`);
1115
- const warn2 = (msg) => console.log(`${c3.yellow}!${c3.reset} ${msg}`);
1116
- const fail = (msg) => console.log(`${c3.red}\u2717${c3.reset} ${msg}`);
1078
+ const ok3 = (msg) => console.log(`${c4.green}\u2713${c4.reset} ${msg}`);
1079
+ const warn2 = (msg) => console.log(`${c4.yellow}!${c4.reset} ${msg}`);
1080
+ const fail = (msg) => console.log(`${c4.red}\u2717${c4.reset} ${msg}`);
1117
1081
  console.log(`
1118
- ${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
1082
+ ${c4.bold}Doctor \u2014 ${PLUGIN_NAME3}${c4.reset}
1119
1083
  `);
1120
1084
  const ocVersion = cmd("opencode --version 2>/dev/null | head -1");
1121
1085
  if (ocVersion) {
1122
- ok2(`opencode ${ocVersion}`);
1086
+ ok3(`opencode ${ocVersion}`);
1123
1087
  } else {
1124
1088
  fail("opencode CLI not found \u2014 install from https://opencode.ai");
1125
1089
  }
1126
1090
  const configPath = getOpencodeConfigPath3();
1127
- if (fs5.existsSync(configPath)) {
1091
+ if (fs4.existsSync(configPath)) {
1128
1092
  try {
1129
- const config = JSON.parse(fs5.readFileSync(configPath, "utf8"));
1093
+ const config = JSON.parse(fs4.readFileSync(configPath, "utf8"));
1130
1094
  const plugins = Array.isArray(config.plugin) ? config.plugin : [];
1131
1095
  let pluginOptions = null;
1132
1096
  const hasPlugin = plugins.some((p) => {
@@ -1144,7 +1108,7 @@ ${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
1144
1108
  return false;
1145
1109
  });
1146
1110
  if (hasPlugin) {
1147
- ok2(`"${PLUGIN_NAME3}" present in opencode.json plugin array`);
1111
+ ok3(`"${PLUGIN_NAME3}" present in opencode.json plugin array`);
1148
1112
  } else {
1149
1113
  warn2(`"${PLUGIN_NAME3}" NOT in opencode.json plugin array \u2014 run: bunx ${PLUGIN_NAME3} install`);
1150
1114
  }
@@ -1181,20 +1145,20 @@ ${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
1181
1145
  }
1182
1146
  }
1183
1147
  if (invalid.length === 0) {
1184
- ok2("model overrides look valid");
1148
+ ok3("model overrides look valid");
1185
1149
  } else {
1186
1150
  for (const entry of invalid) {
1187
1151
  fail(`invalid model override at ${entry.keyPath}: "${entry.value}"`);
1188
1152
  if (entry.reason) {
1189
- console.log(` ${c3.yellow}reason:${c3.reset} ${entry.reason}`);
1153
+ console.log(` ${c4.yellow}reason:${c4.reset} ${entry.reason}`);
1190
1154
  }
1191
1155
  if (entry.suggestion) {
1192
1156
  console.log(
1193
- ` ${c3.yellow}fix:${c3.reset} remove this key, or replace with \`${entry.suggestion}\``
1157
+ ` ${c4.yellow}fix:${c4.reset} remove this key, or replace with \`${entry.suggestion}\``
1194
1158
  );
1195
1159
  } else {
1196
1160
  console.log(
1197
- ` ${c3.yellow}fix:${c3.reset} remove this key, or run \`bunx ${PLUGIN_NAME3} install\` to pick a current preset`
1161
+ ` ${c4.yellow}fix:${c4.reset} remove this key, or run \`bunx ${PLUGIN_NAME3} install\` to pick a current preset`
1198
1162
  );
1199
1163
  }
1200
1164
  }
@@ -1207,563 +1171,292 @@ ${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
1207
1171
  warn2(`No opencode.json at ${configPath} \u2014 run: bunx ${PLUGIN_NAME3} install`);
1208
1172
  }
1209
1173
  if (which("uvx")) {
1210
- ok2("uvx (serena + git MCPs)");
1174
+ ok3("uvx (serena + git MCPs)");
1211
1175
  } else {
1212
1176
  warn2("uvx not found \u2014 serena and git MCPs won't work. Install: brew install uv");
1213
1177
  }
1214
1178
  if (which("node") && which("npx")) {
1215
- ok2(`node ${cmd("node --version") ?? ""} + npx (memory MCP)`);
1179
+ ok3(`node ${cmd("node --version") ?? ""} + npx (memory MCP)`);
1216
1180
  } else {
1217
1181
  warn2("node/npx not found \u2014 memory MCP won't work");
1218
1182
  }
1219
- const planCheckResult = cmd(`bunx ${PLUGIN_NAME3} plan-check --help 2>/dev/null`);
1220
- if (planCheckResult !== null) {
1221
- ok2("plan-check CLI invokable");
1222
- } else {
1223
- warn2("plan-check CLI not invokable \u2014 try: bun install");
1224
- }
1225
1183
  if (which("bun")) {
1226
- ok2(`bun ${cmd("bun --version") ?? ""}`);
1184
+ ok3(`bun ${cmd("bun --version") ?? ""}`);
1227
1185
  } else if (which("npm")) {
1228
- ok2(`npm ${cmd("npm --version") ?? ""} (install bun for faster installs)`);
1186
+ ok3(`npm ${cmd("npm --version") ?? ""} (install bun for faster installs)`);
1229
1187
  } else {
1230
1188
  fail("Neither bun nor npm found \u2014 cannot install plugins");
1231
1189
  }
1232
1190
  console.log();
1233
1191
  }
1234
1192
 
1235
- // src/bin/plan-check.ts
1236
- import { execFileSync } from "child_process";
1237
- import { fileURLToPath as fileURLToPath2 } from "url";
1238
- import { dirname as dirname3, join as join5 } from "path";
1239
- function planCheck(args) {
1240
- const here = dirname3(fileURLToPath2(import.meta.url));
1241
- const candidates = [
1242
- join5(here, "plan-check.sh"),
1243
- // dev: src/bin/plan-check.sh
1244
- join5(here, "bin", "plan-check.sh")
1245
- // dist: dist/ → dist/bin/plan-check.sh
1246
- ];
1247
- let scriptPath;
1248
- for (const p of candidates) {
1249
- try {
1250
- execFileSync("test", ["-f", p]);
1251
- scriptPath = p;
1252
- break;
1253
- } catch {
1254
- }
1255
- }
1256
- if (!scriptPath) {
1257
- console.error("plan-check: could not find plan-check.sh");
1258
- process.exit(2);
1259
- }
1260
- try {
1261
- execFileSync("bash", [scriptPath, ...args], {
1262
- stdio: "inherit",
1263
- encoding: "utf8"
1264
- });
1265
- } catch (e) {
1266
- process.exit(e.status ?? 1);
1267
- }
1268
- }
1269
-
1270
- // src/plan-paths.ts
1271
- import { execFile } from "child_process";
1272
- import * as fs6 from "fs/promises";
1273
- import * as os5 from "os";
1274
- import * as path6 from "path";
1275
- function execFileP(file, args, opts = {}) {
1276
- const { cwd, timeoutMs = 5e3 } = opts;
1277
- return new Promise((resolve2, reject) => {
1278
- const controller = new AbortController();
1279
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1280
- execFile(
1281
- file,
1282
- args,
1283
- { signal: controller.signal, cwd, encoding: "utf8" },
1284
- (err, stdout) => {
1285
- clearTimeout(timer);
1286
- if (err) {
1287
- reject(err);
1288
- return;
1289
- }
1290
- resolve2(stdout ?? "");
1291
- }
1292
- );
1293
- });
1294
- }
1295
- function expandTilde(p) {
1296
- if (p === "~") return os5.homedir();
1297
- if (p.startsWith("~/")) return path6.join(os5.homedir(), p.slice(2));
1298
- return p;
1193
+ // src/cli/configure.ts
1194
+ import { command } from "cmd-ts";
1195
+ import * as fs5 from "fs";
1196
+ import * as path5 from "path";
1197
+ import * as os4 from "os";
1198
+ var PLUGIN_NAME4 = "@glrs-dev/harness-plugin-opencode";
1199
+ var c2 = {
1200
+ reset: "\x1B[0m",
1201
+ green: "\x1B[32m",
1202
+ yellow: "\x1B[33m",
1203
+ blue: "\x1B[34m",
1204
+ dim: "\x1B[2m",
1205
+ bold: "\x1B[1m",
1206
+ cyan: "\x1B[36m"
1207
+ };
1208
+ var ok2 = (msg) => console.log(`${c2.green}\u2713${c2.reset} ${msg}`);
1209
+ var info2 = (msg) => console.log(`${c2.blue}\u2022${c2.reset} ${msg}`);
1210
+ function getOpencodeConfigPath4() {
1211
+ const configHome = process.env["XDG_CONFIG_HOME"] ?? path5.join(os4.homedir(), ".config");
1212
+ return path5.join(configHome, "opencode", "opencode.json");
1299
1213
  }
1300
- async function getRepoFolder(worktreeDir) {
1301
- let stdout;
1214
+ function readConfig(configPath) {
1215
+ if (!fs5.existsSync(configPath)) return null;
1302
1216
  try {
1303
- stdout = await execFileP(
1304
- "git",
1305
- ["rev-parse", "--git-common-dir"],
1306
- { cwd: worktreeDir }
1307
- );
1308
- } catch (err) {
1309
- const msg = err instanceof Error ? err.message : "unknown error running `git rev-parse --git-common-dir`";
1310
- throw new Error(
1311
- `getRepoFolder: failed to resolve git-common-dir for ${worktreeDir}: ${msg}`
1312
- );
1313
- }
1314
- const gitCommonDir = stdout.trim();
1315
- if (!gitCommonDir) {
1316
- throw new Error(
1317
- `getRepoFolder: \`git rev-parse --git-common-dir\` returned empty for ${worktreeDir}`
1318
- );
1217
+ return JSON.parse(fs5.readFileSync(configPath, "utf8"));
1218
+ } catch {
1219
+ return null;
1319
1220
  }
1320
- const absCommonDir = path6.isAbsolute(gitCommonDir) ? gitCommonDir : path6.resolve(worktreeDir, gitCommonDir);
1321
- const repoRoot = path6.dirname(absCommonDir);
1322
- return path6.basename(repoRoot);
1323
1221
  }
1324
- async function getPlanDir(worktreeDir) {
1325
- const override = process.env.GLORIOUS_PLAN_DIR;
1326
- const base = override ? expandTilde(override) : path6.join(os5.homedir(), ".glorious", "opencode");
1327
- const repoFolder = await getRepoFolder(worktreeDir);
1328
- const planDir = path6.join(base, repoFolder, "plans");
1329
- await fs6.mkdir(planDir, { recursive: true });
1330
- return planDir;
1331
- }
1332
- async function migratePlans(worktreeDir, planDir) {
1333
- const oldDir = path6.join(worktreeDir, ".agent", "plans");
1334
- const marker = path6.join(oldDir, ".migrated");
1335
- try {
1336
- await fs6.stat(oldDir);
1337
- } catch {
1338
- return;
1222
+ function extractPluginOptions2(config) {
1223
+ const plugins = config.plugin;
1224
+ if (!Array.isArray(plugins)) return null;
1225
+ for (const entry of plugins) {
1226
+ if (Array.isArray(entry) && entry.length >= 2 && (entry[0] === PLUGIN_NAME4 || String(entry[0]).startsWith(`${PLUGIN_NAME4}@`) || String(entry[0]).startsWith("file://"))) {
1227
+ return entry[1];
1228
+ }
1339
1229
  }
1340
- try {
1341
- await fs6.stat(marker);
1342
- return;
1343
- } catch {
1230
+ return null;
1231
+ }
1232
+ var TIER_LABELS = {
1233
+ deep: "@plan, @prime, @architecture-advisor",
1234
+ mid: "@build, @docs-maintainer, @lib-reader",
1235
+ "mid-execute": "@build, @spec-reviewer, @code-reviewer (overrides mid when set)",
1236
+ "autopilot-execute": "autopilot --fast",
1237
+ fast: "@code-searcher"
1238
+ };
1239
+ var TIERS = ["deep", "mid", "mid-execute", "autopilot-execute", "fast"];
1240
+ async function configureModels(configPath, currentModels) {
1241
+ console.log(`
1242
+ ${c2.bold}Current model configuration:${c2.reset}
1243
+ `);
1244
+ for (const tier2 of TIERS) {
1245
+ const model = currentModels[tier2]?.[0] ?? "(not set)";
1246
+ const label = TIER_LABELS[tier2] ?? tier2;
1247
+ console.log(` ${c2.cyan}${label}${c2.reset}`);
1248
+ console.log(` ${model}
1249
+ `);
1344
1250
  }
1345
- let entries;
1346
- try {
1347
- entries = await fs6.readdir(oldDir);
1348
- } catch {
1251
+ console.log();
1252
+ const tierChoices = [
1253
+ ...TIERS.map((t) => {
1254
+ const label = TIER_LABELS[t] ?? t;
1255
+ const model = currentModels[t]?.[0] ?? "(not set)";
1256
+ return `${label} \u2192 ${model}`;
1257
+ }),
1258
+ "\u2190 Back"
1259
+ ];
1260
+ const tierIdx = await promptChoice("Which tier to change?", tierChoices, tierChoices.length - 1);
1261
+ if (tierIdx >= TIERS.length) return;
1262
+ const tier = TIERS[tierIdx];
1263
+ info2("Fetching available models\u2026");
1264
+ const providers = await fetchModelsDevProviders();
1265
+ if (!providers || providers.length === 0) {
1266
+ console.log(`${c2.yellow}!${c2.reset} Could not reach Models.dev API. Enter model ID manually.`);
1267
+ const { input } = await import("@inquirer/prompts");
1268
+ const modelId = await input({
1269
+ message: ` ${tier} model ID:`,
1270
+ default: currentModels[tier]?.[0] ?? ""
1271
+ });
1272
+ if (modelId) {
1273
+ const newModels2 = { ...currentModels, [tier]: [modelId] };
1274
+ writePluginOption(configPath, "models", newModels2, { dryRun: false });
1275
+ }
1349
1276
  return;
1350
1277
  }
1351
- const planFiles = entries.filter(
1352
- (name) => name.endsWith(".md") && !name.startsWith(".")
1353
- );
1354
- await fs6.mkdir(planDir, { recursive: true });
1355
- for (const name of planFiles) {
1356
- const src = path6.join(oldDir, name);
1357
- const dst = path6.join(planDir, name);
1358
- let dstExists = false;
1359
- try {
1360
- await fs6.stat(dst);
1361
- dstExists = true;
1362
- } catch {
1363
- dstExists = false;
1364
- }
1365
- if (!dstExists) {
1366
- await fs6.rename(src, dst);
1367
- continue;
1368
- }
1369
- const [srcBuf, dstBuf] = await Promise.all([
1370
- fs6.readFile(src),
1371
- fs6.readFile(dst)
1372
- ]);
1373
- if (srcBuf.equals(dstBuf)) {
1374
- await fs6.unlink(src);
1375
- continue;
1278
+ const allModels = [];
1279
+ for (const provider of providers) {
1280
+ for (const [modelId, model] of Object.entries(provider.models)) {
1281
+ allModels.push({
1282
+ id: `${provider.id}/${modelId}`,
1283
+ provider: provider.name,
1284
+ name: model.name ?? modelId
1285
+ });
1376
1286
  }
1377
- process.stderr.write(
1378
- `[harness-opencode] migratePlans: conflict on ${name} \u2014 destination ${dst} exists with different content; leaving source ${src} in place. Resolve manually.
1379
- `
1380
- );
1381
1287
  }
1382
- await fs6.writeFile(marker, "");
1288
+ const byProvider = /* @__PURE__ */ new Map();
1289
+ for (const m of allModels) {
1290
+ if (!byProvider.has(m.provider)) byProvider.set(m.provider, []);
1291
+ byProvider.get(m.provider).push(m);
1292
+ }
1293
+ const providerNames = [...byProvider.keys()];
1294
+ providerNames.push("\u2190 Back");
1295
+ const providerIdx = await promptChoice(`Provider for ${tier}:`, providerNames, 0);
1296
+ if (providerIdx >= providerNames.length - 1) return;
1297
+ const providerModels = byProvider.get(providerNames[providerIdx]);
1298
+ const modelChoices = providerModels.map((m) => m.id);
1299
+ modelChoices.push("\u2190 Back");
1300
+ const currentModel = currentModels[tier]?.[0] ?? "";
1301
+ const currentIdx = modelChoices.indexOf(currentModel);
1302
+ const modelIdx = await promptChoice(
1303
+ `${tier} model:`,
1304
+ modelChoices,
1305
+ currentIdx >= 0 ? currentIdx : 0
1306
+ );
1307
+ if (modelIdx >= modelChoices.length - 1) return;
1308
+ const selectedModel = modelChoices[modelIdx];
1309
+ const newModels = { ...currentModels, [tier]: [selectedModel] };
1310
+ writePluginOption(configPath, "models", newModels, { dryRun: false });
1311
+ ok2(`${tier} \u2192 ${selectedModel}`);
1383
1312
  }
1384
-
1385
- // src/autopilot/cli.ts
1386
- import { command, option, positional, string as stringType, optional, number as numberType } from "cmd-ts";
1387
-
1388
- // src/autopilot/loop.ts
1389
- import { execFile as execFileCb } from "child_process";
1390
- import { promisify as promisify2 } from "util";
1391
- import { readFileSync as readFileSync6 } from "fs";
1392
- import { join as join8 } from "path";
1393
-
1394
- // src/lib/opencode-server.ts
1395
- import { execFile as execFile2 } from "child_process";
1396
- import { promisify } from "util";
1397
- import {
1398
- createOpencodeServer,
1399
- createOpencodeClient
1400
- } from "@opencode-ai/sdk";
1401
- var execFileP2 = promisify(execFile2);
1402
- var DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
1403
- async function ensureOpencodeOnPath() {
1404
- try {
1405
- await execFileP2("opencode", ["--version"]);
1406
- } catch {
1407
- throw new Error(
1408
- "opencode CLI not found on PATH.\n Install: https://opencode.ai\n Or: bunx opencode upgrade"
1409
- );
1313
+ async function configureNotifications(configPath, currentNotifyUrl) {
1314
+ console.log(`
1315
+ ${c2.bold}Notifications configuration:${c2.reset}
1316
+ `);
1317
+ if (currentNotifyUrl) {
1318
+ console.log(` Current webhook URL: ${c2.cyan}${currentNotifyUrl}${c2.reset}
1319
+ `);
1320
+ } else {
1321
+ console.log(` ${c2.dim}No webhook URL configured.${c2.reset}
1322
+ `);
1410
1323
  }
1411
- }
1412
- async function startServer(opts) {
1413
- await ensureOpencodeOnPath();
1414
- const timeoutMs = opts.timeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
1415
- const port = opts.port ?? 0;
1416
- const server = await createOpencodeServer({
1417
- port,
1418
- timeout: timeoutMs
1419
- });
1420
- const client = createOpencodeClient({ url: server.url });
1421
- let shutdownCalled = false;
1422
- const shutdown = async () => {
1423
- if (shutdownCalled) return;
1424
- shutdownCalled = true;
1425
- try {
1426
- await server.close();
1427
- } catch {
1428
- }
1429
- };
1430
- return { url: server.url, client, shutdown };
1431
- }
1432
- async function createSession(client, opts) {
1433
- const session = await client.session.create({
1434
- body: {
1435
- directory: opts.cwd,
1436
- ...opts.agentName ? { agentID: opts.agentName } : {}
1437
- }
1438
- });
1439
- return session.id;
1440
- }
1441
- async function sendAndWait(client, opts) {
1442
- const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
1443
- await client.session.chat({
1444
- sessionID: opts.sessionId,
1445
- body: { content: [{ type: "text", text: opts.message }] }
1446
- });
1447
- return waitForIdle(client, {
1448
- sessionId: opts.sessionId,
1449
- stallMs,
1450
- abortSignal: opts.abortSignal
1451
- });
1452
- }
1453
- async function waitForIdle(client, opts) {
1454
- const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
1455
- return new Promise((resolve2) => {
1456
- let stallTimer = null;
1457
- let unsubscribe = null;
1458
- let settled = false;
1459
- const settle = (result) => {
1460
- if (settled) return;
1461
- settled = true;
1462
- if (stallTimer) clearTimeout(stallTimer);
1463
- if (unsubscribe) unsubscribe();
1464
- resolve2(result);
1465
- };
1466
- const resetStall = () => {
1467
- if (stallTimer) clearTimeout(stallTimer);
1468
- stallTimer = setTimeout(() => settle({ kind: "stall", stallMs }), stallMs);
1469
- };
1470
- if (opts.abortSignal) {
1471
- if (opts.abortSignal.aborted) {
1472
- settle({ kind: "abort" });
1473
- return;
1474
- }
1475
- opts.abortSignal.addEventListener("abort", () => settle({ kind: "abort" }), { once: true });
1476
- }
1477
- resetStall();
1478
- const stream = client.event.subscribe();
1479
- let streamDone = false;
1480
- (async () => {
1481
- try {
1482
- for await (const event of stream) {
1483
- if (settled) break;
1484
- const props = event.properties ?? {};
1485
- const eventSessionId = props["sessionID"];
1486
- if (eventSessionId !== opts.sessionId) continue;
1487
- resetStall();
1488
- const type = event.type ?? "";
1489
- if (type === "session.idle") {
1490
- settle({ kind: "idle" });
1491
- break;
1492
- }
1493
- if (type === "session.error") {
1494
- const msg = props["message"] ?? "session error";
1495
- settle({ kind: "error", message: msg });
1496
- break;
1497
- }
1498
- }
1499
- } catch (err) {
1500
- if (!settled) {
1501
- settle({ kind: "error", message: err instanceof Error ? err.message : String(err) });
1502
- }
1503
- } finally {
1504
- streamDone = true;
1505
- }
1506
- })();
1507
- unsubscribe = () => {
1508
- };
1324
+ const choices = [
1325
+ "Set Slack incoming webhook URL",
1326
+ "Set custom webhook URL",
1327
+ "Clear webhook URL",
1328
+ "\u2190 Back"
1329
+ ];
1330
+ const choice = await promptChoice("Notifications:", choices, choices.length - 1);
1331
+ if (choice === choices.length - 1) return currentNotifyUrl;
1332
+ if (choice === 2) {
1333
+ writeNotifyUrl(configPath, void 0);
1334
+ ok2("Webhook URL cleared.");
1335
+ return void 0;
1336
+ }
1337
+ const { input } = await import("@inquirer/prompts");
1338
+ const prompt = choice === 0 ? " Slack incoming webhook URL (https://hooks.slack.com/...):" : " Webhook URL:";
1339
+ const url = await input({
1340
+ message: prompt,
1341
+ default: currentNotifyUrl ?? ""
1509
1342
  });
1510
- }
1511
- async function getLastAssistantMessage(client, sessionId) {
1512
- try {
1513
- const messages = await client.session.messages({ path: { id: sessionId } });
1514
- const assistantMessages = messages.filter((m) => m.info.role === "assistant");
1515
- if (assistantMessages.length === 0) return "";
1516
- const last = assistantMessages[assistantMessages.length - 1];
1517
- return last.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
1518
- } catch {
1519
- return "";
1343
+ if (url) {
1344
+ writeNotifyUrl(configPath, url);
1345
+ ok2(`Webhook URL set: ${url}`);
1346
+ return url;
1520
1347
  }
1348
+ return currentNotifyUrl;
1521
1349
  }
1522
-
1523
- // src/autopilot/config.ts
1524
- var MAX_ITERATIONS = 50;
1525
- var STRUGGLE_THRESHOLD = 3;
1526
- var TIMEOUT_MS = 4 * 60 * 60 * 1e3;
1527
- var STALL_MS = 60 * 60 * 1e3;
1528
- var KILL_SWITCH_PATH = ".agent/autopilot-disable";
1529
- var SENTINEL_TAG = "<autopilot-done>";
1530
-
1531
- // src/autopilot/sentinel.ts
1532
- function detectSentinel(text) {
1533
- if (!text.includes(SENTINEL_TAG)) {
1534
- return false;
1535
- }
1536
- const withoutFences = text.replace(/```[\s\S]*?```/g, "");
1537
- const withoutInline = withoutFences.replace(/`[^`\n]*`/g, "");
1538
- return withoutInline.includes(SENTINEL_TAG);
1539
- }
1540
-
1541
- // src/autopilot/struggle.ts
1542
- import * as fs7 from "fs";
1543
- import * as path7 from "path";
1544
- var StruggleDetector = class {
1545
- _consecutiveStalls = 0;
1546
- _threshold;
1547
- constructor(threshold) {
1548
- this._threshold = threshold;
1549
- }
1550
- /** Number of consecutive stall iterations recorded so far. */
1551
- get consecutiveStalls() {
1552
- return this._consecutiveStalls;
1553
- }
1554
- /**
1555
- * Record the result of one iteration.
1556
- * @param madeProgress - true if the agent made filesystem changes this iteration.
1557
- */
1558
- record(madeProgress) {
1559
- if (madeProgress) {
1560
- this._consecutiveStalls = 0;
1350
+ function writeNotifyUrl(configPath, url) {
1351
+ try {
1352
+ if (!fs5.existsSync(configPath)) return;
1353
+ const raw = fs5.readFileSync(configPath, "utf8");
1354
+ const config = JSON.parse(raw);
1355
+ if (!Array.isArray(config.plugin)) return;
1356
+ const pluginIdx = config.plugin.findIndex((entry) => {
1357
+ const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : null;
1358
+ return name === PLUGIN_NAME4 || String(name ?? "").startsWith(`${PLUGIN_NAME4}@`) || String(name ?? "").includes("harness-opencode");
1359
+ });
1360
+ if (pluginIdx < 0) return;
1361
+ const current = config.plugin[pluginIdx];
1362
+ const pluginName2 = Array.isArray(current) ? current[0] : current;
1363
+ const existingOpts = Array.isArray(current) && current.length >= 2 ? { ...current[1] } : {};
1364
+ if (url === void 0) {
1365
+ delete existingOpts.notifyUrl;
1561
1366
  } else {
1562
- this._consecutiveStalls++;
1367
+ existingOpts.notifyUrl = url;
1563
1368
  }
1369
+ config.plugin[pluginIdx] = [pluginName2, existingOpts];
1370
+ const bakPath = `${configPath}.bak.${Date.now()}-${process.pid}`;
1371
+ fs5.copyFileSync(configPath, bakPath);
1372
+ fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
1373
+ } catch (err) {
1374
+ const msg = err instanceof Error ? err.message : String(err);
1375
+ process.stderr.write(`\x1B[33m\u26A0 Failed to write notifyUrl: ${msg}\x1B[0m
1376
+ `);
1564
1377
  }
1565
- /**
1566
- * Returns true if the agent has stalled for `threshold` consecutive
1567
- * iterations without making progress.
1568
- */
1569
- isStruggling() {
1570
- return this._consecutiveStalls >= this._threshold;
1571
- }
1572
- };
1573
- function checkKillSwitch(cwd) {
1574
- const killSwitchFile = path7.join(cwd, KILL_SWITCH_PATH);
1575
- return fs7.existsSync(killSwitchFile);
1576
1378
  }
1577
-
1578
- // src/autopilot/loop.ts
1579
- var execFile3 = promisify2(execFileCb);
1580
- function buildFullPrompt(userPrompt) {
1581
- const candidates = [
1582
- join8(import.meta.dir, "prompt-template.md"),
1583
- join8(import.meta.dir, "..", "..", "src", "autopilot", "prompt-template.md")
1584
- ];
1585
- let template = "";
1586
- for (const candidate of candidates) {
1587
- try {
1588
- const raw = readFileSync6(candidate, "utf8");
1589
- template = raw.replace(/^---\n[\s\S]*?\n---\n/, "");
1590
- break;
1591
- } catch {
1379
+ var configureCmd = command({
1380
+ name: "configure",
1381
+ description: "Interactively edit opencode.json settings \u2014 models, MCPs, plugin add-ons.",
1382
+ args: {},
1383
+ handler: async () => {
1384
+ const configPath = getOpencodeConfigPath4();
1385
+ const config = readConfig(configPath);
1386
+ if (!config) {
1387
+ console.log(`No config found at ${configPath}. Run ${c2.green}glrs oc install${c2.reset} first.`);
1388
+ process.exit(1);
1592
1389
  }
1593
- }
1594
- const withArgs = template.replace("$ARGUMENTS", userPrompt);
1595
- return withArgs || userPrompt;
1596
- }
1597
- async function checkProgress(cwd, baseRef) {
1598
- try {
1599
- const { stdout } = await execFile3("git", ["diff", "--stat", baseRef], { cwd });
1600
- return stdout.trim().length > 0;
1601
- } catch {
1602
- return true;
1603
- }
1604
- }
1605
- async function getHeadSha(cwd) {
1606
- try {
1607
- const { stdout } = await execFile3("git", ["rev-parse", "HEAD"], { cwd });
1608
- return stdout.trim();
1609
- } catch {
1610
- return "HEAD";
1611
- }
1612
- }
1613
- async function runRalphLoop(opts) {
1614
- const maxIterations = opts.maxIterations ?? MAX_ITERATIONS;
1615
- const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
1616
- const stallMs = opts.stallMs ?? STALL_MS;
1617
- const struggleThreshold = opts.struggleThreshold ?? STRUGGLE_THRESHOLD;
1618
- const _startServer = opts._deps?.startServer ?? startServer;
1619
- const _createSession = opts._deps?.createSession ?? createSession;
1620
- const _sendAndWait = opts._deps?.sendAndWait ?? sendAndWait;
1621
- const _getLastAssistantMessage = opts._deps?.getLastAssistantMessage ?? getLastAssistantMessage;
1622
- const fullPrompt = buildFullPrompt(opts.prompt);
1623
- const struggle = new StruggleDetector(struggleThreshold);
1624
- const startTime = Date.now();
1625
- const server = await _startServer({ cwd: opts.cwd });
1626
- const abort = new AbortController();
1627
- const timeoutHandle = setTimeout(() => {
1628
- abort.abort();
1629
- }, timeoutMs);
1630
- try {
1631
- const sessionId = await _createSession(server.client, {
1632
- cwd: opts.cwd,
1633
- agentName: "prime"
1634
- });
1635
- for (let iteration = 1; iteration <= maxIterations; iteration++) {
1636
- if (checkKillSwitch(opts.cwd)) {
1637
- return {
1638
- exitReason: "kill-switch",
1639
- iterations: iteration - 1,
1640
- message: `Kill switch active (.agent/autopilot-disable exists). Stopping after ${iteration - 1} iteration(s).`
1641
- };
1642
- }
1643
- if (Date.now() - startTime >= timeoutMs) {
1644
- return {
1645
- exitReason: "timeout",
1646
- iterations: iteration - 1,
1647
- message: `Total timeout (${timeoutMs}ms) exceeded after ${iteration - 1} iteration(s).`
1648
- };
1649
- }
1650
- const headBefore = await getHeadSha(opts.cwd);
1651
- const result = await _sendAndWait(server.client, {
1652
- sessionId,
1653
- message: fullPrompt,
1654
- stallMs,
1655
- abortSignal: abort.signal
1656
- });
1657
- if (result.kind === "abort") {
1658
- return {
1659
- exitReason: "timeout",
1660
- iterations: iteration,
1661
- message: `Aborted after ${iteration} iteration(s) (total timeout exceeded).`
1662
- };
1663
- }
1664
- if (result.kind === "stall") {
1665
- return {
1666
- exitReason: "stall",
1667
- iterations: iteration,
1668
- message: `Iteration ${iteration} stalled for ${result.stallMs}ms with no idle signal.`
1669
- };
1390
+ const opts = extractPluginOptions2(config);
1391
+ const models = opts?.models ?? {};
1392
+ let notifyUrl = opts?.notifyUrl;
1393
+ console.log(`
1394
+ ${c2.bold}${c2.blue}glrs oc configure${c2.reset}
1395
+ `);
1396
+ while (true) {
1397
+ const deepModel = models.deep?.[0] ?? "(not set)";
1398
+ const midModel = models.mid?.[0] ?? "(not set)";
1399
+ const midExecModel = models["mid-execute"]?.[0] ?? "(not set)";
1400
+ const autopilotExecModel = models["autopilot-execute"]?.[0] ?? `(falls back to ${midExecModel})`;
1401
+ const fastModel = models.fast?.[0] ?? "(not set)";
1402
+ const mcpEnabled = Object.entries(config.mcp ?? {}).filter(([, v]) => v?.enabled).map(([k]) => k);
1403
+ const slackConfigured = notifyUrl?.includes("hooks.slack.com/") ?? false;
1404
+ const notifyLabel = notifyUrl ? slackConfigured ? "Slack" : "custom webhook" : "none";
1405
+ const sections = [
1406
+ `Models \u2014 deep: ${deepModel.split("/").pop()}, autopilot --fast: ${autopilotExecModel.split("/").pop()}`,
1407
+ `MCPs \u2014 ${mcpEnabled.length > 0 ? mcpEnabled.join(", ") : "none"}`,
1408
+ `Notifications \u2014 ${notifyLabel}`,
1409
+ "Done"
1410
+ ];
1411
+ const choice = await promptChoice("What to configure?", sections, sections.length - 1);
1412
+ if (choice === sections.length - 1) {
1413
+ console.log(`
1414
+ ${c2.bold}Done.${c2.reset} Restart opencode to pick up changes.
1415
+ `);
1416
+ break;
1670
1417
  }
1671
- if (result.kind === "error") {
1672
- return {
1673
- exitReason: "error",
1674
- iterations: iteration,
1675
- message: `Error in iteration ${iteration}: ${result.message}`
1676
- };
1418
+ if (choice === 0) {
1419
+ await configureModels(configPath, models);
1420
+ const updated = readConfig(configPath);
1421
+ if (updated) {
1422
+ const updatedOpts = extractPluginOptions2(updated);
1423
+ if (updatedOpts?.models) {
1424
+ Object.assign(models, updatedOpts.models);
1425
+ }
1426
+ }
1677
1427
  }
1678
- const lastMessage = await _getLastAssistantMessage(server.client, sessionId);
1679
- if (detectSentinel(lastMessage)) {
1680
- return {
1681
- exitReason: "sentinel",
1682
- iterations: iteration,
1683
- message: `Agent emitted <autopilot-done> at iteration ${iteration}.`
1684
- };
1428
+ if (choice === 1) {
1429
+ const { promptMulti: promptMulti2 } = await import("./plugin-check-GJRD2OK6.js");
1430
+ const MCP_TOGGLES2 = [
1431
+ { name: "playwright", label: "Playwright \u2014 browser automation" },
1432
+ { name: "linear", label: "Linear \u2014 issue tracker" }
1433
+ ];
1434
+ const currentMcps = new Set(
1435
+ Object.entries(config.mcp ?? {}).filter(([, v]) => v?.enabled).map(([k]) => k)
1436
+ );
1437
+ const selected = await promptMulti2(
1438
+ "Enable MCPs:",
1439
+ MCP_TOGGLES2.map((t) => ({ label: t.label, defaultOn: currentMcps.has(t.name) }))
1440
+ );
1441
+ const newEnabled = new Set([...selected].map((i) => MCP_TOGGLES2[i].name));
1442
+ writeMcpToggles(configPath, newEnabled, { dryRun: false });
1685
1443
  }
1686
- const madeProgress = await checkProgress(opts.cwd, headBefore);
1687
- struggle.record(madeProgress);
1688
- if (struggle.isStruggling()) {
1689
- return {
1690
- exitReason: "struggle",
1691
- iterations: iteration,
1692
- message: `Agent made no filesystem progress for ${struggleThreshold} consecutive iteration(s). Stopping at iteration ${iteration}.`
1693
- };
1444
+ if (choice === 2) {
1445
+ notifyUrl = await configureNotifications(configPath, notifyUrl);
1694
1446
  }
1695
1447
  }
1696
- return {
1697
- exitReason: "max-iterations",
1698
- iterations: maxIterations,
1699
- message: `Reached maximum iterations (${maxIterations}). Stopping.`
1700
- };
1701
- } finally {
1702
- clearTimeout(timeoutHandle);
1703
- await server.shutdown();
1704
- }
1705
- }
1706
-
1707
- // src/autopilot/cli.ts
1708
- var autopilotCmd = command({
1709
- name: "autopilot",
1710
- description: "Run the Ralph loop: send a prompt to PRIME repeatedly until it emits <autopilot-done> or a budget is exhausted.",
1711
- args: {
1712
- prompt: positional({
1713
- type: stringType,
1714
- displayName: "prompt",
1715
- description: "The prompt to send to PRIME each iteration (e.g. a Linear issue ref or free-form task)."
1716
- }),
1717
- maxIterations: option({
1718
- long: "max-iterations",
1719
- type: optional(numberType),
1720
- description: `Maximum number of loop iterations (default: ${MAX_ITERATIONS}).`
1721
- }),
1722
- timeout: option({
1723
- long: "timeout",
1724
- type: optional(numberType),
1725
- description: `Total wall-clock timeout in milliseconds (default: ${TIMEOUT_MS} = 4 hours).`
1726
- })
1727
- },
1728
- handler: async ({ prompt, maxIterations, timeout }) => {
1729
- const cwd = process.cwd();
1730
- process.stdout.write("\n\x1B[1mAutopilot \u2014 Ralph loop\x1B[0m\n");
1731
- process.stdout.write(`Prompt: ${prompt.slice(0, 80)}${prompt.length > 80 ? "..." : ""}
1732
- `);
1733
- process.stdout.write(`Max iterations: ${maxIterations ?? MAX_ITERATIONS}
1734
- `);
1735
- process.stdout.write(`Timeout: ${((timeout ?? TIMEOUT_MS) / 36e5).toFixed(1)}h
1736
-
1737
- `);
1738
- const result = await runRalphLoop({
1739
- prompt,
1740
- cwd,
1741
- maxIterations: maxIterations ?? void 0,
1742
- timeoutMs: timeout ?? void 0
1743
- });
1744
- const icon = result.exitReason === "sentinel" ? "\x1B[32m\u2713\x1B[0m" : result.exitReason === "kill-switch" ? "\x1B[33m\u2298\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
1745
- process.stdout.write(`
1746
- ${icon} ${result.message}
1747
- `);
1748
- process.stdout.write(` Iterations: ${result.iterations}
1749
-
1750
- `);
1751
- if (result.exitReason !== "sentinel" && result.exitReason !== "kill-switch") {
1752
- process.exit(1);
1753
- }
1754
- process.exit(0);
1755
1448
  }
1756
1449
  });
1757
1450
 
1758
1451
  // src/cli/cli-update.ts
1759
- import * as fs8 from "fs";
1760
- import * as path8 from "path";
1761
- import * as os6 from "os";
1452
+ import * as fs6 from "fs";
1453
+ import * as path6 from "path";
1454
+ import * as os5 from "os";
1762
1455
  import { spawn } from "child_process";
1763
- import { fileURLToPath as fileURLToPath3 } from "url";
1456
+ import { fileURLToPath as fileURLToPath2 } from "url";
1764
1457
  var PACKAGE_NAME = "@glrs-dev/harness-plugin-opencode";
1765
1458
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1766
- var c2 = {
1459
+ var c3 = {
1767
1460
  reset: "\x1B[0m",
1768
1461
  green: "\x1B[32m",
1769
1462
  yellow: "\x1B[33m",
@@ -1785,12 +1478,12 @@ function isMajorBump(current, latest) {
1785
1478
  return latest.major > current.major;
1786
1479
  }
1787
1480
  function getStateFilePath() {
1788
- const cacheHome = process.env["XDG_CACHE_HOME"] ?? path8.join(os6.homedir(), ".cache");
1789
- return path8.join(cacheHome, "harness-opencode", "cli-update.json");
1481
+ const cacheHome = process.env["XDG_CACHE_HOME"] ?? path6.join(os5.homedir(), ".cache");
1482
+ return path6.join(cacheHome, "harness-opencode", "cli-update.json");
1790
1483
  }
1791
1484
  function readState() {
1792
1485
  try {
1793
- const raw = fs8.readFileSync(getStateFilePath(), "utf8");
1486
+ const raw = fs6.readFileSync(getStateFilePath(), "utf8");
1794
1487
  return JSON.parse(raw);
1795
1488
  } catch {
1796
1489
  return null;
@@ -1799,21 +1492,21 @@ function readState() {
1799
1492
  function writeState(state) {
1800
1493
  try {
1801
1494
  const statePath = getStateFilePath();
1802
- fs8.mkdirSync(path8.dirname(statePath), { recursive: true });
1803
- fs8.writeFileSync(statePath, JSON.stringify(state));
1495
+ fs6.mkdirSync(path6.dirname(statePath), { recursive: true });
1496
+ fs6.writeFileSync(statePath, JSON.stringify(state));
1804
1497
  } catch {
1805
1498
  }
1806
1499
  }
1807
1500
  function readInstalledVersion() {
1808
- const here = path8.dirname(fileURLToPath3(import.meta.url));
1501
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
1809
1502
  const candidates = [
1810
- path8.join(here, "..", "package.json"),
1811
- path8.join(here, "..", "..", "package.json"),
1812
- path8.join(here, "package.json")
1503
+ path6.join(here, "..", "package.json"),
1504
+ path6.join(here, "..", "..", "package.json"),
1505
+ path6.join(here, "package.json")
1813
1506
  ];
1814
1507
  for (const candidate of candidates) {
1815
1508
  try {
1816
- const raw = fs8.readFileSync(candidate, "utf8");
1509
+ const raw = fs6.readFileSync(candidate, "utf8");
1817
1510
  const parsed = JSON.parse(raw);
1818
1511
  if (parsed.name === PACKAGE_NAME && typeof parsed.version === "string") {
1819
1512
  return parsed.version;
@@ -1884,7 +1577,7 @@ function startUpdateCheck() {
1884
1577
  action = () => {
1885
1578
  process.stderr.write(
1886
1579
  `
1887
- ${c2.blue}\u2022${c2.reset} Updating ${PACKAGE_NAME} ${c2.dim}${currentVersionStr}${c2.reset} \u2192 ${c2.green}${latestStr}${c2.reset} in the background...
1580
+ ${c3.blue}\u2022${c3.reset} Updating ${PACKAGE_NAME} ${c3.dim}${currentVersionStr}${c3.reset} \u2192 ${c3.green}${latestStr}${c3.reset} in the background...
1888
1581
  `
1889
1582
  );
1890
1583
  spawnBackgroundUpdate();
@@ -1899,8 +1592,8 @@ ${c2.blue}\u2022${c2.reset} Updating ${PACKAGE_NAME} ${c2.dim}${currentVersionSt
1899
1592
  function printMajorNotice(current, latest) {
1900
1593
  process.stderr.write(
1901
1594
  `
1902
- ${c2.yellow}${c2.bold}Major update available:${c2.reset} ${current} \u2192 ${c2.green}${latest}${c2.reset}
1903
- ${c2.dim}Review the changelog before upgrading:${c2.reset}
1595
+ ${c3.yellow}${c3.bold}Major update available:${c3.reset} ${current} \u2192 ${c3.green}${latest}${c3.reset}
1596
+ ${c3.dim}Review the changelog before upgrading:${c3.reset}
1904
1597
  bun update -g ${PACKAGE_NAME}
1905
1598
  `
1906
1599
  );
@@ -1977,57 +1670,6 @@ var doctorCmd = command2({
1977
1670
  doctor();
1978
1671
  }
1979
1672
  });
1980
- var planCheckCmd = command2({
1981
- name: "plan-check",
1982
- description: "Parse a plan file's plan-state fence (legacy markdown plans).",
1983
- args: {
1984
- run: option2({
1985
- long: "run",
1986
- type: optional2(string),
1987
- description: "Print verify commands for pending items, one per line."
1988
- }),
1989
- check: option2({
1990
- long: "check",
1991
- type: optional2(string),
1992
- description: "Structural validation; exits 1 if any item is invalid."
1993
- }),
1994
- rest: restPositionals({
1995
- type: string,
1996
- displayName: "plan-path",
1997
- description: "Path to a plan markdown file. Required unless --run / --check is given."
1998
- })
1999
- },
2000
- handler: ({ run: run2, check, rest }) => {
2001
- const legacy = [];
2002
- if (run2 !== void 0) {
2003
- legacy.push("--run", run2);
2004
- } else if (check !== void 0) {
2005
- legacy.push("--check", check);
2006
- } else {
2007
- legacy.push(...rest);
2008
- }
2009
- planCheck(legacy);
2010
- }
2011
- });
2012
- var planDirCmd = command2({
2013
- name: "plan-dir",
2014
- description: "Print the repo-shared plan directory for the current worktree (resolves + creates + migrates legacy).",
2015
- args: {},
2016
- handler: async () => {
2017
- try {
2018
- const cwd = process.cwd();
2019
- const planDir = await getPlanDir(cwd);
2020
- await migratePlans(cwd, planDir);
2021
- process.stdout.write(planDir + "\n");
2022
- process.exit(0);
2023
- } catch (err) {
2024
- const msg = err instanceof Error ? err.message : String(err);
2025
- process.stderr.write(`plan-dir: ${msg}
2026
- `);
2027
- process.exit(1);
2028
- }
2029
- }
2030
- });
2031
1673
  var installPluginCmd = command2({
2032
1674
  name: "install-plugin",
2033
1675
  description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
@@ -2053,10 +1695,9 @@ var cli = subcommands({
2053
1695
  "install-plugin": installPluginCmd,
2054
1696
  install: installCmd,
2055
1697
  uninstall: uninstallCmd,
2056
- doctor: doctorCmd,
2057
- "plan-check": planCheckCmd,
2058
- "plan-dir": planDirCmd,
2059
- autopilot: autopilotCmd
1698
+ configure: configureCmd,
1699
+ doctor: doctorCmd
1700
+ // Note: `loop` and `autopilot` commands have moved to @glrs-dev/cli.
2060
1701
  }
2061
1702
  });
2062
1703
  var printUpdate = startUpdateCheck();