@lawaty/lacode 2.3.0-beta.2 → 2.3.0-beta.3

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/CHANGELOG.md CHANGED
@@ -9,6 +9,20 @@ commit as the changelog entry it describes.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [2.3.0-beta.3] — persist autonomy modes in global config
13
+
14
+ ### Changed
15
+
16
+ - Modes: YOLO/YKB modes now persist in the lacode plugin options inside the global `opencode.jsonc` (single config file, comment-preserving edits).
17
+
18
+ ### Removed
19
+
20
+ - Modes: removed `~/.config/lacode/modes.json` and `LACODE_MODES_FILE` seeding.
21
+
22
+ ### Fixed
23
+
24
+ - Build: externalized the `jsonc-parser` runtime dependency.
25
+
12
26
  ## [2.3.0-beta.2] — degenerate refreshes never shrink the pool
13
27
 
14
28
  ### Added
@@ -1226,6 +1240,10 @@ experimental; `latest` remains 0.4.2.
1226
1240
  [Unreleased]: https://github.com/lawaty/lacode/compare/v2.1.0-beta.0...HEAD
1227
1241
  [2.1.0-beta.0]: https://github.com/lawaty/lacode/compare/v2.0.0-beta.0...v2.1.0-beta.0
1228
1242
  [2.0.0-beta.0]: https://github.com/lawaty/lacode/compare/v1.3.1...v2.0.0-beta.0
1243
+ [2.3.0-beta.3]: https://github.com/lawaty/lacode/compare/v2.3.0-beta.2...v2.3.0-beta.3
1244
+ [2.3.0-beta.2]: https://github.com/lawaty/lacode/compare/v2.3.0-beta.1...v2.3.0-beta.2
1245
+ [2.3.0-beta.1]: https://github.com/lawaty/lacode/compare/v2.3.0-beta.0...v2.3.0-beta.1
1246
+ [2.3.0-beta.0]: https://github.com/lawaty/lacode/releases/tag/v2.3.0-beta.0
1229
1247
  [1.3.1]: https://github.com/lawaty/lacode/releases/tag/v1.3.1
1230
1248
  [1.3.0-beta.9]: https://github.com/lawaty/lacode/releases/tag/v1.3.0-beta.9
1231
1249
  [1.3.0-beta.8]: https://github.com/lawaty/lacode/releases/tag/v1.3.0-beta.8
package/README.md CHANGED
@@ -156,15 +156,15 @@ option is optional:
156
156
  | `feedbackHint` | `true` | Show the one-time feedback note on the first interactive run. `false` — or `LACODE_NO_FEEDBACK_HINT=1` — turns it off. The flag lives in `~/.local/share/opencode/lacode/feedback.json`; nothing is sent anywhere. |
157
157
  | `sound` | `true` | Play a short tone with each notification, one per outcome, so you can tell a finished session from a question or a permission prompt without reading the popup. It is opt-out: `false` — or `LACODE_SOUND=0` (`false`, `off`, `no` work too, and beat the option) — turns the sound off, and anything else, a typo included, leaves it on. |
158
158
  | `omniroute` | — | Optional [OmniRoute](docs/OMNIROUTE.md) gateway integration. Absent or `{ enabled: false }` changes nothing. |
159
- | `yolo` | `false` | Start with YOLO mode on: permission requests that would ask are auto-approved. Explicit `deny` rules still win. Toggle live with `/yolo`. |
160
- | `ykb` | `false` | Start with YKB (You Know Better) mode on: the agent is told not to ask, and questions that still appear are answered with the recommended option. Toggle live with `/ykb`. |
159
+ | `yolo` | `false` | YOLO mode: permission requests that would ask are auto-approved. Explicit `deny` rules still win. The TUI writes this option when you run `/yolo`. |
160
+ | `ykb` | `false` | YKB (You Know Better) mode: the agent is told not to ask, and questions that still appear are answered with the recommended option. The TUI writes this option when you run `/ykb`. |
161
161
  | `watchMs` | `1000` | How often the pool files are checked for changes. `0` disables live reload. |
162
162
  | `watchDebounceMs` | `250` | Collapse a burst of editor writes into one reload. |
163
163
  | `imagePaste` | — | Image attachments: `maxBytes` (4 MiB), `maxImages` (10) and `dir`. |
164
164
 
165
165
  ## Modes
166
166
 
167
- Two runtime modes can be seeded from plugin options and toggled from the TUI:
167
+ Two runtime modes live on the lacode plugin entry and can be toggled from the TUI:
168
168
 
169
169
  | Command | Effect |
170
170
  | --- | --- |
@@ -172,7 +172,15 @@ Two runtime modes can be seeded from plugin options and toggled from the TUI:
172
172
  | `/ykb` | Toggle YKB (You Know Better). The agent is told not to ask questions and to continue with its recommendation. A question that still appears is answered with its `(Recommended)` option, or its first option when none is marked. |
173
173
  | `/modes` | Show both switches. |
174
174
 
175
- The toggles write `~/.config/lacode/modes.json` (override with `LACODE_MODES_FILE`) and the server picks the change up on the next decision — no restart. Turning a mode on also answers requests that are already pending. The `yolo` / `ykb` plugin options only seed the file on first run; after that, the file wins. A free-text question with no options is left for the user, since there is no option to choose.
175
+ ```jsonc
176
+ {
177
+ "plugin": [
178
+ ["@lawaty/lacode", { "yolo": true, "ykb": false }]
179
+ ]
180
+ }
181
+ ```
182
+
183
+ The toggles edit the `yolo` / `ykb` options on the lacode plugin entry in your global opencode config (`~/.config/opencode/opencode.jsonc`, or `$XDG_CONFIG_HOME/opencode/opencode.jsonc`), preserving comments. The server reads that file on the next decision — no restart. Turning a mode on also answers requests that are already pending. The lacode plugin entry must already exist in the global config; a project-only entry cannot be toggled. A free-text question with no options is left for the user, since there is no option to choose.
176
184
 
177
185
  ### Staying up to date
178
186
 
package/dist/index.js CHANGED
@@ -2282,9 +2282,9 @@ var STATE_DIR = join3(homedir2(), ".local", "share", "opencode", "lacode");
2282
2282
  var STATE_PATH = join3(STATE_DIR, "selfupdate.json");
2283
2283
  var LOCK_PATH = join3(STATE_DIR, "selfupdate.lock");
2284
2284
  function compareSemver(a, b) {
2285
- const parse = (v) => v.split("-")[0].split("+")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
2286
- const left = parse(a);
2287
- const right = parse(b);
2285
+ const parse2 = (v) => v.split("-")[0].split("+")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
2286
+ const left = parse2(a);
2287
+ const right = parse2(b);
2288
2288
  for (let i = 0; i < 3; i++) {
2289
2289
  const l = left[i] ?? 0;
2290
2290
  const r = right[i] ?? 0;
@@ -6167,35 +6167,82 @@ async function imagePasteHooks(input, options, deps) {
6167
6167
  }
6168
6168
  }
6169
6169
 
6170
- // src/lib/modes.ts
6170
+ // src/lib/opencode-config.ts
6171
6171
  import { existsSync as existsSync5, mkdirSync as mkdirSync9, readFileSync as readFileSync10, renameSync as renameSync7, writeFileSync as writeFileSync10 } from "node:fs";
6172
6172
  import { homedir as homedir8 } from "node:os";
6173
6173
  import { dirname as dirname6, join as join12 } from "node:path";
6174
- var MODES_FILE = process.env.LACODE_MODES_FILE ?? join12(homedir8(), ".config", "lacode", "modes.json");
6174
+ import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
6175
+ function globalOpencodeConfigPath() {
6176
+ const base = process.env.XDG_CONFIG_HOME ?? join12(homedir8(), ".config");
6177
+ const jsonc = join12(base, "opencode", "opencode.jsonc");
6178
+ return existsSync5(jsonc) ? jsonc : join12(base, "opencode", "opencode.json");
6179
+ }
6180
+ var OpenCodeConfigError = class extends Error {
6181
+ constructor(message) {
6182
+ super(message);
6183
+ this.name = "OpenCodeConfigError";
6184
+ }
6185
+ };
6186
+ function isPlainObject(value) {
6187
+ return !!value && typeof value === "object" && !Array.isArray(value);
6188
+ }
6189
+ function readOpencodeConfig(path4 = globalOpencodeConfigPath()) {
6190
+ if (!existsSync5(path4)) return {};
6191
+ const text = readFileSync10(path4, "utf8");
6192
+ const errors = [];
6193
+ const parsed = parse(text, errors, { allowTrailingComma: true, disallowComments: false });
6194
+ if (errors.length > 0) {
6195
+ const detail = errors.map((e) => `${printParseErrorCode(e.error)} at offset ${e.offset}`).join(", ");
6196
+ throw new OpenCodeConfigError(`Failed to parse ${path4}: ${detail}`);
6197
+ }
6198
+ if (!isPlainObject(parsed)) {
6199
+ throw new OpenCodeConfigError(`Config root must be an object: ${path4}`);
6200
+ }
6201
+ return parsed;
6202
+ }
6203
+ function pluginName(entry) {
6204
+ if (typeof entry === "string") return entry;
6205
+ if (Array.isArray(entry) && typeof entry[0] === "string") return entry[0];
6206
+ return void 0;
6207
+ }
6208
+ function matchesLacode(name) {
6209
+ if (name === "@lawaty/lacode") return true;
6210
+ const segments = name.split(/[\\/]/);
6211
+ return segments.some((s) => s.toLowerCase() === "lacode");
6212
+ }
6213
+ function findLacodePlugin(config) {
6214
+ const plugins = config.plugin;
6215
+ if (!Array.isArray(plugins)) return void 0;
6216
+ for (let index = 0; index < plugins.length; index++) {
6217
+ const entry = plugins[index];
6218
+ const name = pluginName(entry);
6219
+ if (!name || !matchesLacode(name)) continue;
6220
+ const options = Array.isArray(entry) && isPlainObject(entry[1]) ? entry[1] : void 0;
6221
+ return { index, name, options };
6222
+ }
6223
+ return void 0;
6224
+ }
6225
+ function readModeOptions(config = readOpencodeConfig()) {
6226
+ const entry = findLacodePlugin(config);
6227
+ const options = entry?.options;
6228
+ const yolo = typeof options?.yolo === "boolean" ? options.yolo : void 0;
6229
+ const ykb = typeof options?.ykb === "boolean" ? options.ykb : void 0;
6230
+ return { yolo, ykb };
6231
+ }
6232
+
6233
+ // src/lib/modes.ts
6175
6234
  var YKB_INSTRUCTION = "YKB MODE (You Know Better) is ON. Do not ask the user questions, request clarification, or wait for confirmation. When a decision is needed, choose the option you would recommend, state the choice briefly, and continue until the task is complete. Stop only if you are genuinely blocked by a missing credential, an unrecoverable error, or a permission that cannot be granted.";
6176
- var bool = (value, fallback) => typeof value === "boolean" ? value : fallback;
6177
6235
  function readModes(defaults = {}) {
6178
- const fallback = { yolo: defaults.yolo === true, ykb: defaults.ykb === true };
6179
6236
  try {
6180
- const raw = JSON.parse(readFileSync10(MODES_FILE, "utf8"));
6181
- return { yolo: bool(raw?.yolo, fallback.yolo), ykb: bool(raw?.ykb, fallback.ykb) };
6237
+ const options = readModeOptions();
6238
+ return {
6239
+ yolo: options.yolo ?? defaults.yolo === true,
6240
+ ykb: options.ykb ?? defaults.ykb === true
6241
+ };
6182
6242
  } catch {
6183
- return fallback;
6243
+ return { yolo: defaults.yolo === true, ykb: defaults.ykb === true };
6184
6244
  }
6185
6245
  }
6186
- function writeModes(modes) {
6187
- const path4 = MODES_FILE;
6188
- mkdirSync9(dirname6(path4), { recursive: true });
6189
- const tmp = `${path4}.tmp`;
6190
- writeFileSync10(tmp, `${JSON.stringify({ yolo: modes.yolo, ykb: modes.ykb }, null, 2)}
6191
- `);
6192
- renameSync7(tmp, path4);
6193
- }
6194
- function seedModes(defaults) {
6195
- if (existsSync5(MODES_FILE)) return;
6196
- if (defaults.yolo !== true && defaults.ykb !== true) return;
6197
- writeModes({ yolo: defaults.yolo === true, ykb: defaults.ykb === true });
6198
- }
6199
6246
  function recommendedAnswer(question) {
6200
6247
  const q = question;
6201
6248
  const options = (Array.isArray(q?.options) ? q.options : []).filter(
@@ -6214,7 +6261,6 @@ async function modesHooks(input, options) {
6214
6261
  const defaults = {};
6215
6262
  if (typeof opts.yolo === "boolean") defaults.yolo = opts.yolo;
6216
6263
  if (typeof opts.ykb === "boolean") defaults.ykb = opts.ykb;
6217
- seedModes(defaults);
6218
6264
  const state2 = () => readModes(defaults);
6219
6265
  const warn2 = (message) => {
6220
6266
  input.client.app.log({ body: { service: "modes", level: "warn", message } }).catch(() => {
package/dist/tui.js CHANGED
@@ -943,28 +943,129 @@ var plugin = {
943
943
  };
944
944
  var timeline_default = plugin;
945
945
 
946
- // src/lib/modes.ts
946
+ // src/lib/opencode-config.ts
947
947
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
948
948
  import { homedir as homedir3 } from "node:os";
949
949
  import { dirname, join as join4 } from "node:path";
950
- var MODES_FILE = process.env.LACODE_MODES_FILE ?? join4(homedir3(), ".config", "lacode", "modes.json");
951
- var bool = (value, fallback) => typeof value === "boolean" ? value : fallback;
950
+ import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
951
+ function globalOpencodeConfigPath() {
952
+ const base = process.env.XDG_CONFIG_HOME ?? join4(homedir3(), ".config");
953
+ const jsonc = join4(base, "opencode", "opencode.jsonc");
954
+ return existsSync2(jsonc) ? jsonc : join4(base, "opencode", "opencode.json");
955
+ }
956
+ var OpenCodeConfigError = class extends Error {
957
+ constructor(message) {
958
+ super(message);
959
+ this.name = "OpenCodeConfigError";
960
+ }
961
+ };
962
+ function isPlainObject(value) {
963
+ return !!value && typeof value === "object" && !Array.isArray(value);
964
+ }
965
+ function readOpencodeConfig(path = globalOpencodeConfigPath()) {
966
+ if (!existsSync2(path)) return {};
967
+ const text = readFileSync3(path, "utf8");
968
+ const errors = [];
969
+ const parsed = parse(text, errors, { allowTrailingComma: true, disallowComments: false });
970
+ if (errors.length > 0) {
971
+ const detail = errors.map((e) => `${printParseErrorCode(e.error)} at offset ${e.offset}`).join(", ");
972
+ throw new OpenCodeConfigError(`Failed to parse ${path}: ${detail}`);
973
+ }
974
+ if (!isPlainObject(parsed)) {
975
+ throw new OpenCodeConfigError(`Config root must be an object: ${path}`);
976
+ }
977
+ return parsed;
978
+ }
979
+ function pluginName(entry) {
980
+ if (typeof entry === "string") return entry;
981
+ if (Array.isArray(entry) && typeof entry[0] === "string") return entry[0];
982
+ return void 0;
983
+ }
984
+ function matchesLacode(name) {
985
+ if (name === "@lawaty/lacode") return true;
986
+ const segments = name.split(/[\\/]/);
987
+ return segments.some((s) => s.toLowerCase() === "lacode");
988
+ }
989
+ function findLacodePlugin(config) {
990
+ const plugins = config.plugin;
991
+ if (!Array.isArray(plugins)) return void 0;
992
+ for (let index = 0; index < plugins.length; index++) {
993
+ const entry = plugins[index];
994
+ const name = pluginName(entry);
995
+ if (!name || !matchesLacode(name)) continue;
996
+ const options = Array.isArray(entry) && isPlainObject(entry[1]) ? entry[1] : void 0;
997
+ return { index, name, options };
998
+ }
999
+ return void 0;
1000
+ }
1001
+ function readModeOptions(config = readOpencodeConfig()) {
1002
+ const entry = findLacodePlugin(config);
1003
+ const options = entry?.options;
1004
+ const yolo = typeof options?.yolo === "boolean" ? options.yolo : void 0;
1005
+ const ykb = typeof options?.ykb === "boolean" ? options.ykb : void 0;
1006
+ return { yolo, ykb };
1007
+ }
1008
+ function writeModeOptions(patch, path = globalOpencodeConfigPath()) {
1009
+ if (!existsSync2(path)) {
1010
+ throw new OpenCodeConfigError(`Cannot write mode options: config does not exist: ${path}`);
1011
+ }
1012
+ let text = readFileSync3(path, "utf8");
1013
+ const errors = [];
1014
+ const parsed = parse(text, errors, { allowTrailingComma: true, disallowComments: false });
1015
+ if (errors.length > 0) {
1016
+ const detail = errors.map((e) => `${printParseErrorCode(e.error)} at offset ${e.offset}`).join(", ");
1017
+ throw new OpenCodeConfigError(`Failed to parse ${path}: ${detail}`);
1018
+ }
1019
+ if (!isPlainObject(parsed)) {
1020
+ throw new OpenCodeConfigError(`Config root must be an object: ${path}`);
1021
+ }
1022
+ const entry = findLacodePlugin(parsed);
1023
+ if (!entry) {
1024
+ throw new OpenCodeConfigError(`lacode plugin entry not found in ${path}`);
1025
+ }
1026
+ const formattingOptions = { insertSpaces: true, tabSize: 2, eol: "\n" };
1027
+ const pluginEntry = parsed.plugin[entry.index];
1028
+ if (typeof pluginEntry === "string") {
1029
+ const obj = {};
1030
+ if (patch.yolo !== void 0) obj.yolo = patch.yolo;
1031
+ if (patch.ykb !== void 0) obj.ykb = patch.ykb;
1032
+ const edits = modify(text, ["plugin", entry.index], [entry.name, obj], { formattingOptions });
1033
+ text = applyEdits(text, edits);
1034
+ } else if (Array.isArray(pluginEntry) && !isPlainObject(pluginEntry[1])) {
1035
+ const obj = {};
1036
+ if (patch.yolo !== void 0) obj.yolo = patch.yolo;
1037
+ if (patch.ykb !== void 0) obj.ykb = patch.ykb;
1038
+ const edits = modify(text, ["plugin", entry.index, 1], obj, { formattingOptions });
1039
+ text = applyEdits(text, edits);
1040
+ } else {
1041
+ const keys = ["yolo", "ykb"];
1042
+ for (const key of keys) {
1043
+ const value = patch[key];
1044
+ if (value === void 0) continue;
1045
+ const edits = modify(text, ["plugin", entry.index, 1, key], value, { formattingOptions });
1046
+ text = applyEdits(text, edits);
1047
+ }
1048
+ }
1049
+ mkdirSync(dirname(path), { recursive: true });
1050
+ const tmp = `${path}.tmp`;
1051
+ writeFileSync(tmp, text);
1052
+ renameSync(tmp, path);
1053
+ }
1054
+
1055
+ // src/lib/modes.ts
952
1056
  function readModes(defaults = {}) {
953
- const fallback = { yolo: defaults.yolo === true, ykb: defaults.ykb === true };
954
1057
  try {
955
- const raw = JSON.parse(readFileSync3(MODES_FILE, "utf8"));
956
- return { yolo: bool(raw?.yolo, fallback.yolo), ykb: bool(raw?.ykb, fallback.ykb) };
1058
+ const options = readModeOptions();
1059
+ return {
1060
+ yolo: options.yolo ?? defaults.yolo === true,
1061
+ ykb: options.ykb ?? defaults.ykb === true
1062
+ };
957
1063
  } catch {
958
- return fallback;
1064
+ return { yolo: defaults.yolo === true, ykb: defaults.ykb === true };
959
1065
  }
960
1066
  }
961
1067
  function writeModes(modes) {
962
- const path = MODES_FILE;
963
- mkdirSync(dirname(path), { recursive: true });
964
- const tmp = `${path}.tmp`;
965
- writeFileSync(tmp, `${JSON.stringify({ yolo: modes.yolo, ykb: modes.ykb }, null, 2)}
966
- `);
967
- renameSync(tmp, path);
1068
+ writeModeOptions({ yolo: modes.yolo, ykb: modes.ykb });
968
1069
  }
969
1070
  function toggleMode(name, defaults = {}) {
970
1071
  const current = readModes(defaults);
@@ -52,7 +52,7 @@ restart.
52
52
 
53
53
  ## Modes
54
54
 
55
- Two runtime modes can be seeded from plugin options and toggled from the TUI:
55
+ Two runtime modes live on the lacode plugin entry and can be toggled from the TUI:
56
56
 
57
57
  | Command | Effect |
58
58
  | --- | --- |
@@ -60,7 +60,15 @@ Two runtime modes can be seeded from plugin options and toggled from the TUI:
60
60
  | `/ykb` | Toggle YKB (You Know Better). The agent is told not to ask questions and to continue with its recommendation. A question that still appears is answered with its `(Recommended)` option, or its first option when none is marked. |
61
61
  | `/modes` | Show both switches. |
62
62
 
63
- The toggles write `~/.config/lacode/modes.json` (override with `LACODE_MODES_FILE`) and the server picks the change up on the next decision — no restart. Turning a mode on also answers requests that are already pending. The `yolo` / `ykb` plugin options only seed the file on first run; after that, the file wins. A free-text question with no options is left for the user, since there is no option to choose.
63
+ ```jsonc
64
+ {
65
+ "plugin": [
66
+ ["@lawaty/lacode", { "yolo": true, "ykb": false }]
67
+ ]
68
+ }
69
+ ```
70
+
71
+ The toggles edit the `yolo` / `ykb` options on the lacode plugin entry in your global opencode config (`~/.config/opencode/opencode.jsonc`, or `$XDG_CONFIG_HOME/opencode/opencode.jsonc`), preserving comments. The server reads that file on the next decision — no restart. Turning a mode on also answers requests that are already pending. The lacode plugin entry must already exist in the global config; a project-only entry cannot be toggled. A free-text question with no options is left for the user, since there is no option to choose.
64
72
 
65
73
 
66
74
  ## Which models run which agent
@@ -569,15 +569,15 @@ A failed summarization used to clear the pending flag and never reschedule, so o
569
569
  ### US-37: YOLO mode approves permissions without stopping
570
570
  **As a** user running a long autonomous task, **I want** permission prompts to be approved automatically while YOLO is on, **so that** the agent keeps working instead of waiting for routine approvals, while an explicit `deny` still protects me.
571
571
 
572
- Permission prompts are the main thing that breaks a long autonomous run: every `ask` blocks the session until a human answers. YOLO flips that decision to `allow` at the permission hook, keeps an explicit `deny` authoritative, and answers any request that still reaches the event bus. The state is a small file the TUI writes and the server reads on the next decision, so enabling it also clears what is already pending.
572
+ Permission prompts are the main thing that breaks a long autonomous run: every `ask` blocks the session until a human answers. YOLO flips that decision to `allow` at the permission hook, keeps an explicit `deny` authoritative, and answers any request that still reaches the event bus. The state lives in the `yolo` option on the lacode plugin entry in the global `opencode.jsonc`; the TUI writes it and the server reads it on the next decision, so enabling it also clears what is already pending.
573
573
 
574
574
  **Story ID(s) for tests:** US-37
575
575
  **Acceptance criteria:**
576
576
  - [ ] Given YOLO is on, When the runtime's permission decision is `ask`, Then it becomes `allow` without a prompt, while an existing `deny` or `allow` is left unchanged.
577
577
  - [ ] Given YOLO is off, When the runtime's permission decision is `ask`, Then it stays `ask` and still reaches the user.
578
578
  - [ ] Given a `permission.asked` event still reaches the event bus while YOLO is on and carries a request id and a session id, When the handler runs, Then the plugin replies `once` for that request; an event missing either id is ignored.
579
- - [ ] Given YOLO is toggled in the TUI, When the mode file changes, Then the next permission decision sees the new value without a restart, and requests already pending are answered.
580
- - [ ] Given `yolo: true` in the plugin options and no live mode file, When the plugin starts, Then the file is seeded; after a TUI toggle, the live file wins.
579
+ - [ ] Given YOLO is toggled in the TUI, When the global `opencode.jsonc` changes, Then the next permission decision sees the new value without a restart, and requests already pending are answered.
580
+ - [ ] Given the TUI toggles a mode, When it writes the global `opencode.jsonc`, Then only the `yolo` / `ykb` options on the lacode plugin entry change, comments and formatting are preserved, and the config remains the single source of truth.
581
581
  - [ ] Given the README and configuration reference, Then the `yolo` option and the `/yolo` command are documented alongside `/modes`.
582
582
 
583
583
  ### US-38: YKB mode chooses the recommendation instead of asking
@@ -654,6 +654,6 @@ The root-level suites under `tests/` are unit-style; the `tests/stories/` suites
654
654
  the behavioural layer, written one epic per file with every test name carrying
655
655
  its story id. Run everything with `npm test`.
656
656
 
657
- 40/40 stories covered by 375 test case(s); thinnest story has 1.
657
+ 40/40 stories covered by 376 test case(s); thinnest story has 1.
658
658
  No story is untested. The scan covers the `tests/stories/` suites and the root-level
659
659
  `tests/*-test.mjs` suites.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawaty/lacode",
3
- "version": "2.3.0-beta.2",
3
+ "version": "2.3.0-beta.3",
4
4
  "description": "Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -68,8 +68,8 @@
68
68
  },
69
69
  "peerDependencies": {
70
70
  "@opencode-ai/plugin": "1.18.34",
71
- "solid-js": "*",
72
- "@opentui/solid": "*"
71
+ "@opentui/solid": "*",
72
+ "solid-js": "*"
73
73
  },
74
74
  "peerDependenciesMeta": {
75
75
  "solid-js": {
@@ -85,5 +85,8 @@
85
85
  },
86
86
  "publishConfig": {
87
87
  "tag": "latest"
88
+ },
89
+ "dependencies": {
90
+ "jsonc-parser": "^3.3.1"
88
91
  }
89
92
  }