mister-wolf 2.20.0 → 2.21.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 (32) hide show
  1. package/README.md +1 -1
  2. package/README.ru.md +1 -1
  3. package/dist/adapters/platforms/index.d.ts.map +1 -1
  4. package/dist/adapters/platforms/index.js +2 -1
  5. package/dist/adapters/platforms/index.js.map +1 -1
  6. package/dist/adapters/platforms/opencode-adapter.d.ts +49 -7
  7. package/dist/adapters/platforms/opencode-adapter.d.ts.map +1 -1
  8. package/dist/adapters/platforms/opencode-adapter.js +540 -102
  9. package/dist/adapters/platforms/opencode-adapter.js.map +1 -1
  10. package/dist/adapters/platforms/opencode-config-keys.d.ts +110 -0
  11. package/dist/adapters/platforms/opencode-config-keys.d.ts.map +1 -0
  12. package/dist/adapters/platforms/opencode-config-keys.js +214 -0
  13. package/dist/adapters/platforms/opencode-config-keys.js.map +1 -0
  14. package/dist/app/use-cases/doctor.d.ts.map +1 -1
  15. package/dist/app/use-cases/doctor.js +6 -0
  16. package/dist/app/use-cases/doctor.js.map +1 -1
  17. package/dist/app/use-cases/init-project.d.ts.map +1 -1
  18. package/dist/app/use-cases/init-project.js +8 -3
  19. package/dist/app/use-cases/init-project.js.map +1 -1
  20. package/dist/app/use-cases/repair-platform.d.ts.map +1 -1
  21. package/dist/app/use-cases/repair-platform.js +4 -1
  22. package/dist/app/use-cases/repair-platform.js.map +1 -1
  23. package/dist/app/use-cases/tool-librarian.d.ts.map +1 -1
  24. package/dist/app/use-cases/tool-librarian.js +3 -1
  25. package/dist/app/use-cases/tool-librarian.js.map +1 -1
  26. package/dist/app/use-cases/uninstall.js +1 -1
  27. package/dist/app/use-cases/uninstall.js.map +1 -1
  28. package/dist/ports/platform-adapter.port.d.ts +19 -4
  29. package/dist/ports/platform-adapter.port.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/templates/.wolf/router.log +1 -1
  32. package/update-map.json +3 -3
@@ -2,14 +2,22 @@ import * as fs from 'fs/promises';
2
2
  import { existsSync } from 'fs';
3
3
  import { execFile } from 'child_process';
4
4
  import { promisify } from 'util';
5
- import { basename, join } from 'path';
5
+ import { basename, join, relative } from 'path';
6
6
  import { parseJsonc } from './jsonc.js';
7
7
  import { writeFileAtomic } from '../fs/markdown-memory-store.js';
8
8
  import { UserFacingError } from '../../domain/errors.js';
9
- /** Дефолтный агент Wolf в opencode (§6.1). */
10
- export const DEFAULT_AGENT = 'mr-wolf';
11
- /** Глубина субагентов: executor-lead спавнит worker-* (трёхуровневая схема). */
12
- export const SUBAGENT_DEPTH = 2;
9
+ import { parseStamp } from '../render/stamp.js';
10
+ import yaml from 'js-yaml';
11
+ import { CONFIG_LOCATIONS, DEFAULT_AGENT, DEFAULT_AGENT_KEY, MCP_KEY, MCP_ROOT, MCP_SERVERS, SUBAGENT_DEPTH, SUBAGENT_DEPTH_KEY, WOLF_MCP_COMMAND, WRITE_LOCATION, WRITE_LOCATION_ID, deletePath, getPath, getRecordAt, isWolfMcpEntry, legacyPathOf, blockedSegment, locatePath, mcpEntryNative, mergeSurface, pathWritable, relPathOf, setPath, } from './opencode-config-keys.js';
12
+ export { DEFAULT_AGENT, SUBAGENT_DEPTH };
13
+ /** Каноническая команда wolf: removeWolf/configIssues не получают cmd, ownership проверяем ею. */
14
+ const WOLF_CMD = WOLF_MCP_COMMAND;
15
+ /** Значение глубины в действующем месте — наше или совместимое (>= SUBAGENT_DEPTH). */
16
+ function depthIsOurs(value) {
17
+ return typeof value === 'number' && value >= SUBAGENT_DEPTH;
18
+ }
19
+ const dotted = (key) => key.writePath.join('.');
20
+ const leaf = (key) => key.writePath[key.writePath.length - 1];
13
21
  const execFileAsync = promisify(execFile);
14
22
  /** Минимальная поддержанная v1 (порог диапазона). */
15
23
  const V1_MIN_MINOR = 18;
@@ -17,6 +25,9 @@ const V1_MIN_PATCH = 29;
17
25
  /**
18
26
  * Чистый парсер `opencode --version`. Эмпирика форматов: v1 печатает `1.18.35`,
19
27
  * v2 — `opencode v2.0.24`; regex вытаскивает M.m.p из обоих.
28
+ *
29
+ * Нативная форма `mcp.servers.<name>` принимается opencode >= 1.18.29 (v2-compat hoist),
30
+ * а порог v1 в коде — ровно 1.18.29: ветвление по версии для выбора формы не нужно.
20
31
  */
21
32
  export function parseOpencodeVersion(raw) {
22
33
  const trimmed = raw.trim();
@@ -50,126 +61,560 @@ export class OpencodeAdapter {
50
61
  return { status: 'absent', message: err instanceof Error ? err.message : String(err) };
51
62
  }
52
63
  }
53
- /** Существующий конфиг (jsonc — только если нет json); для новой установки — opencode.json. */
54
- configFile(projectRoot) {
55
- if (existsSync(join(projectRoot, 'opencode.jsonc')) && !existsSync(join(projectRoot, 'opencode.json'))) {
56
- return join(projectRoot, 'opencode.jsonc');
64
+ /**
65
+ * Файл локации: jsonc — только если json отсутствует (правило прежнее),
66
+ * иначе json. Если файла нет — возвращается дефолтный путь (цель записи).
67
+ */
68
+ locationFile(projectRoot, location) {
69
+ const dir = location.dir === '' ? projectRoot : join(projectRoot, location.dir);
70
+ const jsonc = join(dir, `${location.fileBase}.jsonc`);
71
+ const json = join(dir, `${location.fileBase}.json`);
72
+ return existsSync(jsonc) && !existsSync(json) ? jsonc : json;
73
+ }
74
+ /** Корневой конфиг — единственное место, куда wolf пишет свои ключи. */
75
+ rootFile(projectRoot) {
76
+ return this.locationFile(projectRoot, WRITE_LOCATION);
77
+ }
78
+ /**
79
+ * Поверхность конфига (6.7): обе локации + эффективный (слейдженный) вид.
80
+ * Битый/нечитаемый файл ИЗОЛИРУЕТСЯ: он попадает в `errors`, а остальные локации
81
+ * читаются как обычно (прежнее поведение «всё или ничего» чинило в обратную сторону).
82
+ */
83
+ async loadSurface(projectRoot) {
84
+ const files = [];
85
+ const errors = [];
86
+ for (const location of CONFIG_LOCATIONS) {
87
+ const file = this.locationFile(projectRoot, location);
88
+ if (!existsSync(file))
89
+ continue;
90
+ const rel = relative(projectRoot, file);
91
+ let raw;
92
+ try {
93
+ raw = await fs.readFile(file, 'utf-8');
94
+ }
95
+ catch (err) {
96
+ errors.push({
97
+ location: location.id,
98
+ file,
99
+ rel,
100
+ kind: 'not readable',
101
+ message: `${file} is not readable: ${err instanceof Error ? err.message : err}`,
102
+ });
103
+ continue;
104
+ }
105
+ let parsed;
106
+ try {
107
+ parsed = parseJsonc(raw);
108
+ }
109
+ catch (err) {
110
+ // паритет с ClaudeAdapter: ошибки парсинга — UserFacingError, не сырой SyntaxError
111
+ errors.push({
112
+ location: location.id,
113
+ file,
114
+ rel,
115
+ kind: 'not valid JSONC',
116
+ message: `${file} is not valid JSONC: ${err instanceof Error ? err.message : err}`,
117
+ });
118
+ continue;
119
+ }
120
+ const cfg = asRecord(parsed);
121
+ if (cfg === undefined) {
122
+ errors.push({
123
+ location: location.id,
124
+ file,
125
+ rel,
126
+ kind: 'not a JSON object',
127
+ message: `${file} is not a JSON object — refusing to touch it`,
128
+ });
129
+ continue;
130
+ }
131
+ files.push({ location: location.id, file, rel, cfg });
57
132
  }
58
- return join(projectRoot, 'opencode.json');
133
+ return { files, errors, effective: mergeSurface(files) };
134
+ }
135
+ /** Битая/нечитаемая корневая локация (та, куда wolf пишет) — прежний контракт: ошибка наружу. */
136
+ static rootError(surface) {
137
+ return surface.errors.find((error) => error.location === WRITE_LOCATION_ID);
59
138
  }
139
+ /**
140
+ * Эффективный конфиг обеих локаций; null — конфиг-файлов нет ни в одной локации.
141
+ * Битый корневой файл — UserFacingError (прежний контракт для корня);
142
+ * битый `.opencode` — не мешает: возвращаем вид читаемых файлов.
143
+ */
60
144
  async readConfig(projectRoot) {
61
- const file = this.configFile(projectRoot);
62
- let raw = null;
63
- try {
64
- raw = await fs.readFile(file, 'utf-8');
65
- }
66
- catch {
145
+ const surface = await this.loadSurface(projectRoot);
146
+ const rootError = OpencodeAdapter.rootError(surface);
147
+ if (rootError !== undefined)
148
+ throw new UserFacingError(rootError.message);
149
+ if (surface.files.length === 0)
67
150
  return null;
151
+ return surface.effective;
152
+ }
153
+ /**
154
+ * Запись wolf-ключей (6.7): решения принимаются по эффективному виду обеих
155
+ * локаций, запись — только в корневой конфиг (в .opencode wolf не пишет).
156
+ *
157
+ * opts.createFile=false — режим repair-only: файл конфига не создаётся, если его нет
158
+ * (минимальный init чинит существующий конфиг и никогда не создаёт платформенный).
159
+ */
160
+ async writeConfig(projectRoot, cmd, opts) {
161
+ const surface = await this.loadSurface(projectRoot);
162
+ // битый/нечитаемый корень — прежнее поведение: файл не трогаем, ошибка наружу
163
+ const rootError = OpencodeAdapter.rootError(surface);
164
+ if (rootError !== undefined)
165
+ throw new UserFacingError(rootError.message);
166
+ const file = this.rootFile(projectRoot);
167
+ const rootExists = surface.files.some((f) => f.location === WRITE_LOCATION_ID);
168
+ if (!rootExists && opts?.createFile === false) {
169
+ // минимальный init: конфига нет — не создаём (документированное обещание)
170
+ const other = surface.files.find((f) => f.location !== WRITE_LOCATION_ID);
171
+ const reason = other !== undefined
172
+ ? `no root ${relPathOf(WRITE_LOCATION)} to repair — the project config is ${other.rel}; a minimal init never creates a platform config (use wolf init --platform ${this.id})`
173
+ : `no root ${relPathOf(WRITE_LOCATION)} to repair — a minimal init never creates a platform config (use wolf init --platform ${this.id})`;
174
+ return { action: 'skipped', reason };
68
175
  }
69
- let parsed;
70
- try {
71
- parsed = parseJsonc(raw);
176
+ const rootCfg = rootConfigOf(surface);
177
+ // 6.3: пишем нативную форму mcp.servers.wolf, легаси mcp.wolf снимаем только если он наш
178
+ const desired = mcpEntryNative(cmd);
179
+ const legacyMcpPath = legacyPathOf(MCP_KEY);
180
+ const nativeAt = locatePath(surface.files, MCP_KEY.writePath);
181
+ const legacyAt = locatePath(surface.files, legacyMcpPath);
182
+ const legacyMcpOurs = legacyAt !== undefined && isWolfMcpEntry(legacyAt.value, cmd);
183
+ const mcpWritable = pathWritable(rootCfg, MCP_KEY.writePath);
184
+ const blockedReasons = [];
185
+ const agentReasons = [];
186
+ const depthReasons = [];
187
+ const mcpBlocked = blockedSegment(rootCfg, MCP_KEY.writePath);
188
+ if (mcpBlocked !== undefined) {
189
+ blockedReasons.push(`${mcpBlocked} is not an object; ${MCP_KEY.label} is not written — fix the conflicting key manually`);
72
190
  }
73
- catch (err) {
74
- // паритет с ClaudeAdapter: ошибки парсинга — UserFacingError, не сырой SyntaxError
75
- throw new UserFacingError(`${file} is not valid JSONC: ${err instanceof Error ? err.message : err}`);
76
- }
77
- return asConfig(parsed, file);
78
- }
79
- async writeConfig(projectRoot, cmd) {
80
- const file = this.configFile(projectRoot);
81
- const cfg = (await this.readConfig(projectRoot)) ?? {};
82
- const mcp = asRecord(cfg.mcp) ?? {};
83
- // каноническая проекция McpCommand в формат opencode: command — массив
84
- const desired = { type: 'local', command: [cmd.command, ...cmd.args], enabled: true };
85
- const mcpOk = JSON.stringify(mcp.wolf) === JSON.stringify(desired);
86
- // §6.1: default_agent и subagent_depth мерджатся рядом с mcp.wolf
87
- const da = cfg.default_agent;
88
- const sd = cfg.subagent_depth;
89
- const reasons = [];
191
+ // §6.1: default_agent — по эффективному виду (корень + .opencode)
192
+ const da = locatePath(surface.files, DEFAULT_AGENT_KEY.writePath)?.value;
90
193
  if (da !== undefined && da !== DEFAULT_AGENT) {
91
- reasons.push(`default_agent=${da} is taken; mr-wolf not assigned`);
194
+ agentReasons.push(`${DEFAULT_AGENT_KEY.label}=${da} is taken; ${DEFAULT_AGENT} not assigned`);
195
+ }
196
+ // 6.7: объявленное в .opencode не перекрываем вторым ключом в корне
197
+ let writeMcp = false;
198
+ let replaceMcp = false;
199
+ let mcpNativeReason;
200
+ if (nativeAt !== undefined) {
201
+ if (JSON.stringify(nativeAt.value) === JSON.stringify(desired)) {
202
+ // уже канонично — писать нечего
203
+ }
204
+ else if (nativeAt.location === WRITE_LOCATION_ID) {
205
+ if (mcpWritable) {
206
+ writeMcp = true;
207
+ replaceMcp = true;
208
+ mcpNativeReason = `replaced non-canonical ${MCP_KEY.label} entry with the canonical Wolf entry`;
209
+ }
210
+ }
211
+ else {
212
+ mcpNativeReason = `${MCP_KEY.label} is declared in ${relOf(surface, nativeAt.location)} — the .opencode value overrides the root one, so a root entry would have no effect; wolf leaves it untouched (wolf writes the root config)`;
213
+ }
214
+ }
215
+ else if (legacyAt === undefined || !legacyMcpOurs || legacyAt.location === WRITE_LOCATION_ID) {
216
+ writeMcp = mcpWritable;
217
+ }
218
+ // легаси-форма: снимаем только свою и только в корне (писать можно лишь туда)
219
+ let dropLegacyMcp = false;
220
+ let mcpLegacyReason;
221
+ if (legacyAt !== undefined) {
222
+ if (!legacyMcpOurs) {
223
+ mcpLegacyReason = `top-level ${legacyMcpPath.join('.')} is deprecated and foreign (command differs); ${MCP_KEY.label} is used`;
224
+ }
225
+ else if (legacyAt.location === WRITE_LOCATION_ID) {
226
+ dropLegacyMcp = true;
227
+ mcpLegacyReason = `removed deprecated top-level ${legacyMcpPath.join('.')} (migrated to ${MCP_KEY.label})`;
228
+ }
229
+ else if (nativeAt === undefined) {
230
+ mcpLegacyReason = `wolf entry is in the deprecated top-level ${legacyMcpPath.join('.')} form in ${relOf(surface, legacyAt.location)} — the .opencode value overrides the root one, so a root entry would have no effect; migrate it to ${MCP_KEY.label} manually`;
231
+ }
232
+ else {
233
+ mcpLegacyReason = `deprecated top-level ${legacyMcpPath.join('.')} in ${relOf(surface, legacyAt.location)} is ignored by opencode v2 — the .opencode value overrides the root one; remove it manually`;
234
+ }
235
+ }
236
+ // opencode v2: действует только experimental.subagent_depth. Верхнеуровневый ключ —
237
+ // deprecated: наше точное значение мигрируем, чужое не трогаем и сообщаем.
238
+ const depthPath = SUBAGENT_DEPTH_KEY.writePath;
239
+ const depthLeaf = leaf(SUBAGENT_DEPTH_KEY);
240
+ const depthLabel = dotted(SUBAGENT_DEPTH_KEY);
241
+ const legacyDepthPath = legacyPathOf(SUBAGENT_DEPTH_KEY);
242
+ const depthAt = locatePath(surface.files, depthPath);
243
+ const legacyDepthAt = locatePath(surface.files, legacyDepthPath);
244
+ const depth = depthAt?.value;
245
+ const legacyDepth = legacyDepthAt?.value;
246
+ const depthOk = depthIsOurs(depth);
247
+ const legacyOurs = legacyDepth === SUBAGENT_DEPTH;
248
+ let setDepth = false;
249
+ let dropLegacy = false;
250
+ const depthWritable = pathWritable(rootCfg, depthPath);
251
+ if (!depthWritable) {
252
+ depthReasons.push(`experimental is not an object; ${depthLeaf} is not written — set ${depthLabel}: ${SUBAGENT_DEPTH} manually`);
253
+ }
254
+ else if (depthOk) {
255
+ if (legacyOurs) {
256
+ if (legacyDepthAt.location === WRITE_LOCATION_ID) {
257
+ dropLegacy = true; // оба ключа наши: легаси — только шум, убираем
258
+ depthReasons.push(`removed deprecated top-level ${depthLeaf}=${SUBAGENT_DEPTH} (ignored by opencode v2)`);
259
+ }
260
+ else {
261
+ depthReasons.push(`top-level ${depthLeaf}=${SUBAGENT_DEPTH} in ${relOf(surface, legacyDepthAt.location)} is deprecated and ignored by opencode v2 — the .opencode value overrides the root one; remove it manually`);
262
+ }
263
+ }
264
+ else if (legacyDepth !== undefined) {
265
+ depthReasons.push(`top-level ${depthLeaf}=${legacyDepth} is deprecated and ignored by opencode v2; ${depthLabel} is used`);
266
+ }
267
+ }
268
+ else if (depth !== undefined) {
269
+ depthReasons.push(`${depthLabel}=${depth} is taken; the three-level scheme will not work, set >=2`);
270
+ }
271
+ else if (legacyOurs) {
272
+ setDepth = true;
273
+ if (legacyDepthAt.location === WRITE_LOCATION_ID) {
274
+ dropLegacy = true;
275
+ depthReasons.push(`migrated ${depthLeaf} to ${depthLabel}`);
276
+ }
277
+ else {
278
+ // легаси в .opencode удалить не можем (wolf пишет только в корень) — но и второй
279
+ // ключ не нужен: каноническую форму wolf записывает в корневой конфиг сам.
280
+ depthReasons.push(`top-level ${depthLeaf}=${SUBAGENT_DEPTH} in ${relOf(surface, legacyDepthAt.location)} is deprecated and ignored by opencode v2 — wolf wrote ${depthLabel}=${SUBAGENT_DEPTH} in the root config; remove the deprecated key manually`);
281
+ }
92
282
  }
93
- if (sd !== undefined && !(typeof sd === 'number' && sd >= SUBAGENT_DEPTH)) {
94
- reasons.push(`subagent_depth=${sd} is taken; the three-level scheme will not work, set >=2`);
283
+ else if (legacyDepth !== undefined) {
284
+ depthReasons.push(`top-level ${depthLeaf}=${legacyDepth} is taken; the key is deprecated and ignored by opencode v2 — set ${depthLabel} >=${SUBAGENT_DEPTH}`);
95
285
  }
286
+ else {
287
+ setDepth = true;
288
+ }
289
+ const reasons = [
290
+ ...blockedReasons,
291
+ ...agentReasons,
292
+ ...(mcpLegacyReason !== undefined ? [mcpLegacyReason] : []),
293
+ ...(mcpNativeReason !== undefined ? [mcpNativeReason] : []),
294
+ ...depthReasons,
295
+ // битая/нечитаемая локация не мешает починке корня — но называем её
296
+ ...surface.errors.map((error) => `skipped: ${error.rel} is ${error.kind}`),
297
+ ];
96
298
  const reason = reasons.length > 0 ? reasons.join('; ') : undefined;
97
- // F6 (спека 2.1.0 §2.4): фактические wolf-ключи после ensured — только наши
98
- // (mcp.wolf в обоих ветках ниже каноничен; default_agent/subagent_depth — если ключ наш, не занят чужим значением)
299
+ // F6 (спека 2.1.0 §2.4): фактические wolf-ключи после ensured — по эффективному виду
300
+ // (mcp.servers.wolf каноничен; default_agent/subagent_depth — если ключ наш, не занят чужим значением)
99
301
  const ours = () => {
100
- const keys = ['mcp.wolf'];
101
- if (cfg.default_agent === DEFAULT_AGENT)
102
- keys.push(`default_agent=${DEFAULT_AGENT}`);
103
- if (typeof cfg.subagent_depth === 'number' && cfg.subagent_depth >= SUBAGENT_DEPTH)
104
- keys.push(`subagent_depth=${cfg.subagent_depth}`);
302
+ const effective = effectiveAfter(surface, rootCfg);
303
+ const keys = [];
304
+ if (isWolfMcpEntry(getPath(effective, MCP_KEY.writePath), cmd))
305
+ keys.push(MCP_KEY.label);
306
+ if (getPath(effective, DEFAULT_AGENT_KEY.writePath) === DEFAULT_AGENT)
307
+ keys.push(`${DEFAULT_AGENT_KEY.label}=${DEFAULT_AGENT}`);
308
+ const sd = getPath(effective, depthPath);
309
+ if (depthIsOurs(sd))
310
+ keys.push(`${depthLeaf}=${sd}`);
105
311
  return keys;
106
312
  };
107
- // unchanged — по ВСЕМ ключам: корректный mcp.wolf не маскирует отсутствие default_agent/subagent_depth
108
- if (mcpOk && da !== undefined && sd !== undefined)
109
- return { action: 'unchanged', reason, configFile: basename(file), keys: ours() };
110
- const replaced = mcp.wolf !== undefined && !mcpOk;
111
- mcp.wolf = desired;
112
- cfg.mcp = mcp;
113
- if (da === undefined)
114
- cfg.default_agent = DEFAULT_AGENT; // конфликтный ключ не трогаем
115
- if (sd === undefined)
116
- cfg.subagent_depth = SUBAGENT_DEPTH; // конфликтный ключ не трогаем
117
- await writeFileAtomic(file, JSON.stringify(cfg, null, 2) + '\n');
118
- return { action: replaced ? 'replaced' : 'written', reason, configFile: basename(file), keys: ours() };
313
+ // F-10: отчёт init называет файл, где реально лежат wolf-ключи (при отсутствии
314
+ // записи — файл, где объявлен эффективный wolf-ключ; иначе — файл записи).
315
+ const wolfKeyFile = () => {
316
+ const mcp = locatePath(surface.files, MCP_KEY.writePath);
317
+ if (mcp !== undefined)
318
+ return relOf(surface, mcp.location);
319
+ const agent = locatePath(surface.files, DEFAULT_AGENT_KEY.writePath);
320
+ if (agent !== undefined)
321
+ return relOf(surface, agent.location);
322
+ const depth = locatePath(surface.files, SUBAGENT_DEPTH_KEY.writePath);
323
+ if (depth !== undefined)
324
+ return relOf(surface, depth.location);
325
+ return undefined;
326
+ };
327
+ // unchanged — по ВСЕМ ключам: корректный mcp.servers.wolf не маскирует отсутствие default_agent/subagent_depth;
328
+ // занятое чужим значением место depth тоже settled (не перезаписываем — только reason);
329
+ // наш легаси mcp.wolf — не settled: его надо снять записью.
330
+ const setAgent = da === undefined;
331
+ if (!writeMcp && !dropLegacyMcp && !setAgent && !setDepth && !dropLegacy)
332
+ return { action: 'unchanged', reason, configFile: wolfKeyFile() ?? basename(file), keys: ours() };
333
+ if (writeMcp)
334
+ setPath(rootCfg, MCP_KEY.writePath, desired);
335
+ if (dropLegacyMcp)
336
+ deletePath(rootCfg, legacyMcpPath);
337
+ if (setAgent)
338
+ setPath(rootCfg, DEFAULT_AGENT_KEY.writePath, DEFAULT_AGENT); // конфликтный ключ не трогаем
339
+ if (setDepth)
340
+ setPath(rootCfg, depthPath, SUBAGENT_DEPTH);
341
+ if (dropLegacy)
342
+ deletePath(rootCfg, legacyDepthPath);
343
+ await writeFileAtomic(file, JSON.stringify(rootCfg, null, 2) + '\n');
344
+ return { action: replaceMcp ? 'replaced' : 'written', reason, configFile: basename(file), keys: ours() };
119
345
  }
346
+ /**
347
+ * 6.7: wolf-запись MCP (нативная и легаси, только если она наша) удаляется
348
+ * из ВСЕХ локаций, где она реально объявлена.
349
+ */
120
350
  async removeWolf(projectRoot) {
121
- const file = this.configFile(projectRoot);
122
- const cfg = await this.readConfig(projectRoot);
123
- if (cfg === null)
351
+ const surface = await this.loadSurface(projectRoot);
352
+ if (surface.files.length === 0)
124
353
  return false;
125
- const mcp = asRecord(cfg.mcp);
126
- if (mcp === undefined || mcp.wolf === undefined)
127
- return false;
128
- delete mcp.wolf;
129
- if (Object.keys(mcp).length === 0)
130
- delete cfg.mcp;
131
- else
132
- cfg.mcp = mcp;
133
- await writeFileAtomic(file, JSON.stringify(cfg, null, 2) + '\n');
134
- return true;
354
+ const legacyMcpPath = legacyPathOf(MCP_KEY);
355
+ let removed = false;
356
+ for (const file of surface.files) {
357
+ let changed = false;
358
+ const nativeEntry = getPath(file.cfg, MCP_KEY.writePath);
359
+ const legacyEntry = getPath(file.cfg, legacyMcpPath);
360
+ if (nativeEntry !== undefined && isWolfMcpEntry(nativeEntry, WOLF_CMD)) {
361
+ deletePath(file.cfg, MCP_KEY.writePath);
362
+ changed = true;
363
+ }
364
+ if (legacyEntry !== undefined && isWolfMcpEntry(legacyEntry, WOLF_CMD)) {
365
+ deletePath(file.cfg, legacyMcpPath);
366
+ changed = true;
367
+ }
368
+ if (!changed)
369
+ continue;
370
+ removed = true;
371
+ await writeFileAtomic(file.file, JSON.stringify(file.cfg, null, 2) + '\n');
372
+ }
373
+ return removed;
135
374
  }
136
375
  /**
137
376
  * 2.16.2 (uninstall, F-07): только wolf-ключи с точным нашим значением.
138
377
  * Пользовательские default_agent/subagent_depth (не наши точные значения) не трогаем.
378
+ * Глубина: `experimental.subagent_depth === 2` (действующий путь) + легаси верхнеуровневый.
379
+ * 6.3: удаляем обе формы MCP (нативную и легаси) — только если запись наша.
380
+ * 6.7: снимаем ключи из ВСЕХ локаций, где они объявлены; битый файл — в причину.
139
381
  */
140
382
  async removeWolfKeys(projectRoot) {
141
- const file = this.configFile(projectRoot);
142
- const cfg = await this.readConfig(projectRoot).catch(() => null);
143
- if (cfg === null)
144
- return { action: 'skipped', reason: `no ${basename(file)}`, configFile: basename(file) };
145
- const removed = [];
146
- const mcp = asRecord(cfg.mcp);
147
- if (mcp !== undefined && mcp.wolf !== undefined) {
148
- delete mcp.wolf;
149
- if (Object.keys(mcp).length === 0)
150
- delete cfg.mcp;
151
- else
152
- cfg.mcp = mcp;
153
- removed.push('mcp.wolf');
154
- }
155
- if (cfg.default_agent === DEFAULT_AGENT) {
156
- delete cfg.default_agent;
157
- removed.push(`default_agent=${DEFAULT_AGENT}`);
158
- }
159
- if (cfg.subagent_depth === SUBAGENT_DEPTH) {
160
- delete cfg.subagent_depth;
161
- removed.push(`subagent_depth=${SUBAGENT_DEPTH}`);
383
+ const file = this.rootFile(projectRoot);
384
+ const surface = await this.loadSurface(projectRoot);
385
+ const broken = surface.errors.map((error) => `skipped: ${error.rel} is ${error.kind}`);
386
+ if (surface.files.length === 0) {
387
+ // «no opencode.json» — только когда конфиг-файлов нет НИ В ОДНОЙ локации
388
+ return {
389
+ action: 'skipped',
390
+ reason: broken.length > 0 ? broken.join('; ') : `no ${basename(file)}`,
391
+ configFile: basename(file),
392
+ };
162
393
  }
394
+ let mcpRemoved = false;
395
+ let agentRemoved = false;
396
+ let depthRemoved = false;
397
+ const changed = [];
398
+ const legacyMcpPath = legacyPathOf(MCP_KEY);
399
+ const depthLeaf = leaf(SUBAGENT_DEPTH_KEY);
400
+ const legacyDepthPath = legacyPathOf(SUBAGENT_DEPTH_KEY);
401
+ // корень первым — так же, как в configFile списке результата
402
+ for (const file of [...surface.files].reverse()) {
403
+ let touched = false;
404
+ const nativeEntry = getPath(file.cfg, MCP_KEY.writePath);
405
+ const legacyEntry = getPath(file.cfg, legacyMcpPath);
406
+ if (nativeEntry !== undefined && isWolfMcpEntry(nativeEntry, WOLF_CMD)) {
407
+ deletePath(file.cfg, MCP_KEY.writePath);
408
+ mcpRemoved = true;
409
+ touched = true;
410
+ }
411
+ if (legacyEntry !== undefined && isWolfMcpEntry(legacyEntry, WOLF_CMD)) {
412
+ deletePath(file.cfg, legacyMcpPath);
413
+ mcpRemoved = true;
414
+ touched = true;
415
+ }
416
+ if (getPath(file.cfg, DEFAULT_AGENT_KEY.writePath) === DEFAULT_AGENT) {
417
+ deletePath(file.cfg, DEFAULT_AGENT_KEY.writePath);
418
+ agentRemoved = true;
419
+ touched = true;
420
+ }
421
+ if (getPath(file.cfg, SUBAGENT_DEPTH_KEY.writePath) === SUBAGENT_DEPTH) {
422
+ deletePath(file.cfg, SUBAGENT_DEPTH_KEY.writePath);
423
+ depthRemoved = true;
424
+ touched = true;
425
+ }
426
+ if (getPath(file.cfg, legacyDepthPath) === SUBAGENT_DEPTH) {
427
+ // легаси верхнеуровневый ключ (deprecated в opencode v2) — тоже наш
428
+ deletePath(file.cfg, legacyDepthPath);
429
+ depthRemoved = true;
430
+ touched = true;
431
+ }
432
+ if (!touched)
433
+ continue; // перезаписываем только файлы, из которых сняты наши ключи
434
+ changed.push(file.rel);
435
+ await writeFileAtomic(file.file, JSON.stringify(file.cfg, null, 2) + '\n');
436
+ }
437
+ const removed = [
438
+ ...(mcpRemoved ? [MCP_KEY.label] : []),
439
+ ...(agentRemoved ? [`${DEFAULT_AGENT_KEY.label}=${DEFAULT_AGENT}`] : []),
440
+ ...(depthRemoved ? [`${depthLeaf}=${SUBAGENT_DEPTH}`] : []),
441
+ ];
163
442
  if (removed.length === 0) {
164
443
  return {
165
444
  action: 'unchanged',
166
- reason: 'no wolf keys with our exact values',
445
+ reason: ['no wolf keys with our exact values', ...broken].join('; '),
167
446
  configFile: basename(file),
168
447
  keys: [],
169
448
  };
170
449
  }
171
- await writeFileAtomic(file, JSON.stringify(cfg, null, 2) + '\n');
172
- return { action: 'removed', configFile: basename(file), keys: removed };
450
+ return {
451
+ action: 'removed',
452
+ ...(broken.length > 0 ? { reason: broken.join('; ') } : {}),
453
+ configFile: changed.join(', '),
454
+ keys: removed,
455
+ };
456
+ }
457
+ /**
458
+ * 6.1/6.3/6.4: doctor-проверки, которые знает только opencode-адаптер —
459
+ * все ключи, которые Wolf пишет, + смешанный frontmatter managed-агентов.
460
+ * Строки идут в общий `issues` проекта и маскируются doctor'ом как обычно.
461
+ * 6.7: проверки — по эффективному виду обеих локаций; битый файл — свой issue с именем.
462
+ */
463
+ async configIssues(projectRoot) {
464
+ const surface = await this.loadSurface(projectRoot);
465
+ const issues = surface.errors.map((error) => `${this.id}: ${error.rel} is ${error.kind} — fix it or remove the file, then run wolf init`);
466
+ if (surface.files.length === 0) {
467
+ // «missing» советовал бы init, который честно скипает битый конфиг
468
+ if (issues.length === 0)
469
+ return [`${this.id}: wolf entry missing — run wolf init`];
470
+ return issues;
471
+ }
472
+ return [
473
+ ...issues,
474
+ ...mcpIssues(surface),
475
+ ...defaultAgentIssues(surface.effective),
476
+ ...depthIssues(surface),
477
+ ...(await mixedFrontmatterIssues(projectRoot, this.id)),
478
+ ];
479
+ }
480
+ }
481
+ /** Собственный конфиг корневой локации (не слейдженный): в него wolf пишет. */
482
+ function rootConfigOf(surface) {
483
+ return surface.files.find((f) => f.location === WRITE_LOCATION_ID)?.cfg ?? {};
484
+ }
485
+ /** Путь файла локации относительно корня проекта (для сообщений). */
486
+ function relOf(surface, location) {
487
+ return surface.files.find((f) => f.location === location)?.rel ?? '';
488
+ }
489
+ /** Эффективный вид после записи: обновлённый корень + .opencode (порядок — в mergeSurface). */
490
+ function effectiveAfter(surface, rootCfg) {
491
+ return mergeSurface(surface.files, { location: WRITE_LOCATION_ID, cfg: rootCfg });
492
+ }
493
+ /** 6.3/6.7: wolf-сервер отсутствует в обеих формах / легаси-форма подлежит миграции. */
494
+ function mcpIssues(surface) {
495
+ const cfg = surface.effective;
496
+ // запись невозможна: init не вылечит такой конфиг — не советуем его
497
+ const blocked = blockedSegment(cfg, MCP_KEY.writePath);
498
+ if (blocked !== undefined)
499
+ return [`opencode: ${blocked} is not an object — ${MCP_KEY.label} is not written; fix it manually`];
500
+ const legacyPath = legacyPathOf(MCP_KEY);
501
+ const nativeAt = locatePath(surface.files, MCP_KEY.writePath);
502
+ const legacyAt = locatePath(surface.files, legacyPath);
503
+ // сервер не объявлен НИ В ОДНОЙ локации — только тогда «wolf entry missing»
504
+ if (nativeAt === undefined && legacyAt === undefined)
505
+ return [`opencode: wolf entry missing — run wolf init`];
506
+ if (legacyAt === undefined)
507
+ return [];
508
+ const ours = isWolfMcpEntry(legacyAt.value, WOLF_CMD);
509
+ // легаси в .opencode: init туда не пишет — советуем ручную миграцию с именем файла
510
+ if (legacyAt.location === WRITE_LOCATION_ID) {
511
+ return [
512
+ ours
513
+ ? `opencode: wolf entry is in the deprecated top-level ${legacyPath.join('.')} form — run wolf init to migrate it to ${MCP_KEY.label}`
514
+ : `opencode: top-level ${legacyPath.join('.')} is foreign and deprecated (opencode v2 reads ${MCP_ROOT}.${MCP_SERVERS}) — wolf leaves it untouched`,
515
+ ];
516
+ }
517
+ const rel = relOf(surface, legacyAt.location);
518
+ return [
519
+ ours
520
+ ? `opencode: wolf entry is in the deprecated top-level ${legacyPath.join('.')} form in ${rel} — migrate it to ${MCP_KEY.label} manually`
521
+ : `opencode: top-level ${legacyPath.join('.')} in ${rel} is foreign and deprecated (opencode v2 reads ${MCP_ROOT}.${MCP_SERVERS}) — wolf leaves it untouched`,
522
+ ];
523
+ }
524
+ /** 6.1: default_agent === mr-wolf, иначе L0 стартует не тем агентом. */
525
+ function defaultAgentIssues(cfg) {
526
+ const da = getPath(cfg, DEFAULT_AGENT_KEY.writePath);
527
+ if (da === undefined)
528
+ return [
529
+ `opencode: ${DEFAULT_AGENT_KEY.label} is not set — ${DEFAULT_AGENT} is not the default agent; run wolf init`,
530
+ ];
531
+ if (da !== DEFAULT_AGENT)
532
+ return [
533
+ `opencode: ${DEFAULT_AGENT_KEY.label}=${String(da)} is taken; ${DEFAULT_AGENT} is not assigned (foreign value is never overwritten)`,
534
+ ];
535
+ return [];
536
+ }
537
+ /** 6.1/6.7: experimental.subagent_depth >= 2 + deprecated верхнеуровневый ключ (по эффективному виду). */
538
+ function depthIssues(surface) {
539
+ const cfg = surface.effective;
540
+ const out = [];
541
+ const depthPath = SUBAGENT_DEPTH_KEY.writePath;
542
+ const parentPath = depthPath.slice(0, -1);
543
+ const depthLabel = depthPath.join('.');
544
+ const depthLeaf = depthPath[depthPath.length - 1];
545
+ const parentRaw = getPath(cfg, parentPath);
546
+ const experimental = getRecordAt(cfg, parentPath);
547
+ const depth = experimental?.[depthLeaf];
548
+ if (parentRaw !== undefined && experimental === undefined) {
549
+ out.push(`opencode: ${parentPath.join('.')} is not an object — ${depthLabel} is not set; set it to ${SUBAGENT_DEPTH} manually`);
550
+ }
551
+ else if (depth === undefined) {
552
+ out.push(`opencode: ${depthLabel} is not set — the three-level scheme needs >=${SUBAGENT_DEPTH}; run wolf init`);
553
+ }
554
+ else if (typeof depth !== 'number' || depth < SUBAGENT_DEPTH) {
555
+ out.push(`opencode: ${depthLabel}=${String(depth)} — the three-level scheme needs >=${SUBAGENT_DEPTH}; foreign value is never overwritten`);
556
+ }
557
+ const legacyPath = legacyPathOf(SUBAGENT_DEPTH_KEY);
558
+ const legacyAt = locatePath(surface.files, legacyPath);
559
+ if (legacyAt !== undefined) {
560
+ const legacy = legacyAt.value;
561
+ // легаси в .opencode: init туда не пишет — миграция ручная, с именем файла
562
+ if (legacyAt.location === WRITE_LOCATION_ID) {
563
+ out.push(legacy === SUBAGENT_DEPTH
564
+ ? `opencode: top-level ${legacyPath.join('.')}=${SUBAGENT_DEPTH} is deprecated and ignored by opencode v2 — run wolf init to migrate it to ${depthLabel}`
565
+ : `opencode: top-level ${legacyPath.join('.')}=${String(legacy)} is deprecated and ignored by opencode v2 — foreign value, set ${depthLabel} >=${SUBAGENT_DEPTH} manually`);
566
+ }
567
+ else {
568
+ const rel = relOf(surface, legacyAt.location);
569
+ out.push(legacy === SUBAGENT_DEPTH
570
+ ? `opencode: top-level ${legacyPath.join('.')}=${SUBAGENT_DEPTH} in ${rel} is deprecated and ignored by opencode v2 — migrate it to ${depthLabel} manually`
571
+ : `opencode: top-level ${legacyPath.join('.')}=${String(legacy)} in ${rel} is deprecated and ignored by opencode v2 — foreign value, set ${depthLabel} >=${SUBAGENT_DEPTH} manually`);
572
+ }
573
+ }
574
+ return out;
575
+ }
576
+ /** v2-поля агента: их появление включает чтение файла по схеме V2. */
577
+ const V2_AGENT_FIELDS = ['permissions', 'request', 'system', 'disabled'];
578
+ /** v1-поля агента: в V2-режиме молча игнорируются (6.4). */
579
+ const V1_AGENT_FIELDS = ['permission', 'temperature', 'mode'];
580
+ /**
581
+ * 6.4: managed-агент (есть штамп Wolf) со смешанным frontmatter читается как V2,
582
+ * и v1-правила (`permission.task: deny`) исчезают — предупреждаем, не правим файл.
583
+ */
584
+ async function mixedFrontmatterIssues(projectRoot, platformId) {
585
+ const dir = join(projectRoot, '.opencode', 'agents');
586
+ const names = await fs.readdir(dir).catch(() => []);
587
+ const out = [];
588
+ for (const name of names) {
589
+ if (!name.endsWith('.md'))
590
+ continue;
591
+ const raw = await fs.readFile(join(dir, name), 'utf-8').catch(() => null);
592
+ if (raw === null || parseStamp(raw) === null)
593
+ continue; // чужой файл — не наш scope
594
+ const front = frontmatterOf(raw);
595
+ if (front === undefined)
596
+ continue;
597
+ const v2 = V2_AGENT_FIELDS.filter((field) => front[field] !== undefined);
598
+ const v1 = V1_AGENT_FIELDS.filter((field) => front[field] !== undefined);
599
+ if (v2.length === 0 || v1.length === 0)
600
+ continue;
601
+ out.push(`${platformId}: ${join('.opencode', 'agents', name)} mixes v2 (${v2.join(', ')}) and v1 (${v1.join(', ')}) frontmatter — the file is read as V2 and v1 fields are ignored; do not mix formats in one file`);
602
+ }
603
+ return out;
604
+ }
605
+ /** YAML-frontmatter файла агента; нет блока/битый YAML — undefined. */
606
+ function frontmatterOf(raw) {
607
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
608
+ if (match === null)
609
+ return undefined;
610
+ try {
611
+ const parsed = yaml.load(match[1]);
612
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
613
+ ? parsed
614
+ : undefined;
615
+ }
616
+ catch {
617
+ return undefined;
173
618
  }
174
619
  }
175
620
  function asRecord(value) {
@@ -177,11 +622,4 @@ function asRecord(value) {
177
622
  ? value
178
623
  : undefined;
179
624
  }
180
- function asConfig(value, file) {
181
- const cfg = asRecord(value);
182
- if (cfg === undefined) {
183
- throw new UserFacingError(`${file} is not a JSON object — refusing to touch it`);
184
- }
185
- return cfg;
186
- }
187
625
  //# sourceMappingURL=opencode-adapter.js.map