@pablozrrrr/jevguard 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -166,13 +166,18 @@ proposal request to a hidden `jevguard-proposer` subagent:
166
166
  `jevguard-proposer` subagent has wildcard-deny permissions and wildcard-disabled
167
167
  tools, so it cannot call a tool, edit a file, or produce a patch. It returns a
168
168
  strategy and one manual apply instruction only.
169
+ - `remediation.model` is optional. When set in `.jev/config.yaml`, it selects the
170
+ proposer model; when it is absent, the proposer inherits the host's model.
169
171
  - A generic toast reports that the proposal is ready in the child session. It carries
170
172
  no rule, task, diff, finding, or credential.
171
173
 
172
174
  The plugin creates at most one proposal per evaluated turn, and the child session is
173
175
  excluded from review for the plugin lifetime, so a proposal can never recurse or
174
176
  trigger a second one. A host, model, or toast failure is contained and never changes
175
- the review.
177
+ the review. A contained proposal failure — an invalid model specifier, a child-session
178
+ failure, or a prompt failure — shows a generic error toast and writes one structured
179
+ `error` log entry carrying only a typed reason code, never any task, diff, finding,
180
+ path, or credential.
176
181
 
177
182
  The proposer is a strategy step, not an apply step: it never edits code, and nothing
178
183
  is applied automatically. You read the proposal, copy its manual apply instruction
@@ -187,8 +192,29 @@ or secret is logged.
187
192
 
188
193
  ## Install
189
194
 
190
- Once `@pablozrrrr/jevguard` is installed, the intended setup is one line in
191
- `opencode.json`:
195
+ Installing `@pablozrrrr/jevguard` does **not** register it with OpenCode by itself.
196
+ OpenCode only loads plugins listed in `plugin: [...]` in `opencode.json`, so register
197
+ the plugin from the installed CLI:
198
+
199
+ ```sh
200
+ # global config: ~/.config/opencode/opencode.json
201
+ jevguard install
202
+
203
+ # project config: ./opencode.json
204
+ jevguard install --project
205
+ ```
206
+
207
+ The installer asks for confirmation, edits only the `plugin` array, and is
208
+ idempotent. It only edits a `plugin` array of plain strings, so OpenCode's
209
+ `[["pkg", { "options": {} }]]` tuple form is a known limitation: the installer leaves
210
+ the file unchanged and prints the manual snippet instead. If `jevguard` is not on
211
+ `PATH`, run it through `bunx`:
212
+
213
+ ```sh
214
+ bunx --package @pablozrrrr/jevguard jevguard install
215
+ ```
216
+
217
+ The manual alternative is one entry in `opencode.json`:
192
218
 
193
219
  ```json
194
220
  { "plugin": ["@pablozrrrr/jevguard"] }
package/cli/main.js CHANGED
@@ -1259,7 +1259,7 @@ var REMEDIATION_MAX_TASK_LENGTH = 16384;
1259
1259
  var DEFAULT_REMEDIATION_CONFIG = {
1260
1260
  autoPropose: true,
1261
1261
  proposeOn: ["FAIL"],
1262
- model: "opencode/gpt-5.6-luna"
1262
+ model: null
1263
1263
  };
1264
1264
 
1265
1265
  // packages/core/src/remediation/request.ts
@@ -1349,6 +1349,7 @@ function snapshotRule(rule) {
1349
1349
  // packages/core/src/remediation/config.ts
1350
1350
  var MODEL_PATTERN = /^[^\s/]+\/[^\s/]+$/;
1351
1351
  var KNOWN_KEYS = new Set(["auto_propose", "propose_on", "model"]);
1352
+ var INVALID_MODEL = Symbol("invalid-model");
1352
1353
  function resolveRemediationConfig(value) {
1353
1354
  if (value === null || value === undefined) {
1354
1355
  return valid(DEFAULT_REMEDIATION_CONFIG);
@@ -1371,8 +1372,8 @@ function resolveRemediationConfig(value) {
1371
1372
  if (proposeOn === null) {
1372
1373
  return invalid2();
1373
1374
  }
1374
- const model = remediation.model ?? DEFAULT_REMEDIATION_CONFIG.model;
1375
- if (typeof model !== "string" || !MODEL_PATTERN.test(model)) {
1375
+ const model = resolveModel(remediation.model);
1376
+ if (model === INVALID_MODEL) {
1376
1377
  return invalid2();
1377
1378
  }
1378
1379
  return valid({ autoPropose, proposeOn, model });
@@ -1387,6 +1388,15 @@ function parseModelSpecifier(model) {
1387
1388
  modelID: model.slice(separator + 1)
1388
1389
  };
1389
1390
  }
1391
+ function resolveModel(value) {
1392
+ if (value === undefined) {
1393
+ return null;
1394
+ }
1395
+ if (typeof value !== "string" || !MODEL_PATTERN.test(value)) {
1396
+ return INVALID_MODEL;
1397
+ }
1398
+ return value;
1399
+ }
1390
1400
  function readProposeOn(value) {
1391
1401
  if (value === undefined) {
1392
1402
  return DEFAULT_REMEDIATION_CONFIG.proposeOn;
@@ -1695,6 +1705,217 @@ function batchFailed(reason) {
1695
1705
  function failedFor(request, reason) {
1696
1706
  return request.kind === "RULE" ? ruleFailed(reason) : batchFailed(reason);
1697
1707
  }
1708
+ // packages/opencode-adapter/src/opencode-config/confirmation.ts
1709
+ import { createInterface } from "readline/promises";
1710
+ function createConfirmationPrompt() {
1711
+ return {
1712
+ confirm: askForConfirmation
1713
+ };
1714
+ }
1715
+ async function askForConfirmation(message) {
1716
+ if (process.stdin.isTTY !== true) {
1717
+ return { status: "FAILED", reason: "NOT_A_TTY" };
1718
+ }
1719
+ try {
1720
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
1721
+ try {
1722
+ const answer = await readline.question(`${message} [y/N] `);
1723
+ return isAffirmative(answer) ? { status: "CONFIRMED" } : { status: "DECLINED" };
1724
+ } finally {
1725
+ readline.close();
1726
+ }
1727
+ } catch {
1728
+ return { status: "DECLINED" };
1729
+ }
1730
+ }
1731
+ function isAffirmative(answer) {
1732
+ const normalized = answer.trim().toLowerCase();
1733
+ return normalized === "y" || normalized === "yes";
1734
+ }
1735
+ // packages/opencode-adapter/src/opencode-config/entry.ts
1736
+ var JEVGUARD_PLUGIN_ENTRY = "@pablozrrrr/jevguard";
1737
+ var CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
1738
+ function createDefaultConfigDocument() {
1739
+ return { $schema: CONFIG_SCHEMA_URL };
1740
+ }
1741
+ function addPluginEntry(document, entry) {
1742
+ if (!isPlainObject(document)) {
1743
+ return { status: "INVALID_SHAPE" };
1744
+ }
1745
+ const existing = document.plugin;
1746
+ if (existing === undefined) {
1747
+ return { status: "ADDED", document: { ...document, plugin: [entry] } };
1748
+ }
1749
+ if (!isStringArray(existing)) {
1750
+ return { status: "INVALID_SHAPE" };
1751
+ }
1752
+ if (existing.some((value) => isPluginEntryFor(value, entry))) {
1753
+ return { status: "PRESENT" };
1754
+ }
1755
+ return { status: "ADDED", document: { ...document, plugin: [...existing, entry] } };
1756
+ }
1757
+ function isPlainObject(value) {
1758
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1759
+ }
1760
+ function isStringArray(value) {
1761
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
1762
+ }
1763
+ function isPluginEntryFor(value, entry) {
1764
+ return value === entry || value.startsWith(`${entry}@`);
1765
+ }
1766
+ // packages/opencode-adapter/src/opencode-config/file-system.ts
1767
+ import { randomUUID } from "crypto";
1768
+ import { access, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
1769
+ import { dirname, join } from "path";
1770
+ function createNodeConfigFileSystem() {
1771
+ return {
1772
+ readFile: readOptionalFile,
1773
+ fileExists,
1774
+ writeFile: writeConfigFile,
1775
+ ensureParentDirectory
1776
+ };
1777
+ }
1778
+ async function readOptionalFile(path) {
1779
+ try {
1780
+ return await readFile(path, "utf8");
1781
+ } catch (error) {
1782
+ if (isMissingFileError(error)) {
1783
+ return null;
1784
+ }
1785
+ throw error;
1786
+ }
1787
+ }
1788
+ async function fileExists(path) {
1789
+ try {
1790
+ await access(path);
1791
+ return true;
1792
+ } catch {
1793
+ return false;
1794
+ }
1795
+ }
1796
+ async function writeConfigFile(path, contents) {
1797
+ const temporaryPath = join(dirname(path), `.opencode.json.jevguard-tmp-${randomUUID()}`);
1798
+ try {
1799
+ await writeFile(temporaryPath, contents, "utf8");
1800
+ await rename(temporaryPath, path);
1801
+ } catch (error) {
1802
+ await rm(temporaryPath, { force: true }).catch(() => {
1803
+ return;
1804
+ });
1805
+ throw error;
1806
+ }
1807
+ }
1808
+ async function ensureParentDirectory(path) {
1809
+ await mkdir(dirname(path), { recursive: true });
1810
+ }
1811
+ function isMissingFileError(error) {
1812
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1813
+ }
1814
+ // packages/opencode-adapter/src/opencode-config/install.ts
1815
+ import { dirname as dirname2, join as join3 } from "path";
1816
+
1817
+ // packages/opencode-adapter/src/opencode-config/paths.ts
1818
+ import { isAbsolute, join as join2 } from "path";
1819
+ var PROJECT_CONFIG_FILENAME = "opencode.json";
1820
+ function resolveConfigPath(target, context) {
1821
+ if (target === "project") {
1822
+ return join2(context.cwd, PROJECT_CONFIG_FILENAME);
1823
+ }
1824
+ return join2(resolveConfigHome(context), "opencode", PROJECT_CONFIG_FILENAME);
1825
+ }
1826
+ function resolveConfigHome(context) {
1827
+ const configured = context.environment.XDG_CONFIG_HOME;
1828
+ if (configured !== undefined && configured !== "" && isAbsolute(configured)) {
1829
+ return configured;
1830
+ }
1831
+ return join2(context.homedir, ".config");
1832
+ }
1833
+
1834
+ // packages/opencode-adapter/src/opencode-config/install.ts
1835
+ var CONFIRMATION_MESSAGE = "Add JevGuard to the opencode.json plugin list?";
1836
+ async function install(dependencies) {
1837
+ const path = resolveConfigPath(dependencies.target, dependencies);
1838
+ const read = await readConfigDocument(dependencies.fileSystem, path);
1839
+ if (read.status === "FAILED") {
1840
+ return failed(dependencies.target, path, read.reason);
1841
+ }
1842
+ const mutation = addPluginEntry(read.document, JEVGUARD_PLUGIN_ENTRY);
1843
+ if (mutation.status === "INVALID_SHAPE") {
1844
+ return failed(dependencies.target, path, "INVALID_CONFIG_SHAPE");
1845
+ }
1846
+ if (mutation.status === "PRESENT") {
1847
+ return { status: "ALREADY_INSTALLED", target: dependencies.target, path };
1848
+ }
1849
+ const confirmation = await requestConfirmation(dependencies.confirmation);
1850
+ if (confirmation.status === "FAILED") {
1851
+ return failed(dependencies.target, path, confirmation.reason);
1852
+ }
1853
+ if (confirmation.status === "DECLINED") {
1854
+ return { status: "ABORTED", target: dependencies.target, path };
1855
+ }
1856
+ const written = await writeConfigDocument(dependencies.fileSystem, path, mutation.document);
1857
+ if (!written) {
1858
+ return failed(dependencies.target, path, "WRITE_FAILURE");
1859
+ }
1860
+ return { status: "INSTALLED", target: dependencies.target, path };
1861
+ }
1862
+ async function readConfigDocument(fileSystem, path) {
1863
+ const contents = await readOptionalContents(fileSystem, path);
1864
+ if (contents.status === "FAILED") {
1865
+ return contents;
1866
+ }
1867
+ if (contents.value === null) {
1868
+ return readMissingConfigDocument(fileSystem, path);
1869
+ }
1870
+ try {
1871
+ return { status: "READ", document: JSON.parse(contents.value) };
1872
+ } catch {
1873
+ return { status: "FAILED", reason: "INVALID_JSON" };
1874
+ }
1875
+ }
1876
+ async function readOptionalContents(fileSystem, path) {
1877
+ try {
1878
+ return { status: "READ", value: await fileSystem.readFile(path) };
1879
+ } catch {
1880
+ return { status: "FAILED", reason: "READ_FAILURE" };
1881
+ }
1882
+ }
1883
+ async function readMissingConfigDocument(fileSystem, path) {
1884
+ try {
1885
+ const hasJsonc = await fileSystem.fileExists(join3(dirname2(path), "opencode.jsonc"));
1886
+ if (hasJsonc) {
1887
+ return { status: "FAILED", reason: "UNSUPPORTED_JSONC" };
1888
+ }
1889
+ return { status: "READ", document: createDefaultConfigDocument() };
1890
+ } catch {
1891
+ return { status: "FAILED", reason: "READ_FAILURE" };
1892
+ }
1893
+ }
1894
+ async function requestConfirmation(confirmation) {
1895
+ try {
1896
+ const result = await confirmation.confirm(CONFIRMATION_MESSAGE);
1897
+ if (result.status === "FAILED") {
1898
+ return { status: "FAILED", reason: result.reason };
1899
+ }
1900
+ return { status: result.status };
1901
+ } catch {
1902
+ return { status: "FAILED", reason: "NOT_A_TTY" };
1903
+ }
1904
+ }
1905
+ async function writeConfigDocument(fileSystem, path, document) {
1906
+ const serialized = `${JSON.stringify(document, null, 2)}
1907
+ `;
1908
+ try {
1909
+ await fileSystem.ensureParentDirectory(path);
1910
+ await fileSystem.writeFile(path, serialized);
1911
+ return true;
1912
+ } catch {
1913
+ return false;
1914
+ }
1915
+ }
1916
+ function failed(target, path, reason) {
1917
+ return { status: "FAILED", target, path, reason };
1918
+ }
1698
1919
  // packages/opencode-adapter/src/policy/config-yaml.ts
1699
1920
  import { parseDocument, visit } from "yaml";
1700
1921
  function parsePolicyConfig(text) {
@@ -1731,16 +1952,16 @@ function invalid3() {
1731
1952
  return { status: "INVALID", reason: "INVALID_CONFIG" };
1732
1953
  }
1733
1954
  // packages/opencode-adapter/src/policy/file-source.ts
1734
- import { readFile } from "fs/promises";
1735
- import { join } from "path";
1955
+ import { readFile as readFile2 } from "fs/promises";
1956
+ import { join as join4 } from "path";
1736
1957
  function createPolicyFileLoader(dependencies) {
1737
1958
  return {
1738
1959
  async load() {
1739
- const rules = await readOptionalFile(dependencies.fileSystem, join(dependencies.root, ".jev", "rules.md"));
1960
+ const rules = await readOptionalFile2(dependencies.fileSystem, join4(dependencies.root, ".jev", "rules.md"));
1740
1961
  if (rules.status === "FAILED") {
1741
1962
  return { status: "FAILED", reason: "RULES_READ_FAILURE" };
1742
1963
  }
1743
- const config = await readOptionalFile(dependencies.fileSystem, join(dependencies.root, ".jev", "config.yaml"));
1964
+ const config = await readOptionalFile2(dependencies.fileSystem, join4(dependencies.root, ".jev", "config.yaml"));
1744
1965
  if (config.status === "FAILED") {
1745
1966
  return { status: "FAILED", reason: "CONFIG_READ_FAILURE" };
1746
1967
  }
@@ -1753,20 +1974,20 @@ function createPolicyFileLoader(dependencies) {
1753
1974
  }
1754
1975
  function createNodePolicyFileSystem() {
1755
1976
  return {
1756
- readFile: (path) => readFile(path, "utf8")
1977
+ readFile: (path) => readFile2(path, "utf8")
1757
1978
  };
1758
1979
  }
1759
- async function readOptionalFile(fileSystem, path) {
1980
+ async function readOptionalFile2(fileSystem, path) {
1760
1981
  try {
1761
1982
  return { status: "READ", value: await fileSystem.readFile(path) };
1762
1983
  } catch (error) {
1763
- if (isMissingFileError(error)) {
1984
+ if (isMissingFileError2(error)) {
1764
1985
  return { status: "READ", value: null };
1765
1986
  }
1766
1987
  return { status: "FAILED" };
1767
1988
  }
1768
1989
  }
1769
- function isMissingFileError(error) {
1990
+ function isMissingFileError2(error) {
1770
1991
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1771
1992
  }
1772
1993
  // packages/opencode-adapter/src/presentation/display.ts
@@ -1996,25 +2217,50 @@ function toOutcomeExtra(result) {
1996
2217
  }
1997
2218
  // packages/opencode-adapter/src/presentation/remediation.ts
1998
2219
  var TITLE = "JevGuard";
2220
+ var SERVICE_NAME2 = "jevguard";
1999
2221
  var PREPARING_MESSAGE = "JevGuard is preparing a remediation proposal in the child session.";
2000
2222
  var READY_MESSAGE = "JevGuard remediation proposal ready.";
2223
+ var FAILED_MESSAGE = "JevGuard could not prepare a remediation proposal.";
2224
+ var FAILURE_LOG_MESSAGE = "JevGuard remediation proposal failed";
2001
2225
  function createRemediationNotifier(toast) {
2002
2226
  return {
2003
2227
  async proposalPreparing() {
2004
- return showRemediationToast(toast, PREPARING_MESSAGE);
2228
+ return showRemediationToast(toast, PREPARING_MESSAGE, "info");
2005
2229
  },
2006
2230
  async proposalReady() {
2007
- return showRemediationToast(toast, READY_MESSAGE);
2231
+ return showRemediationToast(toast, READY_MESSAGE, "info");
2232
+ },
2233
+ async proposalFailed() {
2234
+ return showRemediationToast(toast, FAILED_MESSAGE, "error");
2008
2235
  }
2009
2236
  };
2010
2237
  }
2011
- async function showRemediationToast(toast, message) {
2238
+ async function showRemediationToast(toast, message, variant) {
2012
2239
  try {
2013
- return await toast.show({ title: TITLE, message, variant: "info" });
2240
+ return await toast.show({ title: TITLE, message, variant });
2014
2241
  } catch {
2015
2242
  return "FAILED";
2016
2243
  }
2017
2244
  }
2245
+ function createRemediationFailureLogSink(client) {
2246
+ return {
2247
+ async write(reason) {
2248
+ try {
2249
+ const result = await client.app.log({
2250
+ body: {
2251
+ service: SERVICE_NAME2,
2252
+ level: "error",
2253
+ message: FAILURE_LOG_MESSAGE,
2254
+ extra: { reason }
2255
+ }
2256
+ });
2257
+ return result.error === undefined ? "DELIVERED" : "FAILED";
2258
+ } catch {
2259
+ return "FAILED";
2260
+ }
2261
+ }
2262
+ };
2263
+ }
2018
2264
  function createOpenCodeSessionNavigator(client) {
2019
2265
  return {
2020
2266
  async navigate(sessionID) {
@@ -2065,11 +2311,12 @@ function createOpenCodeProposalSessionFacade(client) {
2065
2311
  return result.data.id;
2066
2312
  },
2067
2313
  async prompt(input) {
2314
+ const model = input.model;
2068
2315
  const result = await client.session.prompt({
2069
2316
  path: { id: input.sessionID },
2070
2317
  body: {
2071
2318
  agent: input.agent,
2072
- model: { providerID: input.model.providerID, modelID: input.model.modelID },
2319
+ ...model === null ? {} : { model: { providerID: model.providerID, modelID: model.modelID } },
2073
2320
  system: input.system,
2074
2321
  tools: { ...input.tools },
2075
2322
  parts: [{ type: "text", text: input.text }]
@@ -2081,15 +2328,37 @@ function createOpenCodeProposalSessionFacade(client) {
2081
2328
  }
2082
2329
  };
2083
2330
  }
2331
+ // packages/plugin/src/cli/install.ts
2332
+ import { homedir } from "os";
2333
+ function performInstall(target) {
2334
+ return install({
2335
+ confirmation: createConfirmationPrompt(),
2336
+ fileSystem: createNodeConfigFileSystem(),
2337
+ target,
2338
+ cwd: process.cwd(),
2339
+ environment: process.env,
2340
+ homedir: homedir()
2341
+ });
2342
+ }
2343
+
2084
2344
  // packages/plugin/src/cli/login.ts
2085
2345
  function performLogin() {
2086
2346
  return login({ prompt: createSecretPrompt(), store: createKeyringCredentialStore() });
2087
2347
  }
2088
2348
 
2089
2349
  // packages/plugin/src/cli/messages.ts
2090
- var USAGE = "Usage: jevguard login";
2350
+ var USAGE = [
2351
+ "Usage: jevguard login",
2352
+ " jevguard install [--project]",
2353
+ "",
2354
+ "`jev-init` and `jevguard-rules` are OpenCode skills, not CLI commands. Register",
2355
+ "the plugin in opencode.json (or run `jevguard install`) and invoke them from",
2356
+ "OpenCode."
2357
+ ].join(`
2358
+ `);
2091
2359
  var LOGIN_SAVED_MESSAGE = "Credential saved to the OS credential store.";
2092
2360
  var LOGIN_ABORTED_MESSAGE = "Login cancelled. No credential was saved.";
2361
+ var MANUAL_INSTALL_SNIPPET = `{ "plugin": ["${JEVGUARD_PLUGIN_ENTRY}"] }`;
2093
2362
  function describeLoginFailure(reason) {
2094
2363
  switch (reason) {
2095
2364
  case "NOT_A_TTY":
@@ -2102,13 +2371,51 @@ function describeLoginFailure(reason) {
2102
2371
  return "Could not save the credential to the OS credential store. Check that a credential store is available and try again.";
2103
2372
  }
2104
2373
  }
2374
+ function describeInstallInstalled(target, path) {
2375
+ return `Registered JevGuard for the ${target} target in ${path}. Restart OpenCode to load it.`;
2376
+ }
2377
+ function describeInstallAlreadyInstalled(target, path) {
2378
+ return `JevGuard is already registered for the ${target} target in ${path}.`;
2379
+ }
2380
+ function describeInstallAborted(path) {
2381
+ return `Install cancelled. ${path} was not changed.`;
2382
+ }
2383
+ function describeInstallFailure(reason, path) {
2384
+ switch (reason) {
2385
+ case "NOT_A_TTY":
2386
+ return manualSnippetFailure(`${path} cannot be confirmed without an interactive terminal.`);
2387
+ case "UNSUPPORTED_JSONC":
2388
+ return manualSnippetFailure(`${path} is absent, but a sibling opencode.jsonc exists. JevGuard never edits .jsonc files.`);
2389
+ case "INVALID_JSON":
2390
+ return manualSnippetFailure(`Could not parse ${path} as JSON.`);
2391
+ case "INVALID_CONFIG_SHAPE":
2392
+ return manualSnippetFailure(`The plugin entry in ${path} is not an array of strings, so JevGuard did not change it.`);
2393
+ case "READ_FAILURE":
2394
+ return `Could not read ${path}. Check the file permissions and try again.`;
2395
+ case "WRITE_FAILURE":
2396
+ return `Could not write ${path}. Check the file permissions and try again.`;
2397
+ }
2398
+ }
2399
+ function manualSnippetFailure(message) {
2400
+ return [message, "Add this to the plugin array manually:", MANUAL_INSTALL_SNIPPET].join(`
2401
+ `);
2402
+ }
2105
2403
 
2106
2404
  // packages/plugin/src/cli/run-cli.ts
2107
2405
  async function runCli(argv, io, dependencies) {
2108
- if (argv.length !== 1 || argv[0] !== "login") {
2109
- io.writeErr(USAGE);
2110
- return 1;
2406
+ if (argv.length === 1 && argv[0] === "login") {
2407
+ return runLogin(io, dependencies);
2408
+ }
2409
+ if (argv.length === 1 && argv[0] === "install") {
2410
+ return runInstall(io, dependencies, "global");
2411
+ }
2412
+ if (argv.length === 2 && argv[0] === "install" && argv[1] === "--project") {
2413
+ return runInstall(io, dependencies, "project");
2111
2414
  }
2415
+ io.writeErr(USAGE);
2416
+ return 1;
2417
+ }
2418
+ async function runLogin(io, dependencies) {
2112
2419
  const outcome = await dependencies.login();
2113
2420
  if (outcome.status === "SAVED") {
2114
2421
  io.writeOut(LOGIN_SAVED_MESSAGE);
@@ -2121,6 +2428,26 @@ async function runCli(argv, io, dependencies) {
2121
2428
  io.writeErr(describeLoginFailure(outcome.reason));
2122
2429
  return 1;
2123
2430
  }
2431
+ async function runInstall(io, dependencies, target) {
2432
+ const outcome = await dependencies.install(target);
2433
+ return reportInstall(io, outcome);
2434
+ }
2435
+ function reportInstall(io, outcome) {
2436
+ if (outcome.status === "INSTALLED") {
2437
+ io.writeOut(describeInstallInstalled(outcome.target, outcome.path));
2438
+ return 0;
2439
+ }
2440
+ if (outcome.status === "ALREADY_INSTALLED") {
2441
+ io.writeOut(describeInstallAlreadyInstalled(outcome.target, outcome.path));
2442
+ return 0;
2443
+ }
2444
+ if (outcome.status === "ABORTED") {
2445
+ io.writeErr(describeInstallAborted(outcome.path));
2446
+ return 1;
2447
+ }
2448
+ io.writeErr(describeInstallFailure(outcome.reason, outcome.path));
2449
+ return 1;
2450
+ }
2124
2451
 
2125
2452
  // packages/plugin/src/cli/main.ts
2126
2453
  var io = {
@@ -2129,5 +2456,8 @@ var io = {
2129
2456
  writeErr: (message) => process.stderr.write(`${message}
2130
2457
  `)
2131
2458
  };
2132
- var exitCode = await runCli(process.argv.slice(2), io, { login: performLogin });
2459
+ var exitCode = await runCli(process.argv.slice(2), io, {
2460
+ login: performLogin,
2461
+ install: performInstall
2462
+ });
2133
2463
  process.exitCode = exitCode;
package/index.js CHANGED
@@ -906,7 +906,7 @@ var REMEDIATION_MAX_TASK_LENGTH = 16384;
906
906
  var DEFAULT_REMEDIATION_CONFIG = {
907
907
  autoPropose: true,
908
908
  proposeOn: ["FAIL"],
909
- model: "opencode/gpt-5.6-luna"
909
+ model: null
910
910
  };
911
911
 
912
912
  // packages/core/src/remediation/request.ts
@@ -996,6 +996,7 @@ function snapshotRule(rule) {
996
996
  // packages/core/src/remediation/config.ts
997
997
  var MODEL_PATTERN = /^[^\s/]+\/[^\s/]+$/;
998
998
  var KNOWN_KEYS = new Set(["auto_propose", "propose_on", "model"]);
999
+ var INVALID_MODEL = Symbol("invalid-model");
999
1000
  function resolveRemediationConfig(value) {
1000
1001
  if (value === null || value === undefined) {
1001
1002
  return valid(DEFAULT_REMEDIATION_CONFIG);
@@ -1018,8 +1019,8 @@ function resolveRemediationConfig(value) {
1018
1019
  if (proposeOn === null) {
1019
1020
  return invalid2();
1020
1021
  }
1021
- const model = remediation.model ?? DEFAULT_REMEDIATION_CONFIG.model;
1022
- if (typeof model !== "string" || !MODEL_PATTERN.test(model)) {
1022
+ const model = resolveModel(remediation.model);
1023
+ if (model === INVALID_MODEL) {
1023
1024
  return invalid2();
1024
1025
  }
1025
1026
  return valid({ autoPropose, proposeOn, model });
@@ -1034,6 +1035,15 @@ function parseModelSpecifier(model) {
1034
1035
  modelID: model.slice(separator + 1)
1035
1036
  };
1036
1037
  }
1038
+ function resolveModel(value) {
1039
+ if (value === undefined) {
1040
+ return null;
1041
+ }
1042
+ if (typeof value !== "string" || !MODEL_PATTERN.test(value)) {
1043
+ return INVALID_MODEL;
1044
+ }
1045
+ return value;
1046
+ }
1037
1047
  function readProposeOn(value) {
1038
1048
  if (value === undefined) {
1039
1049
  return DEFAULT_REMEDIATION_CONFIG.proposeOn;
@@ -1693,6 +1703,217 @@ function batchFailed(reason) {
1693
1703
  function failedFor(request, reason) {
1694
1704
  return request.kind === "RULE" ? ruleFailed(reason) : batchFailed(reason);
1695
1705
  }
1706
+ // packages/opencode-adapter/src/opencode-config/confirmation.ts
1707
+ import { createInterface } from "readline/promises";
1708
+ function createConfirmationPrompt() {
1709
+ return {
1710
+ confirm: askForConfirmation
1711
+ };
1712
+ }
1713
+ async function askForConfirmation(message) {
1714
+ if (process.stdin.isTTY !== true) {
1715
+ return { status: "FAILED", reason: "NOT_A_TTY" };
1716
+ }
1717
+ try {
1718
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
1719
+ try {
1720
+ const answer = await readline.question(`${message} [y/N] `);
1721
+ return isAffirmative(answer) ? { status: "CONFIRMED" } : { status: "DECLINED" };
1722
+ } finally {
1723
+ readline.close();
1724
+ }
1725
+ } catch {
1726
+ return { status: "DECLINED" };
1727
+ }
1728
+ }
1729
+ function isAffirmative(answer) {
1730
+ const normalized = answer.trim().toLowerCase();
1731
+ return normalized === "y" || normalized === "yes";
1732
+ }
1733
+ // packages/opencode-adapter/src/opencode-config/entry.ts
1734
+ var JEVGUARD_PLUGIN_ENTRY = "@pablozrrrr/jevguard";
1735
+ var CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
1736
+ function createDefaultConfigDocument() {
1737
+ return { $schema: CONFIG_SCHEMA_URL };
1738
+ }
1739
+ function addPluginEntry(document, entry) {
1740
+ if (!isPlainObject(document)) {
1741
+ return { status: "INVALID_SHAPE" };
1742
+ }
1743
+ const existing = document.plugin;
1744
+ if (existing === undefined) {
1745
+ return { status: "ADDED", document: { ...document, plugin: [entry] } };
1746
+ }
1747
+ if (!isStringArray(existing)) {
1748
+ return { status: "INVALID_SHAPE" };
1749
+ }
1750
+ if (existing.some((value) => isPluginEntryFor(value, entry))) {
1751
+ return { status: "PRESENT" };
1752
+ }
1753
+ return { status: "ADDED", document: { ...document, plugin: [...existing, entry] } };
1754
+ }
1755
+ function isPlainObject(value) {
1756
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1757
+ }
1758
+ function isStringArray(value) {
1759
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
1760
+ }
1761
+ function isPluginEntryFor(value, entry) {
1762
+ return value === entry || value.startsWith(`${entry}@`);
1763
+ }
1764
+ // packages/opencode-adapter/src/opencode-config/file-system.ts
1765
+ import { randomUUID } from "crypto";
1766
+ import { access, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
1767
+ import { dirname, join } from "path";
1768
+ function createNodeConfigFileSystem() {
1769
+ return {
1770
+ readFile: readOptionalFile,
1771
+ fileExists,
1772
+ writeFile: writeConfigFile,
1773
+ ensureParentDirectory
1774
+ };
1775
+ }
1776
+ async function readOptionalFile(path) {
1777
+ try {
1778
+ return await readFile(path, "utf8");
1779
+ } catch (error) {
1780
+ if (isMissingFileError(error)) {
1781
+ return null;
1782
+ }
1783
+ throw error;
1784
+ }
1785
+ }
1786
+ async function fileExists(path) {
1787
+ try {
1788
+ await access(path);
1789
+ return true;
1790
+ } catch {
1791
+ return false;
1792
+ }
1793
+ }
1794
+ async function writeConfigFile(path, contents) {
1795
+ const temporaryPath = join(dirname(path), `.opencode.json.jevguard-tmp-${randomUUID()}`);
1796
+ try {
1797
+ await writeFile(temporaryPath, contents, "utf8");
1798
+ await rename(temporaryPath, path);
1799
+ } catch (error) {
1800
+ await rm(temporaryPath, { force: true }).catch(() => {
1801
+ return;
1802
+ });
1803
+ throw error;
1804
+ }
1805
+ }
1806
+ async function ensureParentDirectory(path) {
1807
+ await mkdir(dirname(path), { recursive: true });
1808
+ }
1809
+ function isMissingFileError(error) {
1810
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1811
+ }
1812
+ // packages/opencode-adapter/src/opencode-config/install.ts
1813
+ import { dirname as dirname2, join as join3 } from "path";
1814
+
1815
+ // packages/opencode-adapter/src/opencode-config/paths.ts
1816
+ import { isAbsolute, join as join2 } from "path";
1817
+ var PROJECT_CONFIG_FILENAME = "opencode.json";
1818
+ function resolveConfigPath(target, context) {
1819
+ if (target === "project") {
1820
+ return join2(context.cwd, PROJECT_CONFIG_FILENAME);
1821
+ }
1822
+ return join2(resolveConfigHome(context), "opencode", PROJECT_CONFIG_FILENAME);
1823
+ }
1824
+ function resolveConfigHome(context) {
1825
+ const configured = context.environment.XDG_CONFIG_HOME;
1826
+ if (configured !== undefined && configured !== "" && isAbsolute(configured)) {
1827
+ return configured;
1828
+ }
1829
+ return join2(context.homedir, ".config");
1830
+ }
1831
+
1832
+ // packages/opencode-adapter/src/opencode-config/install.ts
1833
+ var CONFIRMATION_MESSAGE = "Add JevGuard to the opencode.json plugin list?";
1834
+ async function install(dependencies) {
1835
+ const path = resolveConfigPath(dependencies.target, dependencies);
1836
+ const read = await readConfigDocument(dependencies.fileSystem, path);
1837
+ if (read.status === "FAILED") {
1838
+ return failed(dependencies.target, path, read.reason);
1839
+ }
1840
+ const mutation = addPluginEntry(read.document, JEVGUARD_PLUGIN_ENTRY);
1841
+ if (mutation.status === "INVALID_SHAPE") {
1842
+ return failed(dependencies.target, path, "INVALID_CONFIG_SHAPE");
1843
+ }
1844
+ if (mutation.status === "PRESENT") {
1845
+ return { status: "ALREADY_INSTALLED", target: dependencies.target, path };
1846
+ }
1847
+ const confirmation = await requestConfirmation(dependencies.confirmation);
1848
+ if (confirmation.status === "FAILED") {
1849
+ return failed(dependencies.target, path, confirmation.reason);
1850
+ }
1851
+ if (confirmation.status === "DECLINED") {
1852
+ return { status: "ABORTED", target: dependencies.target, path };
1853
+ }
1854
+ const written = await writeConfigDocument(dependencies.fileSystem, path, mutation.document);
1855
+ if (!written) {
1856
+ return failed(dependencies.target, path, "WRITE_FAILURE");
1857
+ }
1858
+ return { status: "INSTALLED", target: dependencies.target, path };
1859
+ }
1860
+ async function readConfigDocument(fileSystem, path) {
1861
+ const contents = await readOptionalContents(fileSystem, path);
1862
+ if (contents.status === "FAILED") {
1863
+ return contents;
1864
+ }
1865
+ if (contents.value === null) {
1866
+ return readMissingConfigDocument(fileSystem, path);
1867
+ }
1868
+ try {
1869
+ return { status: "READ", document: JSON.parse(contents.value) };
1870
+ } catch {
1871
+ return { status: "FAILED", reason: "INVALID_JSON" };
1872
+ }
1873
+ }
1874
+ async function readOptionalContents(fileSystem, path) {
1875
+ try {
1876
+ return { status: "READ", value: await fileSystem.readFile(path) };
1877
+ } catch {
1878
+ return { status: "FAILED", reason: "READ_FAILURE" };
1879
+ }
1880
+ }
1881
+ async function readMissingConfigDocument(fileSystem, path) {
1882
+ try {
1883
+ const hasJsonc = await fileSystem.fileExists(join3(dirname2(path), "opencode.jsonc"));
1884
+ if (hasJsonc) {
1885
+ return { status: "FAILED", reason: "UNSUPPORTED_JSONC" };
1886
+ }
1887
+ return { status: "READ", document: createDefaultConfigDocument() };
1888
+ } catch {
1889
+ return { status: "FAILED", reason: "READ_FAILURE" };
1890
+ }
1891
+ }
1892
+ async function requestConfirmation(confirmation) {
1893
+ try {
1894
+ const result = await confirmation.confirm(CONFIRMATION_MESSAGE);
1895
+ if (result.status === "FAILED") {
1896
+ return { status: "FAILED", reason: result.reason };
1897
+ }
1898
+ return { status: result.status };
1899
+ } catch {
1900
+ return { status: "FAILED", reason: "NOT_A_TTY" };
1901
+ }
1902
+ }
1903
+ async function writeConfigDocument(fileSystem, path, document) {
1904
+ const serialized = `${JSON.stringify(document, null, 2)}
1905
+ `;
1906
+ try {
1907
+ await fileSystem.ensureParentDirectory(path);
1908
+ await fileSystem.writeFile(path, serialized);
1909
+ return true;
1910
+ } catch {
1911
+ return false;
1912
+ }
1913
+ }
1914
+ function failed(target, path, reason) {
1915
+ return { status: "FAILED", target, path, reason };
1916
+ }
1696
1917
  // packages/opencode-adapter/src/policy/config-yaml.ts
1697
1918
  import { parseDocument, visit } from "yaml";
1698
1919
  function parsePolicyConfig(text) {
@@ -1729,16 +1950,16 @@ function invalid3() {
1729
1950
  return { status: "INVALID", reason: "INVALID_CONFIG" };
1730
1951
  }
1731
1952
  // packages/opencode-adapter/src/policy/file-source.ts
1732
- import { readFile } from "fs/promises";
1733
- import { join } from "path";
1953
+ import { readFile as readFile2 } from "fs/promises";
1954
+ import { join as join4 } from "path";
1734
1955
  function createPolicyFileLoader(dependencies) {
1735
1956
  return {
1736
1957
  async load() {
1737
- const rules = await readOptionalFile(dependencies.fileSystem, join(dependencies.root, ".jev", "rules.md"));
1958
+ const rules = await readOptionalFile2(dependencies.fileSystem, join4(dependencies.root, ".jev", "rules.md"));
1738
1959
  if (rules.status === "FAILED") {
1739
1960
  return { status: "FAILED", reason: "RULES_READ_FAILURE" };
1740
1961
  }
1741
- const config = await readOptionalFile(dependencies.fileSystem, join(dependencies.root, ".jev", "config.yaml"));
1962
+ const config = await readOptionalFile2(dependencies.fileSystem, join4(dependencies.root, ".jev", "config.yaml"));
1742
1963
  if (config.status === "FAILED") {
1743
1964
  return { status: "FAILED", reason: "CONFIG_READ_FAILURE" };
1744
1965
  }
@@ -1751,20 +1972,20 @@ function createPolicyFileLoader(dependencies) {
1751
1972
  }
1752
1973
  function createNodePolicyFileSystem() {
1753
1974
  return {
1754
- readFile: (path) => readFile(path, "utf8")
1975
+ readFile: (path) => readFile2(path, "utf8")
1755
1976
  };
1756
1977
  }
1757
- async function readOptionalFile(fileSystem, path) {
1978
+ async function readOptionalFile2(fileSystem, path) {
1758
1979
  try {
1759
1980
  return { status: "READ", value: await fileSystem.readFile(path) };
1760
1981
  } catch (error) {
1761
- if (isMissingFileError(error)) {
1982
+ if (isMissingFileError2(error)) {
1762
1983
  return { status: "READ", value: null };
1763
1984
  }
1764
1985
  return { status: "FAILED" };
1765
1986
  }
1766
1987
  }
1767
- function isMissingFileError(error) {
1988
+ function isMissingFileError2(error) {
1768
1989
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1769
1990
  }
1770
1991
  // packages/opencode-adapter/src/presentation/display.ts
@@ -1994,25 +2215,50 @@ function toOutcomeExtra(result) {
1994
2215
  }
1995
2216
  // packages/opencode-adapter/src/presentation/remediation.ts
1996
2217
  var TITLE = "JevGuard";
2218
+ var SERVICE_NAME2 = "jevguard";
1997
2219
  var PREPARING_MESSAGE = "JevGuard is preparing a remediation proposal in the child session.";
1998
2220
  var READY_MESSAGE = "JevGuard remediation proposal ready.";
2221
+ var FAILED_MESSAGE = "JevGuard could not prepare a remediation proposal.";
2222
+ var FAILURE_LOG_MESSAGE = "JevGuard remediation proposal failed";
1999
2223
  function createRemediationNotifier(toast) {
2000
2224
  return {
2001
2225
  async proposalPreparing() {
2002
- return showRemediationToast(toast, PREPARING_MESSAGE);
2226
+ return showRemediationToast(toast, PREPARING_MESSAGE, "info");
2003
2227
  },
2004
2228
  async proposalReady() {
2005
- return showRemediationToast(toast, READY_MESSAGE);
2229
+ return showRemediationToast(toast, READY_MESSAGE, "info");
2230
+ },
2231
+ async proposalFailed() {
2232
+ return showRemediationToast(toast, FAILED_MESSAGE, "error");
2006
2233
  }
2007
2234
  };
2008
2235
  }
2009
- async function showRemediationToast(toast, message) {
2236
+ async function showRemediationToast(toast, message, variant) {
2010
2237
  try {
2011
- return await toast.show({ title: TITLE, message, variant: "info" });
2238
+ return await toast.show({ title: TITLE, message, variant });
2012
2239
  } catch {
2013
2240
  return "FAILED";
2014
2241
  }
2015
2242
  }
2243
+ function createRemediationFailureLogSink(client) {
2244
+ return {
2245
+ async write(reason) {
2246
+ try {
2247
+ const result = await client.app.log({
2248
+ body: {
2249
+ service: SERVICE_NAME2,
2250
+ level: "error",
2251
+ message: FAILURE_LOG_MESSAGE,
2252
+ extra: { reason }
2253
+ }
2254
+ });
2255
+ return result.error === undefined ? "DELIVERED" : "FAILED";
2256
+ } catch {
2257
+ return "FAILED";
2258
+ }
2259
+ }
2260
+ };
2261
+ }
2016
2262
  function createOpenCodeSessionNavigator(client) {
2017
2263
  return {
2018
2264
  async navigate(sessionID) {
@@ -2063,11 +2309,12 @@ function createOpenCodeProposalSessionFacade(client) {
2063
2309
  return result.data.id;
2064
2310
  },
2065
2311
  async prompt(input) {
2312
+ const model = input.model;
2066
2313
  const result = await client.session.prompt({
2067
2314
  path: { id: input.sessionID },
2068
2315
  body: {
2069
2316
  agent: input.agent,
2070
- model: { providerID: input.model.providerID, modelID: input.model.modelID },
2317
+ ...model === null ? {} : { model: { providerID: model.providerID, modelID: model.modelID } },
2071
2318
  system: input.system,
2072
2319
  tools: { ...input.tools },
2073
2320
  parts: [{ type: "text", text: input.text }]
@@ -2146,21 +2393,28 @@ async function proposeForTurn(request, dependencies) {
2146
2393
  if (request === null) {
2147
2394
  return;
2148
2395
  }
2149
- const model = parseModelSpecifier(request.model);
2150
- if (model === null) {
2396
+ const model = request.model === null ? null : parseModelSpecifier(request.model);
2397
+ if (request.model !== null && model === null) {
2398
+ await reportProposalFailure("MODEL_SPECIFIER_INVALID", dependencies);
2151
2399
  return;
2152
2400
  }
2153
2401
  if (!dependencies.proposals.claim(request.evaluationId)) {
2154
2402
  return;
2155
2403
  }
2404
+ let sessionID;
2156
2405
  try {
2157
- const sessionID = await dependencies.proposalFacade.createChildSession({
2406
+ sessionID = await dependencies.proposalFacade.createChildSession({
2158
2407
  parentID: request.sessionID,
2159
2408
  title: PROPOSAL_SESSION_TITLE
2160
2409
  });
2161
- dependencies.proposals.registerChildSession(sessionID);
2162
- await navigateToProposal(dependencies.navigator, sessionID);
2163
- await notifyProposalPreparing(dependencies.notifier);
2410
+ } catch {
2411
+ await reportProposalFailure("CHILD_SESSION_FAILED", dependencies);
2412
+ return;
2413
+ }
2414
+ dependencies.proposals.registerChildSession(sessionID);
2415
+ await navigateToProposal(dependencies.navigator, sessionID);
2416
+ await notifyProposalPreparing(dependencies.notifier);
2417
+ try {
2164
2418
  await dependencies.proposalFacade.prompt({
2165
2419
  sessionID,
2166
2420
  agent: PROPOSER_AGENT_NAME,
@@ -2169,7 +2423,26 @@ async function proposeForTurn(request, dependencies) {
2169
2423
  tools: PROPOSER_TOOLS,
2170
2424
  text: buildProposalText(request)
2171
2425
  });
2172
- await notifyProposalReady(dependencies.notifier);
2426
+ } catch {
2427
+ await reportProposalFailure("PROPOSAL_PROMPT_FAILED", dependencies);
2428
+ return;
2429
+ }
2430
+ await notifyProposalReady(dependencies.notifier);
2431
+ }
2432
+ async function reportProposalFailure(reason, dependencies) {
2433
+ await writeFailureLog(dependencies.failures, reason);
2434
+ await notifyProposalFailed(dependencies.notifier);
2435
+ }
2436
+ async function writeFailureLog(sink, reason) {
2437
+ try {
2438
+ await sink.write(reason);
2439
+ } catch {
2440
+ return;
2441
+ }
2442
+ }
2443
+ async function notifyProposalFailed(notifier) {
2444
+ try {
2445
+ await notifier.proposalFailed();
2173
2446
  } catch {
2174
2447
  return;
2175
2448
  }
@@ -2416,7 +2689,7 @@ function createConfigHook(options) {
2416
2689
 
2417
2690
  // packages/plugin/src/skills/skill-directory.ts
2418
2691
  import { existsSync } from "fs";
2419
- import { join as join2 } from "path";
2692
+ import { join as join5 } from "path";
2420
2693
  import { fileURLToPath } from "url";
2421
2694
  var BUNDLED_SKILL_NAMES = ["jevguard-rules", "jev-init"];
2422
2695
  var SKILL_ENTRY_FILE = "SKILL.md";
@@ -2424,7 +2697,7 @@ var SKILL_DIRECTORY_PREFIXES = ["./skills/", "../skills/"];
2424
2697
  function resolveSkillDirectory(moduleUrl, skillName, exists) {
2425
2698
  for (const prefix of SKILL_DIRECTORY_PREFIXES) {
2426
2699
  const directory = fileURLToPath(new URL(`${prefix}${skillName}`, moduleUrl));
2427
- if (exists(join2(directory, SKILL_ENTRY_FILE))) {
2700
+ if (exists(join5(directory, SKILL_ENTRY_FILE))) {
2428
2701
  return directory;
2429
2702
  }
2430
2703
  }
@@ -2461,6 +2734,7 @@ var JevGuardPlugin = async (input) => {
2461
2734
  proposalFacade: createOpenCodeProposalSessionFacade(input.client),
2462
2735
  navigator: createOpenCodeSessionNavigator(presentationClient),
2463
2736
  notifier: createRemediationNotifier(toast),
2737
+ failures: createRemediationFailureLogSink(presentationClient),
2464
2738
  jev: createFifoJevPort(createTypeSafeJevTransport(createCredentialProvider({
2465
2739
  environment: createProcessEnvironment(),
2466
2740
  store: createKeyringCredentialStore()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablozrrrr/jevguard",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "OpenCode plugin that reviews a completed agent turn against a repository semantic rule using Jev.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -165,11 +165,12 @@ remediation:
165
165
  auto_propose: true
166
166
  propose_on:
167
167
  - FAIL
168
- model: opencode/gpt-5.6-luna
169
168
  ```
170
169
 
171
- Do not invent thresholds, add keys, or change the model. The default configuration is
172
- valid and uses the documented defaults.
170
+ Do not invent thresholds or add keys. The default configuration is valid and uses the
171
+ documented defaults. Never pin `remediation.model`: when it is absent the proposer
172
+ inherits the host model, which is the only portable choice. A hard-coded provider
173
+ model can be absent on the user's machine and would make every proposal fail.
173
174
 
174
175
  ### 7. Preview both files and require explicit confirmation
175
176
 
@@ -910,7 +910,7 @@ var REMEDIATION_MAX_TASK_LENGTH = 16384;
910
910
  var DEFAULT_REMEDIATION_CONFIG = {
911
911
  autoPropose: true,
912
912
  proposeOn: ["FAIL"],
913
- model: "opencode/gpt-5.6-luna"
913
+ model: null
914
914
  };
915
915
 
916
916
  // packages/core/src/remediation/request.ts
@@ -1000,6 +1000,7 @@ function snapshotRule(rule) {
1000
1000
  // packages/core/src/remediation/config.ts
1001
1001
  var MODEL_PATTERN = /^[^\s/]+\/[^\s/]+$/;
1002
1002
  var KNOWN_KEYS = new Set(["auto_propose", "propose_on", "model"]);
1003
+ var INVALID_MODEL = Symbol("invalid-model");
1003
1004
  function resolveRemediationConfig(value) {
1004
1005
  if (value === null || value === undefined) {
1005
1006
  return valid(DEFAULT_REMEDIATION_CONFIG);
@@ -1022,8 +1023,8 @@ function resolveRemediationConfig(value) {
1022
1023
  if (proposeOn === null) {
1023
1024
  return invalid2();
1024
1025
  }
1025
- const model = remediation.model ?? DEFAULT_REMEDIATION_CONFIG.model;
1026
- if (typeof model !== "string" || !MODEL_PATTERN.test(model)) {
1026
+ const model = resolveModel(remediation.model);
1027
+ if (model === INVALID_MODEL) {
1027
1028
  return invalid2();
1028
1029
  }
1029
1030
  return valid({ autoPropose, proposeOn, model });
@@ -1038,6 +1039,15 @@ function parseModelSpecifier(model) {
1038
1039
  modelID: model.slice(separator + 1)
1039
1040
  };
1040
1041
  }
1042
+ function resolveModel(value) {
1043
+ if (value === undefined) {
1044
+ return null;
1045
+ }
1046
+ if (typeof value !== "string" || !MODEL_PATTERN.test(value)) {
1047
+ return INVALID_MODEL;
1048
+ }
1049
+ return value;
1050
+ }
1041
1051
  function readProposeOn(value) {
1042
1052
  if (value === undefined) {
1043
1053
  return DEFAULT_REMEDIATION_CONFIG.proposeOn;
@@ -910,7 +910,7 @@ var REMEDIATION_MAX_TASK_LENGTH = 16384;
910
910
  var DEFAULT_REMEDIATION_CONFIG = {
911
911
  autoPropose: true,
912
912
  proposeOn: ["FAIL"],
913
- model: "opencode/gpt-5.6-luna"
913
+ model: null
914
914
  };
915
915
 
916
916
  // packages/core/src/remediation/request.ts
@@ -1000,6 +1000,7 @@ function snapshotRule(rule) {
1000
1000
  // packages/core/src/remediation/config.ts
1001
1001
  var MODEL_PATTERN = /^[^\s/]+\/[^\s/]+$/;
1002
1002
  var KNOWN_KEYS = new Set(["auto_propose", "propose_on", "model"]);
1003
+ var INVALID_MODEL = Symbol("invalid-model");
1003
1004
  function resolveRemediationConfig(value) {
1004
1005
  if (value === null || value === undefined) {
1005
1006
  return valid(DEFAULT_REMEDIATION_CONFIG);
@@ -1022,8 +1023,8 @@ function resolveRemediationConfig(value) {
1022
1023
  if (proposeOn === null) {
1023
1024
  return invalid2();
1024
1025
  }
1025
- const model = remediation.model ?? DEFAULT_REMEDIATION_CONFIG.model;
1026
- if (typeof model !== "string" || !MODEL_PATTERN.test(model)) {
1026
+ const model = resolveModel(remediation.model);
1027
+ if (model === INVALID_MODEL) {
1027
1028
  return invalid2();
1028
1029
  }
1029
1030
  return valid({ autoPropose, proposeOn, model });
@@ -1038,6 +1039,15 @@ function parseModelSpecifier(model) {
1038
1039
  modelID: model.slice(separator + 1)
1039
1040
  };
1040
1041
  }
1042
+ function resolveModel(value) {
1043
+ if (value === undefined) {
1044
+ return null;
1045
+ }
1046
+ if (typeof value !== "string" || !MODEL_PATTERN.test(value)) {
1047
+ return INVALID_MODEL;
1048
+ }
1049
+ return value;
1050
+ }
1041
1051
  function readProposeOn(value) {
1042
1052
  if (value === undefined) {
1043
1053
  return DEFAULT_REMEDIATION_CONFIG.proposeOn;