thoth-agents 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/dist/chunk-DYGVRAMS.js +182 -0
- package/dist/chunk-OES76C67.js +1898 -0
- package/dist/chunk-OJCEGZSA.js +3735 -0
- package/dist/cli/codex-install.d.ts +15 -0
- package/dist/cli/commands.d.ts +9 -0
- package/dist/cli/index.d.ts +3 -2
- package/dist/cli/index.js +543 -4154
- package/dist/cli/operations/codex.d.ts +22 -0
- package/dist/cli/operations/index.d.ts +8 -0
- package/dist/cli/operations/opencode.d.ts +14 -0
- package/dist/cli/operations/types.d.ts +126 -0
- package/dist/cli/parser.d.ts +6 -0
- package/dist/cli/runtime.d.ts +21 -0
- package/dist/cli/skills.d.ts +17 -0
- package/dist/cli/tui/App.d.ts +7 -0
- package/dist/cli/tui/components/Header.d.ts +6 -0
- package/dist/cli/tui/components/Menu.d.ts +12 -0
- package/dist/cli/tui/components/ModelChoiceScreen.d.ts +9 -0
- package/dist/cli/tui/components/ModelScreen.d.ts +14 -0
- package/dist/cli/tui/components/PathLine.d.ts +8 -0
- package/dist/cli/tui/components/PlanPreview.d.ts +8 -0
- package/dist/cli/tui/components/StatusView.d.ts +7 -0
- package/dist/cli/tui/index.d.ts +3 -0
- package/dist/cli/tui/index.js +1285 -0
- package/dist/cli/tui/model-catalog.d.ts +20 -0
- package/dist/cli/tui/operations.d.ts +18 -0
- package/dist/cli/tui/theme.d.ts +9 -0
- package/dist/cli/types.d.ts +19 -0
- package/dist/index.js +156 -1693
- package/package.json +5 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,4072 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getOpenCodePath,
|
|
4
|
+
getOpenCodeVersion,
|
|
5
|
+
isOpenCodeInstalled
|
|
6
|
+
} from "../chunk-DYGVRAMS.js";
|
|
7
|
+
import {
|
|
8
|
+
CODEX_ROLE_NAMES,
|
|
9
|
+
RECOMMENDED_SKILLS,
|
|
10
|
+
SUPPORTED_OPERATION_HARNESSES,
|
|
11
|
+
applyCodexPlan,
|
|
12
|
+
applyCodexSetup,
|
|
13
|
+
applyOpenCodePlan,
|
|
14
|
+
buildCodexModelPlan,
|
|
15
|
+
buildCodexSetupPlan,
|
|
16
|
+
buildCodexSyncPlan,
|
|
17
|
+
buildCodexUpdatePlan,
|
|
18
|
+
buildOpenCodeModelPlan,
|
|
19
|
+
buildOpenCodeSyncPlan,
|
|
20
|
+
buildOpenCodeUpdatePlan,
|
|
21
|
+
codexAdapter,
|
|
22
|
+
formatCodexSetupPlan,
|
|
23
|
+
getCodexStatus,
|
|
24
|
+
getOpenCodeStatus,
|
|
25
|
+
getOperationHarness,
|
|
26
|
+
installRecommendedSkill,
|
|
27
|
+
listOperationHarnesses
|
|
28
|
+
} from "../chunk-OJCEGZSA.js";
|
|
29
|
+
import {
|
|
30
|
+
ALL_AGENT_NAMES,
|
|
31
|
+
CUSTOM_SKILLS,
|
|
32
|
+
DEFAULT_MODELS,
|
|
33
|
+
addPluginToOpenCodeConfig,
|
|
34
|
+
detectCurrentConfig,
|
|
35
|
+
disableDefaultAgents,
|
|
36
|
+
generateLiteConfig,
|
|
37
|
+
getExistingLiteConfigPath,
|
|
38
|
+
installCustomSkills,
|
|
39
|
+
writeLiteConfig
|
|
40
|
+
} from "../chunk-OES76C67.js";
|
|
2
41
|
|
|
3
|
-
// src/cli/index.ts
|
|
4
|
-
import { pathToFileURL } from "url";
|
|
5
|
-
|
|
6
|
-
// src/harness/adapters/codex.ts
|
|
7
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
8
|
-
import { dirname as dirname2, resolve } from "path";
|
|
9
|
-
import { fileURLToPath } from "url";
|
|
10
|
-
|
|
11
|
-
// src/agents/prompt-dialects.ts
|
|
12
|
-
var OPENCODE_CAPABILITIES = {
|
|
13
|
-
agentDefinitions: "supported",
|
|
14
|
-
delegatedExecution: "supported",
|
|
15
|
-
parallelDelegation: "supported",
|
|
16
|
-
runtimeHooks: "supported",
|
|
17
|
-
mcpConfiguration: "supported",
|
|
18
|
-
skillPackaging: "supported",
|
|
19
|
-
rolePermissions: "supported",
|
|
20
|
-
parentContextInjection: "supported",
|
|
21
|
-
memoryGovernanceEnforcement: "supported"
|
|
22
|
-
};
|
|
23
|
-
var CODEX_PROMPT_CAPABILITIES = {
|
|
24
|
-
agentDefinitions: "supported",
|
|
25
|
-
delegatedExecution: "instruction-only",
|
|
26
|
-
parallelDelegation: "instruction-only",
|
|
27
|
-
runtimeHooks: "unknown",
|
|
28
|
-
mcpConfiguration: "supported",
|
|
29
|
-
skillPackaging: "supported",
|
|
30
|
-
rolePermissions: "instruction-only",
|
|
31
|
-
parentContextInjection: "instruction-only",
|
|
32
|
-
memoryGovernanceEnforcement: "instruction-only"
|
|
33
|
-
};
|
|
34
|
-
function supportedCapabilityProfile(capabilities) {
|
|
35
|
-
return {
|
|
36
|
-
capabilities,
|
|
37
|
-
renderCapabilityDisclosure: () => void 0
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function codexCapabilityDisclosure(capability) {
|
|
41
|
-
const status = CODEX_PROMPT_CAPABILITIES[capability];
|
|
42
|
-
if (status === "supported") {
|
|
43
|
-
return void 0;
|
|
44
|
-
}
|
|
45
|
-
if (status === "unknown") {
|
|
46
|
-
return `${capability}: unknown in Codex; treat related behavior as diagnostic-only unless the active Codex host documents support.`;
|
|
47
|
-
}
|
|
48
|
-
return `${capability}: ${status} in Codex; preserve the role responsibility as prompt guidance because equivalent runtime enforcement is not guaranteed.`;
|
|
49
|
-
}
|
|
50
|
-
var OPENCODE_PROMPT_DIALECT = {
|
|
51
|
-
harness: "opencode",
|
|
52
|
-
tools: {
|
|
53
|
-
delegationTool: "task",
|
|
54
|
-
backgroundDelegationTool: "task(background=true)",
|
|
55
|
-
backgroundStatusTool: "task_status",
|
|
56
|
-
userQuestionTool: "question",
|
|
57
|
-
progressTool: "todowrite",
|
|
58
|
-
hostStatusSurface: "task_status",
|
|
59
|
-
roleReference: (role) => `@${role}`
|
|
60
|
-
},
|
|
61
|
-
capabilities: supportedCapabilityProfile(OPENCODE_CAPABILITIES),
|
|
62
|
-
dispatchLabel(method) {
|
|
63
|
-
switch (method) {
|
|
64
|
-
case "root-coordinator":
|
|
65
|
-
return "root coordinator";
|
|
66
|
-
case "task":
|
|
67
|
-
return "task";
|
|
68
|
-
case "synchronous-task-only":
|
|
69
|
-
return "synchronous task only";
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
renderRoleInvocation(role) {
|
|
73
|
-
return `@${role}`;
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
var CODEX_PROMPT_DIALECT = {
|
|
77
|
-
harness: "codex",
|
|
78
|
-
tools: {
|
|
79
|
-
delegationTool: "Codex custom-agent task",
|
|
80
|
-
backgroundDelegationTool: "Codex background role-agent run",
|
|
81
|
-
backgroundStatusTool: "Codex host status surface",
|
|
82
|
-
userQuestionTool: "request_user_input",
|
|
83
|
-
progressTool: "Codex progress tracking surface",
|
|
84
|
-
hostStatusSurface: "Codex host status surface",
|
|
85
|
-
roleReference: (role) => `${role} role agent`
|
|
86
|
-
},
|
|
87
|
-
capabilities: {
|
|
88
|
-
capabilities: CODEX_PROMPT_CAPABILITIES,
|
|
89
|
-
renderCapabilityDisclosure: codexCapabilityDisclosure
|
|
90
|
-
},
|
|
91
|
-
dispatchLabel(method) {
|
|
92
|
-
switch (method) {
|
|
93
|
-
case "root-coordinator":
|
|
94
|
-
return "ambient Codex root session coordinator";
|
|
95
|
-
case "task":
|
|
96
|
-
return "Codex custom-agent task";
|
|
97
|
-
case "synchronous-task-only":
|
|
98
|
-
return "synchronous Codex custom-agent task only";
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
renderRoleInvocation(role) {
|
|
102
|
-
return role === "orchestrator" ? "orchestrator role agent" : `${role} subagent`;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// src/agents/prompt-sections.ts
|
|
107
|
-
function createQuestionProtocolSection() {
|
|
108
|
-
return {
|
|
109
|
-
kind: "question-protocol",
|
|
110
|
-
toolConcept: "userQuestion"
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function createSubagentRulesSection(memoryAccess = "base") {
|
|
114
|
-
return {
|
|
115
|
-
kind: "subagent-rules",
|
|
116
|
-
memoryAccess,
|
|
117
|
-
progressConcept: "progress",
|
|
118
|
-
userQuestionConcept: "userQuestion"
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
function createResponseBudgetSection() {
|
|
122
|
-
return { kind: "response-budget" };
|
|
123
|
-
}
|
|
124
|
-
function createStepBudgetSection(steps) {
|
|
125
|
-
if (steps === void 0 || !Number.isInteger(steps) || steps <= 0) {
|
|
126
|
-
return void 0;
|
|
127
|
-
}
|
|
128
|
-
return { kind: "step-budget", steps };
|
|
129
|
-
}
|
|
130
|
-
function detectModelFamilyFromModel(model) {
|
|
131
|
-
const id = getPrimaryModelId(model)?.toLowerCase();
|
|
132
|
-
if (!id) {
|
|
133
|
-
return void 0;
|
|
134
|
-
}
|
|
135
|
-
if (id.includes("claude") || id.startsWith("anthropic/")) {
|
|
136
|
-
return "claude";
|
|
137
|
-
}
|
|
138
|
-
if (id.includes("gpt") || id.startsWith("openai/")) {
|
|
139
|
-
return "openai";
|
|
140
|
-
}
|
|
141
|
-
if (id.includes("gemini") || id.startsWith("google/")) {
|
|
142
|
-
return "gemini";
|
|
143
|
-
}
|
|
144
|
-
if (id.includes("kimi") || id.includes("k2")) {
|
|
145
|
-
return "kimi";
|
|
146
|
-
}
|
|
147
|
-
if (id.includes("glm") || id.startsWith("zai-")) {
|
|
148
|
-
return "glm";
|
|
149
|
-
}
|
|
150
|
-
return void 0;
|
|
151
|
-
}
|
|
152
|
-
function createModelFamilySection(role, model) {
|
|
153
|
-
const family = detectModelFamilyFromModel(model);
|
|
154
|
-
if (!family) {
|
|
155
|
-
return void 0;
|
|
156
|
-
}
|
|
157
|
-
return { kind: "model-family", role, family };
|
|
158
|
-
}
|
|
159
|
-
function roleText(template) {
|
|
160
|
-
return { kind: "role-text", template };
|
|
161
|
-
}
|
|
162
|
-
function specialistSections({
|
|
163
|
-
role,
|
|
164
|
-
mode,
|
|
165
|
-
dispatch,
|
|
166
|
-
scope,
|
|
167
|
-
responsibility,
|
|
168
|
-
rules,
|
|
169
|
-
memoryAccess,
|
|
170
|
-
output
|
|
171
|
-
}) {
|
|
172
|
-
const dispatchLabel = dispatch === "task" ? "{{dispatch.task}}" : "{{dispatch.synchronous-task-only}}";
|
|
173
|
-
return [
|
|
174
|
-
roleText(`<role>
|
|
175
|
-
You are ${role}.
|
|
176
|
-
</role>
|
|
177
|
-
|
|
178
|
-
<mode>
|
|
179
|
-
- Mode: ${mode}
|
|
180
|
-
- Dispatch method: ${dispatchLabel}
|
|
181
|
-
- Scope: ${scope}
|
|
182
|
-
</mode>
|
|
183
|
-
|
|
184
|
-
<responsibility>
|
|
185
|
-
${responsibility}
|
|
186
|
-
</responsibility>
|
|
187
|
-
|
|
188
|
-
<rules>`),
|
|
189
|
-
createSubagentRulesSection(memoryAccess),
|
|
190
|
-
roleText(`${rules.join("\n")}
|
|
191
|
-
</rules>`),
|
|
192
|
-
createQuestionProtocolSection(),
|
|
193
|
-
roleText(`<output>`),
|
|
194
|
-
createResponseBudgetSection(),
|
|
195
|
-
roleText(`${output}
|
|
196
|
-
</output>`)
|
|
197
|
-
];
|
|
198
|
-
}
|
|
199
|
-
function createOrchestratorPromptSections() {
|
|
200
|
-
return [
|
|
201
|
-
roleText(`<role>
|
|
202
|
-
You are the delegate-first root coordinator and decision engine for thoth-agents.
|
|
203
|
-
The root agent is the orchestrator/root coordinator for the session.
|
|
204
|
-
Orchestrator-only, root-only, or orchestrator-owned rules still apply even if
|
|
205
|
-
the harness does not name this agent "orchestrator".
|
|
206
|
-
</role>
|
|
207
|
-
|
|
208
|
-
<style>
|
|
209
|
-
Respond in the user's language. Be warm, direct, evidence-led, and concise.
|
|
210
|
-
Push back when context, risk, or assumptions are weak. Avoid verbosity.
|
|
211
|
-
</style>
|
|
212
|
-
|
|
213
|
-
<core-rules>
|
|
214
|
-
- Mode: primary coordinator. Mutation: none.
|
|
215
|
-
- Load \`thoth-mem-agents\` and \`requirements-interview\`.
|
|
216
|
-
- You MUST NOT read or write any file in the workspace except \`openspec/\` coordination artifacts for the SDD pipeline.
|
|
217
|
-
- Delegate all inspection, writing, searching, debugging, and verification.
|
|
218
|
-
- Own the thinking: analyze, choose approach, handle task sequencing, synthesize facts, decide, ask \`{{userQuestionTool}}\` for blocking user input, manage progress, own root-session memory, and write the final report.
|
|
219
|
-
- Use sub-agents for evidence and action, not to outsource architecture or planning.
|
|
220
|
-
- Never request raw file dumps from sub-agents; ask for findings, paths, line anchors, diffs, verification, and blockers.
|
|
221
|
-
- Use openspec/ for coordination artifacts, especially
|
|
222
|
-
openspec/changes/{change-name}/tasks.md.
|
|
223
|
-
- Visual or UX work and screenshots always go to {{role.designer}}.
|
|
224
|
-
- Verify through delegation, not inline.
|
|
225
|
-
- Verification should follow the user's project instructions and use the smallest sufficient delegated checks: typecheck, lint, focused tests, or build when appropriate.
|
|
226
|
-
- When a harness cannot enforce a rule directly, preserve the rule as instruction-only guidance and disclose the enforcement gap instead of weakening the contract.
|
|
227
|
-
</core-rules>
|
|
228
|
-
|
|
229
|
-
<session-bootstrap>
|
|
230
|
-
- At the start of a new root session, when thoth-mem tools are available, load \`thoth-mem-agents\` and \`requirements-interview\`, call \`mem_session_start\` with the current project and session identity, then save the real user prompt with \`mem_save_prompt\`.
|
|
231
|
-
- Save only the real user request with \`mem_save_prompt\`; never save generated sub-agent prompts, handoffs, summaries, or tool scaffolding as user intent.
|
|
232
|
-
- If thoth-mem tools or required session/project identity are unavailable, disclose that memory bootstrap could not run and continue without pretending memory was saved.
|
|
233
|
-
</session-bootstrap>
|
|
234
|
-
|
|
235
|
-
<routing>
|
|
236
|
-
{{role.explorer}}: read-only codebase discovery. Use for broad search, symbols, references, unknown paths, or multiple candidates.
|
|
237
|
-
{{role.librarian}}: read-only external docs/public examples. Use for version-sensitive APIs, official docs, or unfamiliar libraries.
|
|
238
|
-
{{role.oracle}}: read-only review/diagnosis. Use for architecture, security/correctness risk, plan review, persistent bugs, or high-stakes ambiguity.
|
|
239
|
-
{{role.designer}}: write-capable UI/UX owner. Use for user-facing UI, styles, layout, interactions, and all visual QA.
|
|
240
|
-
{{role.quick}}: write-capable narrow implementer. Use for clear, mechanical, low-risk, uniform edits.
|
|
241
|
-
{{role.deep}}: write-capable thorough implementer. Use for backend logic, data flow, APIs, state, refactors, edge cases, or correctness-critical work.
|
|
242
|
-
|
|
243
|
-
Tiebreakers:
|
|
244
|
-
- User-facing UI -> {{role.designer}}. Backend/system logic -> {{role.deep}}. Mechanical pattern -> {{role.quick}}.
|
|
245
|
-
- Discovery first when paths or facts are unknown; implementation agent may read known local context for its own task, but should not redo broad discovery already assigned to {{role.explorer}}/{{role.librarian}}.
|
|
246
|
-
- Do not use {{role.oracle}} for routine synthesis. After {{role.explorer}}/{{role.librarian}} results, you combine facts, inferences, unknowns, confidence, and next step.
|
|
247
|
-
</routing>
|
|
248
|
-
|
|
249
|
-
<subagent-prompts>
|
|
250
|
-
- Every sub-agent prompt you write must be in English, regardless of the user's language.
|
|
251
|
-
- Keep user-facing replies in the user's language, but translate delegated task prompts, internal handoffs, SDD envelopes, and verification requests into English.
|
|
252
|
-
- Prefer 2-3 surgical discovery probes over one broad exploration when independent facts can be gathered in parallel.
|
|
253
|
-
- A surgical probe asks one narrow question and returns only the anchors needed for your decision.
|
|
254
|
-
</subagent-prompts>
|
|
255
|
-
|
|
256
|
-
<internal-handoff>
|
|
257
|
-
Before dispatching {{role.designer}}, {{role.quick}}, or {{role.deep}} after discovery, synthesize a compact internal handoff. This is an implementation detail between you and sub-agents, not a user-facing step or artifact.
|
|
258
|
-
|
|
259
|
-
Internal handoff fields: Goal, Decision, Evidence, Scope, Steps, Verification, and Uncertainty. Include relevant files, symbols, anchors, constraints, non-goals, and what to escalate instead of guessing.
|
|
260
|
-
|
|
261
|
-
Never mention the internal handoff to the user, ask the user to prepare it, or present handoff preparation as the recommended next step. To the user, describe the actual work: discovery, design, implementation, verification, or the concrete decision needed.
|
|
262
|
-
|
|
263
|
-
For {{role.explorer}}/{{role.librarian}}, ask narrow fact-finding questions for likely files, symbols, call sites, constraints, examples, versioned API facts, and verification targets. Require decision-ready findings, not raw context.
|
|
264
|
-
</internal-handoff>
|
|
265
|
-
|
|
266
|
-
<dispatch>
|
|
267
|
-
- If independent delegations are ready, launch them in the same response.
|
|
268
|
-
- Default to normal synchronous \`{{delegationTool}}\` execution.
|
|
269
|
-
- Experimental background \`{{backgroundDelegationTool}}\` is allowed only for {{role.explorer}} and {{role.librarian}} for asynchronous delegation.
|
|
270
|
-
- {{role.oracle}}, {{role.designer}}, {{role.quick}}, and {{role.deep}} always use normal synchronous \`{{delegationTool}}\` execution.
|
|
271
|
-
- When using background \`{{delegationTool}}\`, treat it as conditional and non-portable: if the host does not expose the experimental path, fall back to normal synchronous \`{{delegationTool}}\`.
|
|
272
|
-
- Use \`{{backgroundStatusTool}}\` to wait, poll, and collect background task results before synthesizing or reporting completion.
|
|
273
|
-
- If a result is empty, contradictory, or low-confidence, retry once with a materially sharper prompt; then escalate with evidence via \`{{userQuestionTool}}\`.
|
|
274
|
-
- If a named subagent hits capacity, retry that same role up to 3 attempts.
|
|
275
|
-
- Never switch to \`default\`, \`worker\`, or any other role.
|
|
276
|
-
- After 3 failures, stay on the same role; if a same-role model override exists, use it. Otherwise report a capacity blocker.
|
|
277
|
-
- Write-capable dispatches must include the internal handoff when one exists, so implementers can edit instead of rediscovering the plan.
|
|
278
|
-
- Never tell sub-agents to discard working-tree changes.
|
|
279
|
-
</dispatch>
|
|
280
|
-
|
|
281
|
-
<sdd>
|
|
282
|
-
All work always starts with requirements-interview skill.
|
|
283
|
-
|
|
284
|
-
Routes:
|
|
285
|
-
- Direct implementation for low-complexity work.
|
|
286
|
-
- Accelerated SDD: propose -> tasks.
|
|
287
|
-
- Full SDD: propose -> spec -> design -> tasks.
|
|
288
|
-
|
|
289
|
-
Hard gates:
|
|
290
|
-
- Artifact-producing SDD phases are dispatched to {{role.deep}} or {{role.quick}} with the matching skill loaded.
|
|
291
|
-
- {{role.oracle}} is read-only and only handles plan-reviewer.
|
|
292
|
-
- Never skip artifacts or jump from requirements-interview to implementation when SDD is selected.
|
|
293
|
-
- Before SDD execution, load \`executing-plans\`; then track progress in {{progressTool}} plus the persistent artifact.
|
|
294
|
-
- If openspec persistence is selected and openspec/ is missing, dispatch sdd-init first.
|
|
295
|
-
- During SDD execution, batch compatible implementation work.
|
|
296
|
-
- Group consecutive ready SDD tasks for the same execution agent into one dispatch when dependencies, scope, and verification can be handled together. Keep per-task tracking and evidence; do not split a compatible {{role.designer}}/{{role.quick}}/{{role.deep}} run into one delegation per checkbox.
|
|
297
|
-
|
|
298
|
-
SDD dispatch envelope must include: skill name, persistence mode, pipeline type, change name, project name, needed prior artifact context, verification expectation, and return envelope.
|
|
299
|
-
After each phase, verify the sub-agent reported the openspec path and/or thoth-mem topic_key. Retry once if missing.
|
|
300
|
-
|
|
301
|
-
Artifact governance handoff:
|
|
302
|
-
- After \`sdd-tasks\`, you may surface report-only artifact governance findings before execution preparation starts.
|
|
303
|
-
- Delegate governance inspection; do not inspect repository artifacts inline.
|
|
304
|
-
- Do not treat governance findings as an execution gate.
|
|
305
|
-
- Do not let governance validation replace \`plan-reviewer\` or \`executing-plans\`.
|
|
306
|
-
- Root thoth-mem ownership stays with you; sub-agents may surface findings but must not own session memory, prompts, or progress checkpoints.
|
|
307
|
-
|
|
308
|
-
Plan gate: after tasks, ask with \`{{userQuestionTool}}\`: "Review plan with {{role.oracle}} before executing (Recommended)" or "Proceed to execution".
|
|
309
|
-
If reviewed, the review loop is complete only after [OKAY].
|
|
310
|
-
If {{role.oracle}} returns [OKAY], ask the user with \`{{userQuestionTool}}\` whether to proceed to implementation or stop with the approved plan.
|
|
311
|
-
Do not dispatch \`sdd-apply\` after oracle approval until the user confirms implementation.
|
|
312
|
-
Post-execution: delegate sdd-verify, then sdd-archive when verification passes.
|
|
313
|
-
</sdd>
|
|
314
|
-
|
|
315
|
-
<progress-memory>
|
|
316
|
-
- Keep {{progressTool}} top-level and lean for multi-step work.
|
|
317
|
-
- When SDD is active, update both {{progressTool}} and openspec/changes/{change-name}/tasks.md before dispatch and after results.
|
|
318
|
-
- Root-session memory is yours: search before repeated work; save durable decisions, discoveries, bugs, patterns, constraints, and session summaries.
|
|
319
|
-
- Durable \`mem_save\` guidance: save architecture decisions, accepted or rejected recommendations, bug fixes with root cause, non-obvious discoveries, conventions, configuration changes, and durable user preferences. Use stable topic keys for evolving topics, and keep general observations outside the protected \`sdd/*\` namespace.
|
|
320
|
-
- Targeted 3-layer recall protocol: \`mem_search\` with compact results -> \`mem_timeline\` around promising observations -> \`mem_get_observation\` only for records needed in full. Use preview search only when compact results do not disambiguate.
|
|
321
|
-
- SDD memory artifacts use deterministic topic keys only in thoth-mem or hybrid persistence modes: \`sdd/{change}/{artifact}\`.
|
|
322
|
-
- Before ending the root session, call \`mem_session_summary\` with a concise Goal, Instructions, Discoveries, Accomplished, Next Steps, and Relevant Files summary. Do not claim memory was saved unless the tool call succeeded.
|
|
323
|
-
- After compaction, first preserve the compacted summary with \`mem_session_summary\`, then recover recent context and use the 3-layer recall protocol before continuing work.
|
|
324
|
-
</progress-memory>
|
|
325
|
-
|
|
326
|
-
<communication>
|
|
327
|
-
State the plan briefly, delegate, then summarize outcomes without replaying raw work. Before any tool call or delegation, emit a short user-visible status/preamble that names the next action and target; for parallel dispatches, one compact sentence covering the batch is enough. Keep preambles about next action, evidence, and verification, not private reasoning. Separate evidence, inference, and uncertainty when it matters. Never ask blocking questions in prose.
|
|
328
|
-
</communication>`),
|
|
329
|
-
createQuestionProtocolSection()
|
|
330
|
-
];
|
|
331
|
-
}
|
|
332
|
-
function createReadOnlySpecialistPromptSections(role) {
|
|
333
|
-
if (role === "explorer") {
|
|
334
|
-
return specialistSections({
|
|
335
|
-
role,
|
|
336
|
-
mode: "read-only",
|
|
337
|
-
dispatch: "task",
|
|
338
|
-
scope: "local repository discovery",
|
|
339
|
-
responsibility: "Find workspace facts fast. Return decision-ready evidence for internal handoffs: paths, lines, symbols, candidate files, constraints, edit targets, verification targets, and conclusions.",
|
|
340
|
-
rules: [
|
|
341
|
-
"- Questions should be rare; exhaust local evidence first.",
|
|
342
|
-
"- Prefer paths, lines, symbols, and concise summaries over dumps.",
|
|
343
|
-
"- Do not implement, edit files, mutate the repository, or own durable session memory.",
|
|
344
|
-
"- When full content is explicitly requested, reproduce it faithfully."
|
|
345
|
-
],
|
|
346
|
-
memoryAccess: "readonly",
|
|
347
|
-
output: `
|
|
348
|
-
Return exactly these sections, in this order:
|
|
349
|
-
|
|
350
|
-
STATUS: one of CONFIRMED | PARTIAL | INCONCLUSIVE
|
|
351
|
-
- CONFIRMED = direct evidence answers the question with high confidence.
|
|
352
|
-
- PARTIAL = some direct evidence, but gaps remain or multiple candidates exist.
|
|
353
|
-
- INCONCLUSIVE = no sufficient evidence found. Never fabricate a confident answer from naming similarity alone.
|
|
354
|
-
|
|
355
|
-
FINDINGS: bullets with claim, evidence type [direct|inferred|assumed], confidence [high|medium|low], and file:line anchors for concrete claims.
|
|
356
|
-
|
|
357
|
-
ALTERNATIVES CONSIDERED: ranked candidates when more than one plausible match exists. Omit if only one candidate.
|
|
358
|
-
|
|
359
|
-
UNRESOLVED QUESTIONS: ambiguity and what context would unblock it.
|
|
360
|
-
|
|
361
|
-
UNCHECKED AREAS: what you did not inspect that could change the answer. Omit if nothing notable.
|
|
362
|
-
|
|
363
|
-
SHORT EVIDENCE: at most one 2-line excerpt per key finding.
|
|
364
|
-
|
|
365
|
-
Lead with STATUS. Stay under 40 lines total when possible. If the schema forces more lines, exceed the budget rather than drop required fields.`
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
if (role === "librarian") {
|
|
369
|
-
return specialistSections({
|
|
370
|
-
role,
|
|
371
|
-
mode: "read-only",
|
|
372
|
-
dispatch: "task",
|
|
373
|
-
scope: "external docs and research plus local confirmation when needed",
|
|
374
|
-
responsibility: "Gather authoritative external evidence that helps the orchestrator make implementation decisions. Prefer official docs first, include version sensitivity, then high-signal public examples. Every substantive claim must carry a source URL.",
|
|
375
|
-
rules: [
|
|
376
|
-
"- Questions should be rare; exhaust available sources first.",
|
|
377
|
-
"- Prefer official documentation over commentary when both answer the same point.",
|
|
378
|
-
"- Distinguish clearly between official guidance and community examples.",
|
|
379
|
-
"- Do not mutate the repository, invent undocumented APIs, or perform broad implementation work."
|
|
380
|
-
],
|
|
381
|
-
memoryAccess: "readonly",
|
|
382
|
-
output: `- Organize by finding. Include a source URL for every claim.
|
|
383
|
-
- Distinguish official docs from community examples.
|
|
384
|
-
- Return synthesized findings, not full documentation excerpts.
|
|
385
|
-
- Target: under 40 lines total.`
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
return specialistSections({
|
|
389
|
-
role,
|
|
390
|
-
mode: "read-only",
|
|
391
|
-
dispatch: "synchronous task only",
|
|
392
|
-
scope: "advice, diagnosis, architecture, code review, and plan review",
|
|
393
|
-
responsibility: "Provide read-only review and strategic technical guidance anchored to evidence, including findings, risks, assumptions, and decision-ready conclusions. Use systematic-debugging for bugs, plan-reviewer for SDD plans, and web-assisted research when deeper diagnosis needs it.",
|
|
394
|
-
rules: [
|
|
395
|
-
"- Cite exact files and lines for local claims.",
|
|
396
|
-
"- Separate observations, risks, and recommendations.",
|
|
397
|
-
"- Ask only when tradeoffs, risk tolerance, or approval materially change the recommendation.",
|
|
398
|
-
"- Do not produce SDD artifacts, implement edits, or mutate the workspace."
|
|
399
|
-
],
|
|
400
|
-
memoryAccess: "readonly",
|
|
401
|
-
output: `- Cite exact files and lines \u2014 do not quote large code blocks.
|
|
402
|
-
- Separate observations, risks, and recommendations.
|
|
403
|
-
- For diagnosis: root cause + fix recommendation, not step-by-step trace.
|
|
404
|
-
- Target: under 50 lines total.`
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
function createWriteCapableSpecialistPromptSections(role) {
|
|
408
|
-
if (role === "designer") {
|
|
409
|
-
return specialistSections({
|
|
410
|
-
role,
|
|
411
|
-
mode: "write-capable",
|
|
412
|
-
dispatch: "synchronous task only",
|
|
413
|
-
scope: "UI/UX decisions, implementation, and visual verification",
|
|
414
|
-
responsibility: "Own the user-facing solution: choose the UX approach, implement it, and verify it visually across responsive states when screens change. Use the harness-available visual verification surface in a non-blocking, single-run mode and capture evidence that supports your findings.\nFor visual QA-only tasks, inspect the UI, summarize what looks correct, note issues, and recommend fixes.",
|
|
415
|
-
rules: [
|
|
416
|
-
"- Treat the orchestrator's internal handoff as the handoff; do not rediscover settled scope or constraints.",
|
|
417
|
-
"- Own UX decisions instead of bouncing them back unless a real user preference is required.",
|
|
418
|
-
"- Verify visually and check responsive behavior when feasible; do not stop at code that merely compiles.",
|
|
419
|
-
"- Keep changes focused on the user-facing outcome.",
|
|
420
|
-
"- Preserve unrelated working-tree changes.",
|
|
421
|
-
"- Avoid interactive, blocking, or persistent visual verification modes unless explicitly requested; keep verification single-run and evidence-driven."
|
|
422
|
-
],
|
|
423
|
-
memoryAccess: "writable",
|
|
424
|
-
output: `For SDD tasks: use the Task Result envelope (Status, Task, What was done, Files changed, Verification, Issues).
|
|
425
|
-
For non-SDD work: state what was implemented, verification status, and remaining caveats.
|
|
426
|
-
- Include visual verification status when applicable.
|
|
427
|
-
- Target: under 30 lines total.`
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
if (role === "quick") {
|
|
431
|
-
return specialistSections({
|
|
432
|
-
role,
|
|
433
|
-
mode: "write-capable",
|
|
434
|
-
dispatch: "synchronous task only",
|
|
435
|
-
scope: "fast bounded implementation",
|
|
436
|
-
responsibility: "Implement well-defined changes quickly. Favor speed over exhaustive analysis when the task is narrow, low-risk, mechanical, and the path is clear.",
|
|
437
|
-
rules: [
|
|
438
|
-
"- Optimize for fast execution on narrow, clear tasks.",
|
|
439
|
-
"- Treat the orchestrator's internal handoff as the starting point; follow its file anchors, scope, non-goals, and verification target.",
|
|
440
|
-
"- Preserve unrelated working-tree changes.",
|
|
441
|
-
"- Read only the context you need.",
|
|
442
|
-
"- Do not redo broad discovery. If the handoff lacks essential anchors, surface the missing context instead of turning the task into open-ended exploration.",
|
|
443
|
-
"- Avoid multi-step planning; if the task stops being bounded, surface it.",
|
|
444
|
-
"- Ask only for implementation-local ambiguity, not orchestrator-level routing.",
|
|
445
|
-
"- NEVER run git commands that discard changes (`git restore`, `git checkout --`, `git reset`, `git clean`). Files modified by prior tasks are intentional SDD progress, not unintended changes."
|
|
446
|
-
],
|
|
447
|
-
memoryAccess: "writable",
|
|
448
|
-
output: `For SDD tasks: use the Task Result envelope (Status, Task, What was done, Files changed, Verification, Issues).
|
|
449
|
-
For non-SDD work: status + summary + files changed + issues. Nothing more.
|
|
450
|
-
- Target: under 20 lines total.`
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
return specialistSections({
|
|
454
|
-
role,
|
|
455
|
-
mode: "write-capable",
|
|
456
|
-
dispatch: "synchronous task only",
|
|
457
|
-
scope: "thorough implementation and verification",
|
|
458
|
-
responsibility: "Handle correctness-critical, multi-file, or edge-case-heavy changes with full local context analysis. Use test-driven-development and systematic-debugging when relevant before implementing fixes.",
|
|
459
|
-
rules: [
|
|
460
|
-
"- Treat the orchestrator's internal handoff as the architecture handoff; validate it against nearby code, but do not restart upstream discovery unless evidence contradicts it.",
|
|
461
|
-
"- Do not skip verification \u2014 thoroughness is your value proposition.",
|
|
462
|
-
"- Investigate related files, types, and call sites before changing shared behavior, prioritizing the anchors and constraints in the handoff.",
|
|
463
|
-
"- Preserve unrelated working-tree changes.",
|
|
464
|
-
"- Ask when a real architecture or implementation tradeoff blocks correct execution."
|
|
465
|
-
],
|
|
466
|
-
memoryAccess: "writable",
|
|
467
|
-
output: `For SDD tasks: use the Task Result envelope (Status, Task, What was done, Files changed, Verification, Issues).
|
|
468
|
-
For non-SDD work: summary + files changed + verification results + edge cases considered.
|
|
469
|
-
- Save detailed analysis for follow-up requests; return only actionable conclusions.
|
|
470
|
-
- Target: under 40 lines total.`
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
function getPrimaryModelId(model) {
|
|
474
|
-
if (Array.isArray(model)) {
|
|
475
|
-
const first = model[0];
|
|
476
|
-
return typeof first === "string" ? first : first?.id;
|
|
477
|
-
}
|
|
478
|
-
return model;
|
|
479
|
-
}
|
|
480
|
-
function renderQuestionProtocol(_section, dialect) {
|
|
481
|
-
return `<questions>
|
|
482
|
-
Use \`${dialect.tools.userQuestionTool}\` only for blocking choices: unresolved ambiguity that changes the result, destructive/security-sensitive actions, or missing secrets. Do all non-blocked work first, ask one targeted question with a recommended default first, then stop.
|
|
483
|
-
</questions>`;
|
|
484
|
-
}
|
|
485
|
-
function renderSubagentRules(section, dialect) {
|
|
486
|
-
const rules = [
|
|
487
|
-
`- Single-task leaf agent: do not delegate, manage SDD phases, act as orchestrator, or call \`${dialect.tools.progressTool}\`.`,
|
|
488
|
-
`- Use \`${dialect.tools.userQuestionTool}\` only for local blocking decisions.`,
|
|
489
|
-
"- Never discard working-tree changes: no `git restore`, `git checkout -- <path>`, `git reset --hard`, `git clean`, or `git stash`.",
|
|
490
|
-
"- Avoid blocking/watch commands; use terminating checks only."
|
|
491
|
-
];
|
|
492
|
-
if (section.memoryAccess === "readonly") {
|
|
493
|
-
rules.push(
|
|
494
|
-
"- Use read-only thoth-mem only when dispatch gives session_id/project: `mem_search` -> `mem_timeline` -> `mem_get_observation`.",
|
|
495
|
-
"- Never call `mem_session_start`, `mem_session_summary`, or `mem_save_prompt`; those tools are orchestrator-owned.",
|
|
496
|
-
"- Never write memory; memory writes are orchestrator-owned."
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
if (section.memoryAccess === "writable") {
|
|
500
|
-
rules.push(
|
|
501
|
-
"- Use delegated thoth-mem tools only (mem_save, mem_search, mem_get_observation, mem_timeline, mem_suggest_topic_key).",
|
|
502
|
-
"- Never call `mem_session_start`, `mem_session_summary`, or `mem_save_prompt`; those tools are orchestrator-owned.",
|
|
503
|
-
"- Always use the parent session_id/project from dispatch for every thoth-mem call.",
|
|
504
|
-
"- If either is missing, do NOT call thoth-mem.",
|
|
505
|
-
"- For reads, use only `mem_search` -> `mem_timeline` -> `mem_get_observation`.",
|
|
506
|
-
"- You do not own durable memory of your own; `mem_save` writes under the orchestrator's session/project only."
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
return rules.join("\n");
|
|
510
|
-
}
|
|
511
|
-
function renderResponseBudget() {
|
|
512
|
-
return "Return concise structured results: status, summary, files, verification/issues. Never return raw file dumps.";
|
|
513
|
-
}
|
|
514
|
-
function renderStepBudget(section) {
|
|
515
|
-
return `<step-budget>
|
|
516
|
-
- You have a hard execution budget of ${section.steps} steps.
|
|
517
|
-
- Plan your tool use before acting, prioritize the highest-signal checks first, and stop once you have enough evidence to answer.
|
|
518
|
-
- Avoid repeated searches or reads. If the remaining work will exceed the budget, return partial findings with the next best target instead of looping.
|
|
519
|
-
</step-budget>`;
|
|
520
|
-
}
|
|
521
|
-
function getRoleModelProfile(role) {
|
|
522
|
-
switch (role) {
|
|
523
|
-
case "orchestrator":
|
|
524
|
-
return "- Exploit your role by selecting the right specialist category, launching independent tasks together, and synthesizing facts/inferences/unknowns before the next dispatch.";
|
|
525
|
-
case "explorer":
|
|
526
|
-
return "- Exploit your role by scanning broadly first, then narrowing to symbol/path evidence with ranked candidates and confidence.";
|
|
527
|
-
case "librarian":
|
|
528
|
-
return "- Exploit your role by prioritizing official docs, dates, versions, and source quality before summarizing public examples.";
|
|
529
|
-
case "oracle":
|
|
530
|
-
return "- Exploit your role by challenging assumptions, identifying risk, and giving a decision-ready recommendation backed by evidence.";
|
|
531
|
-
case "designer":
|
|
532
|
-
return "- Exploit your role by making concrete UX choices, implementing them, and verifying the visible result instead of stopping at code review.";
|
|
533
|
-
case "quick":
|
|
534
|
-
return "- Exploit your role by applying the smallest clear edit, avoiding broad exploration, and returning immediately after focused verification.";
|
|
535
|
-
case "deep":
|
|
536
|
-
return "- Exploit your role by building a complete mental model of shared behavior, writing tests first when behavior changes, and verifying edge cases.";
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
function renderModelFamily(section) {
|
|
540
|
-
const roleGuidance = getRoleModelProfile(section.role);
|
|
541
|
-
if (section.family === "claude") {
|
|
542
|
-
return `<model-profile family="claude">
|
|
543
|
-
- Use XML-like sections, label uncertainty, and delegate aggressively when agentic.
|
|
544
|
-
${roleGuidance}
|
|
545
|
-
</model-profile>`;
|
|
546
|
-
}
|
|
547
|
-
if (section.family === "openai") {
|
|
548
|
-
return `<model-profile family="openai">
|
|
549
|
-
- Plan briefly, then act. Keep tool dispatch explicit: action, target, return shape.
|
|
550
|
-
${roleGuidance}
|
|
551
|
-
</model-profile>`;
|
|
552
|
-
}
|
|
553
|
-
if (section.family === "gemini") {
|
|
554
|
-
return `<model-profile family="gemini">
|
|
555
|
-
- Use long-context breadth deliberately, then ground conclusions in exact anchors.
|
|
556
|
-
${roleGuidance}
|
|
557
|
-
</model-profile>`;
|
|
558
|
-
}
|
|
559
|
-
if (section.family === "kimi") {
|
|
560
|
-
return `<model-profile family="kimi">
|
|
561
|
-
- Favor repository-scale navigation before edits; keep patches grounded in current file state.
|
|
562
|
-
${roleGuidance}
|
|
563
|
-
</model-profile>`;
|
|
564
|
-
}
|
|
565
|
-
return `<model-profile family="glm">
|
|
566
|
-
- Use compact checklists, conservative steps, clear verification, and concrete blockers.
|
|
567
|
-
${roleGuidance}
|
|
568
|
-
</model-profile>`;
|
|
569
|
-
}
|
|
570
|
-
function renderRoleText(section, dialect) {
|
|
571
|
-
return section.template.replaceAll("{{delegationTool}}", dialect.tools.delegationTool).replaceAll(
|
|
572
|
-
"{{backgroundDelegationTool}}",
|
|
573
|
-
dialect.tools.backgroundDelegationTool ?? dialect.tools.delegationTool
|
|
574
|
-
).replaceAll(
|
|
575
|
-
"{{backgroundStatusTool}}",
|
|
576
|
-
dialect.tools.backgroundStatusTool ?? dialect.tools.hostStatusSurface ?? ""
|
|
577
|
-
).replaceAll("{{userQuestionTool}}", dialect.tools.userQuestionTool).replaceAll("{{progressTool}}", dialect.tools.progressTool).replaceAll("{{dispatch.task}}", dialect.dispatchLabel("task")).replaceAll(
|
|
578
|
-
"{{dispatch.synchronous-task-only}}",
|
|
579
|
-
dialect.dispatchLabel("synchronous-task-only")
|
|
580
|
-
).replace(
|
|
581
|
-
/\{\{role\.(\w+)\}\}/g,
|
|
582
|
-
(_match, role) => dialect.renderRoleInvocation(role)
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
function renderPromptSection(section, dialect) {
|
|
586
|
-
switch (section.kind) {
|
|
587
|
-
case "question-protocol":
|
|
588
|
-
return renderQuestionProtocol(section, dialect);
|
|
589
|
-
case "subagent-rules":
|
|
590
|
-
return renderSubagentRules(section, dialect);
|
|
591
|
-
case "response-budget":
|
|
592
|
-
return renderResponseBudget();
|
|
593
|
-
case "step-budget":
|
|
594
|
-
return renderStepBudget(section);
|
|
595
|
-
case "model-family":
|
|
596
|
-
return renderModelFamily(section);
|
|
597
|
-
case "role-text":
|
|
598
|
-
return renderRoleText(section, dialect);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
function renderRolePrompt(sections, dialect) {
|
|
602
|
-
return sections.map((section) => renderPromptSection(section, dialect).trim()).filter(Boolean).join("\n\n");
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// src/agents/prompt-utils.ts
|
|
606
|
-
var QUESTION_PROTOCOL = renderPromptSection(
|
|
607
|
-
createQuestionProtocolSection(),
|
|
608
|
-
OPENCODE_PROMPT_DIALECT
|
|
609
|
-
);
|
|
610
|
-
var SUBAGENT_RULES = renderPromptSection(
|
|
611
|
-
createSubagentRulesSection("base"),
|
|
612
|
-
OPENCODE_PROMPT_DIALECT
|
|
613
|
-
);
|
|
614
|
-
var SUBAGENT_RULES_READONLY = renderPromptSection(
|
|
615
|
-
createSubagentRulesSection("readonly"),
|
|
616
|
-
OPENCODE_PROMPT_DIALECT
|
|
617
|
-
);
|
|
618
|
-
var SUBAGENT_RULES_WRITABLE = renderPromptSection(
|
|
619
|
-
createSubagentRulesSection("writable"),
|
|
620
|
-
OPENCODE_PROMPT_DIALECT
|
|
621
|
-
);
|
|
622
|
-
var RESPONSE_BUDGET = renderPromptSection(
|
|
623
|
-
createResponseBudgetSection(),
|
|
624
|
-
OPENCODE_PROMPT_DIALECT
|
|
625
|
-
);
|
|
626
|
-
function trimPromptSection(section) {
|
|
627
|
-
const trimmed = section?.trim();
|
|
628
|
-
return trimmed ? trimmed : void 0;
|
|
629
|
-
}
|
|
630
|
-
function appendPromptSections(...sections) {
|
|
631
|
-
return sections.map(trimPromptSection).filter(Boolean).join("\n\n");
|
|
632
|
-
}
|
|
633
|
-
function replacePromptPlaceholders(template, placeholders = {}) {
|
|
634
|
-
return Object.entries(placeholders).reduce((prompt, [key, value]) => {
|
|
635
|
-
if (value === void 0) {
|
|
636
|
-
return prompt;
|
|
637
|
-
}
|
|
638
|
-
return prompt.replaceAll(`{{${key}}}`, String(value));
|
|
639
|
-
}, template);
|
|
640
|
-
}
|
|
641
|
-
function composeAgentPrompt({
|
|
642
|
-
basePrompt,
|
|
643
|
-
customPrompt,
|
|
644
|
-
customAppendPrompt,
|
|
645
|
-
placeholders
|
|
646
|
-
}) {
|
|
647
|
-
const resolvedBase = replacePromptPlaceholders(basePrompt, placeholders);
|
|
648
|
-
if (customPrompt) {
|
|
649
|
-
return replacePromptPlaceholders(customPrompt, placeholders);
|
|
650
|
-
}
|
|
651
|
-
return appendPromptSections(
|
|
652
|
-
resolvedBase,
|
|
653
|
-
replacePromptPlaceholders(customAppendPrompt ?? "", placeholders)
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// src/config/constants.ts
|
|
658
|
-
var AGENT_ALIASES = {
|
|
659
|
-
explore: "explorer",
|
|
660
|
-
"frontend-ui-ux-engineer": "designer"
|
|
661
|
-
};
|
|
662
|
-
var DEFAULT_MODELS = {
|
|
663
|
-
orchestrator: void 0,
|
|
664
|
-
oracle: "openai/gpt-5.4",
|
|
665
|
-
librarian: "openai/gpt-5.4-mini",
|
|
666
|
-
explorer: "openai/gpt-5.4-mini",
|
|
667
|
-
designer: "openai/gpt-5.4-mini",
|
|
668
|
-
quick: "openai/gpt-5.4-mini",
|
|
669
|
-
deep: "openai/gpt-5.4"
|
|
670
|
-
};
|
|
671
|
-
var DEFAULT_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
672
|
-
var MAX_POLL_TIME_MS = 5 * 60 * 1e3;
|
|
673
|
-
var DEFAULT_THOTH_COMMAND = ["npx", "-y", "thoth-mem"];
|
|
674
|
-
|
|
675
|
-
// src/config/loader.ts
|
|
676
|
-
import * as fs from "fs";
|
|
677
|
-
import * as path from "path";
|
|
678
|
-
|
|
679
|
-
// src/cli/config-io.ts
|
|
680
|
-
import {
|
|
681
|
-
copyFileSync,
|
|
682
|
-
existsSync as existsSync2,
|
|
683
|
-
readFileSync,
|
|
684
|
-
renameSync,
|
|
685
|
-
statSync,
|
|
686
|
-
writeFileSync
|
|
687
|
-
} from "fs";
|
|
688
|
-
|
|
689
|
-
// src/cli/paths.ts
|
|
690
|
-
import { existsSync, mkdirSync } from "fs";
|
|
691
|
-
import { homedir } from "os";
|
|
692
|
-
import { dirname, join } from "path";
|
|
693
|
-
function getDefaultOpenCodeConfigDir() {
|
|
694
|
-
const userConfigDir = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config");
|
|
695
|
-
return join(userConfigDir, "opencode");
|
|
696
|
-
}
|
|
697
|
-
function getCustomOpenCodeConfigDir() {
|
|
698
|
-
const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
699
|
-
return configDir || void 0;
|
|
700
|
-
}
|
|
701
|
-
function getConfigDir() {
|
|
702
|
-
const customConfigDir = getCustomOpenCodeConfigDir();
|
|
703
|
-
if (customConfigDir) {
|
|
704
|
-
return customConfigDir;
|
|
705
|
-
}
|
|
706
|
-
return getDefaultOpenCodeConfigDir();
|
|
707
|
-
}
|
|
708
|
-
function getOpenCodeConfigPaths() {
|
|
709
|
-
const configDir = getDefaultOpenCodeConfigDir();
|
|
710
|
-
return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
|
|
711
|
-
}
|
|
712
|
-
function getConfigJson() {
|
|
713
|
-
return getOpenCodeConfigPaths()[0];
|
|
714
|
-
}
|
|
715
|
-
function getConfigJsonc() {
|
|
716
|
-
return getOpenCodeConfigPaths()[1];
|
|
717
|
-
}
|
|
718
|
-
function getLiteConfig() {
|
|
719
|
-
return join(getConfigDir(), "thoth-agents.json");
|
|
720
|
-
}
|
|
721
|
-
function getLiteConfigJsonc() {
|
|
722
|
-
return join(getConfigDir(), "thoth-agents.jsonc");
|
|
723
|
-
}
|
|
724
|
-
function getExistingLiteConfigPath() {
|
|
725
|
-
const jsonPath = getLiteConfig();
|
|
726
|
-
if (existsSync(jsonPath)) return jsonPath;
|
|
727
|
-
const jsoncPath = getLiteConfigJsonc();
|
|
728
|
-
if (existsSync(jsoncPath)) return jsoncPath;
|
|
729
|
-
return jsonPath;
|
|
730
|
-
}
|
|
731
|
-
function getExistingConfigPath() {
|
|
732
|
-
const jsonPath = getConfigJson();
|
|
733
|
-
if (existsSync(jsonPath)) return jsonPath;
|
|
734
|
-
const jsoncPath = getConfigJsonc();
|
|
735
|
-
if (existsSync(jsoncPath)) return jsoncPath;
|
|
736
|
-
return jsonPath;
|
|
737
|
-
}
|
|
738
|
-
function ensureConfigDir() {
|
|
739
|
-
const configDir = getConfigDir();
|
|
740
|
-
if (!existsSync(configDir)) {
|
|
741
|
-
mkdirSync(configDir, { recursive: true });
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
function ensureOpenCodeConfigDir() {
|
|
745
|
-
const configDir = dirname(getConfigJson());
|
|
746
|
-
if (!existsSync(configDir)) {
|
|
747
|
-
mkdirSync(configDir, { recursive: true });
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// src/cli/providers.ts
|
|
752
|
-
var MODEL_MAPPINGS = {
|
|
753
|
-
openai: {
|
|
754
|
-
orchestrator: { model: "openai/gpt-5.4" },
|
|
755
|
-
oracle: { model: "openai/gpt-5.4", variant: "high" },
|
|
756
|
-
librarian: { model: "openai/gpt-5.4-mini", variant: "low" },
|
|
757
|
-
explorer: { model: "openai/gpt-5.4-mini", variant: "low" },
|
|
758
|
-
designer: { model: "openai/gpt-5.4-mini", variant: "medium" },
|
|
759
|
-
quick: { model: "openai/gpt-5.4-mini", variant: "low" },
|
|
760
|
-
deep: { model: "openai/gpt-5.4", variant: "high" }
|
|
761
|
-
},
|
|
762
|
-
kimi: {
|
|
763
|
-
orchestrator: { model: "kimi-for-coding/k2p5" },
|
|
764
|
-
oracle: { model: "kimi-for-coding/k2p5", variant: "high" },
|
|
765
|
-
librarian: { model: "kimi-for-coding/k2p5", variant: "low" },
|
|
766
|
-
explorer: { model: "kimi-for-coding/k2p5", variant: "low" },
|
|
767
|
-
designer: { model: "kimi-for-coding/k2p5", variant: "medium" },
|
|
768
|
-
quick: { model: "kimi-for-coding/k2p5", variant: "low" },
|
|
769
|
-
deep: { model: "kimi-for-coding/k2p5", variant: "high" }
|
|
770
|
-
},
|
|
771
|
-
copilot: {
|
|
772
|
-
orchestrator: { model: "github-copilot/claude-opus-4.6" },
|
|
773
|
-
oracle: { model: "github-copilot/claude-opus-4.6", variant: "high" },
|
|
774
|
-
librarian: { model: "github-copilot/grok-code-fast-1", variant: "low" },
|
|
775
|
-
explorer: { model: "github-copilot/grok-code-fast-1", variant: "low" },
|
|
776
|
-
designer: {
|
|
777
|
-
model: "github-copilot/gemini-3.1-pro-preview",
|
|
778
|
-
variant: "medium"
|
|
779
|
-
},
|
|
780
|
-
quick: { model: "github-copilot/claude-sonnet-4.6", variant: "low" },
|
|
781
|
-
deep: { model: "github-copilot/claude-opus-4.6", variant: "high" }
|
|
782
|
-
},
|
|
783
|
-
"zai-plan": {
|
|
784
|
-
orchestrator: { model: "zai-coding-plan/glm-5" },
|
|
785
|
-
oracle: { model: "zai-coding-plan/glm-5", variant: "high" },
|
|
786
|
-
librarian: { model: "zai-coding-plan/glm-5", variant: "low" },
|
|
787
|
-
explorer: { model: "zai-coding-plan/glm-5", variant: "low" },
|
|
788
|
-
designer: { model: "zai-coding-plan/glm-5", variant: "medium" },
|
|
789
|
-
quick: { model: "zai-coding-plan/glm-5", variant: "low" },
|
|
790
|
-
deep: { model: "zai-coding-plan/glm-5", variant: "high" }
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
function generateLiteConfig(installConfig) {
|
|
794
|
-
const config = {
|
|
795
|
-
preset: "openai",
|
|
796
|
-
presets: {}
|
|
797
|
-
};
|
|
798
|
-
const createAgentConfig = (modelInfo) => {
|
|
799
|
-
return {
|
|
800
|
-
model: modelInfo.model,
|
|
801
|
-
variant: modelInfo.variant
|
|
802
|
-
};
|
|
803
|
-
};
|
|
804
|
-
const buildPreset = (mappingName) => {
|
|
805
|
-
const mapping = MODEL_MAPPINGS[mappingName];
|
|
806
|
-
return Object.fromEntries(
|
|
807
|
-
Object.entries(mapping).map(([agentName, modelInfo]) => [
|
|
808
|
-
agentName,
|
|
809
|
-
createAgentConfig(modelInfo)
|
|
810
|
-
])
|
|
811
|
-
);
|
|
812
|
-
};
|
|
813
|
-
config.presets.openai = buildPreset("openai");
|
|
814
|
-
if (installConfig.hasTmux) {
|
|
815
|
-
config.tmux = {
|
|
816
|
-
enabled: true,
|
|
817
|
-
layout: "main-vertical",
|
|
818
|
-
main_pane_size: 60
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
return config;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// src/cli/config-io.ts
|
|
825
|
-
var PACKAGE_NAME = "thoth-agents";
|
|
826
|
-
function stripJsonComments(json) {
|
|
827
|
-
const commentPattern = /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g;
|
|
828
|
-
const trailingCommaPattern = /\\"|"(?:\\"|[^"])*"|(,)(\s*[}\]])/g;
|
|
829
|
-
return json.replace(
|
|
830
|
-
commentPattern,
|
|
831
|
-
(match, commentGroup) => commentGroup ? "" : match
|
|
832
|
-
).replace(
|
|
833
|
-
trailingCommaPattern,
|
|
834
|
-
(match, comma, closing) => comma ? closing : match
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
function parseConfigFile(path3) {
|
|
838
|
-
try {
|
|
839
|
-
if (!existsSync2(path3)) return { config: null };
|
|
840
|
-
const stat = statSync(path3);
|
|
841
|
-
if (stat.size === 0) return { config: null };
|
|
842
|
-
const content = readFileSync(path3, "utf-8");
|
|
843
|
-
if (content.trim().length === 0) return { config: null };
|
|
844
|
-
return { config: JSON.parse(stripJsonComments(content)) };
|
|
845
|
-
} catch (err) {
|
|
846
|
-
return { config: null, error: String(err) };
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
function parseConfig(path3) {
|
|
850
|
-
const result = parseConfigFile(path3);
|
|
851
|
-
if (result.config || result.error) return result;
|
|
852
|
-
if (path3.endsWith(".json")) {
|
|
853
|
-
const jsoncPath = path3.replace(/\.json$/, ".jsonc");
|
|
854
|
-
return parseConfigFile(jsoncPath);
|
|
855
|
-
}
|
|
856
|
-
return { config: null };
|
|
857
|
-
}
|
|
858
|
-
function writeConfig(configPath, config) {
|
|
859
|
-
if (configPath.endsWith(".jsonc")) {
|
|
860
|
-
console.warn(
|
|
861
|
-
"[config-manager] Writing to .jsonc file - comments will not be preserved"
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
const tmpPath = `${configPath}.tmp`;
|
|
865
|
-
const bakPath = `${configPath}.bak`;
|
|
866
|
-
const content = `${JSON.stringify(config, null, 2)}
|
|
867
|
-
`;
|
|
868
|
-
if (existsSync2(configPath)) {
|
|
869
|
-
copyFileSync(configPath, bakPath);
|
|
870
|
-
}
|
|
871
|
-
writeFileSync(tmpPath, content);
|
|
872
|
-
renameSync(tmpPath, configPath);
|
|
873
|
-
}
|
|
874
|
-
async function addPluginToOpenCodeConfig() {
|
|
875
|
-
const configPath = getExistingConfigPath();
|
|
876
|
-
try {
|
|
877
|
-
ensureOpenCodeConfigDir();
|
|
878
|
-
} catch (err) {
|
|
879
|
-
return {
|
|
880
|
-
success: false,
|
|
881
|
-
configPath,
|
|
882
|
-
error: `Failed to create config directory: ${err}`
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
try {
|
|
886
|
-
const { config: parsedConfig, error } = parseConfig(configPath);
|
|
887
|
-
if (error) {
|
|
888
|
-
return {
|
|
889
|
-
success: false,
|
|
890
|
-
configPath,
|
|
891
|
-
error: `Failed to parse config: ${error}`
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
const config = parsedConfig ?? {};
|
|
895
|
-
const plugins = config.plugin ?? [];
|
|
896
|
-
const filteredPlugins = plugins.filter(
|
|
897
|
-
(p) => p !== PACKAGE_NAME && !p.startsWith(`${PACKAGE_NAME}@`)
|
|
898
|
-
);
|
|
899
|
-
filteredPlugins.push(`${PACKAGE_NAME}@latest`);
|
|
900
|
-
config.plugin = filteredPlugins;
|
|
901
|
-
writeConfig(configPath, config);
|
|
902
|
-
return { success: true, configPath };
|
|
903
|
-
} catch (err) {
|
|
904
|
-
return {
|
|
905
|
-
success: false,
|
|
906
|
-
configPath,
|
|
907
|
-
error: `Failed to update opencode config: ${err}`
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
function writeLiteConfig(installConfig, targetPath) {
|
|
912
|
-
const configPath = targetPath ?? getLiteConfig();
|
|
913
|
-
try {
|
|
914
|
-
ensureConfigDir();
|
|
915
|
-
const config = generateLiteConfig(installConfig);
|
|
916
|
-
const tmpPath = `${configPath}.tmp`;
|
|
917
|
-
const bakPath = `${configPath}.bak`;
|
|
918
|
-
const content = `${JSON.stringify(config, null, 2)}
|
|
919
|
-
`;
|
|
920
|
-
if (existsSync2(configPath)) {
|
|
921
|
-
copyFileSync(configPath, bakPath);
|
|
922
|
-
}
|
|
923
|
-
writeFileSync(tmpPath, content);
|
|
924
|
-
renameSync(tmpPath, configPath);
|
|
925
|
-
return { success: true, configPath };
|
|
926
|
-
} catch (err) {
|
|
927
|
-
return {
|
|
928
|
-
success: false,
|
|
929
|
-
configPath,
|
|
930
|
-
error: `Failed to write lite config: ${err}`
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
function disableDefaultAgents() {
|
|
935
|
-
const configPath = getExistingConfigPath();
|
|
936
|
-
try {
|
|
937
|
-
ensureOpenCodeConfigDir();
|
|
938
|
-
const { config: parsedConfig, error } = parseConfig(configPath);
|
|
939
|
-
if (error) {
|
|
940
|
-
return {
|
|
941
|
-
success: false,
|
|
942
|
-
configPath,
|
|
943
|
-
error: `Failed to parse config: ${error}`
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
const config = parsedConfig ?? {};
|
|
947
|
-
const agent = config.agent ?? {};
|
|
948
|
-
agent.explore = { disable: true };
|
|
949
|
-
agent.general = { disable: true };
|
|
950
|
-
config.agent = agent;
|
|
951
|
-
writeConfig(configPath, config);
|
|
952
|
-
return { success: true, configPath };
|
|
953
|
-
} catch (err) {
|
|
954
|
-
return {
|
|
955
|
-
success: false,
|
|
956
|
-
configPath,
|
|
957
|
-
error: `Failed to disable default agents: ${err}`
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
function detectCurrentConfig() {
|
|
962
|
-
const result = {
|
|
963
|
-
isInstalled: false,
|
|
964
|
-
hasKimi: false,
|
|
965
|
-
hasOpenAI: false,
|
|
966
|
-
hasAnthropic: false,
|
|
967
|
-
hasCopilot: false,
|
|
968
|
-
hasZaiPlan: false,
|
|
969
|
-
hasAntigravity: false,
|
|
970
|
-
hasChutes: false,
|
|
971
|
-
hasOpencodeZen: false,
|
|
972
|
-
hasTmux: false
|
|
973
|
-
};
|
|
974
|
-
const { config } = parseConfig(getExistingConfigPath());
|
|
975
|
-
if (!config) return result;
|
|
976
|
-
const plugins = config.plugin ?? [];
|
|
977
|
-
result.isInstalled = plugins.some((p) => p.startsWith(PACKAGE_NAME));
|
|
978
|
-
result.hasAntigravity = plugins.some(
|
|
979
|
-
(p) => p.startsWith("opencode-antigravity-auth")
|
|
980
|
-
);
|
|
981
|
-
const providers = config.provider;
|
|
982
|
-
result.hasKimi = !!providers?.kimi;
|
|
983
|
-
result.hasAnthropic = !!providers?.anthropic;
|
|
984
|
-
result.hasCopilot = !!providers?.["github-copilot"];
|
|
985
|
-
result.hasZaiPlan = !!providers?.["zai-coding-plan"];
|
|
986
|
-
result.hasChutes = !!providers?.chutes;
|
|
987
|
-
if (providers?.google) result.hasAntigravity = true;
|
|
988
|
-
const { config: liteConfig } = parseConfig(getLiteConfig());
|
|
989
|
-
if (liteConfig && typeof liteConfig === "object") {
|
|
990
|
-
const configObj = liteConfig;
|
|
991
|
-
const presetName = configObj.preset;
|
|
992
|
-
const presets = configObj.presets;
|
|
993
|
-
const agents = presets?.[presetName];
|
|
994
|
-
if (agents) {
|
|
995
|
-
const models = Object.values(agents).map((a) => a?.model).filter(Boolean);
|
|
996
|
-
result.hasOpenAI = models.some((m) => m?.startsWith("openai/"));
|
|
997
|
-
result.hasAnthropic = models.some((m) => m?.startsWith("anthropic/"));
|
|
998
|
-
result.hasCopilot = models.some((m) => m?.startsWith("github-copilot/"));
|
|
999
|
-
result.hasZaiPlan = models.some((m) => m?.startsWith("zai-coding-plan/"));
|
|
1000
|
-
result.hasOpencodeZen = models.some((m) => m?.startsWith("opencode/"));
|
|
1001
|
-
if (models.some((m) => m?.startsWith("google/"))) {
|
|
1002
|
-
result.hasAntigravity = true;
|
|
1003
|
-
}
|
|
1004
|
-
if (models.some((m) => m?.startsWith("chutes/"))) {
|
|
1005
|
-
result.hasChutes = true;
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
if (configObj.tmux && typeof configObj.tmux === "object") {
|
|
1009
|
-
const tmuxConfig = configObj.tmux;
|
|
1010
|
-
result.hasTmux = tmuxConfig.enabled === true;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
return result;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// src/config/schema.ts
|
|
1017
|
-
import { z } from "zod";
|
|
1018
|
-
var AGENT_NAMES = [
|
|
1019
|
-
"orchestrator",
|
|
1020
|
-
"oracle",
|
|
1021
|
-
"designer",
|
|
1022
|
-
"explorer",
|
|
1023
|
-
"librarian",
|
|
1024
|
-
"quick",
|
|
1025
|
-
"deep"
|
|
1026
|
-
];
|
|
1027
|
-
var FALLBACK_AGENT_NAMES = [...AGENT_NAMES];
|
|
1028
|
-
var MANUAL_AGENT_NAMES = [...AGENT_NAMES];
|
|
1029
|
-
var ProviderModelIdSchema = z.string().regex(
|
|
1030
|
-
/^[^/\s]+\/[^\s]+$/,
|
|
1031
|
-
"Expected provider/model format (provider/.../model)"
|
|
1032
|
-
);
|
|
1033
|
-
var ManualAgentPlanSchema = z.object({
|
|
1034
|
-
primary: ProviderModelIdSchema,
|
|
1035
|
-
fallback1: ProviderModelIdSchema,
|
|
1036
|
-
fallback2: ProviderModelIdSchema,
|
|
1037
|
-
fallback3: ProviderModelIdSchema
|
|
1038
|
-
}).superRefine((value, ctx) => {
|
|
1039
|
-
const unique = /* @__PURE__ */ new Set([
|
|
1040
|
-
value.primary,
|
|
1041
|
-
value.fallback1,
|
|
1042
|
-
value.fallback2,
|
|
1043
|
-
value.fallback3
|
|
1044
|
-
]);
|
|
1045
|
-
if (unique.size !== 4) {
|
|
1046
|
-
ctx.addIssue({
|
|
1047
|
-
code: z.ZodIssueCode.custom,
|
|
1048
|
-
message: "primary and fallbacks must be unique per agent"
|
|
1049
|
-
});
|
|
1050
|
-
}
|
|
1051
|
-
});
|
|
1052
|
-
var ManualPlanSchema = z.object({
|
|
1053
|
-
orchestrator: ManualAgentPlanSchema,
|
|
1054
|
-
oracle: ManualAgentPlanSchema,
|
|
1055
|
-
designer: ManualAgentPlanSchema,
|
|
1056
|
-
explorer: ManualAgentPlanSchema,
|
|
1057
|
-
librarian: ManualAgentPlanSchema,
|
|
1058
|
-
quick: ManualAgentPlanSchema,
|
|
1059
|
-
deep: ManualAgentPlanSchema
|
|
1060
|
-
}).strict();
|
|
1061
|
-
var AgentModelChainSchema = z.array(z.string()).min(1);
|
|
1062
|
-
var FallbackChainsSchema = z.object({
|
|
1063
|
-
orchestrator: AgentModelChainSchema.optional(),
|
|
1064
|
-
oracle: AgentModelChainSchema.optional(),
|
|
1065
|
-
designer: AgentModelChainSchema.optional(),
|
|
1066
|
-
explorer: AgentModelChainSchema.optional(),
|
|
1067
|
-
librarian: AgentModelChainSchema.optional(),
|
|
1068
|
-
quick: AgentModelChainSchema.optional(),
|
|
1069
|
-
deep: AgentModelChainSchema.optional()
|
|
1070
|
-
}).catchall(AgentModelChainSchema);
|
|
1071
|
-
var AgentOverrideConfigSchema = z.object({
|
|
1072
|
-
model: z.union([
|
|
1073
|
-
z.string(),
|
|
1074
|
-
z.array(
|
|
1075
|
-
z.union([
|
|
1076
|
-
z.string(),
|
|
1077
|
-
z.object({
|
|
1078
|
-
id: z.string(),
|
|
1079
|
-
variant: z.string().optional()
|
|
1080
|
-
})
|
|
1081
|
-
])
|
|
1082
|
-
)
|
|
1083
|
-
]).optional(),
|
|
1084
|
-
temperature: z.number().min(0).max(2).optional(),
|
|
1085
|
-
steps: z.number().int().min(1).optional(),
|
|
1086
|
-
variant: z.string().optional().catch(void 0)
|
|
1087
|
-
});
|
|
1088
|
-
var TmuxLayoutSchema = z.enum([
|
|
1089
|
-
"main-horizontal",
|
|
1090
|
-
// Main pane on top, agents stacked below
|
|
1091
|
-
"main-vertical",
|
|
1092
|
-
// Main pane on left, agents stacked on right
|
|
1093
|
-
"tiled",
|
|
1094
|
-
// All panes equal size grid
|
|
1095
|
-
"even-horizontal",
|
|
1096
|
-
// All panes side by side
|
|
1097
|
-
"even-vertical"
|
|
1098
|
-
// All panes stacked vertically
|
|
1099
|
-
]);
|
|
1100
|
-
var TmuxConfigSchema = z.object({
|
|
1101
|
-
enabled: z.boolean().default(false),
|
|
1102
|
-
layout: TmuxLayoutSchema.default("main-vertical"),
|
|
1103
|
-
main_pane_size: z.number().min(20).max(80).default(60)
|
|
1104
|
-
// percentage for main pane
|
|
1105
|
-
});
|
|
1106
|
-
var PresetSchema = z.record(z.string(), AgentOverrideConfigSchema);
|
|
1107
|
-
var AgentNameSchema = z.enum(AGENT_NAMES);
|
|
1108
|
-
var McpNameSchema = z.enum([
|
|
1109
|
-
"exa",
|
|
1110
|
-
"context7",
|
|
1111
|
-
"grep_app",
|
|
1112
|
-
"thoth_mem"
|
|
1113
|
-
]);
|
|
1114
|
-
var ThothConfigSchema = z.object({
|
|
1115
|
-
command: z.array(z.string()).optional(),
|
|
1116
|
-
data_dir: z.string().optional(),
|
|
1117
|
-
environment: z.record(z.string(), z.string()).optional(),
|
|
1118
|
-
timeout: z.number().optional(),
|
|
1119
|
-
http_port: z.number().optional()
|
|
1120
|
-
});
|
|
1121
|
-
var ArtifactStoreModeSchema = z.enum([
|
|
1122
|
-
"thoth-mem",
|
|
1123
|
-
"openspec",
|
|
1124
|
-
"hybrid"
|
|
1125
|
-
]);
|
|
1126
|
-
var ArtifactStoreConfigSchema = z.object({
|
|
1127
|
-
mode: ArtifactStoreModeSchema.default("hybrid")
|
|
1128
|
-
});
|
|
1129
|
-
var CodexGenerationConfigSchema = z.object({
|
|
1130
|
-
enabled: z.boolean().default(false),
|
|
1131
|
-
outputRoot: z.string().optional(),
|
|
1132
|
-
dryRun: z.boolean().default(true)
|
|
1133
|
-
});
|
|
1134
|
-
var FailoverConfigSchema = z.object({
|
|
1135
|
-
enabled: z.boolean().default(true),
|
|
1136
|
-
timeoutMs: z.number().min(0).default(15e3),
|
|
1137
|
-
retryDelayMs: z.number().min(0).default(500),
|
|
1138
|
-
chains: FallbackChainsSchema.default({})
|
|
1139
|
-
});
|
|
1140
|
-
var PluginConfigSchema = z.object({
|
|
1141
|
-
preset: z.string().optional(),
|
|
1142
|
-
setDefaultAgent: z.boolean().optional(),
|
|
1143
|
-
scoringEngineVersion: z.enum(["v1", "v2-shadow", "v2"]).optional(),
|
|
1144
|
-
balanceProviderUsage: z.boolean().optional(),
|
|
1145
|
-
manualPlan: ManualPlanSchema.optional(),
|
|
1146
|
-
presets: z.record(z.string(), PresetSchema).optional(),
|
|
1147
|
-
agents: z.record(z.string(), AgentOverrideConfigSchema).optional(),
|
|
1148
|
-
disabled_mcps: z.array(z.string()).optional(),
|
|
1149
|
-
tmux: TmuxConfigSchema.optional(),
|
|
1150
|
-
fallback: FailoverConfigSchema.optional(),
|
|
1151
|
-
thoth: ThothConfigSchema.optional(),
|
|
1152
|
-
artifactStore: ArtifactStoreConfigSchema.optional(),
|
|
1153
|
-
codex: CodexGenerationConfigSchema.optional()
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
// src/config/loader.ts
|
|
1157
|
-
var PROMPTS_DIR_NAME = "thoth-agents";
|
|
1158
|
-
function loadAgentPrompt(agentName, preset) {
|
|
1159
|
-
const presetDirName = preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : void 0;
|
|
1160
|
-
const promptsDir = path.join(getConfigDir(), PROMPTS_DIR_NAME);
|
|
1161
|
-
const promptSearchDirs = presetDirName ? [path.join(promptsDir, presetDirName), promptsDir] : [promptsDir];
|
|
1162
|
-
const result = {};
|
|
1163
|
-
const readFirstPrompt = (fileName, errorPrefix) => {
|
|
1164
|
-
for (const dir of promptSearchDirs) {
|
|
1165
|
-
const promptPath = path.join(dir, fileName);
|
|
1166
|
-
if (!fs.existsSync(promptPath)) {
|
|
1167
|
-
continue;
|
|
1168
|
-
}
|
|
1169
|
-
try {
|
|
1170
|
-
return fs.readFileSync(promptPath, "utf-8");
|
|
1171
|
-
} catch (error) {
|
|
1172
|
-
console.warn(
|
|
1173
|
-
`[thoth-agents] ${errorPrefix} ${promptPath}:`,
|
|
1174
|
-
error instanceof Error ? error.message : String(error)
|
|
1175
|
-
);
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
return void 0;
|
|
1179
|
-
};
|
|
1180
|
-
result.prompt = readFirstPrompt(
|
|
1181
|
-
`${agentName}.md`,
|
|
1182
|
-
"Error reading prompt file"
|
|
1183
|
-
);
|
|
1184
|
-
result.appendPrompt = readFirstPrompt(
|
|
1185
|
-
`${agentName}_append.md`,
|
|
1186
|
-
"Error reading append prompt file"
|
|
1187
|
-
);
|
|
1188
|
-
return result;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
// src/config/utils.ts
|
|
1192
|
-
function getAgentOverride(config, name) {
|
|
1193
|
-
const overrides = config?.agents ?? {};
|
|
1194
|
-
return overrides[name] ?? overrides[Object.keys(AGENT_ALIASES).find((k) => AGENT_ALIASES[k] === name) ?? ""];
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// src/mcp/context7.ts
|
|
1198
|
-
var CONTEXT7_MCP_URL = "https://mcp.context7.com/mcp";
|
|
1199
|
-
var context7 = {
|
|
1200
|
-
type: "remote",
|
|
1201
|
-
url: CONTEXT7_MCP_URL,
|
|
1202
|
-
headers: process.env.CONTEXT7_API_KEY ? { CONTEXT7_API_KEY: process.env.CONTEXT7_API_KEY } : void 0,
|
|
1203
|
-
oauth: false
|
|
1204
|
-
};
|
|
1205
|
-
|
|
1206
|
-
// src/mcp/exa.ts
|
|
1207
|
-
var exa = {
|
|
1208
|
-
type: "local",
|
|
1209
|
-
command: ["npx", "-y", "exa-mcp-server"]
|
|
1210
|
-
};
|
|
1211
|
-
|
|
1212
|
-
// src/mcp/grep-app.ts
|
|
1213
|
-
var GREP_APP_MCP_URL = "https://mcp.grep.app";
|
|
1214
|
-
|
|
1215
|
-
// src/harness/core/agent-pack.ts
|
|
1216
|
-
var AGENT_ROLES = [
|
|
1217
|
-
{
|
|
1218
|
-
name: "orchestrator",
|
|
1219
|
-
mode: "primary-non-mutating",
|
|
1220
|
-
dispatch: "root-coordinator",
|
|
1221
|
-
canMutateWorkspace: false,
|
|
1222
|
-
scope: "coordination, routing, decisions, progress, and root memory",
|
|
1223
|
-
responsibility: "Delegate-first coordinator for SDD workflow, specialist dispatch, and root-session memory ownership.",
|
|
1224
|
-
toolGovernance: [
|
|
1225
|
-
"delegates inspection, writing, debugging, and verification",
|
|
1226
|
-
"owns question prompts and root-session memory",
|
|
1227
|
-
"does not perform inline workspace implementation"
|
|
1228
|
-
],
|
|
1229
|
-
verification: [
|
|
1230
|
-
"routes verification to specialists",
|
|
1231
|
-
"summarizes evidence from changed files, diagnostics, and tests"
|
|
1232
|
-
]
|
|
1233
|
-
},
|
|
1234
|
-
{
|
|
1235
|
-
name: "explorer",
|
|
1236
|
-
mode: "read-only",
|
|
1237
|
-
dispatch: "task",
|
|
1238
|
-
canMutateWorkspace: false,
|
|
1239
|
-
scope: "local repository discovery",
|
|
1240
|
-
responsibility: "Find workspace facts fast and return paths, lines, symbols, constraints, edit targets, and conclusions.",
|
|
1241
|
-
toolGovernance: [
|
|
1242
|
-
"read/search/code-navigation tools only",
|
|
1243
|
-
"no durable memory writes",
|
|
1244
|
-
"no task delegation or progress ownership"
|
|
1245
|
-
],
|
|
1246
|
-
verification: ["reports confidence, anchors, unchecked areas, and gaps"]
|
|
1247
|
-
},
|
|
1248
|
-
{
|
|
1249
|
-
name: "librarian",
|
|
1250
|
-
mode: "read-only",
|
|
1251
|
-
dispatch: "task",
|
|
1252
|
-
canMutateWorkspace: false,
|
|
1253
|
-
scope: "external research plus local confirmation when needed",
|
|
1254
|
-
responsibility: "Gather authoritative external evidence and distinguish official docs from examples.",
|
|
1255
|
-
toolGovernance: [
|
|
1256
|
-
"research and read-only local confirmation tools",
|
|
1257
|
-
"no workspace mutation",
|
|
1258
|
-
"no durable memory writes"
|
|
1259
|
-
],
|
|
1260
|
-
verification: ["sources every substantive external claim with a URL"]
|
|
1261
|
-
},
|
|
1262
|
-
{
|
|
1263
|
-
name: "oracle",
|
|
1264
|
-
mode: "read-only",
|
|
1265
|
-
dispatch: "synchronous-task-only",
|
|
1266
|
-
canMutateWorkspace: false,
|
|
1267
|
-
scope: "advice, diagnosis, architecture, code review, and plan review",
|
|
1268
|
-
responsibility: "Provide strategic technical guidance anchored to evidence and review SDD plans.",
|
|
1269
|
-
toolGovernance: [
|
|
1270
|
-
"read-only analysis and review",
|
|
1271
|
-
"no implementation or artifact-producing SDD phases",
|
|
1272
|
-
"no task delegation"
|
|
1273
|
-
],
|
|
1274
|
-
verification: ["separates observations, risks, and recommendations"]
|
|
1275
|
-
},
|
|
1276
|
-
{
|
|
1277
|
-
name: "designer",
|
|
1278
|
-
mode: "write-capable",
|
|
1279
|
-
dispatch: "synchronous-task-only",
|
|
1280
|
-
canMutateWorkspace: true,
|
|
1281
|
-
scope: "UI/UX decisions, implementation, and visual verification",
|
|
1282
|
-
responsibility: "Own user-facing implementation choices and visual QA for UI work.",
|
|
1283
|
-
toolGovernance: [
|
|
1284
|
-
"may edit focused UI/UX files",
|
|
1285
|
-
"owns screenshots and visual QA",
|
|
1286
|
-
"does not delegate or own SDD progress"
|
|
1287
|
-
],
|
|
1288
|
-
verification: ["includes visual verification status when applicable"]
|
|
1289
|
-
},
|
|
1290
|
-
{
|
|
1291
|
-
name: "quick",
|
|
1292
|
-
mode: "write-capable",
|
|
1293
|
-
dispatch: "synchronous-task-only",
|
|
1294
|
-
canMutateWorkspace: true,
|
|
1295
|
-
scope: "fast bounded implementation",
|
|
1296
|
-
responsibility: "Implement well-defined narrow or mechanical changes with focused verification.",
|
|
1297
|
-
toolGovernance: [
|
|
1298
|
-
"may edit bounded targets",
|
|
1299
|
-
"does not perform broad rediscovery",
|
|
1300
|
-
"does not delegate or own SDD progress"
|
|
1301
|
-
],
|
|
1302
|
-
verification: ["runs the smallest sufficient focused check"]
|
|
1303
|
-
},
|
|
1304
|
-
{
|
|
1305
|
-
name: "deep",
|
|
1306
|
-
mode: "write-capable",
|
|
1307
|
-
dispatch: "synchronous-task-only",
|
|
1308
|
-
canMutateWorkspace: true,
|
|
1309
|
-
scope: "thorough implementation and verification",
|
|
1310
|
-
responsibility: "Handle correctness-critical, multi-file, or edge-case-heavy changes with full local context analysis.",
|
|
1311
|
-
toolGovernance: [
|
|
1312
|
-
"may edit implementation and tests",
|
|
1313
|
-
"validates shared behavior against related code and call sites",
|
|
1314
|
-
"does not delegate or own SDD progress"
|
|
1315
|
-
],
|
|
1316
|
-
verification: ["does not skip verification and reports edge-case evidence"]
|
|
1317
|
-
}
|
|
1318
|
-
];
|
|
1319
|
-
var DELEGATE_FIRST_RULES = [
|
|
1320
|
-
"The orchestrator coordinates, decides, asks blocking questions, and delegates evidence or action.",
|
|
1321
|
-
"Explorer, librarian, and oracle remain read-only specialists.",
|
|
1322
|
-
"Designer, quick, and deep are write-capable leaf agents with synchronous task dispatch.",
|
|
1323
|
-
"Subagents return findings, diffs, verification, and blockers rather than raw file dumps.",
|
|
1324
|
-
"No leaf agent owns SDD progress checkboxes or orchestrator-only memory."
|
|
1325
|
-
];
|
|
1326
|
-
var VERIFICATION_PROTOCOL = [
|
|
1327
|
-
"Completion reports include changed files and verification evidence.",
|
|
1328
|
-
"Behavior changes require the smallest sufficient automated check or an explicitly documented check.",
|
|
1329
|
-
"Visual changes require designer-owned visual QA when feasible."
|
|
1330
|
-
];
|
|
1331
|
-
var AGENT_PACK_CONTRACT = {
|
|
1332
|
-
roles: [...AGENT_ROLES],
|
|
1333
|
-
delegateFirstRules: [...DELEGATE_FIRST_RULES],
|
|
1334
|
-
verificationProtocol: [...VERIFICATION_PROTOCOL]
|
|
1335
|
-
};
|
|
1336
|
-
function getAgentPackContract() {
|
|
1337
|
-
return {
|
|
1338
|
-
roles: AGENT_PACK_CONTRACT.roles.map((role) => ({
|
|
1339
|
-
...role,
|
|
1340
|
-
toolGovernance: [...role.toolGovernance],
|
|
1341
|
-
verification: [...role.verification]
|
|
1342
|
-
})),
|
|
1343
|
-
delegateFirstRules: [...AGENT_PACK_CONTRACT.delegateFirstRules],
|
|
1344
|
-
verificationProtocol: [...AGENT_PACK_CONTRACT.verificationProtocol]
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
// src/harness/core/memory-governance.ts
|
|
1349
|
-
var ROOT_OWNED_TOOLS = [
|
|
1350
|
-
"mem_session_start",
|
|
1351
|
-
"mem_session_summary",
|
|
1352
|
-
"mem_save_prompt"
|
|
1353
|
-
];
|
|
1354
|
-
var READ_RECALL_CHAIN = [
|
|
1355
|
-
"mem_search",
|
|
1356
|
-
"mem_timeline",
|
|
1357
|
-
"mem_get_observation"
|
|
1358
|
-
];
|
|
1359
|
-
var WRITE_CAPABLE_DELEGATED_TOOLS = [
|
|
1360
|
-
"mem_save",
|
|
1361
|
-
"mem_search",
|
|
1362
|
-
"mem_get_observation",
|
|
1363
|
-
"mem_timeline",
|
|
1364
|
-
"mem_suggest_topic_key"
|
|
1365
|
-
];
|
|
1366
|
-
var ALL_MEMORY_TOOLS = [
|
|
1367
|
-
...ROOT_OWNED_TOOLS,
|
|
1368
|
-
...READ_RECALL_CHAIN,
|
|
1369
|
-
"mem_suggest_topic_key",
|
|
1370
|
-
"mem_save"
|
|
1371
|
-
];
|
|
1372
|
-
function uniqueTools(tools) {
|
|
1373
|
-
return [...new Set(tools)];
|
|
1374
|
-
}
|
|
1375
|
-
function roleAllowedTools(role) {
|
|
1376
|
-
if (role.name === "orchestrator") {
|
|
1377
|
-
return uniqueTools([...ROOT_OWNED_TOOLS, ...WRITE_CAPABLE_DELEGATED_TOOLS]);
|
|
1378
|
-
}
|
|
1379
|
-
if (role.mode === "read-only") {
|
|
1380
|
-
return [...READ_RECALL_CHAIN];
|
|
1381
|
-
}
|
|
1382
|
-
return [...WRITE_CAPABLE_DELEGATED_TOOLS];
|
|
1383
|
-
}
|
|
1384
|
-
function roleRules(role) {
|
|
1385
|
-
const sharedSubagentRules = [
|
|
1386
|
-
"Every subagent memory call requires the parent session_id and project from dispatch; if either is missing, do not call thoth-mem.",
|
|
1387
|
-
"Never call mem_session_start, mem_session_summary, or mem_save_prompt; those tools are root/orchestrator-owned.",
|
|
1388
|
-
"Protect the sdd/* topic namespace; SDD artifacts may use deterministic sdd/{change}/{artifact} topic keys only in persistence modes that include thoth-mem."
|
|
1389
|
-
];
|
|
1390
|
-
if (role.name === "orchestrator") {
|
|
1391
|
-
return [
|
|
1392
|
-
"mem_session_start, mem_session_summary, and mem_save_prompt are root/main orchestrator-owned tools and responsibilities.",
|
|
1393
|
-
"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.",
|
|
1394
|
-
"Dispatch parent session_id and project when authorizing subagent memory use.",
|
|
1395
|
-
"Protect the sdd/* topic namespace and write SDD memory artifacts only in thoth-mem or hybrid persistence modes."
|
|
1396
|
-
];
|
|
1397
|
-
}
|
|
1398
|
-
if (role.mode === "read-only") {
|
|
1399
|
-
return [
|
|
1400
|
-
...sharedSubagentRules,
|
|
1401
|
-
"Read-only agents may only perform bounded, project-scoped recall with mem_search -> mem_timeline -> mem_get_observation when authorized.",
|
|
1402
|
-
"Read-only agents must never write durable memory."
|
|
1403
|
-
];
|
|
1404
|
-
}
|
|
1405
|
-
return [
|
|
1406
|
-
...sharedSubagentRules,
|
|
1407
|
-
"Write-capable agents may call mem_save only for delegated durable observations under the parent session/project.",
|
|
1408
|
-
"For reads, use only mem_search -> mem_timeline -> mem_get_observation."
|
|
1409
|
-
];
|
|
1410
|
-
}
|
|
1411
|
-
function getRoleMemoryGovernance(role) {
|
|
1412
|
-
const allowedTools = roleAllowedTools(role);
|
|
1413
|
-
return {
|
|
1414
|
-
role: role.name,
|
|
1415
|
-
rootOwnedTools: [...ROOT_OWNED_TOOLS],
|
|
1416
|
-
allowedTools,
|
|
1417
|
-
forbiddenTools: ALL_MEMORY_TOOLS.filter(
|
|
1418
|
-
(tool) => !allowedTools.includes(tool)
|
|
1419
|
-
),
|
|
1420
|
-
requiresParentContext: role.name !== "orchestrator",
|
|
1421
|
-
mayReadProjectMemory: role.name !== "orchestrator",
|
|
1422
|
-
mayWriteDurableObservations: role.mode === "write-capable",
|
|
1423
|
-
protectsSddNamespace: true,
|
|
1424
|
-
rules: roleRules(role)
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
|
-
function renderMemoryGovernanceInstructions(role, dialect) {
|
|
1428
|
-
const governance = getRoleMemoryGovernance(role);
|
|
1429
|
-
const harnessRules = dialect ? [
|
|
1430
|
-
`- Harness wording: use ${dialect.renderRoleInvocation("orchestrator")} as the memory owner and \`${dialect.tools.userQuestionTool}\` for blocking memory-context questions.`,
|
|
1431
|
-
`- Progress ownership remains with the coordinator; report memory-governance verification for tracking in ${dialect.tools.progressTool}.`
|
|
1432
|
-
] : [];
|
|
1433
|
-
return [
|
|
1434
|
-
"thoth-mem governance:",
|
|
1435
|
-
...governance.rules.map((rule) => `- ${rule}`),
|
|
1436
|
-
...harnessRules,
|
|
1437
|
-
`- Runtime enforcement: ${role.name === "orchestrator" ? "root-owned" : "instruction-level unless the target harness validates per-agent memory controls"}.`
|
|
1438
|
-
].join("\n");
|
|
1439
|
-
}
|
|
1440
|
-
function memoryGovernanceDiagnostics(input) {
|
|
1441
|
-
const diagnostics = [];
|
|
1442
|
-
if (input.permissionControls !== "supported") {
|
|
1443
|
-
diagnostics.push({
|
|
1444
|
-
severity: "warning",
|
|
1445
|
-
code: `${input.harness}.permission.memory.enforcement_gap`,
|
|
1446
|
-
message: "Runtime controls for root-only memory tools are unavailable; governance is rendered as instruction-level guidance.",
|
|
1447
|
-
harness: input.harness,
|
|
1448
|
-
capability: "rolePermissions",
|
|
1449
|
-
fallback: "instruction-only"
|
|
1450
|
-
});
|
|
1451
|
-
}
|
|
1452
|
-
if (input.parentContextInjection !== "supported") {
|
|
1453
|
-
diagnostics.push({
|
|
1454
|
-
severity: input.parentContextInjection === "unknown" ? "error" : "warning",
|
|
1455
|
-
code: `${input.harness}.context.parent_injection.unvalidated`,
|
|
1456
|
-
message: "Parent session_id/project injection is not runtime-enforced; subagents must be instructed not to use memory without explicit dispatch context.",
|
|
1457
|
-
harness: input.harness,
|
|
1458
|
-
capability: "parentContextInjection",
|
|
1459
|
-
fallback: "instruction-only"
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
if (input.memoryWriteControls !== "supported") {
|
|
1463
|
-
diagnostics.push({
|
|
1464
|
-
severity: "warning",
|
|
1465
|
-
code: `${input.harness}.permission.memory_write.enforcement_gap`,
|
|
1466
|
-
message: "Runtime controls for delegated memory writes are unavailable; write-capable agents receive instruction-level mem_save limits only.",
|
|
1467
|
-
harness: input.harness,
|
|
1468
|
-
capability: "memoryGovernanceEnforcement",
|
|
1469
|
-
fallback: "instruction-only"
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
return diagnostics;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// src/harness/core/skills.ts
|
|
1476
|
-
var ORCHESTRATOR_ONLY = ["orchestrator"];
|
|
1477
|
-
var SHARED_SKILL_SUPPORT = {
|
|
1478
|
-
name: "_shared",
|
|
1479
|
-
description: "Shared OpenSpec, persistence, and thoth-mem SDD conventions",
|
|
1480
|
-
allowedRoles: [
|
|
1481
|
-
"orchestrator",
|
|
1482
|
-
"explorer",
|
|
1483
|
-
"librarian",
|
|
1484
|
-
"oracle",
|
|
1485
|
-
"designer",
|
|
1486
|
-
"quick",
|
|
1487
|
-
"deep"
|
|
1488
|
-
],
|
|
1489
|
-
sourcePath: "src/skills/_shared",
|
|
1490
|
-
kind: "shared-support",
|
|
1491
|
-
purpose: "support"
|
|
1492
|
-
};
|
|
1493
|
-
var BUNDLED_SKILL_REGISTRY = [
|
|
1494
|
-
{
|
|
1495
|
-
name: "requirements-interview",
|
|
1496
|
-
description: "Mandatory step-0 discovery interview to understand user intent, clarify scope, and choose the right path before implementation",
|
|
1497
|
-
allowedRoles: ORCHESTRATOR_ONLY,
|
|
1498
|
-
sourcePath: "src/skills/requirements-interview",
|
|
1499
|
-
kind: "skill",
|
|
1500
|
-
purpose: "requirements"
|
|
1501
|
-
},
|
|
1502
|
-
{
|
|
1503
|
-
name: "thoth-mem-agents",
|
|
1504
|
-
description: "Orchestrator/subagent thoth-mem workflow contract for parent session_id/project ownership, prompt-save prohibitions, and safe durable memory usage",
|
|
1505
|
-
allowedRoles: [
|
|
1506
|
-
"orchestrator",
|
|
1507
|
-
"explorer",
|
|
1508
|
-
"librarian",
|
|
1509
|
-
"oracle",
|
|
1510
|
-
"designer",
|
|
1511
|
-
"quick",
|
|
1512
|
-
"deep"
|
|
1513
|
-
],
|
|
1514
|
-
sourcePath: "src/skills/thoth-mem-agents",
|
|
1515
|
-
kind: "skill",
|
|
1516
|
-
purpose: "memory"
|
|
1517
|
-
},
|
|
1518
|
-
{
|
|
1519
|
-
name: "plan-reviewer",
|
|
1520
|
-
description: "Review SDD task plans for execution blockers and valid references",
|
|
1521
|
-
allowedRoles: ["orchestrator", "oracle"],
|
|
1522
|
-
sourcePath: "src/skills/plan-reviewer",
|
|
1523
|
-
kind: "skill",
|
|
1524
|
-
purpose: "review"
|
|
1525
|
-
},
|
|
1526
|
-
{
|
|
1527
|
-
name: "sdd-init",
|
|
1528
|
-
description: "Initialize OpenSpec structure and SDD project context",
|
|
1529
|
-
allowedRoles: ORCHESTRATOR_ONLY,
|
|
1530
|
-
sourcePath: "src/skills/sdd-init",
|
|
1531
|
-
kind: "skill",
|
|
1532
|
-
purpose: "sdd"
|
|
1533
|
-
},
|
|
1534
|
-
{
|
|
1535
|
-
name: "sdd-propose",
|
|
1536
|
-
description: "Create change proposals for OpenSpec workflows",
|
|
1537
|
-
allowedRoles: ORCHESTRATOR_ONLY,
|
|
1538
|
-
sourcePath: "src/skills/sdd-propose",
|
|
1539
|
-
kind: "skill",
|
|
1540
|
-
purpose: "sdd"
|
|
1541
|
-
},
|
|
1542
|
-
{
|
|
1543
|
-
name: "sdd-spec",
|
|
1544
|
-
description: "Write OpenSpec delta specifications",
|
|
1545
|
-
allowedRoles: ORCHESTRATOR_ONLY,
|
|
1546
|
-
sourcePath: "src/skills/sdd-spec",
|
|
1547
|
-
kind: "skill",
|
|
1548
|
-
purpose: "sdd"
|
|
1549
|
-
},
|
|
1550
|
-
{
|
|
1551
|
-
name: "sdd-design",
|
|
1552
|
-
description: "Create technical solution design artifacts for changes",
|
|
1553
|
-
allowedRoles: ORCHESTRATOR_ONLY,
|
|
1554
|
-
sourcePath: "src/skills/sdd-design",
|
|
1555
|
-
kind: "skill",
|
|
1556
|
-
purpose: "sdd"
|
|
1557
|
-
},
|
|
1558
|
-
{
|
|
1559
|
-
name: "sdd-tasks",
|
|
1560
|
-
description: "Generate phased implementation task checklists",
|
|
1561
|
-
allowedRoles: ORCHESTRATOR_ONLY,
|
|
1562
|
-
sourcePath: "src/skills/sdd-tasks",
|
|
1563
|
-
kind: "skill",
|
|
1564
|
-
purpose: "sdd"
|
|
1565
|
-
},
|
|
1566
|
-
{
|
|
1567
|
-
name: "sdd-apply",
|
|
1568
|
-
description: "Execute tasks and persist implementation progress",
|
|
1569
|
-
allowedRoles: ORCHESTRATOR_ONLY,
|
|
1570
|
-
sourcePath: "src/skills/sdd-apply",
|
|
1571
|
-
kind: "skill",
|
|
1572
|
-
purpose: "sdd"
|
|
1573
|
-
},
|
|
1574
|
-
{
|
|
1575
|
-
name: "executing-plans",
|
|
1576
|
-
description: "Execute SDD task lists with real-time progress tracking, sub-agent dispatch, and verification checkpoints",
|
|
1577
|
-
allowedRoles: ORCHESTRATOR_ONLY,
|
|
1578
|
-
sourcePath: "src/skills/executing-plans",
|
|
1579
|
-
kind: "skill",
|
|
1580
|
-
purpose: "sdd"
|
|
1581
|
-
},
|
|
1582
|
-
{
|
|
1583
|
-
name: "sdd-verify",
|
|
1584
|
-
description: "Build verification reports and compliance matrices",
|
|
1585
|
-
allowedRoles: ORCHESTRATOR_ONLY,
|
|
1586
|
-
sourcePath: "src/skills/sdd-verify",
|
|
1587
|
-
kind: "skill",
|
|
1588
|
-
purpose: "sdd"
|
|
1589
|
-
},
|
|
1590
|
-
{
|
|
1591
|
-
name: "sdd-archive",
|
|
1592
|
-
description: "Archive completed OpenSpec changes with audit trails",
|
|
1593
|
-
allowedRoles: ORCHESTRATOR_ONLY,
|
|
1594
|
-
sourcePath: "src/skills/sdd-archive",
|
|
1595
|
-
kind: "skill",
|
|
1596
|
-
purpose: "sdd"
|
|
1597
|
-
}
|
|
1598
|
-
];
|
|
1599
|
-
var SKILL_REGISTRY = [
|
|
1600
|
-
...BUNDLED_SKILL_REGISTRY,
|
|
1601
|
-
SHARED_SKILL_SUPPORT
|
|
1602
|
-
];
|
|
1603
|
-
function getSkillRegistry() {
|
|
1604
|
-
return SKILL_REGISTRY.map((entry) => ({
|
|
1605
|
-
...entry,
|
|
1606
|
-
allowedRoles: [...entry.allowedRoles]
|
|
1607
|
-
}));
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
// src/harness/writers/codex-plugin-package.ts
|
|
1611
|
-
import { createHash } from "crypto";
|
|
1612
|
-
|
|
1613
|
-
// src/harness/adapters/codex-surfaces.ts
|
|
1614
|
-
var CODEX_HOOK_EVENTS = [
|
|
1615
|
-
"SessionStart",
|
|
1616
|
-
"UserPromptSubmit",
|
|
1617
|
-
"PreToolUse",
|
|
1618
|
-
"PermissionRequest",
|
|
1619
|
-
"PostToolUse",
|
|
1620
|
-
"Stop"
|
|
1621
|
-
];
|
|
1622
|
-
var SUPPORTED_CODEX_HOOK_OUTPUT_FIELDS = ["message"];
|
|
1623
|
-
var CODEX_PLUGIN_MANIFEST_FIELDS = [
|
|
1624
|
-
"name",
|
|
1625
|
-
"version",
|
|
1626
|
-
"description",
|
|
1627
|
-
"skills",
|
|
1628
|
-
"mcpServers",
|
|
1629
|
-
"apps",
|
|
1630
|
-
"hooks",
|
|
1631
|
-
"interface"
|
|
1632
|
-
];
|
|
1633
|
-
var CODEX_SURFACES = [
|
|
1634
|
-
{
|
|
1635
|
-
id: "project-agent-toml",
|
|
1636
|
-
target: "agent-definition",
|
|
1637
|
-
status: "validated",
|
|
1638
|
-
artifactKind: "agent-config",
|
|
1639
|
-
path: ".codex/agents/{name}.toml",
|
|
1640
|
-
fields: [
|
|
1641
|
-
"name",
|
|
1642
|
-
"description",
|
|
1643
|
-
"developer_instructions",
|
|
1644
|
-
"model",
|
|
1645
|
-
"model_reasoning_effort",
|
|
1646
|
-
"sandbox_mode"
|
|
1647
|
-
],
|
|
1648
|
-
diagnosticCode: "codex.surface.agent_definition.validated",
|
|
1649
|
-
summary: "Project-scoped custom agents are standalone TOML files.",
|
|
1650
|
-
evidence: "OpenAI Codex Subagents docs: ~/.codex/agents/ and .codex/agents/ with required name, description, developer_instructions."
|
|
1651
|
-
},
|
|
1652
|
-
{
|
|
1653
|
-
id: "project-config-toml",
|
|
1654
|
-
target: "config",
|
|
1655
|
-
status: "validated",
|
|
1656
|
-
artifactKind: "harness-config",
|
|
1657
|
-
path: ".codex/config.toml",
|
|
1658
|
-
fields: [
|
|
1659
|
-
"model",
|
|
1660
|
-
"model_reasoning_effort",
|
|
1661
|
-
"approval_policy",
|
|
1662
|
-
"sandbox_mode",
|
|
1663
|
-
"features",
|
|
1664
|
-
"mcp_servers",
|
|
1665
|
-
"skills.config",
|
|
1666
|
-
"agents"
|
|
1667
|
-
],
|
|
1668
|
-
diagnosticCode: "codex.surface.config.validated",
|
|
1669
|
-
summary: "Codex supports project-scoped config TOML after trust.",
|
|
1670
|
-
evidence: "OpenAI Codex Configuration Reference: ~/.codex/config.toml and project .codex/config.toml overrides."
|
|
1671
|
-
},
|
|
1672
|
-
{
|
|
1673
|
-
id: "mcp-server-config",
|
|
1674
|
-
target: "mcp-config",
|
|
1675
|
-
status: "validated",
|
|
1676
|
-
artifactKind: "mcp-config",
|
|
1677
|
-
path: ".codex/config.toml",
|
|
1678
|
-
fields: ["mcp_servers.<id>", "url", "command", "args", "env", "tools"],
|
|
1679
|
-
diagnosticCode: "codex.surface.mcp.validated",
|
|
1680
|
-
summary: "MCP servers are configured through Codex config TOML.",
|
|
1681
|
-
evidence: "OpenAI Codex config docs describe mcp_servers and per-tool approval overrides."
|
|
1682
|
-
},
|
|
1683
|
-
{
|
|
1684
|
-
id: "repo-skills-directory",
|
|
1685
|
-
target: "skill-directory",
|
|
1686
|
-
status: "validated",
|
|
1687
|
-
artifactKind: "skill",
|
|
1688
|
-
path: ".agents/skills/{skill}/SKILL.md",
|
|
1689
|
-
fields: ["SKILL.md", "scripts/", "references/", "assets/"],
|
|
1690
|
-
diagnosticCode: "codex.surface.skills.validated",
|
|
1691
|
-
summary: "Repository skills are discovered from .agents/skills.",
|
|
1692
|
-
evidence: "OpenAI Codex Skills docs: repo skills are scanned from .agents/skills up to the repository root."
|
|
1693
|
-
},
|
|
1694
|
-
{
|
|
1695
|
-
id: "project-hooks-json",
|
|
1696
|
-
target: "hook-config",
|
|
1697
|
-
status: "validated",
|
|
1698
|
-
artifactKind: "hook-config",
|
|
1699
|
-
path: ".codex/hooks.json",
|
|
1700
|
-
fields: [
|
|
1701
|
-
"SessionStart.command",
|
|
1702
|
-
"UserPromptSubmit.command",
|
|
1703
|
-
"PreToolUse.command",
|
|
1704
|
-
"PermissionRequest.command",
|
|
1705
|
-
"PostToolUse.command",
|
|
1706
|
-
"Stop.command"
|
|
1707
|
-
],
|
|
1708
|
-
diagnosticCode: "codex.surface.hooks_json.validated",
|
|
1709
|
-
summary: "Codex supports project-local hooks.json command handlers.",
|
|
1710
|
-
evidence: "OpenAI Codex hooks docs describe hooks.json with SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, and Stop command handlers."
|
|
1711
|
-
},
|
|
1712
|
-
{
|
|
1713
|
-
id: "inline-hooks-table",
|
|
1714
|
-
target: "hook-config",
|
|
1715
|
-
status: "validated",
|
|
1716
|
-
artifactKind: "hook-config",
|
|
1717
|
-
path: ".codex/config.toml",
|
|
1718
|
-
fields: [
|
|
1719
|
-
"hooks.SessionStart.command",
|
|
1720
|
-
"hooks.UserPromptSubmit.command",
|
|
1721
|
-
"hooks.PreToolUse.command",
|
|
1722
|
-
"hooks.PermissionRequest.command",
|
|
1723
|
-
"hooks.PostToolUse.command",
|
|
1724
|
-
"hooks.Stop.command"
|
|
1725
|
-
],
|
|
1726
|
-
diagnosticCode: "codex.surface.inline_hooks.validated",
|
|
1727
|
-
summary: "Codex supports inline [hooks] configuration in TOML.",
|
|
1728
|
-
evidence: "OpenAI Codex configuration docs describe inline [hooks] settings gated by trusted project configuration."
|
|
1729
|
-
},
|
|
1730
|
-
{
|
|
1731
|
-
id: "features-hooks-toggle",
|
|
1732
|
-
target: "hook-config",
|
|
1733
|
-
status: "validated",
|
|
1734
|
-
artifactKind: "hook-config",
|
|
1735
|
-
path: ".codex/config.toml",
|
|
1736
|
-
fields: ["features.hooks"],
|
|
1737
|
-
diagnosticCode: "codex.surface.features_hooks.validated",
|
|
1738
|
-
summary: "Codex hook loading is controlled by the features.hooks toggle.",
|
|
1739
|
-
evidence: "OpenAI Codex configuration docs require enabling [features].hooks before project hook configuration is active."
|
|
1740
|
-
},
|
|
1741
|
-
{
|
|
1742
|
-
id: "plugin-hooks-bundle",
|
|
1743
|
-
target: "hook-config",
|
|
1744
|
-
status: "validated",
|
|
1745
|
-
artifactKind: "hook-config",
|
|
1746
|
-
path: ".codex/plugins/{plugin}/hooks.json",
|
|
1747
|
-
fields: ["features.plugin_hooks", "plugin.hooks", "plugin.trust_review"],
|
|
1748
|
-
diagnosticCode: "codex.surface.plugin_hooks.validated",
|
|
1749
|
-
summary: "Codex plugin hook bundles are documented behind plugin hook feature and trust review gates.",
|
|
1750
|
-
evidence: "OpenAI Codex plugin docs describe bundled hook configuration requiring plugin_hooks enablement and trust review."
|
|
1751
|
-
},
|
|
1752
|
-
{
|
|
1753
|
-
id: "plugin-manifest-json",
|
|
1754
|
-
target: "plugin-manifest",
|
|
1755
|
-
status: "validated",
|
|
1756
|
-
artifactKind: "manifest",
|
|
1757
|
-
path: ".codex-plugin/plugin.json",
|
|
1758
|
-
fields: [...CODEX_PLUGIN_MANIFEST_FIELDS],
|
|
1759
|
-
diagnosticCode: "codex.surface.plugin_manifest.validated",
|
|
1760
|
-
summary: "Codex plugin packages are described by a plugin-root plugin.json manifest.",
|
|
1761
|
-
evidence: "OpenAI Codex plugin docs describe plugin.json with official fields including name, version, description, skills, mcpServers, apps, hooks, and interface."
|
|
1762
|
-
},
|
|
1763
|
-
{
|
|
1764
|
-
id: "plugin-skills-directory",
|
|
1765
|
-
target: "skill-directory",
|
|
1766
|
-
status: "validated",
|
|
1767
|
-
artifactKind: "skill",
|
|
1768
|
-
path: ".codex-plugin/skills/{skill}/SKILL.md",
|
|
1769
|
-
fields: ["skills", "./skills/"],
|
|
1770
|
-
diagnosticCode: "codex.surface.plugin_skills.validated",
|
|
1771
|
-
summary: "Codex plugin packages can bundle skills under plugin-root skills/.",
|
|
1772
|
-
evidence: "OpenAI Codex plugin docs describe plugin-bundled skills referenced from plugin.json using plugin-root relative paths."
|
|
1773
|
-
},
|
|
1774
|
-
{
|
|
1775
|
-
id: "plugin-hooks-json",
|
|
1776
|
-
target: "hook-config",
|
|
1777
|
-
status: "validated",
|
|
1778
|
-
artifactKind: "hook-config",
|
|
1779
|
-
path: ".codex-plugin/hooks/hooks.json",
|
|
1780
|
-
fields: ["hooks", "./hooks/hooks.json"],
|
|
1781
|
-
diagnosticCode: "codex.surface.plugin_hooks_json.validated",
|
|
1782
|
-
summary: "Codex plugin packages can bundle hook configuration under plugin-root hooks/.",
|
|
1783
|
-
evidence: "OpenAI Codex plugin docs describe hook bundle assets referenced from plugin.json and gated by plugin_hooks plus trust review."
|
|
1784
|
-
},
|
|
1785
|
-
{
|
|
1786
|
-
id: "plugin-mcp-json",
|
|
1787
|
-
target: "mcp-config",
|
|
1788
|
-
status: "validated",
|
|
1789
|
-
artifactKind: "mcp-config",
|
|
1790
|
-
path: ".codex-plugin/.mcp.json",
|
|
1791
|
-
fields: ["mcpServers", "./.mcp.json"],
|
|
1792
|
-
diagnosticCode: "codex.surface.plugin_mcp_json.validated",
|
|
1793
|
-
summary: "Codex plugin packages can bundle MCP server definitions in plugin-root .mcp.json.",
|
|
1794
|
-
evidence: "OpenAI Codex plugin docs describe bundled .mcp.json server definitions referenced from plugin.json using plugin-root relative paths."
|
|
1795
|
-
},
|
|
1796
|
-
{
|
|
1797
|
-
id: "inline-hooks",
|
|
1798
|
-
target: "hook-config",
|
|
1799
|
-
status: "unknown",
|
|
1800
|
-
artifactKind: "hook-config",
|
|
1801
|
-
fields: ["features.hooks", "hooks"],
|
|
1802
|
-
diagnosticCode: "codex.surface.hooks.unvalidated",
|
|
1803
|
-
summary: "Lifecycle hook availability is documented, but hook event shape and parity with OpenCode runtime hooks is not validated for this adapter.",
|
|
1804
|
-
evidence: "Codex config reference mentions hooks; this implementation has not validated exact event schemas for plugin parity.",
|
|
1805
|
-
fallback: "diagnostic-only"
|
|
1806
|
-
},
|
|
1807
|
-
{
|
|
1808
|
-
id: "per-agent-runtime-permissions",
|
|
1809
|
-
target: "permission-control",
|
|
1810
|
-
status: "unsupported",
|
|
1811
|
-
fields: ["per-agent tool allow/deny equivalent to OpenCode permissions"],
|
|
1812
|
-
diagnosticCode: "codex.permission.memory.enforcement_gap",
|
|
1813
|
-
summary: "Codex sandbox and approval policy can constrain sessions, but OpenCode-style per-agent MCP/tool deny maps are not validated.",
|
|
1814
|
-
evidence: "Codex approvals/security docs cover sandbox and approval policies, not exact OpenCode per-agent permission maps.",
|
|
1815
|
-
fallback: "instruction-only"
|
|
1816
|
-
},
|
|
1817
|
-
{
|
|
1818
|
-
id: "programmatic-delegation-runtime",
|
|
1819
|
-
target: "delegation-runtime",
|
|
1820
|
-
status: "unsupported",
|
|
1821
|
-
fields: [
|
|
1822
|
-
"task API",
|
|
1823
|
-
"background task sessions",
|
|
1824
|
-
"tmux lifecycle hooks",
|
|
1825
|
-
"automatic subagent session close"
|
|
1826
|
-
],
|
|
1827
|
-
diagnosticCode: "codex.delegation.runtime.unsupported",
|
|
1828
|
-
summary: "Codex subagents are user/instruction-triggered; no OpenCode plugin task API or automatic subagent session close parity is validated.",
|
|
1829
|
-
evidence: "Codex subagents docs describe manual spawning and agent threads, not this plugin runtime task API or a validated runtime close hook.",
|
|
1830
|
-
fallback: "instruction-only"
|
|
1831
|
-
},
|
|
1832
|
-
{
|
|
1833
|
-
id: "parent-context-injection",
|
|
1834
|
-
target: "parent-context-injection",
|
|
1835
|
-
status: "unknown",
|
|
1836
|
-
fields: ["parent session_id", "project"],
|
|
1837
|
-
diagnosticCode: "codex.context.parent_injection.unvalidated",
|
|
1838
|
-
summary: "No machine-enforced parent context injection mechanism is validated; prompts must instruct users to include parent session_id/project.",
|
|
1839
|
-
evidence: "No validated Codex surface in Phase 1 proves automatic parent context injection into spawned agents.",
|
|
1840
|
-
fallback: "instruction-only"
|
|
1841
|
-
}
|
|
1842
|
-
];
|
|
1843
|
-
function getCodexSurfaceRecords() {
|
|
1844
|
-
return CODEX_SURFACES.map((surface) => ({
|
|
1845
|
-
...surface,
|
|
1846
|
-
fields: [...surface.fields]
|
|
1847
|
-
}));
|
|
1848
|
-
}
|
|
1849
|
-
function getCodexSurface(id) {
|
|
1850
|
-
return getCodexSurfaceRecords().find((surface) => surface.id === id);
|
|
1851
|
-
}
|
|
1852
|
-
function assertCodexSurfaceCanGenerate(id) {
|
|
1853
|
-
const surface = getCodexSurface(id);
|
|
1854
|
-
if (!surface) {
|
|
1855
|
-
return {
|
|
1856
|
-
ok: false,
|
|
1857
|
-
diagnostic: {
|
|
1858
|
-
severity: "error",
|
|
1859
|
-
code: "harness.surface_unvalidated",
|
|
1860
|
-
message: `Codex surface "${id}" is not registered and cannot generate artifacts.`,
|
|
1861
|
-
harness: "codex",
|
|
1862
|
-
surface: id,
|
|
1863
|
-
fallback: "diagnostic-only"
|
|
1864
|
-
}
|
|
1865
|
-
};
|
|
1866
|
-
}
|
|
1867
|
-
if (surface.status !== "validated" || !surface.artifactKind) {
|
|
1868
|
-
return {
|
|
1869
|
-
ok: false,
|
|
1870
|
-
diagnostic: codexSurfaceDiagnostic(surface)
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
|
-
return { ok: true, surface };
|
|
1874
|
-
}
|
|
1875
|
-
function codexSurfaceDiagnostic(surface) {
|
|
1876
|
-
const severity = surface.status === "unsupported" ? "warning" : "error";
|
|
1877
|
-
return {
|
|
1878
|
-
severity,
|
|
1879
|
-
code: surface.diagnosticCode,
|
|
1880
|
-
message: `${surface.summary} Artifact generation is disabled for this surface.`,
|
|
1881
|
-
harness: "codex",
|
|
1882
|
-
surface: surface.id,
|
|
1883
|
-
fallback: surface.fallback ?? "diagnostic-only"
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
function normalizeCodexPath(value) {
|
|
1887
|
-
return value.replace(/\\/g, "/");
|
|
1888
|
-
}
|
|
1889
|
-
function codexPluginPackageDiagnostic(code, message, surface) {
|
|
1890
|
-
return {
|
|
1891
|
-
severity: "error",
|
|
1892
|
-
code,
|
|
1893
|
-
message,
|
|
1894
|
-
harness: "codex",
|
|
1895
|
-
surface,
|
|
1896
|
-
fallback: "diagnostic-only"
|
|
1897
|
-
};
|
|
1898
|
-
}
|
|
1899
|
-
function isPathUnderCodexPlugin(pathValue) {
|
|
1900
|
-
const normalized = normalizeCodexPath(pathValue);
|
|
1901
|
-
const segments = normalized.split("/");
|
|
1902
|
-
return !normalized.startsWith("/") && !/^[A-Za-z]:\//.test(normalized) && !segments.includes("..") && (normalized === ".codex-plugin" || normalized === ".codex-plugin/" || normalized.startsWith(".codex-plugin/"));
|
|
1903
|
-
}
|
|
1904
|
-
function fieldAllowedForPluginSurface(surface, field) {
|
|
1905
|
-
if (surface.id === "plugin-manifest-json") {
|
|
1906
|
-
return CODEX_PLUGIN_MANIFEST_FIELDS.includes(
|
|
1907
|
-
field
|
|
1908
|
-
);
|
|
1909
|
-
}
|
|
1910
|
-
return surface.fields.includes(field);
|
|
1911
|
-
}
|
|
1912
|
-
function validateCodexPluginPackageSurface(input) {
|
|
1913
|
-
const canGenerate = assertCodexSurfaceCanGenerate(input.surfaceId);
|
|
1914
|
-
if (!canGenerate.ok) {
|
|
1915
|
-
return {
|
|
1916
|
-
ok: false,
|
|
1917
|
-
diagnostics: [
|
|
1918
|
-
{
|
|
1919
|
-
...canGenerate.diagnostic,
|
|
1920
|
-
code: "codex.plugin.surface.unvalidated",
|
|
1921
|
-
message: `Codex plugin package surface "${input.surfaceId}" is not validated for .codex-plugin artifacts.`
|
|
1922
|
-
}
|
|
1923
|
-
]
|
|
1924
|
-
};
|
|
1925
|
-
}
|
|
1926
|
-
const diagnostics = [];
|
|
1927
|
-
const surface = canGenerate.surface;
|
|
1928
|
-
if (!surface.path?.startsWith(".codex-plugin")) {
|
|
1929
|
-
diagnostics.push(
|
|
1930
|
-
codexPluginPackageDiagnostic(
|
|
1931
|
-
"codex.plugin.surface.unvalidated",
|
|
1932
|
-
`Codex surface "${input.surfaceId}" is validated, but not as a .codex-plugin package surface.`,
|
|
1933
|
-
input.surfaceId
|
|
1934
|
-
)
|
|
1935
|
-
);
|
|
1936
|
-
}
|
|
1937
|
-
if (input.path && !isPathUnderCodexPlugin(input.path)) {
|
|
1938
|
-
diagnostics.push(
|
|
1939
|
-
codexPluginPackageDiagnostic(
|
|
1940
|
-
"codex.plugin.path.unvalidated",
|
|
1941
|
-
`Codex plugin package path "${input.path}" must stay under .codex-plugin/.`,
|
|
1942
|
-
input.surfaceId
|
|
1943
|
-
)
|
|
1944
|
-
);
|
|
1945
|
-
}
|
|
1946
|
-
for (const field of input.fields ?? []) {
|
|
1947
|
-
if (!fieldAllowedForPluginSurface(surface, field)) {
|
|
1948
|
-
diagnostics.push(
|
|
1949
|
-
codexPluginPackageDiagnostic(
|
|
1950
|
-
"codex.plugin.field.unvalidated",
|
|
1951
|
-
`Codex plugin field "${field}" is not validated for surface "${input.surfaceId}".`,
|
|
1952
|
-
input.surfaceId
|
|
1953
|
-
)
|
|
1954
|
-
);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
if (diagnostics.length > 0) return { ok: false, diagnostics };
|
|
1958
|
-
return { ok: true, surface };
|
|
1959
|
-
}
|
|
1960
|
-
function isCodexHookEvent(event) {
|
|
1961
|
-
return CODEX_HOOK_EVENTS.includes(event);
|
|
1962
|
-
}
|
|
1963
|
-
function codexHookDiagnostic(code, message) {
|
|
1964
|
-
return {
|
|
1965
|
-
severity: "warning",
|
|
1966
|
-
code,
|
|
1967
|
-
message,
|
|
1968
|
-
harness: "codex",
|
|
1969
|
-
surface: "hook-config",
|
|
1970
|
-
fallback: "diagnostic-only"
|
|
1971
|
-
};
|
|
1972
|
-
}
|
|
1973
|
-
function inferHookHandlerType(handler) {
|
|
1974
|
-
if (!handler) return void 0;
|
|
1975
|
-
if (typeof handler.type === "string") return handler.type;
|
|
1976
|
-
if ("command" in handler) return "command";
|
|
1977
|
-
if ("prompt" in handler) return "prompt";
|
|
1978
|
-
if ("agent" in handler) return "agent";
|
|
1979
|
-
return void 0;
|
|
1980
|
-
}
|
|
1981
|
-
function validateCodexHookSurface(input) {
|
|
1982
|
-
const diagnostics = [];
|
|
1983
|
-
const handlerType = inferHookHandlerType(input.handler);
|
|
1984
|
-
if (!isCodexHookEvent(input.event)) {
|
|
1985
|
-
diagnostics.push(
|
|
1986
|
-
codexHookDiagnostic(
|
|
1987
|
-
"codex.hooks.event.unsupported",
|
|
1988
|
-
`Codex hook event "${input.event}" is not documented for this adapter.`
|
|
1989
|
-
)
|
|
1990
|
-
);
|
|
1991
|
-
}
|
|
1992
|
-
if (handlerType !== "command") {
|
|
1993
|
-
diagnostics.push(
|
|
1994
|
-
codexHookDiagnostic(
|
|
1995
|
-
handlerType === "prompt" ? "codex.hooks.handler.prompt_unsupported" : handlerType === "agent" ? "codex.hooks.handler.agent_unsupported" : "codex.hooks.handler.unsupported",
|
|
1996
|
-
`Codex hook handler "${handlerType ?? "unknown"}" is not supported; only command handlers are validated.`
|
|
1997
|
-
)
|
|
1998
|
-
);
|
|
1999
|
-
}
|
|
2000
|
-
if (input.handler?.async === true) {
|
|
2001
|
-
diagnostics.push(
|
|
2002
|
-
codexHookDiagnostic(
|
|
2003
|
-
"codex.hooks.async.unsupported",
|
|
2004
|
-
"Async Codex hook execution is not validated for this adapter."
|
|
2005
|
-
)
|
|
2006
|
-
);
|
|
2007
|
-
}
|
|
2008
|
-
const unsupportedOutputField = input.outputFields?.find(
|
|
2009
|
-
(field) => !SUPPORTED_CODEX_HOOK_OUTPUT_FIELDS.includes(field)
|
|
2010
|
-
);
|
|
2011
|
-
if (unsupportedOutputField) {
|
|
2012
|
-
diagnostics.push(
|
|
2013
|
-
codexHookDiagnostic(
|
|
2014
|
-
"codex.hooks.output_field.unsupported",
|
|
2015
|
-
`Codex hook output field "${unsupportedOutputField}" is not supported by the validated hook surface.`
|
|
2016
|
-
)
|
|
2017
|
-
);
|
|
2018
|
-
}
|
|
2019
|
-
if (input.interceptsToolExecution) {
|
|
2020
|
-
diagnostics.push(
|
|
2021
|
-
codexHookDiagnostic(
|
|
2022
|
-
"codex.hooks.tool_interception.unsupported",
|
|
2023
|
-
"Full tool interception is not supported; Codex hooks are diagnostic/config surfaces, not OpenCode runtime enforcement hooks."
|
|
2024
|
-
)
|
|
2025
|
-
);
|
|
2026
|
-
}
|
|
2027
|
-
if (diagnostics.length > 0 || !isCodexHookEvent(input.event)) {
|
|
2028
|
-
return { ok: false, diagnostics };
|
|
2029
|
-
}
|
|
2030
|
-
return { ok: true, event: input.event, handlerType: "command" };
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
// src/harness/writers/codex-plugin-package.ts
|
|
2034
|
-
var PLUGIN_MANIFEST_SURFACE_ID = "plugin-manifest-json";
|
|
2035
|
-
function normalizePath(value) {
|
|
2036
|
-
return value.replace(/\\/g, "/");
|
|
2037
|
-
}
|
|
2038
|
-
function pluginRootReference(pathValue) {
|
|
2039
|
-
const normalized = normalizePath(pathValue);
|
|
2040
|
-
const relative4 = normalized.slice(".codex-plugin/".length);
|
|
2041
|
-
return `./${relative4}`;
|
|
2042
|
-
}
|
|
2043
|
-
function sha256(content) {
|
|
2044
|
-
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
2045
|
-
}
|
|
2046
|
-
function isRecord(value) {
|
|
2047
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2048
|
-
}
|
|
2049
|
-
function stableValue(value) {
|
|
2050
|
-
if (Array.isArray(value)) return value.map(stableValue);
|
|
2051
|
-
if (!isRecord(value)) return value;
|
|
2052
|
-
return Object.fromEntries(
|
|
2053
|
-
Object.keys(value).sort((left, right) => left.localeCompare(right)).map((key) => [key, stableValue(value[key])])
|
|
2054
|
-
);
|
|
2055
|
-
}
|
|
2056
|
-
function stableJson(value) {
|
|
2057
|
-
return `${JSON.stringify(value, null, 2)}
|
|
2058
|
-
`;
|
|
2059
|
-
}
|
|
2060
|
-
function orderManifestFields(manifest) {
|
|
2061
|
-
const ordered = {};
|
|
2062
|
-
for (const field of CODEX_PLUGIN_MANIFEST_FIELDS) {
|
|
2063
|
-
if (field in manifest) ordered[field] = manifest[field];
|
|
2064
|
-
}
|
|
2065
|
-
return ordered;
|
|
2066
|
-
}
|
|
2067
|
-
function fieldDiagnostic(field) {
|
|
2068
|
-
return {
|
|
2069
|
-
severity: "error",
|
|
2070
|
-
code: "codex.plugin.field.unvalidated",
|
|
2071
|
-
message: `Skipping unvalidated Codex plugin manifest field "${field}".`,
|
|
2072
|
-
harness: "codex",
|
|
2073
|
-
surface: PLUGIN_MANIFEST_SURFACE_ID,
|
|
2074
|
-
fallback: "diagnostic-only"
|
|
2075
|
-
};
|
|
2076
|
-
}
|
|
2077
|
-
function artifactKindForSurface(surfaceId) {
|
|
2078
|
-
if (surfaceId === "plugin-mcp-json") return "mcp-config";
|
|
2079
|
-
return surfaceId === "plugin-hooks-json" ? "hook-config" : "skill";
|
|
2080
|
-
}
|
|
2081
|
-
function codexHookPackageDiagnostic(code, message) {
|
|
2082
|
-
return {
|
|
2083
|
-
severity: "warning",
|
|
2084
|
-
code,
|
|
2085
|
-
message,
|
|
2086
|
-
harness: "codex",
|
|
2087
|
-
surface: "plugin-hooks-json",
|
|
2088
|
-
fallback: "diagnostic-only"
|
|
2089
|
-
};
|
|
2090
|
-
}
|
|
2091
|
-
function normalizeHookDefinition(hook) {
|
|
2092
|
-
return {
|
|
2093
|
-
command: hook.handler.command,
|
|
2094
|
-
...hook.outputFields?.length ? { output: [...hook.outputFields].sort() } : {}
|
|
2095
|
-
};
|
|
2096
|
-
}
|
|
2097
|
-
function renderHookDefinitions(hookDefinitions) {
|
|
2098
|
-
const diagnostics = [];
|
|
2099
|
-
const hooksByEvent = /* @__PURE__ */ new Map();
|
|
2100
|
-
for (const hook of hookDefinitions) {
|
|
2101
|
-
const validation = validateCodexHookSurface(hook);
|
|
2102
|
-
if (!validation.ok) {
|
|
2103
|
-
diagnostics.push(...validation.diagnostics);
|
|
2104
|
-
continue;
|
|
2105
|
-
}
|
|
2106
|
-
const eventHooks = hooksByEvent.get(validation.event) ?? [];
|
|
2107
|
-
eventHooks.push(normalizeHookDefinition(hook));
|
|
2108
|
-
hooksByEvent.set(validation.event, eventHooks);
|
|
2109
|
-
}
|
|
2110
|
-
if (hooksByEvent.size === 0) {
|
|
2111
|
-
diagnostics.push(
|
|
2112
|
-
codexHookPackageDiagnostic(
|
|
2113
|
-
"codex.plugin.hooks.none_packaged",
|
|
2114
|
-
"No Codex plugin hooks were packaged because no hook definitions passed existing Codex hook validation."
|
|
2115
|
-
)
|
|
2116
|
-
);
|
|
2117
|
-
return { diagnostics };
|
|
2118
|
-
}
|
|
2119
|
-
const content = stableJson(
|
|
2120
|
-
Object.fromEntries(
|
|
2121
|
-
[...hooksByEvent.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([event, hooks]) => [event, hooks.map(stableValue)])
|
|
2122
|
-
)
|
|
2123
|
-
);
|
|
2124
|
-
return { content, diagnostics };
|
|
2125
|
-
}
|
|
2126
|
-
function parseRawHookContent(content) {
|
|
2127
|
-
const diagnostics = [];
|
|
2128
|
-
const hookDefinitions = [];
|
|
2129
|
-
let parsed;
|
|
2130
|
-
try {
|
|
2131
|
-
parsed = JSON.parse(content);
|
|
2132
|
-
} catch {
|
|
2133
|
-
return {
|
|
2134
|
-
hookDefinitions,
|
|
2135
|
-
diagnostics: [
|
|
2136
|
-
codexHookPackageDiagnostic(
|
|
2137
|
-
"codex.plugin.hooks.invalid_json",
|
|
2138
|
-
"Skipping Codex plugin hooks asset because hooks.json content is not valid JSON."
|
|
2139
|
-
)
|
|
2140
|
-
]
|
|
2141
|
-
};
|
|
2142
|
-
}
|
|
2143
|
-
if (!isRecord(parsed)) {
|
|
2144
|
-
return {
|
|
2145
|
-
hookDefinitions,
|
|
2146
|
-
diagnostics: [
|
|
2147
|
-
codexHookPackageDiagnostic(
|
|
2148
|
-
"codex.plugin.hooks.invalid_shape",
|
|
2149
|
-
"Skipping Codex plugin hooks asset because hooks.json must be an object keyed by Codex hook event."
|
|
2150
|
-
)
|
|
2151
|
-
]
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
for (const [event, definitions] of Object.entries(parsed)) {
|
|
2155
|
-
if (!Array.isArray(definitions)) {
|
|
2156
|
-
diagnostics.push(
|
|
2157
|
-
codexHookPackageDiagnostic(
|
|
2158
|
-
"codex.plugin.hooks.invalid_shape",
|
|
2159
|
-
`Skipping Codex plugin hook event "${event}" because its value is not an array of command handlers.`
|
|
2160
|
-
)
|
|
2161
|
-
);
|
|
2162
|
-
continue;
|
|
2163
|
-
}
|
|
2164
|
-
for (const definition of definitions) {
|
|
2165
|
-
if (!isRecord(definition)) {
|
|
2166
|
-
diagnostics.push(
|
|
2167
|
-
codexHookPackageDiagnostic(
|
|
2168
|
-
"codex.plugin.hooks.invalid_shape",
|
|
2169
|
-
`Skipping Codex plugin hook event "${event}" entry because it is not an object.`
|
|
2170
|
-
)
|
|
2171
|
-
);
|
|
2172
|
-
continue;
|
|
2173
|
-
}
|
|
2174
|
-
const output = definition.output;
|
|
2175
|
-
hookDefinitions.push({
|
|
2176
|
-
event,
|
|
2177
|
-
handler: {
|
|
2178
|
-
type: typeof definition.type === "string" ? definition.type : "command",
|
|
2179
|
-
command: definition.command,
|
|
2180
|
-
async: definition.async
|
|
2181
|
-
},
|
|
2182
|
-
outputFields: Array.isArray(output) ? output.filter((field) => typeof field === "string") : void 0,
|
|
2183
|
-
interceptsToolExecution: definition.interceptsToolExecution === true
|
|
2184
|
-
});
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
return { hookDefinitions, diagnostics };
|
|
2188
|
-
}
|
|
2189
|
-
function resolveHookAssetContent(asset) {
|
|
2190
|
-
if (asset.surfaceId !== "plugin-hooks-json") {
|
|
2191
|
-
return { content: asset.content, diagnostics: [] };
|
|
2192
|
-
}
|
|
2193
|
-
const fromInput = asset.hookDefinitions ? { hookDefinitions: [...asset.hookDefinitions], diagnostics: [] } : asset.content !== void 0 ? parseRawHookContent(asset.content) : { hookDefinitions: [], diagnostics: [] };
|
|
2194
|
-
const rendered = renderHookDefinitions(fromInput.hookDefinitions);
|
|
2195
|
-
return {
|
|
2196
|
-
content: rendered.content,
|
|
2197
|
-
diagnostics: [...fromInput.diagnostics, ...rendered.diagnostics]
|
|
2198
|
-
};
|
|
2199
|
-
}
|
|
2200
|
-
function renderCodexPluginPackage(input) {
|
|
2201
|
-
const artifacts = [];
|
|
2202
|
-
const diagnostics = [];
|
|
2203
|
-
const manifest = {};
|
|
2204
|
-
const provenance = [];
|
|
2205
|
-
for (const field of Object.keys(input.manifest)) {
|
|
2206
|
-
if (!CODEX_PLUGIN_MANIFEST_FIELDS.includes(
|
|
2207
|
-
field
|
|
2208
|
-
)) {
|
|
2209
|
-
diagnostics.push(fieldDiagnostic(field));
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
const manifestValidation = validateCodexPluginPackageSurface({
|
|
2213
|
-
surfaceId: PLUGIN_MANIFEST_SURFACE_ID,
|
|
2214
|
-
path: ".codex-plugin/plugin.json",
|
|
2215
|
-
fields: Object.keys(input.manifest).filter(
|
|
2216
|
-
(field) => CODEX_PLUGIN_MANIFEST_FIELDS.includes(
|
|
2217
|
-
field
|
|
2218
|
-
)
|
|
2219
|
-
)
|
|
2220
|
-
});
|
|
2221
|
-
if (!manifestValidation.ok)
|
|
2222
|
-
diagnostics.push(...manifestValidation.diagnostics);
|
|
2223
|
-
for (const field of CODEX_PLUGIN_MANIFEST_FIELDS) {
|
|
2224
|
-
if (field in input.manifest)
|
|
2225
|
-
manifest[field] = stableValue(input.manifest[field]);
|
|
2226
|
-
}
|
|
2227
|
-
for (const asset of [...input.assets ?? []].sort(
|
|
2228
|
-
(left, right) => normalizePath(left.path).localeCompare(normalizePath(right.path))
|
|
2229
|
-
)) {
|
|
2230
|
-
const assetPath = normalizePath(asset.path);
|
|
2231
|
-
const validation = validateCodexPluginPackageSurface({
|
|
2232
|
-
surfaceId: asset.surfaceId,
|
|
2233
|
-
path: assetPath,
|
|
2234
|
-
fields: [asset.manifestField]
|
|
2235
|
-
});
|
|
2236
|
-
if (!validation.ok) {
|
|
2237
|
-
diagnostics.push(...validation.diagnostics);
|
|
2238
|
-
continue;
|
|
2239
|
-
}
|
|
2240
|
-
const hookContent = resolveHookAssetContent(asset);
|
|
2241
|
-
diagnostics.push(...hookContent.diagnostics);
|
|
2242
|
-
if (asset.surfaceId === "plugin-hooks-json" && !hookContent.content) {
|
|
2243
|
-
continue;
|
|
2244
|
-
}
|
|
2245
|
-
const assetContent = hookContent.content ?? asset.content;
|
|
2246
|
-
const reference = pluginRootReference(assetPath);
|
|
2247
|
-
manifest[asset.manifestField] = reference;
|
|
2248
|
-
if (assetContent !== void 0 && !assetPath.endsWith("/")) {
|
|
2249
|
-
artifacts.push({
|
|
2250
|
-
harness: "codex",
|
|
2251
|
-
kind: artifactKindForSurface(asset.surfaceId),
|
|
2252
|
-
path: assetPath,
|
|
2253
|
-
description: asset.description ?? `Codex plugin asset for ${asset.manifestField}`,
|
|
2254
|
-
content: assetContent
|
|
2255
|
-
});
|
|
2256
|
-
}
|
|
2257
|
-
provenance.push({
|
|
2258
|
-
field: asset.manifestField,
|
|
2259
|
-
path: assetPath,
|
|
2260
|
-
reference,
|
|
2261
|
-
...asset.provenanceName ? { name: asset.provenanceName } : {},
|
|
2262
|
-
...asset.sourcePath ? { sourcePath: normalizePath(asset.sourcePath) } : {},
|
|
2263
|
-
...assetContent !== void 0 ? { sha256: sha256(assetContent) } : {}
|
|
2264
|
-
});
|
|
2265
|
-
}
|
|
2266
|
-
artifacts.push({
|
|
2267
|
-
harness: "codex",
|
|
2268
|
-
kind: "manifest",
|
|
2269
|
-
path: ".codex-plugin/plugin.json",
|
|
2270
|
-
description: "Deterministic Codex plugin package manifest.",
|
|
2271
|
-
content: stableJson(orderManifestFields(manifest))
|
|
2272
|
-
});
|
|
2273
|
-
artifacts.push({
|
|
2274
|
-
harness: "codex",
|
|
2275
|
-
kind: "manifest",
|
|
2276
|
-
path: ".codex-plugin/.thoth-agents-plugin-assets.json",
|
|
2277
|
-
description: "Deterministic Codex plugin package asset provenance.",
|
|
2278
|
-
content: stableJson({
|
|
2279
|
-
generatedBy: "thoth-agents",
|
|
2280
|
-
assets: provenance.sort(
|
|
2281
|
-
(left, right) => left.path.localeCompare(right.path)
|
|
2282
|
-
)
|
|
2283
|
-
})
|
|
2284
|
-
});
|
|
2285
|
-
return { artifacts, diagnostics };
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
// src/harness/writers/codex-toml.ts
|
|
2289
|
-
function escapeTomlString(value) {
|
|
2290
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\f/g, "\\f").replace(/\r/g, "\\r");
|
|
2291
|
-
}
|
|
2292
|
-
function escapeTomlMultilineString(value) {
|
|
2293
|
-
return value.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"').replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2294
|
-
}
|
|
2295
|
-
function renderScalar(value) {
|
|
2296
|
-
if (typeof value === "string") return `"${escapeTomlString(value)}"`;
|
|
2297
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
2298
|
-
return String(value);
|
|
2299
|
-
}
|
|
2300
|
-
if (Array.isArray(value)) {
|
|
2301
|
-
return `[${value.map(renderScalar).join(", ")}]`;
|
|
2302
|
-
}
|
|
2303
|
-
throw new Error(`Unsupported TOML scalar: ${String(value)}`);
|
|
2304
|
-
}
|
|
2305
|
-
function renderField(key, value) {
|
|
2306
|
-
if (key === "developer_instructions" && typeof value === "string") {
|
|
2307
|
-
return [`${key} = """`, escapeTomlMultilineString(value), '"""', ""];
|
|
2308
|
-
}
|
|
2309
|
-
return [`${key} = ${renderScalar(value)}`];
|
|
2310
|
-
}
|
|
2311
|
-
function isRecord2(value) {
|
|
2312
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2313
|
-
}
|
|
2314
|
-
function fieldOrder(fields, keys) {
|
|
2315
|
-
const orderedRoots = fields.map((field) => field.split(".")[0]);
|
|
2316
|
-
return [...keys].sort((left, right) => {
|
|
2317
|
-
const leftIndex = orderedRoots.indexOf(left);
|
|
2318
|
-
const rightIndex = orderedRoots.indexOf(right);
|
|
2319
|
-
if (leftIndex !== -1 || rightIndex !== -1) {
|
|
2320
|
-
return (leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex) - (rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex);
|
|
2321
|
-
}
|
|
2322
|
-
return left.localeCompare(right);
|
|
2323
|
-
});
|
|
2324
|
-
}
|
|
2325
|
-
function isAllowedRoot(fields, key) {
|
|
2326
|
-
return fields.some(
|
|
2327
|
-
(field) => field === key || field.startsWith(`${key}.`) || field.includes("<id>") && key === field.split(".")[0]
|
|
2328
|
-
);
|
|
2329
|
-
}
|
|
2330
|
-
function renderTable(lines, tablePath, value) {
|
|
2331
|
-
lines.push(`[${tablePath.join(".")}]`);
|
|
2332
|
-
const nested = [];
|
|
2333
|
-
for (const key of Object.keys(value).sort()) {
|
|
2334
|
-
const entry = value[key];
|
|
2335
|
-
if (isRecord2(entry)) {
|
|
2336
|
-
nested.push([key, entry]);
|
|
2337
|
-
} else {
|
|
2338
|
-
lines.push(...renderField(key, entry));
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
lines.push("");
|
|
2342
|
-
for (const [key, entry] of nested) {
|
|
2343
|
-
renderTable(lines, [...tablePath, key], entry);
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
function renderCodexToml(input) {
|
|
2347
|
-
const canGenerate = assertCodexSurfaceCanGenerate(input.surfaceId);
|
|
2348
|
-
if (!canGenerate.ok) {
|
|
2349
|
-
return { content: "", diagnostics: [canGenerate.diagnostic] };
|
|
2350
|
-
}
|
|
2351
|
-
const surface = getCodexSurface(input.surfaceId);
|
|
2352
|
-
const fields = surface?.fields ?? [];
|
|
2353
|
-
const diagnostics = [];
|
|
2354
|
-
const lines = [];
|
|
2355
|
-
const tables = [];
|
|
2356
|
-
for (const key of fieldOrder(fields, Object.keys(input.values))) {
|
|
2357
|
-
const value = input.values[key];
|
|
2358
|
-
if (!isAllowedRoot(fields, key)) {
|
|
2359
|
-
diagnostics.push({
|
|
2360
|
-
severity: "warning",
|
|
2361
|
-
code: "codex.toml.field.unvalidated",
|
|
2362
|
-
message: `Skipping unvalidated Codex TOML field "${key}" for surface "${input.surfaceId}".`,
|
|
2363
|
-
harness: "codex",
|
|
2364
|
-
surface: input.surfaceId,
|
|
2365
|
-
fallback: "diagnostic-only"
|
|
2366
|
-
});
|
|
2367
|
-
continue;
|
|
2368
|
-
}
|
|
2369
|
-
if (isRecord2(value)) {
|
|
2370
|
-
tables.push([key, value]);
|
|
2371
|
-
} else {
|
|
2372
|
-
lines.push(...renderField(key, value));
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
if (tables.length > 0 && lines.length > 0) lines.push("");
|
|
2376
|
-
for (const [key, value] of tables) {
|
|
2377
|
-
renderTable(lines, [key], value);
|
|
2378
|
-
}
|
|
2379
|
-
while (lines.at(-1) === "") lines.pop();
|
|
2380
|
-
return {
|
|
2381
|
-
content: lines.join("\n") + (lines.length > 0 ? "\n" : ""),
|
|
2382
|
-
diagnostics
|
|
2383
|
-
};
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
// src/harness/writers/skill-layout.ts
|
|
2387
|
-
import { createHash as createHash2 } from "crypto";
|
|
2388
|
-
import * as fs2 from "fs";
|
|
2389
|
-
import * as path2 from "path";
|
|
2390
|
-
var OUTPUT_MODE_CONFIG = {
|
|
2391
|
-
"plugin-package": {
|
|
2392
|
-
basePath: ".codex-plugin/skills",
|
|
2393
|
-
manifestPath: ".codex-plugin/skills/.thoth-agents-manifest.json",
|
|
2394
|
-
surfaceId: "plugin-skills-directory",
|
|
2395
|
-
label: "plugin-bundled"
|
|
2396
|
-
},
|
|
2397
|
-
"repo-local-fallback": {
|
|
2398
|
-
basePath: ".agents/skills",
|
|
2399
|
-
manifestPath: ".agents/skills/.thoth-agents-manifest.json",
|
|
2400
|
-
surfaceId: "repo-skills-directory",
|
|
2401
|
-
label: "fallback .agents/skills"
|
|
2402
|
-
}
|
|
2403
|
-
};
|
|
2404
|
-
function normalizePath2(value) {
|
|
2405
|
-
return value.split(path2.sep).join("/");
|
|
2406
|
-
}
|
|
2407
|
-
function collectFiles(directory) {
|
|
2408
|
-
if (!fs2.existsSync(directory)) return [];
|
|
2409
|
-
const entries = fs2.readdirSync(directory, { withFileTypes: true });
|
|
2410
|
-
const files = [];
|
|
2411
|
-
for (const entry of entries) {
|
|
2412
|
-
const entryPath = path2.join(directory, entry.name);
|
|
2413
|
-
if (entry.isDirectory()) {
|
|
2414
|
-
files.push(...collectFiles(entryPath));
|
|
2415
|
-
} else if (entry.isFile()) {
|
|
2416
|
-
files.push(entryPath);
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
return files.sort((left, right) => left.localeCompare(right));
|
|
2420
|
-
}
|
|
2421
|
-
function sha2562(content) {
|
|
2422
|
-
return `sha256:${createHash2("sha256").update(content).digest("hex")}`;
|
|
2423
|
-
}
|
|
2424
|
-
function resolveOutputModes(input) {
|
|
2425
|
-
const requested = input.outputModes ?? (input.outputMode ? [input.outputMode] : []);
|
|
2426
|
-
const modes = requested.length > 0 ? requested : ["plugin-package"];
|
|
2427
|
-
return [...new Set(modes)];
|
|
2428
|
-
}
|
|
2429
|
-
function duplicateScopeDiagnostic(skillNames, surfaceId) {
|
|
2430
|
-
if (skillNames.length === 0) return void 0;
|
|
2431
|
-
return {
|
|
2432
|
-
severity: "warning",
|
|
2433
|
-
code: "codex.skill.duplicate_scope_precedence_unverified",
|
|
2434
|
-
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.`,
|
|
2435
|
-
harness: "codex",
|
|
2436
|
-
surface: surfaceId,
|
|
2437
|
-
fallback: "diagnostic-only"
|
|
2438
|
-
};
|
|
2439
|
-
}
|
|
2440
|
-
function renderCodexSkillLayout(input) {
|
|
2441
|
-
const surface = assertCodexSurfaceCanGenerate(input.surfaceId);
|
|
2442
|
-
if (!surface.ok) {
|
|
2443
|
-
return { artifacts: [], diagnostics: [surface.diagnostic] };
|
|
2444
|
-
}
|
|
2445
|
-
const artifacts = [];
|
|
2446
|
-
const diagnostics = [];
|
|
2447
|
-
const outputModes = resolveOutputModes(input);
|
|
2448
|
-
for (const mode of outputModes) {
|
|
2449
|
-
const modeSurface = assertCodexSurfaceCanGenerate(
|
|
2450
|
-
OUTPUT_MODE_CONFIG[mode].surfaceId
|
|
2451
|
-
);
|
|
2452
|
-
if (!modeSurface.ok) diagnostics.push(modeSurface.diagnostic);
|
|
2453
|
-
}
|
|
2454
|
-
const duplicateDiagnostic = duplicateScopeDiagnostic(
|
|
2455
|
-
outputModes.length > 1 ? input.skills.map((skill) => skill.name).sort() : [],
|
|
2456
|
-
input.surfaceId
|
|
2457
|
-
);
|
|
2458
|
-
if (duplicateDiagnostic) diagnostics.push(duplicateDiagnostic);
|
|
2459
|
-
const manifests = new Map(
|
|
2460
|
-
outputModes.map((mode) => [mode, []])
|
|
2461
|
-
);
|
|
2462
|
-
for (const skill of [...input.skills].sort(
|
|
2463
|
-
(left, right) => left.name.localeCompare(right.name)
|
|
2464
|
-
)) {
|
|
2465
|
-
const sourceBaseRoot = input.packageRoot ?? input.projectRoot;
|
|
2466
|
-
const sourceRoot = path2.join(sourceBaseRoot, skill.sourcePath);
|
|
2467
|
-
const files = collectFiles(sourceRoot);
|
|
2468
|
-
if (files.length === 0) {
|
|
2469
|
-
diagnostics.push({
|
|
2470
|
-
severity: "warning",
|
|
2471
|
-
code: "codex.skill.source_missing",
|
|
2472
|
-
message: `Skipping Codex skill "${skill.name}" because source path "${skill.sourcePath}" was not found.`,
|
|
2473
|
-
harness: "codex",
|
|
2474
|
-
surface: input.surfaceId,
|
|
2475
|
-
fallback: "diagnostic-only"
|
|
2476
|
-
});
|
|
2477
|
-
continue;
|
|
2478
|
-
}
|
|
2479
|
-
for (const file of files) {
|
|
2480
|
-
const relative4 = normalizePath2(path2.relative(sourceRoot, file));
|
|
2481
|
-
const content = fs2.readFileSync(file, "utf8");
|
|
2482
|
-
const sourcePath = normalizePath2(path2.relative(sourceBaseRoot, file));
|
|
2483
|
-
for (const mode of outputModes) {
|
|
2484
|
-
const config = OUTPUT_MODE_CONFIG[mode];
|
|
2485
|
-
const outputPath = `${config.basePath}/${skill.name}/${relative4}`;
|
|
2486
|
-
artifacts.push({
|
|
2487
|
-
harness: "codex",
|
|
2488
|
-
kind: "skill",
|
|
2489
|
-
path: outputPath,
|
|
2490
|
-
description: `Codex ${config.label} skill artifact for ${skill.name}`,
|
|
2491
|
-
content
|
|
2492
|
-
});
|
|
2493
|
-
manifests.get(mode)?.push({
|
|
2494
|
-
name: skill.name,
|
|
2495
|
-
sourcePath,
|
|
2496
|
-
outputPath,
|
|
2497
|
-
sha256: sha2562(content)
|
|
2498
|
-
});
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
for (const mode of outputModes) {
|
|
2503
|
-
const config = OUTPUT_MODE_CONFIG[mode];
|
|
2504
|
-
artifacts.push({
|
|
2505
|
-
harness: "codex",
|
|
2506
|
-
kind: "manifest",
|
|
2507
|
-
path: config.manifestPath,
|
|
2508
|
-
description: `Generated Codex ${config.label} skill source manifest with source hashes.`,
|
|
2509
|
-
content: `${JSON.stringify({ generatedBy: "thoth-agents", skills: manifests.get(mode) ?? [] }, null, 2)}
|
|
2510
|
-
`
|
|
2511
|
-
});
|
|
2512
|
-
}
|
|
2513
|
-
return { artifacts, diagnostics };
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
// src/harness/adapters/codex.ts
|
|
2517
|
-
function readRootPackageVersion(context) {
|
|
2518
|
-
const packageJsonPath = findRootPackageJsonPath([
|
|
2519
|
-
...hasCodexPackageRoot(context) ? [context.packageRoot] : [],
|
|
2520
|
-
context.projectRoot,
|
|
2521
|
-
process.cwd(),
|
|
2522
|
-
fileURLToPath(new URL(".", import.meta.url))
|
|
2523
|
-
]);
|
|
2524
|
-
return readPackageJsonVersion(packageJsonPath);
|
|
2525
|
-
}
|
|
2526
|
-
function createCodexPluginPackageManifest(context) {
|
|
2527
|
-
return {
|
|
2528
|
-
name: "thoth-agents",
|
|
2529
|
-
version: readRootPackageVersion(context),
|
|
2530
|
-
description: "Delegate-first OpenCode plugin with seven agents, thoth-mem persistence, and bundled SDD skills."
|
|
2531
|
-
};
|
|
2532
|
-
}
|
|
2533
|
-
function findRootPackageJsonPath(startDirs) {
|
|
2534
|
-
for (const startDir of startDirs) {
|
|
2535
|
-
let currentDir = resolve(startDir);
|
|
2536
|
-
while (true) {
|
|
2537
|
-
const packageJsonPath = resolve(currentDir, "package.json");
|
|
2538
|
-
if (existsSync5(packageJsonPath)) {
|
|
2539
|
-
const packageJsonText = readFileSync4(packageJsonPath, "utf8");
|
|
2540
|
-
const packageJson = JSON.parse(packageJsonText);
|
|
2541
|
-
if (packageJson.name === "thoth-agents") {
|
|
2542
|
-
return packageJsonPath;
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
const parentDir = dirname2(currentDir);
|
|
2546
|
-
if (parentDir === currentDir) {
|
|
2547
|
-
break;
|
|
2548
|
-
}
|
|
2549
|
-
currentDir = parentDir;
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
throw new Error(
|
|
2553
|
-
"Unable to locate the thoth-agents root package.json from the render context or current working directory."
|
|
2554
|
-
);
|
|
2555
|
-
}
|
|
2556
|
-
function readPackageJsonVersion(packageJsonPath) {
|
|
2557
|
-
const packageJsonText = readFileSync4(packageJsonPath, "utf8");
|
|
2558
|
-
const packageJson = JSON.parse(packageJsonText);
|
|
2559
|
-
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
|
2560
|
-
throw new Error("Root package.json version must be a non-empty string.");
|
|
2561
|
-
}
|
|
2562
|
-
return packageJson.version;
|
|
2563
|
-
}
|
|
2564
|
-
function stableJson2(value) {
|
|
2565
|
-
return `${JSON.stringify(value, null, 2)}
|
|
2566
|
-
`;
|
|
2567
|
-
}
|
|
2568
|
-
function codexCommandConfig(commandParts) {
|
|
2569
|
-
const [command = "", ...args] = commandParts;
|
|
2570
|
-
return {
|
|
2571
|
-
command,
|
|
2572
|
-
...args.length > 0 ? { args } : {}
|
|
2573
|
-
};
|
|
2574
|
-
}
|
|
2575
|
-
function codexMcpConfig(config) {
|
|
2576
|
-
if ("url" in config) {
|
|
2577
|
-
return { url: config.url };
|
|
2578
|
-
}
|
|
2579
|
-
const [command = "", ...args] = config.command;
|
|
2580
|
-
return {
|
|
2581
|
-
command,
|
|
2582
|
-
...args.length > 0 ? { args } : {},
|
|
2583
|
-
...config.environment && Object.keys(config.environment).length > 0 ? { env: config.environment } : {}
|
|
2584
|
-
};
|
|
2585
|
-
}
|
|
2586
|
-
function createCodexBuiltinMcpServers() {
|
|
2587
|
-
return {
|
|
2588
|
-
exa: codexMcpConfig(exa),
|
|
2589
|
-
context7: { url: CONTEXT7_MCP_URL },
|
|
2590
|
-
grep_app: { url: GREP_APP_MCP_URL },
|
|
2591
|
-
thoth_mem: codexCommandConfig(DEFAULT_THOTH_COMMAND)
|
|
2592
|
-
};
|
|
2593
|
-
}
|
|
2594
|
-
function createCodexBuiltinMcpJsonConfig() {
|
|
2595
|
-
return {
|
|
2596
|
-
mcpServers: createCodexBuiltinMcpServers()
|
|
2597
|
-
};
|
|
2598
|
-
}
|
|
2599
|
-
function createCodexBuiltinMcpTomlConfig() {
|
|
2600
|
-
return {
|
|
2601
|
-
mcp_servers: createCodexBuiltinMcpServers()
|
|
2602
|
-
};
|
|
2603
|
-
}
|
|
2604
|
-
var CODEX_SUBAGENT_DEFAULT_MODELS = {
|
|
2605
|
-
oracle: "gpt-5.5",
|
|
2606
|
-
librarian: "gpt-5.4-mini",
|
|
2607
|
-
explorer: "gpt-5.4-mini",
|
|
2608
|
-
designer: "gpt-5.4-mini",
|
|
2609
|
-
quick: "gpt-5.4-mini",
|
|
2610
|
-
deep: "gpt-5.5"
|
|
2611
|
-
};
|
|
2612
|
-
var CODEX_SUBAGENT_REASONING_EFFORTS = {
|
|
2613
|
-
oracle: "high",
|
|
2614
|
-
explorer: "low",
|
|
2615
|
-
librarian: "medium",
|
|
2616
|
-
designer: "medium",
|
|
2617
|
-
quick: "low",
|
|
2618
|
-
deep: "medium"
|
|
2619
|
-
};
|
|
2620
|
-
var CODEX_ROOT_START = "<!-- thoth-agents:codex-root:start -->";
|
|
2621
|
-
var CODEX_ROOT_END = "<!-- thoth-agents:codex-root:end -->";
|
|
2622
|
-
var CODEX_CAPABILITIES = CODEX_PROMPT_DIALECT.capabilities.capabilities;
|
|
2623
|
-
function codexPromptSections(roleName) {
|
|
2624
|
-
switch (roleName) {
|
|
2625
|
-
case "orchestrator":
|
|
2626
|
-
return createOrchestratorPromptSections();
|
|
2627
|
-
case "explorer":
|
|
2628
|
-
case "librarian":
|
|
2629
|
-
case "oracle":
|
|
2630
|
-
return createReadOnlySpecialistPromptSections(roleName);
|
|
2631
|
-
case "designer":
|
|
2632
|
-
case "quick":
|
|
2633
|
-
case "deep":
|
|
2634
|
-
return createWriteCapableSpecialistPromptSections(roleName);
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
function codexModelFamilyPromptSection(roleName, model) {
|
|
2638
|
-
const section = createModelFamilySection(roleName, model);
|
|
2639
|
-
return section ? renderPromptSection(section, CODEX_PROMPT_DIALECT) : void 0;
|
|
2640
|
-
}
|
|
2641
|
-
function codexStepBudgetPromptSection(steps) {
|
|
2642
|
-
const section = createStepBudgetSection(steps);
|
|
2643
|
-
return section ? renderPromptSection(section, CODEX_PROMPT_DIALECT) : void 0;
|
|
2644
|
-
}
|
|
2645
|
-
function renderCodexRolePrompt(roleName, config, model) {
|
|
2646
|
-
const promptOverrides = loadAgentPrompt(roleName, config?.preset);
|
|
2647
|
-
const override = getAgentOverride(config, roleName);
|
|
2648
|
-
const basePrompt = renderRolePrompt(
|
|
2649
|
-
codexPromptSections(roleName),
|
|
2650
|
-
CODEX_PROMPT_DIALECT
|
|
2651
|
-
);
|
|
2652
|
-
const prompt = composeAgentPrompt({
|
|
2653
|
-
basePrompt,
|
|
2654
|
-
customPrompt: promptOverrides.prompt,
|
|
2655
|
-
customAppendPrompt: appendPromptSections(
|
|
2656
|
-
codexModelFamilyPromptSection(roleName, model),
|
|
2657
|
-
promptOverrides.appendPrompt
|
|
2658
|
-
)
|
|
2659
|
-
});
|
|
2660
|
-
return appendPromptSections(
|
|
2661
|
-
prompt,
|
|
2662
|
-
codexStepBudgetPromptSection(override?.steps)
|
|
2663
|
-
);
|
|
2664
|
-
}
|
|
2665
|
-
function codexRoleInstructions(role) {
|
|
2666
|
-
return [
|
|
2667
|
-
"<role-operational-contract>",
|
|
2668
|
-
`- Role: ${role.name}`,
|
|
2669
|
-
`- Mode: ${role.mode}`,
|
|
2670
|
-
`- Scope: ${role.scope}`,
|
|
2671
|
-
`- Responsibility: ${role.responsibility}`,
|
|
2672
|
-
"- Use request_user_input for local blocking decisions.",
|
|
2673
|
-
"- Permissions, memory governance, runtime hooks, and provider-per-agent controls are instruction-level unless the active Codex runtime documents stronger enforcement.",
|
|
2674
|
-
`- ${role.name} runs as a Codex custom-agent TOML entry; the orchestrator remains the ambient Codex root session, not a generated role TOML.`,
|
|
2675
|
-
...role.toolGovernance.map((rule) => `- ${rule}`),
|
|
2676
|
-
...role.verification.map((rule) => `- ${rule}`),
|
|
2677
|
-
"</role-operational-contract>"
|
|
2678
|
-
].join("\n");
|
|
2679
|
-
}
|
|
2680
|
-
function roleInstructions(role, config) {
|
|
2681
|
-
const model = getCodexAgentModel(role, config) ?? DEFAULT_MODELS[role.name];
|
|
2682
|
-
return [
|
|
2683
|
-
renderCodexRolePrompt(role.name, config, model),
|
|
2684
|
-
codexRoleInstructions(role),
|
|
2685
|
-
renderMemoryGovernanceInstructions(role, CODEX_PROMPT_DIALECT)
|
|
2686
|
-
].join("\n\n");
|
|
2687
|
-
}
|
|
2688
|
-
function renderCodexRootInstructions(config) {
|
|
2689
|
-
const rootOverride = getAgentOverride(config, "orchestrator");
|
|
2690
|
-
const rootPrompt = renderCodexRolePrompt(
|
|
2691
|
-
"orchestrator",
|
|
2692
|
-
config,
|
|
2693
|
-
rootOverride?.model ?? DEFAULT_MODELS.orchestrator
|
|
2694
|
-
);
|
|
2695
|
-
return [
|
|
2696
|
-
CODEX_ROOT_START,
|
|
2697
|
-
rootPrompt,
|
|
2698
|
-
"<codex-runtime>",
|
|
2699
|
-
"- 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.",
|
|
2700
|
-
"- 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.",
|
|
2701
|
-
"- If thoth-mem tools or identity values are unavailable, disclose that memory bootstrap could not run and continue without claiming memory was saved.",
|
|
2702
|
-
"- Use the ambient Codex root session as the delegate-first root coordinator; do not generate or select an orchestrator TOML.",
|
|
2703
|
-
"- Delegate by invoking the installed Codex role agents: explorer, librarian, oracle, designer, quick, and deep.",
|
|
2704
|
-
"- 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.",
|
|
2705
|
-
"- Use packaged thoth-agents plugin capabilities through Codex plugin, skill, MCP, and hook review surfaces after enabling them with /plugins and /hooks.",
|
|
2706
|
-
"- 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.",
|
|
2707
|
-
"- Memory governance, role permissions, provider-per-agent controls, and hooks are instruction-level unless the active Codex runtime documents stronger enforcement.",
|
|
2708
|
-
"</codex-runtime>",
|
|
2709
|
-
CODEX_ROOT_END,
|
|
2710
|
-
""
|
|
2711
|
-
].join("\n");
|
|
2712
|
-
}
|
|
2713
|
-
function agentModelReasoningEffort(role) {
|
|
2714
|
-
return isCodexSubagentName(role.name) ? CODEX_SUBAGENT_REASONING_EFFORTS[role.name] : "medium";
|
|
2715
|
-
}
|
|
2716
|
-
function isCodexSubagentName(name) {
|
|
2717
|
-
return name in CODEX_SUBAGENT_DEFAULT_MODELS;
|
|
2718
|
-
}
|
|
2719
|
-
function getPrimaryModelId2(model) {
|
|
2720
|
-
if (Array.isArray(model)) {
|
|
2721
|
-
const first = model[0];
|
|
2722
|
-
return typeof first === "string" ? first : first?.id;
|
|
2723
|
-
}
|
|
2724
|
-
return model;
|
|
2725
|
-
}
|
|
2726
|
-
function getCodexAgentModel(role, config) {
|
|
2727
|
-
if (!isCodexSubagentName(role.name)) return void 0;
|
|
2728
|
-
return getPrimaryModelId2(config?.agents?.[role.name]?.model) ?? CODEX_SUBAGENT_DEFAULT_MODELS[role.name];
|
|
2729
|
-
}
|
|
2730
|
-
function codexSurfaceHasField(surfaceId, field) {
|
|
2731
|
-
return getCodexSurface(surfaceId)?.fields.includes(field) ?? false;
|
|
2732
|
-
}
|
|
2733
|
-
function hasCodexConfig(context) {
|
|
2734
|
-
return "config" in context;
|
|
2735
|
-
}
|
|
2736
|
-
function hasCodexPackageRoot(context) {
|
|
2737
|
-
return "packageRoot" in context && typeof context.packageRoot === "string" && context.packageRoot.length > 0;
|
|
2738
|
-
}
|
|
2739
|
-
function renderAgentArtifacts({ config }) {
|
|
2740
|
-
const artifacts = [];
|
|
2741
|
-
const diagnostics = [];
|
|
2742
|
-
const supportsReasoningEffort = codexSurfaceHasField(
|
|
2743
|
-
"project-agent-toml",
|
|
2744
|
-
"model_reasoning_effort"
|
|
2745
|
-
);
|
|
2746
|
-
for (const role of getAgentPackContract().roles.filter(
|
|
2747
|
-
(candidate) => candidate.name !== "orchestrator"
|
|
2748
|
-
)) {
|
|
2749
|
-
const model = getCodexAgentModel(role, config);
|
|
2750
|
-
const toml = renderCodexToml({
|
|
2751
|
-
surfaceId: "project-agent-toml",
|
|
2752
|
-
values: {
|
|
2753
|
-
name: role.name,
|
|
2754
|
-
description: role.responsibility,
|
|
2755
|
-
developer_instructions: roleInstructions(role, config),
|
|
2756
|
-
...model ? { model } : {},
|
|
2757
|
-
...supportsReasoningEffort ? { model_reasoning_effort: agentModelReasoningEffort(role) } : {},
|
|
2758
|
-
sandbox_mode: role.canMutateWorkspace ? "workspace-write" : "read-only"
|
|
2759
|
-
}
|
|
2760
|
-
});
|
|
2761
|
-
diagnostics.push(...toml.diagnostics);
|
|
2762
|
-
artifacts.push({
|
|
2763
|
-
harness: "codex",
|
|
2764
|
-
kind: "agent-config",
|
|
2765
|
-
path: `.codex/agents/thoth-agents-${role.name}.toml`,
|
|
2766
|
-
description: `Codex agent definition for ${role.name}.`,
|
|
2767
|
-
content: toml.content
|
|
2768
|
-
});
|
|
2769
|
-
}
|
|
2770
|
-
return { artifacts, diagnostics };
|
|
2771
|
-
}
|
|
2772
|
-
function renderConfigArtifacts() {
|
|
2773
|
-
const config = renderCodexToml({
|
|
2774
|
-
surfaceId: "project-config-toml",
|
|
2775
|
-
values: {
|
|
2776
|
-
approval_policy: "on-request",
|
|
2777
|
-
sandbox_mode: "workspace-write",
|
|
2778
|
-
"skills.config": { enabled: true, sources: ["repo"] },
|
|
2779
|
-
agents: getAgentPackContract().roles.filter((role) => role.name !== "orchestrator").map((role) => role.name)
|
|
2780
|
-
}
|
|
2781
|
-
});
|
|
2782
|
-
const mcp = renderCodexToml({
|
|
2783
|
-
surfaceId: "mcp-server-config",
|
|
2784
|
-
values: createCodexBuiltinMcpTomlConfig()
|
|
2785
|
-
});
|
|
2786
|
-
return {
|
|
2787
|
-
artifacts: [
|
|
2788
|
-
{
|
|
2789
|
-
harness: "codex",
|
|
2790
|
-
kind: "harness-config",
|
|
2791
|
-
path: ".codex/config.toml",
|
|
2792
|
-
description: "Codex project configuration snippet for the agent pack.",
|
|
2793
|
-
content: config.content
|
|
2794
|
-
},
|
|
2795
|
-
{
|
|
2796
|
-
harness: "codex",
|
|
2797
|
-
kind: "mcp-config",
|
|
2798
|
-
path: ".codex/config.toml",
|
|
2799
|
-
description: "Codex MCP configuration snippet for thoth-mem.",
|
|
2800
|
-
content: mcp.content
|
|
2801
|
-
}
|
|
2802
|
-
],
|
|
2803
|
-
diagnostics: [...config.diagnostics, ...mcp.diagnostics]
|
|
2804
|
-
};
|
|
2805
|
-
}
|
|
2806
|
-
function capabilityDiagnostics() {
|
|
2807
|
-
const surfaceDiagnostics = getCodexSurfaceRecords().filter((surface) => surface.status !== "validated").map(codexSurfaceDiagnostic);
|
|
2808
|
-
const governanceDiagnostics = memoryGovernanceDiagnostics({
|
|
2809
|
-
harness: "codex",
|
|
2810
|
-
permissionControls: CODEX_CAPABILITIES.rolePermissions,
|
|
2811
|
-
parentContextInjection: CODEX_CAPABILITIES.parentContextInjection,
|
|
2812
|
-
memoryWriteControls: CODEX_CAPABILITIES.memoryGovernanceEnforcement
|
|
2813
|
-
});
|
|
2814
|
-
return [...surfaceDiagnostics, ...governanceDiagnostics];
|
|
2815
|
-
}
|
|
2816
|
-
function hookReadinessDiagnostics() {
|
|
2817
|
-
return [
|
|
2818
|
-
{
|
|
2819
|
-
severity: "warning",
|
|
2820
|
-
code: "codex.hooks.project_trust.required",
|
|
2821
|
-
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.",
|
|
2822
|
-
harness: "codex",
|
|
2823
|
-
surface: "project-hooks-json",
|
|
2824
|
-
fallback: "diagnostic-only"
|
|
2825
|
-
},
|
|
2826
|
-
{
|
|
2827
|
-
severity: "warning",
|
|
2828
|
-
code: "codex.hooks.features_hooks.required",
|
|
2829
|
-
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.",
|
|
2830
|
-
harness: "codex",
|
|
2831
|
-
surface: "features-hooks-toggle",
|
|
2832
|
-
fallback: "diagnostic-only"
|
|
2833
|
-
},
|
|
2834
|
-
{
|
|
2835
|
-
severity: "warning",
|
|
2836
|
-
code: "codex.hooks.plugin_trust.required",
|
|
2837
|
-
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.",
|
|
2838
|
-
harness: "codex",
|
|
2839
|
-
surface: "plugin-hooks-bundle",
|
|
2840
|
-
fallback: "diagnostic-only"
|
|
2841
|
-
}
|
|
2842
|
-
];
|
|
2843
|
-
}
|
|
2844
|
-
function resolveSkillOutputModes(context) {
|
|
2845
|
-
return context.options?.codexSkillOutputModes ?? ["plugin-package"];
|
|
2846
|
-
}
|
|
2847
|
-
var codexAdapter = {
|
|
2848
|
-
id: "codex",
|
|
2849
|
-
displayName: "Codex",
|
|
2850
|
-
capabilities: CODEX_CAPABILITIES,
|
|
2851
|
-
render(context) {
|
|
2852
|
-
const config = hasCodexConfig(context) ? context.config : void 0;
|
|
2853
|
-
const agentArtifacts = renderAgentArtifacts({ config });
|
|
2854
|
-
const configArtifacts = renderConfigArtifacts();
|
|
2855
|
-
const skillOutputModes = resolveSkillOutputModes(context);
|
|
2856
|
-
const skillLayout = renderCodexSkillLayout({
|
|
2857
|
-
projectRoot: context.projectRoot,
|
|
2858
|
-
...hasCodexPackageRoot(context) ? { packageRoot: context.packageRoot } : {},
|
|
2859
|
-
skills: getSkillRegistry(),
|
|
2860
|
-
surfaceId: "plugin-skills-directory",
|
|
2861
|
-
outputModes: skillOutputModes
|
|
2862
|
-
});
|
|
2863
|
-
const pluginPackage = renderCodexPluginPackage({
|
|
2864
|
-
manifest: createCodexPluginPackageManifest(context),
|
|
2865
|
-
assets: [
|
|
2866
|
-
{
|
|
2867
|
-
surfaceId: "plugin-skills-directory",
|
|
2868
|
-
manifestField: "skills",
|
|
2869
|
-
path: ".codex-plugin/skills/",
|
|
2870
|
-
description: "Codex plugin-bundled skill directory."
|
|
2871
|
-
},
|
|
2872
|
-
{
|
|
2873
|
-
surfaceId: "plugin-mcp-json",
|
|
2874
|
-
manifestField: "mcpServers",
|
|
2875
|
-
path: ".codex-plugin/.mcp.json",
|
|
2876
|
-
description: "Codex plugin-bundled MCP server definitions.",
|
|
2877
|
-
content: stableJson2(createCodexBuiltinMcpJsonConfig())
|
|
2878
|
-
},
|
|
2879
|
-
{
|
|
2880
|
-
surfaceId: "plugin-hooks-json",
|
|
2881
|
-
manifestField: "hooks",
|
|
2882
|
-
path: ".codex-plugin/hooks/hooks.json",
|
|
2883
|
-
description: "Codex plugin-bundled hook configuration.",
|
|
2884
|
-
hookDefinitions: []
|
|
2885
|
-
}
|
|
2886
|
-
]
|
|
2887
|
-
});
|
|
2888
|
-
return {
|
|
2889
|
-
harness: "codex",
|
|
2890
|
-
artifacts: [
|
|
2891
|
-
...agentArtifacts.artifacts,
|
|
2892
|
-
...configArtifacts.artifacts,
|
|
2893
|
-
...pluginPackage.artifacts,
|
|
2894
|
-
...skillLayout.artifacts
|
|
2895
|
-
],
|
|
2896
|
-
diagnostics: [
|
|
2897
|
-
...agentArtifacts.diagnostics,
|
|
2898
|
-
...configArtifacts.diagnostics,
|
|
2899
|
-
...pluginPackage.diagnostics,
|
|
2900
|
-
...skillLayout.diagnostics,
|
|
2901
|
-
...hookReadinessDiagnostics(),
|
|
2902
|
-
...capabilityDiagnostics()
|
|
2903
|
-
]
|
|
2904
|
-
};
|
|
2905
|
-
}
|
|
2906
|
-
};
|
|
2907
|
-
|
|
2908
|
-
// src/cli/install.ts
|
|
2909
|
-
import { existsSync as existsSync10 } from "fs";
|
|
2910
|
-
import { homedir as homedir3 } from "os";
|
|
2911
|
-
import { cwd } from "process";
|
|
2912
|
-
|
|
2913
|
-
// src/cli/codex-install.ts
|
|
2914
|
-
import {
|
|
2915
|
-
copyFileSync as copyFileSync4,
|
|
2916
|
-
existsSync as existsSync9,
|
|
2917
|
-
mkdirSync as mkdirSync5,
|
|
2918
|
-
readFileSync as readFileSync7,
|
|
2919
|
-
rmSync as rmSync2,
|
|
2920
|
-
writeFileSync as writeFileSync4
|
|
2921
|
-
} from "fs";
|
|
2922
|
-
import { basename, dirname as dirname5, isAbsolute, join as join8, relative as relative3 } from "path";
|
|
2923
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2924
|
-
|
|
2925
|
-
// src/harness/codex-plugin-paths.ts
|
|
2926
|
-
import { join as join4 } from "path";
|
|
2927
|
-
var CODEX_PLUGIN_ARTIFACT_PREFIX = ".codex-plugin/";
|
|
2928
|
-
function codexPluginRootArtifactPath(artifactPath) {
|
|
2929
|
-
if (!artifactPath.startsWith(CODEX_PLUGIN_ARTIFACT_PREFIX)) {
|
|
2930
|
-
throw new Error(`Expected Codex plugin artifact path: ${artifactPath}`);
|
|
2931
|
-
}
|
|
2932
|
-
const relativePath = artifactPath.slice(CODEX_PLUGIN_ARTIFACT_PREFIX.length);
|
|
2933
|
-
if (relativePath === ".mcp.json") return relativePath;
|
|
2934
|
-
if (relativePath === "plugin.json" || relativePath.startsWith(".")) {
|
|
2935
|
-
return join4(".codex-plugin", relativePath);
|
|
2936
|
-
}
|
|
2937
|
-
return relativePath;
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
// src/cli/codex-config-io.ts
|
|
2941
|
-
import {
|
|
2942
|
-
copyFileSync as copyFileSync2,
|
|
2943
|
-
existsSync as existsSync6,
|
|
2944
|
-
mkdirSync as mkdirSync2,
|
|
2945
|
-
readFileSync as readFileSync5,
|
|
2946
|
-
renameSync as renameSync2,
|
|
2947
|
-
writeFileSync as writeFileSync2
|
|
2948
|
-
} from "fs";
|
|
2949
|
-
import { dirname as dirname3 } from "path";
|
|
2950
|
-
function splitTomlLines(content) {
|
|
2951
|
-
return (content.match(/[^\r\n]*(?:\r\n|\n|\r)|[^\r\n]+$/g) ?? []).map(
|
|
2952
|
-
(rawLine) => {
|
|
2953
|
-
const eol = /(\r\n|\n|\r)$/.exec(rawLine)?.[1] ?? "";
|
|
2954
|
-
return {
|
|
2955
|
-
text: eol ? rawLine.slice(0, -eol.length) : rawLine,
|
|
2956
|
-
eol
|
|
2957
|
-
};
|
|
2958
|
-
}
|
|
2959
|
-
);
|
|
2960
|
-
}
|
|
2961
|
-
function tomlLineBreak(content) {
|
|
2962
|
-
return /(\r\n|\n|\r)/.exec(content)?.[1] ?? "\n";
|
|
2963
|
-
}
|
|
2964
|
-
function uncommentedTomlLine(line) {
|
|
2965
|
-
return line.split("#", 1)[0].trim();
|
|
2966
|
-
}
|
|
2967
|
-
function isTomlTableHeader(line) {
|
|
2968
|
-
const trimmed = uncommentedTomlLine(line);
|
|
2969
|
-
return trimmed.startsWith("[[") && trimmed.endsWith("]]") || trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
2970
|
-
}
|
|
2971
|
-
function isFeaturesHeader(line) {
|
|
2972
|
-
return uncommentedTomlLine(line) === "[features]";
|
|
2973
|
-
}
|
|
2974
|
-
function renderTomlLines(lines) {
|
|
2975
|
-
return lines.map((line) => `${line.text}${line.eol}`).join("");
|
|
2976
|
-
}
|
|
2977
|
-
function ensureLineHasEol(lines, index, eol) {
|
|
2978
|
-
if (index >= 0 && lines[index]?.eol === "") lines[index].eol = eol;
|
|
2979
|
-
}
|
|
2980
|
-
function appendFeaturesSection(content) {
|
|
2981
|
-
const eol = tomlLineBreak(content);
|
|
2982
|
-
if (!content) {
|
|
2983
|
-
return `[features]${eol}default_mode_request_user_input = true${eol}`;
|
|
2984
|
-
}
|
|
2985
|
-
const separator = content.endsWith("\n") || content.endsWith("\r") ? content.endsWith(`${eol}${eol}`) ? "" : eol : `${eol}${eol}`;
|
|
2986
|
-
return `${content}${separator}[features]${eol}default_mode_request_user_input = true${eol}`;
|
|
2987
|
-
}
|
|
2988
|
-
function patchCodexDefaultModeUserInputFeature(content) {
|
|
2989
|
-
const lines = splitTomlLines(content);
|
|
2990
|
-
const eol = tomlLineBreak(content);
|
|
2991
|
-
const featuresStart = lines.findIndex((line) => isFeaturesHeader(line.text));
|
|
2992
|
-
if (featuresStart === -1) return appendFeaturesSection(content);
|
|
2993
|
-
let featuresEnd = lines.length;
|
|
2994
|
-
for (let index = featuresStart + 1; index < lines.length; index++) {
|
|
2995
|
-
if (isTomlTableHeader(lines[index].text)) {
|
|
2996
|
-
featuresEnd = index;
|
|
2997
|
-
break;
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
const flagPattern = /^(\s*default_mode_request_user_input\s*=\s*)(true|false)(\b.*)$/;
|
|
3001
|
-
for (let index = featuresStart + 1; index < featuresEnd; index++) {
|
|
3002
|
-
const match = flagPattern.exec(lines[index].text);
|
|
3003
|
-
if (!match) continue;
|
|
3004
|
-
if (match[2] === "true") return content;
|
|
3005
|
-
lines[index].text = `${match[1]}true${match[3]}`;
|
|
3006
|
-
return renderTomlLines(lines);
|
|
3007
|
-
}
|
|
3008
|
-
let insertAt = featuresEnd;
|
|
3009
|
-
while (insertAt > featuresStart + 1 && lines[insertAt - 1].text.trim() === "")
|
|
3010
|
-
insertAt--;
|
|
3011
|
-
ensureLineHasEol(lines, insertAt - 1, eol);
|
|
3012
|
-
lines.splice(insertAt, 0, {
|
|
3013
|
-
text: "default_mode_request_user_input = true",
|
|
3014
|
-
eol
|
|
3015
|
-
});
|
|
3016
|
-
return renderTomlLines(lines);
|
|
3017
|
-
}
|
|
3018
|
-
function buildCodexManagedConfigPatch(content, options) {
|
|
3019
|
-
const diffSummary = [
|
|
3020
|
-
"ensure features.default_mode_request_user_input = true"
|
|
3021
|
-
];
|
|
3022
|
-
if (options.pluginId) {
|
|
3023
|
-
diffSummary.push(
|
|
3024
|
-
`plugin enablement for "${options.pluginId}" is not textually merged; use /plugins to enable it`
|
|
3025
|
-
);
|
|
3026
|
-
} else {
|
|
3027
|
-
diffSummary.push(
|
|
3028
|
-
"plugin enablement left to /plugins; no guessed plugin id written"
|
|
3029
|
-
);
|
|
3030
|
-
}
|
|
3031
|
-
return {
|
|
3032
|
-
content: patchCodexDefaultModeUserInputFeature(content),
|
|
3033
|
-
diffSummary,
|
|
3034
|
-
warnings: []
|
|
3035
|
-
};
|
|
3036
|
-
}
|
|
3037
|
-
function writeCodexConfigMerge(options) {
|
|
3038
|
-
try {
|
|
3039
|
-
const before = existsSync6(options.configPath) ? readFileSync5(options.configPath, "utf8") : "";
|
|
3040
|
-
const merged = buildCodexManagedConfigPatch(before, {
|
|
3041
|
-
pluginId: options.pluginId
|
|
3042
|
-
});
|
|
3043
|
-
const changed = before !== merged.content;
|
|
3044
|
-
if (options.dryRun || !changed) {
|
|
3045
|
-
return {
|
|
3046
|
-
success: true,
|
|
3047
|
-
configPath: options.configPath,
|
|
3048
|
-
changed,
|
|
3049
|
-
diffSummary: merged.diffSummary,
|
|
3050
|
-
warnings: merged.warnings
|
|
3051
|
-
};
|
|
3052
|
-
}
|
|
3053
|
-
mkdirSync2(dirname3(options.configPath), { recursive: true });
|
|
3054
|
-
const backupPath = `${options.configPath}.bak`;
|
|
3055
|
-
if (existsSync6(options.configPath))
|
|
3056
|
-
copyFileSync2(options.configPath, backupPath);
|
|
3057
|
-
const tmpPath = `${options.configPath}.tmp`;
|
|
3058
|
-
writeFileSync2(tmpPath, merged.content);
|
|
3059
|
-
renameSync2(tmpPath, options.configPath);
|
|
3060
|
-
return {
|
|
3061
|
-
success: true,
|
|
3062
|
-
configPath: options.configPath,
|
|
3063
|
-
backupPath: existsSync6(backupPath) ? backupPath : void 0,
|
|
3064
|
-
changed,
|
|
3065
|
-
diffSummary: merged.diffSummary,
|
|
3066
|
-
warnings: merged.warnings
|
|
3067
|
-
};
|
|
3068
|
-
} catch (error) {
|
|
3069
|
-
return {
|
|
3070
|
-
success: false,
|
|
3071
|
-
configPath: options.configPath,
|
|
3072
|
-
changed: false,
|
|
3073
|
-
diffSummary: [],
|
|
3074
|
-
warnings: [],
|
|
3075
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3076
|
-
};
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3079
|
-
|
|
3080
|
-
// src/cli/codex-paths.ts
|
|
3081
|
-
import { homedir as homedir2 } from "os";
|
|
3082
|
-
import { join as join5 } from "path";
|
|
3083
|
-
var CODEX_ROLE_NAMES = [
|
|
3084
|
-
"explorer",
|
|
3085
|
-
"librarian",
|
|
3086
|
-
"oracle",
|
|
3087
|
-
"designer",
|
|
3088
|
-
"quick",
|
|
3089
|
-
"deep"
|
|
3090
|
-
];
|
|
3091
|
-
function getCodexHome(options = {}) {
|
|
3092
|
-
const explicit = options.codexHome ?? process.env.CODEX_HOME?.trim();
|
|
3093
|
-
return explicit || join5(options.homeDir ?? homedir2(), ".codex");
|
|
3094
|
-
}
|
|
3095
|
-
function resolveCodexTargets(options) {
|
|
3096
|
-
const codexHome = getCodexHome(options);
|
|
3097
|
-
const agentsDir = options.scope === "project" ? join5(options.projectRoot, ".codex", "agents") : join5(codexHome, "agents");
|
|
3098
|
-
return {
|
|
3099
|
-
scope: options.scope,
|
|
3100
|
-
codexHome,
|
|
3101
|
-
configPath: join5(codexHome, "config.toml"),
|
|
3102
|
-
rootInstructionsPath: join5(codexHome, "AGENTS.md"),
|
|
3103
|
-
roleAgentPaths: CODEX_ROLE_NAMES.map((role) => ({
|
|
3104
|
-
role,
|
|
3105
|
-
path: join5(agentsDir, `thoth-agents-${role}.toml`)
|
|
3106
|
-
})),
|
|
3107
|
-
managedModelsPath: join5(
|
|
3108
|
-
codexHome,
|
|
3109
|
-
"agents",
|
|
3110
|
-
".thoth-agents-managed-models.json"
|
|
3111
|
-
),
|
|
3112
|
-
skillsDir: options.scope === "project" ? join5(options.projectRoot, ".agents", "skills") : join5(options.homeDir ?? homedir2(), ".agents", "skills"),
|
|
3113
|
-
packageRoot: join5(options.projectRoot, ".codex-plugin"),
|
|
3114
|
-
personalPluginRoot: join5(codexHome, "plugins", "thoth-agents"),
|
|
3115
|
-
personalMarketplacePath: join5(
|
|
3116
|
-
options.homeDir ?? homedir2(),
|
|
3117
|
-
".agents",
|
|
3118
|
-
"plugins",
|
|
3119
|
-
"marketplace.json"
|
|
3120
|
-
)
|
|
3121
|
-
};
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
// src/cli/custom-skills.ts
|
|
3125
|
-
import {
|
|
3126
|
-
copyFileSync as copyFileSync3,
|
|
3127
|
-
existsSync as existsSync8,
|
|
3128
|
-
mkdirSync as mkdirSync4,
|
|
3129
|
-
readdirSync as readdirSync3,
|
|
3130
|
-
rmSync,
|
|
3131
|
-
statSync as statSync3
|
|
3132
|
-
} from "fs";
|
|
3133
|
-
import { dirname as dirname4, join as join7, parse } from "path";
|
|
3134
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3135
|
-
|
|
3136
|
-
// src/cli/skill-manifest.ts
|
|
3137
|
-
import { createHash as createHash3 } from "crypto";
|
|
3138
|
-
import {
|
|
3139
|
-
existsSync as existsSync7,
|
|
3140
|
-
mkdirSync as mkdirSync3,
|
|
3141
|
-
readdirSync as readdirSync2,
|
|
3142
|
-
readFileSync as readFileSync6,
|
|
3143
|
-
statSync as statSync2,
|
|
3144
|
-
writeFileSync as writeFileSync3
|
|
3145
|
-
} from "fs";
|
|
3146
|
-
import { join as join6, relative as relative2 } from "path";
|
|
3147
|
-
var SHARED_SKILL_DIRECTORY = "_shared";
|
|
3148
|
-
var SKILLS_SOURCE_ROOT = join6("src", "skills");
|
|
3149
|
-
var MANIFEST_FILE_NAME = ".skill-manifest.json";
|
|
3150
|
-
function getManifestPath() {
|
|
3151
|
-
return join6(getCustomSkillsDir(), MANIFEST_FILE_NAME);
|
|
3152
|
-
}
|
|
3153
|
-
function listFilesRecursive(dirPath) {
|
|
3154
|
-
if (!existsSync7(dirPath)) {
|
|
3155
|
-
return [];
|
|
3156
|
-
}
|
|
3157
|
-
const files = [];
|
|
3158
|
-
const entries = readdirSync2(dirPath).sort(
|
|
3159
|
-
(left, right) => left.localeCompare(right)
|
|
3160
|
-
);
|
|
3161
|
-
for (const entry of entries) {
|
|
3162
|
-
const entryPath = join6(dirPath, entry);
|
|
3163
|
-
const stat = statSync2(entryPath);
|
|
3164
|
-
if (stat.isDirectory()) {
|
|
3165
|
-
files.push(...listFilesRecursive(entryPath));
|
|
3166
|
-
continue;
|
|
3167
|
-
}
|
|
3168
|
-
files.push(entryPath);
|
|
3169
|
-
}
|
|
3170
|
-
return files;
|
|
3171
|
-
}
|
|
3172
|
-
function readPackageVersion(packageRoot) {
|
|
3173
|
-
const packageJsonPath = join6(packageRoot, "package.json");
|
|
3174
|
-
const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
|
|
3175
|
-
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
|
3176
|
-
throw new Error(`Invalid package version in ${packageJsonPath}`);
|
|
3177
|
-
}
|
|
3178
|
-
return packageJson.version;
|
|
3179
|
-
}
|
|
3180
|
-
function readManifest() {
|
|
3181
|
-
const manifestPath = getManifestPath();
|
|
3182
|
-
if (!existsSync7(manifestPath)) {
|
|
3183
|
-
return null;
|
|
3184
|
-
}
|
|
3185
|
-
try {
|
|
3186
|
-
return JSON.parse(readFileSync6(manifestPath, "utf-8"));
|
|
3187
|
-
} catch (error) {
|
|
3188
|
-
console.warn(`Failed to read skill manifest: ${manifestPath}`, error);
|
|
3189
|
-
return null;
|
|
3190
|
-
}
|
|
3191
|
-
}
|
|
3192
|
-
function writeManifest(manifest) {
|
|
3193
|
-
const manifestPath = getManifestPath();
|
|
3194
|
-
mkdirSync3(getCustomSkillsDir(), { recursive: true });
|
|
3195
|
-
writeFileSync3(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
3196
|
-
`);
|
|
3197
|
-
}
|
|
3198
|
-
function computeSkillHash(skillDirPath) {
|
|
3199
|
-
const hash = createHash3("sha256");
|
|
3200
|
-
for (const filePath of listFilesRecursive(skillDirPath)) {
|
|
3201
|
-
hash.update(`${relative2(skillDirPath, filePath)}
|
|
3202
|
-
`);
|
|
3203
|
-
hash.update(readFileSync6(filePath));
|
|
3204
|
-
hash.update("\n");
|
|
3205
|
-
}
|
|
3206
|
-
return hash.digest("hex");
|
|
3207
|
-
}
|
|
3208
|
-
function findRemovedSkills(manifest) {
|
|
3209
|
-
const currentSkillNames = new Set(CUSTOM_SKILLS.map((skill) => skill.name));
|
|
3210
|
-
return Object.keys(manifest.skills).filter(
|
|
3211
|
-
(skillName) => !currentSkillNames.has(skillName)
|
|
3212
|
-
);
|
|
3213
|
-
}
|
|
3214
|
-
function checkSkillsNeedUpdate(packageRoot) {
|
|
3215
|
-
const manifest = readManifest();
|
|
3216
|
-
const pluginVersion = readPackageVersion(packageRoot);
|
|
3217
|
-
const sharedHash = computeSkillHash(
|
|
3218
|
-
join6(packageRoot, SKILLS_SOURCE_ROOT, SHARED_SKILL_DIRECTORY)
|
|
3219
|
-
);
|
|
3220
|
-
const versionChanged = manifest !== null && manifest.pluginVersion !== pluginVersion;
|
|
3221
|
-
const sharedChanged = manifest !== null && manifest.sharedHash !== sharedHash;
|
|
3222
|
-
const removedSkills = manifest ? findRemovedSkills(manifest) : [];
|
|
3223
|
-
const skillsNeedingUpdate = CUSTOM_SKILLS.flatMap((skill) => {
|
|
3224
|
-
const sourcePath = join6(packageRoot, skill.sourcePath);
|
|
3225
|
-
const targetPath = join6(getCustomSkillsDir(), skill.name);
|
|
3226
|
-
const sourceHash = computeSkillHash(sourcePath);
|
|
3227
|
-
const manifestEntry = manifest?.skills[skill.name];
|
|
3228
|
-
const reasons = [];
|
|
3229
|
-
if (!manifest) {
|
|
3230
|
-
reasons.push("manifest-missing");
|
|
3231
|
-
}
|
|
3232
|
-
if (versionChanged) {
|
|
3233
|
-
reasons.push("version-change");
|
|
3234
|
-
}
|
|
3235
|
-
if (sharedChanged) {
|
|
3236
|
-
reasons.push("shared-hash-mismatch");
|
|
3237
|
-
}
|
|
3238
|
-
if (!manifestEntry) {
|
|
3239
|
-
reasons.push("new-skill");
|
|
3240
|
-
} else if (manifestEntry.hash !== sourceHash) {
|
|
3241
|
-
reasons.push("hash-mismatch");
|
|
3242
|
-
}
|
|
3243
|
-
if (!existsSync7(targetPath)) {
|
|
3244
|
-
reasons.push("missing-install");
|
|
3245
|
-
}
|
|
3246
|
-
if (reasons.length === 0) {
|
|
3247
|
-
return [];
|
|
3248
|
-
}
|
|
3249
|
-
return [
|
|
3250
|
-
{
|
|
3251
|
-
skill,
|
|
3252
|
-
sourceHash,
|
|
3253
|
-
targetPath,
|
|
3254
|
-
reasons
|
|
3255
|
-
}
|
|
3256
|
-
];
|
|
3257
|
-
});
|
|
3258
|
-
return {
|
|
3259
|
-
pluginVersion,
|
|
3260
|
-
sharedHash,
|
|
3261
|
-
manifest,
|
|
3262
|
-
versionChanged,
|
|
3263
|
-
sharedChanged,
|
|
3264
|
-
needsUpdate: skillsNeedingUpdate.length > 0 || removedSkills.length > 0,
|
|
3265
|
-
skillsNeedingUpdate,
|
|
3266
|
-
removedSkills
|
|
3267
|
-
};
|
|
3268
|
-
}
|
|
3269
|
-
|
|
3270
|
-
// src/cli/custom-skills.ts
|
|
3271
|
-
var SHARED_SKILL_DIRECTORY2 = "_shared";
|
|
3272
|
-
var SHARED_SKILL_SOURCE_PATH = `src/skills/${SHARED_SKILL_DIRECTORY2}`;
|
|
3273
|
-
var CUSTOM_SKILLS = BUNDLED_SKILL_REGISTRY.map(
|
|
3274
|
-
(skill) => ({
|
|
3275
|
-
name: skill.name,
|
|
3276
|
-
description: skill.description,
|
|
3277
|
-
allowedAgents: [...skill.allowedRoles],
|
|
3278
|
-
sourcePath: skill.sourcePath
|
|
3279
|
-
})
|
|
3280
|
-
);
|
|
3281
|
-
function getCustomSkillsDir() {
|
|
3282
|
-
return join7(getConfigDir(), "skills");
|
|
3283
|
-
}
|
|
3284
|
-
function copyDirRecursive(src, dest) {
|
|
3285
|
-
if (!existsSync8(dest)) {
|
|
3286
|
-
mkdirSync4(dest, { recursive: true });
|
|
3287
|
-
}
|
|
3288
|
-
const entries = readdirSync3(src);
|
|
3289
|
-
for (const entry of entries) {
|
|
3290
|
-
const srcPath = join7(src, entry);
|
|
3291
|
-
const destPath = join7(dest, entry);
|
|
3292
|
-
const stat = statSync3(srcPath);
|
|
3293
|
-
if (stat.isDirectory()) {
|
|
3294
|
-
copyDirRecursive(srcPath, destPath);
|
|
3295
|
-
} else {
|
|
3296
|
-
const destDir = dirname4(destPath);
|
|
3297
|
-
if (!existsSync8(destDir)) {
|
|
3298
|
-
mkdirSync4(destDir, { recursive: true });
|
|
3299
|
-
}
|
|
3300
|
-
copyFileSync3(srcPath, destPath);
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
}
|
|
3304
|
-
function installSharedSkillAssets(packageRoot) {
|
|
3305
|
-
const sharedSourcePath = join7(packageRoot, SHARED_SKILL_SOURCE_PATH);
|
|
3306
|
-
const sharedTargetPath = join7(getCustomSkillsDir(), SHARED_SKILL_DIRECTORY2);
|
|
3307
|
-
if (!existsSync8(sharedSourcePath)) {
|
|
3308
|
-
console.error(`Custom skill shared assets not found: ${sharedSourcePath}`);
|
|
3309
|
-
return false;
|
|
3310
|
-
}
|
|
3311
|
-
rmSync(sharedTargetPath, { recursive: true, force: true });
|
|
3312
|
-
copyDirRecursive(sharedSourcePath, sharedTargetPath);
|
|
3313
|
-
return true;
|
|
3314
|
-
}
|
|
3315
|
-
function findPackageRoot(startDir) {
|
|
3316
|
-
let currentDir = startDir;
|
|
3317
|
-
const filesystemRoot = parse(startDir).root;
|
|
3318
|
-
while (true) {
|
|
3319
|
-
if (existsSync8(join7(currentDir, "package.json")) && existsSync8(join7(currentDir, "src", "skills"))) {
|
|
3320
|
-
return currentDir;
|
|
3321
|
-
}
|
|
3322
|
-
if (currentDir === filesystemRoot) {
|
|
3323
|
-
return null;
|
|
3324
|
-
}
|
|
3325
|
-
const parentDir = dirname4(currentDir);
|
|
3326
|
-
if (parentDir === currentDir) {
|
|
3327
|
-
return null;
|
|
3328
|
-
}
|
|
3329
|
-
currentDir = parentDir;
|
|
3330
|
-
}
|
|
3331
|
-
}
|
|
3332
|
-
function resolvePackageRoot(packageRoot) {
|
|
3333
|
-
if (packageRoot) {
|
|
3334
|
-
return packageRoot;
|
|
3335
|
-
}
|
|
3336
|
-
const moduleDir = fileURLToPath2(new URL(".", import.meta.url));
|
|
3337
|
-
return findPackageRoot(moduleDir) ?? fileURLToPath2(new URL("../..", import.meta.url));
|
|
3338
|
-
}
|
|
3339
|
-
function installCustomSkillFiles(skill, packageRoot) {
|
|
3340
|
-
const sourcePath = join7(packageRoot, skill.sourcePath);
|
|
3341
|
-
const targetPath = join7(getCustomSkillsDir(), skill.name);
|
|
3342
|
-
if (!existsSync8(sourcePath)) {
|
|
3343
|
-
console.error(`Custom skill source not found: ${sourcePath}`);
|
|
3344
|
-
return false;
|
|
3345
|
-
}
|
|
3346
|
-
rmSync(targetPath, { recursive: true, force: true });
|
|
3347
|
-
copyDirRecursive(sourcePath, targetPath);
|
|
3348
|
-
return true;
|
|
3349
|
-
}
|
|
3350
|
-
function removeObsoleteSkills(removedSkillNames) {
|
|
3351
|
-
const removedSkills = [];
|
|
3352
|
-
for (const skillName of removedSkillNames) {
|
|
3353
|
-
const targetPath = join7(getCustomSkillsDir(), skillName);
|
|
3354
|
-
try {
|
|
3355
|
-
console.log(`Removing obsolete bundled skill: ${skillName}`);
|
|
3356
|
-
rmSync(targetPath, { recursive: true, force: true });
|
|
3357
|
-
removedSkills.push(skillName);
|
|
3358
|
-
} catch (error) {
|
|
3359
|
-
console.warn(`Failed to remove obsolete bundled skill: ${skillName}`);
|
|
3360
|
-
console.warn(error);
|
|
3361
|
-
}
|
|
3362
|
-
}
|
|
3363
|
-
return removedSkills;
|
|
3364
|
-
}
|
|
3365
|
-
function buildManifest(packageRoot, updatedSkills, previousManifest, pluginVersion, sharedHash) {
|
|
3366
|
-
const installedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3367
|
-
const updatedSkillNames = new Set(
|
|
3368
|
-
updatedSkills.map(({ skill }) => skill.name)
|
|
3369
|
-
);
|
|
3370
|
-
const skills = Object.fromEntries(
|
|
3371
|
-
CUSTOM_SKILLS.map((skill) => {
|
|
3372
|
-
const previousEntry = previousManifest?.skills[skill.name];
|
|
3373
|
-
const nextInstalledAt = updatedSkillNames.has(skill.name) ? installedAt : previousEntry?.installedAt ?? installedAt;
|
|
3374
|
-
return [
|
|
3375
|
-
skill.name,
|
|
3376
|
-
{
|
|
3377
|
-
hash: computeSkillHash(join7(packageRoot, skill.sourcePath)),
|
|
3378
|
-
installedAt: nextInstalledAt
|
|
3379
|
-
}
|
|
3380
|
-
];
|
|
3381
|
-
})
|
|
3382
|
-
);
|
|
3383
|
-
return {
|
|
3384
|
-
pluginVersion,
|
|
3385
|
-
sharedHash,
|
|
3386
|
-
skills
|
|
3387
|
-
};
|
|
3388
|
-
}
|
|
3389
|
-
function pruneRemovedSkillsFromManifest(manifest, removedSkills) {
|
|
3390
|
-
const removedSkillNames = new Set(removedSkills);
|
|
3391
|
-
return {
|
|
3392
|
-
...manifest,
|
|
3393
|
-
skills: Object.fromEntries(
|
|
3394
|
-
Object.entries(manifest.skills).filter(
|
|
3395
|
-
([skillName]) => !removedSkillNames.has(skillName)
|
|
3396
|
-
)
|
|
3397
|
-
)
|
|
3398
|
-
};
|
|
3399
|
-
}
|
|
3400
|
-
function installCustomSkills(packageRoot = resolvePackageRoot()) {
|
|
3401
|
-
const updateCheck = checkSkillsNeedUpdate(packageRoot);
|
|
3402
|
-
if (!updateCheck.needsUpdate) {
|
|
3403
|
-
return {
|
|
3404
|
-
success: true,
|
|
3405
|
-
updatedSkills: [],
|
|
3406
|
-
skippedSkills: [...CUSTOM_SKILLS],
|
|
3407
|
-
failedSkills: [],
|
|
3408
|
-
removedSkills: []
|
|
3409
|
-
};
|
|
3410
|
-
}
|
|
3411
|
-
const removedSkills = removeObsoleteSkills(updateCheck.removedSkills);
|
|
3412
|
-
if (removedSkills.length > 0 && updateCheck.manifest) {
|
|
3413
|
-
writeManifest(
|
|
3414
|
-
pruneRemovedSkillsFromManifest(updateCheck.manifest, removedSkills)
|
|
3415
|
-
);
|
|
3416
|
-
}
|
|
3417
|
-
if (updateCheck.skillsNeedingUpdate.length === 0) {
|
|
3418
|
-
writeManifest(
|
|
3419
|
-
buildManifest(
|
|
3420
|
-
packageRoot,
|
|
3421
|
-
[],
|
|
3422
|
-
updateCheck.manifest,
|
|
3423
|
-
updateCheck.pluginVersion,
|
|
3424
|
-
updateCheck.sharedHash
|
|
3425
|
-
)
|
|
3426
|
-
);
|
|
3427
|
-
return {
|
|
3428
|
-
success: true,
|
|
3429
|
-
updatedSkills: [],
|
|
3430
|
-
skippedSkills: [...CUSTOM_SKILLS],
|
|
3431
|
-
failedSkills: [],
|
|
3432
|
-
removedSkills
|
|
3433
|
-
};
|
|
3434
|
-
}
|
|
3435
|
-
if (!installSharedSkillAssets(packageRoot)) {
|
|
3436
|
-
return {
|
|
3437
|
-
success: false,
|
|
3438
|
-
updatedSkills: [],
|
|
3439
|
-
skippedSkills: [],
|
|
3440
|
-
failedSkills: updateCheck.skillsNeedingUpdate.map(
|
|
3441
|
-
({ skill, reasons }) => ({
|
|
3442
|
-
skill,
|
|
3443
|
-
reasons
|
|
3444
|
-
})
|
|
3445
|
-
),
|
|
3446
|
-
removedSkills
|
|
3447
|
-
};
|
|
3448
|
-
}
|
|
3449
|
-
const updatesBySkillName = new Map(
|
|
3450
|
-
updateCheck.skillsNeedingUpdate.map((entry) => [entry.skill.name, entry])
|
|
3451
|
-
);
|
|
3452
|
-
const updatedSkills = [];
|
|
3453
|
-
const skippedSkills = [];
|
|
3454
|
-
const failedSkills = [];
|
|
3455
|
-
for (const skill of CUSTOM_SKILLS) {
|
|
3456
|
-
const pendingUpdate = updatesBySkillName.get(skill.name);
|
|
3457
|
-
if (!pendingUpdate) {
|
|
3458
|
-
skippedSkills.push(skill);
|
|
3459
|
-
continue;
|
|
3460
|
-
}
|
|
3461
|
-
if (installCustomSkillFiles(skill, packageRoot)) {
|
|
3462
|
-
updatedSkills.push({
|
|
3463
|
-
skill,
|
|
3464
|
-
reasons: pendingUpdate.reasons
|
|
3465
|
-
});
|
|
3466
|
-
continue;
|
|
3467
|
-
}
|
|
3468
|
-
failedSkills.push({
|
|
3469
|
-
skill,
|
|
3470
|
-
reasons: pendingUpdate.reasons
|
|
3471
|
-
});
|
|
3472
|
-
}
|
|
3473
|
-
if (failedSkills.length > 0) {
|
|
3474
|
-
return {
|
|
3475
|
-
success: false,
|
|
3476
|
-
updatedSkills,
|
|
3477
|
-
skippedSkills,
|
|
3478
|
-
failedSkills,
|
|
3479
|
-
removedSkills
|
|
3480
|
-
};
|
|
3481
|
-
}
|
|
3482
|
-
writeManifest(
|
|
3483
|
-
buildManifest(
|
|
3484
|
-
packageRoot,
|
|
3485
|
-
updatedSkills,
|
|
3486
|
-
updateCheck.manifest,
|
|
3487
|
-
updateCheck.pluginVersion,
|
|
3488
|
-
updateCheck.sharedHash
|
|
3489
|
-
)
|
|
3490
|
-
);
|
|
3491
|
-
return {
|
|
3492
|
-
success: true,
|
|
3493
|
-
updatedSkills,
|
|
3494
|
-
skippedSkills,
|
|
3495
|
-
failedSkills,
|
|
3496
|
-
removedSkills
|
|
3497
|
-
};
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
// src/cli/codex-install.ts
|
|
3501
|
-
var ROOT_START = "<!-- thoth-agents:codex-root:start -->";
|
|
3502
|
-
var ROOT_END = "<!-- thoth-agents:codex-root:end -->";
|
|
3503
|
-
var MANAGED_MODEL_STATE_VERSION = 1;
|
|
3504
|
-
function mergeManagedBlock(existing, managedBlock) {
|
|
3505
|
-
const start = existing.indexOf(ROOT_START);
|
|
3506
|
-
const end = existing.indexOf(ROOT_END);
|
|
3507
|
-
if (start !== -1 && end !== -1 && end > start) {
|
|
3508
|
-
return `${existing.slice(0, start)}${managedBlock}${existing.slice(end + ROOT_END.length).replace(/^\s*\n/, "")}`;
|
|
3509
|
-
}
|
|
3510
|
-
return `${existing}${existing.endsWith("\n") || existing.length === 0 ? "" : "\n"}
|
|
3511
|
-
${managedBlock}`;
|
|
3512
|
-
}
|
|
3513
|
-
function writeTextWithBackup(path3, content) {
|
|
3514
|
-
mkdirSync5(dirname5(path3), { recursive: true });
|
|
3515
|
-
if (existsSync9(path3) && readFileSync7(path3, "utf8") === content) return false;
|
|
3516
|
-
if (existsSync9(path3)) copyFileSync4(path3, `${path3}.bak`);
|
|
3517
|
-
writeFileSync4(path3, content);
|
|
3518
|
-
return true;
|
|
3519
|
-
}
|
|
3520
|
-
function packageArtifactTarget(packageRoot, artifact) {
|
|
3521
|
-
return join8(packageRoot, codexPluginRootArtifactPath(artifact.path));
|
|
3522
|
-
}
|
|
3523
|
-
function resolvePackageRoot2(packageRoot) {
|
|
3524
|
-
if (packageRoot) return packageRoot;
|
|
3525
|
-
return findPackageRoot(fileURLToPath3(new URL(".", import.meta.url))) ?? void 0;
|
|
3526
|
-
}
|
|
3527
|
-
function normalizeRelativeMarketplacePath(path3) {
|
|
3528
|
-
const normalized = path3.replaceAll("\\", "/");
|
|
3529
|
-
if (isAbsolute(path3) || /^[A-Za-z]:\//.test(normalized)) return normalized;
|
|
3530
|
-
if (normalized.startsWith("./")) return normalized;
|
|
3531
|
-
return `./${normalized}`;
|
|
3532
|
-
}
|
|
3533
|
-
function marketplaceSourcePath(homeDir, personalPluginRoot) {
|
|
3534
|
-
return normalizeRelativeMarketplacePath(
|
|
3535
|
-
relative3(homeDir, personalPluginRoot)
|
|
3536
|
-
);
|
|
3537
|
-
}
|
|
3538
|
-
function managedMarketplaceEntry(homeDir, personalPluginRoot) {
|
|
3539
|
-
return {
|
|
3540
|
-
name: "thoth-agents",
|
|
3541
|
-
source: {
|
|
3542
|
-
source: "local",
|
|
3543
|
-
path: marketplaceSourcePath(homeDir, personalPluginRoot)
|
|
3544
|
-
},
|
|
3545
|
-
policy: {
|
|
3546
|
-
installation: "AVAILABLE",
|
|
3547
|
-
authentication: "ON_INSTALL"
|
|
3548
|
-
},
|
|
3549
|
-
category: "Productivity"
|
|
3550
|
-
};
|
|
3551
|
-
}
|
|
3552
|
-
function stableJson3(value) {
|
|
3553
|
-
return `${JSON.stringify(value, null, 2)}
|
|
3554
|
-
`;
|
|
3555
|
-
}
|
|
3556
|
-
function emptyManagedModelState() {
|
|
3557
|
-
return {
|
|
3558
|
-
version: MANAGED_MODEL_STATE_VERSION,
|
|
3559
|
-
models: {}
|
|
3560
|
-
};
|
|
3561
|
-
}
|
|
3562
|
-
function readManagedModelState(path3) {
|
|
3563
|
-
if (!existsSync9(path3)) return emptyManagedModelState();
|
|
3564
|
-
try {
|
|
3565
|
-
const parsed = JSON.parse(readFileSync7(path3, "utf8"));
|
|
3566
|
-
if (parsed.version !== MANAGED_MODEL_STATE_VERSION || !parsed.models || typeof parsed.models !== "object" || Array.isArray(parsed.models)) {
|
|
3567
|
-
return emptyManagedModelState();
|
|
3568
|
-
}
|
|
3569
|
-
return {
|
|
3570
|
-
version: MANAGED_MODEL_STATE_VERSION,
|
|
3571
|
-
models: Object.fromEntries(
|
|
3572
|
-
Object.entries(parsed.models).filter(
|
|
3573
|
-
(entry) => typeof entry[0] === "string" && typeof entry[1] === "string"
|
|
3574
|
-
)
|
|
3575
|
-
)
|
|
3576
|
-
};
|
|
3577
|
-
} catch {
|
|
3578
|
-
return emptyManagedModelState();
|
|
3579
|
-
}
|
|
3580
|
-
}
|
|
3581
|
-
function parseRoleTomlModel(content) {
|
|
3582
|
-
const match = /^model\s*=\s*"((?:\\.|[^"\\])*)"\s*$/m.exec(content);
|
|
3583
|
-
if (!match) return void 0;
|
|
3584
|
-
return match[1].replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
3585
|
-
}
|
|
3586
|
-
function escapeTomlString2(value) {
|
|
3587
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\f/g, "\\f").replace(/\r/g, "\\r");
|
|
3588
|
-
}
|
|
3589
|
-
function replaceRoleTomlModel(content, model) {
|
|
3590
|
-
const rendered = `model = "${escapeTomlString2(model)}"`;
|
|
3591
|
-
if (/^model\s*=\s*"(?:\\.|[^"\\])*"\s*$/m.test(content)) {
|
|
3592
|
-
return content.replace(/^model\s*=\s*"(?:\\.|[^"\\])*"\s*$/m, rendered);
|
|
3593
|
-
}
|
|
3594
|
-
return `${rendered}
|
|
3595
|
-
${content}`;
|
|
3596
|
-
}
|
|
3597
|
-
function roleManagedModelStateKey(path3) {
|
|
3598
|
-
return basename(path3);
|
|
3599
|
-
}
|
|
3600
|
-
function resolveRoleTomlContent(options) {
|
|
3601
|
-
const renderedModel = parseRoleTomlModel(options.renderedContent);
|
|
3602
|
-
const key = roleManagedModelStateKey(options.targetPath);
|
|
3603
|
-
if (!renderedModel) return options.renderedContent;
|
|
3604
|
-
if (options.reset || !existsSync9(options.targetPath)) {
|
|
3605
|
-
options.nextState.models[key] = renderedModel;
|
|
3606
|
-
return options.renderedContent;
|
|
3607
|
-
}
|
|
3608
|
-
const currentModel = parseRoleTomlModel(
|
|
3609
|
-
readFileSync7(options.targetPath, "utf8")
|
|
3610
|
-
);
|
|
3611
|
-
const trackedModel = options.state.models[key];
|
|
3612
|
-
const isUserOwned = currentModel !== void 0 && (trackedModel === void 0 ? currentModel !== renderedModel : currentModel !== trackedModel);
|
|
3613
|
-
if (isUserOwned) {
|
|
3614
|
-
if (trackedModel !== void 0)
|
|
3615
|
-
options.nextState.models[key] = trackedModel;
|
|
3616
|
-
return replaceRoleTomlModel(options.renderedContent, currentModel);
|
|
3617
|
-
}
|
|
3618
|
-
options.nextState.models[key] = renderedModel;
|
|
3619
|
-
return options.renderedContent;
|
|
3620
|
-
}
|
|
3621
|
-
function mergePersonalMarketplace(existing, homeDir, personalPluginRoot) {
|
|
3622
|
-
const parsed = existing.trim() ? JSON.parse(existing) : {};
|
|
3623
|
-
const plugins = Array.isArray(parsed.plugins) ? parsed.plugins : [];
|
|
3624
|
-
const managedEntry = managedMarketplaceEntry(homeDir, personalPluginRoot);
|
|
3625
|
-
const nextPlugins = plugins.filter(
|
|
3626
|
-
(entry) => !(entry && typeof entry === "object" && "name" in entry && entry.name === "thoth-agents")
|
|
3627
|
-
).concat(managedEntry);
|
|
3628
|
-
return stableJson3({
|
|
3629
|
-
...parsed,
|
|
3630
|
-
name: typeof parsed.name === "string" ? parsed.name : "personal-marketplace",
|
|
3631
|
-
interface: parsed.interface && typeof parsed.interface === "object" ? parsed.interface : { displayName: "Personal Plugin Marketplace" },
|
|
3632
|
-
plugins: nextPlugins
|
|
3633
|
-
});
|
|
3634
|
-
}
|
|
3635
|
-
function roleArtifactContent(role, artifacts) {
|
|
3636
|
-
const path3 = `.codex/agents/thoth-agents-${role}.toml`;
|
|
3637
|
-
const artifact = artifacts.find((candidate) => candidate.path === path3);
|
|
3638
|
-
if (!artifact?.content)
|
|
3639
|
-
throw new Error(`Missing Codex role artifact: ${path3}`);
|
|
3640
|
-
return String(artifact.content);
|
|
3641
|
-
}
|
|
3642
|
-
function buildCodexSetupPlan(config) {
|
|
3643
|
-
const targets = resolveCodexTargets({
|
|
3644
|
-
scope: config.scope,
|
|
3645
|
-
projectRoot: config.projectRoot,
|
|
3646
|
-
homeDir: config.homeDir,
|
|
3647
|
-
codexHome: config.codexHome
|
|
3648
|
-
});
|
|
3649
|
-
const packageRoot = resolvePackageRoot2(config.packageRoot);
|
|
3650
|
-
const render = codexAdapter.render({
|
|
3651
|
-
projectRoot: config.projectRoot,
|
|
3652
|
-
...packageRoot ? { packageRoot } : {}
|
|
3653
|
-
});
|
|
3654
|
-
const packageArtifacts = render.artifacts.filter(
|
|
3655
|
-
(artifact) => artifact.path.startsWith(".codex-plugin/")
|
|
3656
|
-
);
|
|
3657
|
-
const rootBlock = renderCodexRootInstructions();
|
|
3658
|
-
const managedModelState = readManagedModelState(targets.managedModelsPath);
|
|
3659
|
-
const nextManagedModelState = emptyManagedModelState();
|
|
3660
|
-
const items = [
|
|
3661
|
-
{
|
|
3662
|
-
kind: "root-instructions",
|
|
3663
|
-
action: "merge-managed-block",
|
|
3664
|
-
targetPath: targets.rootInstructionsPath,
|
|
3665
|
-
description: `Merge managed Codex root instructions into ${targets.rootInstructionsPath}.`,
|
|
3666
|
-
requiresBackup: true,
|
|
3667
|
-
content: rootBlock
|
|
3668
|
-
},
|
|
3669
|
-
...targets.roleAgentPaths.map(
|
|
3670
|
-
(target) => ({
|
|
3671
|
-
kind: "role-subagent-toml",
|
|
3672
|
-
action: "write-role-toml",
|
|
3673
|
-
targetPath: target.path,
|
|
3674
|
-
description: `Materialize Codex role subagent ${target.role}.`,
|
|
3675
|
-
requiresBackup: existsSync9(target.path),
|
|
3676
|
-
role: target.role,
|
|
3677
|
-
content: resolveRoleTomlContent({
|
|
3678
|
-
renderedContent: roleArtifactContent(target.role, render.artifacts),
|
|
3679
|
-
targetPath: target.path,
|
|
3680
|
-
state: managedModelState,
|
|
3681
|
-
nextState: nextManagedModelState,
|
|
3682
|
-
reset: config.reset
|
|
3683
|
-
})
|
|
3684
|
-
})
|
|
3685
|
-
),
|
|
3686
|
-
{
|
|
3687
|
-
kind: "managed-model-state",
|
|
3688
|
-
action: "write-managed-model-state",
|
|
3689
|
-
targetPath: targets.managedModelsPath,
|
|
3690
|
-
description: "Record thoth-agents-managed Codex role model ownership state.",
|
|
3691
|
-
requiresBackup: existsSync9(targets.managedModelsPath),
|
|
3692
|
-
content: stableJson3(nextManagedModelState)
|
|
3693
|
-
},
|
|
3694
|
-
...packageArtifacts.map(
|
|
3695
|
-
(artifact) => ({
|
|
3696
|
-
kind: "personal-plugin-source",
|
|
3697
|
-
action: "refresh-package",
|
|
3698
|
-
targetPath: packageArtifactTarget(targets.personalPluginRoot, artifact),
|
|
3699
|
-
description: `Refresh Personal Codex plugin source asset ${artifact.path}.`,
|
|
3700
|
-
requiresBackup: false,
|
|
3701
|
-
content: String(artifact.content ?? "")
|
|
3702
|
-
})
|
|
3703
|
-
),
|
|
3704
|
-
{
|
|
3705
|
-
kind: "personal-marketplace",
|
|
3706
|
-
action: "merge-marketplace",
|
|
3707
|
-
targetPath: targets.personalMarketplacePath,
|
|
3708
|
-
description: "Register Personal Codex marketplace entry for the local thoth-agents plugin source.",
|
|
3709
|
-
requiresBackup: existsSync9(targets.personalMarketplacePath),
|
|
3710
|
-
content: targets.personalPluginRoot
|
|
3711
|
-
},
|
|
3712
|
-
{
|
|
3713
|
-
kind: "user-config",
|
|
3714
|
-
action: "merge-toml",
|
|
3715
|
-
targetPath: targets.configPath,
|
|
3716
|
-
description: "Merge managed Codex feature gates into user config.toml.",
|
|
3717
|
-
requiresBackup: true
|
|
3718
|
-
},
|
|
3719
|
-
{
|
|
3720
|
-
kind: "diagnostic",
|
|
3721
|
-
action: "diagnose-only",
|
|
3722
|
-
targetPath: targets.codexHome,
|
|
3723
|
-
description: "Report /plugins, /hooks, precedence, and capability review steps.",
|
|
3724
|
-
requiresBackup: false
|
|
3725
|
-
}
|
|
3726
|
-
];
|
|
3727
|
-
return {
|
|
3728
|
-
dryRun: config.dryRun === true,
|
|
3729
|
-
reset: config.reset,
|
|
3730
|
-
items,
|
|
3731
|
-
configPath: targets.configPath,
|
|
3732
|
-
pluginId: config.pluginId,
|
|
3733
|
-
diagnostics: [
|
|
3734
|
-
"Restart Codex, then run /plugins to review and enable the Personal thoth-agents plugin registered through ~/.agents/plugins/marketplace.json.",
|
|
3735
|
-
"Run /hooks to review and trust plugin hooks; features.plugin_hooks does not bypass hook trust review.",
|
|
3736
|
-
"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.",
|
|
3737
|
-
"Higher-precedence Codex config (project, profile, CLI, system, or admin) may override user config feature flags."
|
|
3738
|
-
],
|
|
3739
|
-
disclaimers: [
|
|
3740
|
-
"Role permissions, provider-per-agent settings, memory governance, and hook enforcement are instruction-level or user-managed unless documented Codex runtime controls are available.",
|
|
3741
|
-
"Codex v1 reset is managed-only; no broad destructive --force behavior is implemented."
|
|
3742
|
-
]
|
|
3743
|
-
};
|
|
3744
|
-
}
|
|
3745
|
-
function formatCodexSetupPlan(plan) {
|
|
3746
|
-
const refreshPackageGroups = /* @__PURE__ */ new Map();
|
|
3747
|
-
for (const item of plan.items) {
|
|
3748
|
-
if (item.action !== "refresh-package") continue;
|
|
3749
|
-
const group = refreshPackageGroups.get(item.kind) ?? [];
|
|
3750
|
-
group.push(item);
|
|
3751
|
-
refreshPackageGroups.set(item.kind, group);
|
|
3752
|
-
}
|
|
3753
|
-
const renderedRefreshKinds = /* @__PURE__ */ new Set();
|
|
3754
|
-
const lines = [];
|
|
3755
|
-
for (const item of plan.items) {
|
|
3756
|
-
if (item.action !== "refresh-package") {
|
|
3757
|
-
lines.push(`- ${item.action}: ${item.targetPath} (${item.description})`);
|
|
3758
|
-
continue;
|
|
3759
|
-
}
|
|
3760
|
-
if (renderedRefreshKinds.has(item.kind)) continue;
|
|
3761
|
-
renderedRefreshKinds.add(item.kind);
|
|
3762
|
-
lines.push(formatRefreshPackageGroup(item.kind, refreshPackageGroups));
|
|
3763
|
-
}
|
|
3764
|
-
return ["Codex setup plan:", ...lines].join("\n");
|
|
3765
|
-
}
|
|
3766
|
-
function formatRefreshPackageGroup(kind, groups) {
|
|
3767
|
-
const items = groups.get(kind) ?? [];
|
|
3768
|
-
const description = kind === "personal-plugin-source" ? "Refresh Personal Codex plugin source" : "Refresh documented .codex-plugin package";
|
|
3769
|
-
return `- refresh-package: ${commonTargetDirectory(items)} (${description}, ${items.length} files.)`;
|
|
3770
|
-
}
|
|
3771
|
-
function commonTargetDirectory(items) {
|
|
3772
|
-
if (items.length === 0) return "";
|
|
3773
|
-
let common = dirname5(items[0]?.targetPath ?? "");
|
|
3774
|
-
for (const item of items.slice(1)) {
|
|
3775
|
-
while (!isSameOrChildPath(item.targetPath, common)) {
|
|
3776
|
-
const parent = dirname5(common);
|
|
3777
|
-
if (parent === common) return common;
|
|
3778
|
-
common = parent;
|
|
3779
|
-
}
|
|
3780
|
-
}
|
|
3781
|
-
return common;
|
|
3782
|
-
}
|
|
3783
|
-
function isSameOrChildPath(path3, parent) {
|
|
3784
|
-
return path3 === parent || path3.startsWith(`${parent}\\`) || path3.startsWith(`${parent}/`);
|
|
3785
|
-
}
|
|
3786
|
-
function uniqueMessages(messages) {
|
|
3787
|
-
return [...new Set(messages)];
|
|
3788
|
-
}
|
|
3789
|
-
function applyCodexSetup(plan) {
|
|
3790
|
-
const changed = [];
|
|
3791
|
-
const diagnostics = uniqueMessages([
|
|
3792
|
-
...plan.diagnostics,
|
|
3793
|
-
...plan.disclaimers
|
|
3794
|
-
]);
|
|
3795
|
-
if (plan.dryRun) return { success: true, changed, diagnostics };
|
|
3796
|
-
try {
|
|
3797
|
-
for (const targetPath of managedRefreshRoots(plan)) {
|
|
3798
|
-
rmSync2(targetPath, { recursive: true, force: true });
|
|
3799
|
-
}
|
|
3800
|
-
for (const item of plan.items) {
|
|
3801
|
-
if (item.action === "diagnose-only") continue;
|
|
3802
|
-
if (item.action === "merge-toml") {
|
|
3803
|
-
const result = writeCodexConfigMerge({
|
|
3804
|
-
configPath: item.targetPath,
|
|
3805
|
-
dryRun: false,
|
|
3806
|
-
pluginId: plan.pluginId
|
|
3807
|
-
});
|
|
3808
|
-
diagnostics.push(...result.diffSummary, ...result.warnings);
|
|
3809
|
-
if (!result.success) throw new Error(result.error);
|
|
3810
|
-
if (result.changed) changed.push(item.targetPath);
|
|
3811
|
-
continue;
|
|
3812
|
-
}
|
|
3813
|
-
if (item.action === "merge-marketplace") {
|
|
3814
|
-
if (item.content === void 0) continue;
|
|
3815
|
-
const content2 = mergePersonalMarketplace(
|
|
3816
|
-
existsSync9(item.targetPath) ? readFileSync7(item.targetPath, "utf8") : "",
|
|
3817
|
-
dirname5(dirname5(dirname5(item.targetPath))),
|
|
3818
|
-
item.content
|
|
3819
|
-
);
|
|
3820
|
-
if (writeTextWithBackup(item.targetPath, content2))
|
|
3821
|
-
changed.push(item.targetPath);
|
|
3822
|
-
continue;
|
|
3823
|
-
}
|
|
3824
|
-
if (item.content === void 0) continue;
|
|
3825
|
-
const content = item.action === "merge-managed-block" ? mergeManagedBlock(
|
|
3826
|
-
existsSync9(item.targetPath) ? readFileSync7(item.targetPath, "utf8") : "",
|
|
3827
|
-
item.content
|
|
3828
|
-
) : item.content;
|
|
3829
|
-
if (writeTextWithBackup(item.targetPath, content))
|
|
3830
|
-
changed.push(item.targetPath);
|
|
3831
|
-
}
|
|
3832
|
-
return { success: true, changed, diagnostics: uniqueMessages(diagnostics) };
|
|
3833
|
-
} catch (error) {
|
|
3834
|
-
return {
|
|
3835
|
-
success: false,
|
|
3836
|
-
changed,
|
|
3837
|
-
diagnostics: uniqueMessages(diagnostics),
|
|
3838
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3839
|
-
};
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
function managedRefreshRoots(plan) {
|
|
3843
|
-
const refreshGroups = /* @__PURE__ */ new Map();
|
|
3844
|
-
for (const item of plan.items) {
|
|
3845
|
-
if (item.action !== "refresh-package") continue;
|
|
3846
|
-
const group = refreshGroups.get(item.kind) ?? [];
|
|
3847
|
-
group.push(item);
|
|
3848
|
-
refreshGroups.set(item.kind, group);
|
|
3849
|
-
}
|
|
3850
|
-
return [...refreshGroups].filter(([kind]) => kind === "personal-plugin-source").map(([, items]) => commonTargetDirectory(items)).filter((path3) => path3.length > 0);
|
|
3851
|
-
}
|
|
3852
|
-
|
|
3853
|
-
// src/cli/system.ts
|
|
3854
|
-
import { statSync as statSync4 } from "fs";
|
|
3855
|
-
|
|
3856
|
-
// src/utils/subprocess.ts
|
|
3857
|
-
import {
|
|
3858
|
-
spawn as nodeSpawn,
|
|
3859
|
-
spawnSync as nodeSpawnSync
|
|
3860
|
-
} from "child_process";
|
|
3861
|
-
import { Readable } from "stream";
|
|
3862
|
-
function emptyReadableStream() {
|
|
3863
|
-
return new ReadableStream({
|
|
3864
|
-
start(controller) {
|
|
3865
|
-
controller.close();
|
|
3866
|
-
}
|
|
3867
|
-
});
|
|
3868
|
-
}
|
|
3869
|
-
function toWebReadable(stream) {
|
|
3870
|
-
if (!stream) {
|
|
3871
|
-
return emptyReadableStream();
|
|
3872
|
-
}
|
|
3873
|
-
return Readable.toWeb(
|
|
3874
|
-
stream
|
|
3875
|
-
);
|
|
3876
|
-
}
|
|
3877
|
-
function fallbackStdin() {
|
|
3878
|
-
return {
|
|
3879
|
-
write: () => void 0,
|
|
3880
|
-
end: () => void 0
|
|
3881
|
-
};
|
|
3882
|
-
}
|
|
3883
|
-
function spawn(command, options = {}) {
|
|
3884
|
-
const child = nodeSpawn(command[0], command.slice(1), {
|
|
3885
|
-
cwd: options.cwd,
|
|
3886
|
-
env: options.env,
|
|
3887
|
-
stdio: [
|
|
3888
|
-
options.stdin ?? "pipe",
|
|
3889
|
-
options.stdout ?? "pipe",
|
|
3890
|
-
options.stderr ?? "pipe"
|
|
3891
|
-
]
|
|
3892
|
-
});
|
|
3893
|
-
const managed = {
|
|
3894
|
-
stdin: child.stdin ?? fallbackStdin(),
|
|
3895
|
-
stdout: toWebReadable(child.stdout),
|
|
3896
|
-
stderr: toWebReadable(child.stderr),
|
|
3897
|
-
exited: new Promise((resolve2) => {
|
|
3898
|
-
child.on("exit", (code) => {
|
|
3899
|
-
managed.exitCode = code;
|
|
3900
|
-
resolve2(code ?? 1);
|
|
3901
|
-
});
|
|
3902
|
-
child.on("error", () => {
|
|
3903
|
-
managed.exitCode = 1;
|
|
3904
|
-
resolve2(1);
|
|
3905
|
-
});
|
|
3906
|
-
}),
|
|
3907
|
-
exitCode: child.exitCode,
|
|
3908
|
-
kill: () => {
|
|
3909
|
-
child.kill();
|
|
3910
|
-
}
|
|
3911
|
-
};
|
|
3912
|
-
return managed;
|
|
3913
|
-
}
|
|
3914
|
-
|
|
3915
|
-
// src/cli/system.ts
|
|
3916
|
-
var cachedOpenCodePath = null;
|
|
3917
|
-
function getOpenCodePaths() {
|
|
3918
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
3919
|
-
return [
|
|
3920
|
-
// PATH (try this first)
|
|
3921
|
-
"opencode",
|
|
3922
|
-
// User local installations (Linux & macOS)
|
|
3923
|
-
`${home}/.local/bin/opencode`,
|
|
3924
|
-
`${home}/.opencode/bin/opencode`,
|
|
3925
|
-
`${home}/bin/opencode`,
|
|
3926
|
-
// System-wide installations
|
|
3927
|
-
"/usr/local/bin/opencode",
|
|
3928
|
-
"/opt/opencode/bin/opencode",
|
|
3929
|
-
"/usr/bin/opencode",
|
|
3930
|
-
"/bin/opencode",
|
|
3931
|
-
// macOS specific
|
|
3932
|
-
"/Applications/OpenCode.app/Contents/MacOS/opencode",
|
|
3933
|
-
`${home}/Applications/OpenCode.app/Contents/MacOS/opencode`,
|
|
3934
|
-
// Homebrew (macOS & Linux)
|
|
3935
|
-
"/opt/homebrew/bin/opencode",
|
|
3936
|
-
"/home/linuxbrew/.linuxbrew/bin/opencode",
|
|
3937
|
-
`${home}/homebrew/bin/opencode`,
|
|
3938
|
-
// macOS user Library
|
|
3939
|
-
`${home}/Library/Application Support/opencode/bin/opencode`,
|
|
3940
|
-
// Snap (Linux)
|
|
3941
|
-
"/snap/bin/opencode",
|
|
3942
|
-
"/var/snap/opencode/current/bin/opencode",
|
|
3943
|
-
// Flatpak (Linux)
|
|
3944
|
-
"/var/lib/flatpak/exports/bin/ai.opencode.OpenCode",
|
|
3945
|
-
`${home}/.local/share/flatpak/exports/bin/ai.opencode.OpenCode`,
|
|
3946
|
-
// Nix (Linux/macOS)
|
|
3947
|
-
"/nix/store/opencode/bin/opencode",
|
|
3948
|
-
`${home}/.nix-profile/bin/opencode`,
|
|
3949
|
-
"/run/current-system/sw/bin/opencode",
|
|
3950
|
-
// Cargo (Rust toolchain)
|
|
3951
|
-
`${home}/.cargo/bin/opencode`,
|
|
3952
|
-
// npm/npx global
|
|
3953
|
-
`${home}/.npm-global/bin/opencode`,
|
|
3954
|
-
"/usr/local/lib/node_modules/opencode/bin/opencode",
|
|
3955
|
-
// Yarn global
|
|
3956
|
-
`${home}/.yarn/bin/opencode`,
|
|
3957
|
-
// PNPM
|
|
3958
|
-
`${home}/.pnpm-global/bin/opencode`
|
|
3959
|
-
];
|
|
3960
|
-
}
|
|
3961
|
-
function resolveOpenCodePath() {
|
|
3962
|
-
if (cachedOpenCodePath) {
|
|
3963
|
-
return cachedOpenCodePath;
|
|
3964
|
-
}
|
|
3965
|
-
const paths = getOpenCodePaths();
|
|
3966
|
-
for (const opencodePath of paths) {
|
|
3967
|
-
if (opencodePath === "opencode") continue;
|
|
3968
|
-
try {
|
|
3969
|
-
const stat = statSync4(opencodePath);
|
|
3970
|
-
if (stat.isFile()) {
|
|
3971
|
-
cachedOpenCodePath = opencodePath;
|
|
3972
|
-
return opencodePath;
|
|
3973
|
-
}
|
|
3974
|
-
} catch {
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
return "opencode";
|
|
3978
|
-
}
|
|
3979
|
-
async function isOpenCodeInstalled() {
|
|
3980
|
-
const paths = getOpenCodePaths();
|
|
3981
|
-
for (const opencodePath of paths) {
|
|
3982
|
-
try {
|
|
3983
|
-
const proc = spawn([opencodePath, "--version"], {
|
|
3984
|
-
stdout: "pipe",
|
|
3985
|
-
stderr: "pipe"
|
|
3986
|
-
});
|
|
3987
|
-
await proc.exited;
|
|
3988
|
-
if (proc.exitCode === 0) {
|
|
3989
|
-
cachedOpenCodePath = opencodePath;
|
|
3990
|
-
return true;
|
|
3991
|
-
}
|
|
3992
|
-
} catch {
|
|
3993
|
-
}
|
|
3994
|
-
}
|
|
3995
|
-
return false;
|
|
3996
|
-
}
|
|
3997
|
-
async function getOpenCodeVersion() {
|
|
3998
|
-
const opencodePath = resolveOpenCodePath();
|
|
3999
|
-
try {
|
|
4000
|
-
const proc = spawn([opencodePath, "--version"], {
|
|
4001
|
-
stdout: "pipe",
|
|
4002
|
-
stderr: "pipe"
|
|
4003
|
-
});
|
|
4004
|
-
const output = await new Response(proc.stdout).text();
|
|
4005
|
-
await proc.exited;
|
|
4006
|
-
if (proc.exitCode === 0) {
|
|
4007
|
-
return output.trim();
|
|
4008
|
-
}
|
|
4009
|
-
} catch {
|
|
4010
|
-
}
|
|
4011
|
-
return null;
|
|
4012
|
-
}
|
|
4013
|
-
function getOpenCodePath() {
|
|
4014
|
-
const path3 = resolveOpenCodePath();
|
|
4015
|
-
return path3 === "opencode" ? null : path3;
|
|
4016
|
-
}
|
|
4017
|
-
|
|
4018
|
-
// src/cli/skills.ts
|
|
4019
|
-
import { spawnSync } from "child_process";
|
|
4020
|
-
var RECOMMENDED_SKILLS = [
|
|
4021
|
-
{
|
|
4022
|
-
name: "simplify",
|
|
4023
|
-
repo: "https://github.com/brianlovin/claude-config",
|
|
4024
|
-
skillName: "simplify",
|
|
4025
|
-
description: "YAGNI code simplification expert"
|
|
4026
|
-
},
|
|
4027
|
-
{
|
|
4028
|
-
name: "playwright-cli",
|
|
4029
|
-
repo: "https://github.com/microsoft/playwright-cli",
|
|
4030
|
-
skillName: "playwright-cli",
|
|
4031
|
-
description: "Browser automation for visual checks and testing"
|
|
4032
|
-
}
|
|
4033
|
-
];
|
|
4034
|
-
function installSkill(skill) {
|
|
4035
|
-
const args = [
|
|
4036
|
-
"skills",
|
|
4037
|
-
"add",
|
|
4038
|
-
skill.repo,
|
|
4039
|
-
"--skill",
|
|
4040
|
-
skill.skillName,
|
|
4041
|
-
"-a",
|
|
4042
|
-
"opencode",
|
|
4043
|
-
"-y",
|
|
4044
|
-
"--global"
|
|
4045
|
-
];
|
|
4046
|
-
try {
|
|
4047
|
-
const result = spawnSync("npx", args, { stdio: "inherit" });
|
|
4048
|
-
if (result.status !== 0) {
|
|
4049
|
-
return false;
|
|
4050
|
-
}
|
|
4051
|
-
if (skill.postInstallCommands && skill.postInstallCommands.length > 0) {
|
|
4052
|
-
console.log(`Running post-install commands for ${skill.name}...`);
|
|
4053
|
-
for (const cmd of skill.postInstallCommands) {
|
|
4054
|
-
console.log(`> ${cmd}`);
|
|
4055
|
-
const [command, ...cmdArgs] = cmd.split(" ");
|
|
4056
|
-
const cmdResult = spawnSync(command, cmdArgs, { stdio: "inherit" });
|
|
4057
|
-
if (cmdResult.status !== 0) {
|
|
4058
|
-
console.warn(`Post-install command failed: ${cmd}`);
|
|
4059
|
-
}
|
|
4060
|
-
}
|
|
4061
|
-
}
|
|
4062
|
-
return true;
|
|
4063
|
-
} catch (error) {
|
|
4064
|
-
console.error(`Failed to install skill: ${skill.name}`, error);
|
|
4065
|
-
return false;
|
|
4066
|
-
}
|
|
4067
|
-
}
|
|
42
|
+
// src/cli/index.ts
|
|
43
|
+
import { pathToFileURL } from "url";
|
|
4068
44
|
|
|
4069
45
|
// src/cli/install.ts
|
|
46
|
+
import { existsSync } from "fs";
|
|
47
|
+
import { homedir } from "os";
|
|
48
|
+
import { cwd } from "process";
|
|
4070
49
|
var GREEN = "\x1B[32m";
|
|
4071
50
|
var BLUE = "\x1B[34m";
|
|
4072
51
|
var YELLOW = "\x1B[33m";
|
|
@@ -4125,11 +104,11 @@ async function checkOpenCodeInstalled() {
|
|
|
4125
104
|
return { ok: false };
|
|
4126
105
|
}
|
|
4127
106
|
const version = await getOpenCodeVersion();
|
|
4128
|
-
const
|
|
107
|
+
const path = getOpenCodePath();
|
|
4129
108
|
const detectedVersion = version ?? "";
|
|
4130
|
-
const pathInfo =
|
|
109
|
+
const pathInfo = path ? ` (${DIM}${path}${RESET})` : "";
|
|
4131
110
|
printSuccess(`OpenCode ${detectedVersion} detected${pathInfo}`);
|
|
4132
|
-
return { ok: true, version: version ?? void 0, path:
|
|
111
|
+
return { ok: true, version: version ?? void 0, path: path ?? void 0 };
|
|
4133
112
|
}
|
|
4134
113
|
function handleStepResult(result, successMsg) {
|
|
4135
114
|
if (!result.success) {
|
|
@@ -4195,7 +174,7 @@ ${JSON.stringify(liteConfig, null, 2)}
|
|
|
4195
174
|
`);
|
|
4196
175
|
} else {
|
|
4197
176
|
const configPath = getExistingLiteConfigPath();
|
|
4198
|
-
const configExists =
|
|
177
|
+
const configExists = existsSync(configPath);
|
|
4199
178
|
if (configExists && !config.reset) {
|
|
4200
179
|
printInfo(
|
|
4201
180
|
`Configuration already exists at ${configPath}. Use --reset to overwrite.`
|
|
@@ -4221,17 +200,23 @@ ${JSON.stringify(liteConfig, null, 2)}
|
|
|
4221
200
|
}
|
|
4222
201
|
} else {
|
|
4223
202
|
let skillsInstalled = 0;
|
|
203
|
+
let skillsAlreadyInstalled = 0;
|
|
4224
204
|
for (const skill of RECOMMENDED_SKILLS) {
|
|
4225
205
|
printInfo(`Installing ${skill.name}...`);
|
|
4226
|
-
|
|
206
|
+
const result = installRecommendedSkill(skill);
|
|
207
|
+
if (result.status === "installed") {
|
|
4227
208
|
printSuccess(`Installed: ${skill.name}`);
|
|
4228
209
|
skillsInstalled++;
|
|
4229
|
-
} else {
|
|
210
|
+
} else if (result.status === "already-installed") {
|
|
4230
211
|
printInfo(`Skipped: ${skill.name} (already installed)`);
|
|
212
|
+
skillsAlreadyInstalled++;
|
|
213
|
+
} else {
|
|
214
|
+
printError(`Failed to install recommended skill: ${skill.name}`);
|
|
215
|
+
return 1;
|
|
4231
216
|
}
|
|
4232
217
|
}
|
|
4233
218
|
printSuccess(
|
|
4234
|
-
`${skillsInstalled}
|
|
219
|
+
`${skillsInstalled} installed, ${skillsAlreadyInstalled} already installed`
|
|
4235
220
|
);
|
|
4236
221
|
}
|
|
4237
222
|
}
|
|
@@ -4312,7 +297,7 @@ async function install(args) {
|
|
|
4312
297
|
reset: config.reset,
|
|
4313
298
|
scope: "user",
|
|
4314
299
|
projectRoot: cwd(),
|
|
4315
|
-
homeDir:
|
|
300
|
+
homeDir: homedir()
|
|
4316
301
|
});
|
|
4317
302
|
console.log(formatCodexSetupPlan(plan));
|
|
4318
303
|
const result = applyCodexSetup(plan);
|
|
@@ -4329,7 +314,446 @@ async function install(args) {
|
|
|
4329
314
|
return runInstall(config);
|
|
4330
315
|
}
|
|
4331
316
|
|
|
4332
|
-
// src/cli/
|
|
317
|
+
// src/cli/commands.ts
|
|
318
|
+
function formatHarnessName(harness) {
|
|
319
|
+
const metadata = getOperationHarness(harness);
|
|
320
|
+
return `${metadata.displayName} (${metadata.id})`;
|
|
321
|
+
}
|
|
322
|
+
function formatTarget(target) {
|
|
323
|
+
const label = target.label ? `${target.label}: ` : "";
|
|
324
|
+
const location = target.path ?? target.description ?? target.kind;
|
|
325
|
+
const state = target.state ? ` [${target.state}]` : "";
|
|
326
|
+
const expected = target.expected ? ` expected ${target.expected}` : "";
|
|
327
|
+
const observed = target.observed ? ` observed ${target.observed}` : "";
|
|
328
|
+
return `- ${label}${location}${state}${expected}${observed}`;
|
|
329
|
+
}
|
|
330
|
+
function formatPaths(paths) {
|
|
331
|
+
return paths.map((path) => {
|
|
332
|
+
const label = path.label ? `${path.label}: ` : "";
|
|
333
|
+
return `- ${label}${path.path}`;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function formatWarnings(warnings) {
|
|
337
|
+
return warnings.map(
|
|
338
|
+
(warning) => `- [${warning.severity}] ${warning.message}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
function formatDisclaimers(disclaimers) {
|
|
342
|
+
return disclaimers.map((disclaimer) => `- ${disclaimer.message}`);
|
|
343
|
+
}
|
|
344
|
+
function formatBackup(backup) {
|
|
345
|
+
const lines = [
|
|
346
|
+
`Backup: ${backup.required ? "required" : "not required"} (${backup.strategy})`
|
|
347
|
+
];
|
|
348
|
+
if (backup.description) {
|
|
349
|
+
lines.push(`Backup note: ${backup.description}`);
|
|
350
|
+
}
|
|
351
|
+
if (backup.destinations && backup.destinations.length > 0) {
|
|
352
|
+
lines.push("Backup paths:", ...formatPaths(backup.destinations));
|
|
353
|
+
}
|
|
354
|
+
return lines;
|
|
355
|
+
}
|
|
356
|
+
function joinSections(sections) {
|
|
357
|
+
return sections.filter((section) => section.length > 0).map((section) => section.join("\n")).join("\n\n");
|
|
358
|
+
}
|
|
359
|
+
function formatHarnessStatusReport(reports) {
|
|
360
|
+
return joinSections(
|
|
361
|
+
reports.map((report) => [
|
|
362
|
+
`${report.displayName ?? getOperationHarness(report.harness).displayName} (${report.harness})`,
|
|
363
|
+
`State: ${report.state}`,
|
|
364
|
+
`Summary: ${report.summary}`,
|
|
365
|
+
...report.targets.length > 0 ? ["Targets:", ...report.targets.map(formatTarget)] : [],
|
|
366
|
+
...report.diagnostics.length > 0 ? ["Warnings:", ...formatWarnings(report.diagnostics)] : [],
|
|
367
|
+
...report.disclaimers && report.disclaimers.length > 0 ? ["Disclaimers:", ...formatDisclaimers(report.disclaimers)] : [],
|
|
368
|
+
...report.actions.length > 0 ? [
|
|
369
|
+
"Actions:",
|
|
370
|
+
...report.actions.map(
|
|
371
|
+
(action) => `- ${action.label} - ${action.description}`
|
|
372
|
+
)
|
|
373
|
+
] : []
|
|
374
|
+
])
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
function formatHarnessList(harnesses) {
|
|
378
|
+
return joinSections(
|
|
379
|
+
harnesses.map((harness) => [
|
|
380
|
+
`${harness.displayName} [${harness.available ? "available" : "unavailable"}]`,
|
|
381
|
+
harness.description,
|
|
382
|
+
...harness.reason ? [`Reason: ${harness.reason}`] : [],
|
|
383
|
+
...harness.actions.length > 0 ? [
|
|
384
|
+
"Actions:",
|
|
385
|
+
...harness.actions.map(
|
|
386
|
+
(action) => `- ${action.label} - ${action.description}`
|
|
387
|
+
)
|
|
388
|
+
] : []
|
|
389
|
+
])
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
function formatOperationPlan(plan) {
|
|
393
|
+
return joinSections([
|
|
394
|
+
[
|
|
395
|
+
plan.title,
|
|
396
|
+
`Target harness: ${formatHarnessName(plan.harness)}`,
|
|
397
|
+
`Action: ${plan.action}`,
|
|
398
|
+
`Dry run: ${plan.dryRun ? "yes" : "no"}`,
|
|
399
|
+
`Can apply: ${plan.canApply ? "yes" : "no"}`,
|
|
400
|
+
`Summary: ${plan.summary}`
|
|
401
|
+
],
|
|
402
|
+
plan.targets.length > 0 ? ["Targets:", ...plan.targets.map(formatTarget)] : [],
|
|
403
|
+
plan.surfaces.length > 0 ? [
|
|
404
|
+
"Managed surfaces:",
|
|
405
|
+
...plan.surfaces.map(
|
|
406
|
+
(surface) => `- ${surface.label}${surface.path ? `: ${surface.path}` : ""}${surface.state ? ` [${surface.state}]` : ""}`
|
|
407
|
+
)
|
|
408
|
+
] : [],
|
|
409
|
+
formatBackup(plan.backup),
|
|
410
|
+
plan.items.length > 0 ? [
|
|
411
|
+
"Plan items:",
|
|
412
|
+
...plan.items.flatMap((item) => [
|
|
413
|
+
`- ${item.title}`,
|
|
414
|
+
` Target: ${item.target.path ?? item.target.label ?? item.target.kind}`,
|
|
415
|
+
...item.state ? [` State: ${item.state}`] : [],
|
|
416
|
+
...item.preview ? [` Preview: ${item.preview}`] : []
|
|
417
|
+
])
|
|
418
|
+
] : [],
|
|
419
|
+
plan.warnings.length > 0 ? ["Warnings:", ...formatWarnings(plan.warnings)] : [],
|
|
420
|
+
plan.disclaimers.length > 0 ? ["Disclaimers:", ...formatDisclaimers(plan.disclaimers)] : []
|
|
421
|
+
]);
|
|
422
|
+
}
|
|
423
|
+
function formatOperationApplyResult(result) {
|
|
424
|
+
return joinSections([
|
|
425
|
+
[
|
|
426
|
+
`Target harness: ${formatHarnessName(result.harness)}`,
|
|
427
|
+
`Action: ${result.action}`,
|
|
428
|
+
`Applied: ${result.applied ? "yes" : "no"}`,
|
|
429
|
+
`Summary: ${result.summary}`
|
|
430
|
+
],
|
|
431
|
+
result.changedTargets.length > 0 ? ["Changed targets:", ...result.changedTargets.map(formatTarget)] : [],
|
|
432
|
+
result.backups.length > 0 ? ["Backups:", ...formatPaths(result.backups)] : [],
|
|
433
|
+
result.warnings.length > 0 ? ["Warnings:", ...formatWarnings(result.warnings)] : [],
|
|
434
|
+
result.disclaimers.length > 0 ? ["Disclaimers:", ...formatDisclaimers(result.disclaimers)] : []
|
|
435
|
+
]);
|
|
436
|
+
}
|
|
437
|
+
function printHelp() {
|
|
438
|
+
console.log(`
|
|
439
|
+
thoth-agents CLI (npm binary: thoth-agents)
|
|
440
|
+
|
|
441
|
+
Usage: thoth-agents [COMMAND] [OPTIONS]
|
|
442
|
+
npx thoth-agents@latest [COMMAND] [OPTIONS]
|
|
443
|
+
pnpm dlx thoth-agents@latest [COMMAND] [OPTIONS]
|
|
444
|
+
pnpm dlx thoth-agents install [OPTIONS]
|
|
445
|
+
pnpm dlx thoth-agents generate --harness=codex --dry-run
|
|
446
|
+
|
|
447
|
+
Commands:
|
|
448
|
+
(no command) Open the interactive TUI in a TTY; fall back to OpenCode install in CI/non-TTY
|
|
449
|
+
install Install OpenCode or Codex agent assets
|
|
450
|
+
generate Generate harness-specific artifacts
|
|
451
|
+
status Show managed install status
|
|
452
|
+
list List managed surfaces and actions
|
|
453
|
+
update Preview managed updates
|
|
454
|
+
sync Preview managed configuration sync
|
|
455
|
+
model Preview role model/provider settings
|
|
456
|
+
|
|
457
|
+
Options:
|
|
458
|
+
--tmux=yes|no Enable tmux integration (yes/no)
|
|
459
|
+
--skills=yes|no Install recommended external skills
|
|
460
|
+
--no-tui Non-interactive mode
|
|
461
|
+
--dry-run Simulate install without writing files
|
|
462
|
+
--reset Repair managed installer-owned targets
|
|
463
|
+
--agent=opencode|codex Select OpenCode plugin install (default) or Codex agent-pack setup
|
|
464
|
+
-h, --help Show this help message
|
|
465
|
+
|
|
466
|
+
Generate options:
|
|
467
|
+
--harness=codex Explicitly select Codex artifact generation
|
|
468
|
+
--output-root=PATH Override generation root metadata
|
|
469
|
+
|
|
470
|
+
OpenCode plugin config and the npm binary are separate surfaces.
|
|
471
|
+
OpenCode loads the plugin with config such as:
|
|
472
|
+
plugin: ["thoth-agents@latest"]
|
|
473
|
+
|
|
474
|
+
That plugin entry does not create a global thoth-agents command.
|
|
475
|
+
Run this CLI through a global install, npx, or pnpm dlx.
|
|
476
|
+
|
|
477
|
+
OpenCode install configures the seven-agent roster, thoth-mem defaults,
|
|
478
|
+
native task delegation, and bundled SDD skills for OpenCode.
|
|
479
|
+
|
|
480
|
+
Bundled thoth-agents skills are always installed.
|
|
481
|
+
Use --skills=no to skip only recommended external skills.
|
|
482
|
+
|
|
483
|
+
The generated config uses OpenAI by default.
|
|
484
|
+
For alternative providers, see docs/provider-configurations.md.
|
|
485
|
+
|
|
486
|
+
Examples:
|
|
487
|
+
thoth-agents
|
|
488
|
+
pnpm dlx thoth-agents@latest
|
|
489
|
+
pnpm dlx thoth-agents@latest install
|
|
490
|
+
pnpm dlx thoth-agents@latest install --agent=opencode
|
|
491
|
+
pnpm dlx thoth-agents@latest install --agent=codex
|
|
492
|
+
pnpm dlx thoth-agents@latest install --agent=codex --dry-run
|
|
493
|
+
pnpm dlx thoth-agents install --no-tui --tmux=no --skills=yes
|
|
494
|
+
pnpm dlx thoth-agents install --dry-run
|
|
495
|
+
pnpm dlx thoth-agents install --reset
|
|
496
|
+
pnpm dlx thoth-agents generate --harness=codex --dry-run
|
|
497
|
+
`);
|
|
498
|
+
}
|
|
499
|
+
function operationContext() {
|
|
500
|
+
return { cwd: process.cwd() };
|
|
501
|
+
}
|
|
502
|
+
function selectedHarness(args, fallback = "opencode") {
|
|
503
|
+
return args.harness ?? fallback;
|
|
504
|
+
}
|
|
505
|
+
function statusReports(args) {
|
|
506
|
+
const harnesses = args.all || !args.harness ? SUPPORTED_OPERATION_HARNESSES : [args.harness];
|
|
507
|
+
const context = operationContext();
|
|
508
|
+
return harnesses.map(
|
|
509
|
+
(harness) => harness === "opencode" ? getOpenCodeStatus(context) : getCodexStatus(context)
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
function buildOperationPlan(command, args) {
|
|
513
|
+
const harness = selectedHarness(args);
|
|
514
|
+
const context = operationContext();
|
|
515
|
+
if (harness === "opencode") {
|
|
516
|
+
return command === "update" ? buildOpenCodeUpdatePlan(context) : buildOpenCodeSyncPlan(context);
|
|
517
|
+
}
|
|
518
|
+
return command === "update" ? buildCodexUpdatePlan(context) : buildCodexSyncPlan(context);
|
|
519
|
+
}
|
|
520
|
+
function defaultModelRoles(harness) {
|
|
521
|
+
if (harness === "codex") {
|
|
522
|
+
return CODEX_ROLE_NAMES.map((role) => ({
|
|
523
|
+
role,
|
|
524
|
+
model: "openai/gpt-5.4-mini"
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
return ALL_AGENT_NAMES.map((role) => ({
|
|
528
|
+
role,
|
|
529
|
+
model: DEFAULT_MODELS[role] ?? "openai/gpt-5.4"
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
function modelRoles(harness, roles) {
|
|
533
|
+
return roles.length > 0 ? [...roles] : defaultModelRoles(harness);
|
|
534
|
+
}
|
|
535
|
+
function printModelGuidance() {
|
|
536
|
+
console.log(
|
|
537
|
+
[
|
|
538
|
+
"Model command requires explicit model input.",
|
|
539
|
+
"Examples:",
|
|
540
|
+
" thoth-agents model --harness=codex --role=deep --model=openai/gpt-5.4-mini",
|
|
541
|
+
" thoth-agents model --harness=opencode --role-model=deep=openai/gpt-5.4",
|
|
542
|
+
"Preview is the default. Add --apply only after reviewing the plan."
|
|
543
|
+
].join("\n")
|
|
544
|
+
);
|
|
545
|
+
return 1;
|
|
546
|
+
}
|
|
547
|
+
function buildModelPlan(args) {
|
|
548
|
+
if (args.roles.length === 0) return void 0;
|
|
549
|
+
const harness = selectedHarness(args);
|
|
550
|
+
const context = operationContext();
|
|
551
|
+
const input = {
|
|
552
|
+
harness,
|
|
553
|
+
dryRun: true,
|
|
554
|
+
roles: modelRoles(harness, args.roles)
|
|
555
|
+
};
|
|
556
|
+
return harness === "opencode" ? buildOpenCodeModelPlan(input, context) : buildCodexModelPlan(input, context);
|
|
557
|
+
}
|
|
558
|
+
function applyOperationPlan(plan) {
|
|
559
|
+
return plan.harness === "opencode" ? applyOpenCodePlan(plan) : applyCodexPlan(plan);
|
|
560
|
+
}
|
|
561
|
+
function printPlanOrApply(plan, args) {
|
|
562
|
+
if (args.apply && args.dryRun) {
|
|
563
|
+
console.error("--apply cannot be combined with --dry-run.");
|
|
564
|
+
return 1;
|
|
565
|
+
}
|
|
566
|
+
if (!args.apply) {
|
|
567
|
+
console.log(formatOperationPlan(plan));
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
console.log(formatOperationApplyResult(applyOperationPlan(plan)));
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
function runOperationCommand(command, args) {
|
|
574
|
+
if (command === "status") {
|
|
575
|
+
console.log(formatHarnessStatusReport(statusReports(args)));
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
if (command === "list") {
|
|
579
|
+
console.log(formatHarnessList(listOperationHarnesses()));
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
582
|
+
if (command === "update" || command === "sync") {
|
|
583
|
+
return printPlanOrApply(buildOperationPlan(command, args), args);
|
|
584
|
+
}
|
|
585
|
+
const plan = buildModelPlan(args);
|
|
586
|
+
if (!plan) return printModelGuidance();
|
|
587
|
+
return printPlanOrApply(plan, args);
|
|
588
|
+
}
|
|
589
|
+
function printCodexGeneration(args) {
|
|
590
|
+
if (!args.dryRun) {
|
|
591
|
+
console.error(
|
|
592
|
+
"Codex generation is dry-run only in this MVP. Pass --dry-run."
|
|
593
|
+
);
|
|
594
|
+
return 1;
|
|
595
|
+
}
|
|
596
|
+
const result = codexAdapter.render({
|
|
597
|
+
projectRoot: process.cwd(),
|
|
598
|
+
options: {
|
|
599
|
+
dryRun: true,
|
|
600
|
+
outputRoot: args.outputRoot,
|
|
601
|
+
targetHarness: "codex"
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
console.log(JSON.stringify(result, null, 2));
|
|
605
|
+
return 0;
|
|
606
|
+
}
|
|
607
|
+
async function runCliCommand(parsed) {
|
|
608
|
+
if (parsed.command === "install") {
|
|
609
|
+
return install(parsed.installArgs);
|
|
610
|
+
}
|
|
611
|
+
if (parsed.command === "generate") {
|
|
612
|
+
return printCodexGeneration(parsed.generateArgs);
|
|
613
|
+
}
|
|
614
|
+
if (parsed.command === "help") {
|
|
615
|
+
printHelp();
|
|
616
|
+
return 0;
|
|
617
|
+
}
|
|
618
|
+
if (parsed.command === "tui") {
|
|
619
|
+
const { runInteractiveTui } = await import("./tui/index.js");
|
|
620
|
+
return runInteractiveTui();
|
|
621
|
+
}
|
|
622
|
+
if (parsed.command === "error") {
|
|
623
|
+
console.error(parsed.message);
|
|
624
|
+
console.error("Run with --help for usage information");
|
|
625
|
+
return 1;
|
|
626
|
+
}
|
|
627
|
+
return runOperationCommand(parsed.command, parsed.operationArgs);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/cli/runtime.ts
|
|
631
|
+
var automationEnvironmentKeys = [
|
|
632
|
+
"CI",
|
|
633
|
+
"CONTINUOUS_INTEGRATION",
|
|
634
|
+
"BUILD_NUMBER",
|
|
635
|
+
"GITHUB_ACTIONS",
|
|
636
|
+
"GITLAB_CI",
|
|
637
|
+
"TF_BUILD",
|
|
638
|
+
"TEAMCITY_VERSION"
|
|
639
|
+
];
|
|
640
|
+
function isTruthyEnvValue(value) {
|
|
641
|
+
if (value === void 0 || value === "") return false;
|
|
642
|
+
const normalized = value.toLowerCase();
|
|
643
|
+
return normalized !== "0" && normalized !== "false";
|
|
644
|
+
}
|
|
645
|
+
function detectRuntimeContext(processLike = process) {
|
|
646
|
+
return {
|
|
647
|
+
stdinIsTTY: processLike.stdin?.isTTY === true,
|
|
648
|
+
stdoutIsTTY: processLike.stdout?.isTTY === true,
|
|
649
|
+
stderrIsTTY: processLike.stderr?.isTTY === true,
|
|
650
|
+
env: { ...processLike.env ?? {} }
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function isInteractiveRuntime(context) {
|
|
654
|
+
if (!context.stdinIsTTY || !context.stdoutIsTTY) return false;
|
|
655
|
+
if (context.env.TERM?.toLowerCase() === "dumb") return false;
|
|
656
|
+
return !automationEnvironmentKeys.some(
|
|
657
|
+
(key) => isTruthyEnvValue(context.env[key])
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/cli/parser.ts
|
|
662
|
+
var defaultRuntimeContext = {
|
|
663
|
+
stdinIsTTY: false,
|
|
664
|
+
stdoutIsTTY: false,
|
|
665
|
+
env: {}
|
|
666
|
+
};
|
|
667
|
+
var operationCommands = /* @__PURE__ */ new Set([
|
|
668
|
+
"status",
|
|
669
|
+
"list",
|
|
670
|
+
"update",
|
|
671
|
+
"sync",
|
|
672
|
+
"model"
|
|
673
|
+
]);
|
|
674
|
+
function parseBooleanArg(name, value) {
|
|
675
|
+
if (value !== "yes" && value !== "no") {
|
|
676
|
+
throw new Error(`${name} must be yes or no.`);
|
|
677
|
+
}
|
|
678
|
+
return value;
|
|
679
|
+
}
|
|
680
|
+
function parseOperationHarness(value) {
|
|
681
|
+
if (value !== "opencode" && value !== "codex") {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`Unsupported operation harness: ${value}. Supported harnesses: opencode, codex.`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return value;
|
|
687
|
+
}
|
|
688
|
+
function parseRoleModel(value) {
|
|
689
|
+
const separator = value.includes("=") ? "=" : ":";
|
|
690
|
+
const [role, ...modelParts] = value.split(separator);
|
|
691
|
+
const model = modelParts.join(separator);
|
|
692
|
+
if (!role || !model) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
"--role-model must use role=model or role:model, for example --role-model=deep=openai/gpt-5.4-mini."
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return { role, model };
|
|
698
|
+
}
|
|
699
|
+
function parseOperationArgs(args) {
|
|
700
|
+
const result = { roles: [] };
|
|
701
|
+
let pendingRole;
|
|
702
|
+
let pendingProvider;
|
|
703
|
+
function pushPendingRole(model) {
|
|
704
|
+
if (!pendingRole) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
"--model requires --role, or pass --role-model=role=model."
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
result.roles.push({
|
|
710
|
+
role: pendingRole,
|
|
711
|
+
model,
|
|
712
|
+
provider: pendingProvider
|
|
713
|
+
});
|
|
714
|
+
pendingRole = void 0;
|
|
715
|
+
pendingProvider = void 0;
|
|
716
|
+
}
|
|
717
|
+
for (const arg of args) {
|
|
718
|
+
if (arg.startsWith("--harness=")) {
|
|
719
|
+
result.harness = parseOperationHarness(arg.split("=")[1] ?? "");
|
|
720
|
+
} else if (arg.startsWith("--agent=")) {
|
|
721
|
+
result.harness = parseOperationHarness(arg.split("=")[1] ?? "");
|
|
722
|
+
} else if (arg === "--all") {
|
|
723
|
+
result.all = true;
|
|
724
|
+
} else if (arg === "--apply") {
|
|
725
|
+
result.apply = true;
|
|
726
|
+
} else if (arg === "--dry-run") {
|
|
727
|
+
result.dryRun = true;
|
|
728
|
+
} else if (arg.startsWith("--role=")) {
|
|
729
|
+
pendingRole = arg.split("=")[1] ?? "";
|
|
730
|
+
if (!pendingRole) throw new Error("--role requires a value.");
|
|
731
|
+
} else if (arg.startsWith("--provider=")) {
|
|
732
|
+
pendingProvider = arg.split("=")[1] ?? "";
|
|
733
|
+
if (!pendingProvider) throw new Error("--provider requires a value.");
|
|
734
|
+
} else if (arg.startsWith("--model=")) {
|
|
735
|
+
pushPendingRole(arg.split("=")[1] ?? "");
|
|
736
|
+
} else if (arg.startsWith("--role-model=")) {
|
|
737
|
+
const parsed = parseRoleModel(arg.slice("--role-model=".length));
|
|
738
|
+
result.roles.push({
|
|
739
|
+
...parsed,
|
|
740
|
+
provider: pendingProvider
|
|
741
|
+
});
|
|
742
|
+
pendingProvider = void 0;
|
|
743
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
744
|
+
throw new Error("help");
|
|
745
|
+
} else {
|
|
746
|
+
throw new Error(`Unsupported operation option: ${arg}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (pendingRole) {
|
|
750
|
+
throw new Error("--role requires a matching --model.");
|
|
751
|
+
}
|
|
752
|
+
if (result.all && result.harness) {
|
|
753
|
+
throw new Error("--all cannot be combined with --harness or --agent.");
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
4333
757
|
function parseInstallArgs(args) {
|
|
4334
758
|
const result = {
|
|
4335
759
|
tui: true,
|
|
@@ -4339,9 +763,9 @@ function parseInstallArgs(args) {
|
|
|
4339
763
|
if (arg === "--no-tui") {
|
|
4340
764
|
result.tui = false;
|
|
4341
765
|
} else if (arg.startsWith("--tmux=")) {
|
|
4342
|
-
result.tmux = arg.split("=")[1];
|
|
766
|
+
result.tmux = parseBooleanArg("--tmux", arg.split("=")[1] ?? "");
|
|
4343
767
|
} else if (arg.startsWith("--skills=")) {
|
|
4344
|
-
result.skills = arg.split("=")[1];
|
|
768
|
+
result.skills = parseBooleanArg("--skills", arg.split("=")[1] ?? "");
|
|
4345
769
|
} else if (arg === "--dry-run") {
|
|
4346
770
|
result.dryRun = true;
|
|
4347
771
|
} else if (arg === "--reset") {
|
|
@@ -4355,8 +779,7 @@ function parseInstallArgs(args) {
|
|
|
4355
779
|
}
|
|
4356
780
|
result.agent = agent;
|
|
4357
781
|
} else if (arg === "-h" || arg === "--help") {
|
|
4358
|
-
|
|
4359
|
-
process.exit(0);
|
|
782
|
+
throw new Error("help");
|
|
4360
783
|
}
|
|
4361
784
|
}
|
|
4362
785
|
return result;
|
|
@@ -4394,13 +817,26 @@ function parseGenerateArgs(args) {
|
|
|
4394
817
|
}
|
|
4395
818
|
};
|
|
4396
819
|
}
|
|
4397
|
-
function parseCliArgs(args) {
|
|
4398
|
-
if (args.length === 0
|
|
4399
|
-
|
|
820
|
+
function parseCliArgs(args, runtimeContext = defaultRuntimeContext) {
|
|
821
|
+
if (args.length === 0) {
|
|
822
|
+
if (isInteractiveRuntime(runtimeContext)) {
|
|
823
|
+
return { command: "tui" };
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
command: "install",
|
|
827
|
+
installArgs: { tui: false, agent: "opencode" }
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if (args[0] === "install") {
|
|
4400
831
|
try {
|
|
4401
|
-
|
|
4402
|
-
|
|
832
|
+
return {
|
|
833
|
+
command: "install",
|
|
834
|
+
installArgs: parseInstallArgs(args.slice(1))
|
|
835
|
+
};
|
|
4403
836
|
} catch (error) {
|
|
837
|
+
if (error instanceof Error && error.message === "help") {
|
|
838
|
+
return { command: "help" };
|
|
839
|
+
}
|
|
4404
840
|
return {
|
|
4405
841
|
command: "error",
|
|
4406
842
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -4413,81 +849,32 @@ function parseCliArgs(args) {
|
|
|
4413
849
|
if (args[0] === "-h" || args[0] === "--help") {
|
|
4414
850
|
return { command: "help" };
|
|
4415
851
|
}
|
|
852
|
+
if (operationCommands.has(args[0])) {
|
|
853
|
+
try {
|
|
854
|
+
return {
|
|
855
|
+
command: args[0],
|
|
856
|
+
operationArgs: parseOperationArgs(args.slice(1))
|
|
857
|
+
};
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (error instanceof Error && error.message === "help") {
|
|
860
|
+
return { command: "help" };
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
command: "error",
|
|
864
|
+
message: error instanceof Error ? error.message : String(error)
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
}
|
|
4416
868
|
return { command: "error", message: `Unknown command: ${args[0]}` };
|
|
4417
869
|
}
|
|
4418
|
-
function printHelp() {
|
|
4419
|
-
console.log(`
|
|
4420
|
-
thoth-agents installer (thoth-agents)
|
|
4421
|
-
|
|
4422
|
-
Usage: pnpm dlx thoth-agents install [OPTIONS]
|
|
4423
|
-
pnpm dlx thoth-agents generate --harness=codex --dry-run
|
|
4424
|
-
|
|
4425
|
-
Options:
|
|
4426
|
-
--tmux=yes|no Enable tmux integration (yes/no)
|
|
4427
|
-
--skills=yes|no Install recommended external skills
|
|
4428
|
-
--no-tui Non-interactive mode
|
|
4429
|
-
--dry-run Simulate install without writing files
|
|
4430
|
-
--reset Repair managed installer-owned targets
|
|
4431
|
-
--agent=opencode|codex Select OpenCode plugin install (default) or Codex agent-pack setup
|
|
4432
|
-
-h, --help Show this help message
|
|
4433
|
-
|
|
4434
|
-
Generate options:
|
|
4435
|
-
--harness=codex Explicitly select Codex artifact generation
|
|
4436
|
-
--output-root=PATH Override generation root metadata
|
|
4437
|
-
|
|
4438
|
-
thoth-agents installs the seven-agent roster, thoth-mem defaults,
|
|
4439
|
-
native task delegation, and bundled SDD skills for OpenCode.
|
|
4440
|
-
|
|
4441
|
-
Bundled thoth-agents skills are always installed.
|
|
4442
|
-
Use --skills=no to skip only recommended external skills.
|
|
4443
|
-
|
|
4444
|
-
The generated config uses OpenAI by default.
|
|
4445
|
-
For alternative providers, see docs/provider-configurations.md.
|
|
4446
870
|
|
|
4447
|
-
|
|
4448
|
-
pnpm dlx thoth-agents@latest install
|
|
4449
|
-
pnpm dlx thoth-agents@latest install --agent=opencode
|
|
4450
|
-
pnpm dlx thoth-agents@latest install --agent=codex
|
|
4451
|
-
pnpm dlx thoth-agents@latest install --agent=codex --dry-run
|
|
4452
|
-
pnpm dlx thoth-agents install --no-tui --tmux=no --skills=yes
|
|
4453
|
-
pnpm dlx thoth-agents install --dry-run
|
|
4454
|
-
pnpm dlx thoth-agents install --reset
|
|
4455
|
-
pnpm dlx thoth-agents generate --harness=codex --dry-run
|
|
4456
|
-
`);
|
|
4457
|
-
}
|
|
4458
|
-
function printCodexGeneration(args) {
|
|
4459
|
-
if (!args.dryRun) {
|
|
4460
|
-
console.error(
|
|
4461
|
-
"Codex generation is dry-run only in this MVP. Pass --dry-run."
|
|
4462
|
-
);
|
|
4463
|
-
return 1;
|
|
4464
|
-
}
|
|
4465
|
-
const result = codexAdapter.render({
|
|
4466
|
-
projectRoot: process.cwd(),
|
|
4467
|
-
options: {
|
|
4468
|
-
dryRun: true,
|
|
4469
|
-
outputRoot: args.outputRoot,
|
|
4470
|
-
targetHarness: "codex"
|
|
4471
|
-
}
|
|
4472
|
-
});
|
|
4473
|
-
console.log(JSON.stringify(result, null, 2));
|
|
4474
|
-
return 0;
|
|
4475
|
-
}
|
|
871
|
+
// src/cli/index.ts
|
|
4476
872
|
async function main() {
|
|
4477
|
-
const parsed = parseCliArgs(
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
process.exit(printCodexGeneration(parsed.generateArgs));
|
|
4483
|
-
} else if (parsed.command === "help") {
|
|
4484
|
-
printHelp();
|
|
4485
|
-
process.exit(0);
|
|
4486
|
-
} else {
|
|
4487
|
-
console.error(parsed.message);
|
|
4488
|
-
console.error("Run with --help for usage information");
|
|
4489
|
-
process.exit(1);
|
|
4490
|
-
}
|
|
873
|
+
const parsed = parseCliArgs(
|
|
874
|
+
process.argv.slice(2),
|
|
875
|
+
detectRuntimeContext(process)
|
|
876
|
+
);
|
|
877
|
+
process.exit(await runCliCommand(parsed));
|
|
4491
878
|
}
|
|
4492
879
|
var entrypointUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : void 0;
|
|
4493
880
|
if (import.meta.url === entrypointUrl) {
|
|
@@ -4497,5 +884,7 @@ if (import.meta.url === entrypointUrl) {
|
|
|
4497
884
|
});
|
|
4498
885
|
}
|
|
4499
886
|
export {
|
|
887
|
+
detectRuntimeContext,
|
|
888
|
+
isInteractiveRuntime,
|
|
4500
889
|
parseCliArgs
|
|
4501
890
|
};
|