thoth-agents 0.2.9 → 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.
- package/README.md +11 -4
- package/dist/agents/prompt-dialects.d.ts +8 -0
- package/dist/{chunk-75JOF37E.js → chunk-6BNAV534.js} +1 -1
- package/dist/{chunk-6NMPKYWY.js → chunk-ASKXWU6E.js} +850 -201
- package/dist/{chunk-YLRPDTU7.js → chunk-OZE7JVGS.js} +80 -54
- package/dist/cli/config-io.d.ts +5 -0
- package/dist/cli/custom-skills.d.ts +2 -1
- package/dist/cli/index.js +19 -18
- package/dist/cli/operations/types.d.ts +2 -0
- package/dist/cli/tui/index.js +91 -18
- package/dist/harness/adapters/codex-surfaces.d.ts +7 -8
- package/dist/harness/core/sdd.d.ts +2 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/src/skills/sdd-tasks/SKILL.md +39 -15
|
@@ -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-
|
|
40
|
+
} from "./chunk-OZE7JVGS.js";
|
|
41
41
|
|
|
42
42
|
// src/cli/codex-paths.ts
|
|
43
43
|
import { homedir } from "os";
|
|
@@ -611,26 +611,31 @@ var CODEX_SURFACES = [
|
|
|
611
611
|
id: "per-agent-runtime-permissions",
|
|
612
612
|
target: "permission-control",
|
|
613
613
|
status: "unsupported",
|
|
614
|
-
fields: [
|
|
614
|
+
fields: [
|
|
615
|
+
"named installed-role selection",
|
|
616
|
+
"per-role permission enforcement",
|
|
617
|
+
"per-agent tool allow/deny equivalent to OpenCode permissions"
|
|
618
|
+
],
|
|
615
619
|
diagnosticCode: "codex.permission.memory.enforcement_gap",
|
|
616
|
-
summary: "
|
|
620
|
+
summary: "Named installed-role selection and per-role permission enforcement are instruction-only because collaboration.spawn_agent has no role selector; OpenCode-style per-agent MCP/tool deny maps are not validated.",
|
|
617
621
|
evidence: "Codex approvals/security docs cover sandbox and approval policies, not exact OpenCode per-agent permission maps.",
|
|
618
622
|
fallback: "instruction-only"
|
|
619
623
|
},
|
|
620
624
|
{
|
|
621
625
|
id: "programmatic-delegation-runtime",
|
|
622
626
|
target: "delegation-runtime",
|
|
623
|
-
status: "
|
|
627
|
+
status: "validated",
|
|
624
628
|
fields: [
|
|
625
|
-
"
|
|
626
|
-
"
|
|
627
|
-
"
|
|
628
|
-
"
|
|
629
|
+
"collaboration.spawn_agent",
|
|
630
|
+
"collaboration.wait_agent",
|
|
631
|
+
"collaboration.list_agents",
|
|
632
|
+
"collaboration.send_message",
|
|
633
|
+
"collaboration.followup_task",
|
|
634
|
+
"collaboration.interrupt_agent"
|
|
629
635
|
],
|
|
630
|
-
diagnosticCode: "codex.delegation.runtime.
|
|
631
|
-
summary: "Codex
|
|
632
|
-
evidence: "
|
|
633
|
-
fallback: "instruction-only"
|
|
636
|
+
diagnosticCode: "codex.delegation.runtime.validated",
|
|
637
|
+
summary: "The current Codex collaboration surface supports generic programmatic delegation, mailbox waits, live task-tree inspection, messages, follow-up turns, and explicit interruption.",
|
|
638
|
+
evidence: "The callable runtime exposes collaboration.spawn_agent with task_name, message, and optional fork_turns plus wait_agent, list_agents, send_message, followup_task, and interrupt_agent."
|
|
634
639
|
},
|
|
635
640
|
{
|
|
636
641
|
id: "parent-context-injection",
|
|
@@ -1432,15 +1437,18 @@ function renderCodexRolePrompt(roleName, config, model) {
|
|
|
1432
1437
|
function codexInternalHandoffGuidance() {
|
|
1433
1438
|
return [
|
|
1434
1439
|
"<codex-delegation-guidance>",
|
|
1435
|
-
"- The user has explicitly authorized this generated Codex orchestrator to use `
|
|
1436
|
-
"-
|
|
1440
|
+
"- The user has explicitly authorized this generated Codex orchestrator to use `collaboration.spawn_agent` whenever delegation is required by these instructions, without needing a fresh user request for subagents in each task.",
|
|
1441
|
+
"- Collaboration tools are direct tools and must not be called from inside `functions.exec`.",
|
|
1442
|
+
"- Delegate with `collaboration.spawn_agent` using `task_name`, `message`, and optional `fork_turns`; `fork_turns` must be `none`, `all`, or a positive integer string.",
|
|
1443
|
+
"- Role behavior is carried by `task_name` and `message`; named installed-role selection and hard role/profile enforcement are instruction-level because this surface has no role selector.",
|
|
1437
1444
|
"- Pass the self-contained delegated task instructions plus handoff retrieval instructions in `message`; do not embed the root-owned handoff summary body in `message`.",
|
|
1438
1445
|
'- When memory recovery is delegated, include parent `session_id`, project, permissions, and the recovery funnel `mem_recall(mode="compact")` -> `mem_recall(mode="context")` -> `mem_get(...)`.',
|
|
1439
1446
|
'- For that funnel, use `mem_recall` `limit` from 1 to 20; use `mem_get` with `kind="observation"|"prompt"`, `include_timeline=true` plus `before`/`after`, and `offset`/`max_length`; `mem_project(action="graph")` relations are `HAS_TYPE`, `IN_PROJECT`, `HAS_TOPIC_KEY`, `HAS_WHAT`, `HAS_WHY`, `HAS_WHERE`, and `HAS_LEARNED`.',
|
|
1440
|
-
"- Do not include the handoff body in `message
|
|
1441
|
-
"-
|
|
1442
|
-
"-
|
|
1443
|
-
"- Use `
|
|
1447
|
+
"- Do not include the handoff body in `message`.",
|
|
1448
|
+
"- `collaboration.wait_agent` waits for mailbox updates. A `collaboration.wait_agent` timeout or silence is nonterminal and remains in progress, so inspect `collaboration.list_agents` with the same task path before deciding what to do next.",
|
|
1449
|
+
"- `collaboration.send_message` delivers a message without triggering a turn; `collaboration.followup_task` triggers a turn when an idle task must continue.",
|
|
1450
|
+
"- Use `collaboration.interrupt_agent` only for explicit cancellation, a deadline, or supersession.",
|
|
1451
|
+
"- Do not invent result payloads or numeric wait, poll, or timeout rules.",
|
|
1444
1452
|
"- Memory ownership, handoff recovery, permissions, and prompt-body exclusion are instruction-level unless the active Codex runtime documents stronger enforcement.",
|
|
1445
1453
|
"</codex-delegation-guidance>"
|
|
1446
1454
|
].join("\n");
|
|
@@ -1488,8 +1496,7 @@ function renderCodexRootInstructions(config) {
|
|
|
1488
1496
|
"- If metadata tooling is unavailable, or the required fields are missing, disclose that memory bootstrap could not run and continue without claiming memory was saved.",
|
|
1489
1497
|
'- Before delegating after meaningful context changes, save or refresh the handoff body with root-owned mem_session(action="summary") or mem_save(kind="session_summary") when available; if unavailable, disclose that root-owned compaction could not be persisted.',
|
|
1490
1498
|
"- Use the ambient Codex root session as the delegate-first root coordinator; do not generate or select an orchestrator TOML.",
|
|
1491
|
-
"- Delegate by invoking `
|
|
1492
|
-
"- After receiving a delegated subagent response, close that subagent session unless you will retry or intentionally keep using that exact same session; explorer and librarian sessions must always be closed immediately after their response, and retry sessions must be closed after the retry result unless explicit same-session reuse is still required.",
|
|
1499
|
+
"- Delegate by invoking `collaboration.spawn_agent`; use role-prefixed task names and explicit role instructions for the installed Codex role agents: explorer, librarian, oracle, designer, quick, and deep.",
|
|
1493
1500
|
"- Use packaged thoth-agents plugin capabilities through Codex plugin, skill, MCP, and hook review surfaces after enabling them with /plugins and /hooks.",
|
|
1494
1501
|
"- For blocking user decisions in Codex Default mode, use request_user_input after features.default_mode_request_user_input is enabled; do not ask those questions in plain prose.",
|
|
1495
1502
|
"- Whenever the root orchestrator calls `request_user_input`, it MUST NEVER set or pass `autoResolutionMs`; omit the field entirely.",
|
|
@@ -4173,6 +4180,7 @@ function applyCodexPlan(plan) {
|
|
|
4173
4180
|
}
|
|
4174
4181
|
|
|
4175
4182
|
// src/cli/operations/opencode.ts
|
|
4183
|
+
import { createHash as createHash3 } from "crypto";
|
|
4176
4184
|
import { existsSync as existsSync10 } from "fs";
|
|
4177
4185
|
import { join as join10 } from "path";
|
|
4178
4186
|
|
|
@@ -4285,6 +4293,143 @@ var PACKAGE_NAME = "thoth-agents";
|
|
|
4285
4293
|
var EXPECTED_PLUGIN = `${PACKAGE_NAME}@latest`;
|
|
4286
4294
|
var OPENAI_PRESET = "openai";
|
|
4287
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
|
+
}
|
|
4288
4433
|
var openCodeActions = [
|
|
4289
4434
|
{
|
|
4290
4435
|
id: "opencode-status",
|
|
@@ -4353,7 +4498,7 @@ function targetForMainConfig(state) {
|
|
|
4353
4498
|
kind: "config",
|
|
4354
4499
|
path: getExistingConfigPath(),
|
|
4355
4500
|
label: "OpenCode config",
|
|
4356
|
-
state,
|
|
4501
|
+
...state ? { state } : {},
|
|
4357
4502
|
expected: `plugin includes ${EXPECTED_PLUGIN}`
|
|
4358
4503
|
};
|
|
4359
4504
|
}
|
|
@@ -4362,7 +4507,7 @@ function targetForLiteConfig(state) {
|
|
|
4362
4507
|
kind: "config",
|
|
4363
4508
|
path: getExistingLiteConfigPath(),
|
|
4364
4509
|
label: "thoth-agents config",
|
|
4365
|
-
state,
|
|
4510
|
+
...state ? { state } : {},
|
|
4366
4511
|
expected: `seven-agent ${OPENAI_PRESET} roster`
|
|
4367
4512
|
};
|
|
4368
4513
|
}
|
|
@@ -4377,7 +4522,7 @@ function titleCaseSkillName(value) {
|
|
|
4377
4522
|
}).join("-");
|
|
4378
4523
|
}
|
|
4379
4524
|
function homeDirFromContext(context2) {
|
|
4380
|
-
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);
|
|
4381
4526
|
}
|
|
4382
4527
|
function openCodeSkillTargets(context2) {
|
|
4383
4528
|
const homeDir = homeDirFromContext(context2);
|
|
@@ -4395,16 +4540,38 @@ function openCodeSkillTargets(context2) {
|
|
|
4395
4540
|
};
|
|
4396
4541
|
}
|
|
4397
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
|
+
}
|
|
4398
4555
|
const bundledTargets = CUSTOM_SKILLS.map((skill) => {
|
|
4399
4556
|
const path4 = join10(getCustomSkillsDir(), skill.name, "SKILL.md");
|
|
4400
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
|
+
}
|
|
4401
4568
|
return {
|
|
4402
4569
|
kind: "skill",
|
|
4403
4570
|
path: path4,
|
|
4404
4571
|
label: titleCaseSkillName(skill.name),
|
|
4405
|
-
state
|
|
4572
|
+
state,
|
|
4406
4573
|
expected: "bundled thoth-agents OpenCode skill",
|
|
4407
|
-
observed
|
|
4574
|
+
observed
|
|
4408
4575
|
};
|
|
4409
4576
|
});
|
|
4410
4577
|
const diagnostics = [];
|
|
@@ -4422,6 +4589,20 @@ function openCodeSkillTargets(context2) {
|
|
|
4422
4589
|
code: "opencode-bundled-skills-missing"
|
|
4423
4590
|
});
|
|
4424
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
|
+
}
|
|
4425
4606
|
return {
|
|
4426
4607
|
targets: [...recommendedTargets, ...bundledTargets],
|
|
4427
4608
|
diagnostics
|
|
@@ -4443,8 +4624,11 @@ function liteConfigMarker(config) {
|
|
|
4443
4624
|
const preset = typeof config?.preset === "string" ? config.preset : void 0;
|
|
4444
4625
|
const presets = config?.presets && typeof config.presets === "object" ? config.presets : {};
|
|
4445
4626
|
const openaiPreset = presets[OPENAI_PRESET] && typeof presets[OPENAI_PRESET] === "object" ? presets[OPENAI_PRESET] : {};
|
|
4446
|
-
const
|
|
4447
|
-
|
|
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(", ")}`;
|
|
4448
4632
|
}
|
|
4449
4633
|
function hasSevenAgentPreset(config) {
|
|
4450
4634
|
if (!config || config.preset !== OPENAI_PRESET) return false;
|
|
@@ -4458,8 +4642,122 @@ function hasSevenAgentPreset(config) {
|
|
|
4458
4642
|
return value !== null && typeof value === "object" && typeof value.model === "string";
|
|
4459
4643
|
});
|
|
4460
4644
|
}
|
|
4461
|
-
function
|
|
4462
|
-
|
|
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;
|
|
4463
4761
|
}
|
|
4464
4762
|
function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
|
|
4465
4763
|
const mainPath = getExistingConfigPath();
|
|
@@ -4515,6 +4813,18 @@ function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
|
|
|
4515
4813
|
};
|
|
4516
4814
|
if (!mainExists || !hasExpectedPlugin(main.config)) {
|
|
4517
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
|
+
}
|
|
4518
4828
|
return {
|
|
4519
4829
|
harness: "opencode",
|
|
4520
4830
|
displayName: "OpenCode",
|
|
@@ -4522,7 +4832,10 @@ function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
|
|
|
4522
4832
|
summary: "OpenCode config has a managed thoth-agents plugin entry that is not latest.",
|
|
4523
4833
|
targets: [
|
|
4524
4834
|
{ ...mainTarget, state: "drift" },
|
|
4525
|
-
{
|
|
4835
|
+
{
|
|
4836
|
+
...liteTarget,
|
|
4837
|
+
state: liteState
|
|
4838
|
+
},
|
|
4526
4839
|
...skillStatus.targets
|
|
4527
4840
|
],
|
|
4528
4841
|
diagnostics: [
|
|
@@ -4531,6 +4844,132 @@ function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
|
|
|
4531
4844
|
message: "Managed plugin drift requires an explicit repair before applying sync plans.",
|
|
4532
4845
|
code: "opencode-plugin-drift"
|
|
4533
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
|
+
},
|
|
4534
4973
|
...skillStatus.diagnostics
|
|
4535
4974
|
],
|
|
4536
4975
|
actions: openCodeActions
|
|
@@ -4546,7 +4985,17 @@ function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
|
|
|
4546
4985
|
{ ...liteTarget, state: liteExists ? "installed" : "missing" },
|
|
4547
4986
|
...skillStatus.targets
|
|
4548
4987
|
],
|
|
4549
|
-
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
|
+
],
|
|
4550
4999
|
actions: openCodeActions
|
|
4551
5000
|
};
|
|
4552
5001
|
}
|
|
@@ -4561,23 +5010,32 @@ function getOpenCodeStatus(context2 = { cwd: process.cwd() }) {
|
|
|
4561
5010
|
{ ...liteTarget, state: "missing" },
|
|
4562
5011
|
...skillStatus.targets
|
|
4563
5012
|
],
|
|
4564
|
-
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
|
+
],
|
|
4565
5022
|
actions: openCodeActions
|
|
4566
5023
|
};
|
|
4567
5024
|
}
|
|
4568
5025
|
if (!hasSevenAgentPreset(lite.config)) {
|
|
5026
|
+
const selectedNamedPreset = hasSelectedNamedPreset(lite.config);
|
|
4569
5027
|
return {
|
|
4570
5028
|
harness: "opencode",
|
|
4571
5029
|
displayName: "OpenCode",
|
|
4572
5030
|
state: "drift",
|
|
4573
|
-
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.",
|
|
4574
5032
|
targets: [
|
|
4575
5033
|
{ ...mainTarget, state: "installed" },
|
|
4576
5034
|
{ ...liteTarget, state: "drift" },
|
|
4577
5035
|
...skillStatus.targets
|
|
4578
5036
|
],
|
|
4579
5037
|
diagnostics: [
|
|
4580
|
-
{
|
|
5038
|
+
selectedNamedPreset ? activePresetSelectedDiagnostic() : {
|
|
4581
5039
|
severity: "important",
|
|
4582
5040
|
message: "Roster drift requires repair before applying generated plans.",
|
|
4583
5041
|
code: "opencode-roster-drift"
|
|
@@ -4608,6 +5066,37 @@ function defaultBackup(path4) {
|
|
|
4608
5066
|
destinations: [{ path: `${path4}.bak`, label: "managed backup" }]
|
|
4609
5067
|
};
|
|
4610
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
|
+
}
|
|
4611
5100
|
function defaultDisclaimers() {
|
|
4612
5101
|
return [
|
|
4613
5102
|
{
|
|
@@ -4616,9 +5105,10 @@ function defaultDisclaimers() {
|
|
|
4616
5105
|
}
|
|
4617
5106
|
];
|
|
4618
5107
|
}
|
|
4619
|
-
function planFromItems(id, action, title, summary, items) {
|
|
4620
|
-
const status = getOpenCodeStatus();
|
|
4621
|
-
const
|
|
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;
|
|
4622
5112
|
const warnings = safe ? status.diagnostics : [
|
|
4623
5113
|
...status.diagnostics,
|
|
4624
5114
|
{
|
|
@@ -4628,37 +5118,41 @@ function planFromItems(id, action, title, summary, items) {
|
|
|
4628
5118
|
}
|
|
4629
5119
|
];
|
|
4630
5120
|
return {
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
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
|
+
}
|
|
4657
5151
|
};
|
|
4658
5152
|
}
|
|
4659
5153
|
function buildOpenCodeUpdatePlan(_context = { cwd: process.cwd() }) {
|
|
4660
5154
|
const path4 = getExistingConfigPath();
|
|
4661
|
-
|
|
5155
|
+
const { plan, status } = planFromItems(
|
|
4662
5156
|
"opencode-update-preview",
|
|
4663
5157
|
"update",
|
|
4664
5158
|
"Update OpenCode managed plugin entry",
|
|
@@ -4667,12 +5161,17 @@ function buildOpenCodeUpdatePlan(_context = { cwd: process.cwd() }) {
|
|
|
4667
5161
|
{
|
|
4668
5162
|
title: "Ensure OpenCode plugin points at thoth-agents@latest",
|
|
4669
5163
|
target: targetForMainConfig(),
|
|
4670
|
-
state: getOpenCodeStatus().state,
|
|
5164
|
+
state: getOpenCodeStatus(_context).state,
|
|
4671
5165
|
preview: `plugin: ["${EXPECTED_PLUGIN}"]`,
|
|
4672
5166
|
backup: defaultBackup(path4)
|
|
4673
5167
|
}
|
|
4674
|
-
]
|
|
5168
|
+
],
|
|
5169
|
+
_context
|
|
4675
5170
|
);
|
|
5171
|
+
return issueOpenCodePlan(plan, status, _context, {
|
|
5172
|
+
kind: "fixed",
|
|
5173
|
+
action: "update"
|
|
5174
|
+
});
|
|
4676
5175
|
}
|
|
4677
5176
|
function buildOpenCodeSyncPlan(_context = { cwd: process.cwd() }) {
|
|
4678
5177
|
const generatedConfig = generateLiteConfig({
|
|
@@ -4684,7 +5183,7 @@ function buildOpenCodeSyncPlan(_context = { cwd: process.cwd() }) {
|
|
|
4684
5183
|
reset: false
|
|
4685
5184
|
});
|
|
4686
5185
|
const litePath = getExistingLiteConfigPath();
|
|
4687
|
-
|
|
5186
|
+
const { plan, status } = planFromItems(
|
|
4688
5187
|
"opencode-sync-preview",
|
|
4689
5188
|
"sync",
|
|
4690
5189
|
"Sync OpenCode managed configuration",
|
|
@@ -4693,7 +5192,7 @@ function buildOpenCodeSyncPlan(_context = { cwd: process.cwd() }) {
|
|
|
4693
5192
|
{
|
|
4694
5193
|
title: "Ensure OpenCode plugin points at thoth-agents@latest",
|
|
4695
5194
|
target: targetForMainConfig(),
|
|
4696
|
-
state: getOpenCodeStatus().state,
|
|
5195
|
+
state: getOpenCodeStatus(_context).state,
|
|
4697
5196
|
preview: `plugin: ["${EXPECTED_PLUGIN}"]`,
|
|
4698
5197
|
backup: defaultBackup(getExistingConfigPath())
|
|
4699
5198
|
},
|
|
@@ -4708,9 +5207,15 @@ function buildOpenCodeSyncPlan(_context = { cwd: process.cwd() }) {
|
|
|
4708
5207
|
target: targetForLiteConfig(),
|
|
4709
5208
|
preview: JSON.stringify(generatedConfig, null, 2),
|
|
4710
5209
|
backup: defaultBackup(litePath)
|
|
4711
|
-
}
|
|
4712
|
-
|
|
5210
|
+
},
|
|
5211
|
+
bundledSkillsRefreshItem()
|
|
5212
|
+
],
|
|
5213
|
+
_context
|
|
4713
5214
|
);
|
|
5215
|
+
return issueOpenCodePlan(plan, status, _context, {
|
|
5216
|
+
kind: "fixed",
|
|
5217
|
+
action: "sync"
|
|
5218
|
+
});
|
|
4714
5219
|
}
|
|
4715
5220
|
function tuiInstallConfig() {
|
|
4716
5221
|
return {
|
|
@@ -4732,7 +5237,7 @@ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
|
|
|
4732
5237
|
equivalentCommand: "install --agent=opencode --no-tui --tmux=no --skills=yes"
|
|
4733
5238
|
};
|
|
4734
5239
|
const litePath = getExistingLiteConfigPath();
|
|
4735
|
-
|
|
5240
|
+
const { plan, status } = planFromItems(
|
|
4736
5241
|
"opencode-install-preview",
|
|
4737
5242
|
"install",
|
|
4738
5243
|
"Preview install",
|
|
@@ -4744,7 +5249,7 @@ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
|
|
|
4744
5249
|
kind: "config",
|
|
4745
5250
|
path: getExistingConfigPath(),
|
|
4746
5251
|
label: "OpenCode install options",
|
|
4747
|
-
state: getOpenCodeStatus().state,
|
|
5252
|
+
state: getOpenCodeStatus(_context).state,
|
|
4748
5253
|
expected: "--no-tui --tmux=no --skills=yes"
|
|
4749
5254
|
},
|
|
4750
5255
|
preview: JSON.stringify(installPreview, null, 2)
|
|
@@ -4752,7 +5257,7 @@ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
|
|
|
4752
5257
|
{
|
|
4753
5258
|
title: "Ensure OpenCode plugin points at thoth-agents@latest",
|
|
4754
5259
|
target: targetForMainConfig(),
|
|
4755
|
-
state: getOpenCodeStatus().state,
|
|
5260
|
+
state: getOpenCodeStatus(_context).state,
|
|
4756
5261
|
preview: `plugin: ["${EXPECTED_PLUGIN}"]`,
|
|
4757
5262
|
backup: defaultBackup(getExistingConfigPath())
|
|
4758
5263
|
},
|
|
@@ -4768,6 +5273,7 @@ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
|
|
|
4768
5273
|
preview: JSON.stringify(generatedConfig, null, 2),
|
|
4769
5274
|
backup: defaultBackup(litePath)
|
|
4770
5275
|
},
|
|
5276
|
+
bundledSkillsRefreshItem(),
|
|
4771
5277
|
{
|
|
4772
5278
|
title: "Install recommended external skills",
|
|
4773
5279
|
target: {
|
|
@@ -4777,8 +5283,13 @@ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
|
|
|
4777
5283
|
},
|
|
4778
5284
|
preview: JSON.stringify({ installSkills: true }, null, 2)
|
|
4779
5285
|
}
|
|
4780
|
-
]
|
|
5286
|
+
],
|
|
5287
|
+
_context
|
|
4781
5288
|
);
|
|
5289
|
+
return issueOpenCodePlan(plan, status, _context, {
|
|
5290
|
+
kind: "fixed",
|
|
5291
|
+
action: "install"
|
|
5292
|
+
});
|
|
4782
5293
|
}
|
|
4783
5294
|
function normalizeRoleModel(input) {
|
|
4784
5295
|
if (input.provider && !input.model.includes("/")) {
|
|
@@ -4787,16 +5298,24 @@ function normalizeRoleModel(input) {
|
|
|
4787
5298
|
return input.model;
|
|
4788
5299
|
}
|
|
4789
5300
|
function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
|
|
4790
|
-
const
|
|
5301
|
+
const normalizedRoles = input.roles.map((role) => {
|
|
4791
5302
|
const model = normalizeRoleModel(role);
|
|
4792
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 }) => {
|
|
4793
5312
|
return {
|
|
4794
|
-
title: `Set ${role
|
|
5313
|
+
title: `Set ${role} OpenCode model override`,
|
|
4795
5314
|
target: targetForLiteConfig(),
|
|
4796
5315
|
preview: JSON.stringify({
|
|
4797
|
-
[role
|
|
5316
|
+
[role]: {
|
|
4798
5317
|
model,
|
|
4799
|
-
variant
|
|
5318
|
+
variant
|
|
4800
5319
|
}
|
|
4801
5320
|
}),
|
|
4802
5321
|
backup: defaultBackup(getExistingLiteConfigPath()),
|
|
@@ -4811,12 +5330,13 @@ function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
|
|
|
4811
5330
|
} : {}
|
|
4812
5331
|
};
|
|
4813
5332
|
});
|
|
4814
|
-
const plan = planFromItems(
|
|
5333
|
+
const { plan, status } = planFromItems(
|
|
4815
5334
|
"opencode-model-config-preview",
|
|
4816
5335
|
"model-config",
|
|
4817
5336
|
"Configure OpenCode role model overrides",
|
|
4818
5337
|
"Preview thoth-agents role model overrides in the plugin config agents map.",
|
|
4819
|
-
items
|
|
5338
|
+
items,
|
|
5339
|
+
_context
|
|
4820
5340
|
);
|
|
4821
5341
|
const effortWarnings = items.flatMap((item) => item.warnings ?? []);
|
|
4822
5342
|
if (effortWarnings.length > 0) {
|
|
@@ -4824,7 +5344,7 @@ function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
|
|
|
4824
5344
|
plan.warnings.push(...effortWarnings);
|
|
4825
5345
|
}
|
|
4826
5346
|
if (input.harness !== "opencode") {
|
|
4827
|
-
|
|
5347
|
+
const rejectedPlan = {
|
|
4828
5348
|
...plan,
|
|
4829
5349
|
canApply: false,
|
|
4830
5350
|
warnings: [
|
|
@@ -4836,12 +5356,13 @@ function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
|
|
|
4836
5356
|
}
|
|
4837
5357
|
]
|
|
4838
5358
|
};
|
|
5359
|
+
return rejectedPlan;
|
|
4839
5360
|
}
|
|
4840
5361
|
const unsupportedRoles = input.roles.filter(
|
|
4841
5362
|
(role) => !ROLE_NAMES.includes(role.role)
|
|
4842
5363
|
);
|
|
4843
5364
|
if (input.roles.length === 0 || unsupportedRoles.length > 0) {
|
|
4844
|
-
|
|
5365
|
+
const rejectedPlan = {
|
|
4845
5366
|
...plan,
|
|
4846
5367
|
canApply: false,
|
|
4847
5368
|
warnings: [
|
|
@@ -4853,116 +5374,154 @@ function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
|
|
|
4853
5374
|
}
|
|
4854
5375
|
]
|
|
4855
5376
|
};
|
|
5377
|
+
return rejectedPlan;
|
|
4856
5378
|
}
|
|
4857
|
-
|
|
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
|
+
});
|
|
4858
5394
|
}
|
|
4859
|
-
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
|
+
);
|
|
4860
5406
|
return {
|
|
4861
|
-
harness:
|
|
4862
|
-
action
|
|
5407
|
+
harness: issued?.harness ?? "opencode",
|
|
5408
|
+
action,
|
|
4863
5409
|
applied: false,
|
|
4864
5410
|
summary: message,
|
|
4865
5411
|
changedTargets: [],
|
|
5412
|
+
diagnosticTargets: blockers.targets,
|
|
4866
5413
|
backups: [],
|
|
4867
|
-
warnings: [{ severity, message }],
|
|
5414
|
+
warnings: [...liveStatus.diagnostics, { severity, code, message }],
|
|
4868
5415
|
disclaimers: defaultDisclaimers()
|
|
4869
5416
|
};
|
|
4870
5417
|
}
|
|
4871
|
-
function ensureLatestPluginEntry() {
|
|
4872
|
-
const configPath = getExistingConfigPath();
|
|
4873
|
-
try {
|
|
4874
|
-
ensureOpenCodeConfigDir();
|
|
4875
|
-
const { config: parsedConfig, error } = parseConfig(configPath);
|
|
4876
|
-
if (error) {
|
|
4877
|
-
return {
|
|
4878
|
-
success: false,
|
|
4879
|
-
configPath,
|
|
4880
|
-
error: `Failed to parse config: ${error}`
|
|
4881
|
-
};
|
|
4882
|
-
}
|
|
4883
|
-
const config = parsedConfig ?? {};
|
|
4884
|
-
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
4885
|
-
config.plugin = [
|
|
4886
|
-
...plugins.filter(
|
|
4887
|
-
(plugin) => plugin !== PACKAGE_NAME && !plugin.startsWith(`${PACKAGE_NAME}@`)
|
|
4888
|
-
),
|
|
4889
|
-
EXPECTED_PLUGIN
|
|
4890
|
-
];
|
|
4891
|
-
writeConfig(configPath, config);
|
|
4892
|
-
return { success: true, configPath };
|
|
4893
|
-
} catch (err) {
|
|
4894
|
-
return {
|
|
4895
|
-
success: false,
|
|
4896
|
-
configPath,
|
|
4897
|
-
error: `Failed to update OpenCode plugin config: ${err}`
|
|
4898
|
-
};
|
|
4899
|
-
}
|
|
4900
|
-
}
|
|
4901
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
|
+
}
|
|
4902
5458
|
if (plan.harness !== "opencode") {
|
|
4903
|
-
return rejectPlan3(
|
|
5459
|
+
return rejectPlan3(
|
|
5460
|
+
plan,
|
|
5461
|
+
"Only OpenCode operation plans can be applied.",
|
|
5462
|
+
"critical",
|
|
5463
|
+
"opencode-plan-harness-mismatch",
|
|
5464
|
+
issued.context
|
|
5465
|
+
);
|
|
4904
5466
|
}
|
|
4905
5467
|
if (!plan.canApply) {
|
|
4906
5468
|
return rejectPlan3(
|
|
4907
5469
|
plan,
|
|
4908
|
-
"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
|
|
4909
5474
|
);
|
|
4910
5475
|
}
|
|
4911
5476
|
if (!["install", "update", "sync", "model-config"].includes(plan.action)) {
|
|
4912
5477
|
return rejectPlan3(
|
|
4913
5478
|
plan,
|
|
4914
|
-
`Unsupported OpenCode apply action: ${plan.action}
|
|
5479
|
+
`Unsupported OpenCode apply action: ${plan.action}.`,
|
|
5480
|
+
"critical",
|
|
5481
|
+
"opencode-plan-action-unsupported",
|
|
5482
|
+
issued.context
|
|
4915
5483
|
);
|
|
4916
5484
|
}
|
|
4917
5485
|
if (plan.items.length === 0) {
|
|
4918
|
-
return rejectPlan3(plan, "OpenCode plan has no items to apply.");
|
|
4919
|
-
}
|
|
4920
|
-
if (plan.items.some(
|
|
4921
|
-
(item) => !item.title || !item.target.path && item.target.kind !== "skill" || item.state === "drift" || item.state === "unknown"
|
|
4922
|
-
)) {
|
|
4923
5486
|
return rejectPlan3(
|
|
4924
5487
|
plan,
|
|
4925
|
-
"OpenCode plan
|
|
5488
|
+
"OpenCode plan has no items to apply.",
|
|
5489
|
+
"critical",
|
|
5490
|
+
"opencode-plan-empty",
|
|
5491
|
+
issued.context
|
|
4926
5492
|
);
|
|
4927
5493
|
}
|
|
4928
5494
|
return null;
|
|
4929
5495
|
}
|
|
4930
|
-
function
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
);
|
|
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.";
|
|
4939
5504
|
}
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
parsed2 = item.preview ? JSON.parse(item.preview) : {};
|
|
4943
|
-
} catch {
|
|
4944
|
-
return rejectPlan3(
|
|
4945
|
-
plan,
|
|
4946
|
-
"OpenCode model plan contains malformed preview JSON."
|
|
4947
|
-
);
|
|
5505
|
+
if (seen.has(role.role)) {
|
|
5506
|
+
return `Model payload contains duplicate role ${role.role}.`;
|
|
4948
5507
|
}
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
"OpenCode model plan contains an invalid role or model."
|
|
4956
|
-
);
|
|
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.`;
|
|
4957
5514
|
}
|
|
4958
|
-
roleModels.set(role, { model, variant });
|
|
4959
|
-
}
|
|
4960
|
-
if (roleModels.size === 0) {
|
|
4961
|
-
return rejectPlan3(
|
|
4962
|
-
plan,
|
|
4963
|
-
"OpenCode model plan does not contain any role overrides."
|
|
4964
|
-
);
|
|
4965
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
|
+
);
|
|
4966
5525
|
ensureConfigDir();
|
|
4967
5526
|
const targetPath = getExistingLiteConfigPath();
|
|
4968
5527
|
const parsed = parseConfig(targetPath);
|
|
@@ -5029,68 +5588,148 @@ function applyModelPlan(plan) {
|
|
|
5029
5588
|
disclaimers: defaultDisclaimers()
|
|
5030
5589
|
};
|
|
5031
5590
|
}
|
|
5032
|
-
function
|
|
5591
|
+
function applyRecommendedSkills() {
|
|
5592
|
+
const warnings = [];
|
|
5033
5593
|
for (const skill of RECOMMENDED_SKILLS) {
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
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
|
+
});
|
|
5040
5609
|
}
|
|
5041
5610
|
}
|
|
5042
|
-
|
|
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
|
+
}
|
|
5043
5647
|
if (!bundled.success) {
|
|
5044
|
-
|
|
5648
|
+
const failedNames = bundled.failedSkills.map(({ skill }) => skill.name);
|
|
5649
|
+
const detail = failedNames.length > 0 ? failedNames.join(", ") : "unknown bundled skills";
|
|
5650
|
+
return bundledSkillsFailure(
|
|
5045
5651
|
plan,
|
|
5046
|
-
|
|
5652
|
+
changedTargets,
|
|
5653
|
+
backups,
|
|
5654
|
+
warnings,
|
|
5655
|
+
detail
|
|
5047
5656
|
);
|
|
5048
5657
|
}
|
|
5658
|
+
changedTargets.push({
|
|
5659
|
+
kind: "skill",
|
|
5660
|
+
label: "Bundled thoth-agents OpenCode skills",
|
|
5661
|
+
state: "installed",
|
|
5662
|
+
observed: "managed bundled skills refreshed"
|
|
5663
|
+
});
|
|
5049
5664
|
return null;
|
|
5050
5665
|
}
|
|
5051
5666
|
function applyOpenCodePlan(plan) {
|
|
5052
5667
|
const rejection = validateApplyPlan(plan);
|
|
5053
5668
|
if (rejection) return rejection;
|
|
5054
|
-
const
|
|
5055
|
-
if (!
|
|
5669
|
+
const issued = issuedOpenCodePlans.get(plan);
|
|
5670
|
+
if (!issued) {
|
|
5671
|
+
return rejectPlan3(
|
|
5672
|
+
plan,
|
|
5673
|
+
"OpenCode plan provenance was lost before apply.",
|
|
5674
|
+
"critical",
|
|
5675
|
+
"opencode-plan-unissued"
|
|
5676
|
+
);
|
|
5677
|
+
}
|
|
5678
|
+
const status = getOpenCodeStatus(issued.context);
|
|
5679
|
+
if (!canApplyToManagedHealth(plan.action, status)) {
|
|
5056
5680
|
return rejectPlan3(
|
|
5057
5681
|
plan,
|
|
5058
|
-
`OpenCode state is ${status.state}; refusing to apply plan without a safe status
|
|
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
|
|
5059
5686
|
);
|
|
5060
5687
|
}
|
|
5061
|
-
if (
|
|
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
|
+
}
|
|
5062
5701
|
const changedTargets = [];
|
|
5063
5702
|
const backups = [];
|
|
5064
|
-
const
|
|
5065
|
-
|
|
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) {
|
|
5066
5709
|
return rejectPlan3(
|
|
5067
5710
|
plan,
|
|
5068
|
-
|
|
5711
|
+
mainConfigResult.error ?? "Failed to update OpenCode main config.",
|
|
5712
|
+
"critical",
|
|
5713
|
+
"opencode-main-config-update-failed",
|
|
5714
|
+
issued.context
|
|
5069
5715
|
);
|
|
5070
5716
|
}
|
|
5071
5717
|
changedTargets.push({
|
|
5072
5718
|
...targetForMainConfig("installed"),
|
|
5073
5719
|
observed: `plugin includes ${EXPECTED_PLUGIN}`
|
|
5074
5720
|
});
|
|
5075
|
-
if (existsSync10(`${
|
|
5721
|
+
if (existsSync10(`${mainConfigResult.configPath}.bak`)) {
|
|
5076
5722
|
backups.push({
|
|
5077
|
-
path: `${
|
|
5723
|
+
path: `${mainConfigResult.configPath}.bak`,
|
|
5078
5724
|
label: "OpenCode config backup"
|
|
5079
5725
|
});
|
|
5080
5726
|
}
|
|
5081
|
-
if (
|
|
5082
|
-
const defaultAgentResult = disableDefaultAgents();
|
|
5083
|
-
if (!defaultAgentResult.success) {
|
|
5084
|
-
return rejectPlan3(
|
|
5085
|
-
plan,
|
|
5086
|
-
defaultAgentResult.error ?? "Failed to disable OpenCode default agents."
|
|
5087
|
-
);
|
|
5088
|
-
}
|
|
5727
|
+
if (issued.payload.action === "sync" || issued.payload.action === "install") {
|
|
5089
5728
|
const liteResult = writeLiteConfig(
|
|
5090
5729
|
{
|
|
5091
5730
|
agent: "opencode",
|
|
5092
5731
|
hasTmux: false,
|
|
5093
|
-
installSkills:
|
|
5732
|
+
installSkills: issued.payload.action === "install",
|
|
5094
5733
|
installCustomSkills: true,
|
|
5095
5734
|
dryRun: false,
|
|
5096
5735
|
reset: false
|
|
@@ -5113,15 +5752,23 @@ function applyOpenCodePlan(plan) {
|
|
|
5113
5752
|
label: "thoth-agents config backup"
|
|
5114
5753
|
});
|
|
5115
5754
|
}
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5755
|
+
const skillsRejection = applyBundledSkills(
|
|
5756
|
+
plan,
|
|
5757
|
+
changedTargets,
|
|
5758
|
+
backups,
|
|
5759
|
+
warnings
|
|
5760
|
+
);
|
|
5119
5761
|
if (skillsRejection) return skillsRejection;
|
|
5762
|
+
}
|
|
5763
|
+
if (issued.payload.action === "install") {
|
|
5764
|
+
warnings.push(...applyRecommendedSkills());
|
|
5120
5765
|
changedTargets.push({
|
|
5121
5766
|
kind: "skill",
|
|
5122
|
-
label: "Recommended
|
|
5123
|
-
state:
|
|
5124
|
-
|
|
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"
|
|
5125
5772
|
});
|
|
5126
5773
|
}
|
|
5127
5774
|
return {
|
|
@@ -5131,7 +5778,7 @@ function applyOpenCodePlan(plan) {
|
|
|
5131
5778
|
summary: plan.action === "install" ? "Applied OpenCode install plan." : plan.action === "sync" ? "Applied OpenCode managed configuration sync." : "Applied OpenCode plugin update.",
|
|
5132
5779
|
changedTargets,
|
|
5133
5780
|
backups,
|
|
5134
|
-
warnings
|
|
5781
|
+
warnings,
|
|
5135
5782
|
disclaimers: defaultDisclaimers()
|
|
5136
5783
|
};
|
|
5137
5784
|
}
|
|
@@ -5469,12 +6116,12 @@ function getOpenCodeModelsInvocation(platform = process.platform) {
|
|
|
5469
6116
|
};
|
|
5470
6117
|
if (platform === "win32") {
|
|
5471
6118
|
return {
|
|
5472
|
-
command: "opencode models",
|
|
6119
|
+
command: "opencode models --pure",
|
|
5473
6120
|
args: [],
|
|
5474
6121
|
options: { ...options, shell: true }
|
|
5475
6122
|
};
|
|
5476
6123
|
}
|
|
5477
|
-
return { command: "opencode", args: ["models"], options };
|
|
6124
|
+
return { command: "opencode", args: ["models", "--pure"], options };
|
|
5478
6125
|
}
|
|
5479
6126
|
function getNativeOpenCodeOptions() {
|
|
5480
6127
|
try {
|
|
@@ -5629,13 +6276,15 @@ function readRoleField(config, role, field) {
|
|
|
5629
6276
|
function getOpenCodeModelRoles() {
|
|
5630
6277
|
const parsed = parseConfig(getExistingLiteConfigPath());
|
|
5631
6278
|
const agents = parsed.config?.agents && typeof parsed.config.agents === "object" ? parsed.config.agents : void 0;
|
|
5632
|
-
const presets = parsed.config?.presets && typeof parsed.config.presets === "object" ? parsed.config.presets : {};
|
|
5633
|
-
const
|
|
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;
|
|
5634
6283
|
return ALL_AGENT_NAMES.map((role) => {
|
|
5635
|
-
const variant = readRoleField(agents, role, "variant") ?? readRoleField(
|
|
6284
|
+
const variant = readRoleField(agents, role, "variant") ?? readRoleField(activePreset, role, "variant");
|
|
5636
6285
|
return {
|
|
5637
6286
|
role,
|
|
5638
|
-
model: readRoleField(agents, role, "model") ?? readRoleField(
|
|
6287
|
+
model: readRoleField(agents, role, "model") ?? readRoleField(activePreset, role, "model") ?? DEFAULT_MODELS[role] ?? "openai/gpt-5.4",
|
|
5639
6288
|
effort: variant ? { kind: "effort", value: variant } : { kind: "inherit" }
|
|
5640
6289
|
};
|
|
5641
6290
|
});
|