@kisev/agentomatic 11.0.2 → 12.0.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 (64) hide show
  1. package/README.md +32 -16
  2. package/README.ru.md +33 -16
  3. package/dist/agent-profiles.d.ts +8 -2
  4. package/dist/agent-profiles.js +131 -82
  5. package/dist/assets/agents/architect.md +4 -4
  6. package/dist/assets/agents/critic.md +10 -11
  7. package/dist/assets/agents/manager.md +11 -12
  8. package/dist/assets/agents/mapper.md +4 -4
  9. package/dist/assets/agents/review.md +11 -12
  10. package/dist/assets/agents/worker.md +29 -32
  11. package/dist/assets/commands/asd-ste100.md +10 -0
  12. package/dist/assets/commands/code-simplify.md +12 -0
  13. package/dist/assets/commands/eli5.md +10 -0
  14. package/dist/assets/commands/handoff.md +10 -0
  15. package/dist/assets/commands/rtk-stats.md +2 -2
  16. package/dist/assets/commands/skill-doctor.md +10 -0
  17. package/dist/assets/migration-inventory.json +17 -6
  18. package/dist/assets/plugins/code-simplify.js +3 -0
  19. package/dist/assets/plugins/rtk.js +1 -1
  20. package/dist/assets/plugins/rules-injector.js +1 -1
  21. package/dist/assets/plugins/zed-bell.js +1 -1
  22. package/dist/catalog.d.ts +2 -2
  23. package/dist/catalog.js +9 -3
  24. package/dist/cli-output.js +6 -6
  25. package/dist/cli.d.ts +1 -1
  26. package/dist/cli.js +1 -1257
  27. package/dist/command-cli.d.ts +1 -0
  28. package/dist/command-cli.js +1004 -0
  29. package/dist/config-setup.d.ts +14 -8
  30. package/dist/config-setup.js +197 -64
  31. package/dist/contracts.js +26 -0
  32. package/dist/doctor.d.ts +0 -1
  33. package/dist/doctor.js +42 -55
  34. package/dist/index.d.ts +13 -118
  35. package/dist/index.js +118 -125
  36. package/dist/installer.d.ts +25 -2
  37. package/dist/installer.js +68 -23
  38. package/dist/jsonc.d.ts +18 -2
  39. package/dist/jsonc.js +109 -5
  40. package/dist/opencode-config.d.ts +11 -0
  41. package/dist/opencode-config.js +237 -0
  42. package/dist/plugins/code-simplify.d.ts +25 -0
  43. package/dist/plugins/code-simplify.js +49 -0
  44. package/dist/plugins/events.d.ts +20 -0
  45. package/dist/plugins/events.js +48 -0
  46. package/dist/plugins/rtk.d.ts +9 -12
  47. package/dist/plugins/rtk.js +20 -5
  48. package/dist/plugins/rules-injector.d.ts +21 -11
  49. package/dist/plugins/rules-injector.js +43 -31
  50. package/dist/plugins/zed-bell.d.ts +8 -5
  51. package/dist/plugins/zed-bell.js +10 -2
  52. package/dist/reconcile.d.ts +4 -1
  53. package/dist/reconcile.js +12 -16
  54. package/dist/registry.d.ts +1 -0
  55. package/dist/registry.js +21 -5
  56. package/dist/routing.d.ts +5 -3
  57. package/dist/routing.js +57 -22
  58. package/dist/self-install.d.ts +7 -0
  59. package/dist/self-install.js +52 -1
  60. package/dist/terminal-wizard.d.ts +1 -1
  61. package/dist/terminal-wizard.js +2 -2
  62. package/package.json +11 -6
  63. package/dist/assets/commands/skill-improve.md +0 -10
  64. package/dist/assets/commands/stopit.md +0 -10
@@ -6,20 +6,20 @@ export declare const CONFIG_TARGETS: readonly ["opencode", "kilo", "mimo"];
6
6
  export type ConfigTargetName = (typeof CONFIG_TARGETS)[number];
7
7
  export type TargetFileKind = "main" | "tui";
8
8
  export declare const CONFIG_FRAGMENTS: readonly [{
9
+ readonly name: "core-disable";
10
+ readonly description: "Disconnect only the agentomatic plugin, preserving unrelated configuration";
11
+ readonly targets: readonly ["opencode"];
12
+ readonly file: "main";
13
+ }, {
9
14
  readonly name: "core-plugin";
10
15
  readonly description: "Register @kisev/agentomatic in the plugin array (OpenCode only)";
11
16
  readonly targets: readonly ["opencode"];
12
17
  readonly file: "main";
13
18
  }, {
14
19
  readonly name: "skills-state-permissions";
15
- readonly description: "Allow the standard skills XDG state paths without per-run prompts";
20
+ readonly description: "Allow the canonical ~/.agents/skills tree, legacy skills state paths, and reviewmatic-managed review worktrees without per-run prompts";
16
21
  readonly targets: readonly ["opencode", "kilo", "mimo"];
17
22
  readonly file: "main";
18
- }, {
19
- readonly name: "lsp-preset";
20
- readonly description: "Add LSP servers from the shared catalog (OpenCode only)";
21
- readonly targets: readonly ["opencode"];
22
- readonly file: "main";
23
23
  }, {
24
24
  readonly name: "secrets-guard";
25
25
  readonly description: "Deny reads and edits of common secret files";
@@ -32,7 +32,7 @@ export declare const CONFIG_FRAGMENTS: readonly [{
32
32
  readonly file: "main";
33
33
  }, {
34
34
  readonly name: "tui-schema";
35
- readonly description: "Unify each agent TUI file: schema, theme, stacked diffs, and leader keybinds";
35
+ readonly description: "Configure terminal settings: OpenCode V2 cli.json or Kilo/MiMo TUI files";
36
36
  readonly targets: readonly ["opencode", "kilo", "mimo"];
37
37
  readonly file: "tui";
38
38
  }];
@@ -77,9 +77,15 @@ export declare function recoverConfigSetup(scope: Scope, dryRun: boolean, cwd?:
77
77
  }>;
78
78
  export declare function normalizeConfigSelection(scope: Scope, value?: Partial<ConfigSetupSelection>): ConfigSetupSelection;
79
79
  export declare function defaultConfigSelection(scope: Scope, cwd?: string, home?: string): Promise<ConfigSetupSelection>;
80
- export declare function previewConfigSetup(selection: ConfigSetupSelection, scope: Scope, cwd?: string, home?: string, provisionDependency?: boolean): Promise<ConfigSetupPlan>;
80
+ export declare function inspectIntegration(scope: Scope, cwd?: string, home?: string): Promise<{
81
+ path: string;
82
+ connected: boolean;
83
+ problem?: string;
84
+ }>;
85
+ export declare function previewConfigSetup(selection: ConfigSetupSelection, scope: Scope, cwd?: string, home?: string, provisionDependency?: boolean, syncSelection?: boolean): Promise<ConfigSetupPlan>;
81
86
  export declare function applyConfigSetup(selection: ConfigSetupSelection, scope: Scope, cwd?: string, home?: string, options?: {
82
87
  dependencyRunner?: DependencyRunner;
83
88
  provisionDependency?: boolean;
84
89
  receipt?: string;
90
+ syncSelection?: boolean;
85
91
  }): Promise<ConfigSetupPlan>;
@@ -1,19 +1,30 @@
1
- import { readFileSync } from "node:fs";
2
1
  import { randomUUID } from "node:crypto";
3
2
  import { lstat } from "node:fs/promises";
4
3
  import { homedir } from "node:os";
5
- import { dirname, join, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
- import { archiveMutations } from "./installer.js";
4
+ import { join, resolve } from "node:path";
5
+ import { archiveMutations, coreSelectionMutation } from "./installer.js";
8
6
  import { requirePackageVersion } from "./package-metadata.js";
9
7
  import { ensureDependency, planDependency } from "./self-install.js";
10
8
  import { applyTransaction, archiveRoot, deploymentRoot, LifecycleError, lifecycleRoot, inspectTransaction, readRegular, recoverTransaction, sha256, withLifecycleLock, } from "./lifecycle.js";
11
9
  import { applyJsoncEdits, parseJsonc } from "./jsonc.js";
12
- const PACKAGE_NAME = "@kisev/agentomatic";
13
- const LEGACY_PACKAGE_NAME = "@kisev/skills-opencode";
14
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
+ import { corePluginEdits, corePluginRemovalEdits, permissionEdits, } from "./opencode-config.js";
15
11
  const XDG_STATE_GLOB = "~/.local/state/agent-skills/**";
16
12
  const OPENCODE_SKILLS_GLOB = "~/.config/opencode/skills/**";
13
+ const AGENTS_SKILLS_GLOB = "~/.agents/skills/**";
14
+ // Structural pattern for reviewmatic-managed review worktrees: one managed
15
+ // catalog per repository as a sibling of the checkout (`<repo>.worktrees/
16
+ // reviewmatic/<hash>`). No per-repository or per-MR path is predetermined, and
17
+ // a typo'd sibling state directory such as `agent-skill` never matches this
18
+ // pattern or the state globs above.
19
+ const WORKTREE_TREE_GLOB = "*.worktrees/reviewmatic/**";
20
+ const WORKTREE_ROOT = "*.worktrees/reviewmatic";
21
+ const WORKTREE_BASE = "*.worktrees";
22
+ // V2 checks external paths against a canonical directory boundary that
23
+ // normally ends in `/*`, so the exact roots above never match an actual
24
+ // worktree access. This boundary pattern covers the managed reviewmatic
25
+ // catalog and every managed worktree inside it, without opening sibling
26
+ // directories of other tools under `<repo>.worktrees`.
27
+ const WORKTREE_BOUNDARY_GLOB = "*.worktrees/reviewmatic/*";
17
28
  const SECRET_PATHS = [
18
29
  "**/.env",
19
30
  "**/.env.*",
@@ -40,6 +51,12 @@ export class ConfigSetupError extends LifecycleError {
40
51
  }
41
52
  export const CONFIG_TARGETS = ["opencode", "kilo", "mimo"];
42
53
  export const CONFIG_FRAGMENTS = [
54
+ {
55
+ name: "core-disable",
56
+ description: "Disconnect only the agentomatic plugin, preserving unrelated configuration",
57
+ targets: ["opencode"],
58
+ file: "main",
59
+ },
43
60
  {
44
61
  name: "core-plugin",
45
62
  description: "Register @kisev/agentomatic in the plugin array (OpenCode only)",
@@ -48,16 +65,10 @@ export const CONFIG_FRAGMENTS = [
48
65
  },
49
66
  {
50
67
  name: "skills-state-permissions",
51
- description: "Allow the standard skills XDG state paths without per-run prompts",
68
+ description: "Allow the canonical ~/.agents/skills tree, legacy skills state paths, and reviewmatic-managed review worktrees without per-run prompts",
52
69
  targets: ["opencode", "kilo", "mimo"],
53
70
  file: "main",
54
71
  },
55
- {
56
- name: "lsp-preset",
57
- description: "Add LSP servers from the shared catalog (OpenCode only)",
58
- targets: ["opencode"],
59
- file: "main",
60
- },
61
72
  {
62
73
  name: "secrets-guard",
63
74
  description: "Deny reads and edits of common secret files",
@@ -72,7 +83,7 @@ export const CONFIG_FRAGMENTS = [
72
83
  },
73
84
  {
74
85
  name: "tui-schema",
75
- description: "Unify each agent TUI file: schema, theme, stacked diffs, and leader keybinds",
86
+ description: "Configure terminal settings: OpenCode V2 cli.json or Kilo/MiMo TUI files",
76
87
  targets: ["opencode", "kilo", "mimo"],
77
88
  file: "tui",
78
89
  },
@@ -94,16 +105,43 @@ async function configSnapshot(built) {
94
105
  }
95
106
  async function requireNoRecovery(stateRoot) {
96
107
  if (await readRegular(join(stateRoot, "transaction-journal.json")))
97
- throw new ConfigSetupError("recovery_required", "An interrupted transaction requires config recover --dry-run, then confirmed config recover before a fresh preview");
108
+ throw new ConfigSetupError("recovery_required", "An interrupted transaction requires maintenance recover --dry-run, then confirmed maintenance recover before a fresh preview");
98
109
  }
99
110
  export async function recoverConfigSetup(scope, dryRun, cwd = process.cwd(), home = homedir(), expectedDigest) {
100
111
  const root = deploymentRoot(scope, cwd, home);
101
112
  const state = lifecycleRoot(scope, cwd, home);
102
- const built = await build({ targets: [...CONFIG_TARGETS], fragments: [...FRAGMENT_NAMES] }, scope, cwd, home);
103
- const allowed = [...built.allowedRoots, archiveRoot(scope, cwd, home)];
104
- const paths = await inspectTransaction(root, state, allowed);
105
113
  const journalPath = join(state, "transaction-journal.json");
106
114
  const journal = await readRegular(journalPath);
115
+ const scopeRoot = resolve(scope === "global" ? home : cwd);
116
+ let recoveryRoot = root;
117
+ if (journal) {
118
+ let candidate;
119
+ try {
120
+ candidate = JSON.parse(journal.toString("utf8"));
121
+ }
122
+ catch {
123
+ throw new ConfigSetupError("invalid_journal", "Transaction journal is not valid JSON");
124
+ }
125
+ if (candidate.root === scopeRoot && scopeRoot !== root) {
126
+ const prefix = scope === "global" ? ".config/opencode/" : ".opencode/";
127
+ if (!Array.isArray(candidate.operations) ||
128
+ candidate.operations.some((operation) => !operation.root && !operation.path?.startsWith(prefix)))
129
+ throw new ConfigSetupError("invalid_journal", "Cleanup journal contains paths outside the deployment");
130
+ recoveryRoot = scopeRoot;
131
+ }
132
+ }
133
+ const allowed = [
134
+ root,
135
+ archiveRoot(scope, cwd, home),
136
+ ...(scope === "project"
137
+ ? [resolve(cwd)]
138
+ : [
139
+ join(home, ".config", "opencode"),
140
+ join(home, ".config", "kilo"),
141
+ join(home, ".config", "mimocode"),
142
+ ]),
143
+ ];
144
+ const paths = await inspectTransaction(recoveryRoot, state, allowed);
107
145
  const digest = journal ? sha256(journal) : null;
108
146
  if (dryRun || !paths.length)
109
147
  return { paths, recovered: false, digest };
@@ -111,7 +149,7 @@ export async function recoverConfigSetup(scope, dryRun, cwd = process.cwd(), hom
111
149
  const current = await readRegular(journalPath);
112
150
  if (!current || !expectedDigest || sha256(current) !== expectedDigest)
113
151
  throw new ConfigSetupError("stale_receipt", "Recovery journal changed; preview recovery again");
114
- return recoverTransaction(root, state, allowed);
152
+ return recoverTransaction(recoveryRoot, state, allowed);
115
153
  });
116
154
  return { paths, recovered, digest };
117
155
  }
@@ -137,23 +175,26 @@ const UNIFIED_TUI_KEYBINDS = {
137
175
  messages_toggle_conceal: "<leader>h,<leader>р",
138
176
  tips_toggle: "<leader>h,<leader>р",
139
177
  };
140
- const LSP_SERVER_COMMANDS = {
141
- python: ["basedpyright-langserver", "--stdio"],
142
- typescript: ["typescript-language-server", "--stdio"],
143
- yaml: ["yaml-language-server", "--stdio"],
144
- shell: ["bash-language-server", "start"],
178
+ const OPENCODE_KEYBINDS = {
179
+ "command.palette.show": "alt+p",
180
+ "app.exit": "ctrl+d,<leader>q,<leader>й",
181
+ "session.first": "ctrl+g",
182
+ "session.last": "ctrl+alt+g",
183
+ "prompt.editor": "<leader>e,<leader>у",
184
+ "theme.switch": "<leader>t,<leader>е",
185
+ "session.sidebar.toggle": "<leader>b,<leader>и",
186
+ "opencode.status": "<leader>s,<leader>ы",
187
+ "session.export": "<leader>x,<leader>ч",
188
+ "session.new": "<leader>n,<leader>т",
189
+ "session.list": "<leader>l,<leader>д",
190
+ "session.timeline": "<leader>g,<leader>п",
191
+ "session.compact": "<leader>c,<leader>с",
192
+ "model.list": "<leader>m,<leader>ь",
193
+ "agent.list": "<leader>a,<leader>ф",
194
+ "messages.copy": "<leader>y,<leader>н",
195
+ "session.undo": "<leader>u,<leader>г",
196
+ "session.redo": "<leader>r,<leader>к",
145
197
  };
146
- function lspCatalog() {
147
- const raw = readFileSync(resolve(packageRoot, "dist", "assets", "lsp-catalog.json"), "utf8");
148
- const catalog = JSON.parse(raw);
149
- if (!Array.isArray(catalog.servers))
150
- throw new ConfigSetupError("invalid_package", "LSP catalog is unavailable");
151
- return catalog.servers
152
- .filter((server) => typeof server.name === "string" &&
153
- Array.isArray(server.extensions) &&
154
- server.extensions.every((extension) => typeof extension === "string"))
155
- .filter((server) => LSP_SERVER_COMMANDS[server.name]);
156
- }
157
198
  function mapAllowsAll(value) {
158
199
  return Boolean(value &&
159
200
  typeof value === "object" &&
@@ -171,20 +212,41 @@ function fragmentEdits(fragment, target, current) {
171
212
  ? value.permission
172
213
  : {};
173
214
  if (fragment === "core-plugin") {
174
- return [
175
- { kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/config.json" },
176
- { kind: "set-if-absent", path: ["plugin"], value: [] },
177
- {
178
- kind: "replace-array-value",
179
- path: ["plugin"],
180
- from: LEGACY_PACKAGE_NAME,
181
- to: PACKAGE_NAME,
182
- },
183
- { kind: "append-unique", path: ["plugin"], value: PACKAGE_NAME },
184
- ];
215
+ return corePluginEdits(value);
185
216
  }
217
+ if (fragment === "core-disable")
218
+ return corePluginRemovalEdits(value);
186
219
  if (fragment === "skills-state-permissions") {
187
- const statePaths = target === "opencode" ? [XDG_STATE_GLOB, OPENCODE_SKILLS_GLOB] : [XDG_STATE_GLOB];
220
+ const statePaths = target === "opencode"
221
+ ? [XDG_STATE_GLOB, OPENCODE_SKILLS_GLOB, AGENTS_SKILLS_GLOB]
222
+ : [XDG_STATE_GLOB];
223
+ if (target === "opencode")
224
+ return permissionEdits(value, [
225
+ ...statePaths.flatMap((resource) => [
226
+ { action: "read", resource, effect: "allow" },
227
+ { action: "external_directory", resource, effect: "allow" },
228
+ ]),
229
+ { action: "edit", resource: XDG_STATE_GLOB, effect: "allow" },
230
+ // Directory enumeration (glob/list) targets the roots themselves, which
231
+ // the tree globs above do not match; allow those exact directories.
232
+ ...statePaths.map((resource) => ({
233
+ action: "external_directory",
234
+ resource: resource.replace(/\/\*\*$/, ""),
235
+ effect: "allow",
236
+ })),
237
+ { action: "external_directory", resource: "~/.agents", effect: "allow" },
238
+ // Read-only access to reviewmatic-managed review worktrees, including
239
+ // the directory boundaries external access is checked against. Read is
240
+ // scoped to the managed reviewmatic catalog, and pre-existing user or
241
+ // secret denies keep priority: the insertion rule in permissionEdits
242
+ // places each new allow before any overlapping recorded deny. No shell
243
+ // or edit permission is added, and sibling catalogs of other tools
244
+ // under `<repo>.worktrees` stay unapproved.
245
+ { action: "read", resource: WORKTREE_TREE_GLOB, effect: "allow" },
246
+ { action: "external_directory", resource: WORKTREE_BOUNDARY_GLOB, effect: "allow" },
247
+ { action: "external_directory", resource: WORKTREE_ROOT, effect: "allow" },
248
+ { action: "external_directory", resource: WORKTREE_BASE, effect: "allow" },
249
+ ]);
188
250
  const edits = [];
189
251
  if (!mapAllowsAll(permission.read))
190
252
  edits.push(...permissionMapEdits(["permission", "read"], Object.fromEntries(statePaths.map((glob) => [glob, "allow"]))));
@@ -192,16 +254,22 @@ function fragmentEdits(fragment, target, current) {
192
254
  edits.push(...permissionMapEdits(["permission", "edit"], { [XDG_STATE_GLOB]: "allow" }));
193
255
  if (!mapAllowsAll(permission.external_directory))
194
256
  edits.push(...permissionMapEdits(["permission", "external_directory"], Object.fromEntries(statePaths.map((glob) => [glob, "allow"]))));
195
- return edits;
196
- }
197
- if (fragment === "lsp-preset") {
198
- return lspCatalog().map((server) => ({
199
- kind: "set-if-absent",
200
- path: ["lsp", server.name],
201
- value: { command: LSP_SERVER_COMMANDS[server.name], extensions: server.extensions },
257
+ edits.push(...permissionMapEdits(["permission", "read"], { [WORKTREE_TREE_GLOB]: "allow" }), ...permissionMapEdits(["permission", "external_directory"], {
258
+ [WORKTREE_BOUNDARY_GLOB]: "allow",
259
+ [WORKTREE_ROOT]: "allow",
260
+ [WORKTREE_BASE]: "allow",
202
261
  }));
262
+ return edits;
203
263
  }
204
264
  if (fragment === "secrets-guard") {
265
+ if (target === "opencode")
266
+ return permissionEdits(value, [
267
+ ...SECRET_PATHS.flatMap((glob) => [
268
+ { action: "read", resource: glob.replace(/^\*\*\//, "*"), effect: "deny" },
269
+ { action: "edit", resource: glob.replace(/^\*\*\//, "*"), effect: "deny" },
270
+ ]),
271
+ { action: "read", resource: "*.env.example", effect: "allow" },
272
+ ]);
205
273
  const deny = Object.fromEntries(SECRET_PATHS.map((glob) => [glob, "deny"]));
206
274
  return [
207
275
  ...permissionMapEdits(["permission", "read"], {
@@ -219,8 +287,18 @@ function fragmentEdits(fragment, target, current) {
219
287
  { kind: "set-if-absent", path: ["mcp_tool_display"], value: "expanded" },
220
288
  ];
221
289
  }
290
+ if (target === "opencode")
291
+ return [
292
+ { kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/v2/cli.json" },
293
+ { kind: "set-if-absent", path: ["theme", "name"], value: "ayu" },
294
+ ...Object.entries(OPENCODE_KEYBINDS).map(([key, value]) => ({
295
+ kind: "set-if-absent",
296
+ path: ["keybinds", key],
297
+ value,
298
+ })),
299
+ ];
222
300
  const tuiSchemas = {
223
- opencode: "https://opencode.ai/tui.json",
301
+ opencode: undefined,
224
302
  kilo: undefined,
225
303
  mimo: "https://mimo.xiaomi.com/mimocode/tui.json",
226
304
  };
@@ -255,6 +333,8 @@ export function normalizeConfigSelection(scope, value = {}) {
255
333
  if (scope === "project" && (targets.length !== 1 || targets[0] !== "opencode"))
256
334
  throw new ConfigSetupError("invalid_selection", "Project scope supports only the opencode target");
257
335
  const requestedFragments = value.fragments ?? [];
336
+ if (requestedFragments.includes("core-plugin") && requestedFragments.includes("core-disable"))
337
+ throw new ConfigSetupError("invalid_selection", "Choose either core-plugin or core-disable");
258
338
  const fragments = FRAGMENT_NAMES.filter((fragment) => requestedFragments.includes(fragment));
259
339
  if (fragments.length !== requestedFragments.length)
260
340
  throw new ConfigSetupError("invalid_selection", "Unknown config fragment selection");
@@ -275,9 +355,26 @@ export async function defaultConfigSelection(scope, cwd = process.cwd(), home =
275
355
  }
276
356
  return normalizeConfigSelection(scope, {
277
357
  targets: [...new Set(targets)],
278
- fragments: [...FRAGMENT_NAMES],
358
+ fragments: FRAGMENT_NAMES.filter((name) => name !== "core-disable"),
279
359
  });
280
360
  }
361
+ export async function inspectIntegration(scope, cwd = process.cwd(), home = homedir()) {
362
+ const file = await resolveTargetFile("opencode", scope, cwd, home);
363
+ const raw = await readRegular(file.absolute);
364
+ if (!raw)
365
+ return { path: file.absolute, connected: false };
366
+ try {
367
+ const current = parseJsonc(raw.toString("utf8"));
368
+ return { path: file.absolute, connected: corePluginRemovalEdits(current).length > 0 };
369
+ }
370
+ catch (error) {
371
+ return {
372
+ path: file.absolute,
373
+ connected: false,
374
+ problem: error instanceof Error ? error.message : "Invalid configuration",
375
+ };
376
+ }
377
+ }
281
378
  async function resolveTargetFile(target, scope, cwd, home, kind = "main") {
282
379
  const existing = async (root, path) => Boolean(await readRegular(join(root, path)).catch(() => undefined));
283
380
  const globalRoot = deploymentRoot("global", cwd, home);
@@ -308,9 +405,9 @@ async function resolveTargetFile(target, scope, cwd, home, kind = "main") {
308
405
  return {
309
406
  target,
310
407
  root: globalRoot,
311
- path: "tui.json",
312
- absolute: join(globalRoot, "tui.json"),
313
- exists: await existing(globalRoot, "tui.json"),
408
+ path: "cli.json",
409
+ absolute: join(globalRoot, "cli.json"),
410
+ exists: await existing(globalRoot, "cli.json"),
314
411
  };
315
412
  }
316
413
  if (target === "opencode") {
@@ -362,7 +459,7 @@ async function resolveTargetFile(target, scope, cwd, home, kind = "main") {
362
459
  exists: false,
363
460
  };
364
461
  }
365
- async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
462
+ async function build(selection, scope, cwd = process.cwd(), home = homedir(), syncSelection = true) {
366
463
  const root = deploymentRoot(scope, cwd, home);
367
464
  const operations = [];
368
465
  const mutations = [];
@@ -404,6 +501,20 @@ async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
404
501
  for (const fragment of selection.fragments) {
405
502
  if (!applicable(fragment, target) || fragmentFileKind(fragment) !== kind)
406
503
  continue;
504
+ if (target === "opencode" &&
505
+ kind === "tui" &&
506
+ !file.exists &&
507
+ ((await readRegular(join(file.root, "tui.json"))) ||
508
+ (await readRegular(join(file.root, "tui.jsonc"))))) {
509
+ operations.push({
510
+ target,
511
+ path: file.absolute,
512
+ fragment,
513
+ operation: "conflict",
514
+ reason: "Start OpenCode V2 once to migrate existing TUI preferences into cli.json before applying terminal presets",
515
+ });
516
+ continue;
517
+ }
407
518
  let parsed = undefined;
408
519
  try {
409
520
  parsed = parseJsonc(text);
@@ -464,6 +575,23 @@ async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
464
575
  ? "TUI files are global-only"
465
576
  : "no selected target supports this fragment",
466
577
  }));
578
+ const core = selection.fragments.find((fragment) => fragment === "core-plugin" || fragment === "core-disable");
579
+ if (syncSelection &&
580
+ core &&
581
+ selection.targets.includes("opencode") &&
582
+ !operations.some((operation) => operation.fragment === core && operation.operation === "conflict")) {
583
+ const mutation = await coreSelectionMutation(root, core === "core-plugin");
584
+ if (mutation) {
585
+ mutations.push(mutation);
586
+ operations.push({
587
+ target: "opencode",
588
+ path: join(root, mutation.path),
589
+ fragment: core,
590
+ operation: "update",
591
+ reason: "Save connection choice for repeat install and repair",
592
+ });
593
+ }
594
+ }
467
595
  const sorted = operations.sort((left, right) => CONFIG_TARGETS.indexOf(left.target) - CONFIG_TARGETS.indexOf(right.target) ||
468
596
  FRAGMENT_NAMES.indexOf(left.fragment) -
469
597
  FRAGMENT_NAMES.indexOf(right.fragment) ||
@@ -491,11 +619,11 @@ async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
491
619
  allowedRoots: [...allowedRoots],
492
620
  };
493
621
  }
494
- export async function previewConfigSetup(selection, scope, cwd = process.cwd(), home = homedir(), provisionDependency = true) {
622
+ export async function previewConfigSetup(selection, scope, cwd = process.cwd(), home = homedir(), provisionDependency = true, syncSelection = true) {
495
623
  const stateRoot = lifecycleRoot(scope, cwd, home);
496
624
  try {
497
625
  await requireNoRecovery(stateRoot);
498
- const built = await build(selection, scope, cwd, home);
626
+ const built = await build(selection, scope, cwd, home, syncSelection);
499
627
  if (provisionDependency &&
500
628
  selection.fragments.includes("core-plugin") &&
501
629
  selection.targets.includes("opencode"))
@@ -507,6 +635,7 @@ export async function previewConfigSetup(selection, scope, cwd = process.cwd(),
507
635
  resolve(cwd),
508
636
  resolve(home),
509
637
  provisionDependency,
638
+ syncSelection,
510
639
  ]);
511
640
  for (const [id, prior] of configReceipts)
512
641
  if (prior.key === key || prior.expires < Date.now())
@@ -532,7 +661,8 @@ export async function applyConfigSetup(selection, scope, cwd = process.cwd(), ho
532
661
  try {
533
662
  return await withLifecycleLock(stateRoot, async () => {
534
663
  await requireNoRecovery(stateRoot);
535
- const built = await build(selection, scope, cwd, home);
664
+ const syncSelection = options.syncSelection !== false;
665
+ const built = await build(selection, scope, cwd, home, syncSelection);
536
666
  const provisionDependency = options.provisionDependency !== false;
537
667
  if (provisionDependency &&
538
668
  selection.fragments.includes("core-plugin") &&
@@ -544,6 +674,7 @@ export async function applyConfigSetup(selection, scope, cwd = process.cwd(), ho
544
674
  resolve(cwd),
545
675
  resolve(home),
546
676
  provisionDependency,
677
+ syncSelection,
547
678
  ]);
548
679
  const id = options.receipt ?? [...configReceipts].find(([, entry]) => entry.key === key)?.[0];
549
680
  const receipt = id ? configReceipts.get(id) : undefined;
@@ -574,6 +705,8 @@ export async function applyConfigSetup(selection, scope, cwd = process.cwd(), ho
574
705
  await provision();
575
706
  return built.plan;
576
707
  }
708
+ if (!built.plan.operations.some((operation) => operation.operation === "conflict"))
709
+ return built.plan;
577
710
  throw new ConfigSetupError("invalid_state", "Config setup plan has no applicable changes");
578
711
  }
579
712
  const backups = [];
package/dist/contracts.js CHANGED
@@ -9,6 +9,32 @@ function strings(value) {
9
9
  export function validateAgentReport(agent, report, card) {
10
10
  if (!object(report))
11
11
  throw new Error("agent report must be an object");
12
+ if ((agent === "review" || agent === "critic") &&
13
+ !card &&
14
+ report.schema === "portable-gitlab/critic-receipt/v2") {
15
+ const required = [
16
+ "schema",
17
+ "evidence_digest",
18
+ "run_id",
19
+ "session_id",
20
+ "findings",
21
+ "external_mutations",
22
+ ];
23
+ const allowed = [...required, "scope_digest", "target_finding_ids"];
24
+ if (!required.every((key) => key in report) ||
25
+ Object.keys(report).some((key) => !allowed.includes(key)) ||
26
+ !/^[0-9a-f]{64}$/.test(String(report.evidence_digest)) ||
27
+ typeof report.run_id !== "string" ||
28
+ !report.run_id ||
29
+ typeof report.session_id !== "string" ||
30
+ !report.session_id ||
31
+ !Array.isArray(report.findings) ||
32
+ report.external_mutations !== false ||
33
+ ("scope_digest" in report && !/^[0-9a-f]{64}$/.test(String(report.scope_digest))) ||
34
+ ("target_finding_ids" in report && !strings(report.target_finding_ids)))
35
+ throw new Error("code-review critic receipt has an invalid transport envelope");
36
+ return;
37
+ }
12
38
  if (agent === "critic" && !card && "review_report" in report) {
13
39
  validateAgentReport("review", report);
14
40
  return;
package/dist/doctor.d.ts CHANGED
@@ -9,7 +9,6 @@ export type DoctorCheck = {
9
9
  };
10
10
  export type DoctorHost = {
11
11
  config?: () => Promise<unknown>;
12
- lsp?: () => Promise<unknown>;
13
12
  };
14
13
  export type DoctorReport = {
15
14
  schema_version: 1;