thoth-agents 0.2.10 → 0.2.11

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.
@@ -10,6 +10,7 @@ import {
10
10
  DEFAULT_THOTH_COMMAND,
11
11
  GREP_APP_MCP_URL,
12
12
  appendPromptSections,
13
+ checkCustomSkillsNeedUpdate,
13
14
  claudeCodeSubagentType,
14
15
  composeAgentPrompt,
15
16
  createModelFamilySection,
@@ -17,9 +18,7 @@ import {
17
18
  createReadOnlySpecialistPromptSections,
18
19
  createStepBudgetSection,
19
20
  createWriteCapableSpecialistPromptSections,
20
- disableDefaultAgents,
21
21
  ensureConfigDir,
22
- ensureOpenCodeConfigDir,
23
22
  exa,
24
23
  findPackageRoot,
25
24
  generateLiteConfig,
@@ -35,9 +34,10 @@ import {
35
34
  parseConfig,
36
35
  renderPromptSection,
37
36
  renderRolePrompt,
37
+ updateOpenCodeMainConfig,
38
38
  writeConfig,
39
39
  writeLiteConfig
40
- } from "./chunk-WBRGPRRB.js";
40
+ } from "./chunk-OZE7JVGS.js";
41
41
 
42
42
  // src/cli/codex-paths.ts
43
43
  import { homedir } from "os";
@@ -4180,6 +4180,7 @@ function applyCodexPlan(plan) {
4180
4180
  }
4181
4181
 
4182
4182
  // src/cli/operations/opencode.ts
4183
+ import { createHash as createHash3 } from "crypto";
4183
4184
  import { existsSync as existsSync10 } from "fs";
4184
4185
  import { join as join10 } from "path";
4185
4186
 
@@ -4292,6 +4293,143 @@ var PACKAGE_NAME = "thoth-agents";
4292
4293
  var EXPECTED_PLUGIN = `${PACKAGE_NAME}@latest`;
4293
4294
  var OPENAI_PRESET = "openai";
4294
4295
  var ROLE_NAMES = [...ALL_AGENT_NAMES];
4296
+ var issuedOpenCodePlans = /* @__PURE__ */ new WeakMap();
4297
+ var NonCanonicalDataError = class extends Error {
4298
+ };
4299
+ function rejectSerializationHook(value, prototype) {
4300
+ if (Object.getOwnPropertyDescriptor(value, "toJSON")) {
4301
+ throw new NonCanonicalDataError("Own toJSON hooks are not canonical data.");
4302
+ }
4303
+ let current = prototype;
4304
+ while (current) {
4305
+ if (Object.getOwnPropertyDescriptor(current, "toJSON")) {
4306
+ throw new NonCanonicalDataError(
4307
+ "Inherited toJSON hooks are not canonical data."
4308
+ );
4309
+ }
4310
+ current = Object.getPrototypeOf(current);
4311
+ }
4312
+ }
4313
+ function canonicalData(value, active) {
4314
+ if (value === null) return "null";
4315
+ if (typeof value === "string") return JSON.stringify(value);
4316
+ if (typeof value === "boolean") return value ? "true" : "false";
4317
+ if (typeof value === "number") {
4318
+ if (!Number.isFinite(value)) {
4319
+ throw new NonCanonicalDataError("Non-finite numbers are not canonical.");
4320
+ }
4321
+ return JSON.stringify(value);
4322
+ }
4323
+ if (typeof value !== "object") {
4324
+ throw new NonCanonicalDataError("Only JSON-like data is canonical.");
4325
+ }
4326
+ if (active.has(value)) {
4327
+ throw new NonCanonicalDataError("Cycles are not canonical.");
4328
+ }
4329
+ const prototype = Object.getPrototypeOf(value);
4330
+ const array = Array.isArray(value);
4331
+ if (array && prototype !== Array.prototype || !array && prototype !== Object.prototype && prototype !== null) {
4332
+ throw new NonCanonicalDataError("Non-plain prototypes are not canonical.");
4333
+ }
4334
+ rejectSerializationHook(value, prototype);
4335
+ const ownKeys = Reflect.ownKeys(value);
4336
+ if (ownKeys.some((key) => typeof key === "symbol")) {
4337
+ throw new NonCanonicalDataError("Symbol keys are not canonical.");
4338
+ }
4339
+ const keys = ownKeys;
4340
+ active.add(value);
4341
+ try {
4342
+ if (array) {
4343
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
4344
+ if (!lengthDescriptor || !("value" in lengthDescriptor)) {
4345
+ throw new NonCanonicalDataError("Array length must be data.");
4346
+ }
4347
+ const length = lengthDescriptor.value;
4348
+ if (!Number.isSafeInteger(length) || length < 0) {
4349
+ throw new NonCanonicalDataError("Array length is invalid.");
4350
+ }
4351
+ const indexKeys = keys.filter((key) => key !== "length");
4352
+ if (indexKeys.length !== length || indexKeys.some(
4353
+ (key) => !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= length || String(Number(key)) !== key
4354
+ )) {
4355
+ throw new NonCanonicalDataError(
4356
+ "Sparse arrays and extra array properties are not canonical."
4357
+ );
4358
+ }
4359
+ const parts2 = [];
4360
+ for (let index = 0; index < length; index += 1) {
4361
+ const descriptor = Object.getOwnPropertyDescriptor(
4362
+ value,
4363
+ String(index)
4364
+ );
4365
+ if (!descriptor || !("value" in descriptor)) {
4366
+ throw new NonCanonicalDataError("Array accessors are not canonical.");
4367
+ }
4368
+ parts2.push(canonicalData(descriptor.value, active));
4369
+ }
4370
+ return `[${parts2.join(",")}]`;
4371
+ }
4372
+ const parts = [];
4373
+ for (const key of keys.sort()) {
4374
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
4375
+ if (!descriptor || !("value" in descriptor)) {
4376
+ throw new NonCanonicalDataError("Accessors are not canonical.");
4377
+ }
4378
+ parts.push(
4379
+ `${JSON.stringify(key)}:${canonicalData(descriptor.value, active)}`
4380
+ );
4381
+ }
4382
+ return `{${parts.join(",")}}`;
4383
+ } finally {
4384
+ active.delete(value);
4385
+ }
4386
+ }
4387
+ function canonicalDigest(value) {
4388
+ try {
4389
+ const serialized = canonicalData(value, /* @__PURE__ */ new Set());
4390
+ return {
4391
+ ok: true,
4392
+ digest: createHash3("sha256").update(serialized).digest("hex")
4393
+ };
4394
+ } catch {
4395
+ return { ok: false };
4396
+ }
4397
+ }
4398
+ function liveStatusDigest(status) {
4399
+ return canonicalDigest({
4400
+ state: status.state,
4401
+ diagnostics: status.diagnostics,
4402
+ targets: status.targets
4403
+ });
4404
+ }
4405
+ function immutableContext(context2) {
4406
+ return Object.freeze({
4407
+ cwd: context2.cwd,
4408
+ ...context2.env ? { env: Object.freeze({ ...context2.env }) } : {}
4409
+ });
4410
+ }
4411
+ function issueOpenCodePlan(plan, status, context2, payload) {
4412
+ const planDigest = canonicalDigest(plan);
4413
+ const statusDigest = liveStatusDigest(status);
4414
+ if (!planDigest.ok || !planDigest.digest || !statusDigest.ok || !statusDigest.digest) {
4415
+ return plan;
4416
+ }
4417
+ const immutablePayload = payload.kind === "fixed" ? Object.freeze({ kind: "fixed", action: payload.action }) : Object.freeze({
4418
+ kind: "model",
4419
+ roles: Object.freeze(
4420
+ payload.roles.map((role) => Object.freeze({ ...role }))
4421
+ )
4422
+ });
4423
+ issuedOpenCodePlans.set(plan, {
4424
+ planDigest: planDigest.digest,
4425
+ liveStatusDigest: statusDigest.digest,
4426
+ harness: "opencode",
4427
+ action: payload.kind === "model" ? "model-config" : payload.action,
4428
+ context: immutableContext(context2),
4429
+ payload: immutablePayload
4430
+ });
4431
+ return plan;
4432
+ }
4295
4433
  var openCodeActions = [
4296
4434
  {
4297
4435
  id: "opencode-status",
@@ -4360,7 +4498,7 @@ function targetForMainConfig(state) {
4360
4498
  kind: "config",
4361
4499
  path: getExistingConfigPath(),
4362
4500
  label: "OpenCode config",
4363
- state,
4501
+ ...state ? { state } : {},
4364
4502
  expected: `plugin includes ${EXPECTED_PLUGIN}`
4365
4503
  };
4366
4504
  }
@@ -4369,7 +4507,7 @@ function targetForLiteConfig(state) {
4369
4507
  kind: "config",
4370
4508
  path: getExistingLiteConfigPath(),
4371
4509
  label: "thoth-agents config",
4372
- state,
4510
+ ...state ? { state } : {},
4373
4511
  expected: `seven-agent ${OPENAI_PRESET} roster`
4374
4512
  };
4375
4513
  }
@@ -4384,7 +4522,7 @@ function titleCaseSkillName(value) {
4384
4522
  }).join("-");
4385
4523
  }
4386
4524
  function homeDirFromContext(context2) {
4387
- return context2.env?.HOME ?? context2.env?.USERPROFILE;
4525
+ return context2.env?.HOME ?? context2.env?.USERPROFILE ?? (context2.cwd !== process.cwd() ? join10(context2.cwd, "home") : void 0);
4388
4526
  }
4389
4527
  function openCodeSkillTargets(context2) {
4390
4528
  const homeDir = homeDirFromContext(context2);
@@ -4402,16 +4540,38 @@ function openCodeSkillTargets(context2) {
4402
4540
  };
4403
4541
  }
4404
4542
  );
4543
+ let staleSkillNames = /* @__PURE__ */ new Set();
4544
+ let removedSkillNames = [];
4545
+ let skillHealthError;
4546
+ try {
4547
+ const updateCheck = checkCustomSkillsNeedUpdate();
4548
+ staleSkillNames = new Set(
4549
+ updateCheck.skillsNeedingUpdate.map(({ skill }) => skill.name)
4550
+ );
4551
+ removedSkillNames = updateCheck.removedSkills;
4552
+ } catch (error) {
4553
+ skillHealthError = String(error);
4554
+ }
4405
4555
  const bundledTargets = CUSTOM_SKILLS.map((skill) => {
4406
4556
  const path4 = join10(getCustomSkillsDir(), skill.name, "SKILL.md");
4407
4557
  const installed = existsSync10(path4);
4558
+ const stale = installed && staleSkillNames.has(skill.name);
4559
+ let state = "installed";
4560
+ let observed = "bundled OpenCode skill installed and current";
4561
+ if (!installed) {
4562
+ state = "missing";
4563
+ observed = "bundled OpenCode skill missing";
4564
+ } else if (stale) {
4565
+ state = "drift";
4566
+ observed = "bundled OpenCode skill is outdated";
4567
+ }
4408
4568
  return {
4409
4569
  kind: "skill",
4410
4570
  path: path4,
4411
4571
  label: titleCaseSkillName(skill.name),
4412
- state: installed ? "installed" : "missing",
4572
+ state,
4413
4573
  expected: "bundled thoth-agents OpenCode skill",
4414
- observed: installed ? "bundled OpenCode skill installed" : "bundled OpenCode skill missing"
4574
+ observed
4415
4575
  };
4416
4576
  });
4417
4577
  const diagnostics = [];
@@ -4429,6 +4589,20 @@ function openCodeSkillTargets(context2) {
4429
4589
  code: "opencode-bundled-skills-missing"
4430
4590
  });
4431
4591
  }
4592
+ if (bundledTargets.some((target) => target.state === "drift") || removedSkillNames.length > 0) {
4593
+ diagnostics.push({
4594
+ severity: "important",
4595
+ message: "Bundled thoth-agents OpenCode skills are outdated; run OpenCode install or sync to refresh managed skills.",
4596
+ code: "opencode-bundled-skills-outdated"
4597
+ });
4598
+ }
4599
+ if (skillHealthError) {
4600
+ diagnostics.push({
4601
+ severity: "critical",
4602
+ message: `Bundled skill health could not be classified: ${skillHealthError}`,
4603
+ code: "opencode-bundled-skills-health-unknown"
4604
+ });
4605
+ }
4432
4606
  return {
4433
4607
  targets: [...recommendedTargets, ...bundledTargets],
4434
4608
  diagnostics
@@ -4450,8 +4624,11 @@ function liteConfigMarker(config) {
4450
4624
  const preset = typeof config?.preset === "string" ? config.preset : void 0;
4451
4625
  const presets = config?.presets && typeof config.presets === "object" ? config.presets : {};
4452
4626
  const openaiPreset = presets[OPENAI_PRESET] && typeof presets[OPENAI_PRESET] === "object" ? presets[OPENAI_PRESET] : {};
4453
- const roles = ROLE_NAMES.filter((role) => role in openaiPreset);
4454
- return `preset: ${preset ?? "none"}; ${OPENAI_PRESET} roles: ${roles.join(", ")}`;
4627
+ const legacyAgents = config?.agents && typeof config.agents === "object" ? config.agents : {};
4628
+ const roles = ROLE_NAMES.filter(
4629
+ (role) => role in openaiPreset || role in legacyAgents
4630
+ );
4631
+ return `preset: ${preset ?? "none"}; observed roles: ${roles.join(", ")}`;
4455
4632
  }
4456
4633
  function hasSevenAgentPreset(config) {
4457
4634
  if (!config || config.preset !== OPENAI_PRESET) return false;
@@ -4465,8 +4642,122 @@ function hasSevenAgentPreset(config) {
4465
4642
  return value !== null && typeof value === "object" && typeof value.model === "string";
4466
4643
  });
4467
4644
  }
4468
- function classifyApplySafety(state) {
4469
- return state === "missing" || state === "installed";
4645
+ function hasSelectedNamedPreset(config) {
4646
+ if (!config || typeof config.preset !== "string" || config.preset.length === 0 || config.preset === OPENAI_PRESET || config.preset === "agents") {
4647
+ return false;
4648
+ }
4649
+ if (!config.presets || typeof config.presets !== "object" || Array.isArray(config.presets)) {
4650
+ return false;
4651
+ }
4652
+ const presets = config.presets;
4653
+ if (!Object.hasOwn(presets, config.preset)) return false;
4654
+ const selectedPreset = presets[config.preset];
4655
+ return selectedPreset !== null && typeof selectedPreset === "object" && !Array.isArray(selectedPreset);
4656
+ }
4657
+ function hasLegacySevenAgentRoster(config) {
4658
+ if (!config || config.preset !== "agents") return false;
4659
+ if (!config.agents || typeof config.agents !== "object") return false;
4660
+ const agents = config.agents;
4661
+ return ROLE_NAMES.every((role) => {
4662
+ const value = agents[role];
4663
+ return value !== null && typeof value === "object" && typeof value.model === "string";
4664
+ });
4665
+ }
4666
+ var ACTIVE_PRESET_SELECTED_CODE = "opencode-active-preset-selected";
4667
+ function activePresetSelectedDiagnostic() {
4668
+ return {
4669
+ severity: "important",
4670
+ message: "A valid named preset is active; model configuration preserves it while sync or install may repair the managed roster.",
4671
+ code: ACTIVE_PRESET_SELECTED_CODE
4672
+ };
4673
+ }
4674
+ var REPAIRABLE_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
4675
+ "opencode-plugin-drift",
4676
+ "opencode-main-config-missing",
4677
+ "opencode-lite-config-missing",
4678
+ "opencode-roster-drift",
4679
+ ACTIVE_PRESET_SELECTED_CODE,
4680
+ "opencode-bundled-skills-missing",
4681
+ "opencode-bundled-skills-outdated"
4682
+ ]);
4683
+ var MODEL_PRECONFIG_SKILL_CODES = /* @__PURE__ */ new Set([
4684
+ "opencode-bundled-skills-missing",
4685
+ "opencode-bundled-skills-outdated"
4686
+ ]);
4687
+ function uniqueTargets(targets) {
4688
+ const seen = /* @__PURE__ */ new Set();
4689
+ return targets.filter((target) => {
4690
+ const key = JSON.stringify([
4691
+ target.kind,
4692
+ target.path,
4693
+ target.label,
4694
+ target.state,
4695
+ target.expected,
4696
+ target.observed
4697
+ ]);
4698
+ if (seen.has(key)) return false;
4699
+ seen.add(key);
4700
+ return true;
4701
+ });
4702
+ }
4703
+ function targetsForBlockingDiagnostic(diagnostic, status) {
4704
+ const main = status.targets.find(
4705
+ (target) => target.label === "OpenCode config"
4706
+ );
4707
+ const lite = status.targets.find(
4708
+ (target) => target.label === "thoth-agents config"
4709
+ );
4710
+ const code = diagnostic.code;
4711
+ if (code === "opencode-plugin-drift" || code === "opencode-main-config-missing" || code === "opencode-config-parse-error" || code === "opencode-unmanaged-config") {
4712
+ return main ? [main] : [];
4713
+ }
4714
+ if (code === "opencode-roster-drift" || code === ACTIVE_PRESET_SELECTED_CODE || code === "opencode-lite-config-missing" || code === "thoth-config-parse-error" || code === "opencode-roster-unrecognized") {
4715
+ return lite ? [lite] : [];
4716
+ }
4717
+ if (code === "opencode-bundled-skills-missing" || code === "opencode-bundled-skills-outdated" || code === "opencode-bundled-skills-health-unknown") {
4718
+ return status.targets.filter(
4719
+ (target) => target.expected === "bundled thoth-agents OpenCode skill" && (code === "opencode-bundled-skills-health-unknown" || target.state !== "installed")
4720
+ );
4721
+ }
4722
+ return [
4723
+ {
4724
+ kind: "unknown",
4725
+ label: "Unknown OpenCode blocker",
4726
+ state: "unknown",
4727
+ observed: code ? `${code}: ${diagnostic.message}` : diagnostic.message
4728
+ }
4729
+ ];
4730
+ }
4731
+ function blockingDetails(action, status, includeRepairable = false) {
4732
+ const main = status.targets.find(
4733
+ (target) => target.label === "OpenCode config"
4734
+ );
4735
+ const lite = status.targets.find(
4736
+ (target) => target.label === "thoth-agents config"
4737
+ );
4738
+ const allowModelPreconfiguration = action === "model-config" && main?.state === "missing" && lite?.state === "missing";
4739
+ const diagnostics = status.diagnostics.filter((diagnostic) => {
4740
+ if (diagnostic.severity === "critical") return true;
4741
+ if (diagnostic.severity === "minor") return false;
4742
+ if (!includeRepairable && action === "model-config" && diagnostic.code === ACTIVE_PRESET_SELECTED_CODE) {
4743
+ return false;
4744
+ }
4745
+ if (!includeRepairable && allowModelPreconfiguration && diagnostic.code !== void 0 && MODEL_PRECONFIG_SKILL_CODES.has(diagnostic.code)) {
4746
+ return false;
4747
+ }
4748
+ return includeRepairable || !((action === "sync" || action === "install") && diagnostic.code !== void 0 && REPAIRABLE_DIAGNOSTIC_CODES.has(diagnostic.code));
4749
+ });
4750
+ return {
4751
+ diagnostics,
4752
+ targets: uniqueTargets(
4753
+ diagnostics.flatMap(
4754
+ (diagnostic) => targetsForBlockingDiagnostic(diagnostic, status)
4755
+ )
4756
+ )
4757
+ };
4758
+ }
4759
+ function canApplyToManagedHealth(action, status) {
4760
+ return blockingDetails(action, status).diagnostics.length === 0;
4470
4761
  }
4471
4762
  function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
4472
4763
  const mainPath = getExistingConfigPath();
@@ -4522,6 +4813,18 @@ function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
4522
4813
  };
4523
4814
  if (!mainExists || !hasExpectedPlugin(main.config)) {
4524
4815
  if (hasManagedPluginDrift(main.config)) {
4816
+ const currentRoster = hasSevenAgentPreset(lite.config);
4817
+ const legacyRoster = hasLegacySevenAgentRoster(lite.config);
4818
+ const selectedNamedPreset = hasSelectedNamedPreset(lite.config);
4819
+ const recognizedRoster = currentRoster || legacyRoster || selectedNamedPreset;
4820
+ let liteState = "unknown";
4821
+ if (!liteExists) {
4822
+ liteState = "missing";
4823
+ } else if (currentRoster) {
4824
+ liteState = "installed";
4825
+ } else if (legacyRoster || selectedNamedPreset) {
4826
+ liteState = "drift";
4827
+ }
4525
4828
  return {
4526
4829
  harness: "opencode",
4527
4830
  displayName: "OpenCode",
@@ -4529,7 +4832,10 @@ function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
4529
4832
  summary: "OpenCode config has a managed thoth-agents plugin entry that is not latest.",
4530
4833
  targets: [
4531
4834
  { ...mainTarget, state: "drift" },
4532
- { ...liteTarget, state: liteExists ? "installed" : "missing" },
4835
+ {
4836
+ ...liteTarget,
4837
+ state: liteState
4838
+ },
4533
4839
  ...skillStatus.targets
4534
4840
  ],
4535
4841
  diagnostics: [
@@ -4538,6 +4844,132 @@ function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
4538
4844
  message: "Managed plugin drift requires an explicit repair before applying sync plans.",
4539
4845
  code: "opencode-plugin-drift"
4540
4846
  },
4847
+ ...legacyRoster ? [
4848
+ {
4849
+ severity: "important",
4850
+ message: "Roster drift requires repair before applying generated plans.",
4851
+ code: "opencode-roster-drift"
4852
+ }
4853
+ ] : [],
4854
+ ...selectedNamedPreset && !currentRoster ? [activePresetSelectedDiagnostic()] : [],
4855
+ ...!liteExists ? [
4856
+ {
4857
+ severity: "important",
4858
+ message: "The managed thoth-agents config is missing and must be restored by install or sync.",
4859
+ code: "opencode-lite-config-missing"
4860
+ }
4861
+ ] : [],
4862
+ ...!liteExists ? [
4863
+ {
4864
+ severity: "important",
4865
+ message: "The managed thoth-agents config is missing and must be restored by install or sync.",
4866
+ code: "opencode-lite-config-missing"
4867
+ }
4868
+ ] : [],
4869
+ ...liteExists && !recognizedRoster ? [
4870
+ {
4871
+ severity: "critical",
4872
+ message: "Existing thoth-agents config has an unrecognized roster shape.",
4873
+ code: "opencode-roster-unrecognized"
4874
+ }
4875
+ ] : [],
4876
+ ...skillStatus.diagnostics
4877
+ ],
4878
+ actions: openCodeActions
4879
+ };
4880
+ }
4881
+ if (!mainExists && liteExists) {
4882
+ const currentRoster = hasSevenAgentPreset(lite.config);
4883
+ const legacyRoster = hasLegacySevenAgentRoster(lite.config);
4884
+ const selectedNamedPreset = hasSelectedNamedPreset(lite.config);
4885
+ if (!currentRoster && !legacyRoster && !selectedNamedPreset) {
4886
+ return {
4887
+ harness: "opencode",
4888
+ displayName: "OpenCode",
4889
+ state: "unknown",
4890
+ summary: "Existing thoth-agents config has an unrecognized roster shape.",
4891
+ targets: [
4892
+ { ...mainTarget, state: "missing" },
4893
+ { ...liteTarget, state: "unknown" },
4894
+ ...skillStatus.targets
4895
+ ],
4896
+ diagnostics: [
4897
+ {
4898
+ severity: "critical",
4899
+ message: "Existing thoth-agents config is not a recognized current or legacy managed roster.",
4900
+ code: "opencode-roster-unrecognized"
4901
+ },
4902
+ ...skillStatus.diagnostics
4903
+ ],
4904
+ actions: openCodeActions
4905
+ };
4906
+ }
4907
+ if (selectedNamedPreset && !currentRoster) {
4908
+ return {
4909
+ harness: "opencode",
4910
+ displayName: "OpenCode",
4911
+ state: "drift",
4912
+ summary: "OpenCode plugin is missing and a valid named preset is active.",
4913
+ targets: [
4914
+ { ...mainTarget, state: "missing" },
4915
+ { ...liteTarget, state: "drift" },
4916
+ ...skillStatus.targets
4917
+ ],
4918
+ diagnostics: [
4919
+ {
4920
+ severity: "important",
4921
+ message: "The managed OpenCode main config is missing and must be restored by install or sync.",
4922
+ code: "opencode-main-config-missing"
4923
+ },
4924
+ activePresetSelectedDiagnostic(),
4925
+ ...skillStatus.diagnostics
4926
+ ],
4927
+ actions: openCodeActions
4928
+ };
4929
+ }
4930
+ if (legacyRoster) {
4931
+ return {
4932
+ harness: "opencode",
4933
+ displayName: "OpenCode",
4934
+ state: "drift",
4935
+ summary: "OpenCode plugin is missing and the managed roster uses the recognized legacy layout.",
4936
+ targets: [
4937
+ { ...mainTarget, state: "missing" },
4938
+ { ...liteTarget, state: "drift" },
4939
+ ...skillStatus.targets
4940
+ ],
4941
+ diagnostics: [
4942
+ {
4943
+ severity: "important",
4944
+ message: "The managed OpenCode main config is missing and must be restored by install or sync.",
4945
+ code: "opencode-main-config-missing"
4946
+ },
4947
+ {
4948
+ severity: "important",
4949
+ message: "Roster drift requires repair before applying generated plans.",
4950
+ code: "opencode-roster-drift"
4951
+ },
4952
+ ...skillStatus.diagnostics
4953
+ ],
4954
+ actions: openCodeActions
4955
+ };
4956
+ }
4957
+ return {
4958
+ harness: "opencode",
4959
+ displayName: "OpenCode",
4960
+ state: "missing",
4961
+ summary: "The managed roster is present, but the OpenCode main config is missing.",
4962
+ targets: [
4963
+ { ...mainTarget, state: "missing" },
4964
+ { ...liteTarget, state: "installed" },
4965
+ ...skillStatus.targets
4966
+ ],
4967
+ diagnostics: [
4968
+ {
4969
+ severity: "important",
4970
+ message: "The managed OpenCode main config is missing and must be restored by install or sync.",
4971
+ code: "opencode-main-config-missing"
4972
+ },
4541
4973
  ...skillStatus.diagnostics
4542
4974
  ],
4543
4975
  actions: openCodeActions
@@ -4553,7 +4985,17 @@ function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
4553
4985
  { ...liteTarget, state: liteExists ? "installed" : "missing" },
4554
4986
  ...skillStatus.targets
4555
4987
  ],
4556
- diagnostics: [...diagnostics, ...skillStatus.diagnostics],
4988
+ diagnostics: [
4989
+ ...diagnostics,
4990
+ ...mainExists ? [
4991
+ {
4992
+ severity: "critical",
4993
+ message: "Existing OpenCode config is not positively attributed to thoth-agents management.",
4994
+ code: "opencode-unmanaged-config"
4995
+ }
4996
+ ] : [],
4997
+ ...skillStatus.diagnostics
4998
+ ],
4557
4999
  actions: openCodeActions
4558
5000
  };
4559
5001
  }
@@ -4568,23 +5010,32 @@ function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
4568
5010
  { ...liteTarget, state: "missing" },
4569
5011
  ...skillStatus.targets
4570
5012
  ],
4571
- diagnostics: [...diagnostics, ...skillStatus.diagnostics],
5013
+ diagnostics: [
5014
+ ...diagnostics,
5015
+ {
5016
+ severity: "important",
5017
+ message: "The managed thoth-agents config is missing and must be restored by install or sync.",
5018
+ code: "opencode-lite-config-missing"
5019
+ },
5020
+ ...skillStatus.diagnostics
5021
+ ],
4572
5022
  actions: openCodeActions
4573
5023
  };
4574
5024
  }
4575
5025
  if (!hasSevenAgentPreset(lite.config)) {
5026
+ const selectedNamedPreset = hasSelectedNamedPreset(lite.config);
4576
5027
  return {
4577
5028
  harness: "opencode",
4578
5029
  displayName: "OpenCode",
4579
5030
  state: "drift",
4580
- summary: "thoth-agents config does not match the expected seven-agent roster.",
5031
+ summary: selectedNamedPreset ? "A valid named OpenCode preset is active outside the managed seven-agent roster." : "thoth-agents config does not match the expected seven-agent roster.",
4581
5032
  targets: [
4582
5033
  { ...mainTarget, state: "installed" },
4583
5034
  { ...liteTarget, state: "drift" },
4584
5035
  ...skillStatus.targets
4585
5036
  ],
4586
5037
  diagnostics: [
4587
- {
5038
+ selectedNamedPreset ? activePresetSelectedDiagnostic() : {
4588
5039
  severity: "important",
4589
5040
  message: "Roster drift requires repair before applying generated plans.",
4590
5041
  code: "opencode-roster-drift"
@@ -4615,6 +5066,37 @@ function defaultBackup(path4) {
4615
5066
  destinations: [{ path: `${path4}.bak`, label: "managed backup" }]
4616
5067
  };
4617
5068
  }
5069
+ function backupFromItems(items) {
5070
+ const backups = items.flatMap((item) => item.backup ? [item.backup] : []);
5071
+ const primary = backups.find((backup) => backup.required) ?? backups[0];
5072
+ const destinations = [];
5073
+ const seenPaths = /* @__PURE__ */ new Set();
5074
+ for (const backup of backups) {
5075
+ for (const destination of backup.destinations ?? []) {
5076
+ if (seenPaths.has(destination.path)) continue;
5077
+ seenPaths.add(destination.path);
5078
+ destinations.push(destination);
5079
+ }
5080
+ }
5081
+ return {
5082
+ required: backups.some((backup) => backup.required),
5083
+ strategy: primary?.strategy ?? "none",
5084
+ ...destinations.length > 0 ? { destinations } : {},
5085
+ ...primary?.description ? { description: primary.description } : {}
5086
+ };
5087
+ }
5088
+ function bundledSkillsRefreshItem() {
5089
+ const names = CUSTOM_SKILLS.map(({ name }) => name);
5090
+ return {
5091
+ title: "Refresh bundled thoth-agents OpenCode skills",
5092
+ target: {
5093
+ kind: "skill",
5094
+ label: "Bundled thoth-agents OpenCode skills",
5095
+ expected: `${names.length} managed bundled skills`
5096
+ },
5097
+ preview: JSON.stringify({ count: names.length, names }, null, 2)
5098
+ };
5099
+ }
4618
5100
  function defaultDisclaimers() {
4619
5101
  return [
4620
5102
  {
@@ -4623,9 +5105,10 @@ function defaultDisclaimers() {
4623
5105
  }
4624
5106
  ];
4625
5107
  }
4626
- function planFromItems(id, action, title, summary, items) {
4627
- const status = getOpenCodeStatus();
4628
- const safe = classifyApplySafety(status.state);
5108
+ function planFromItems(id, action, title, summary, items, context2) {
5109
+ const status = getOpenCodeStatus(context2);
5110
+ const blockers = blockingDetails(action, status);
5111
+ const safe = blockers.diagnostics.length === 0;
4629
5112
  const warnings = safe ? status.diagnostics : [
4630
5113
  ...status.diagnostics,
4631
5114
  {
@@ -4635,37 +5118,41 @@ function planFromItems(id, action, title, summary, items) {
4635
5118
  }
4636
5119
  ];
4637
5120
  return {
4638
- id,
4639
- harness: "opencode",
4640
- action,
4641
- title,
4642
- summary,
4643
- dryRun: true,
4644
- canApply: safe,
4645
- targets: status.targets,
4646
- surfaces: [
4647
- {
4648
- id: "opencode-config",
4649
- label: "OpenCode user config",
4650
- path: getExistingConfigPath(),
4651
- state: status.targets[0]?.state
4652
- },
4653
- {
4654
- id: "thoth-agents-config",
4655
- label: "thoth-agents plugin config",
4656
- path: getExistingLiteConfigPath(),
4657
- state: status.targets[1]?.state
4658
- }
4659
- ],
4660
- backup: defaultBackup(getExistingConfigPath()),
4661
- items,
4662
- warnings,
4663
- disclaimers: defaultDisclaimers()
5121
+ status,
5122
+ plan: {
5123
+ id,
5124
+ harness: "opencode",
5125
+ action,
5126
+ title,
5127
+ summary,
5128
+ dryRun: true,
5129
+ canApply: safe,
5130
+ targets: status.targets,
5131
+ blockerTargets: blockers.targets,
5132
+ surfaces: [
5133
+ {
5134
+ id: "opencode-config",
5135
+ label: "OpenCode user config",
5136
+ path: getExistingConfigPath(),
5137
+ state: status.targets[0]?.state
5138
+ },
5139
+ {
5140
+ id: "thoth-agents-config",
5141
+ label: "thoth-agents plugin config",
5142
+ path: getExistingLiteConfigPath(),
5143
+ state: status.targets[1]?.state
5144
+ }
5145
+ ],
5146
+ backup: backupFromItems(items),
5147
+ items,
5148
+ warnings,
5149
+ disclaimers: defaultDisclaimers()
5150
+ }
4664
5151
  };
4665
5152
  }
4666
5153
  function buildOpenCodeUpdatePlan(_context = { cwd: process.cwd() }) {
4667
5154
  const path4 = getExistingConfigPath();
4668
- return planFromItems(
5155
+ const { plan, status } = planFromItems(
4669
5156
  "opencode-update-preview",
4670
5157
  "update",
4671
5158
  "Update OpenCode managed plugin entry",
@@ -4674,12 +5161,17 @@ function buildOpenCodeUpdatePlan(_context = { cwd: process.cwd() }) {
4674
5161
  {
4675
5162
  title: "Ensure OpenCode plugin points at thoth-agents@latest",
4676
5163
  target: targetForMainConfig(),
4677
- state: getOpenCodeStatus().state,
5164
+ state: getOpenCodeStatus(_context).state,
4678
5165
  preview: `plugin: ["${EXPECTED_PLUGIN}"]`,
4679
5166
  backup: defaultBackup(path4)
4680
5167
  }
4681
- ]
5168
+ ],
5169
+ _context
4682
5170
  );
5171
+ return issueOpenCodePlan(plan, status, _context, {
5172
+ kind: "fixed",
5173
+ action: "update"
5174
+ });
4683
5175
  }
4684
5176
  function buildOpenCodeSyncPlan(_context = { cwd: process.cwd() }) {
4685
5177
  const generatedConfig = generateLiteConfig({
@@ -4691,7 +5183,7 @@ function buildOpenCodeSyncPlan(_context = { cwd: process.cwd() }) {
4691
5183
  reset: false
4692
5184
  });
4693
5185
  const litePath = getExistingLiteConfigPath();
4694
- return planFromItems(
5186
+ const { plan, status } = planFromItems(
4695
5187
  "opencode-sync-preview",
4696
5188
  "sync",
4697
5189
  "Sync OpenCode managed configuration",
@@ -4700,7 +5192,7 @@ function buildOpenCodeSyncPlan(_context = { cwd: process.cwd() }) {
4700
5192
  {
4701
5193
  title: "Ensure OpenCode plugin points at thoth-agents@latest",
4702
5194
  target: targetForMainConfig(),
4703
- state: getOpenCodeStatus().state,
5195
+ state: getOpenCodeStatus(_context).state,
4704
5196
  preview: `plugin: ["${EXPECTED_PLUGIN}"]`,
4705
5197
  backup: defaultBackup(getExistingConfigPath())
4706
5198
  },
@@ -4715,9 +5207,15 @@ function buildOpenCodeSyncPlan(_context = { cwd: process.cwd() }) {
4715
5207
  target: targetForLiteConfig(),
4716
5208
  preview: JSON.stringify(generatedConfig, null, 2),
4717
5209
  backup: defaultBackup(litePath)
4718
- }
4719
- ]
5210
+ },
5211
+ bundledSkillsRefreshItem()
5212
+ ],
5213
+ _context
4720
5214
  );
5215
+ return issueOpenCodePlan(plan, status, _context, {
5216
+ kind: "fixed",
5217
+ action: "sync"
5218
+ });
4721
5219
  }
4722
5220
  function tuiInstallConfig() {
4723
5221
  return {
@@ -4739,7 +5237,7 @@ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
4739
5237
  equivalentCommand: "install --agent=opencode --no-tui --tmux=no --skills=yes"
4740
5238
  };
4741
5239
  const litePath = getExistingLiteConfigPath();
4742
- return planFromItems(
5240
+ const { plan, status } = planFromItems(
4743
5241
  "opencode-install-preview",
4744
5242
  "install",
4745
5243
  "Preview install",
@@ -4751,7 +5249,7 @@ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
4751
5249
  kind: "config",
4752
5250
  path: getExistingConfigPath(),
4753
5251
  label: "OpenCode install options",
4754
- state: getOpenCodeStatus().state,
5252
+ state: getOpenCodeStatus(_context).state,
4755
5253
  expected: "--no-tui --tmux=no --skills=yes"
4756
5254
  },
4757
5255
  preview: JSON.stringify(installPreview, null, 2)
@@ -4759,7 +5257,7 @@ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
4759
5257
  {
4760
5258
  title: "Ensure OpenCode plugin points at thoth-agents@latest",
4761
5259
  target: targetForMainConfig(),
4762
- state: getOpenCodeStatus().state,
5260
+ state: getOpenCodeStatus(_context).state,
4763
5261
  preview: `plugin: ["${EXPECTED_PLUGIN}"]`,
4764
5262
  backup: defaultBackup(getExistingConfigPath())
4765
5263
  },
@@ -4775,6 +5273,7 @@ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
4775
5273
  preview: JSON.stringify(generatedConfig, null, 2),
4776
5274
  backup: defaultBackup(litePath)
4777
5275
  },
5276
+ bundledSkillsRefreshItem(),
4778
5277
  {
4779
5278
  title: "Install recommended external skills",
4780
5279
  target: {
@@ -4784,8 +5283,13 @@ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
4784
5283
  },
4785
5284
  preview: JSON.stringify({ installSkills: true }, null, 2)
4786
5285
  }
4787
- ]
5286
+ ],
5287
+ _context
4788
5288
  );
5289
+ return issueOpenCodePlan(plan, status, _context, {
5290
+ kind: "fixed",
5291
+ action: "install"
5292
+ });
4789
5293
  }
4790
5294
  function normalizeRoleModel(input) {
4791
5295
  if (input.provider && !input.model.includes("/")) {
@@ -4794,16 +5298,24 @@ function normalizeRoleModel(input) {
4794
5298
  return input.model;
4795
5299
  }
4796
5300
  function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
4797
- const items = input.roles.map((role) => {
5301
+ const normalizedRoles = input.roles.map((role) => {
4798
5302
  const model = normalizeRoleModel(role);
4799
5303
  const effort = resolveOpenCodeEffort(role);
5304
+ return Object.freeze({
5305
+ role: role.role,
5306
+ model,
5307
+ variant: effort.ok ? effort.variant ?? null : null,
5308
+ effort
5309
+ });
5310
+ });
5311
+ const items = normalizedRoles.map(({ role, model, variant, effort }) => {
4800
5312
  return {
4801
- title: `Set ${role.role} OpenCode model override`,
5313
+ title: `Set ${role} OpenCode model override`,
4802
5314
  target: targetForLiteConfig(),
4803
5315
  preview: JSON.stringify({
4804
- [role.role]: {
5316
+ [role]: {
4805
5317
  model,
4806
- variant: effort.ok ? effort.variant ?? null : null
5318
+ variant
4807
5319
  }
4808
5320
  }),
4809
5321
  backup: defaultBackup(getExistingLiteConfigPath()),
@@ -4818,12 +5330,13 @@ function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
4818
5330
  } : {}
4819
5331
  };
4820
5332
  });
4821
- const plan = planFromItems(
5333
+ const { plan, status } = planFromItems(
4822
5334
  "opencode-model-config-preview",
4823
5335
  "model-config",
4824
5336
  "Configure OpenCode role model overrides",
4825
5337
  "Preview thoth-agents role model overrides in the plugin config agents map.",
4826
- items
5338
+ items,
5339
+ _context
4827
5340
  );
4828
5341
  const effortWarnings = items.flatMap((item) => item.warnings ?? []);
4829
5342
  if (effortWarnings.length > 0) {
@@ -4831,7 +5344,7 @@ function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
4831
5344
  plan.warnings.push(...effortWarnings);
4832
5345
  }
4833
5346
  if (input.harness !== "opencode") {
4834
- return {
5347
+ const rejectedPlan = {
4835
5348
  ...plan,
4836
5349
  canApply: false,
4837
5350
  warnings: [
@@ -4843,12 +5356,13 @@ function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
4843
5356
  }
4844
5357
  ]
4845
5358
  };
5359
+ return rejectedPlan;
4846
5360
  }
4847
5361
  const unsupportedRoles = input.roles.filter(
4848
5362
  (role) => !ROLE_NAMES.includes(role.role)
4849
5363
  );
4850
5364
  if (input.roles.length === 0 || unsupportedRoles.length > 0) {
4851
- return {
5365
+ const rejectedPlan = {
4852
5366
  ...plan,
4853
5367
  canApply: false,
4854
5368
  warnings: [
@@ -4860,116 +5374,154 @@ function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
4860
5374
  }
4861
5375
  ]
4862
5376
  };
5377
+ return rejectedPlan;
4863
5378
  }
4864
- return plan;
5379
+ if (effortWarnings.length > 0) {
5380
+ return plan;
5381
+ }
5382
+ const payloadRoles = Object.freeze(
5383
+ normalizedRoles.map(
5384
+ ({ role, model, variant }) => Object.freeze({ role, model, variant })
5385
+ )
5386
+ );
5387
+ if (!plan.canApply || invalidModelPayloadReason(payloadRoles)) {
5388
+ return plan;
5389
+ }
5390
+ return issueOpenCodePlan(plan, status, _context, {
5391
+ kind: "model",
5392
+ roles: payloadRoles
5393
+ });
4865
5394
  }
4866
- function rejectPlan3(plan, message, severity = "critical") {
5395
+ function rejectPlan3(plan, message, severity = "critical", code = "opencode-plan-rejected", context2 = { cwd: process.cwd() }) {
5396
+ const issued = issuedOpenCodePlans.get(plan);
5397
+ const actionDescriptor = Object.getOwnPropertyDescriptor(plan, "action");
5398
+ const describedAction = actionDescriptor && "value" in actionDescriptor && typeof actionDescriptor.value === "string" ? actionDescriptor.value : void 0;
5399
+ const action = issued?.action ?? (describedAction === "install" || describedAction === "update" || describedAction === "sync" || describedAction === "model-config" ? describedAction : "repair");
5400
+ const liveStatus = getOpenCodeStatus(context2);
5401
+ const blockers = blockingDetails(
5402
+ action,
5403
+ liveStatus,
5404
+ code === "opencode-plan-live-state-changed"
5405
+ );
4867
5406
  return {
4868
- harness: plan.harness,
4869
- action: plan.action,
5407
+ harness: issued?.harness ?? "opencode",
5408
+ action,
4870
5409
  applied: false,
4871
5410
  summary: message,
4872
5411
  changedTargets: [],
5412
+ diagnosticTargets: blockers.targets,
4873
5413
  backups: [],
4874
- warnings: [{ severity, message }],
5414
+ warnings: [...liveStatus.diagnostics, { severity, code, message }],
4875
5415
  disclaimers: defaultDisclaimers()
4876
5416
  };
4877
5417
  }
4878
- function ensureLatestPluginEntry() {
4879
- const configPath = getExistingConfigPath();
4880
- try {
4881
- ensureOpenCodeConfigDir();
4882
- const { config: parsedConfig, error } = parseConfig(configPath);
4883
- if (error) {
4884
- return {
4885
- success: false,
4886
- configPath,
4887
- error: `Failed to parse config: ${error}`
4888
- };
4889
- }
4890
- const config = parsedConfig ?? {};
4891
- const plugins = Array.isArray(config.plugin) ? config.plugin : [];
4892
- config.plugin = [
4893
- ...plugins.filter(
4894
- (plugin) => plugin !== PACKAGE_NAME && !plugin.startsWith(`${PACKAGE_NAME}@`)
4895
- ),
4896
- EXPECTED_PLUGIN
4897
- ];
4898
- writeConfig(configPath, config);
4899
- return { success: true, configPath };
4900
- } catch (err) {
4901
- return {
4902
- success: false,
4903
- configPath,
4904
- error: `Failed to update OpenCode plugin config: ${err}`
4905
- };
4906
- }
4907
- }
4908
5418
  function validateApplyPlan(plan) {
5419
+ const issued = issuedOpenCodePlans.get(plan);
5420
+ if (!issued) {
5421
+ return rejectPlan3(
5422
+ plan,
5423
+ "OpenCode plan was not issued by this process.",
5424
+ "critical",
5425
+ "opencode-plan-unissued"
5426
+ );
5427
+ }
5428
+ const planDigest = canonicalDigest(plan);
5429
+ if (!planDigest.ok || !planDigest.digest) {
5430
+ return rejectPlan3(
5431
+ plan,
5432
+ "OpenCode plan contains noncanonical data and cannot be applied.",
5433
+ "critical",
5434
+ "opencode-plan-noncanonical",
5435
+ issued.context
5436
+ );
5437
+ }
5438
+ if (planDigest.digest !== issued.planDigest) {
5439
+ return rejectPlan3(
5440
+ plan,
5441
+ "OpenCode plan changed after it was issued.",
5442
+ "critical",
5443
+ "opencode-plan-mutated",
5444
+ issued.context
5445
+ );
5446
+ }
5447
+ const liveStatus = getOpenCodeStatus(issued.context);
5448
+ const statusDigest = liveStatusDigest(liveStatus);
5449
+ if (!statusDigest.ok || !statusDigest.digest || statusDigest.digest !== issued.liveStatusDigest) {
5450
+ return rejectPlan3(
5451
+ plan,
5452
+ "OpenCode managed state changed after plan preview.",
5453
+ "critical",
5454
+ "opencode-plan-live-state-changed",
5455
+ issued.context
5456
+ );
5457
+ }
4909
5458
  if (plan.harness !== "opencode") {
4910
- return rejectPlan3(plan, "Only OpenCode operation plans can be applied.");
5459
+ return rejectPlan3(
5460
+ plan,
5461
+ "Only OpenCode operation plans can be applied.",
5462
+ "critical",
5463
+ "opencode-plan-harness-mismatch",
5464
+ issued.context
5465
+ );
4911
5466
  }
4912
5467
  if (!plan.canApply) {
4913
5468
  return rejectPlan3(
4914
5469
  plan,
4915
- "OpenCode plan cannot be applied because canApply is false."
5470
+ "OpenCode plan cannot be applied because canApply is false.",
5471
+ "critical",
5472
+ "opencode-plan-blocked",
5473
+ issued.context
4916
5474
  );
4917
5475
  }
4918
5476
  if (!["install", "update", "sync", "model-config"].includes(plan.action)) {
4919
5477
  return rejectPlan3(
4920
5478
  plan,
4921
- `Unsupported OpenCode apply action: ${plan.action}.`
5479
+ `Unsupported OpenCode apply action: ${plan.action}.`,
5480
+ "critical",
5481
+ "opencode-plan-action-unsupported",
5482
+ issued.context
4922
5483
  );
4923
5484
  }
4924
5485
  if (plan.items.length === 0) {
4925
- return rejectPlan3(plan, "OpenCode plan has no items to apply.");
4926
- }
4927
- if (plan.items.some(
4928
- (item) => !item.title || !item.target.path && item.target.kind !== "skill" || item.state === "drift" || item.state === "unknown"
4929
- )) {
4930
5486
  return rejectPlan3(
4931
5487
  plan,
4932
- "OpenCode plan contains malformed or unsafe items."
5488
+ "OpenCode plan has no items to apply.",
5489
+ "critical",
5490
+ "opencode-plan-empty",
5491
+ issued.context
4933
5492
  );
4934
5493
  }
4935
5494
  return null;
4936
5495
  }
4937
- function applyModelPlan(plan) {
4938
- const roleModels = /* @__PURE__ */ new Map();
4939
- for (const item of plan.items) {
4940
- const match = /^Set (.+) OpenCode model override$/.exec(item.title);
4941
- if (!match) {
4942
- return rejectPlan3(
4943
- plan,
4944
- "OpenCode model plan contains an unrecognized item."
4945
- );
5496
+ function invalidModelPayloadReason(roles) {
5497
+ if (!Array.isArray(roles) || roles.length === 0) {
5498
+ return "Model payload must contain at least one role override.";
5499
+ }
5500
+ const seen = /* @__PURE__ */ new Set();
5501
+ for (const role of roles) {
5502
+ if (!role || typeof role !== "object" || typeof role.role !== "string" || !ROLE_NAMES.includes(role.role)) {
5503
+ return "Model payload contains an unsupported role.";
4946
5504
  }
4947
- let parsed2;
4948
- try {
4949
- parsed2 = item.preview ? JSON.parse(item.preview) : {};
4950
- } catch {
4951
- return rejectPlan3(
4952
- plan,
4953
- "OpenCode model plan contains malformed preview JSON."
4954
- );
5505
+ if (seen.has(role.role)) {
5506
+ return `Model payload contains duplicate role ${role.role}.`;
4955
5507
  }
4956
- const role = match[1] ?? "";
4957
- const model = parsed2[role]?.model;
4958
- const variant = parsed2[role]?.variant;
4959
- if (!ROLE_NAMES.includes(role) || typeof model !== "string" || variant !== null && typeof variant !== "string") {
4960
- return rejectPlan3(
4961
- plan,
4962
- "OpenCode model plan contains an invalid role or model."
4963
- );
5508
+ seen.add(role.role);
5509
+ if (typeof role.model !== "string" || role.model.trim().length === 0) {
5510
+ return `Model payload for ${role.role} has an empty model.`;
5511
+ }
5512
+ if (role.variant !== null && typeof role.variant !== "string") {
5513
+ return `Model payload for ${role.role} has an invalid variant.`;
4964
5514
  }
4965
- roleModels.set(role, { model, variant });
4966
- }
4967
- if (roleModels.size === 0) {
4968
- return rejectPlan3(
4969
- plan,
4970
- "OpenCode model plan does not contain any role overrides."
4971
- );
4972
5515
  }
5516
+ return void 0;
5517
+ }
5518
+ function applyModelPlan(roles) {
5519
+ const roleModels = new Map(
5520
+ roles.map((role) => [
5521
+ role.role,
5522
+ { model: role.model, variant: role.variant }
5523
+ ])
5524
+ );
4973
5525
  ensureConfigDir();
4974
5526
  const targetPath = getExistingLiteConfigPath();
4975
5527
  const parsed = parseConfig(targetPath);
@@ -5036,68 +5588,148 @@ function applyModelPlan(plan) {
5036
5588
  disclaimers: defaultDisclaimers()
5037
5589
  };
5038
5590
  }
5039
- function applyInstallSkills(plan) {
5591
+ function applyRecommendedSkills() {
5592
+ const warnings = [];
5040
5593
  for (const skill of RECOMMENDED_SKILLS) {
5041
- const result = installRecommendedSkill(skill);
5042
- if (result.status === "failed") {
5043
- return rejectPlan3(
5044
- plan,
5045
- `Failed to install recommended OpenCode skill: ${skill.name}.`
5046
- );
5594
+ try {
5595
+ const result = installRecommendedSkill(skill);
5596
+ if (result.status !== "failed") continue;
5597
+ warnings.push({
5598
+ severity: "important",
5599
+ code: "opencode-recommended-skill-failed",
5600
+ message: `Failed to install optional recommended OpenCode skill: ${skill.name}.`
5601
+ });
5602
+ } catch (error) {
5603
+ const reason = error instanceof Error ? error.message : String(error);
5604
+ warnings.push({
5605
+ severity: "important",
5606
+ code: "opencode-recommended-skill-failed",
5607
+ message: `Failed to install optional recommended OpenCode skill ${skill.name}: ${reason}.`
5608
+ });
5047
5609
  }
5048
5610
  }
5049
- const bundled = installCustomSkills();
5611
+ return warnings;
5612
+ }
5613
+ function bundledSkillsFailure(plan, changedTargets, backups, warnings, detail) {
5614
+ const message = `Failed to install bundled thoth-agents OpenCode skills: ${detail}.`;
5615
+ return {
5616
+ harness: plan.harness,
5617
+ action: plan.action,
5618
+ applied: false,
5619
+ summary: message,
5620
+ changedTargets,
5621
+ backups,
5622
+ warnings: [
5623
+ ...warnings,
5624
+ {
5625
+ severity: "critical",
5626
+ code: "opencode-bundled-skills-failed",
5627
+ message
5628
+ }
5629
+ ],
5630
+ disclaimers: defaultDisclaimers()
5631
+ };
5632
+ }
5633
+ function applyBundledSkills(plan, changedTargets, backups, warnings) {
5634
+ let bundled;
5635
+ try {
5636
+ bundled = installCustomSkills();
5637
+ } catch (error) {
5638
+ const detail = error instanceof Error ? error.message : String(error);
5639
+ return bundledSkillsFailure(
5640
+ plan,
5641
+ changedTargets,
5642
+ backups,
5643
+ warnings,
5644
+ detail
5645
+ );
5646
+ }
5050
5647
  if (!bundled.success) {
5051
- return rejectPlan3(
5648
+ const failedNames = bundled.failedSkills.map(({ skill }) => skill.name);
5649
+ const detail = failedNames.length > 0 ? failedNames.join(", ") : "unknown bundled skills";
5650
+ return bundledSkillsFailure(
5052
5651
  plan,
5053
- "Failed to install bundled thoth-agents OpenCode skills."
5652
+ changedTargets,
5653
+ backups,
5654
+ warnings,
5655
+ detail
5054
5656
  );
5055
5657
  }
5658
+ changedTargets.push({
5659
+ kind: "skill",
5660
+ label: "Bundled thoth-agents OpenCode skills",
5661
+ state: "installed",
5662
+ observed: "managed bundled skills refreshed"
5663
+ });
5056
5664
  return null;
5057
5665
  }
5058
5666
  function applyOpenCodePlan(plan) {
5059
5667
  const rejection = validateApplyPlan(plan);
5060
5668
  if (rejection) return rejection;
5061
- const status = getOpenCodeStatus();
5062
- if (!classifyApplySafety(status.state)) {
5669
+ const issued = issuedOpenCodePlans.get(plan);
5670
+ if (!issued) {
5063
5671
  return rejectPlan3(
5064
5672
  plan,
5065
- `OpenCode state is ${status.state}; refusing to apply plan without a safe status.`
5673
+ "OpenCode plan provenance was lost before apply.",
5674
+ "critical",
5675
+ "opencode-plan-unissued"
5066
5676
  );
5067
5677
  }
5068
- if (plan.action === "model-config") return applyModelPlan(plan);
5678
+ const status = getOpenCodeStatus(issued.context);
5679
+ if (!canApplyToManagedHealth(plan.action, status)) {
5680
+ return rejectPlan3(
5681
+ plan,
5682
+ `OpenCode state is ${status.state}; refusing to apply plan without a safe status.`,
5683
+ "critical",
5684
+ "opencode-plan-live-state-blocked",
5685
+ issued.context
5686
+ );
5687
+ }
5688
+ if (issued.payload.kind === "model") {
5689
+ const invalidPayload = invalidModelPayloadReason(issued.payload.roles);
5690
+ if (invalidPayload) {
5691
+ return rejectPlan3(
5692
+ plan,
5693
+ invalidPayload,
5694
+ "critical",
5695
+ "opencode-model-payload-invalid",
5696
+ issued.context
5697
+ );
5698
+ }
5699
+ return applyModelPlan(issued.payload.roles);
5700
+ }
5069
5701
  const changedTargets = [];
5070
5702
  const backups = [];
5071
- const pluginResult = ensureLatestPluginEntry();
5072
- if (!pluginResult.success) {
5703
+ const warnings = [];
5704
+ const mainConfigResult = updateOpenCodeMainConfig({
5705
+ ensurePlugin: true,
5706
+ disableDefaults: issued.payload.action === "sync" || issued.payload.action === "install"
5707
+ });
5708
+ if (!mainConfigResult.success) {
5073
5709
  return rejectPlan3(
5074
5710
  plan,
5075
- pluginResult.error ?? "Failed to update OpenCode plugin config."
5711
+ mainConfigResult.error ?? "Failed to update OpenCode main config.",
5712
+ "critical",
5713
+ "opencode-main-config-update-failed",
5714
+ issued.context
5076
5715
  );
5077
5716
  }
5078
5717
  changedTargets.push({
5079
5718
  ...targetForMainConfig("installed"),
5080
5719
  observed: `plugin includes ${EXPECTED_PLUGIN}`
5081
5720
  });
5082
- if (existsSync10(`${pluginResult.configPath}.bak`)) {
5721
+ if (existsSync10(`${mainConfigResult.configPath}.bak`)) {
5083
5722
  backups.push({
5084
- path: `${pluginResult.configPath}.bak`,
5723
+ path: `${mainConfigResult.configPath}.bak`,
5085
5724
  label: "OpenCode config backup"
5086
5725
  });
5087
5726
  }
5088
- if (plan.action === "sync" || plan.action === "install") {
5089
- const defaultAgentResult = disableDefaultAgents();
5090
- if (!defaultAgentResult.success) {
5091
- return rejectPlan3(
5092
- plan,
5093
- defaultAgentResult.error ?? "Failed to disable OpenCode default agents."
5094
- );
5095
- }
5727
+ if (issued.payload.action === "sync" || issued.payload.action === "install") {
5096
5728
  const liteResult = writeLiteConfig(
5097
5729
  {
5098
5730
  agent: "opencode",
5099
5731
  hasTmux: false,
5100
- installSkills: plan.action === "install",
5732
+ installSkills: issued.payload.action === "install",
5101
5733
  installCustomSkills: true,
5102
5734
  dryRun: false,
5103
5735
  reset: false
@@ -5120,15 +5752,23 @@ function applyOpenCodePlan(plan) {
5120
5752
  label: "thoth-agents config backup"
5121
5753
  });
5122
5754
  }
5123
- }
5124
- if (plan.action === "install") {
5125
- const skillsRejection = applyInstallSkills(plan);
5755
+ const skillsRejection = applyBundledSkills(
5756
+ plan,
5757
+ changedTargets,
5758
+ backups,
5759
+ warnings
5760
+ );
5126
5761
  if (skillsRejection) return skillsRejection;
5762
+ }
5763
+ if (issued.payload.action === "install") {
5764
+ warnings.push(...applyRecommendedSkills());
5127
5765
  changedTargets.push({
5128
5766
  kind: "skill",
5129
- label: "Recommended and bundled OpenCode skills",
5130
- state: "installed",
5131
- observed: "skills=yes processed"
5767
+ label: "Recommended OpenCode skills",
5768
+ state: warnings.some(
5769
+ (warning3) => warning3.code === "opencode-recommended-skill-failed"
5770
+ ) ? "drift" : "installed",
5771
+ observed: "optional skills=yes processed"
5132
5772
  });
5133
5773
  }
5134
5774
  return {
@@ -5138,7 +5778,7 @@ function applyOpenCodePlan(plan) {
5138
5778
  summary: plan.action === "install" ? "Applied OpenCode install plan." : plan.action === "sync" ? "Applied OpenCode managed configuration sync." : "Applied OpenCode plugin update.",
5139
5779
  changedTargets,
5140
5780
  backups,
5141
- warnings: [],
5781
+ warnings,
5142
5782
  disclaimers: defaultDisclaimers()
5143
5783
  };
5144
5784
  }
@@ -5476,12 +6116,12 @@ function getOpenCodeModelsInvocation(platform = process.platform) {
5476
6116
  };
5477
6117
  if (platform === "win32") {
5478
6118
  return {
5479
- command: "opencode models",
6119
+ command: "opencode models --pure",
5480
6120
  args: [],
5481
6121
  options: { ...options, shell: true }
5482
6122
  };
5483
6123
  }
5484
- return { command: "opencode", args: ["models"], options };
6124
+ return { command: "opencode", args: ["models", "--pure"], options };
5485
6125
  }
5486
6126
  function getNativeOpenCodeOptions() {
5487
6127
  try {
@@ -5636,13 +6276,15 @@ function readRoleField(config, role, field) {
5636
6276
  function getOpenCodeModelRoles() {
5637
6277
  const parsed = parseConfig(getExistingLiteConfigPath());
5638
6278
  const agents = parsed.config?.agents && typeof parsed.config.agents === "object" ? parsed.config.agents : void 0;
5639
- const presets = parsed.config?.presets && typeof parsed.config.presets === "object" ? parsed.config.presets : {};
5640
- const openaiPreset = presets.openai && typeof presets.openai === "object" ? presets.openai : void 0;
6279
+ const presets = parsed.config?.presets && typeof parsed.config.presets === "object" && !Array.isArray(parsed.config.presets) ? parsed.config.presets : {};
6280
+ const presetName = typeof parsed.config?.preset === "string" ? parsed.config.preset : void 0;
6281
+ const selectedPreset = presetName !== void 0 ? presets[presetName] : void 0;
6282
+ const activePreset = selectedPreset && typeof selectedPreset === "object" && !Array.isArray(selectedPreset) ? selectedPreset : void 0;
5641
6283
  return ALL_AGENT_NAMES.map((role) => {
5642
- const variant = readRoleField(agents, role, "variant") ?? readRoleField(openaiPreset, role, "variant");
6284
+ const variant = readRoleField(agents, role, "variant") ?? readRoleField(activePreset, role, "variant");
5643
6285
  return {
5644
6286
  role,
5645
- model: readRoleField(agents, role, "model") ?? readRoleField(openaiPreset, role, "model") ?? DEFAULT_MODELS[role] ?? "openai/gpt-5.4",
6287
+ model: readRoleField(agents, role, "model") ?? readRoleField(activePreset, role, "model") ?? DEFAULT_MODELS[role] ?? "openai/gpt-5.4",
5646
6288
  effort: variant ? { kind: "effort", value: variant } : { kind: "inherit" }
5647
6289
  };
5648
6290
  });