@glrs-dev/harness-plugin-opencode 2.4.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  // src/config-hook.ts
10
10
  import * as fs from "fs";
11
- import * as path from "path";
11
+ import * as path2 from "path";
12
12
  import * as os from "os";
13
13
 
14
14
  // src/agents/shared/index.ts
@@ -39,15 +39,16 @@ var UI_EVALUATION_LADDER = readMd("ui-evaluation-ladder.md");
39
39
  // src/agents/index.ts
40
40
  import { readFileSync as readFileSync2 } from "fs";
41
41
  import { fileURLToPath as fileURLToPath2 } from "url";
42
- import { dirname as dirname2, join as join2 } from "path";
42
+ import * as path from "path";
43
+ import { dirname as dirname2, join as join3 } from "path";
43
44
  var HERE2 = dirname2(fileURLToPath2(import.meta.url));
44
45
  function readPrompt(name) {
45
46
  const candidates = [
46
- join2(HERE2, "prompts", name),
47
+ join3(HERE2, "prompts", name),
47
48
  // dev: src/agents/prompts/
48
- join2(HERE2, "agents", "prompts", name),
49
+ join3(HERE2, "agents", "prompts", name),
49
50
  // dist: dist/ → dist/agents/prompts/
50
- join2(HERE2, "..", "..", "src", "agents", "prompts", name)
51
+ join3(HERE2, "..", "..", "src", "agents", "prompts", name)
51
52
  // fallback dev
52
53
  ];
53
54
  for (const p of candidates) {
@@ -216,9 +217,8 @@ var CORE_BASH_ALLOW_LIST = {
216
217
  "eslint *": "allow",
217
218
  "prettier *": "allow",
218
219
  "biome *": "allow",
219
- // Our own CLI (install, doctor, autopilot, etc.) — reviewer/build invocations.
220
- "bunx @glrs-dev/harness-plugin-opencode *": "allow",
221
- "glrs-oc *": "allow",
220
+ // Our own CLI (harness, autopilot, loop, etc.) — reviewer/build invocations.
221
+ "glrs *": "allow",
222
222
  // GitHub CLI — read-only gh calls are fine; destructive `gh pr merge`
223
223
  // is gated at the PRIME level by human intent (user runs /ship).
224
224
  "gh pr view *": "allow",
@@ -244,6 +244,18 @@ var CORE_DESTRUCTIVE_BASH_DENIES = {
244
244
  "git push --force-with-lease*": "allow",
245
245
  "git push * --force-with-lease*": "allow"
246
246
  };
247
+ var AUTOPILOT_BASH_DENIES = {
248
+ ...CORE_DESTRUCTIVE_BASH_DENIES,
249
+ "git checkout *": "deny",
250
+ "git switch *": "deny",
251
+ "git push*": "deny",
252
+ "gh pr *": "deny",
253
+ // Allow checkout of individual files (git checkout <ref> -- <path>)
254
+ "git checkout * -- *": "allow",
255
+ // Allow git checkout for creating new branches from current HEAD only
256
+ // (but NOT switching to existing branches)
257
+ "git checkout -b *": "allow"
258
+ };
247
259
  var PRIME_PERMISSIONS = {
248
260
  edit: "allow",
249
261
  bash: {
@@ -337,47 +349,7 @@ var BUILD_PERMISSIONS = {
337
349
  playwright: "allow",
338
350
  linear: "allow"
339
351
  };
340
- var SPEC_REVIEWER_PERMISSIONS = {
341
- edit: "deny",
342
- bash: {
343
- "*": "allow",
344
- ...CORE_BASH_ALLOW_LIST,
345
- ...CORE_DESTRUCTIVE_BASH_DENIES
346
- },
347
- webfetch: "deny",
348
- ast_grep: "allow",
349
- tsc_check: "allow",
350
- eslint_check: "allow",
351
- todo_scan: "allow",
352
- comment_check: "allow",
353
- question: "allow",
354
- serena: "allow",
355
- memory: "deny",
356
- git: "allow",
357
- playwright: "allow",
358
- linear: "deny"
359
- };
360
- var CODE_REVIEWER_PERMISSIONS = {
361
- edit: "deny",
362
- bash: {
363
- "*": "allow",
364
- ...CORE_BASH_ALLOW_LIST,
365
- ...CORE_DESTRUCTIVE_BASH_DENIES
366
- },
367
- webfetch: "deny",
368
- ast_grep: "allow",
369
- tsc_check: "allow",
370
- eslint_check: "allow",
371
- todo_scan: "allow",
372
- comment_check: "allow",
373
- question: "allow",
374
- serena: "allow",
375
- memory: "deny",
376
- git: "allow",
377
- playwright: "allow",
378
- linear: "deny"
379
- };
380
- var CODE_REVIEWER_THOROUGH_PERMISSIONS = {
352
+ var REVIEWER_PERMISSIONS = {
381
353
  edit: "deny",
382
354
  bash: {
383
355
  "*": "allow",
@@ -591,6 +563,10 @@ function createAgents() {
591
563
  temperature: 0.2,
592
564
  permission: {
593
565
  ...PRIME_PERMISSIONS,
566
+ bash: {
567
+ ...typeof PRIME_PERMISSIONS.bash === "object" ? PRIME_PERMISSIONS.bash : { "*": "allow" },
568
+ ...AUTOPILOT_BASH_DENIES
569
+ },
594
570
  question: "deny"
595
571
  },
596
572
  tools: AUTOPILOT_PRIME_DISABLED_TOOLS
@@ -602,6 +578,10 @@ function createAgents() {
602
578
  temperature: 0.1,
603
579
  permission: {
604
580
  ...PRIME_PERMISSIONS,
581
+ bash: {
582
+ ...typeof PRIME_PERMISSIONS.bash === "object" ? PRIME_PERMISSIONS.bash : { "*": "allow" },
583
+ ...AUTOPILOT_BASH_DENIES
584
+ },
605
585
  question: "deny"
606
586
  },
607
587
  tools: AUTOPILOT_PRIME_DISABLED_TOOLS
@@ -628,13 +608,13 @@ function createAgents() {
628
608
  // via overrides (see permission blocks above). docs-maintainer has no
629
609
  // frontmatter permission declaration and keeps that behavior.
630
610
  "spec-reviewer": agentFromPrompt(specReviewerPrompt, {
631
- permission: SPEC_REVIEWER_PERMISSIONS
611
+ permission: REVIEWER_PERMISSIONS
632
612
  }),
633
613
  "code-reviewer": agentFromPrompt(codeReviewerPrompt, {
634
- permission: CODE_REVIEWER_PERMISSIONS
614
+ permission: REVIEWER_PERMISSIONS
635
615
  }),
636
616
  "code-reviewer-thorough": agentFromPrompt(codeReviewerThoroughPrompt, {
637
- permission: CODE_REVIEWER_THOROUGH_PERMISSIONS
617
+ permission: REVIEWER_PERMISSIONS
638
618
  }),
639
619
  "plan-reviewer": agentFromPrompt(planReviewerPrompt, {
640
620
  permission: PLAN_REVIEWER_PERMISSIONS
@@ -700,19 +680,53 @@ function createAgents() {
700
680
  })
701
681
  };
702
682
  }
683
+ function applyAgentOverrides(agents, overrides, repoRoot) {
684
+ if (!overrides) return agents;
685
+ for (const [agentName, override] of Object.entries(overrides)) {
686
+ const agent = agents[agentName];
687
+ if (!agent) {
688
+ console.warn(`applyAgentOverrides: unknown agent "${agentName}" \u2014 ignoring`);
689
+ continue;
690
+ }
691
+ if (override.model !== void 0) {
692
+ agent.model = override.model;
693
+ }
694
+ if (override.prompt !== void 0) {
695
+ if (path.isAbsolute(override.prompt)) {
696
+ throw new Error(
697
+ `applyAgentOverrides: absolute path not allowed for agent "${agentName}"; got "${override.prompt}". Paths must be relative to repo root.`
698
+ );
699
+ }
700
+ const promptPath = path.join(repoRoot, override.prompt);
701
+ try {
702
+ const rawPrompt = readFileSync2(promptPath, "utf8");
703
+ const processedPrompt = injectUIEvaluationLadder(
704
+ injectWorkflowMechanics(rawPrompt)
705
+ );
706
+ agent.prompt = processedPrompt;
707
+ } catch (err) {
708
+ const message = err instanceof Error ? err.message : String(err);
709
+ throw new Error(
710
+ `applyAgentOverrides: failed to read prompt file for agent "${agentName}": "${promptPath}" \u2014 ${message}`
711
+ );
712
+ }
713
+ }
714
+ }
715
+ return agents;
716
+ }
703
717
 
704
718
  // src/commands/index.ts
705
719
  import { readFileSync as readFileSync3 } from "fs";
706
720
  import { fileURLToPath as fileURLToPath3 } from "url";
707
- import { dirname as dirname3, join as join3 } from "path";
721
+ import { dirname as dirname3, join as join4 } from "path";
708
722
  var HERE3 = dirname3(fileURLToPath3(import.meta.url));
709
723
  function readPrompt2(name) {
710
724
  const candidates = [
711
- join3(HERE3, "prompts", name),
725
+ join4(HERE3, "prompts", name),
712
726
  // dev: src/commands/prompts/
713
- join3(HERE3, "commands", "prompts", name),
727
+ join4(HERE3, "commands", "prompts", name),
714
728
  // dist: dist/ → dist/commands/prompts/
715
- join3(HERE3, "..", "..", "src", "commands", "prompts", name)
729
+ join4(HERE3, "..", "..", "src", "commands", "prompts", name)
716
730
  // fallback dev
717
731
  ];
718
732
  for (const p of candidates) {
@@ -808,19 +822,19 @@ function createMcpConfig() {
808
822
 
809
823
  // src/skills/paths.ts
810
824
  import { fileURLToPath as fileURLToPath4 } from "url";
811
- import { dirname as dirname4, join as join4 } from "path";
825
+ import { dirname as dirname4, join as join5 } from "path";
812
826
  function getSkillsRoot() {
813
827
  const here = dirname4(fileURLToPath4(import.meta.url));
814
- return join4(here, "skills");
828
+ return join5(here, "skills");
815
829
  }
816
830
 
817
831
  // src/config-hook.ts
818
832
  function writePermDebugSnapshot(config) {
819
833
  if (process.env["HARNESS_OPENCODE_PERM_DEBUG"] !== "1") return;
820
834
  try {
821
- const stateDir = process.env["XDG_STATE_HOME"] || path.join(os.homedir(), ".local", "state");
822
- const targetDir = path.join(stateDir, "harness-opencode");
823
- const targetFile = path.join(targetDir, "perm-debug.json");
835
+ const stateDir = process.env["XDG_STATE_HOME"] || path2.join(os.homedir(), ".local", "state");
836
+ const targetDir = path2.join(stateDir, "harness-opencode");
837
+ const targetFile = path2.join(targetDir, "perm-debug.json");
824
838
  const version = readOurPackageVersion(import.meta.url);
825
839
  const agentBlock = config.agent ?? {};
826
840
  const agentPerms = {};
@@ -893,6 +907,21 @@ function resolveHarnessModels(agents, config, pluginOptions) {
893
907
  function applyConfig(config, pluginOptions) {
894
908
  const ourAgents = createAgents();
895
909
  resolveHarnessModels(ourAgents, config, pluginOptions);
910
+ const agentOverrides = pluginOptions?.agents;
911
+ if (agentOverrides) {
912
+ applyAgentOverrides(ourAgents, agentOverrides, process.cwd());
913
+ }
914
+ const envOverrides = process.env["GLRS_AGENT_OVERRIDES"];
915
+ if (envOverrides) {
916
+ try {
917
+ const parsed = JSON.parse(envOverrides);
918
+ if (parsed && typeof parsed === "object") {
919
+ applyAgentOverrides(ourAgents, parsed, process.cwd());
920
+ }
921
+ } catch {
922
+ console.warn(`Failed to parse GLRS_AGENT_OVERRIDES env var: ignoring`);
923
+ }
924
+ }
896
925
  config.agent = { ...ourAgents, ...config.agent ?? {} };
897
926
  const ourCommands = createCommands();
898
927
  config.command = {
@@ -1389,7 +1418,7 @@ function createTools() {
1389
1418
 
1390
1419
  // src/plugins/dotenv.ts
1391
1420
  import * as fs2 from "fs";
1392
- import * as path2 from "path";
1421
+ import * as path3 from "path";
1393
1422
  var DOTENV_FILES = [".env", ".env.local"];
1394
1423
  function parseDotenv(content) {
1395
1424
  const out = {};
@@ -1422,7 +1451,7 @@ function loadDotenv(directory) {
1422
1451
  const merged = {};
1423
1452
  const filesLoaded = [];
1424
1453
  for (const name of DOTENV_FILES) {
1425
- const filePath = path2.join(directory, name);
1454
+ const filePath = path3.join(directory, name);
1426
1455
  let content;
1427
1456
  try {
1428
1457
  content = fs2.readFileSync(filePath, "utf8");
@@ -1477,7 +1506,7 @@ var notify_default = plugin;
1477
1506
 
1478
1507
  // src/plugins/cost-tracker.ts
1479
1508
  import * as fs3 from "fs/promises";
1480
- import * as path3 from "path";
1509
+ import * as path4 from "path";
1481
1510
  import * as os2 from "os";
1482
1511
  var MAX_LINE_BYTES = 2048;
1483
1512
  var ROLLUP_DEBOUNCE_MS = 5e3;
@@ -1485,11 +1514,11 @@ function resolveDataDir() {
1485
1514
  const override = process.env.GLORIOUS_COST_TRACKER_DIR;
1486
1515
  if (override) {
1487
1516
  if (override.startsWith("~")) {
1488
- return path3.join(os2.homedir(), override.slice(1));
1517
+ return path4.join(os2.homedir(), override.slice(1));
1489
1518
  }
1490
1519
  return override;
1491
1520
  }
1492
- return path3.join(os2.homedir(), ".glorious", "opencode");
1521
+ return path4.join(os2.homedir(), ".glorious", "opencode");
1493
1522
  }
1494
1523
  function zeroTokens() {
1495
1524
  return {
@@ -1561,8 +1590,8 @@ var plugin2 = async () => {
1561
1590
  return {};
1562
1591
  }
1563
1592
  const dataDir = resolveDataDir();
1564
- const jsonlPath = path3.join(dataDir, "costs.jsonl");
1565
- const rollupPath = path3.join(dataDir, "costs.json");
1593
+ const jsonlPath = path4.join(dataDir, "costs.jsonl");
1594
+ const rollupPath = path4.join(dataDir, "costs.json");
1566
1595
  const lastSeen = /* @__PURE__ */ new Map();
1567
1596
  const messageMeta = /* @__PURE__ */ new Map();
1568
1597
  const rollup = emptyRollup();
@@ -1799,7 +1828,7 @@ var cost_tracker_default = plugin2;
1799
1828
  // src/plugins/tool-hooks.ts
1800
1829
  import * as crypto from "crypto";
1801
1830
  import * as fs4 from "fs";
1802
- import * as path4 from "path";
1831
+ import * as path5 from "path";
1803
1832
  import * as os3 from "os";
1804
1833
  import { execFile as execFileCb } from "child_process";
1805
1834
  import { promisify as promisify6 } from "util";
@@ -1886,8 +1915,8 @@ function resolveConfig(config, pluginOptions) {
1886
1915
  };
1887
1916
  }
1888
1917
  function getToolOutputDir() {
1889
- const stateHome = process.env["XDG_STATE_HOME"] || path4.join(os3.homedir(), ".local", "state");
1890
- return path4.join(stateHome, "harness-opencode", "tool-output");
1918
+ const stateHome = process.env["XDG_STATE_HOME"] || path5.join(os3.homedir(), ".local", "state");
1919
+ return path5.join(stateHome, "harness-opencode", "tool-output");
1891
1920
  }
1892
1921
  function hashContent(content) {
1893
1922
  return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
@@ -1920,9 +1949,9 @@ async function resolveSessionDir(client, sess, sessionID) {
1920
1949
  }
1921
1950
  function isUnderToolOutputDir(filePath) {
1922
1951
  try {
1923
- const abs = path4.resolve(filePath);
1924
- const spillDir = path4.resolve(getToolOutputDir());
1925
- return abs === spillDir || abs.startsWith(spillDir + path4.sep);
1952
+ const abs = path5.resolve(filePath);
1953
+ const spillDir = path5.resolve(getToolOutputDir());
1954
+ return abs === spillDir || abs.startsWith(spillDir + path5.sep);
1926
1955
  } catch {
1927
1956
  return false;
1928
1957
  }
@@ -1957,7 +1986,7 @@ function applyBackpressure(cfg, toolName, callID, output, args) {
1957
1986
  try {
1958
1987
  const dir = getToolOutputDir();
1959
1988
  fs4.mkdirSync(dir, { recursive: true });
1960
- diskPath = path4.join(dir, `${callID}.txt`);
1989
+ diskPath = path5.join(dir, `${callID}.txt`);
1961
1990
  fs4.writeFileSync(diskPath, text);
1962
1991
  } catch {
1963
1992
  }
@@ -2004,7 +2033,7 @@ ${tail}`;
2004
2033
  }
2005
2034
  async function runPostEditVerify(cfg, client, sess, sessionID, filePath, output) {
2006
2035
  if (!cfg.enabled) return;
2007
- const ext = path4.extname(filePath).toLowerCase();
2036
+ const ext = path5.extname(filePath).toLowerCase();
2008
2037
  if (!TS_EXTENSIONS.has(ext)) return;
2009
2038
  const now = Date.now();
2010
2039
  if (now - sess.lastVerifyTs < 2e3) return;
@@ -2037,17 +2066,17 @@ ${String(stderr)}`;
2037
2066
  }
2038
2067
  if (!raw.trim()) return;
2039
2068
  const errors = parseTscOutput(raw);
2040
- const normPath = path4.resolve(cwd, filePath);
2069
+ const normPath = path5.resolve(cwd, filePath);
2041
2070
  const fileErrors = errors.filter((e) => {
2042
- const errPath = path4.isAbsolute(e.file) ? e.file : path4.resolve(cwd, e.file);
2043
- return path4.normalize(errPath) === path4.normalize(normPath);
2071
+ const errPath = path5.isAbsolute(e.file) ? e.file : path5.resolve(cwd, e.file);
2072
+ return path5.normalize(errPath) === path5.normalize(normPath);
2044
2073
  });
2045
2074
  if (fileErrors.length === 0) return;
2046
2075
  const { rows } = dedupeAndCap(fileErrors, VERIFY_MAX_ERRORS);
2047
2076
  const lines = rows.map(formatRow);
2048
2077
  output.output += `
2049
2078
 
2050
- --- POST-EDIT DIAGNOSTICS (${fileErrors.length} error${fileErrors.length !== 1 ? "s" : ""} in ${path4.basename(filePath)}) ---
2079
+ --- POST-EDIT DIAGNOSTICS (${fileErrors.length} error${fileErrors.length !== 1 ? "s" : ""} in ${path5.basename(filePath)}) ---
2051
2080
  ` + lines.join("\n") + `
2052
2081
  --- Fix these before proceeding ---`;
2053
2082
  } catch {
@@ -2061,7 +2090,7 @@ function checkEditLoop(cfg, sess, filePath, output) {
2061
2090
  output.output += `
2062
2091
 
2063
2092
  --- LOOP WARNING ---
2064
- You've edited ${path4.basename(filePath)} ${count} times this session. Consider reconsidering your approach \u2014 are you stuck in a loop? Step back and think about whether a different strategy would be more effective.
2093
+ You've edited ${path5.basename(filePath)} ${count} times this session. Consider reconsidering your approach \u2014 are you stuck in a loop? Step back and think about whether a different strategy would be more effective.
2065
2094
  ---`;
2066
2095
  }
2067
2096
  }
@@ -2121,16 +2150,16 @@ import { extname as extname2 } from "path";
2121
2150
  import { createHash as createHash2, randomUUID } from "crypto";
2122
2151
  import { homedir as homedir4 } from "os";
2123
2152
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync } from "fs";
2124
- import { join as join9 } from "path";
2153
+ import { join as join10 } from "path";
2125
2154
  var APP_KEY = "A-US-3617699429";
2126
2155
  var ENDPOINT = "https://us.aptabase.com/api/v0/event";
2127
2156
  var PKG_NAME = "@glrs-dev/harness-plugin-opencode";
2128
- var PKG_VERSION = true ? "2.4.1" : "dev";
2157
+ var PKG_VERSION = true ? "2.7.0" : "dev";
2129
2158
  var DISABLED = process.env.HARNESS_OPENCODE_TELEMETRY === "0" || process.env.HARNESS_OPENCODE_TELEMETRY === "false" || process.env.DO_NOT_TRACK === "1" || process.env.CI === "true";
2130
2159
  var SESSION_ID = randomUUID();
2131
2160
  function getInstallId() {
2132
- const dir = join9(homedir4(), ".config", "harness-opencode");
2133
- const file = join9(dir, "install-id");
2161
+ const dir = join10(homedir4(), ".config", "harness-opencode");
2162
+ const file = join10(dir, "install-id");
2134
2163
  try {
2135
2164
  if (existsSync(file)) return readFileSync5(file, "utf8").trim();
2136
2165
  mkdirSync3(dir, { recursive: true });
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "@glrs-dev/harness-plugin-opencode",
3
- "version": "2.4.1",
3
+ "version": "2.7.0",
4
4
  "description": "Opinionated OpenCode agent harness — PRIME, plan, build, QA, skills, MCP wiring, hashline editing.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
8
8
  "types": "./dist/index.d.ts",
9
- "bin": {
10
- "glrs-oc": "dist/cli.js",
11
- "harness-opencode": "dist/cli.js"
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./cli": {
15
+ "import": "./dist/cli-exports.js",
16
+ "types": "./dist/cli-exports.d.ts"
17
+ }
12
18
  },
13
19
  "files": [
14
20
  "dist",
@@ -24,8 +30,7 @@
24
30
  "typecheck": "tsc --noEmit",
25
31
  "test": "bun test",
26
32
  "lint": "echo 'no linter configured yet'",
27
- "dev": "bun run build && bash -c 'PKG_DIR=\"$PWD\"; cd \"${INIT_CWD:-$PWD}\" && GLRS_CLI_DISPATCHED=1 exec bun \"$PKG_DIR/dist/cli.js\" \"$@\"' --",
28
- "dev:nobuild": "bash -c 'PKG_DIR=\"$PWD\"; cd \"${INIT_CWD:-$PWD}\" && GLRS_CLI_DISPATCHED=1 exec bun \"$PKG_DIR/dist/cli.js\" \"$@\"' --"
33
+ "dev": "tsup --watch"
29
34
  },
30
35
  "peerDependencies": {
31
36
  "@opencode-ai/plugin": "^1.15",
package/dist/cli.d.ts DELETED
@@ -1 +0,0 @@
1
- #!/usr/bin/env bun