thoth-agents 0.1.6 → 0.1.8

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.
@@ -0,0 +1,3735 @@
1
+ import {
2
+ ALL_AGENT_NAMES,
3
+ CODEX_PROMPT_DIALECT,
4
+ CONTEXT7_MCP_URL,
5
+ CUSTOM_SKILLS,
6
+ DEFAULT_MODELS,
7
+ DEFAULT_THOTH_COMMAND,
8
+ GREP_APP_MCP_URL,
9
+ appendPromptSections,
10
+ composeAgentPrompt,
11
+ createModelFamilySection,
12
+ createOrchestratorPromptSections,
13
+ createReadOnlySpecialistPromptSections,
14
+ createStepBudgetSection,
15
+ createWriteCapableSpecialistPromptSections,
16
+ disableDefaultAgents,
17
+ ensureConfigDir,
18
+ ensureOpenCodeConfigDir,
19
+ exa,
20
+ findPackageRoot,
21
+ generateLiteConfig,
22
+ getAgentOverride,
23
+ getCustomSkillsDir,
24
+ getExistingConfigPath,
25
+ getExistingLiteConfigPath,
26
+ getSkillRegistry,
27
+ installCustomSkills,
28
+ loadAgentPrompt,
29
+ parseConfig,
30
+ renderPromptSection,
31
+ renderRolePrompt,
32
+ writeConfig,
33
+ writeLiteConfig
34
+ } from "./chunk-OES76C67.js";
35
+
36
+ // src/harness/adapters/codex.ts
37
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
38
+ import { dirname, resolve } from "path";
39
+ import { fileURLToPath } from "url";
40
+
41
+ // src/harness/core/agent-pack.ts
42
+ var AGENT_ROLES = [
43
+ {
44
+ name: "orchestrator",
45
+ mode: "primary-non-mutating",
46
+ dispatch: "root-coordinator",
47
+ canMutateWorkspace: false,
48
+ scope: "coordination, routing, decisions, progress, and root memory",
49
+ responsibility: "Delegate-first coordinator for SDD workflow, specialist dispatch, and root-session memory ownership.",
50
+ toolGovernance: [
51
+ "delegates inspection, writing, debugging, and verification",
52
+ "owns question prompts and root-session memory",
53
+ "does not perform inline workspace implementation"
54
+ ],
55
+ verification: [
56
+ "routes verification to specialists",
57
+ "summarizes evidence from changed files, diagnostics, and tests"
58
+ ]
59
+ },
60
+ {
61
+ name: "explorer",
62
+ mode: "read-only",
63
+ dispatch: "task",
64
+ canMutateWorkspace: false,
65
+ scope: "local repository discovery",
66
+ responsibility: "Find workspace facts fast and return paths, lines, symbols, constraints, edit targets, and conclusions.",
67
+ toolGovernance: [
68
+ "read/search/code-navigation tools only",
69
+ "no durable memory writes",
70
+ "no task delegation or progress ownership"
71
+ ],
72
+ verification: ["reports confidence, anchors, unchecked areas, and gaps"]
73
+ },
74
+ {
75
+ name: "librarian",
76
+ mode: "read-only",
77
+ dispatch: "task",
78
+ canMutateWorkspace: false,
79
+ scope: "external research plus local confirmation when needed",
80
+ responsibility: "Gather authoritative external evidence and distinguish official docs from examples.",
81
+ toolGovernance: [
82
+ "research and read-only local confirmation tools",
83
+ "no workspace mutation",
84
+ "no durable memory writes"
85
+ ],
86
+ verification: ["sources every substantive external claim with a URL"]
87
+ },
88
+ {
89
+ name: "oracle",
90
+ mode: "read-only",
91
+ dispatch: "synchronous-task-only",
92
+ canMutateWorkspace: false,
93
+ scope: "advice, diagnosis, architecture, code review, and plan review",
94
+ responsibility: "Provide strategic technical guidance anchored to evidence and review SDD plans.",
95
+ toolGovernance: [
96
+ "read-only analysis and review",
97
+ "no implementation or artifact-producing SDD phases",
98
+ "no task delegation"
99
+ ],
100
+ verification: ["separates observations, risks, and recommendations"]
101
+ },
102
+ {
103
+ name: "designer",
104
+ mode: "write-capable",
105
+ dispatch: "synchronous-task-only",
106
+ canMutateWorkspace: true,
107
+ scope: "UI/UX decisions, implementation, and visual verification",
108
+ responsibility: "Own user-facing implementation choices and visual QA for UI work.",
109
+ toolGovernance: [
110
+ "may edit focused UI/UX files",
111
+ "owns screenshots and visual QA",
112
+ "does not delegate or own SDD progress"
113
+ ],
114
+ verification: ["includes visual verification status when applicable"]
115
+ },
116
+ {
117
+ name: "quick",
118
+ mode: "write-capable",
119
+ dispatch: "synchronous-task-only",
120
+ canMutateWorkspace: true,
121
+ scope: "fast bounded implementation",
122
+ responsibility: "Implement well-defined narrow or mechanical changes with focused verification.",
123
+ toolGovernance: [
124
+ "may edit bounded targets",
125
+ "does not perform broad rediscovery",
126
+ "does not delegate or own SDD progress"
127
+ ],
128
+ verification: ["runs the smallest sufficient focused check"]
129
+ },
130
+ {
131
+ name: "deep",
132
+ mode: "write-capable",
133
+ dispatch: "synchronous-task-only",
134
+ canMutateWorkspace: true,
135
+ scope: "thorough implementation and verification",
136
+ responsibility: "Handle correctness-critical, multi-file, or edge-case-heavy changes with full local context analysis.",
137
+ toolGovernance: [
138
+ "may edit implementation and tests",
139
+ "validates shared behavior against related code and call sites",
140
+ "does not delegate or own SDD progress"
141
+ ],
142
+ verification: ["does not skip verification and reports edge-case evidence"]
143
+ }
144
+ ];
145
+ var DELEGATE_FIRST_RULES = [
146
+ "The orchestrator coordinates, decides, asks blocking questions, and delegates evidence or action.",
147
+ "Explorer, librarian, and oracle remain read-only specialists.",
148
+ "Designer, quick, and deep are write-capable leaf agents with synchronous task dispatch.",
149
+ "Subagents return findings, diffs, verification, and blockers rather than raw file dumps.",
150
+ "No leaf agent owns SDD progress checkboxes or orchestrator-only memory."
151
+ ];
152
+ var VERIFICATION_PROTOCOL = [
153
+ "Completion reports include changed files and verification evidence.",
154
+ "Behavior changes require the smallest sufficient automated check or an explicitly documented check.",
155
+ "Visual changes require designer-owned visual QA when feasible."
156
+ ];
157
+ var AGENT_PACK_CONTRACT = {
158
+ roles: [...AGENT_ROLES],
159
+ delegateFirstRules: [...DELEGATE_FIRST_RULES],
160
+ verificationProtocol: [...VERIFICATION_PROTOCOL]
161
+ };
162
+ function getAgentPackContract() {
163
+ return {
164
+ roles: AGENT_PACK_CONTRACT.roles.map((role) => ({
165
+ ...role,
166
+ toolGovernance: [...role.toolGovernance],
167
+ verification: [...role.verification]
168
+ })),
169
+ delegateFirstRules: [...AGENT_PACK_CONTRACT.delegateFirstRules],
170
+ verificationProtocol: [...AGENT_PACK_CONTRACT.verificationProtocol]
171
+ };
172
+ }
173
+
174
+ // src/harness/core/memory-governance.ts
175
+ var ROOT_OWNED_TOOLS = [
176
+ "mem_session_start",
177
+ "mem_session_summary",
178
+ "mem_save_prompt"
179
+ ];
180
+ var READ_RECALL_CHAIN = [
181
+ "mem_search",
182
+ "mem_timeline",
183
+ "mem_get_observation"
184
+ ];
185
+ var WRITE_CAPABLE_DELEGATED_TOOLS = [
186
+ "mem_save",
187
+ "mem_search",
188
+ "mem_get_observation",
189
+ "mem_timeline",
190
+ "mem_suggest_topic_key"
191
+ ];
192
+ var ALL_MEMORY_TOOLS = [
193
+ ...ROOT_OWNED_TOOLS,
194
+ ...READ_RECALL_CHAIN,
195
+ "mem_suggest_topic_key",
196
+ "mem_save"
197
+ ];
198
+ function uniqueTools(tools) {
199
+ return [...new Set(tools)];
200
+ }
201
+ function roleAllowedTools(role) {
202
+ if (role.name === "orchestrator") {
203
+ return uniqueTools([...ROOT_OWNED_TOOLS, ...WRITE_CAPABLE_DELEGATED_TOOLS]);
204
+ }
205
+ if (role.mode === "read-only") {
206
+ return [...READ_RECALL_CHAIN];
207
+ }
208
+ return [...WRITE_CAPABLE_DELEGATED_TOOLS];
209
+ }
210
+ function roleRules(role) {
211
+ const sharedSubagentRules = [
212
+ "Every subagent memory call requires the parent session_id and project from dispatch; if either is missing, do not call thoth-mem.",
213
+ "Never call mem_session_start, mem_session_summary, or mem_save_prompt; those tools are root/orchestrator-owned.",
214
+ "Protect the sdd/* topic namespace; SDD artifacts may use deterministic sdd/{change}/{artifact} topic keys only in persistence modes that include thoth-mem."
215
+ ];
216
+ if (role.name === "orchestrator") {
217
+ return [
218
+ "mem_session_start, mem_session_summary, and mem_save_prompt are root/main orchestrator-owned tools and responsibilities.",
219
+ "In harnesses without an orchestrator-named agent, root/main orchestrator-owned means the initial/root agent when the harness does not expose an orchestrator-named agent.",
220
+ "Dispatch parent session_id and project when authorizing subagent memory use.",
221
+ "Protect the sdd/* topic namespace and write SDD memory artifacts only in thoth-mem or hybrid persistence modes."
222
+ ];
223
+ }
224
+ if (role.mode === "read-only") {
225
+ return [
226
+ ...sharedSubagentRules,
227
+ "Read-only agents may only perform bounded, project-scoped recall with mem_search -> mem_timeline -> mem_get_observation when authorized.",
228
+ "Read-only agents must never write durable memory."
229
+ ];
230
+ }
231
+ return [
232
+ ...sharedSubagentRules,
233
+ "Write-capable agents may call mem_save only for delegated durable observations under the parent session/project.",
234
+ "For reads, use only mem_search -> mem_timeline -> mem_get_observation."
235
+ ];
236
+ }
237
+ function getRoleMemoryGovernance(role) {
238
+ const allowedTools = roleAllowedTools(role);
239
+ return {
240
+ role: role.name,
241
+ rootOwnedTools: [...ROOT_OWNED_TOOLS],
242
+ allowedTools,
243
+ forbiddenTools: ALL_MEMORY_TOOLS.filter(
244
+ (tool) => !allowedTools.includes(tool)
245
+ ),
246
+ requiresParentContext: role.name !== "orchestrator",
247
+ mayReadProjectMemory: role.name !== "orchestrator",
248
+ mayWriteDurableObservations: role.mode === "write-capable",
249
+ protectsSddNamespace: true,
250
+ rules: roleRules(role)
251
+ };
252
+ }
253
+ function renderMemoryGovernanceInstructions(role, dialect) {
254
+ const governance = getRoleMemoryGovernance(role);
255
+ const harnessRules = dialect ? [
256
+ `- Harness wording: use ${dialect.renderRoleInvocation("orchestrator")} as the memory owner and \`${dialect.tools.userQuestionTool}\` for blocking memory-context questions.`,
257
+ `- Progress ownership remains with the coordinator; report memory-governance verification for tracking in ${dialect.tools.progressTool}.`
258
+ ] : [];
259
+ return [
260
+ "thoth-mem governance:",
261
+ ...governance.rules.map((rule) => `- ${rule}`),
262
+ ...harnessRules,
263
+ `- Runtime enforcement: ${role.name === "orchestrator" ? "root-owned" : "instruction-level unless the target harness validates per-agent memory controls"}.`
264
+ ].join("\n");
265
+ }
266
+ function memoryGovernanceDiagnostics(input) {
267
+ const diagnostics = [];
268
+ if (input.permissionControls !== "supported") {
269
+ diagnostics.push({
270
+ severity: "warning",
271
+ code: `${input.harness}.permission.memory.enforcement_gap`,
272
+ message: "Runtime controls for root-only memory tools are unavailable; governance is rendered as instruction-level guidance.",
273
+ harness: input.harness,
274
+ capability: "rolePermissions",
275
+ fallback: "instruction-only"
276
+ });
277
+ }
278
+ if (input.parentContextInjection !== "supported") {
279
+ diagnostics.push({
280
+ severity: input.parentContextInjection === "unknown" ? "error" : "warning",
281
+ code: `${input.harness}.context.parent_injection.unvalidated`,
282
+ message: "Parent session_id/project injection is not runtime-enforced; subagents must be instructed not to use memory without explicit dispatch context.",
283
+ harness: input.harness,
284
+ capability: "parentContextInjection",
285
+ fallback: "instruction-only"
286
+ });
287
+ }
288
+ if (input.memoryWriteControls !== "supported") {
289
+ diagnostics.push({
290
+ severity: "warning",
291
+ code: `${input.harness}.permission.memory_write.enforcement_gap`,
292
+ message: "Runtime controls for delegated memory writes are unavailable; write-capable agents receive instruction-level mem_save limits only.",
293
+ harness: input.harness,
294
+ capability: "memoryGovernanceEnforcement",
295
+ fallback: "instruction-only"
296
+ });
297
+ }
298
+ return diagnostics;
299
+ }
300
+
301
+ // src/harness/writers/codex-plugin-package.ts
302
+ import { createHash } from "crypto";
303
+
304
+ // src/harness/adapters/codex-surfaces.ts
305
+ var CODEX_HOOK_EVENTS = [
306
+ "SessionStart",
307
+ "UserPromptSubmit",
308
+ "PreToolUse",
309
+ "PermissionRequest",
310
+ "PostToolUse",
311
+ "Stop"
312
+ ];
313
+ var SUPPORTED_CODEX_HOOK_OUTPUT_FIELDS = ["message"];
314
+ var CODEX_PLUGIN_MANIFEST_FIELDS = [
315
+ "name",
316
+ "version",
317
+ "description",
318
+ "skills",
319
+ "mcpServers",
320
+ "apps",
321
+ "hooks",
322
+ "interface"
323
+ ];
324
+ var CODEX_SURFACES = [
325
+ {
326
+ id: "project-agent-toml",
327
+ target: "agent-definition",
328
+ status: "validated",
329
+ artifactKind: "agent-config",
330
+ path: ".codex/agents/{name}.toml",
331
+ fields: [
332
+ "name",
333
+ "description",
334
+ "developer_instructions",
335
+ "model",
336
+ "model_reasoning_effort",
337
+ "sandbox_mode"
338
+ ],
339
+ diagnosticCode: "codex.surface.agent_definition.validated",
340
+ summary: "Project-scoped custom agents are standalone TOML files.",
341
+ evidence: "OpenAI Codex Subagents docs: ~/.codex/agents/ and .codex/agents/ with required name, description, developer_instructions."
342
+ },
343
+ {
344
+ id: "project-config-toml",
345
+ target: "config",
346
+ status: "validated",
347
+ artifactKind: "harness-config",
348
+ path: ".codex/config.toml",
349
+ fields: [
350
+ "model",
351
+ "model_reasoning_effort",
352
+ "approval_policy",
353
+ "sandbox_mode",
354
+ "features",
355
+ "mcp_servers",
356
+ "skills.config",
357
+ "agents"
358
+ ],
359
+ diagnosticCode: "codex.surface.config.validated",
360
+ summary: "Codex supports project-scoped config TOML after trust.",
361
+ evidence: "OpenAI Codex Configuration Reference: ~/.codex/config.toml and project .codex/config.toml overrides."
362
+ },
363
+ {
364
+ id: "mcp-server-config",
365
+ target: "mcp-config",
366
+ status: "validated",
367
+ artifactKind: "mcp-config",
368
+ path: ".codex/config.toml",
369
+ fields: ["mcp_servers.<id>", "url", "command", "args", "env", "tools"],
370
+ diagnosticCode: "codex.surface.mcp.validated",
371
+ summary: "MCP servers are configured through Codex config TOML.",
372
+ evidence: "OpenAI Codex config docs describe mcp_servers and per-tool approval overrides."
373
+ },
374
+ {
375
+ id: "repo-skills-directory",
376
+ target: "skill-directory",
377
+ status: "validated",
378
+ artifactKind: "skill",
379
+ path: ".agents/skills/{skill}/SKILL.md",
380
+ fields: ["SKILL.md", "scripts/", "references/", "assets/"],
381
+ diagnosticCode: "codex.surface.skills.validated",
382
+ summary: "Repository skills are discovered from .agents/skills.",
383
+ evidence: "OpenAI Codex Skills docs: repo skills are scanned from .agents/skills up to the repository root."
384
+ },
385
+ {
386
+ id: "project-hooks-json",
387
+ target: "hook-config",
388
+ status: "validated",
389
+ artifactKind: "hook-config",
390
+ path: ".codex/hooks.json",
391
+ fields: [
392
+ "SessionStart.command",
393
+ "UserPromptSubmit.command",
394
+ "PreToolUse.command",
395
+ "PermissionRequest.command",
396
+ "PostToolUse.command",
397
+ "Stop.command"
398
+ ],
399
+ diagnosticCode: "codex.surface.hooks_json.validated",
400
+ summary: "Codex supports project-local hooks.json command handlers.",
401
+ evidence: "OpenAI Codex hooks docs describe hooks.json with SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, and Stop command handlers."
402
+ },
403
+ {
404
+ id: "inline-hooks-table",
405
+ target: "hook-config",
406
+ status: "validated",
407
+ artifactKind: "hook-config",
408
+ path: ".codex/config.toml",
409
+ fields: [
410
+ "hooks.SessionStart.command",
411
+ "hooks.UserPromptSubmit.command",
412
+ "hooks.PreToolUse.command",
413
+ "hooks.PermissionRequest.command",
414
+ "hooks.PostToolUse.command",
415
+ "hooks.Stop.command"
416
+ ],
417
+ diagnosticCode: "codex.surface.inline_hooks.validated",
418
+ summary: "Codex supports inline [hooks] configuration in TOML.",
419
+ evidence: "OpenAI Codex configuration docs describe inline [hooks] settings gated by trusted project configuration."
420
+ },
421
+ {
422
+ id: "features-hooks-toggle",
423
+ target: "hook-config",
424
+ status: "validated",
425
+ artifactKind: "hook-config",
426
+ path: ".codex/config.toml",
427
+ fields: ["features.hooks"],
428
+ diagnosticCode: "codex.surface.features_hooks.validated",
429
+ summary: "Codex hook loading is controlled by the features.hooks toggle.",
430
+ evidence: "OpenAI Codex configuration docs require enabling [features].hooks before project hook configuration is active."
431
+ },
432
+ {
433
+ id: "plugin-hooks-bundle",
434
+ target: "hook-config",
435
+ status: "validated",
436
+ artifactKind: "hook-config",
437
+ path: ".codex/plugins/{plugin}/hooks.json",
438
+ fields: ["features.plugin_hooks", "plugin.hooks", "plugin.trust_review"],
439
+ diagnosticCode: "codex.surface.plugin_hooks.validated",
440
+ summary: "Codex plugin hook bundles are documented behind plugin hook feature and trust review gates.",
441
+ evidence: "OpenAI Codex plugin docs describe bundled hook configuration requiring plugin_hooks enablement and trust review."
442
+ },
443
+ {
444
+ id: "plugin-manifest-json",
445
+ target: "plugin-manifest",
446
+ status: "validated",
447
+ artifactKind: "manifest",
448
+ path: ".codex-plugin/plugin.json",
449
+ fields: [...CODEX_PLUGIN_MANIFEST_FIELDS],
450
+ diagnosticCode: "codex.surface.plugin_manifest.validated",
451
+ summary: "Codex plugin packages are described by a plugin-root plugin.json manifest.",
452
+ evidence: "OpenAI Codex plugin docs describe plugin.json with official fields including name, version, description, skills, mcpServers, apps, hooks, and interface."
453
+ },
454
+ {
455
+ id: "plugin-skills-directory",
456
+ target: "skill-directory",
457
+ status: "validated",
458
+ artifactKind: "skill",
459
+ path: ".codex-plugin/skills/{skill}/SKILL.md",
460
+ fields: ["skills", "./skills/"],
461
+ diagnosticCode: "codex.surface.plugin_skills.validated",
462
+ summary: "Codex plugin packages can bundle skills under plugin-root skills/.",
463
+ evidence: "OpenAI Codex plugin docs describe plugin-bundled skills referenced from plugin.json using plugin-root relative paths."
464
+ },
465
+ {
466
+ id: "plugin-hooks-json",
467
+ target: "hook-config",
468
+ status: "validated",
469
+ artifactKind: "hook-config",
470
+ path: ".codex-plugin/hooks/hooks.json",
471
+ fields: ["hooks", "./hooks/hooks.json"],
472
+ diagnosticCode: "codex.surface.plugin_hooks_json.validated",
473
+ summary: "Codex plugin packages can bundle hook configuration under plugin-root hooks/.",
474
+ evidence: "OpenAI Codex plugin docs describe hook bundle assets referenced from plugin.json and gated by plugin_hooks plus trust review."
475
+ },
476
+ {
477
+ id: "plugin-mcp-json",
478
+ target: "mcp-config",
479
+ status: "validated",
480
+ artifactKind: "mcp-config",
481
+ path: ".codex-plugin/.mcp.json",
482
+ fields: ["mcpServers", "./.mcp.json"],
483
+ diagnosticCode: "codex.surface.plugin_mcp_json.validated",
484
+ summary: "Codex plugin packages can bundle MCP server definitions in plugin-root .mcp.json.",
485
+ evidence: "OpenAI Codex plugin docs describe bundled .mcp.json server definitions referenced from plugin.json using plugin-root relative paths."
486
+ },
487
+ {
488
+ id: "inline-hooks",
489
+ target: "hook-config",
490
+ status: "unknown",
491
+ artifactKind: "hook-config",
492
+ fields: ["features.hooks", "hooks"],
493
+ diagnosticCode: "codex.surface.hooks.unvalidated",
494
+ summary: "Lifecycle hook availability is documented, but hook event shape and parity with OpenCode runtime hooks is not validated for this adapter.",
495
+ evidence: "Codex config reference mentions hooks; this implementation has not validated exact event schemas for plugin parity.",
496
+ fallback: "diagnostic-only"
497
+ },
498
+ {
499
+ id: "per-agent-runtime-permissions",
500
+ target: "permission-control",
501
+ status: "unsupported",
502
+ fields: ["per-agent tool allow/deny equivalent to OpenCode permissions"],
503
+ diagnosticCode: "codex.permission.memory.enforcement_gap",
504
+ summary: "Codex sandbox and approval policy can constrain sessions, but OpenCode-style per-agent MCP/tool deny maps are not validated.",
505
+ evidence: "Codex approvals/security docs cover sandbox and approval policies, not exact OpenCode per-agent permission maps.",
506
+ fallback: "instruction-only"
507
+ },
508
+ {
509
+ id: "programmatic-delegation-runtime",
510
+ target: "delegation-runtime",
511
+ status: "unsupported",
512
+ fields: [
513
+ "task API",
514
+ "background task sessions",
515
+ "tmux lifecycle hooks",
516
+ "automatic subagent session close"
517
+ ],
518
+ diagnosticCode: "codex.delegation.runtime.unsupported",
519
+ summary: "Codex subagents are user/instruction-triggered; no OpenCode plugin task API or automatic subagent session close parity is validated.",
520
+ evidence: "Codex subagents docs describe manual spawning and agent threads, not this plugin runtime task API or a validated runtime close hook.",
521
+ fallback: "instruction-only"
522
+ },
523
+ {
524
+ id: "parent-context-injection",
525
+ target: "parent-context-injection",
526
+ status: "unknown",
527
+ fields: ["parent session_id", "project"],
528
+ diagnosticCode: "codex.context.parent_injection.unvalidated",
529
+ summary: "No machine-enforced parent context injection mechanism is validated; prompts must instruct users to include parent session_id/project.",
530
+ evidence: "No validated Codex surface in Phase 1 proves automatic parent context injection into spawned agents.",
531
+ fallback: "instruction-only"
532
+ }
533
+ ];
534
+ function getCodexSurfaceRecords() {
535
+ return CODEX_SURFACES.map((surface) => ({
536
+ ...surface,
537
+ fields: [...surface.fields]
538
+ }));
539
+ }
540
+ function getCodexSurface(id) {
541
+ return getCodexSurfaceRecords().find((surface) => surface.id === id);
542
+ }
543
+ function assertCodexSurfaceCanGenerate(id) {
544
+ const surface = getCodexSurface(id);
545
+ if (!surface) {
546
+ return {
547
+ ok: false,
548
+ diagnostic: {
549
+ severity: "error",
550
+ code: "harness.surface_unvalidated",
551
+ message: `Codex surface "${id}" is not registered and cannot generate artifacts.`,
552
+ harness: "codex",
553
+ surface: id,
554
+ fallback: "diagnostic-only"
555
+ }
556
+ };
557
+ }
558
+ if (surface.status !== "validated" || !surface.artifactKind) {
559
+ return {
560
+ ok: false,
561
+ diagnostic: codexSurfaceDiagnostic(surface)
562
+ };
563
+ }
564
+ return { ok: true, surface };
565
+ }
566
+ function codexSurfaceDiagnostic(surface) {
567
+ const severity = surface.status === "unsupported" ? "warning" : "error";
568
+ return {
569
+ severity,
570
+ code: surface.diagnosticCode,
571
+ message: `${surface.summary} Artifact generation is disabled for this surface.`,
572
+ harness: "codex",
573
+ surface: surface.id,
574
+ fallback: surface.fallback ?? "diagnostic-only"
575
+ };
576
+ }
577
+ function normalizeCodexPath(value) {
578
+ return value.replace(/\\/g, "/");
579
+ }
580
+ function codexPluginPackageDiagnostic(code, message, surface) {
581
+ return {
582
+ severity: "error",
583
+ code,
584
+ message,
585
+ harness: "codex",
586
+ surface,
587
+ fallback: "diagnostic-only"
588
+ };
589
+ }
590
+ function isPathUnderCodexPlugin(pathValue) {
591
+ const normalized = normalizeCodexPath(pathValue);
592
+ const segments = normalized.split("/");
593
+ return !normalized.startsWith("/") && !/^[A-Za-z]:\//.test(normalized) && !segments.includes("..") && (normalized === ".codex-plugin" || normalized === ".codex-plugin/" || normalized.startsWith(".codex-plugin/"));
594
+ }
595
+ function fieldAllowedForPluginSurface(surface, field) {
596
+ if (surface.id === "plugin-manifest-json") {
597
+ return CODEX_PLUGIN_MANIFEST_FIELDS.includes(
598
+ field
599
+ );
600
+ }
601
+ return surface.fields.includes(field);
602
+ }
603
+ function validateCodexPluginPackageSurface(input) {
604
+ const canGenerate = assertCodexSurfaceCanGenerate(input.surfaceId);
605
+ if (!canGenerate.ok) {
606
+ return {
607
+ ok: false,
608
+ diagnostics: [
609
+ {
610
+ ...canGenerate.diagnostic,
611
+ code: "codex.plugin.surface.unvalidated",
612
+ message: `Codex plugin package surface "${input.surfaceId}" is not validated for .codex-plugin artifacts.`
613
+ }
614
+ ]
615
+ };
616
+ }
617
+ const diagnostics = [];
618
+ const surface = canGenerate.surface;
619
+ if (!surface.path?.startsWith(".codex-plugin")) {
620
+ diagnostics.push(
621
+ codexPluginPackageDiagnostic(
622
+ "codex.plugin.surface.unvalidated",
623
+ `Codex surface "${input.surfaceId}" is validated, but not as a .codex-plugin package surface.`,
624
+ input.surfaceId
625
+ )
626
+ );
627
+ }
628
+ if (input.path && !isPathUnderCodexPlugin(input.path)) {
629
+ diagnostics.push(
630
+ codexPluginPackageDiagnostic(
631
+ "codex.plugin.path.unvalidated",
632
+ `Codex plugin package path "${input.path}" must stay under .codex-plugin/.`,
633
+ input.surfaceId
634
+ )
635
+ );
636
+ }
637
+ for (const field of input.fields ?? []) {
638
+ if (!fieldAllowedForPluginSurface(surface, field)) {
639
+ diagnostics.push(
640
+ codexPluginPackageDiagnostic(
641
+ "codex.plugin.field.unvalidated",
642
+ `Codex plugin field "${field}" is not validated for surface "${input.surfaceId}".`,
643
+ input.surfaceId
644
+ )
645
+ );
646
+ }
647
+ }
648
+ if (diagnostics.length > 0) return { ok: false, diagnostics };
649
+ return { ok: true, surface };
650
+ }
651
+ function isCodexHookEvent(event) {
652
+ return CODEX_HOOK_EVENTS.includes(event);
653
+ }
654
+ function codexHookDiagnostic(code, message) {
655
+ return {
656
+ severity: "warning",
657
+ code,
658
+ message,
659
+ harness: "codex",
660
+ surface: "hook-config",
661
+ fallback: "diagnostic-only"
662
+ };
663
+ }
664
+ function inferHookHandlerType(handler) {
665
+ if (!handler) return void 0;
666
+ if (typeof handler.type === "string") return handler.type;
667
+ if ("command" in handler) return "command";
668
+ if ("prompt" in handler) return "prompt";
669
+ if ("agent" in handler) return "agent";
670
+ return void 0;
671
+ }
672
+ function validateCodexHookSurface(input) {
673
+ const diagnostics = [];
674
+ const handlerType = inferHookHandlerType(input.handler);
675
+ if (!isCodexHookEvent(input.event)) {
676
+ diagnostics.push(
677
+ codexHookDiagnostic(
678
+ "codex.hooks.event.unsupported",
679
+ `Codex hook event "${input.event}" is not documented for this adapter.`
680
+ )
681
+ );
682
+ }
683
+ if (handlerType !== "command") {
684
+ diagnostics.push(
685
+ codexHookDiagnostic(
686
+ handlerType === "prompt" ? "codex.hooks.handler.prompt_unsupported" : handlerType === "agent" ? "codex.hooks.handler.agent_unsupported" : "codex.hooks.handler.unsupported",
687
+ `Codex hook handler "${handlerType ?? "unknown"}" is not supported; only command handlers are validated.`
688
+ )
689
+ );
690
+ }
691
+ if (input.handler?.async === true) {
692
+ diagnostics.push(
693
+ codexHookDiagnostic(
694
+ "codex.hooks.async.unsupported",
695
+ "Async Codex hook execution is not validated for this adapter."
696
+ )
697
+ );
698
+ }
699
+ const unsupportedOutputField = input.outputFields?.find(
700
+ (field) => !SUPPORTED_CODEX_HOOK_OUTPUT_FIELDS.includes(field)
701
+ );
702
+ if (unsupportedOutputField) {
703
+ diagnostics.push(
704
+ codexHookDiagnostic(
705
+ "codex.hooks.output_field.unsupported",
706
+ `Codex hook output field "${unsupportedOutputField}" is not supported by the validated hook surface.`
707
+ )
708
+ );
709
+ }
710
+ if (input.interceptsToolExecution) {
711
+ diagnostics.push(
712
+ codexHookDiagnostic(
713
+ "codex.hooks.tool_interception.unsupported",
714
+ "Full tool interception is not supported; Codex hooks are diagnostic/config surfaces, not OpenCode runtime enforcement hooks."
715
+ )
716
+ );
717
+ }
718
+ if (diagnostics.length > 0 || !isCodexHookEvent(input.event)) {
719
+ return { ok: false, diagnostics };
720
+ }
721
+ return { ok: true, event: input.event, handlerType: "command" };
722
+ }
723
+
724
+ // src/harness/writers/codex-plugin-package.ts
725
+ var PLUGIN_MANIFEST_SURFACE_ID = "plugin-manifest-json";
726
+ function normalizePath(value) {
727
+ return value.replace(/\\/g, "/");
728
+ }
729
+ function pluginRootReference(pathValue) {
730
+ const normalized = normalizePath(pathValue);
731
+ const relative3 = normalized.slice(".codex-plugin/".length);
732
+ return `./${relative3}`;
733
+ }
734
+ function sha256(content) {
735
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
736
+ }
737
+ function isRecord(value) {
738
+ return typeof value === "object" && value !== null && !Array.isArray(value);
739
+ }
740
+ function stableValue(value) {
741
+ if (Array.isArray(value)) return value.map(stableValue);
742
+ if (!isRecord(value)) return value;
743
+ return Object.fromEntries(
744
+ Object.keys(value).sort((left, right) => left.localeCompare(right)).map((key) => [key, stableValue(value[key])])
745
+ );
746
+ }
747
+ function stableJson(value) {
748
+ return `${JSON.stringify(value, null, 2)}
749
+ `;
750
+ }
751
+ function orderManifestFields(manifest) {
752
+ const ordered = {};
753
+ for (const field of CODEX_PLUGIN_MANIFEST_FIELDS) {
754
+ if (field in manifest) ordered[field] = manifest[field];
755
+ }
756
+ return ordered;
757
+ }
758
+ function fieldDiagnostic(field) {
759
+ return {
760
+ severity: "error",
761
+ code: "codex.plugin.field.unvalidated",
762
+ message: `Skipping unvalidated Codex plugin manifest field "${field}".`,
763
+ harness: "codex",
764
+ surface: PLUGIN_MANIFEST_SURFACE_ID,
765
+ fallback: "diagnostic-only"
766
+ };
767
+ }
768
+ function artifactKindForSurface(surfaceId) {
769
+ if (surfaceId === "plugin-mcp-json") return "mcp-config";
770
+ return surfaceId === "plugin-hooks-json" ? "hook-config" : "skill";
771
+ }
772
+ function codexHookPackageDiagnostic(code, message) {
773
+ return {
774
+ severity: "warning",
775
+ code,
776
+ message,
777
+ harness: "codex",
778
+ surface: "plugin-hooks-json",
779
+ fallback: "diagnostic-only"
780
+ };
781
+ }
782
+ function normalizeHookDefinition(hook) {
783
+ return {
784
+ command: hook.handler.command,
785
+ ...hook.outputFields?.length ? { output: [...hook.outputFields].sort() } : {}
786
+ };
787
+ }
788
+ function renderHookDefinitions(hookDefinitions) {
789
+ const diagnostics = [];
790
+ const hooksByEvent = /* @__PURE__ */ new Map();
791
+ for (const hook of hookDefinitions) {
792
+ const validation = validateCodexHookSurface(hook);
793
+ if (!validation.ok) {
794
+ diagnostics.push(...validation.diagnostics);
795
+ continue;
796
+ }
797
+ const eventHooks = hooksByEvent.get(validation.event) ?? [];
798
+ eventHooks.push(normalizeHookDefinition(hook));
799
+ hooksByEvent.set(validation.event, eventHooks);
800
+ }
801
+ if (hooksByEvent.size === 0) {
802
+ diagnostics.push(
803
+ codexHookPackageDiagnostic(
804
+ "codex.plugin.hooks.none_packaged",
805
+ "No Codex plugin hooks were packaged because no hook definitions passed existing Codex hook validation."
806
+ )
807
+ );
808
+ return { diagnostics };
809
+ }
810
+ const content = stableJson(
811
+ Object.fromEntries(
812
+ [...hooksByEvent.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([event, hooks]) => [event, hooks.map(stableValue)])
813
+ )
814
+ );
815
+ return { content, diagnostics };
816
+ }
817
+ function parseRawHookContent(content) {
818
+ const diagnostics = [];
819
+ const hookDefinitions = [];
820
+ let parsed;
821
+ try {
822
+ parsed = JSON.parse(content);
823
+ } catch {
824
+ return {
825
+ hookDefinitions,
826
+ diagnostics: [
827
+ codexHookPackageDiagnostic(
828
+ "codex.plugin.hooks.invalid_json",
829
+ "Skipping Codex plugin hooks asset because hooks.json content is not valid JSON."
830
+ )
831
+ ]
832
+ };
833
+ }
834
+ if (!isRecord(parsed)) {
835
+ return {
836
+ hookDefinitions,
837
+ diagnostics: [
838
+ codexHookPackageDiagnostic(
839
+ "codex.plugin.hooks.invalid_shape",
840
+ "Skipping Codex plugin hooks asset because hooks.json must be an object keyed by Codex hook event."
841
+ )
842
+ ]
843
+ };
844
+ }
845
+ for (const [event, definitions] of Object.entries(parsed)) {
846
+ if (!Array.isArray(definitions)) {
847
+ diagnostics.push(
848
+ codexHookPackageDiagnostic(
849
+ "codex.plugin.hooks.invalid_shape",
850
+ `Skipping Codex plugin hook event "${event}" because its value is not an array of command handlers.`
851
+ )
852
+ );
853
+ continue;
854
+ }
855
+ for (const definition of definitions) {
856
+ if (!isRecord(definition)) {
857
+ diagnostics.push(
858
+ codexHookPackageDiagnostic(
859
+ "codex.plugin.hooks.invalid_shape",
860
+ `Skipping Codex plugin hook event "${event}" entry because it is not an object.`
861
+ )
862
+ );
863
+ continue;
864
+ }
865
+ const output = definition.output;
866
+ hookDefinitions.push({
867
+ event,
868
+ handler: {
869
+ type: typeof definition.type === "string" ? definition.type : "command",
870
+ command: definition.command,
871
+ async: definition.async
872
+ },
873
+ outputFields: Array.isArray(output) ? output.filter((field) => typeof field === "string") : void 0,
874
+ interceptsToolExecution: definition.interceptsToolExecution === true
875
+ });
876
+ }
877
+ }
878
+ return { hookDefinitions, diagnostics };
879
+ }
880
+ function resolveHookAssetContent(asset) {
881
+ if (asset.surfaceId !== "plugin-hooks-json") {
882
+ return { content: asset.content, diagnostics: [] };
883
+ }
884
+ const fromInput = asset.hookDefinitions ? { hookDefinitions: [...asset.hookDefinitions], diagnostics: [] } : asset.content !== void 0 ? parseRawHookContent(asset.content) : { hookDefinitions: [], diagnostics: [] };
885
+ const rendered = renderHookDefinitions(fromInput.hookDefinitions);
886
+ return {
887
+ content: rendered.content,
888
+ diagnostics: [...fromInput.diagnostics, ...rendered.diagnostics]
889
+ };
890
+ }
891
+ function renderCodexPluginPackage(input) {
892
+ const artifacts = [];
893
+ const diagnostics = [];
894
+ const manifest = {};
895
+ const provenance = [];
896
+ for (const field of Object.keys(input.manifest)) {
897
+ if (!CODEX_PLUGIN_MANIFEST_FIELDS.includes(
898
+ field
899
+ )) {
900
+ diagnostics.push(fieldDiagnostic(field));
901
+ }
902
+ }
903
+ const manifestValidation = validateCodexPluginPackageSurface({
904
+ surfaceId: PLUGIN_MANIFEST_SURFACE_ID,
905
+ path: ".codex-plugin/plugin.json",
906
+ fields: Object.keys(input.manifest).filter(
907
+ (field) => CODEX_PLUGIN_MANIFEST_FIELDS.includes(
908
+ field
909
+ )
910
+ )
911
+ });
912
+ if (!manifestValidation.ok)
913
+ diagnostics.push(...manifestValidation.diagnostics);
914
+ for (const field of CODEX_PLUGIN_MANIFEST_FIELDS) {
915
+ if (field in input.manifest)
916
+ manifest[field] = stableValue(input.manifest[field]);
917
+ }
918
+ for (const asset of [...input.assets ?? []].sort(
919
+ (left, right) => normalizePath(left.path).localeCompare(normalizePath(right.path))
920
+ )) {
921
+ const assetPath = normalizePath(asset.path);
922
+ const validation = validateCodexPluginPackageSurface({
923
+ surfaceId: asset.surfaceId,
924
+ path: assetPath,
925
+ fields: [asset.manifestField]
926
+ });
927
+ if (!validation.ok) {
928
+ diagnostics.push(...validation.diagnostics);
929
+ continue;
930
+ }
931
+ const hookContent = resolveHookAssetContent(asset);
932
+ diagnostics.push(...hookContent.diagnostics);
933
+ if (asset.surfaceId === "plugin-hooks-json" && !hookContent.content) {
934
+ continue;
935
+ }
936
+ const assetContent = hookContent.content ?? asset.content;
937
+ const reference = pluginRootReference(assetPath);
938
+ manifest[asset.manifestField] = reference;
939
+ if (assetContent !== void 0 && !assetPath.endsWith("/")) {
940
+ artifacts.push({
941
+ harness: "codex",
942
+ kind: artifactKindForSurface(asset.surfaceId),
943
+ path: assetPath,
944
+ description: asset.description ?? `Codex plugin asset for ${asset.manifestField}`,
945
+ content: assetContent
946
+ });
947
+ }
948
+ provenance.push({
949
+ field: asset.manifestField,
950
+ path: assetPath,
951
+ reference,
952
+ ...asset.provenanceName ? { name: asset.provenanceName } : {},
953
+ ...asset.sourcePath ? { sourcePath: normalizePath(asset.sourcePath) } : {},
954
+ ...assetContent !== void 0 ? { sha256: sha256(assetContent) } : {}
955
+ });
956
+ }
957
+ artifacts.push({
958
+ harness: "codex",
959
+ kind: "manifest",
960
+ path: ".codex-plugin/plugin.json",
961
+ description: "Deterministic Codex plugin package manifest.",
962
+ content: stableJson(orderManifestFields(manifest))
963
+ });
964
+ artifacts.push({
965
+ harness: "codex",
966
+ kind: "manifest",
967
+ path: ".codex-plugin/.thoth-agents-plugin-assets.json",
968
+ description: "Deterministic Codex plugin package asset provenance.",
969
+ content: stableJson({
970
+ generatedBy: "thoth-agents",
971
+ assets: provenance.sort(
972
+ (left, right) => left.path.localeCompare(right.path)
973
+ )
974
+ })
975
+ });
976
+ return { artifacts, diagnostics };
977
+ }
978
+
979
+ // src/harness/writers/codex-toml.ts
980
+ function escapeTomlString(value) {
981
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\f/g, "\\f").replace(/\r/g, "\\r");
982
+ }
983
+ function escapeTomlMultilineString(value) {
984
+ return value.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"').replace(/\r\n/g, "\n").replace(/\r/g, "\n");
985
+ }
986
+ function renderScalar(value) {
987
+ if (typeof value === "string") return `"${escapeTomlString(value)}"`;
988
+ if (typeof value === "number" || typeof value === "boolean") {
989
+ return String(value);
990
+ }
991
+ if (Array.isArray(value)) {
992
+ return `[${value.map(renderScalar).join(", ")}]`;
993
+ }
994
+ throw new Error(`Unsupported TOML scalar: ${String(value)}`);
995
+ }
996
+ function renderField(key, value) {
997
+ if (key === "developer_instructions" && typeof value === "string") {
998
+ return [`${key} = """`, escapeTomlMultilineString(value), '"""', ""];
999
+ }
1000
+ return [`${key} = ${renderScalar(value)}`];
1001
+ }
1002
+ function isRecord2(value) {
1003
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1004
+ }
1005
+ function fieldOrder(fields, keys) {
1006
+ const orderedRoots = fields.map((field) => field.split(".")[0]);
1007
+ return [...keys].sort((left, right) => {
1008
+ const leftIndex = orderedRoots.indexOf(left);
1009
+ const rightIndex = orderedRoots.indexOf(right);
1010
+ if (leftIndex !== -1 || rightIndex !== -1) {
1011
+ return (leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex) - (rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex);
1012
+ }
1013
+ return left.localeCompare(right);
1014
+ });
1015
+ }
1016
+ function isAllowedRoot(fields, key) {
1017
+ return fields.some(
1018
+ (field) => field === key || field.startsWith(`${key}.`) || field.includes("<id>") && key === field.split(".")[0]
1019
+ );
1020
+ }
1021
+ function renderTable(lines, tablePath, value) {
1022
+ lines.push(`[${tablePath.join(".")}]`);
1023
+ const nested = [];
1024
+ for (const key of Object.keys(value).sort()) {
1025
+ const entry = value[key];
1026
+ if (isRecord2(entry)) {
1027
+ nested.push([key, entry]);
1028
+ } else {
1029
+ lines.push(...renderField(key, entry));
1030
+ }
1031
+ }
1032
+ lines.push("");
1033
+ for (const [key, entry] of nested) {
1034
+ renderTable(lines, [...tablePath, key], entry);
1035
+ }
1036
+ }
1037
+ function renderCodexToml(input) {
1038
+ const canGenerate = assertCodexSurfaceCanGenerate(input.surfaceId);
1039
+ if (!canGenerate.ok) {
1040
+ return { content: "", diagnostics: [canGenerate.diagnostic] };
1041
+ }
1042
+ const surface = getCodexSurface(input.surfaceId);
1043
+ const fields = surface?.fields ?? [];
1044
+ const diagnostics = [];
1045
+ const lines = [];
1046
+ const tables = [];
1047
+ for (const key of fieldOrder(fields, Object.keys(input.values))) {
1048
+ const value = input.values[key];
1049
+ if (!isAllowedRoot(fields, key)) {
1050
+ diagnostics.push({
1051
+ severity: "warning",
1052
+ code: "codex.toml.field.unvalidated",
1053
+ message: `Skipping unvalidated Codex TOML field "${key}" for surface "${input.surfaceId}".`,
1054
+ harness: "codex",
1055
+ surface: input.surfaceId,
1056
+ fallback: "diagnostic-only"
1057
+ });
1058
+ continue;
1059
+ }
1060
+ if (isRecord2(value)) {
1061
+ tables.push([key, value]);
1062
+ } else {
1063
+ lines.push(...renderField(key, value));
1064
+ }
1065
+ }
1066
+ if (tables.length > 0 && lines.length > 0) lines.push("");
1067
+ for (const [key, value] of tables) {
1068
+ renderTable(lines, [key], value);
1069
+ }
1070
+ while (lines.at(-1) === "") lines.pop();
1071
+ return {
1072
+ content: lines.join("\n") + (lines.length > 0 ? "\n" : ""),
1073
+ diagnostics
1074
+ };
1075
+ }
1076
+
1077
+ // src/harness/writers/skill-layout.ts
1078
+ import { createHash as createHash2 } from "crypto";
1079
+ import * as fs from "fs";
1080
+ import * as path from "path";
1081
+ var OUTPUT_MODE_CONFIG = {
1082
+ "plugin-package": {
1083
+ basePath: ".codex-plugin/skills",
1084
+ manifestPath: ".codex-plugin/skills/.thoth-agents-manifest.json",
1085
+ surfaceId: "plugin-skills-directory",
1086
+ label: "plugin-bundled"
1087
+ },
1088
+ "repo-local-fallback": {
1089
+ basePath: ".agents/skills",
1090
+ manifestPath: ".agents/skills/.thoth-agents-manifest.json",
1091
+ surfaceId: "repo-skills-directory",
1092
+ label: "fallback .agents/skills"
1093
+ }
1094
+ };
1095
+ function normalizePath2(value) {
1096
+ return value.split(path.sep).join("/");
1097
+ }
1098
+ function collectFiles(directory) {
1099
+ if (!fs.existsSync(directory)) return [];
1100
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
1101
+ const files = [];
1102
+ for (const entry of entries) {
1103
+ const entryPath = path.join(directory, entry.name);
1104
+ if (entry.isDirectory()) {
1105
+ files.push(...collectFiles(entryPath));
1106
+ } else if (entry.isFile()) {
1107
+ files.push(entryPath);
1108
+ }
1109
+ }
1110
+ return files.sort((left, right) => left.localeCompare(right));
1111
+ }
1112
+ function sha2562(content) {
1113
+ return `sha256:${createHash2("sha256").update(content).digest("hex")}`;
1114
+ }
1115
+ function resolveOutputModes(input) {
1116
+ const requested = input.outputModes ?? (input.outputMode ? [input.outputMode] : []);
1117
+ const modes = requested.length > 0 ? requested : ["plugin-package"];
1118
+ return [...new Set(modes)];
1119
+ }
1120
+ function duplicateScopeDiagnostic(skillNames, surfaceId) {
1121
+ if (skillNames.length === 0) return void 0;
1122
+ return {
1123
+ severity: "warning",
1124
+ code: "codex.skill.duplicate_scope_precedence_unverified",
1125
+ message: `Codex skills are selected for both plugin-bundled and fallback .agents/skills scopes: ${skillNames.join(", ")}. Plugin-bundled skills are the intended primary package content, but runtime precedence with fallback scopes is unresolved and must be validated before relying on ordering.`,
1126
+ harness: "codex",
1127
+ surface: surfaceId,
1128
+ fallback: "diagnostic-only"
1129
+ };
1130
+ }
1131
+ function renderCodexSkillLayout(input) {
1132
+ const surface = assertCodexSurfaceCanGenerate(input.surfaceId);
1133
+ if (!surface.ok) {
1134
+ return { artifacts: [], diagnostics: [surface.diagnostic] };
1135
+ }
1136
+ const artifacts = [];
1137
+ const diagnostics = [];
1138
+ const outputModes = resolveOutputModes(input);
1139
+ for (const mode of outputModes) {
1140
+ const modeSurface = assertCodexSurfaceCanGenerate(
1141
+ OUTPUT_MODE_CONFIG[mode].surfaceId
1142
+ );
1143
+ if (!modeSurface.ok) diagnostics.push(modeSurface.diagnostic);
1144
+ }
1145
+ const duplicateDiagnostic = duplicateScopeDiagnostic(
1146
+ outputModes.length > 1 ? input.skills.map((skill) => skill.name).sort() : [],
1147
+ input.surfaceId
1148
+ );
1149
+ if (duplicateDiagnostic) diagnostics.push(duplicateDiagnostic);
1150
+ const manifests = new Map(
1151
+ outputModes.map((mode) => [mode, []])
1152
+ );
1153
+ for (const skill of [...input.skills].sort(
1154
+ (left, right) => left.name.localeCompare(right.name)
1155
+ )) {
1156
+ const sourceBaseRoot = input.packageRoot ?? input.projectRoot;
1157
+ const sourceRoot = path.join(sourceBaseRoot, skill.sourcePath);
1158
+ const files = collectFiles(sourceRoot);
1159
+ if (files.length === 0) {
1160
+ diagnostics.push({
1161
+ severity: "warning",
1162
+ code: "codex.skill.source_missing",
1163
+ message: `Skipping Codex skill "${skill.name}" because source path "${skill.sourcePath}" was not found.`,
1164
+ harness: "codex",
1165
+ surface: input.surfaceId,
1166
+ fallback: "diagnostic-only"
1167
+ });
1168
+ continue;
1169
+ }
1170
+ for (const file of files) {
1171
+ const relative3 = normalizePath2(path.relative(sourceRoot, file));
1172
+ const content = fs.readFileSync(file, "utf8");
1173
+ const sourcePath = normalizePath2(path.relative(sourceBaseRoot, file));
1174
+ for (const mode of outputModes) {
1175
+ const config = OUTPUT_MODE_CONFIG[mode];
1176
+ const outputPath = `${config.basePath}/${skill.name}/${relative3}`;
1177
+ artifacts.push({
1178
+ harness: "codex",
1179
+ kind: "skill",
1180
+ path: outputPath,
1181
+ description: `Codex ${config.label} skill artifact for ${skill.name}`,
1182
+ content
1183
+ });
1184
+ manifests.get(mode)?.push({
1185
+ name: skill.name,
1186
+ sourcePath,
1187
+ outputPath,
1188
+ sha256: sha2562(content)
1189
+ });
1190
+ }
1191
+ }
1192
+ }
1193
+ for (const mode of outputModes) {
1194
+ const config = OUTPUT_MODE_CONFIG[mode];
1195
+ artifacts.push({
1196
+ harness: "codex",
1197
+ kind: "manifest",
1198
+ path: config.manifestPath,
1199
+ description: `Generated Codex ${config.label} skill source manifest with source hashes.`,
1200
+ content: `${JSON.stringify({ generatedBy: "thoth-agents", skills: manifests.get(mode) ?? [] }, null, 2)}
1201
+ `
1202
+ });
1203
+ }
1204
+ return { artifacts, diagnostics };
1205
+ }
1206
+
1207
+ // src/harness/adapters/codex.ts
1208
+ function readRootPackageVersion(context) {
1209
+ const packageJsonPath = findRootPackageJsonPath([
1210
+ ...hasCodexPackageRoot(context) ? [context.packageRoot] : [],
1211
+ context.projectRoot,
1212
+ process.cwd(),
1213
+ fileURLToPath(new URL(".", import.meta.url))
1214
+ ]);
1215
+ return readPackageJsonVersion(packageJsonPath);
1216
+ }
1217
+ function createCodexPluginPackageManifest(context) {
1218
+ return {
1219
+ name: "thoth-agents",
1220
+ version: readRootPackageVersion(context),
1221
+ description: "Delegate-first OpenCode plugin with seven agents, thoth-mem persistence, and bundled SDD skills."
1222
+ };
1223
+ }
1224
+ function findRootPackageJsonPath(startDirs) {
1225
+ for (const startDir of startDirs) {
1226
+ let currentDir = resolve(startDir);
1227
+ while (true) {
1228
+ const packageJsonPath = resolve(currentDir, "package.json");
1229
+ if (existsSync2(packageJsonPath)) {
1230
+ const packageJsonText = readFileSync2(packageJsonPath, "utf8");
1231
+ const packageJson = JSON.parse(packageJsonText);
1232
+ if (packageJson.name === "thoth-agents") {
1233
+ return packageJsonPath;
1234
+ }
1235
+ }
1236
+ const parentDir = dirname(currentDir);
1237
+ if (parentDir === currentDir) {
1238
+ break;
1239
+ }
1240
+ currentDir = parentDir;
1241
+ }
1242
+ }
1243
+ throw new Error(
1244
+ "Unable to locate the thoth-agents root package.json from the render context or current working directory."
1245
+ );
1246
+ }
1247
+ function readPackageJsonVersion(packageJsonPath) {
1248
+ const packageJsonText = readFileSync2(packageJsonPath, "utf8");
1249
+ const packageJson = JSON.parse(packageJsonText);
1250
+ if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
1251
+ throw new Error("Root package.json version must be a non-empty string.");
1252
+ }
1253
+ return packageJson.version;
1254
+ }
1255
+ function stableJson2(value) {
1256
+ return `${JSON.stringify(value, null, 2)}
1257
+ `;
1258
+ }
1259
+ function codexCommandConfig(commandParts) {
1260
+ const [command = "", ...args] = commandParts;
1261
+ return {
1262
+ command,
1263
+ ...args.length > 0 ? { args } : {}
1264
+ };
1265
+ }
1266
+ function codexMcpConfig(config) {
1267
+ if ("url" in config) {
1268
+ return { url: config.url };
1269
+ }
1270
+ const [command = "", ...args] = config.command;
1271
+ return {
1272
+ command,
1273
+ ...args.length > 0 ? { args } : {},
1274
+ ...config.environment && Object.keys(config.environment).length > 0 ? { env: config.environment } : {}
1275
+ };
1276
+ }
1277
+ function createCodexBuiltinMcpServers() {
1278
+ return {
1279
+ exa: codexMcpConfig(exa),
1280
+ context7: { url: CONTEXT7_MCP_URL },
1281
+ grep_app: { url: GREP_APP_MCP_URL },
1282
+ thoth_mem: codexCommandConfig(DEFAULT_THOTH_COMMAND)
1283
+ };
1284
+ }
1285
+ function createCodexBuiltinMcpJsonConfig() {
1286
+ return {
1287
+ mcpServers: createCodexBuiltinMcpServers()
1288
+ };
1289
+ }
1290
+ function createCodexBuiltinMcpTomlConfig() {
1291
+ return {
1292
+ mcp_servers: createCodexBuiltinMcpServers()
1293
+ };
1294
+ }
1295
+ var CODEX_SUBAGENT_DEFAULT_MODELS = {
1296
+ oracle: "gpt-5.5",
1297
+ librarian: "gpt-5.4-mini",
1298
+ explorer: "gpt-5.4-mini",
1299
+ designer: "gpt-5.4-mini",
1300
+ quick: "gpt-5.4-mini",
1301
+ deep: "gpt-5.5"
1302
+ };
1303
+ var CODEX_SUBAGENT_REASONING_EFFORTS = {
1304
+ oracle: "high",
1305
+ explorer: "low",
1306
+ librarian: "medium",
1307
+ designer: "medium",
1308
+ quick: "low",
1309
+ deep: "medium"
1310
+ };
1311
+ var CODEX_ROOT_START = "<!-- thoth-agents:codex-root:start -->";
1312
+ var CODEX_ROOT_END = "<!-- thoth-agents:codex-root:end -->";
1313
+ var CODEX_CAPABILITIES = CODEX_PROMPT_DIALECT.capabilities.capabilities;
1314
+ function codexPromptSections(roleName) {
1315
+ switch (roleName) {
1316
+ case "orchestrator":
1317
+ return createOrchestratorPromptSections();
1318
+ case "explorer":
1319
+ case "librarian":
1320
+ case "oracle":
1321
+ return createReadOnlySpecialistPromptSections(roleName);
1322
+ case "designer":
1323
+ case "quick":
1324
+ case "deep":
1325
+ return createWriteCapableSpecialistPromptSections(roleName);
1326
+ }
1327
+ }
1328
+ function codexModelFamilyPromptSection(roleName, model) {
1329
+ const section = createModelFamilySection(roleName, model);
1330
+ return section ? renderPromptSection(section, CODEX_PROMPT_DIALECT) : void 0;
1331
+ }
1332
+ function codexStepBudgetPromptSection(steps) {
1333
+ const section = createStepBudgetSection(steps);
1334
+ return section ? renderPromptSection(section, CODEX_PROMPT_DIALECT) : void 0;
1335
+ }
1336
+ function renderCodexRolePrompt(roleName, config, model) {
1337
+ const promptOverrides = loadAgentPrompt(roleName, config?.preset);
1338
+ const override = getAgentOverride(config, roleName);
1339
+ const basePrompt = renderRolePrompt(
1340
+ codexPromptSections(roleName),
1341
+ CODEX_PROMPT_DIALECT
1342
+ );
1343
+ const prompt = composeAgentPrompt({
1344
+ basePrompt,
1345
+ customPrompt: promptOverrides.prompt,
1346
+ customAppendPrompt: appendPromptSections(
1347
+ codexModelFamilyPromptSection(roleName, model),
1348
+ promptOverrides.appendPrompt
1349
+ )
1350
+ });
1351
+ return appendPromptSections(
1352
+ prompt,
1353
+ codexStepBudgetPromptSection(override?.steps)
1354
+ );
1355
+ }
1356
+ function codexRoleInstructions(role) {
1357
+ return [
1358
+ "<role-operational-contract>",
1359
+ `- Role: ${role.name}`,
1360
+ `- Mode: ${role.mode}`,
1361
+ `- Scope: ${role.scope}`,
1362
+ `- Responsibility: ${role.responsibility}`,
1363
+ "- Use request_user_input for local blocking decisions.",
1364
+ "- Permissions, memory governance, runtime hooks, and provider-per-agent controls are instruction-level unless the active Codex runtime documents stronger enforcement.",
1365
+ `- ${role.name} runs as a Codex custom-agent TOML entry; the orchestrator remains the ambient Codex root session, not a generated role TOML.`,
1366
+ ...role.toolGovernance.map((rule) => `- ${rule}`),
1367
+ ...role.verification.map((rule) => `- ${rule}`),
1368
+ "</role-operational-contract>"
1369
+ ].join("\n");
1370
+ }
1371
+ function roleInstructions(role, config) {
1372
+ const model = getCodexAgentModel(role, config) ?? DEFAULT_MODELS[role.name];
1373
+ return [
1374
+ renderCodexRolePrompt(role.name, config, model),
1375
+ codexRoleInstructions(role),
1376
+ renderMemoryGovernanceInstructions(role, CODEX_PROMPT_DIALECT)
1377
+ ].join("\n\n");
1378
+ }
1379
+ function renderCodexRootInstructions(config) {
1380
+ const rootOverride = getAgentOverride(config, "orchestrator");
1381
+ const rootPrompt = renderCodexRolePrompt(
1382
+ "orchestrator",
1383
+ config,
1384
+ rootOverride?.model ?? DEFAULT_MODELS.orchestrator
1385
+ );
1386
+ return [
1387
+ CODEX_ROOT_START,
1388
+ rootPrompt,
1389
+ "<codex-runtime>",
1390
+ "- The ambient Codex root session is the root/main orchestrator; orchestrator-only and root-owned instructions apply to it because Codex does not generate a selectable orchestrator agent TOML.",
1391
+ "- On each new root session, when thoth-mem tools are installed and session/project identity is known, call mem_session_start with the active project and session identity, then save the real user prompt with mem_save_prompt before later delegation.",
1392
+ "- If thoth-mem tools or identity values are unavailable, disclose that memory bootstrap could not run and continue without claiming memory was saved.",
1393
+ "- Use the ambient Codex root session as the delegate-first root coordinator; do not generate or select an orchestrator TOML.",
1394
+ "- Delegate by invoking the installed Codex role agents: explorer, librarian, oracle, designer, quick, and deep.",
1395
+ "- 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.",
1396
+ "- Use packaged thoth-agents plugin capabilities through Codex plugin, skill, MCP, and hook review surfaces after enabling them with /plugins and /hooks.",
1397
+ "- 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.",
1398
+ "- Memory governance, role permissions, provider-per-agent controls, and hooks are instruction-level unless the active Codex runtime documents stronger enforcement.",
1399
+ "</codex-runtime>",
1400
+ CODEX_ROOT_END,
1401
+ ""
1402
+ ].join("\n");
1403
+ }
1404
+ function agentModelReasoningEffort(role) {
1405
+ return isCodexSubagentName(role.name) ? CODEX_SUBAGENT_REASONING_EFFORTS[role.name] : "medium";
1406
+ }
1407
+ function isCodexSubagentName(name) {
1408
+ return name in CODEX_SUBAGENT_DEFAULT_MODELS;
1409
+ }
1410
+ function getPrimaryModelId(model) {
1411
+ if (Array.isArray(model)) {
1412
+ const first = model[0];
1413
+ return typeof first === "string" ? first : first?.id;
1414
+ }
1415
+ return model;
1416
+ }
1417
+ function getCodexAgentModel(role, config) {
1418
+ if (!isCodexSubagentName(role.name)) return void 0;
1419
+ return getPrimaryModelId(config?.agents?.[role.name]?.model) ?? CODEX_SUBAGENT_DEFAULT_MODELS[role.name];
1420
+ }
1421
+ function codexSurfaceHasField(surfaceId, field) {
1422
+ return getCodexSurface(surfaceId)?.fields.includes(field) ?? false;
1423
+ }
1424
+ function hasCodexConfig(context) {
1425
+ return "config" in context;
1426
+ }
1427
+ function hasCodexPackageRoot(context) {
1428
+ return "packageRoot" in context && typeof context.packageRoot === "string" && context.packageRoot.length > 0;
1429
+ }
1430
+ function renderAgentArtifacts({ config }) {
1431
+ const artifacts = [];
1432
+ const diagnostics = [];
1433
+ const supportsReasoningEffort = codexSurfaceHasField(
1434
+ "project-agent-toml",
1435
+ "model_reasoning_effort"
1436
+ );
1437
+ for (const role of getAgentPackContract().roles.filter(
1438
+ (candidate) => candidate.name !== "orchestrator"
1439
+ )) {
1440
+ const model = getCodexAgentModel(role, config);
1441
+ const toml = renderCodexToml({
1442
+ surfaceId: "project-agent-toml",
1443
+ values: {
1444
+ name: role.name,
1445
+ description: role.responsibility,
1446
+ developer_instructions: roleInstructions(role, config),
1447
+ ...model ? { model } : {},
1448
+ ...supportsReasoningEffort ? { model_reasoning_effort: agentModelReasoningEffort(role) } : {},
1449
+ sandbox_mode: role.canMutateWorkspace ? "workspace-write" : "read-only"
1450
+ }
1451
+ });
1452
+ diagnostics.push(...toml.diagnostics);
1453
+ artifacts.push({
1454
+ harness: "codex",
1455
+ kind: "agent-config",
1456
+ path: `.codex/agents/thoth-agents-${role.name}.toml`,
1457
+ description: `Codex agent definition for ${role.name}.`,
1458
+ content: toml.content
1459
+ });
1460
+ }
1461
+ return { artifacts, diagnostics };
1462
+ }
1463
+ function renderConfigArtifacts() {
1464
+ const config = renderCodexToml({
1465
+ surfaceId: "project-config-toml",
1466
+ values: {
1467
+ approval_policy: "on-request",
1468
+ sandbox_mode: "workspace-write",
1469
+ "skills.config": { enabled: true, sources: ["repo"] },
1470
+ agents: getAgentPackContract().roles.filter((role) => role.name !== "orchestrator").map((role) => role.name)
1471
+ }
1472
+ });
1473
+ const mcp = renderCodexToml({
1474
+ surfaceId: "mcp-server-config",
1475
+ values: createCodexBuiltinMcpTomlConfig()
1476
+ });
1477
+ return {
1478
+ artifacts: [
1479
+ {
1480
+ harness: "codex",
1481
+ kind: "harness-config",
1482
+ path: ".codex/config.toml",
1483
+ description: "Codex project configuration snippet for the agent pack.",
1484
+ content: config.content
1485
+ },
1486
+ {
1487
+ harness: "codex",
1488
+ kind: "mcp-config",
1489
+ path: ".codex/config.toml",
1490
+ description: "Codex MCP configuration snippet for thoth-mem.",
1491
+ content: mcp.content
1492
+ }
1493
+ ],
1494
+ diagnostics: [...config.diagnostics, ...mcp.diagnostics]
1495
+ };
1496
+ }
1497
+ function capabilityDiagnostics() {
1498
+ const surfaceDiagnostics = getCodexSurfaceRecords().filter((surface) => surface.status !== "validated").map(codexSurfaceDiagnostic);
1499
+ const governanceDiagnostics = memoryGovernanceDiagnostics({
1500
+ harness: "codex",
1501
+ permissionControls: CODEX_CAPABILITIES.rolePermissions,
1502
+ parentContextInjection: CODEX_CAPABILITIES.parentContextInjection,
1503
+ memoryWriteControls: CODEX_CAPABILITIES.memoryGovernanceEnforcement
1504
+ });
1505
+ return [...surfaceDiagnostics, ...governanceDiagnostics];
1506
+ }
1507
+ function hookReadinessDiagnostics() {
1508
+ return [
1509
+ {
1510
+ severity: "warning",
1511
+ code: "codex.hooks.project_trust.required",
1512
+ message: "Codex project-local hooks require trusted project configuration before .codex/hooks.json or inline [hooks] command handlers are active; generated artifacts remain diagnostic-only until trust and features.hooks are reviewed.",
1513
+ harness: "codex",
1514
+ surface: "project-hooks-json",
1515
+ fallback: "diagnostic-only"
1516
+ },
1517
+ {
1518
+ severity: "warning",
1519
+ code: "codex.hooks.features_hooks.required",
1520
+ message: "Codex hook loading is gated by features.hooks; this adapter reports the docs-backed config surface but does not generate hook scripts or claim runtime enforcement.",
1521
+ harness: "codex",
1522
+ surface: "features-hooks-toggle",
1523
+ fallback: "diagnostic-only"
1524
+ },
1525
+ {
1526
+ severity: "warning",
1527
+ code: "codex.hooks.plugin_trust.required",
1528
+ message: "Bundled Codex plugin hook configuration is package content only; activation requires features.plugin_hooks and plugin hook trust review, and this adapter does not enable hooks automatically or claim hard permission enforcement.",
1529
+ harness: "codex",
1530
+ surface: "plugin-hooks-bundle",
1531
+ fallback: "diagnostic-only"
1532
+ }
1533
+ ];
1534
+ }
1535
+ function resolveSkillOutputModes(context) {
1536
+ return context.options?.codexSkillOutputModes ?? ["plugin-package"];
1537
+ }
1538
+ var codexAdapter = {
1539
+ id: "codex",
1540
+ displayName: "Codex",
1541
+ capabilities: CODEX_CAPABILITIES,
1542
+ render(context) {
1543
+ const config = hasCodexConfig(context) ? context.config : void 0;
1544
+ const agentArtifacts = renderAgentArtifacts({ config });
1545
+ const configArtifacts = renderConfigArtifacts();
1546
+ const skillOutputModes = resolveSkillOutputModes(context);
1547
+ const skillLayout = renderCodexSkillLayout({
1548
+ projectRoot: context.projectRoot,
1549
+ ...hasCodexPackageRoot(context) ? { packageRoot: context.packageRoot } : {},
1550
+ skills: getSkillRegistry(),
1551
+ surfaceId: "plugin-skills-directory",
1552
+ outputModes: skillOutputModes
1553
+ });
1554
+ const pluginPackage = renderCodexPluginPackage({
1555
+ manifest: createCodexPluginPackageManifest(context),
1556
+ assets: [
1557
+ {
1558
+ surfaceId: "plugin-skills-directory",
1559
+ manifestField: "skills",
1560
+ path: ".codex-plugin/skills/",
1561
+ description: "Codex plugin-bundled skill directory."
1562
+ },
1563
+ {
1564
+ surfaceId: "plugin-mcp-json",
1565
+ manifestField: "mcpServers",
1566
+ path: ".codex-plugin/.mcp.json",
1567
+ description: "Codex plugin-bundled MCP server definitions.",
1568
+ content: stableJson2(createCodexBuiltinMcpJsonConfig())
1569
+ },
1570
+ {
1571
+ surfaceId: "plugin-hooks-json",
1572
+ manifestField: "hooks",
1573
+ path: ".codex-plugin/hooks/hooks.json",
1574
+ description: "Codex plugin-bundled hook configuration.",
1575
+ hookDefinitions: []
1576
+ }
1577
+ ]
1578
+ });
1579
+ return {
1580
+ harness: "codex",
1581
+ artifacts: [
1582
+ ...agentArtifacts.artifacts,
1583
+ ...configArtifacts.artifacts,
1584
+ ...pluginPackage.artifacts,
1585
+ ...skillLayout.artifacts
1586
+ ],
1587
+ diagnostics: [
1588
+ ...agentArtifacts.diagnostics,
1589
+ ...configArtifacts.diagnostics,
1590
+ ...pluginPackage.diagnostics,
1591
+ ...skillLayout.diagnostics,
1592
+ ...hookReadinessDiagnostics(),
1593
+ ...capabilityDiagnostics()
1594
+ ]
1595
+ };
1596
+ }
1597
+ };
1598
+
1599
+ // src/cli/codex-paths.ts
1600
+ import { homedir } from "os";
1601
+ import { join as join2 } from "path";
1602
+ var CODEX_ROLE_NAMES = [
1603
+ "explorer",
1604
+ "librarian",
1605
+ "oracle",
1606
+ "designer",
1607
+ "quick",
1608
+ "deep"
1609
+ ];
1610
+ function getCodexHome(options = {}) {
1611
+ const explicit = options.codexHome ?? process.env.CODEX_HOME?.trim();
1612
+ return explicit || join2(options.homeDir ?? homedir(), ".codex");
1613
+ }
1614
+ function resolveCodexTargets(options) {
1615
+ const codexHome = getCodexHome(options);
1616
+ const agentsDir = options.scope === "project" ? join2(options.projectRoot, ".codex", "agents") : join2(codexHome, "agents");
1617
+ return {
1618
+ scope: options.scope,
1619
+ codexHome,
1620
+ configPath: join2(codexHome, "config.toml"),
1621
+ rootInstructionsPath: join2(codexHome, "AGENTS.md"),
1622
+ roleAgentPaths: CODEX_ROLE_NAMES.map((role) => ({
1623
+ role,
1624
+ path: join2(agentsDir, `thoth-agents-${role}.toml`)
1625
+ })),
1626
+ managedModelsPath: join2(
1627
+ codexHome,
1628
+ "agents",
1629
+ ".thoth-agents-managed-models.json"
1630
+ ),
1631
+ skillsDir: options.scope === "project" ? join2(options.projectRoot, ".agents", "skills") : join2(options.homeDir ?? homedir(), ".agents", "skills"),
1632
+ packageRoot: join2(options.projectRoot, ".codex-plugin"),
1633
+ personalPluginRoot: join2(codexHome, "plugins", "thoth-agents"),
1634
+ personalMarketplacePath: join2(
1635
+ options.homeDir ?? homedir(),
1636
+ ".agents",
1637
+ "plugins",
1638
+ "marketplace.json"
1639
+ )
1640
+ };
1641
+ }
1642
+
1643
+ // src/cli/codex-install.ts
1644
+ import {
1645
+ copyFileSync as copyFileSync2,
1646
+ existsSync as existsSync4,
1647
+ mkdirSync as mkdirSync2,
1648
+ readFileSync as readFileSync4,
1649
+ rmSync,
1650
+ writeFileSync as writeFileSync2
1651
+ } from "fs";
1652
+ import { basename, dirname as dirname3, isAbsolute, join as join4, relative as relative2 } from "path";
1653
+ import { fileURLToPath as fileURLToPath2 } from "url";
1654
+
1655
+ // src/harness/codex-plugin-paths.ts
1656
+ import { join as join3 } from "path";
1657
+ var CODEX_PLUGIN_ARTIFACT_PREFIX = ".codex-plugin/";
1658
+ function codexPluginRootArtifactPath(artifactPath) {
1659
+ if (!artifactPath.startsWith(CODEX_PLUGIN_ARTIFACT_PREFIX)) {
1660
+ throw new Error(`Expected Codex plugin artifact path: ${artifactPath}`);
1661
+ }
1662
+ const relativePath = artifactPath.slice(CODEX_PLUGIN_ARTIFACT_PREFIX.length);
1663
+ if (relativePath === ".mcp.json") return relativePath;
1664
+ if (relativePath === "plugin.json" || relativePath.startsWith(".")) {
1665
+ return join3(".codex-plugin", relativePath);
1666
+ }
1667
+ return relativePath;
1668
+ }
1669
+
1670
+ // src/cli/codex-config-io.ts
1671
+ import {
1672
+ copyFileSync,
1673
+ existsSync as existsSync3,
1674
+ mkdirSync,
1675
+ readFileSync as readFileSync3,
1676
+ renameSync,
1677
+ writeFileSync
1678
+ } from "fs";
1679
+ import { dirname as dirname2 } from "path";
1680
+ function splitTomlLines(content) {
1681
+ return (content.match(/[^\r\n]*(?:\r\n|\n|\r)|[^\r\n]+$/g) ?? []).map(
1682
+ (rawLine) => {
1683
+ const eol = /(\r\n|\n|\r)$/.exec(rawLine)?.[1] ?? "";
1684
+ return {
1685
+ text: eol ? rawLine.slice(0, -eol.length) : rawLine,
1686
+ eol
1687
+ };
1688
+ }
1689
+ );
1690
+ }
1691
+ function tomlLineBreak(content) {
1692
+ return /(\r\n|\n|\r)/.exec(content)?.[1] ?? "\n";
1693
+ }
1694
+ function uncommentedTomlLine(line) {
1695
+ return line.split("#", 1)[0].trim();
1696
+ }
1697
+ function isTomlTableHeader(line) {
1698
+ const trimmed = uncommentedTomlLine(line);
1699
+ return trimmed.startsWith("[[") && trimmed.endsWith("]]") || trimmed.startsWith("[") && trimmed.endsWith("]");
1700
+ }
1701
+ function isFeaturesHeader(line) {
1702
+ return uncommentedTomlLine(line) === "[features]";
1703
+ }
1704
+ function renderTomlLines(lines) {
1705
+ return lines.map((line) => `${line.text}${line.eol}`).join("");
1706
+ }
1707
+ function ensureLineHasEol(lines, index, eol) {
1708
+ if (index >= 0 && lines[index]?.eol === "") lines[index].eol = eol;
1709
+ }
1710
+ function appendFeaturesSection(content) {
1711
+ const eol = tomlLineBreak(content);
1712
+ if (!content) {
1713
+ return `[features]${eol}default_mode_request_user_input = true${eol}`;
1714
+ }
1715
+ const separator = content.endsWith("\n") || content.endsWith("\r") ? content.endsWith(`${eol}${eol}`) ? "" : eol : `${eol}${eol}`;
1716
+ return `${content}${separator}[features]${eol}default_mode_request_user_input = true${eol}`;
1717
+ }
1718
+ function patchCodexDefaultModeUserInputFeature(content) {
1719
+ const lines = splitTomlLines(content);
1720
+ const eol = tomlLineBreak(content);
1721
+ const featuresStart = lines.findIndex((line) => isFeaturesHeader(line.text));
1722
+ if (featuresStart === -1) return appendFeaturesSection(content);
1723
+ let featuresEnd = lines.length;
1724
+ for (let index = featuresStart + 1; index < lines.length; index++) {
1725
+ if (isTomlTableHeader(lines[index].text)) {
1726
+ featuresEnd = index;
1727
+ break;
1728
+ }
1729
+ }
1730
+ const flagPattern = /^(\s*default_mode_request_user_input\s*=\s*)(true|false)(\b.*)$/;
1731
+ for (let index = featuresStart + 1; index < featuresEnd; index++) {
1732
+ const match = flagPattern.exec(lines[index].text);
1733
+ if (!match) continue;
1734
+ if (match[2] === "true") return content;
1735
+ lines[index].text = `${match[1]}true${match[3]}`;
1736
+ return renderTomlLines(lines);
1737
+ }
1738
+ let insertAt = featuresEnd;
1739
+ while (insertAt > featuresStart + 1 && lines[insertAt - 1].text.trim() === "")
1740
+ insertAt--;
1741
+ ensureLineHasEol(lines, insertAt - 1, eol);
1742
+ lines.splice(insertAt, 0, {
1743
+ text: "default_mode_request_user_input = true",
1744
+ eol
1745
+ });
1746
+ return renderTomlLines(lines);
1747
+ }
1748
+ function buildCodexManagedConfigPatch(content, options) {
1749
+ const diffSummary = [
1750
+ "ensure features.default_mode_request_user_input = true"
1751
+ ];
1752
+ if (options.pluginId) {
1753
+ diffSummary.push(
1754
+ `plugin enablement for "${options.pluginId}" is not textually merged; use /plugins to enable it`
1755
+ );
1756
+ } else {
1757
+ diffSummary.push(
1758
+ "plugin enablement left to /plugins; no guessed plugin id written"
1759
+ );
1760
+ }
1761
+ return {
1762
+ content: patchCodexDefaultModeUserInputFeature(content),
1763
+ diffSummary,
1764
+ warnings: []
1765
+ };
1766
+ }
1767
+ function writeCodexConfigMerge(options) {
1768
+ try {
1769
+ const before = existsSync3(options.configPath) ? readFileSync3(options.configPath, "utf8") : "";
1770
+ const merged = buildCodexManagedConfigPatch(before, {
1771
+ pluginId: options.pluginId
1772
+ });
1773
+ const changed = before !== merged.content;
1774
+ if (options.dryRun || !changed) {
1775
+ return {
1776
+ success: true,
1777
+ configPath: options.configPath,
1778
+ changed,
1779
+ diffSummary: merged.diffSummary,
1780
+ warnings: merged.warnings
1781
+ };
1782
+ }
1783
+ mkdirSync(dirname2(options.configPath), { recursive: true });
1784
+ const backupPath = `${options.configPath}.bak`;
1785
+ if (existsSync3(options.configPath))
1786
+ copyFileSync(options.configPath, backupPath);
1787
+ const tmpPath = `${options.configPath}.tmp`;
1788
+ writeFileSync(tmpPath, merged.content);
1789
+ renameSync(tmpPath, options.configPath);
1790
+ return {
1791
+ success: true,
1792
+ configPath: options.configPath,
1793
+ backupPath: existsSync3(backupPath) ? backupPath : void 0,
1794
+ changed,
1795
+ diffSummary: merged.diffSummary,
1796
+ warnings: merged.warnings
1797
+ };
1798
+ } catch (error) {
1799
+ return {
1800
+ success: false,
1801
+ configPath: options.configPath,
1802
+ changed: false,
1803
+ diffSummary: [],
1804
+ warnings: [],
1805
+ error: error instanceof Error ? error.message : String(error)
1806
+ };
1807
+ }
1808
+ }
1809
+
1810
+ // src/cli/codex-install.ts
1811
+ var ROOT_START = "<!-- thoth-agents:codex-root:start -->";
1812
+ var ROOT_END = "<!-- thoth-agents:codex-root:end -->";
1813
+ var MANAGED_MODEL_STATE_VERSION = 1;
1814
+ function mergeManagedBlock(existing, managedBlock) {
1815
+ const start = existing.indexOf(ROOT_START);
1816
+ const end = existing.indexOf(ROOT_END);
1817
+ if (start !== -1 && end !== -1 && end > start) {
1818
+ return `${existing.slice(0, start)}${managedBlock}${existing.slice(end + ROOT_END.length).replace(/^\s*\n/, "")}`;
1819
+ }
1820
+ return `${existing}${existing.endsWith("\n") || existing.length === 0 ? "" : "\n"}
1821
+ ${managedBlock}`;
1822
+ }
1823
+ function writeTextWithBackup(path2, content) {
1824
+ mkdirSync2(dirname3(path2), { recursive: true });
1825
+ if (existsSync4(path2) && readFileSync4(path2, "utf8") === content) return false;
1826
+ if (existsSync4(path2)) copyFileSync2(path2, `${path2}.bak`);
1827
+ writeFileSync2(path2, content);
1828
+ return true;
1829
+ }
1830
+ function packageArtifactTarget(packageRoot, artifact) {
1831
+ return join4(packageRoot, codexPluginRootArtifactPath(artifact.path));
1832
+ }
1833
+ function resolvePackageRoot(packageRoot) {
1834
+ if (packageRoot) return packageRoot;
1835
+ return findPackageRoot(fileURLToPath2(new URL(".", import.meta.url))) ?? void 0;
1836
+ }
1837
+ function normalizeRelativeMarketplacePath(path2) {
1838
+ const normalized = path2.replaceAll("\\", "/");
1839
+ if (isAbsolute(path2) || /^[A-Za-z]:\//.test(normalized)) return normalized;
1840
+ if (normalized.startsWith("./")) return normalized;
1841
+ return `./${normalized}`;
1842
+ }
1843
+ function marketplaceSourcePath(homeDir, personalPluginRoot) {
1844
+ return normalizeRelativeMarketplacePath(
1845
+ relative2(homeDir, personalPluginRoot)
1846
+ );
1847
+ }
1848
+ function managedMarketplaceEntry(homeDir, personalPluginRoot) {
1849
+ return {
1850
+ name: "thoth-agents",
1851
+ source: {
1852
+ source: "local",
1853
+ path: marketplaceSourcePath(homeDir, personalPluginRoot)
1854
+ },
1855
+ policy: {
1856
+ installation: "AVAILABLE",
1857
+ authentication: "ON_INSTALL"
1858
+ },
1859
+ category: "Productivity"
1860
+ };
1861
+ }
1862
+ function stableJson3(value) {
1863
+ return `${JSON.stringify(value, null, 2)}
1864
+ `;
1865
+ }
1866
+ function emptyManagedModelState() {
1867
+ return {
1868
+ version: MANAGED_MODEL_STATE_VERSION,
1869
+ models: {}
1870
+ };
1871
+ }
1872
+ function stringRecord(value) {
1873
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
1874
+ return Object.fromEntries(
1875
+ Object.entries(value).filter(
1876
+ (entry) => typeof entry[0] === "string" && typeof entry[1] === "string"
1877
+ )
1878
+ );
1879
+ }
1880
+ function readManagedModelState(path2) {
1881
+ if (!existsSync4(path2)) return emptyManagedModelState();
1882
+ try {
1883
+ const parsed = JSON.parse(readFileSync4(path2, "utf8"));
1884
+ if (parsed.version !== MANAGED_MODEL_STATE_VERSION || !parsed.models || typeof parsed.models !== "object" || Array.isArray(parsed.models)) {
1885
+ return emptyManagedModelState();
1886
+ }
1887
+ return {
1888
+ version: MANAGED_MODEL_STATE_VERSION,
1889
+ models: stringRecord(parsed.models),
1890
+ ...Object.keys(stringRecord(parsed.configuredModels)).length > 0 ? { configuredModels: stringRecord(parsed.configuredModels) } : {}
1891
+ };
1892
+ } catch {
1893
+ return emptyManagedModelState();
1894
+ }
1895
+ }
1896
+ function parseRoleTomlModel(content) {
1897
+ const match = /^model\s*=\s*"((?:\\.|[^"\\])*)"\s*$/m.exec(content);
1898
+ if (!match) return void 0;
1899
+ return match[1].replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1900
+ }
1901
+ function escapeTomlString2(value) {
1902
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\f/g, "\\f").replace(/\r/g, "\\r");
1903
+ }
1904
+ function replaceRoleTomlModel(content, model) {
1905
+ const rendered = `model = "${escapeTomlString2(model)}"`;
1906
+ if (/^model\s*=\s*"(?:\\.|[^"\\])*"\s*$/m.test(content)) {
1907
+ return content.replace(/^model\s*=\s*"(?:\\.|[^"\\])*"\s*$/m, rendered);
1908
+ }
1909
+ return `${rendered}
1910
+ ${content}`;
1911
+ }
1912
+ function roleManagedModelStateKey(path2) {
1913
+ return basename(path2);
1914
+ }
1915
+ function applyCodexManagedModelOverrides(config, overrides) {
1916
+ if (config.dryRun) {
1917
+ return {
1918
+ success: true,
1919
+ changed: [],
1920
+ diagnostics: [
1921
+ "Dry-run Codex model override apply requested; no files were written."
1922
+ ]
1923
+ };
1924
+ }
1925
+ const plan = buildCodexSetupPlan({ ...config, dryRun: true, reset: false });
1926
+ const stateItem = plan.items.find(
1927
+ (item) => item.action === "write-managed-model-state"
1928
+ );
1929
+ const statePath = stateItem?.targetPath;
1930
+ if (!statePath) {
1931
+ return {
1932
+ success: false,
1933
+ changed: [],
1934
+ diagnostics: plan.diagnostics,
1935
+ error: "Codex managed model state target was not found."
1936
+ };
1937
+ }
1938
+ const changed = [];
1939
+ const diagnostics = uniqueMessages([
1940
+ ...plan.diagnostics,
1941
+ ...plan.disclaimers
1942
+ ]);
1943
+ const state = readManagedModelState(statePath);
1944
+ const nextState = {
1945
+ version: MANAGED_MODEL_STATE_VERSION,
1946
+ models: { ...state.models },
1947
+ ...state.configuredModels ? { configuredModels: { ...state.configuredModels } } : {}
1948
+ };
1949
+ try {
1950
+ for (const override of overrides) {
1951
+ const roleItem = plan.items.find(
1952
+ (item) => item.action === "write-role-toml" && item.role === override.role
1953
+ );
1954
+ if (!roleItem?.content) {
1955
+ throw new Error(
1956
+ `Missing Codex role TOML content for ${override.role}.`
1957
+ );
1958
+ }
1959
+ const before = existsSync4(roleItem.targetPath) ? readFileSync4(roleItem.targetPath, "utf8") : roleItem.content;
1960
+ const updated = replaceRoleTomlModel(before, override.model);
1961
+ if (writeTextWithBackup(roleItem.targetPath, updated)) {
1962
+ changed.push(roleItem.targetPath);
1963
+ }
1964
+ const key = roleManagedModelStateKey(roleItem.targetPath);
1965
+ nextState.models[key] = parseRoleTomlModel(roleItem.content) ?? override.model;
1966
+ nextState.configuredModels ??= {};
1967
+ nextState.configuredModels[key] = override.model;
1968
+ }
1969
+ if (writeTextWithBackup(statePath, stableJson3(nextState))) {
1970
+ changed.push(statePath);
1971
+ }
1972
+ return {
1973
+ success: true,
1974
+ changed,
1975
+ diagnostics: uniqueMessages(diagnostics)
1976
+ };
1977
+ } catch (error) {
1978
+ return {
1979
+ success: false,
1980
+ changed,
1981
+ diagnostics: uniqueMessages(diagnostics),
1982
+ error: error instanceof Error ? error.message : String(error)
1983
+ };
1984
+ }
1985
+ }
1986
+ function resolveRoleTomlContent(options) {
1987
+ const renderedModel = parseRoleTomlModel(options.renderedContent);
1988
+ const key = roleManagedModelStateKey(options.targetPath);
1989
+ if (!renderedModel) return options.renderedContent;
1990
+ const configuredModel = options.reset ? void 0 : options.state.configuredModels?.[key];
1991
+ if (options.reset || !existsSync4(options.targetPath)) {
1992
+ options.nextState.models[key] = renderedModel;
1993
+ if (configuredModel !== void 0) {
1994
+ options.nextState.configuredModels ??= {};
1995
+ options.nextState.configuredModels[key] = configuredModel;
1996
+ return replaceRoleTomlModel(options.renderedContent, configuredModel);
1997
+ }
1998
+ return options.renderedContent;
1999
+ }
2000
+ const currentModel = parseRoleTomlModel(
2001
+ readFileSync4(options.targetPath, "utf8")
2002
+ );
2003
+ if (configuredModel !== void 0) {
2004
+ options.nextState.models[key] = renderedModel;
2005
+ options.nextState.configuredModels ??= {};
2006
+ options.nextState.configuredModels[key] = configuredModel;
2007
+ return replaceRoleTomlModel(
2008
+ options.renderedContent,
2009
+ currentModel && currentModel !== configuredModel ? currentModel : configuredModel
2010
+ );
2011
+ }
2012
+ const trackedModel = options.state.models[key];
2013
+ const isUserOwned = currentModel !== void 0 && (trackedModel === void 0 ? currentModel !== renderedModel : currentModel !== trackedModel);
2014
+ if (isUserOwned) {
2015
+ if (trackedModel !== void 0)
2016
+ options.nextState.models[key] = trackedModel;
2017
+ return replaceRoleTomlModel(options.renderedContent, currentModel);
2018
+ }
2019
+ options.nextState.models[key] = renderedModel;
2020
+ return options.renderedContent;
2021
+ }
2022
+ function mergePersonalMarketplace(existing, homeDir, personalPluginRoot) {
2023
+ const parsed = existing.trim() ? JSON.parse(existing) : {};
2024
+ const plugins = Array.isArray(parsed.plugins) ? parsed.plugins : [];
2025
+ const managedEntry = managedMarketplaceEntry(homeDir, personalPluginRoot);
2026
+ const nextPlugins = plugins.filter(
2027
+ (entry) => !(entry && typeof entry === "object" && "name" in entry && entry.name === "thoth-agents")
2028
+ ).concat(managedEntry);
2029
+ return stableJson3({
2030
+ ...parsed,
2031
+ name: typeof parsed.name === "string" ? parsed.name : "personal-marketplace",
2032
+ interface: parsed.interface && typeof parsed.interface === "object" ? parsed.interface : { displayName: "Personal Plugin Marketplace" },
2033
+ plugins: nextPlugins
2034
+ });
2035
+ }
2036
+ function roleArtifactContent(role, artifacts) {
2037
+ const path2 = `.codex/agents/thoth-agents-${role}.toml`;
2038
+ const artifact = artifacts.find((candidate) => candidate.path === path2);
2039
+ if (!artifact?.content)
2040
+ throw new Error(`Missing Codex role artifact: ${path2}`);
2041
+ return String(artifact.content);
2042
+ }
2043
+ function buildCodexSetupPlan(config) {
2044
+ const targets = resolveCodexTargets({
2045
+ scope: config.scope,
2046
+ projectRoot: config.projectRoot,
2047
+ homeDir: config.homeDir,
2048
+ codexHome: config.codexHome
2049
+ });
2050
+ const packageRoot = resolvePackageRoot(config.packageRoot);
2051
+ const render = codexAdapter.render({
2052
+ projectRoot: config.projectRoot,
2053
+ ...packageRoot ? { packageRoot } : {}
2054
+ });
2055
+ const packageArtifacts = render.artifacts.filter(
2056
+ (artifact) => artifact.path.startsWith(".codex-plugin/")
2057
+ );
2058
+ const rootBlock = renderCodexRootInstructions();
2059
+ const managedModelState2 = readManagedModelState(targets.managedModelsPath);
2060
+ const nextManagedModelState = emptyManagedModelState();
2061
+ const items = [
2062
+ {
2063
+ kind: "root-instructions",
2064
+ action: "merge-managed-block",
2065
+ targetPath: targets.rootInstructionsPath,
2066
+ description: `Merge managed Codex root instructions into ${targets.rootInstructionsPath}.`,
2067
+ requiresBackup: true,
2068
+ content: rootBlock
2069
+ },
2070
+ ...targets.roleAgentPaths.map(
2071
+ (target) => ({
2072
+ kind: "role-subagent-toml",
2073
+ action: "write-role-toml",
2074
+ targetPath: target.path,
2075
+ description: `Materialize Codex role subagent ${target.role}.`,
2076
+ requiresBackup: existsSync4(target.path),
2077
+ role: target.role,
2078
+ content: resolveRoleTomlContent({
2079
+ renderedContent: roleArtifactContent(target.role, render.artifacts),
2080
+ targetPath: target.path,
2081
+ state: managedModelState2,
2082
+ nextState: nextManagedModelState,
2083
+ reset: config.reset
2084
+ })
2085
+ })
2086
+ ),
2087
+ {
2088
+ kind: "managed-model-state",
2089
+ action: "write-managed-model-state",
2090
+ targetPath: targets.managedModelsPath,
2091
+ description: "Record thoth-agents-managed Codex role model ownership state.",
2092
+ requiresBackup: existsSync4(targets.managedModelsPath),
2093
+ content: stableJson3(nextManagedModelState)
2094
+ },
2095
+ ...packageArtifacts.map(
2096
+ (artifact) => ({
2097
+ kind: "personal-plugin-source",
2098
+ action: "refresh-package",
2099
+ targetPath: packageArtifactTarget(targets.personalPluginRoot, artifact),
2100
+ description: `Refresh Personal Codex plugin source asset ${artifact.path}.`,
2101
+ requiresBackup: false,
2102
+ content: String(artifact.content ?? "")
2103
+ })
2104
+ ),
2105
+ {
2106
+ kind: "personal-marketplace",
2107
+ action: "merge-marketplace",
2108
+ targetPath: targets.personalMarketplacePath,
2109
+ description: "Register Personal Codex marketplace entry for the local thoth-agents plugin source.",
2110
+ requiresBackup: existsSync4(targets.personalMarketplacePath),
2111
+ content: targets.personalPluginRoot
2112
+ },
2113
+ {
2114
+ kind: "user-config",
2115
+ action: "merge-toml",
2116
+ targetPath: targets.configPath,
2117
+ description: "Merge managed Codex feature gates into user config.toml.",
2118
+ requiresBackup: true
2119
+ },
2120
+ {
2121
+ kind: "diagnostic",
2122
+ action: "diagnose-only",
2123
+ targetPath: targets.codexHome,
2124
+ description: "Report /plugins, /hooks, precedence, and capability review steps.",
2125
+ requiresBackup: false
2126
+ }
2127
+ ];
2128
+ return {
2129
+ dryRun: config.dryRun === true,
2130
+ reset: config.reset,
2131
+ items,
2132
+ configPath: targets.configPath,
2133
+ pluginId: config.pluginId,
2134
+ diagnostics: [
2135
+ "Restart Codex, then run /plugins to review and enable the Personal thoth-agents plugin registered through ~/.agents/plugins/marketplace.json.",
2136
+ "Run /hooks to review and trust plugin hooks; features.plugin_hooks does not bypass hook trust review.",
2137
+ "Codex Default mode user-input requests require features.default_mode_request_user_input = true and use the request_user_input tool; other modes may not expose it.",
2138
+ "Higher-precedence Codex config (project, profile, CLI, system, or admin) may override user config feature flags."
2139
+ ],
2140
+ disclaimers: [
2141
+ "Role permissions, provider-per-agent settings, memory governance, and hook enforcement are instruction-level or user-managed unless documented Codex runtime controls are available.",
2142
+ "Codex v1 reset is managed-only; no broad destructive --force behavior is implemented."
2143
+ ]
2144
+ };
2145
+ }
2146
+ function formatCodexSetupPlan(plan) {
2147
+ const refreshPackageGroups = /* @__PURE__ */ new Map();
2148
+ for (const item of plan.items) {
2149
+ if (item.action !== "refresh-package") continue;
2150
+ const group = refreshPackageGroups.get(item.kind) ?? [];
2151
+ group.push(item);
2152
+ refreshPackageGroups.set(item.kind, group);
2153
+ }
2154
+ const renderedRefreshKinds = /* @__PURE__ */ new Set();
2155
+ const lines = [];
2156
+ for (const item of plan.items) {
2157
+ if (item.action !== "refresh-package") {
2158
+ lines.push(`- ${item.action}: ${item.targetPath} (${item.description})`);
2159
+ continue;
2160
+ }
2161
+ if (renderedRefreshKinds.has(item.kind)) continue;
2162
+ renderedRefreshKinds.add(item.kind);
2163
+ lines.push(formatRefreshPackageGroup(item.kind, refreshPackageGroups));
2164
+ }
2165
+ return ["Codex setup plan:", ...lines].join("\n");
2166
+ }
2167
+ function formatRefreshPackageGroup(kind, groups) {
2168
+ const items = groups.get(kind) ?? [];
2169
+ const description = kind === "personal-plugin-source" ? "Refresh Personal Codex plugin source" : "Refresh documented .codex-plugin package";
2170
+ return `- refresh-package: ${commonTargetDirectory(items)} (${description}, ${items.length} files.)`;
2171
+ }
2172
+ function commonTargetDirectory(items) {
2173
+ if (items.length === 0) return "";
2174
+ let common = dirname3(items[0]?.targetPath ?? "");
2175
+ for (const item of items.slice(1)) {
2176
+ while (!isSameOrChildPath(item.targetPath, common)) {
2177
+ const parent = dirname3(common);
2178
+ if (parent === common) return common;
2179
+ common = parent;
2180
+ }
2181
+ }
2182
+ return common;
2183
+ }
2184
+ function isSameOrChildPath(path2, parent) {
2185
+ return path2 === parent || path2.startsWith(`${parent}\\`) || path2.startsWith(`${parent}/`);
2186
+ }
2187
+ function uniqueMessages(messages) {
2188
+ return [...new Set(messages)];
2189
+ }
2190
+ function applyCodexSetup(plan) {
2191
+ const changed = [];
2192
+ const diagnostics = uniqueMessages([
2193
+ ...plan.diagnostics,
2194
+ ...plan.disclaimers
2195
+ ]);
2196
+ if (plan.dryRun) return { success: true, changed, diagnostics };
2197
+ try {
2198
+ for (const targetPath of managedRefreshRoots(plan)) {
2199
+ rmSync(targetPath, { recursive: true, force: true });
2200
+ }
2201
+ for (const item of plan.items) {
2202
+ if (item.action === "diagnose-only") continue;
2203
+ if (item.action === "merge-toml") {
2204
+ const result = writeCodexConfigMerge({
2205
+ configPath: item.targetPath,
2206
+ dryRun: false,
2207
+ pluginId: plan.pluginId
2208
+ });
2209
+ diagnostics.push(...result.diffSummary, ...result.warnings);
2210
+ if (!result.success) throw new Error(result.error);
2211
+ if (result.changed) changed.push(item.targetPath);
2212
+ continue;
2213
+ }
2214
+ if (item.action === "merge-marketplace") {
2215
+ if (item.content === void 0) continue;
2216
+ const content2 = mergePersonalMarketplace(
2217
+ existsSync4(item.targetPath) ? readFileSync4(item.targetPath, "utf8") : "",
2218
+ dirname3(dirname3(dirname3(item.targetPath))),
2219
+ item.content
2220
+ );
2221
+ if (writeTextWithBackup(item.targetPath, content2))
2222
+ changed.push(item.targetPath);
2223
+ continue;
2224
+ }
2225
+ if (item.content === void 0) continue;
2226
+ const content = item.action === "merge-managed-block" ? mergeManagedBlock(
2227
+ existsSync4(item.targetPath) ? readFileSync4(item.targetPath, "utf8") : "",
2228
+ item.content
2229
+ ) : item.content;
2230
+ if (writeTextWithBackup(item.targetPath, content))
2231
+ changed.push(item.targetPath);
2232
+ }
2233
+ return { success: true, changed, diagnostics: uniqueMessages(diagnostics) };
2234
+ } catch (error) {
2235
+ return {
2236
+ success: false,
2237
+ changed,
2238
+ diagnostics: uniqueMessages(diagnostics),
2239
+ error: error instanceof Error ? error.message : String(error)
2240
+ };
2241
+ }
2242
+ }
2243
+ function managedRefreshRoots(plan) {
2244
+ const refreshGroups = /* @__PURE__ */ new Map();
2245
+ for (const item of plan.items) {
2246
+ if (item.action !== "refresh-package") continue;
2247
+ const group = refreshGroups.get(item.kind) ?? [];
2248
+ group.push(item);
2249
+ refreshGroups.set(item.kind, group);
2250
+ }
2251
+ return [...refreshGroups].filter(([kind]) => kind === "personal-plugin-source").map(([, items]) => commonTargetDirectory(items)).filter((path2) => path2.length > 0);
2252
+ }
2253
+
2254
+ // src/cli/operations/codex.ts
2255
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
2256
+ import { basename as basename2 } from "path";
2257
+ var CODEX_DISPLAY_NAME = "Codex";
2258
+ var codexPlanSources = /* @__PURE__ */ new WeakMap();
2259
+ var codexModelSources = /* @__PURE__ */ new WeakMap();
2260
+ var codexActions = [
2261
+ {
2262
+ id: "codex-status",
2263
+ kind: "status",
2264
+ label: "Status",
2265
+ description: "Inspect managed Codex setup state",
2266
+ dryRun: false,
2267
+ requiresConfirmation: false,
2268
+ supported: true
2269
+ },
2270
+ {
2271
+ id: "codex-list",
2272
+ kind: "list",
2273
+ label: "List",
2274
+ description: "List managed Codex surfaces and actions",
2275
+ dryRun: false,
2276
+ requiresConfirmation: false,
2277
+ supported: true
2278
+ },
2279
+ {
2280
+ id: "codex-install",
2281
+ kind: "install",
2282
+ label: "Install",
2283
+ description: "Preview Codex agent-pack setup install",
2284
+ dryRun: true,
2285
+ requiresConfirmation: true,
2286
+ supported: true
2287
+ },
2288
+ {
2289
+ id: "codex-update",
2290
+ kind: "update",
2291
+ label: "Update",
2292
+ description: "Preview Codex managed setup refresh",
2293
+ dryRun: true,
2294
+ requiresConfirmation: true,
2295
+ supported: true
2296
+ },
2297
+ {
2298
+ id: "codex-sync",
2299
+ kind: "sync",
2300
+ label: "Sync",
2301
+ description: "Preview Codex managed configuration sync",
2302
+ dryRun: true,
2303
+ requiresConfirmation: true,
2304
+ supported: true
2305
+ },
2306
+ {
2307
+ id: "codex-model-config",
2308
+ kind: "model-config",
2309
+ label: "Model",
2310
+ description: "Preview supported Codex subagent model line changes",
2311
+ dryRun: true,
2312
+ requiresConfirmation: true,
2313
+ supported: true
2314
+ }
2315
+ ];
2316
+ var codexOperationAdapter = {
2317
+ id: "codex",
2318
+ displayName: CODEX_DISPLAY_NAME,
2319
+ available: true,
2320
+ description: "Codex agent-pack and managed subagent surfaces.",
2321
+ actions: codexActions
2322
+ };
2323
+ function codexConfig(context = { cwd: process.cwd() }, dryRun) {
2324
+ return {
2325
+ dryRun,
2326
+ reset: false,
2327
+ scope: context.scope ?? "user",
2328
+ projectRoot: context.cwd,
2329
+ homeDir: context.homeDir,
2330
+ codexHome: context.codexHome,
2331
+ packageRoot: context.packageRoot,
2332
+ pluginId: context.pluginId
2333
+ };
2334
+ }
2335
+ function codexDisclaimers() {
2336
+ return [
2337
+ {
2338
+ message: "Codex root orchestration remains ambient instructions, not a generated root/orchestrator subagent model surface.",
2339
+ code: "codex-root-guidance-only"
2340
+ },
2341
+ {
2342
+ message: "Provider-per-role behavior, hook trust, permissions, and memory governance are instruction-level or user-managed unless Codex exposes runtime controls.",
2343
+ code: "codex-runtime-limits"
2344
+ }
2345
+ ];
2346
+ }
2347
+ function warning(message, code) {
2348
+ return { severity: "important", message, code };
2349
+ }
2350
+ function targetForItem(item, state, observed) {
2351
+ return {
2352
+ kind: item.kind === "managed-model-state" ? "memory-state" : "generated-artifact",
2353
+ path: item.targetPath,
2354
+ label: item.role ? `Codex ${item.role} subagent` : item.kind.replaceAll("-", " "),
2355
+ state,
2356
+ expected: item.action,
2357
+ observed,
2358
+ description: item.description
2359
+ };
2360
+ }
2361
+ function surfaceForItem(item) {
2362
+ return {
2363
+ id: `${item.kind}:${item.role ?? basename2(item.targetPath)}`,
2364
+ label: item.role ? `Codex ${item.role} subagent TOML` : item.kind.replaceAll("-", " "),
2365
+ path: item.targetPath,
2366
+ description: item.description
2367
+ };
2368
+ }
2369
+ function backupForItem(item) {
2370
+ return {
2371
+ required: item.requiresBackup,
2372
+ strategy: item.requiresBackup ? "managed-backup-file" : "none",
2373
+ destinations: item.requiresBackup ? [{ path: `${item.targetPath}.bak`, label: "managed backup" }] : []
2374
+ };
2375
+ }
2376
+ function rootBlockState(item) {
2377
+ if (!existsSync5(item.targetPath))
2378
+ return { state: "missing", observed: "absent" };
2379
+ const content = readFileSync5(item.targetPath, "utf8");
2380
+ if (item.content && content.includes(item.content)) {
2381
+ return { state: "installed", observed: "managed root block present" };
2382
+ }
2383
+ if (content.includes("thoth-agents:codex-root:start")) {
2384
+ return { state: "drift", observed: "managed root block differs" };
2385
+ }
2386
+ return { state: "missing", observed: "managed root block absent" };
2387
+ }
2388
+ function managedModelState(item) {
2389
+ if (!existsSync5(item.targetPath))
2390
+ return { state: "missing", observed: "absent" };
2391
+ try {
2392
+ const parsed = JSON.parse(readFileSync5(item.targetPath, "utf8"));
2393
+ if (parsed.version !== MANAGED_MODEL_STATE_VERSION || !parsed.models || typeof parsed.models !== "object" || Array.isArray(parsed.models)) {
2394
+ return { state: "unknown", observed: "invalid managed model state" };
2395
+ }
2396
+ return item.content === readFileSync5(item.targetPath, "utf8") ? { state: "installed", observed: "managed model state current" } : { state: "drift", observed: "managed model state differs" };
2397
+ } catch {
2398
+ return { state: "unknown", observed: "unparseable managed model state" };
2399
+ }
2400
+ }
2401
+ function userConfigState(item) {
2402
+ if (!existsSync5(item.targetPath))
2403
+ return { state: "missing", observed: "absent" };
2404
+ const content = readFileSync5(item.targetPath, "utf8");
2405
+ if (/^\s*default_mode_request_user_input\s*=\s*true\b/m.test(content)) {
2406
+ return {
2407
+ state: "installed",
2408
+ observed: "default mode request input enabled"
2409
+ };
2410
+ }
2411
+ if (/^\s*default_mode_request_user_input\s*=\s*false\b/m.test(content)) {
2412
+ return { state: "drift", observed: "default mode request input disabled" };
2413
+ }
2414
+ return { state: "missing", observed: "managed feature flag absent" };
2415
+ }
2416
+ function marketplaceState(item) {
2417
+ if (!existsSync5(item.targetPath))
2418
+ return { state: "missing", observed: "absent" };
2419
+ try {
2420
+ const parsed = JSON.parse(readFileSync5(item.targetPath, "utf8"));
2421
+ const plugins = Array.isArray(parsed.plugins) ? parsed.plugins : [];
2422
+ const entry = plugins.find(
2423
+ (plugin) => plugin && typeof plugin === "object" && "name" in plugin && plugin.name === "thoth-agents"
2424
+ );
2425
+ if (!entry)
2426
+ return { state: "missing", observed: "marketplace entry absent" };
2427
+ const sourcePath = typeof entry === "object" && entry && "source" in entry && entry.source && typeof entry.source === "object" && "path" in entry.source ? String(entry.source.path) : "";
2428
+ return sourcePath.includes(".codex/plugins/thoth-agents") ? { state: "installed", observed: "marketplace entry present" } : { state: "drift", observed: `marketplace source ${sourcePath}` };
2429
+ } catch {
2430
+ return { state: "unknown", observed: "unparseable marketplace JSON" };
2431
+ }
2432
+ }
2433
+ function contentState(item) {
2434
+ if (!existsSync5(item.targetPath))
2435
+ return { state: "missing", observed: "absent" };
2436
+ const observed = readFileSync5(item.targetPath, "utf8");
2437
+ if (basename2(item.targetPath) === "plugin.json" && item.targetPath.replaceAll("\\", "/").includes("/.codex-plugin/")) {
2438
+ try {
2439
+ const expectedVersion = JSON.parse(item.content ?? "{}").version;
2440
+ const observedVersion = JSON.parse(observed).version;
2441
+ if (typeof expectedVersion === "string" && typeof observedVersion === "string" && expectedVersion !== observedVersion) {
2442
+ return {
2443
+ state: "outdated",
2444
+ observed: `version ${observedVersion}; expected ${expectedVersion}`
2445
+ };
2446
+ }
2447
+ } catch {
2448
+ return { state: "unknown", observed: "unparseable plugin manifest" };
2449
+ }
2450
+ }
2451
+ if (item.content === observed)
2452
+ return { state: "installed", observed: "current" };
2453
+ if (item.role) {
2454
+ const currentModel = parseRoleTomlModel(observed);
2455
+ const expectedModel = parseRoleTomlModel(item.content ?? "");
2456
+ if (currentModel && expectedModel && currentModel !== expectedModel) {
2457
+ return {
2458
+ state: "drift",
2459
+ observed: `model ${currentModel}; expected ${expectedModel}`
2460
+ };
2461
+ }
2462
+ }
2463
+ return { state: "drift", observed: "content differs" };
2464
+ }
2465
+ function classifyItem(item) {
2466
+ if (item.action === "diagnose-only") {
2467
+ return { state: "unknown", observed: "diagnostic guidance only" };
2468
+ }
2469
+ if (item.action === "merge-managed-block") return rootBlockState(item);
2470
+ if (item.action === "write-managed-model-state") {
2471
+ return managedModelState(item);
2472
+ }
2473
+ if (item.action === "merge-toml") return userConfigState(item);
2474
+ if (item.action === "merge-marketplace") return marketplaceState(item);
2475
+ return contentState(item);
2476
+ }
2477
+ function aggregateState(states) {
2478
+ if (states.includes("unknown")) return "unknown";
2479
+ if (states.includes("drift")) return "drift";
2480
+ if (states.includes("outdated")) return "outdated";
2481
+ if (states.includes("missing")) return "missing";
2482
+ return "installed";
2483
+ }
2484
+ function statusSummary(state) {
2485
+ switch (state) {
2486
+ case "installed":
2487
+ return "Codex managed setup surfaces are installed and current.";
2488
+ case "missing":
2489
+ return "Codex managed setup surfaces are missing.";
2490
+ case "drift":
2491
+ return "Codex managed setup exists but differs from expected managed output.";
2492
+ case "outdated":
2493
+ return "Codex managed setup includes an older generated package artifact.";
2494
+ case "unknown":
2495
+ return "Codex managed setup could not be classified safely.";
2496
+ }
2497
+ }
2498
+ function getCodexStatus(context = { cwd: process.cwd() }) {
2499
+ let plan;
2500
+ try {
2501
+ plan = buildCodexSetupPlan(codexConfig(context, true));
2502
+ } catch (error) {
2503
+ const message = error instanceof Error ? error.message : String(error);
2504
+ return {
2505
+ harness: "codex",
2506
+ displayName: CODEX_DISPLAY_NAME,
2507
+ state: "unknown",
2508
+ summary: `Codex setup plan could not be built: ${message}`,
2509
+ targets: [],
2510
+ diagnostics: [
2511
+ {
2512
+ severity: "critical",
2513
+ message,
2514
+ code: "codex-plan-build-failed"
2515
+ }
2516
+ ],
2517
+ actions: codexActions,
2518
+ disclaimers: codexDisclaimers()
2519
+ };
2520
+ }
2521
+ const classified = plan.items.filter((item) => item.action !== "diagnose-only").map((item) => ({ item, ...classifyItem(item) }));
2522
+ const state = aggregateState(classified.map((item) => item.state));
2523
+ const diagnostics = plan.diagnostics.map((message) => ({
2524
+ severity: "minor",
2525
+ message,
2526
+ code: "codex-diagnostic"
2527
+ }));
2528
+ return {
2529
+ harness: "codex",
2530
+ displayName: CODEX_DISPLAY_NAME,
2531
+ state,
2532
+ summary: statusSummary(state),
2533
+ targets: classified.map(
2534
+ ({ item, state: state2, observed }) => targetForItem(item, state2, observed)
2535
+ ),
2536
+ diagnostics,
2537
+ actions: codexActions,
2538
+ disclaimers: [
2539
+ ...codexDisclaimers(),
2540
+ ...plan.disclaimers.map((message) => ({ message }))
2541
+ ]
2542
+ };
2543
+ }
2544
+ function planItemFromSetup(item) {
2545
+ return {
2546
+ title: item.description,
2547
+ target: targetForItem(item),
2548
+ preview: item.content,
2549
+ backup: backupForItem(item)
2550
+ };
2551
+ }
2552
+ function planFromSetup(id, action, title, summary, setupPlan, context) {
2553
+ const status = getCodexStatus(context);
2554
+ const canApply = status.state === "installed" || status.state === "missing" || status.state === "outdated";
2555
+ const plan = {
2556
+ id,
2557
+ harness: "codex",
2558
+ action,
2559
+ title,
2560
+ summary,
2561
+ dryRun: true,
2562
+ canApply,
2563
+ targets: status.targets,
2564
+ surfaces: setupPlan.items.filter((item) => item.action !== "diagnose-only").map(surfaceForItem),
2565
+ backup: {
2566
+ required: setupPlan.items.some((item) => item.requiresBackup),
2567
+ strategy: "existing-helper",
2568
+ description: "Codex setup apply uses the existing installer backup behavior for files that already exist."
2569
+ },
2570
+ items: setupPlan.items.map(planItemFromSetup),
2571
+ warnings: [
2572
+ ...status.diagnostics,
2573
+ ...canApply ? [] : [
2574
+ warning(
2575
+ `Codex state is ${status.state}; apply is disabled until the state is safely classified or repaired.`,
2576
+ "codex-unsafe-state"
2577
+ )
2578
+ ]
2579
+ ],
2580
+ disclaimers: [
2581
+ ...codexDisclaimers(),
2582
+ ...setupPlan.disclaimers.map((message) => ({ message }))
2583
+ ]
2584
+ };
2585
+ codexPlanSources.set(plan, setupPlan);
2586
+ return plan;
2587
+ }
2588
+ function buildCodexUpdatePlan(context = { cwd: process.cwd() }) {
2589
+ const setupPlan = buildCodexSetupPlan(codexConfig(context, true));
2590
+ return planFromSetup(
2591
+ "codex-update-preview",
2592
+ "update",
2593
+ "Update Codex managed setup",
2594
+ "Preview Codex managed setup refresh using buildCodexSetupPlan().",
2595
+ setupPlan,
2596
+ context
2597
+ );
2598
+ }
2599
+ function buildCodexSyncPlan(context = { cwd: process.cwd() }) {
2600
+ const setupPlan = buildCodexSetupPlan(codexConfig(context, true));
2601
+ return planFromSetup(
2602
+ "codex-sync-preview",
2603
+ "sync",
2604
+ "Sync Codex managed configuration",
2605
+ "Preview Codex managed root instructions, subagents, plugin source, marketplace entry, and feature gates.",
2606
+ setupPlan,
2607
+ context
2608
+ );
2609
+ }
2610
+ function buildCodexInstallPlan(context = { cwd: process.cwd() }) {
2611
+ const setupPlan = buildCodexSetupPlan(codexConfig(context, true));
2612
+ return planFromSetup(
2613
+ "codex-install-preview",
2614
+ "install",
2615
+ "Install Codex managed setup",
2616
+ "Preview Codex managed agent-pack setup using buildCodexSetupPlan().",
2617
+ setupPlan,
2618
+ context
2619
+ );
2620
+ }
2621
+ function normalizeCodexModel(input) {
2622
+ if (input.provider && !input.model.includes("/")) {
2623
+ return `${input.provider}/${input.model}`;
2624
+ }
2625
+ return input.model;
2626
+ }
2627
+ function isCodexRole(role) {
2628
+ return CODEX_ROLE_NAMES.includes(role);
2629
+ }
2630
+ function buildCodexModelPlan(input, context = { cwd: process.cwd() }) {
2631
+ const status = getCodexStatus(context);
2632
+ const supportedRoles = input.roles.filter((role) => isCodexRole(role.role)).map((role) => ({
2633
+ role: role.role,
2634
+ model: normalizeCodexModel(role)
2635
+ }));
2636
+ const unsupportedRoles = input.roles.filter(
2637
+ (role) => !isCodexRole(role.role)
2638
+ );
2639
+ const warnings = [
2640
+ ...status.diagnostics,
2641
+ ...input.warnings ?? [],
2642
+ ...unsupportedRoles.map(
2643
+ (role) => warning(
2644
+ `Codex does not expose a supported generated model surface for ${role.role}; this plan will not write that role.`,
2645
+ "codex-unsupported-model-role"
2646
+ )
2647
+ )
2648
+ ];
2649
+ if (input.harness !== "codex") {
2650
+ warnings.push(
2651
+ warning(
2652
+ "Model plan target harness must be codex.",
2653
+ "codex-model-harness-mismatch"
2654
+ )
2655
+ );
2656
+ }
2657
+ const targets = supportedRoles.map(({ role }) => {
2658
+ const target = status.targets.find(
2659
+ (candidate) => candidate.path?.endsWith(`thoth-agents-${role}.toml`)
2660
+ );
2661
+ return target ?? {
2662
+ kind: "generated-artifact",
2663
+ label: `Codex ${role} subagent`,
2664
+ state: "missing"
2665
+ };
2666
+ });
2667
+ const config = codexConfig(context, false);
2668
+ const stateTarget = status.targets.find(
2669
+ (target) => target.path?.endsWith(".thoth-agents-managed-models.json")
2670
+ );
2671
+ const plan = {
2672
+ id: "codex-model-config-preview",
2673
+ harness: "codex",
2674
+ action: "model-config",
2675
+ title: "Configure Codex subagent model lines",
2676
+ summary: "Preview model changes for generated Codex subagent TOML files and .thoth-agents-managed-models.json only.",
2677
+ dryRun: true,
2678
+ canApply: input.harness === "codex" && supportedRoles.length > 0 && status.state !== "unknown",
2679
+ targets: [...targets, ...stateTarget ? [stateTarget] : []],
2680
+ surfaces: targets.map((target) => ({
2681
+ id: `codex-model:${target.label}`,
2682
+ label: target.label ?? "Codex subagent TOML",
2683
+ path: target.path,
2684
+ state: target.state
2685
+ })),
2686
+ backup: {
2687
+ required: true,
2688
+ strategy: "managed-backup-file",
2689
+ description: "Existing subagent TOML and managed model state files are backed up by the managed write helper."
2690
+ },
2691
+ items: supportedRoles.map(({ role, model }) => ({
2692
+ title: `Set ${role} Codex subagent model line`,
2693
+ target: targets.find(
2694
+ (target) => target.path?.endsWith(`thoth-agents-${role}.toml`)
2695
+ ) ?? {
2696
+ kind: "generated-artifact",
2697
+ label: `Codex ${role} subagent`
2698
+ },
2699
+ preview: JSON.stringify({ role, model }),
2700
+ backup: {
2701
+ required: true,
2702
+ strategy: "managed-backup-file"
2703
+ }
2704
+ })),
2705
+ warnings,
2706
+ disclaimers: [
2707
+ ...codexDisclaimers(),
2708
+ ...input.disclaimers ?? [],
2709
+ {
2710
+ message: "Codex model configuration writes only generated subagent TOML model lines and the managed model state JSON.",
2711
+ code: "codex-model-supported-surface"
2712
+ }
2713
+ ]
2714
+ };
2715
+ codexModelSources.set(plan, { config, roles: supportedRoles });
2716
+ return plan;
2717
+ }
2718
+ function rejectPlan(plan, message, severity = "critical") {
2719
+ return {
2720
+ harness: plan.harness,
2721
+ action: plan.action,
2722
+ applied: false,
2723
+ summary: message,
2724
+ changedTargets: [],
2725
+ backups: [],
2726
+ warnings: [{ severity, message }],
2727
+ disclaimers: codexDisclaimers()
2728
+ };
2729
+ }
2730
+ function validateCodexPlan(plan) {
2731
+ if (plan.harness !== "codex") {
2732
+ return rejectPlan(plan, "Only Codex operation plans can be applied.");
2733
+ }
2734
+ if (!plan.canApply) {
2735
+ return rejectPlan(
2736
+ plan,
2737
+ "Codex plan cannot be applied because canApply is false."
2738
+ );
2739
+ }
2740
+ if (!["install", "update", "sync", "model-config"].includes(plan.action)) {
2741
+ return rejectPlan(plan, `Unsupported Codex apply action: ${plan.action}.`);
2742
+ }
2743
+ if (plan.items.length === 0) {
2744
+ return rejectPlan(plan, "Codex plan has no items to apply.");
2745
+ }
2746
+ return null;
2747
+ }
2748
+ function applyCodexPlan(plan) {
2749
+ const rejection = validateCodexPlan(plan);
2750
+ if (rejection) return rejection;
2751
+ if (plan.action === "model-config") {
2752
+ const source = codexModelSources.get(plan);
2753
+ if (!source) {
2754
+ return rejectPlan(
2755
+ plan,
2756
+ "Codex model plan was not produced by buildCodexModelPlan in this process."
2757
+ );
2758
+ }
2759
+ const result2 = applyCodexManagedModelOverrides(source.config, source.roles);
2760
+ return {
2761
+ harness: "codex",
2762
+ action: "model-config",
2763
+ applied: result2.success,
2764
+ summary: result2.success ? "Applied Codex subagent model overrides." : result2.error ?? "Failed to apply Codex subagent model overrides.",
2765
+ changedTargets: result2.changed.map((path2) => ({
2766
+ kind: path2.endsWith(".json") ? "memory-state" : "generated-artifact",
2767
+ path: path2,
2768
+ label: basename2(path2),
2769
+ state: "installed"
2770
+ })),
2771
+ backups: result2.changed.filter((path2) => existsSync5(`${path2}.bak`)).map((path2) => ({ path: `${path2}.bak`, label: "managed backup" })),
2772
+ warnings: result2.success ? [] : [
2773
+ {
2774
+ severity: "critical",
2775
+ message: result2.error ?? "Codex model apply failed."
2776
+ }
2777
+ ],
2778
+ disclaimers: codexDisclaimers()
2779
+ };
2780
+ }
2781
+ const setupPlan = codexPlanSources.get(plan);
2782
+ if (!setupPlan) {
2783
+ return rejectPlan(
2784
+ plan,
2785
+ "Codex setup plan was not produced by a Codex operation plan builder in this process."
2786
+ );
2787
+ }
2788
+ const result = applyCodexSetup({ ...setupPlan, dryRun: false });
2789
+ return {
2790
+ harness: "codex",
2791
+ action: plan.action,
2792
+ applied: result.success,
2793
+ summary: result.success ? `Applied Codex managed ${plan.action} plan.` : result.error ?? `Failed to apply Codex ${plan.action} plan.`,
2794
+ changedTargets: result.changed.map((path2) => ({
2795
+ kind: path2.endsWith(".json") ? "memory-state" : "generated-artifact",
2796
+ path: path2,
2797
+ label: basename2(path2),
2798
+ state: "installed"
2799
+ })),
2800
+ backups: result.changed.filter((path2) => existsSync5(`${path2}.bak`)).map((path2) => ({ path: `${path2}.bak`, label: "managed backup" })),
2801
+ warnings: result.success ? [] : [
2802
+ {
2803
+ severity: "critical",
2804
+ message: result.error ?? "Codex setup apply failed."
2805
+ }
2806
+ ],
2807
+ disclaimers: codexDisclaimers()
2808
+ };
2809
+ }
2810
+
2811
+ // src/cli/operations/opencode.ts
2812
+ import { existsSync as existsSync7 } from "fs";
2813
+ import { join as join6 } from "path";
2814
+
2815
+ // src/cli/skills.ts
2816
+ import { spawnSync } from "child_process";
2817
+ import { existsSync as existsSync6 } from "fs";
2818
+ import { homedir as homedir2 } from "os";
2819
+ import { join as join5 } from "path";
2820
+ var RECOMMENDED_SKILLS = [
2821
+ {
2822
+ name: "simplify",
2823
+ repo: "https://github.com/brianlovin/claude-config",
2824
+ skillName: "simplify",
2825
+ description: "YAGNI code simplification expert"
2826
+ },
2827
+ {
2828
+ name: "playwright-cli",
2829
+ repo: "https://github.com/microsoft/playwright-cli",
2830
+ skillName: "playwright-cli",
2831
+ description: "Browser automation for visual checks and testing"
2832
+ }
2833
+ ];
2834
+ function getRecommendedSkillPath(skill, homeDir = homedir2()) {
2835
+ return join5(homeDir, ".agents", "skills", skill.skillName, "SKILL.md");
2836
+ }
2837
+ function isRecommendedSkillInstalled(skill, options = {}) {
2838
+ return existsSync6(getRecommendedSkillPath(skill, options.homeDir));
2839
+ }
2840
+ function runSkillInstallCommand(skill) {
2841
+ const args = [
2842
+ "skills",
2843
+ "add",
2844
+ skill.repo,
2845
+ "--skill",
2846
+ skill.skillName,
2847
+ "-a",
2848
+ "opencode",
2849
+ "-y",
2850
+ "--global"
2851
+ ];
2852
+ try {
2853
+ const result = spawnSync("npx", args, { stdio: "inherit" });
2854
+ if (result.status !== 0) {
2855
+ return { success: false };
2856
+ }
2857
+ if (skill.postInstallCommands && skill.postInstallCommands.length > 0) {
2858
+ console.log(`Running post-install commands for ${skill.name}...`);
2859
+ for (const cmd of skill.postInstallCommands) {
2860
+ console.log(`> ${cmd}`);
2861
+ const [command, ...cmdArgs] = cmd.split(" ");
2862
+ const cmdResult = spawnSync(command, cmdArgs, { stdio: "inherit" });
2863
+ if (cmdResult.status !== 0) {
2864
+ console.warn(`Post-install command failed: ${cmd}`);
2865
+ }
2866
+ }
2867
+ }
2868
+ return { success: true };
2869
+ } catch (error) {
2870
+ console.error(`Failed to install skill: ${skill.name}`, error);
2871
+ return { success: false, error };
2872
+ }
2873
+ }
2874
+ function installRecommendedSkill(skill, options = {}) {
2875
+ const skillPath = getRecommendedSkillPath(skill, options.homeDir);
2876
+ if (isRecommendedSkillInstalled(skill, options)) {
2877
+ return { skill, status: "already-installed", skillPath };
2878
+ }
2879
+ const result = runSkillInstallCommand(skill);
2880
+ if (result.success) {
2881
+ return { skill, status: "installed", skillPath };
2882
+ }
2883
+ if (isRecommendedSkillInstalled(skill, options)) {
2884
+ return { skill, status: "already-installed", skillPath };
2885
+ }
2886
+ return { skill, status: "failed", skillPath, error: result.error };
2887
+ }
2888
+
2889
+ // src/cli/operations/opencode.ts
2890
+ var PACKAGE_NAME = "thoth-agents";
2891
+ var EXPECTED_PLUGIN = `${PACKAGE_NAME}@latest`;
2892
+ var OPENAI_PRESET = "openai";
2893
+ var ROLE_NAMES = [...ALL_AGENT_NAMES];
2894
+ var openCodeActions = [
2895
+ {
2896
+ id: "opencode-status",
2897
+ kind: "status",
2898
+ label: "Status",
2899
+ description: "Inspect managed OpenCode config state",
2900
+ dryRun: false,
2901
+ requiresConfirmation: false,
2902
+ supported: true
2903
+ },
2904
+ {
2905
+ id: "opencode-list",
2906
+ kind: "list",
2907
+ label: "List",
2908
+ description: "List managed OpenCode surfaces and actions",
2909
+ dryRun: false,
2910
+ requiresConfirmation: false,
2911
+ supported: true
2912
+ },
2913
+ {
2914
+ id: "opencode-install",
2915
+ kind: "install",
2916
+ label: "Install",
2917
+ description: "Preview OpenCode install using --no-tui --tmux=no --skills=yes semantics",
2918
+ dryRun: true,
2919
+ requiresConfirmation: true,
2920
+ supported: true
2921
+ },
2922
+ {
2923
+ id: "opencode-update",
2924
+ kind: "update",
2925
+ label: "Update",
2926
+ description: "Preview OpenCode plugin entry updates",
2927
+ dryRun: true,
2928
+ requiresConfirmation: true,
2929
+ supported: true
2930
+ },
2931
+ {
2932
+ id: "opencode-sync",
2933
+ kind: "sync",
2934
+ label: "Sync",
2935
+ description: "Preview OpenCode managed configuration sync",
2936
+ dryRun: true,
2937
+ requiresConfirmation: true,
2938
+ supported: true
2939
+ },
2940
+ {
2941
+ id: "opencode-model-config",
2942
+ kind: "model-config",
2943
+ label: "Model",
2944
+ description: "Preview OpenCode role model override changes",
2945
+ dryRun: true,
2946
+ requiresConfirmation: true,
2947
+ supported: true
2948
+ }
2949
+ ];
2950
+ var opencodeOperationAdapter = {
2951
+ id: "opencode",
2952
+ displayName: "OpenCode",
2953
+ available: true,
2954
+ description: "OpenCode plugin and configuration surfaces.",
2955
+ actions: openCodeActions
2956
+ };
2957
+ function targetForMainConfig(state) {
2958
+ return {
2959
+ kind: "config",
2960
+ path: getExistingConfigPath(),
2961
+ label: "OpenCode config",
2962
+ state,
2963
+ expected: `plugin includes ${EXPECTED_PLUGIN}`
2964
+ };
2965
+ }
2966
+ function targetForLiteConfig(state) {
2967
+ return {
2968
+ kind: "config",
2969
+ path: getExistingLiteConfigPath(),
2970
+ label: "thoth-agents config",
2971
+ state,
2972
+ expected: `seven-agent ${OPENAI_PRESET} roster`
2973
+ };
2974
+ }
2975
+ function titleCaseSkillName(value) {
2976
+ return value.split(/[-_\s]+/).filter(Boolean).map((part) => {
2977
+ const normalized = part.toLowerCase();
2978
+ if (normalized === "cli") return "CLI";
2979
+ if (normalized === "mcp") return "MCP";
2980
+ if (normalized === "sdd") return "SDD";
2981
+ if (normalized === "opencode") return "OpenCode";
2982
+ return `${part.charAt(0).toUpperCase()}${part.slice(1).toLowerCase()}`;
2983
+ }).join("-");
2984
+ }
2985
+ function homeDirFromContext(context) {
2986
+ return context.env?.HOME ?? context.env?.USERPROFILE;
2987
+ }
2988
+ function openCodeSkillTargets(context) {
2989
+ const homeDir = homeDirFromContext(context);
2990
+ const recommendedTargets = RECOMMENDED_SKILLS.map(
2991
+ (skill) => {
2992
+ const path2 = getRecommendedSkillPath(skill, homeDir);
2993
+ const installed = existsSync7(path2);
2994
+ return {
2995
+ kind: "skill",
2996
+ path: path2,
2997
+ label: titleCaseSkillName(skill.skillName),
2998
+ state: installed ? "installed" : "missing",
2999
+ expected: "recommended global OpenCode skill",
3000
+ observed: installed ? "recommended global skill installed" : "recommended global skill missing"
3001
+ };
3002
+ }
3003
+ );
3004
+ const bundledTargets = CUSTOM_SKILLS.map((skill) => {
3005
+ const path2 = join6(getCustomSkillsDir(), skill.name, "SKILL.md");
3006
+ const installed = existsSync7(path2);
3007
+ return {
3008
+ kind: "skill",
3009
+ path: path2,
3010
+ label: titleCaseSkillName(skill.name),
3011
+ state: installed ? "installed" : "missing",
3012
+ expected: "bundled thoth-agents OpenCode skill",
3013
+ observed: installed ? "bundled OpenCode skill installed" : "bundled OpenCode skill missing"
3014
+ };
3015
+ });
3016
+ const diagnostics = [];
3017
+ if (recommendedTargets.some((target) => target.state === "missing")) {
3018
+ diagnostics.push({
3019
+ severity: "minor",
3020
+ message: "Recommended OpenCode global skills are missing; run the OpenCode install flow with skills enabled.",
3021
+ code: "opencode-recommended-skills-missing"
3022
+ });
3023
+ }
3024
+ if (bundledTargets.some((target) => target.state === "missing")) {
3025
+ diagnostics.push({
3026
+ severity: "important",
3027
+ message: "Bundled thoth-agents OpenCode skills are missing; run OpenCode install or sync to refresh managed skills.",
3028
+ code: "opencode-bundled-skills-missing"
3029
+ });
3030
+ }
3031
+ return {
3032
+ targets: [...recommendedTargets, ...bundledTargets],
3033
+ diagnostics
3034
+ };
3035
+ }
3036
+ function configPluginMarker(config) {
3037
+ const plugins = Array.isArray(config?.plugin) ? config.plugin : [];
3038
+ return plugins.length > 0 ? `plugin: ${JSON.stringify(plugins)}` : "plugin: []";
3039
+ }
3040
+ function hasExpectedPlugin(config) {
3041
+ return Array.isArray(config?.plugin) && config.plugin.includes(EXPECTED_PLUGIN);
3042
+ }
3043
+ function hasManagedPluginDrift(config) {
3044
+ return Array.isArray(config?.plugin) && config.plugin.some(
3045
+ (plugin) => plugin === PACKAGE_NAME || plugin.startsWith(`${PACKAGE_NAME}@`)
3046
+ ) && !hasExpectedPlugin(config);
3047
+ }
3048
+ function liteConfigMarker(config) {
3049
+ const preset = typeof config?.preset === "string" ? config.preset : void 0;
3050
+ const presets = config?.presets && typeof config.presets === "object" ? config.presets : {};
3051
+ const openaiPreset = presets[OPENAI_PRESET] && typeof presets[OPENAI_PRESET] === "object" ? presets[OPENAI_PRESET] : {};
3052
+ const roles = ROLE_NAMES.filter((role) => role in openaiPreset);
3053
+ return `preset: ${preset ?? "none"}; ${OPENAI_PRESET} roles: ${roles.join(", ")}`;
3054
+ }
3055
+ function hasSevenAgentPreset(config) {
3056
+ if (!config || config.preset !== OPENAI_PRESET) return false;
3057
+ if (!config.presets || typeof config.presets !== "object") return false;
3058
+ const presets = config.presets;
3059
+ const openaiPreset = presets[OPENAI_PRESET];
3060
+ if (!openaiPreset || typeof openaiPreset !== "object") return false;
3061
+ const roles = openaiPreset;
3062
+ return ROLE_NAMES.every((role) => {
3063
+ const value = roles[role];
3064
+ return value !== null && typeof value === "object" && typeof value.model === "string";
3065
+ });
3066
+ }
3067
+ function classifyApplySafety(state) {
3068
+ return state === "missing" || state === "installed";
3069
+ }
3070
+ function getOpenCodeStatus(context = { cwd: process.cwd() }) {
3071
+ const mainPath = getExistingConfigPath();
3072
+ const litePath = getExistingLiteConfigPath();
3073
+ const main = parseConfig(mainPath);
3074
+ const lite = parseConfig(litePath);
3075
+ const diagnostics = [];
3076
+ const skillStatus = openCodeSkillTargets(context);
3077
+ if (main.error) {
3078
+ diagnostics.push({
3079
+ severity: "critical",
3080
+ message: `Failed to parse OpenCode config: ${main.error}`,
3081
+ code: "opencode-config-parse-error"
3082
+ });
3083
+ }
3084
+ if (lite.error) {
3085
+ diagnostics.push({
3086
+ severity: "critical",
3087
+ message: `Failed to parse thoth-agents config: ${lite.error}`,
3088
+ code: "thoth-config-parse-error"
3089
+ });
3090
+ }
3091
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "critical")) {
3092
+ return {
3093
+ harness: "opencode",
3094
+ displayName: "OpenCode",
3095
+ state: "unknown",
3096
+ summary: "OpenCode managed state could not be classified safely.",
3097
+ targets: [
3098
+ {
3099
+ ...targetForMainConfig("unknown"),
3100
+ observed: main.error ?? "unparsed"
3101
+ },
3102
+ {
3103
+ ...targetForLiteConfig("unknown"),
3104
+ observed: lite.error ?? "unparsed"
3105
+ },
3106
+ ...skillStatus.targets
3107
+ ],
3108
+ diagnostics: [...diagnostics, ...skillStatus.diagnostics],
3109
+ actions: openCodeActions
3110
+ };
3111
+ }
3112
+ const mainExists = existsSync7(mainPath);
3113
+ const liteExists = existsSync7(litePath);
3114
+ const mainTarget = {
3115
+ ...targetForMainConfig(),
3116
+ observed: configPluginMarker(main.config)
3117
+ };
3118
+ const liteTarget = {
3119
+ ...targetForLiteConfig(),
3120
+ observed: liteConfigMarker(lite.config)
3121
+ };
3122
+ if (!mainExists || !hasExpectedPlugin(main.config)) {
3123
+ if (hasManagedPluginDrift(main.config)) {
3124
+ return {
3125
+ harness: "opencode",
3126
+ displayName: "OpenCode",
3127
+ state: "drift",
3128
+ summary: "OpenCode config has a managed thoth-agents plugin entry that is not latest.",
3129
+ targets: [
3130
+ { ...mainTarget, state: "drift" },
3131
+ { ...liteTarget, state: liteExists ? "installed" : "missing" },
3132
+ ...skillStatus.targets
3133
+ ],
3134
+ diagnostics: [
3135
+ {
3136
+ severity: "important",
3137
+ message: "Managed plugin drift requires an explicit repair before applying sync plans.",
3138
+ code: "opencode-plugin-drift"
3139
+ },
3140
+ ...skillStatus.diagnostics
3141
+ ],
3142
+ actions: openCodeActions
3143
+ };
3144
+ }
3145
+ return {
3146
+ harness: "opencode",
3147
+ displayName: "OpenCode",
3148
+ state: "missing",
3149
+ summary: "OpenCode does not include the managed thoth-agents plugin entry.",
3150
+ targets: [
3151
+ { ...mainTarget, state: "missing" },
3152
+ { ...liteTarget, state: liteExists ? "installed" : "missing" },
3153
+ ...skillStatus.targets
3154
+ ],
3155
+ diagnostics: [...diagnostics, ...skillStatus.diagnostics],
3156
+ actions: openCodeActions
3157
+ };
3158
+ }
3159
+ if (!liteExists) {
3160
+ return {
3161
+ harness: "opencode",
3162
+ displayName: "OpenCode",
3163
+ state: "missing",
3164
+ summary: "OpenCode plugin is present, but thoth-agents config is missing.",
3165
+ targets: [
3166
+ { ...mainTarget, state: "installed" },
3167
+ { ...liteTarget, state: "missing" },
3168
+ ...skillStatus.targets
3169
+ ],
3170
+ diagnostics: [...diagnostics, ...skillStatus.diagnostics],
3171
+ actions: openCodeActions
3172
+ };
3173
+ }
3174
+ if (!hasSevenAgentPreset(lite.config)) {
3175
+ return {
3176
+ harness: "opencode",
3177
+ displayName: "OpenCode",
3178
+ state: "drift",
3179
+ summary: "thoth-agents config does not match the expected seven-agent roster.",
3180
+ targets: [
3181
+ { ...mainTarget, state: "installed" },
3182
+ { ...liteTarget, state: "drift" },
3183
+ ...skillStatus.targets
3184
+ ],
3185
+ diagnostics: [
3186
+ {
3187
+ severity: "important",
3188
+ message: "Roster drift requires repair before applying generated plans.",
3189
+ code: "opencode-roster-drift"
3190
+ },
3191
+ ...skillStatus.diagnostics
3192
+ ],
3193
+ actions: openCodeActions
3194
+ };
3195
+ }
3196
+ return {
3197
+ harness: "opencode",
3198
+ displayName: "OpenCode",
3199
+ state: "installed",
3200
+ summary: "OpenCode managed thoth-agents configuration is installed.",
3201
+ targets: [
3202
+ { ...mainTarget, state: "installed" },
3203
+ { ...liteTarget, state: "installed" },
3204
+ ...skillStatus.targets
3205
+ ],
3206
+ diagnostics: [...diagnostics, ...skillStatus.diagnostics],
3207
+ actions: openCodeActions
3208
+ };
3209
+ }
3210
+ function defaultBackup(path2) {
3211
+ return {
3212
+ required: true,
3213
+ strategy: "managed-backup-file",
3214
+ destinations: [{ path: `${path2}.bak`, label: "managed backup" }]
3215
+ };
3216
+ }
3217
+ function defaultDisclaimers() {
3218
+ return [
3219
+ {
3220
+ message: "Preview generation does not inspect OpenCode plugin cache internals or write files.",
3221
+ code: "opencode-cache-not-inspected"
3222
+ }
3223
+ ];
3224
+ }
3225
+ function planFromItems(id, action, title, summary, items) {
3226
+ const status = getOpenCodeStatus();
3227
+ const safe = classifyApplySafety(status.state);
3228
+ const warnings = safe ? status.diagnostics : [
3229
+ ...status.diagnostics,
3230
+ {
3231
+ severity: "important",
3232
+ message: `OpenCode state is ${status.state}; apply is disabled until the state is repaired or safely classified.`,
3233
+ code: "opencode-unsafe-state"
3234
+ }
3235
+ ];
3236
+ return {
3237
+ id,
3238
+ harness: "opencode",
3239
+ action,
3240
+ title,
3241
+ summary,
3242
+ dryRun: true,
3243
+ canApply: safe,
3244
+ targets: status.targets,
3245
+ surfaces: [
3246
+ {
3247
+ id: "opencode-config",
3248
+ label: "OpenCode user config",
3249
+ path: getExistingConfigPath(),
3250
+ state: status.targets[0]?.state
3251
+ },
3252
+ {
3253
+ id: "thoth-agents-config",
3254
+ label: "thoth-agents plugin config",
3255
+ path: getExistingLiteConfigPath(),
3256
+ state: status.targets[1]?.state
3257
+ }
3258
+ ],
3259
+ backup: defaultBackup(getExistingConfigPath()),
3260
+ items,
3261
+ warnings,
3262
+ disclaimers: defaultDisclaimers()
3263
+ };
3264
+ }
3265
+ function buildOpenCodeUpdatePlan(_context = { cwd: process.cwd() }) {
3266
+ const path2 = getExistingConfigPath();
3267
+ return planFromItems(
3268
+ "opencode-update-preview",
3269
+ "update",
3270
+ "Update OpenCode managed plugin entry",
3271
+ `Preview ensuring plugin: ["${EXPECTED_PLUGIN}"].`,
3272
+ [
3273
+ {
3274
+ title: "Ensure OpenCode plugin points at thoth-agents@latest",
3275
+ target: targetForMainConfig(),
3276
+ state: getOpenCodeStatus().state,
3277
+ preview: `plugin: ["${EXPECTED_PLUGIN}"]`,
3278
+ backup: defaultBackup(path2)
3279
+ }
3280
+ ]
3281
+ );
3282
+ }
3283
+ function buildOpenCodeSyncPlan(_context = { cwd: process.cwd() }) {
3284
+ const generatedConfig = generateLiteConfig({
3285
+ agent: "opencode",
3286
+ hasTmux: false,
3287
+ installSkills: false,
3288
+ installCustomSkills: true,
3289
+ dryRun: true,
3290
+ reset: false
3291
+ });
3292
+ const litePath = getExistingLiteConfigPath();
3293
+ return planFromItems(
3294
+ "opencode-sync-preview",
3295
+ "sync",
3296
+ "Sync OpenCode managed configuration",
3297
+ "Preview OpenCode plugin entry, default-agent disablement, and thoth-agents config sync.",
3298
+ [
3299
+ {
3300
+ title: "Ensure OpenCode plugin points at thoth-agents@latest",
3301
+ target: targetForMainConfig(),
3302
+ state: getOpenCodeStatus().state,
3303
+ preview: `plugin: ["${EXPECTED_PLUGIN}"]`,
3304
+ backup: defaultBackup(getExistingConfigPath())
3305
+ },
3306
+ {
3307
+ title: "Disable OpenCode default agents",
3308
+ target: targetForMainConfig(),
3309
+ preview: "agent.explore.disable = true; agent.general.disable = true",
3310
+ backup: defaultBackup(getExistingConfigPath())
3311
+ },
3312
+ {
3313
+ title: "Write thoth-agents seven-agent config",
3314
+ target: targetForLiteConfig(),
3315
+ preview: JSON.stringify(generatedConfig, null, 2),
3316
+ backup: defaultBackup(litePath)
3317
+ }
3318
+ ]
3319
+ );
3320
+ }
3321
+ function tuiInstallConfig() {
3322
+ return {
3323
+ agent: "opencode",
3324
+ hasTmux: false,
3325
+ installSkills: true,
3326
+ installCustomSkills: true,
3327
+ dryRun: true,
3328
+ reset: false
3329
+ };
3330
+ }
3331
+ function buildOpenCodeInstallPlan(_context = { cwd: process.cwd() }) {
3332
+ const generatedConfig = generateLiteConfig(tuiInstallConfig());
3333
+ const installPreview = {
3334
+ noTui: true,
3335
+ hasTmux: false,
3336
+ installSkills: true,
3337
+ installCustomSkills: true,
3338
+ equivalentCommand: "install --agent=opencode --no-tui --tmux=no --skills=yes"
3339
+ };
3340
+ const litePath = getExistingLiteConfigPath();
3341
+ return planFromItems(
3342
+ "opencode-install-preview",
3343
+ "install",
3344
+ "Preview install",
3345
+ "Preview OpenCode install equivalent to install --agent=opencode --no-tui --tmux=no --skills=yes.",
3346
+ [
3347
+ {
3348
+ title: "Apply OpenCode TUI install options",
3349
+ target: {
3350
+ kind: "config",
3351
+ path: getExistingConfigPath(),
3352
+ label: "OpenCode install options",
3353
+ state: getOpenCodeStatus().state,
3354
+ expected: "--no-tui --tmux=no --skills=yes"
3355
+ },
3356
+ preview: JSON.stringify(installPreview, null, 2)
3357
+ },
3358
+ {
3359
+ title: "Ensure OpenCode plugin points at thoth-agents@latest",
3360
+ target: targetForMainConfig(),
3361
+ state: getOpenCodeStatus().state,
3362
+ preview: `plugin: ["${EXPECTED_PLUGIN}"]`,
3363
+ backup: defaultBackup(getExistingConfigPath())
3364
+ },
3365
+ {
3366
+ title: "Disable OpenCode default agents",
3367
+ target: targetForMainConfig(),
3368
+ preview: "agent.explore.disable = true; agent.general.disable = true",
3369
+ backup: defaultBackup(getExistingConfigPath())
3370
+ },
3371
+ {
3372
+ title: "Write thoth-agents seven-agent config",
3373
+ target: targetForLiteConfig(),
3374
+ preview: JSON.stringify(generatedConfig, null, 2),
3375
+ backup: defaultBackup(litePath)
3376
+ },
3377
+ {
3378
+ title: "Install recommended external skills",
3379
+ target: {
3380
+ kind: "skill",
3381
+ label: "Recommended OpenCode skills",
3382
+ expected: "skills=yes"
3383
+ },
3384
+ preview: JSON.stringify({ installSkills: true }, null, 2)
3385
+ }
3386
+ ]
3387
+ );
3388
+ }
3389
+ function normalizeRoleModel(input) {
3390
+ if (input.provider && !input.model.includes("/")) {
3391
+ return `${input.provider}/${input.model}`;
3392
+ }
3393
+ return input.model;
3394
+ }
3395
+ function buildOpenCodeModelPlan(input, _context = { cwd: process.cwd() }) {
3396
+ const items = input.roles.map((role) => {
3397
+ const model = normalizeRoleModel(role);
3398
+ return {
3399
+ title: `Set ${role.role} OpenCode model override`,
3400
+ target: targetForLiteConfig(),
3401
+ preview: JSON.stringify({ [role.role]: { model } }),
3402
+ backup: defaultBackup(getExistingLiteConfigPath())
3403
+ };
3404
+ });
3405
+ const plan = planFromItems(
3406
+ "opencode-model-config-preview",
3407
+ "model-config",
3408
+ "Configure OpenCode role model overrides",
3409
+ "Preview thoth-agents role model overrides in the plugin config agents map.",
3410
+ items
3411
+ );
3412
+ if (input.harness !== "opencode") {
3413
+ return {
3414
+ ...plan,
3415
+ canApply: false,
3416
+ warnings: [
3417
+ ...plan.warnings,
3418
+ {
3419
+ severity: "critical",
3420
+ message: "Model plan target harness must be opencode.",
3421
+ code: "opencode-model-harness-mismatch"
3422
+ }
3423
+ ]
3424
+ };
3425
+ }
3426
+ const unsupportedRoles = input.roles.filter(
3427
+ (role) => !ROLE_NAMES.includes(role.role)
3428
+ );
3429
+ if (input.roles.length === 0 || unsupportedRoles.length > 0) {
3430
+ return {
3431
+ ...plan,
3432
+ canApply: false,
3433
+ warnings: [
3434
+ ...plan.warnings,
3435
+ {
3436
+ severity: "critical",
3437
+ message: input.roles.length === 0 ? "Model plan must include at least one role override." : `Unsupported OpenCode model role(s): ${unsupportedRoles.map((role) => role.role).join(", ")}.`,
3438
+ code: "opencode-model-roster-incomplete"
3439
+ }
3440
+ ]
3441
+ };
3442
+ }
3443
+ return plan;
3444
+ }
3445
+ function rejectPlan2(plan, message, severity = "critical") {
3446
+ return {
3447
+ harness: plan.harness,
3448
+ action: plan.action,
3449
+ applied: false,
3450
+ summary: message,
3451
+ changedTargets: [],
3452
+ backups: [],
3453
+ warnings: [{ severity, message }],
3454
+ disclaimers: defaultDisclaimers()
3455
+ };
3456
+ }
3457
+ function ensureLatestPluginEntry() {
3458
+ const configPath = getExistingConfigPath();
3459
+ try {
3460
+ ensureOpenCodeConfigDir();
3461
+ const { config: parsedConfig, error } = parseConfig(configPath);
3462
+ if (error) {
3463
+ return {
3464
+ success: false,
3465
+ configPath,
3466
+ error: `Failed to parse config: ${error}`
3467
+ };
3468
+ }
3469
+ const config = parsedConfig ?? {};
3470
+ const plugins = Array.isArray(config.plugin) ? config.plugin : [];
3471
+ config.plugin = [
3472
+ ...plugins.filter(
3473
+ (plugin) => plugin !== PACKAGE_NAME && !plugin.startsWith(`${PACKAGE_NAME}@`)
3474
+ ),
3475
+ EXPECTED_PLUGIN
3476
+ ];
3477
+ writeConfig(configPath, config);
3478
+ return { success: true, configPath };
3479
+ } catch (err) {
3480
+ return {
3481
+ success: false,
3482
+ configPath,
3483
+ error: `Failed to update OpenCode plugin config: ${err}`
3484
+ };
3485
+ }
3486
+ }
3487
+ function validateApplyPlan(plan) {
3488
+ if (plan.harness !== "opencode") {
3489
+ return rejectPlan2(plan, "Only OpenCode operation plans can be applied.");
3490
+ }
3491
+ if (!plan.canApply) {
3492
+ return rejectPlan2(
3493
+ plan,
3494
+ "OpenCode plan cannot be applied because canApply is false."
3495
+ );
3496
+ }
3497
+ if (!["install", "update", "sync", "model-config"].includes(plan.action)) {
3498
+ return rejectPlan2(
3499
+ plan,
3500
+ `Unsupported OpenCode apply action: ${plan.action}.`
3501
+ );
3502
+ }
3503
+ if (plan.items.length === 0) {
3504
+ return rejectPlan2(plan, "OpenCode plan has no items to apply.");
3505
+ }
3506
+ if (plan.items.some(
3507
+ (item) => !item.title || !item.target.path && item.target.kind !== "skill" || item.state === "drift" || item.state === "unknown"
3508
+ )) {
3509
+ return rejectPlan2(
3510
+ plan,
3511
+ "OpenCode plan contains malformed or unsafe items."
3512
+ );
3513
+ }
3514
+ return null;
3515
+ }
3516
+ function applyModelPlan(plan) {
3517
+ const roleModels = /* @__PURE__ */ new Map();
3518
+ for (const item of plan.items) {
3519
+ const match = /^Set (.+) OpenCode model override$/.exec(item.title);
3520
+ if (!match) {
3521
+ return rejectPlan2(
3522
+ plan,
3523
+ "OpenCode model plan contains an unrecognized item."
3524
+ );
3525
+ }
3526
+ let parsed2;
3527
+ try {
3528
+ parsed2 = item.preview ? JSON.parse(item.preview) : {};
3529
+ } catch {
3530
+ return rejectPlan2(
3531
+ plan,
3532
+ "OpenCode model plan contains malformed preview JSON."
3533
+ );
3534
+ }
3535
+ const role = match[1] ?? "";
3536
+ const model = parsed2[role]?.model;
3537
+ if (!ROLE_NAMES.includes(role) || typeof model !== "string") {
3538
+ return rejectPlan2(
3539
+ plan,
3540
+ "OpenCode model plan contains an invalid role or model."
3541
+ );
3542
+ }
3543
+ roleModels.set(role, model);
3544
+ }
3545
+ if (roleModels.size === 0) {
3546
+ return rejectPlan2(
3547
+ plan,
3548
+ "OpenCode model plan does not contain any role overrides."
3549
+ );
3550
+ }
3551
+ ensureConfigDir();
3552
+ const targetPath = getExistingLiteConfigPath();
3553
+ const parsed = parseConfig(targetPath);
3554
+ const base = parsed.config ?? generateLiteConfig({
3555
+ agent: "opencode",
3556
+ hasTmux: false,
3557
+ installSkills: false,
3558
+ installCustomSkills: true,
3559
+ dryRun: false,
3560
+ reset: false
3561
+ });
3562
+ const agents = base.agents && typeof base.agents === "object" ? { ...base.agents } : {};
3563
+ for (const role of roleModels.keys()) {
3564
+ agents[role] = {
3565
+ ...agents[role] ?? {},
3566
+ model: roleModels.get(role)
3567
+ };
3568
+ }
3569
+ base.agents = agents;
3570
+ writeConfig(targetPath, base);
3571
+ return {
3572
+ harness: "opencode",
3573
+ action: "model-config",
3574
+ applied: true,
3575
+ summary: "Applied OpenCode role model overrides.",
3576
+ changedTargets: [
3577
+ {
3578
+ ...targetForLiteConfig("installed"),
3579
+ observed: "agents role overrides updated"
3580
+ }
3581
+ ],
3582
+ backups: existsSync7(`${targetPath}.bak`) ? [{ path: `${targetPath}.bak`, label: "managed backup" }] : [],
3583
+ warnings: [],
3584
+ disclaimers: defaultDisclaimers()
3585
+ };
3586
+ }
3587
+ function applyInstallSkills(plan) {
3588
+ for (const skill of RECOMMENDED_SKILLS) {
3589
+ const result = installRecommendedSkill(skill);
3590
+ if (result.status === "failed") {
3591
+ return rejectPlan2(
3592
+ plan,
3593
+ `Failed to install recommended OpenCode skill: ${skill.name}.`
3594
+ );
3595
+ }
3596
+ }
3597
+ const bundled = installCustomSkills();
3598
+ if (!bundled.success) {
3599
+ return rejectPlan2(
3600
+ plan,
3601
+ "Failed to install bundled thoth-agents OpenCode skills."
3602
+ );
3603
+ }
3604
+ return null;
3605
+ }
3606
+ function applyOpenCodePlan(plan) {
3607
+ const rejection = validateApplyPlan(plan);
3608
+ if (rejection) return rejection;
3609
+ const status = getOpenCodeStatus();
3610
+ if (!classifyApplySafety(status.state)) {
3611
+ return rejectPlan2(
3612
+ plan,
3613
+ `OpenCode state is ${status.state}; refusing to apply plan without a safe status.`
3614
+ );
3615
+ }
3616
+ if (plan.action === "model-config") return applyModelPlan(plan);
3617
+ const changedTargets = [];
3618
+ const backups = [];
3619
+ const pluginResult = ensureLatestPluginEntry();
3620
+ if (!pluginResult.success) {
3621
+ return rejectPlan2(
3622
+ plan,
3623
+ pluginResult.error ?? "Failed to update OpenCode plugin config."
3624
+ );
3625
+ }
3626
+ changedTargets.push({
3627
+ ...targetForMainConfig("installed"),
3628
+ observed: `plugin includes ${EXPECTED_PLUGIN}`
3629
+ });
3630
+ if (existsSync7(`${pluginResult.configPath}.bak`)) {
3631
+ backups.push({
3632
+ path: `${pluginResult.configPath}.bak`,
3633
+ label: "OpenCode config backup"
3634
+ });
3635
+ }
3636
+ if (plan.action === "sync" || plan.action === "install") {
3637
+ const defaultAgentResult = disableDefaultAgents();
3638
+ if (!defaultAgentResult.success) {
3639
+ return rejectPlan2(
3640
+ plan,
3641
+ defaultAgentResult.error ?? "Failed to disable OpenCode default agents."
3642
+ );
3643
+ }
3644
+ const liteResult = writeLiteConfig(
3645
+ {
3646
+ agent: "opencode",
3647
+ hasTmux: false,
3648
+ installSkills: plan.action === "install",
3649
+ installCustomSkills: true,
3650
+ dryRun: false,
3651
+ reset: false
3652
+ },
3653
+ getExistingLiteConfigPath()
3654
+ );
3655
+ if (!liteResult.success) {
3656
+ return rejectPlan2(
3657
+ plan,
3658
+ liteResult.error ?? "Failed to write thoth-agents config."
3659
+ );
3660
+ }
3661
+ changedTargets.push({
3662
+ ...targetForLiteConfig("installed"),
3663
+ observed: "seven-agent roster written"
3664
+ });
3665
+ if (existsSync7(`${liteResult.configPath}.bak`)) {
3666
+ backups.push({
3667
+ path: `${liteResult.configPath}.bak`,
3668
+ label: "thoth-agents config backup"
3669
+ });
3670
+ }
3671
+ }
3672
+ if (plan.action === "install") {
3673
+ const skillsRejection = applyInstallSkills(plan);
3674
+ if (skillsRejection) return skillsRejection;
3675
+ changedTargets.push({
3676
+ kind: "skill",
3677
+ label: "Recommended and bundled OpenCode skills",
3678
+ state: "installed",
3679
+ observed: "skills=yes processed"
3680
+ });
3681
+ }
3682
+ return {
3683
+ harness: "opencode",
3684
+ action: plan.action,
3685
+ applied: true,
3686
+ summary: plan.action === "install" ? "Applied OpenCode install plan." : plan.action === "sync" ? "Applied OpenCode managed configuration sync." : "Applied OpenCode plugin update.",
3687
+ changedTargets,
3688
+ backups,
3689
+ warnings: [],
3690
+ disclaimers: defaultDisclaimers()
3691
+ };
3692
+ }
3693
+
3694
+ // src/cli/operations/index.ts
3695
+ var OPERATION_HARNESSES = {
3696
+ opencode: opencodeOperationAdapter,
3697
+ codex: codexOperationAdapter
3698
+ };
3699
+ var SUPPORTED_OPERATION_HARNESSES = Object.keys(
3700
+ OPERATION_HARNESSES
3701
+ );
3702
+ function listOperationHarnesses() {
3703
+ return SUPPORTED_OPERATION_HARNESSES.map(
3704
+ (harness) => OPERATION_HARNESSES[harness]
3705
+ );
3706
+ }
3707
+ function getOperationHarness(harness) {
3708
+ return OPERATION_HARNESSES[harness];
3709
+ }
3710
+
3711
+ export {
3712
+ codexAdapter,
3713
+ CODEX_ROLE_NAMES,
3714
+ parseRoleTomlModel,
3715
+ buildCodexSetupPlan,
3716
+ formatCodexSetupPlan,
3717
+ applyCodexSetup,
3718
+ RECOMMENDED_SKILLS,
3719
+ installRecommendedSkill,
3720
+ getCodexStatus,
3721
+ buildCodexUpdatePlan,
3722
+ buildCodexSyncPlan,
3723
+ buildCodexInstallPlan,
3724
+ buildCodexModelPlan,
3725
+ applyCodexPlan,
3726
+ getOpenCodeStatus,
3727
+ buildOpenCodeUpdatePlan,
3728
+ buildOpenCodeSyncPlan,
3729
+ buildOpenCodeInstallPlan,
3730
+ buildOpenCodeModelPlan,
3731
+ applyOpenCodePlan,
3732
+ SUPPORTED_OPERATION_HARNESSES,
3733
+ listOperationHarnesses,
3734
+ getOperationHarness
3735
+ };