opencode-swarm 7.125.6 → 7.126.1

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 (56) hide show
  1. package/.opencode/skills/brainstorm/SKILL.md +18 -14
  2. package/.opencode/skills/plan/SKILL.md +21 -15
  3. package/.opencode/skills/specify/SKILL.md +18 -14
  4. package/.opencode/skills/swarm-pr-feedback/SKILL.md +19 -14
  5. package/.opencode/skills/swarm-pr-review/SKILL.md +17 -13
  6. package/dist/agents/architect.d.ts +9 -7
  7. package/dist/cli/{config-doctor-akxywx0c.js → config-doctor-23jxdqwv.js} +2 -2
  8. package/dist/cli/{core-v1kp7anb.js → core-wg3re04e.js} +1 -1
  9. package/dist/cli/{curation-policy-62w2pwfz.js → curation-policy-0t6hny6g.js} +2 -2
  10. package/dist/cli/{curator-gke6nrc4.js → curator-g8rggzbk.js} +14 -14
  11. package/dist/cli/{curator-llm-factory-40c4852v.js → curator-llm-factory-36psae60.js} +14 -14
  12. package/dist/cli/{guardrail-explain-m34vcgjg.js → guardrail-explain-6ffgrjx9.js} +15 -15
  13. package/dist/cli/{guardrail-log-222xyxfp.js → guardrail-log-57sczn5h.js} +3 -3
  14. package/dist/cli/{hive-promoter-6fg3redm.js → hive-promoter-c458rgp5.js} +14 -14
  15. package/dist/cli/{index-qzkcy22e.js → index-1p9z8cc4.js} +1 -1
  16. package/dist/cli/{index-e315gwc9.js → index-3hkbegq2.js} +1 -1
  17. package/dist/cli/{index-gnvg72y1.js → index-4ff0x4cg.js} +79 -1
  18. package/dist/cli/{index-yq4s3q43.js → index-6j9p0c40.js} +2 -2
  19. package/dist/cli/{index-74xejn4v.js → index-abw10n3f.js} +139 -83
  20. package/dist/cli/{index-r1gdkn2d.js → index-c8sqc3nh.js} +113 -21
  21. package/dist/cli/{index-gbnpz7rh.js → index-cnhp6c0k.js} +1 -1
  22. package/dist/cli/{index-dezgmvtw.js → index-echdpdhy.js} +1 -1
  23. package/dist/cli/{index-gd5w3ef8.js → index-k5xh81c9.js} +3 -3
  24. package/dist/cli/{index-1htanqp1.js → index-kx3533dm.js} +1 -1
  25. package/dist/cli/{index-0f45t7xy.js → index-nkhp7j16.js} +5 -5
  26. package/dist/cli/{index-1012f47p.js → index-nmyg9b7v.js} +4 -4
  27. package/dist/cli/{index-4b1e5m0d.js → index-vvwf9ark.js} +2 -2
  28. package/dist/cli/{index-jt7hnr7q.js → index-w0yztxgq.js} +1 -1
  29. package/dist/cli/{index-g4ef4v3m.js → index-w9cb5zwh.js} +1 -1
  30. package/dist/cli/{index-1cxp3b5y.js → index-x1w4cx3n.js} +2 -2
  31. package/dist/cli/{index-d17kqe54.js → index-y9t766sw.js} +4950 -4890
  32. package/dist/cli/{index-zp330ss0.js → index-yf4kvhrn.js} +16 -16
  33. package/dist/cli/index.js +14 -14
  34. package/dist/cli/{knowledge-escalator-h3hg7ek7.js → knowledge-escalator-kd80m9vp.js} +3 -3
  35. package/dist/cli/{knowledge-events-jtr66wxz.js → knowledge-events-j7jfwm1d.js} +1 -1
  36. package/dist/cli/{knowledge-store-69ttjjjs.js → knowledge-store-1d4byc7y.js} +1 -1
  37. package/dist/cli/{knowledge-validator-yn06rh13.js → knowledge-validator-63mkgt9x.js} +4 -4
  38. package/dist/cli/{scan-cursor-r0w2p5gz.js → scan-cursor-qzn5jmbe.js} +2 -2
  39. package/dist/cli/{schema-69z8w1ka.js → schema-2j92wnns.js} +1 -1
  40. package/dist/cli/{scope-persistence-habn4q8a.js → scope-persistence-dm5bab50.js} +3 -1
  41. package/dist/cli/{skill-generator-a7xfyddq.js → skill-generator-sv6zvdvz.js} +5 -5
  42. package/dist/commands/turbo-constants.d.ts +6 -0
  43. package/dist/config/constants.d.ts +1 -1
  44. package/dist/config/index.d.ts +1 -0
  45. package/dist/config/loader.d.ts +51 -10
  46. package/dist/hooks/pr-workflow-gate.d.ts +39 -0
  47. package/dist/hooks/scope-guard.d.ts +6 -1
  48. package/dist/index.js +47 -84
  49. package/dist/scope/scope-binding.d.ts +21 -2
  50. package/dist/scope/scope-persistence.d.ts +6 -0
  51. package/dist/tools/index.d.ts +2 -0
  52. package/dist/tools/manifest.d.ts +2 -0
  53. package/dist/tools/prepare-pr-feedback-scope.d.ts +5 -0
  54. package/dist/tools/tool-metadata.d.ts +98 -0
  55. package/dist/tools/write-pr-review-artifact.d.ts +5 -0
  56. package/package.json +4 -3
@@ -136,6 +136,7 @@ var SCOPES_DIR = ".swarm/scopes";
136
136
  var MAX_FILES_PER_SCOPE = 1e4;
137
137
  var MAX_PLAN_BYTES = 10 * 1024 * 1024;
138
138
  var MAX_SCOPE_BYTES = 2 * 1024 * 1024;
139
+ var MAX_BINDING_FILES_TO_SCAN = 1e4;
139
140
  var WINDOWS_RESERVED = new Set([
140
141
  "CON",
141
142
  "PRN",
@@ -432,6 +433,83 @@ function resolveAuthorizedScopeBindingForSession(input) {
432
433
  }
433
434
  return matches[0] ?? null;
434
435
  }
436
+ function readPrFeedbackBindingFile(directory, filePath, activeSessionId, taskId) {
437
+ try {
438
+ const leaf = fs2.lstatSync(filePath);
439
+ if (!leaf.isFile() || leaf.isSymbolicLink())
440
+ return null;
441
+ if (normalizePathForComparison(fs2.realpathSync(filePath)) !== normalizePathForComparison(filePath))
442
+ return null;
443
+ } catch {
444
+ return null;
445
+ }
446
+ const nofollow = fs2.constants.O_NOFOLLOW ?? 0;
447
+ let fd;
448
+ try {
449
+ fd = fs2.openSync(filePath, fs2.constants.O_RDONLY | nofollow);
450
+ } catch {
451
+ return null;
452
+ }
453
+ let raw = "";
454
+ try {
455
+ const stat = fs2.fstatSync(fd);
456
+ if (!stat.isFile() || stat.size > MAX_SCOPE_BYTES)
457
+ return null;
458
+ const buffer = Buffer.alloc(stat.size);
459
+ fs2.readSync(fd, buffer, 0, stat.size, 0);
460
+ raw = buffer.toString("utf8");
461
+ } catch {
462
+ return null;
463
+ } finally {
464
+ try {
465
+ fs2.closeSync(fd);
466
+ } catch {}
467
+ }
468
+ let parsed;
469
+ try {
470
+ parsed = JSON.parse(raw);
471
+ } catch {
472
+ return null;
473
+ }
474
+ const workspaceIdentity = canonicalWorkspaceIdentity(directory);
475
+ const files = Array.isArray(parsed.files) ? normalizeScopeFiles(parsed.files.filter((file) => typeof file === "string")) : null;
476
+ const now = Date.now();
477
+ if (parsed.version !== 2 || parsed.source !== "pr_feedback" || !workspaceIdentity || parsed.workspaceIdentity !== workspaceIdentity || !isSafeTaskId(parsed.taskId ?? "") || taskId !== undefined && parsed.taskId !== taskId || parsed.ownerSessionId !== activeSessionId || parsed.activation !== "active" || typeof parsed.dispatchCallId !== "string" || !parsed.dispatchCallId || parsed.ownerMessageId !== parsed.dispatchCallId || typeof parsed.parentOwnerSessionId !== "string" || !parsed.parentOwnerSessionId || parsed.ownerSessionId === parsed.parentOwnerSessionId || parsed.parentCallId !== parsed.dispatchCallId || parsed.workflowSessionId !== parsed.parentOwnerSessionId || typeof parsed.workflowRevisionDigest !== "string" || !parsed.workflowRevisionDigest || parsed.planId !== `pr-feedback:${parsed.workflowSessionId}` || parsed.planStructureHash !== parsed.workflowRevisionDigest || typeof parsed.declaredAt !== "number" || parsed.declaredAt > now || typeof parsed.expiresAt !== "number" || parsed.expiresAt <= now || !files)
478
+ return null;
479
+ const expectedPath = getBindingFilePath(directory, parsed.taskId, activeSessionId);
480
+ if (normalizePathForComparison(expectedPath) !== normalizePathForComparison(filePath))
481
+ return null;
482
+ return { ...parsed, files };
483
+ }
484
+ function resolveAuthorizedPrFeedbackScopeBindingFromDisk(input) {
485
+ if (!input.activeSessionId.trim())
486
+ return null;
487
+ const scopesDir = getScopesDir(input.directory);
488
+ if (!isScopesDirSafe(input.directory, scopesDir))
489
+ return null;
490
+ let candidates;
491
+ if (input.taskId) {
492
+ if (!isSafeTaskId(input.taskId))
493
+ return null;
494
+ candidates = [
495
+ getBindingFilePath(input.directory, input.taskId, input.activeSessionId)
496
+ ];
497
+ } else {
498
+ try {
499
+ const names = fs2.readdirSync(scopesDir);
500
+ if (names.length > MAX_BINDING_FILES_TO_SCAN)
501
+ return null;
502
+ candidates = names.filter((name) => name.startsWith("binding-") && name.endsWith(".json")).map((name) => path2.join(scopesDir, name));
503
+ } catch {
504
+ return null;
505
+ }
506
+ }
507
+ const matches = candidates.map((candidate) => readPrFeedbackBindingFile(input.directory, candidate, input.activeSessionId, input.taskId)).filter((binding) => binding !== null);
508
+ if (matches.length !== 1)
509
+ return null;
510
+ registerScopeBinding(matches[0]);
511
+ return matches[0];
512
+ }
435
513
  async function atomicWrite(targetPath, content) {
436
514
  const tempPath = `${targetPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
437
515
  try {
@@ -561,4 +639,4 @@ function resolveScopeWithFallbacks(input) {
561
639
  return null;
562
640
  }
563
641
 
564
- export { MAX_PENDING_SCOPE_BINDINGS, clearScopeBindings, writeScopeToDisk, writeScopeBindingToDisk, readScopeBindingFromDisk, clearScopeBindingFromDisk, resolveAuthorizedScopeBinding, resolveAuthorizedScopeBindingForSession, readScopeFromDisk, readPlanScope, clearScopeForTask, clearAllScopes, resolveScopeWithFallbacks };
642
+ export { MAX_PENDING_SCOPE_BINDINGS, clearScopeBindings, writeScopeToDisk, writeScopeBindingToDisk, readScopeBindingFromDisk, clearScopeBindingFromDisk, resolveAuthorizedScopeBinding, resolveAuthorizedScopeBindingForSession, resolveAuthorizedPrFeedbackScopeBindingFromDisk, readScopeFromDisk, readPlanScope, clearScopeForTask, clearAllScopes, resolveScopeWithFallbacks };
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  authorizeCuration
4
- } from "./index-1htanqp1.js";
4
+ } from "./index-kx3533dm.js";
5
5
  import {
6
6
  findNearDuplicate,
7
7
  inferTags,
@@ -9,7 +9,7 @@ import {
9
9
  resolveSwarmKnowledgePath,
10
10
  resolveSwarmRejectedPath,
11
11
  transactKnowledge
12
- } from "./index-1012f47p.js";
12
+ } from "./index-nmyg9b7v.js";
13
13
  import {
14
14
  resolveKnowledgeStoreDir
15
15
  } from "./index-q27bajqb.js";
@@ -8,7 +8,7 @@ import {
8
8
  init_constants,
9
9
  init_schema,
10
10
  resolveExternalSkillsConfig
11
- } from "./index-r1gdkn2d.js";
11
+ } from "./index-c8sqc3nh.js";
12
12
  import {
13
13
  advisoryWarn,
14
14
  init_warning_buffer
@@ -100,30 +100,35 @@ function migratePresetsConfig(raw) {
100
100
  }
101
101
  function sanitizeExternalSkillsConfig(raw) {
102
102
  if (!("external_skills" in raw) || raw.external_skills === undefined) {
103
- return raw;
103
+ return { result: raw, strippedKeys: [] };
104
104
  }
105
105
  const esResult = ExternalSkillsConfigSchema.safeParse(raw.external_skills);
106
106
  if (esResult.success) {
107
107
  return {
108
- ...raw,
109
- external_skills: resolveExternalSkillsConfig(esResult.data)
108
+ result: {
109
+ ...raw,
110
+ external_skills: resolveExternalSkillsConfig(esResult.data)
111
+ },
112
+ strippedKeys: []
110
113
  };
111
114
  }
112
115
  advisoryWarn("[opencode-swarm] external_skills config validation failed:", formatZodIssues(esResult.error));
113
116
  advisoryWarn("[opencode-swarm] External skills curation disabled due to invalid config. Fix the external_skills section to enable it.");
114
117
  const cleaned = { ...raw };
115
118
  delete cleaned.external_skills;
116
- return cleaned;
119
+ return { result: cleaned, strippedKeys: ["external_skills"] };
117
120
  }
118
121
  function sanitizeGatesConfig(raw) {
122
+ const strippedKeys = [];
119
123
  if (!("gates" in raw) || raw.gates === undefined) {
120
- return raw;
124
+ return { result: raw, strippedKeys };
121
125
  }
122
126
  if (typeof raw.gates !== "object" || raw.gates === null || Array.isArray(raw.gates)) {
123
127
  advisoryWarn("[opencode-swarm] gates config validation failed: expected an object. Quality gates will use defaults; other config sections remain active.");
124
128
  const cleaned2 = { ...raw };
125
129
  delete cleaned2.gates;
126
- return cleaned2;
130
+ strippedKeys.push("gates");
131
+ return { result: cleaned2, strippedKeys };
127
132
  }
128
133
  const gateSchemas = GateConfigSchema.shape;
129
134
  const cleanedGates = { ...raw.gates };
@@ -132,6 +137,7 @@ function sanitizeGatesConfig(raw) {
132
137
  if (!schema) {
133
138
  advisoryWarn(`[opencode-swarm] Unknown gates config section "gates.${key}" ignored. Other config sections remain active.`);
134
139
  delete cleanedGates[key];
140
+ strippedKeys.push(`gates.${key}`);
135
141
  continue;
136
142
  }
137
143
  const knownFields = GATE_CONFIG_KNOWN_SECTION_KEYS[key];
@@ -143,6 +149,7 @@ function sanitizeGatesConfig(raw) {
143
149
  if (!knownFieldSet.has(fieldName)) {
144
150
  advisoryWarn(`[opencode-swarm] Unknown gates config key "gates.${key}.${fieldName}" ignored. Other config sections remain active.`);
145
151
  delete cleanedSection[fieldName];
152
+ strippedKeys.push(`gates.${key}.${fieldName}`);
146
153
  }
147
154
  }
148
155
  sectionValue = cleanedSection;
@@ -152,22 +159,30 @@ function sanitizeGatesConfig(raw) {
152
159
  if (!sectionResult.success) {
153
160
  advisoryWarn(`[opencode-swarm] gates.${key} config validation failed; that gate section will use defaults and other config sections remain active:`, formatZodIssues(sectionResult.error));
154
161
  delete cleanedGates[key];
162
+ if (!strippedKeys.includes(`gates.${key}`)) {
163
+ strippedKeys.push(`gates.${key}`);
164
+ }
155
165
  }
156
166
  }
157
167
  const gatesResult = GateConfigSchema.safeParse(cleanedGates);
158
168
  if (gatesResult.success) {
159
169
  return {
160
- ...raw,
161
- gates: gatesResult.data
170
+ result: { ...raw, gates: gatesResult.data },
171
+ strippedKeys
162
172
  };
163
173
  }
164
174
  advisoryWarn("[opencode-swarm] gates config validation failed after section cleanup; quality gates will use defaults and other config sections remain active:", formatZodIssues(gatesResult.error));
165
175
  const cleaned = { ...raw };
166
176
  delete cleaned.gates;
167
- return cleaned;
177
+ if (!strippedKeys.includes("gates")) {
178
+ strippedKeys.push("gates");
179
+ }
180
+ return { result: cleaned, strippedKeys };
168
181
  }
169
182
  function sanitizeSectionConfigs(raw) {
170
- return sanitizeGatesConfig(sanitizeExternalSkillsConfig(raw));
183
+ const { result: afterExternal, strippedKeys: externalStripped } = sanitizeExternalSkillsConfig(raw);
184
+ const { result, strippedKeys: gatesStripped } = sanitizeGatesConfig(afterExternal);
185
+ return { result, strippedKeys: [...externalStripped, ...gatesStripped] };
171
186
  }
172
187
  function formatZodIssues(error) {
173
188
  if (error.issues.length === 0)
@@ -205,87 +220,133 @@ function stripUnrecognizedKeys(raw, error) {
205
220
  }
206
221
  return { cleaned, removed };
207
222
  }
208
- function parseConfigWithFallback(mergedRaw, rawUserConfig, failedResult, loadedFromFile, configHadErrors) {
209
- const result = failedResult;
210
- const secure = (config) => loadedFromFile && configHadErrors ? PluginConfigSchema.parse({
211
- ...config,
223
+ function warningForRemovedKeys(removedKeys) {
224
+ return `Ignored ${removedKeys.length} invalid or unrecognized config key(s): ${removedKeys.join(", ")}. The rest of your configuration was preserved. Fix or remove these keys.`;
225
+ }
226
+ function rawFullAutoLocked(raw) {
227
+ if (!raw || typeof raw !== "object")
228
+ return false;
229
+ const fullAuto = raw.full_auto;
230
+ if (!fullAuto || typeof fullAuto !== "object" || Array.isArray(fullAuto)) {
231
+ return false;
232
+ }
233
+ return fullAuto.locked === true;
234
+ }
235
+ function buildConfigWithMeta(rawUserConfig, rawProjectConfig, loadedFromFile, configHadErrors) {
236
+ let mergedRaw = rawUserConfig ?? {};
237
+ if (rawProjectConfig) {
238
+ mergedRaw = deepMerge(mergedRaw, rawProjectConfig);
239
+ }
240
+ if (rawFullAutoLocked(rawUserConfig) || rawFullAutoLocked(rawProjectConfig)) {
241
+ const fa = mergedRaw.full_auto && typeof mergedRaw.full_auto === "object" && !Array.isArray(mergedRaw.full_auto) ? mergedRaw.full_auto : {};
242
+ mergedRaw = { ...mergedRaw, full_auto: { ...fa, locked: true } };
243
+ }
244
+ mergedRaw = migratePresetsConfig(mergedRaw);
245
+ const { result: sanitized, strippedKeys: gatesStripped } = sanitizeSectionConfigs(mergedRaw);
246
+ mergedRaw = sanitized;
247
+ const sanitizedWarning = gatesStripped.length > 0 ? warningForRemovedKeys(gatesStripped) : undefined;
248
+ const secure = (cfg) => loadedFromFile && configHadErrors ? PluginConfigSchema.parse({
249
+ ...cfg,
212
250
  guardrails: { enabled: true }
213
- }) : config;
214
- const { cleaned, removed } = stripUnrecognizedKeys(mergedRaw, result.error);
251
+ }) : cfg;
252
+ const firstResult = PluginConfigSchema.safeParse(mergedRaw);
253
+ if (firstResult.success) {
254
+ const config = secure(firstResult.data);
255
+ if (loadedFromFile && configHadErrors) {
256
+ const securityWarning = "\u26A0\uFE0F SECURITY: Falling back to conservative defaults with guardrails ENABLED. Fix the config file to restore custom configuration.";
257
+ advisoryWarn(`[opencode-swarm] ${securityWarning}`);
258
+ return {
259
+ config,
260
+ recovery: "guardrails_defaults",
261
+ removedKeys: [...gatesStripped],
262
+ warnings: [
263
+ ...sanitizedWarning ? [sanitizedWarning] : [],
264
+ securityWarning
265
+ ]
266
+ };
267
+ }
268
+ if (sanitizedWarning)
269
+ advisoryWarn(`[opencode-swarm] ${sanitizedWarning}`);
270
+ return {
271
+ config,
272
+ recovery: gatesStripped.length > 0 ? "stripped_keys" : "none",
273
+ removedKeys: [...gatesStripped],
274
+ warnings: sanitizedWarning ? [sanitizedWarning] : []
275
+ };
276
+ }
277
+ const { cleaned, removed } = stripUnrecognizedKeys(mergedRaw, firstResult.error);
215
278
  if (removed.length > 0) {
216
279
  const recovered = PluginConfigSchema.safeParse(cleaned);
217
280
  if (recovered.success) {
218
- advisoryWarn(`[opencode-swarm] Ignored ${removed.length} unrecognized config key(s): ${removed.join(", ")}. The rest of your configuration was preserved. Fix or remove these keys.`);
219
- return secure(recovered.data);
281
+ const removedKeys = [...gatesStripped, ...removed];
282
+ const warning = warningForRemovedKeys(removedKeys);
283
+ advisoryWarn(`[opencode-swarm] ${warning}`);
284
+ return {
285
+ config: secure(recovered.data),
286
+ recovery: "stripped_keys",
287
+ removedKeys,
288
+ warnings: [warning]
289
+ };
220
290
  }
221
291
  }
222
292
  if (rawUserConfig) {
223
- const userSanitized = sanitizeSectionConfigs(rawUserConfig ?? {});
293
+ const { result: userSanitized, strippedKeys: userGatesStripped } = sanitizeSectionConfigs(rawUserConfig);
224
294
  const userParseResult = PluginConfigSchema.safeParse(userSanitized);
225
295
  if (userParseResult.success) {
226
296
  advisoryWarn("[opencode-swarm] Project config ignored due to validation errors. Using user config.");
227
- return secure(userParseResult.data);
297
+ return {
298
+ config: secure(userParseResult.data),
299
+ recovery: "user_only",
300
+ removedKeys: [...gatesStripped, ...userGatesStripped],
301
+ warnings: [
302
+ "Project config ignored due to validation errors; using user config."
303
+ ]
304
+ };
228
305
  }
229
306
  const userStripped = stripUnrecognizedKeys(userSanitized, userParseResult.error);
230
307
  if (userStripped.removed.length > 0) {
231
308
  const userRecovered = PluginConfigSchema.safeParse(userStripped.cleaned);
232
309
  if (userRecovered.success) {
233
310
  advisoryWarn(`[opencode-swarm] Project config ignored; also ignored ${userStripped.removed.length} unrecognized user-config key(s): ${userStripped.removed.join(", ")}.`);
234
- return secure(userRecovered.data);
311
+ return {
312
+ config: secure(userRecovered.data),
313
+ recovery: "user_only",
314
+ removedKeys: [
315
+ ...gatesStripped,
316
+ ...userGatesStripped,
317
+ ...userStripped.removed
318
+ ],
319
+ warnings: [
320
+ "Project config ignored due to validation errors; using user config."
321
+ ]
322
+ };
235
323
  }
236
324
  }
237
325
  }
238
- const offending = stripUnrecognizedKeys(mergedRaw, result.error).removed;
326
+ const offending = stripUnrecognizedKeys(mergedRaw, firstResult.error).removed;
239
327
  if (offending.length > 0) {
240
328
  advisoryWarn(`[opencode-swarm] Merged config validation failed. Unrecognized key(s): ${offending.join(", ")}.`);
241
329
  } else {
242
- advisoryWarn("[opencode-swarm] Merged config validation failed:", formatZodIssues(result.error));
330
+ advisoryWarn("[opencode-swarm] Merged config validation failed:", formatZodIssues(firstResult.error));
243
331
  }
244
332
  advisoryWarn("[opencode-swarm] \u26A0\uFE0F SECURITY: Falling back to conservative defaults with guardrails ENABLED. Fix the config file to restore custom configuration.");
245
- return PluginConfigSchema.parse({ guardrails: { enabled: true } });
246
- }
247
- function rawFullAutoLocked(raw) {
248
- if (!raw || typeof raw !== "object")
249
- return false;
250
- const fullAuto = raw.full_auto;
251
- if (!fullAuto || typeof fullAuto !== "object" || Array.isArray(fullAuto)) {
252
- return false;
253
- }
254
- return fullAuto.locked === true;
333
+ return {
334
+ config: PluginConfigSchema.parse({ guardrails: { enabled: true } }),
335
+ recovery: "guardrails_defaults",
336
+ removedKeys: [...gatesStripped],
337
+ warnings: [
338
+ "Config failed validation; using safe defaults with guardrails enabled."
339
+ ]
340
+ };
255
341
  }
256
342
  function loadPluginConfig(directory) {
257
343
  const userConfigPath = path.join(getUserConfigDir(), "opencode", CONFIG_FILENAME);
258
344
  const projectConfigPath = path.join(directory, ".opencode", CONFIG_FILENAME);
259
345
  const userResult = loadRawConfigFromPath(userConfigPath);
260
346
  const projectResult = loadRawConfigFromPath(projectConfigPath);
261
- const rawUserConfig = userResult.config;
262
- const rawProjectConfig = projectResult.config;
263
347
  const loadedFromFile = userResult.fileExisted || projectResult.fileExisted;
264
348
  const configHadErrors = userResult.hadError || projectResult.hadError;
265
- let mergedRaw = rawUserConfig ?? {};
266
- if (rawProjectConfig) {
267
- mergedRaw = deepMerge(mergedRaw, rawProjectConfig);
268
- }
269
- if (rawFullAutoLocked(rawUserConfig) || rawFullAutoLocked(rawProjectConfig)) {
270
- const fullAutoRaw = mergedRaw.full_auto && typeof mergedRaw.full_auto === "object" && !Array.isArray(mergedRaw.full_auto) ? mergedRaw.full_auto : {};
271
- mergedRaw = {
272
- ...mergedRaw,
273
- full_auto: { ...fullAutoRaw, locked: true }
274
- };
275
- }
276
- mergedRaw = migratePresetsConfig(mergedRaw);
277
- mergedRaw = sanitizeSectionConfigs(mergedRaw);
278
- const result = PluginConfigSchema.safeParse(mergedRaw);
279
- if (result.success) {
280
- if (loadedFromFile && configHadErrors) {
281
- return PluginConfigSchema.parse({
282
- ...mergedRaw,
283
- guardrails: { enabled: true }
284
- });
285
- }
286
- return result.data;
287
- }
288
- return parseConfigWithFallback(mergedRaw, rawUserConfig, result, loadedFromFile, configHadErrors);
349
+ return buildConfigWithMeta(userResult.config, projectResult.config, loadedFromFile, configHadErrors).config;
289
350
  }
290
351
  function loadPluginConfigWithMeta(directory) {
291
352
  const userConfigPath = path.join(getUserConfigDir(), "opencode", CONFIG_FILENAME);
@@ -294,8 +355,15 @@ function loadPluginConfigWithMeta(directory) {
294
355
  const projectResult = loadRawConfigFromPath(projectConfigPath);
295
356
  const loadedFromFile = userResult.fileExisted || projectResult.fileExisted;
296
357
  const configHadErrors = userResult.hadError || projectResult.hadError;
297
- const config = loadPluginConfig(directory);
298
- return { config, loadedFromFile, configHadErrors };
358
+ const { config, recovery, removedKeys, warnings } = buildConfigWithMeta(userResult.config, projectResult.config, loadedFromFile, configHadErrors);
359
+ return {
360
+ config,
361
+ loadedFromFile,
362
+ configHadErrors,
363
+ recovery,
364
+ removedKeys,
365
+ warnings
366
+ };
299
367
  }
300
368
  async function loadRawConfigFromPathAsync(configPath) {
301
369
  try {
@@ -335,25 +403,6 @@ async function loadRawConfigFromPathAsync(configPath) {
335
403
  return { config: null, fileExisted: true, hadError: true };
336
404
  }
337
405
  }
338
- function reduceParsedConfig(rawUserConfig, rawProjectConfig, loadedFromFile, configHadErrors) {
339
- let mergedRaw = rawUserConfig ?? {};
340
- if (rawProjectConfig) {
341
- mergedRaw = deepMerge(mergedRaw, rawProjectConfig);
342
- }
343
- mergedRaw = migratePresetsConfig(mergedRaw);
344
- mergedRaw = sanitizeSectionConfigs(mergedRaw);
345
- const result = PluginConfigSchema.safeParse(mergedRaw);
346
- if (result.success) {
347
- if (loadedFromFile && configHadErrors) {
348
- return PluginConfigSchema.parse({
349
- ...mergedRaw,
350
- guardrails: { enabled: true }
351
- });
352
- }
353
- return result.data;
354
- }
355
- return parseConfigWithFallback(mergedRaw, rawUserConfig, result, loadedFromFile, configHadErrors);
356
- }
357
406
  async function loadPluginConfigWithMetaAsync(directory) {
358
407
  const userConfigPath = path.join(getUserConfigDir(), "opencode", CONFIG_FILENAME);
359
408
  const projectConfigPath = path.join(directory, ".opencode", CONFIG_FILENAME);
@@ -363,8 +412,15 @@ async function loadPluginConfigWithMetaAsync(directory) {
363
412
  ]);
364
413
  const loadedFromFile = userResult.fileExisted || projectResult.fileExisted;
365
414
  const configHadErrors = userResult.hadError || projectResult.hadError;
366
- const config = reduceParsedConfig(userResult.config, projectResult.config, loadedFromFile, configHadErrors);
367
- return { config, loadedFromFile, configHadErrors };
415
+ const { config, recovery, removedKeys, warnings } = buildConfigWithMeta(userResult.config, projectResult.config, loadedFromFile, configHadErrors);
416
+ return {
417
+ config,
418
+ loadedFromFile,
419
+ configHadErrors,
420
+ recovery,
421
+ removedKeys,
422
+ warnings
423
+ };
368
424
  }
369
425
  function loadAgentPrompt(agentName) {
370
426
  const promptsDir = path.join(getUserConfigDir(), "opencode", PROMPTS_DIR_NAME);