thoth-agents 0.7.0 → 0.9.0
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/dist/agents/prompt-dialects.d.ts +2 -1
- package/dist/{chunk-QOIZYJ5C.js → chunk-ARUBELNT.js} +18 -7
- package/dist/{chunk-W5TSNGEE.js → chunk-FBLIVTKM.js} +4036 -3028
- package/dist/{chunk-P3GDM5RH.js → chunk-IJBKUC5J.js} +1 -1
- package/dist/{chunk-3MHJGWS4.js → chunk-V5GDCWFR.js} +1 -1
- package/dist/cli/codex-config-io.d.ts +34 -2
- package/dist/cli/codex-hook-consent.d.ts +5 -0
- package/dist/cli/codex-hook-trust.d.ts +23 -0
- package/dist/cli/codex-install.d.ts +21 -6
- package/dist/cli/codex-lifecycle.d.ts +13 -0
- package/dist/cli/codex-plugin-install.d.ts +21 -0
- package/dist/cli/index.js +539 -44
- package/dist/cli/managed-state-io.d.ts +19 -0
- package/dist/cli/operations/codex.d.ts +2 -0
- package/dist/cli/operations/types.d.ts +1 -1
- package/dist/cli/safe-atomic-write.d.ts +2 -0
- package/dist/cli/tui/index.js +56 -5
- package/dist/cli/tui/operations.d.ts +3 -1
- package/dist/cli/types.d.ts +4 -1
- package/dist/harness/adapters/codex-legacy-agents.d.ts +31 -0
- package/dist/harness/adapters/codex-surfaces.d.ts +1 -1
- package/dist/harness/adapters/codex.d.ts +3 -1
- package/dist/harness/codex-hook-script.d.ts +2 -0
- package/dist/harness/core/agent-pack.d.ts +2 -0
- package/dist/harness/writers/codex-plugin-package.d.ts +7 -0
- package/dist/index.js +2 -2
- package/dist/pi.js +2 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/.thoth-agents-plugin-assets.json +48 -0
- package/plugin/.codex-plugin/plugin.json +3 -2
- package/plugin/agents/orchestrator.md +1 -1
- package/plugin/codex-hooks/context.cjs +22 -0
- package/plugin/codex-hooks/hooks.json +32 -0
- package/plugin/codex-hooks/payloads/designer.md +72 -0
- package/plugin/codex-hooks/payloads/explorer.md +76 -0
- package/plugin/codex-hooks/payloads/librarian.md +76 -0
- package/plugin/codex-hooks/payloads/oracle.md +83 -0
- package/plugin/codex-hooks/payloads/root.md +105 -0
- package/plugin/codex-hooks/payloads/worker.md +72 -0
- package/skills/README.md +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
getOpenCodeVersion,
|
|
5
5
|
isOpenCodeInstalled,
|
|
6
6
|
opencodeAdapter
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-V5GDCWFR.js";
|
|
8
8
|
import {
|
|
9
|
+
CODEX_PLUGIN_ID,
|
|
9
10
|
CODEX_ROLE_NAMES,
|
|
10
11
|
REQUIRED_SKILLS,
|
|
11
12
|
SUPPORTED_OPERATION_HARNESSES,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
applyOpenCodePlan,
|
|
18
19
|
applyPiPlan,
|
|
19
20
|
applyPiSetup,
|
|
21
|
+
assertSafeBackupDestination,
|
|
20
22
|
buildClaudeCodeModelPlan,
|
|
21
23
|
buildClaudeCodeSetupPlan,
|
|
22
24
|
buildClaudeCodeSyncPlan,
|
|
@@ -33,9 +35,13 @@ import {
|
|
|
33
35
|
buildPiSetupPlan,
|
|
34
36
|
buildPiSyncPlan,
|
|
35
37
|
buildPiUpdatePlan,
|
|
38
|
+
checkCodexHookCapability,
|
|
36
39
|
claudeCodeAdapter,
|
|
37
40
|
codexAdapter,
|
|
41
|
+
codexHookSource,
|
|
42
|
+
codexHookTrustEntries,
|
|
38
43
|
defaultClaudeCodeModelRoles,
|
|
44
|
+
defaultCommandExecutor,
|
|
39
45
|
defaultPiModelRoles,
|
|
40
46
|
finalizeHarnessInstall,
|
|
41
47
|
formatClaudeCodeSetupPlan,
|
|
@@ -55,15 +61,26 @@ import {
|
|
|
55
61
|
getRequiredSkillInstallCommand,
|
|
56
62
|
inspectPiPackageSkills,
|
|
57
63
|
installRequiredSkill,
|
|
64
|
+
isMigratableLegacyRole,
|
|
58
65
|
listOperationHarnesses,
|
|
59
66
|
normalizeEffortSelection,
|
|
67
|
+
parseCodexToml,
|
|
68
|
+
parseManagedModelStateJson,
|
|
69
|
+
patchScalar,
|
|
70
|
+
planLegacyRootRemoval,
|
|
71
|
+
readCodexThreadsValue,
|
|
72
|
+
readInstallLedger,
|
|
60
73
|
recordHarnessInstallCompletion,
|
|
74
|
+
resolveCodexTargets,
|
|
75
|
+
roleStructureFingerprint,
|
|
76
|
+
safeAtomicWriteFileSync,
|
|
77
|
+
scalarLocation,
|
|
61
78
|
syncOpenCodeOwnedSkills
|
|
62
|
-
} from "../chunk-
|
|
79
|
+
} from "../chunk-FBLIVTKM.js";
|
|
63
80
|
import "../chunk-62L3ORXN.js";
|
|
64
81
|
import {
|
|
65
82
|
piAdapter
|
|
66
|
-
} from "../chunk-
|
|
83
|
+
} from "../chunk-IJBKUC5J.js";
|
|
67
84
|
import {
|
|
68
85
|
ALL_AGENT_NAMES,
|
|
69
86
|
detectCurrentConfig,
|
|
@@ -74,7 +91,7 @@ import {
|
|
|
74
91
|
resolveExecutingPackageVersion,
|
|
75
92
|
updateOpenCodeMainConfig,
|
|
76
93
|
writeLiteConfig
|
|
77
|
-
} from "../chunk-
|
|
94
|
+
} from "../chunk-ARUBELNT.js";
|
|
78
95
|
|
|
79
96
|
// src/cli/index.ts
|
|
80
97
|
import { pathToFileURL } from "url";
|
|
@@ -91,10 +108,474 @@ function getHarnessAdapter(harness) {
|
|
|
91
108
|
return HARNESS_ADAPTERS[harness];
|
|
92
109
|
}
|
|
93
110
|
|
|
111
|
+
// src/cli/codex-hook-consent.ts
|
|
112
|
+
import { createInterface } from "readline/promises";
|
|
113
|
+
|
|
114
|
+
// src/cli/runtime.ts
|
|
115
|
+
var automationEnvironmentKeys = [
|
|
116
|
+
"CI",
|
|
117
|
+
"CONTINUOUS_INTEGRATION",
|
|
118
|
+
"BUILD_NUMBER",
|
|
119
|
+
"GITHUB_ACTIONS",
|
|
120
|
+
"GITLAB_CI",
|
|
121
|
+
"TF_BUILD",
|
|
122
|
+
"TEAMCITY_VERSION"
|
|
123
|
+
];
|
|
124
|
+
function isTruthyEnvValue(value) {
|
|
125
|
+
if (value === void 0 || value === "") return false;
|
|
126
|
+
const normalized = value.toLowerCase();
|
|
127
|
+
return normalized !== "0" && normalized !== "false";
|
|
128
|
+
}
|
|
129
|
+
function detectRuntimeContext(processLike = process) {
|
|
130
|
+
return {
|
|
131
|
+
stdinIsTTY: processLike.stdin?.isTTY === true,
|
|
132
|
+
stdoutIsTTY: processLike.stdout?.isTTY === true,
|
|
133
|
+
stderrIsTTY: processLike.stderr?.isTTY === true,
|
|
134
|
+
env: { ...processLike.env ?? {} }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function isInteractiveRuntime(context) {
|
|
138
|
+
if (!context.stdinIsTTY || !context.stdoutIsTTY) return false;
|
|
139
|
+
if (context.env.TERM?.toLowerCase() === "dumb") return false;
|
|
140
|
+
return !automationEnvironmentKeys.some(
|
|
141
|
+
(key) => isTruthyEnvValue(context.env[key])
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/cli/codex-hook-consent.ts
|
|
146
|
+
var CODEX_HOOK_CONSENT_PROMPT = "Trust thoth-agents SessionStart and SubagentStart hooks? Trusted hooks can run outside the sandbox. [y/N] ";
|
|
147
|
+
async function requestCodexHookConsent(options) {
|
|
148
|
+
if (options.trustHooks) return true;
|
|
149
|
+
if (options.dryRun || !isInteractiveRuntime(detectRuntimeContext()))
|
|
150
|
+
return false;
|
|
151
|
+
const prompt = createInterface({
|
|
152
|
+
input: process.stdin,
|
|
153
|
+
output: process.stdout
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
return /^(y|yes)$/i.test(
|
|
157
|
+
(await prompt.question(CODEX_HOOK_CONSENT_PROMPT)).trim()
|
|
158
|
+
);
|
|
159
|
+
} finally {
|
|
160
|
+
prompt.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/cli/codex-lifecycle.ts
|
|
165
|
+
import { spawnSync } from "child_process";
|
|
166
|
+
import { createHash } from "crypto";
|
|
167
|
+
import { copyFileSync, lstatSync, readFileSync, rmSync } from "fs";
|
|
168
|
+
import { basename, dirname, join, parse, resolve } from "path";
|
|
169
|
+
function repairCommand(context) {
|
|
170
|
+
return context.localPluginBundlePath || context.pluginId === "thoth-agents@personal" ? "pnpm run setup:codex:local" : "thoth-agents update --agent=codex --apply";
|
|
171
|
+
}
|
|
172
|
+
function readBytes(path) {
|
|
173
|
+
const absolute = resolve(path);
|
|
174
|
+
let cursor = parse(absolute).root;
|
|
175
|
+
for (const part of absolute.slice(cursor.length).split(/[\\/]/)) {
|
|
176
|
+
cursor = join(cursor, part);
|
|
177
|
+
try {
|
|
178
|
+
if (lstatSync(cursor).isSymbolicLink())
|
|
179
|
+
throw new Error(`Unsafe symlink: ${cursor}`);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error.code === "ENOENT") return void 0;
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return readFileSync(path);
|
|
186
|
+
}
|
|
187
|
+
function read(path) {
|
|
188
|
+
return readBytes(path)?.toString("utf8");
|
|
189
|
+
}
|
|
190
|
+
function readConfig(path) {
|
|
191
|
+
const bytes = readBytes(path);
|
|
192
|
+
const text = bytes?.toString("utf8") ?? "";
|
|
193
|
+
if (bytes && !Buffer.from(text, "utf8").equals(bytes))
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Invalid UTF-8 config; preserve ${path} and review manually.`
|
|
196
|
+
);
|
|
197
|
+
return text;
|
|
198
|
+
}
|
|
199
|
+
function snapshot(context) {
|
|
200
|
+
const targets = resolveCodexTargets({
|
|
201
|
+
scope: "user",
|
|
202
|
+
projectRoot: context.cwd,
|
|
203
|
+
homeDir: context.homeDir,
|
|
204
|
+
codexHome: context.codexHome
|
|
205
|
+
});
|
|
206
|
+
const stateBytes = readBytes(targets.managedModelsPath);
|
|
207
|
+
const stateText = stateBytes?.toString("utf8");
|
|
208
|
+
if (stateText !== void 0) {
|
|
209
|
+
const raw = JSON.parse(stateText);
|
|
210
|
+
if (raw?.version !== 1 || !raw.models || typeof raw.models !== "object" || Array.isArray(raw.models))
|
|
211
|
+
throw new Error(
|
|
212
|
+
"Unreadable managed model state; repair it manually before uninstall."
|
|
213
|
+
);
|
|
214
|
+
if (raw.priorThreadsValue !== void 0 && raw.priorThreadsValue !== null && (typeof raw.priorThreadsValue !== "string" || !/^\+?\d[\d_]*$/.test(raw.priorThreadsValue)))
|
|
215
|
+
throw new Error("Invalid prior threads ownership state.");
|
|
216
|
+
for (const field of ["roleDigests", "roleStructureDigests"])
|
|
217
|
+
if (raw[field] !== void 0 && (typeof raw[field] !== "object" || raw[field] === null || Array.isArray(raw[field]) || Object.values(raw[field]).some(
|
|
218
|
+
(value) => typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)
|
|
219
|
+
)))
|
|
220
|
+
throw new Error(`Invalid ${field} ownership state.`);
|
|
221
|
+
if (raw.ownedHookTrustKeys !== void 0 && (!Array.isArray(raw.ownedHookTrustKeys) || raw.ownedHookTrustKeys.some(
|
|
222
|
+
(key) => typeof key !== "string" || !key.startsWith(`${codexHookSource(context.pluginId)}:`)
|
|
223
|
+
)))
|
|
224
|
+
throw new Error("Invalid hook ownership state.");
|
|
225
|
+
}
|
|
226
|
+
const state = parseManagedModelStateJson(stateText, 1);
|
|
227
|
+
const config = readConfig(targets.configPath);
|
|
228
|
+
parseCodexToml(config);
|
|
229
|
+
const roles = targets.roleAgentPaths.map((target) => {
|
|
230
|
+
const bytes = readBytes(target.path);
|
|
231
|
+
const content = bytes?.toString("utf8");
|
|
232
|
+
const key = basename(target.path);
|
|
233
|
+
const status = bytes === void 0 ? "missing" : createHash("sha256").update(bytes).digest("hex") === state.roleDigests?.[key] ? "owned" : roleStructureFingerprint(content ?? "") === state.roleStructureDigests?.[key] ? "model-edited" : "drifted";
|
|
234
|
+
return { ...target, content, bytes, status };
|
|
235
|
+
});
|
|
236
|
+
return { targets, state, stateBytes, config, roles };
|
|
237
|
+
}
|
|
238
|
+
function trust(context) {
|
|
239
|
+
const capability = checkCodexHookCapability({
|
|
240
|
+
projectRoot: context.cwd,
|
|
241
|
+
homeDir: context.homeDir,
|
|
242
|
+
codexHome: context.codexHome,
|
|
243
|
+
commandExecutor: context.codexPluginCommandExecutor,
|
|
244
|
+
pluginId: context.pluginId,
|
|
245
|
+
localPluginBundlePath: context.localPluginBundlePath
|
|
246
|
+
});
|
|
247
|
+
const entries = capability.bundlePath ? codexHookTrustEntries(
|
|
248
|
+
JSON.parse(
|
|
249
|
+
readFileSync(
|
|
250
|
+
join(capability.bundlePath, "codex-hooks", "hooks.json"),
|
|
251
|
+
"utf8"
|
|
252
|
+
)
|
|
253
|
+
),
|
|
254
|
+
codexHookSource(context.pluginId),
|
|
255
|
+
process.platform
|
|
256
|
+
) : {};
|
|
257
|
+
return { capability, entries };
|
|
258
|
+
}
|
|
259
|
+
function storedHash(content, key) {
|
|
260
|
+
const document = parseCodexToml(content);
|
|
261
|
+
const hooks = document.hooks;
|
|
262
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks))
|
|
263
|
+
return void 0;
|
|
264
|
+
const state = hooks.state;
|
|
265
|
+
if (!state || typeof state !== "object" || Array.isArray(state))
|
|
266
|
+
return void 0;
|
|
267
|
+
const entry = state[key];
|
|
268
|
+
return entry && typeof entry === "object" && !Array.isArray(entry) ? entry.trusted_hash : void 0;
|
|
269
|
+
}
|
|
270
|
+
function removeScalar(content, path) {
|
|
271
|
+
const { lines, match, sectionStart, sectionEnd } = scalarLocation(
|
|
272
|
+
content,
|
|
273
|
+
path
|
|
274
|
+
);
|
|
275
|
+
if (match) {
|
|
276
|
+
if (path[0] === "hooks" && sectionStart >= 0 && lines.slice(sectionStart + 1, sectionEnd).every(
|
|
277
|
+
(line, index) => sectionStart + 1 + index === match.index || /^\s*(?:#.*)?$/.test(line.text)
|
|
278
|
+
))
|
|
279
|
+
lines[sectionStart].text = "";
|
|
280
|
+
lines.splice(match.index, 1);
|
|
281
|
+
}
|
|
282
|
+
return lines.map((line) => line.text + line.eol).join("");
|
|
283
|
+
}
|
|
284
|
+
function uninstallCodex(context, dryRun = false) {
|
|
285
|
+
const messages = [];
|
|
286
|
+
const mutations = [];
|
|
287
|
+
try {
|
|
288
|
+
let unchanged2 = function(path, expected) {
|
|
289
|
+
try {
|
|
290
|
+
const current = readBytes(path);
|
|
291
|
+
if (current === void 0 ? expected === void 0 : expected !== void 0 && current.equals(expected))
|
|
292
|
+
return true;
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
messages.push(`Preserve concurrently changed ${path}; review manually.`);
|
|
296
|
+
return false;
|
|
297
|
+
}, ownershipUnchanged2 = function() {
|
|
298
|
+
return unchanged2(targets.managedModelsPath, stateBytes);
|
|
299
|
+
};
|
|
300
|
+
var unchanged = unchanged2, ownershipUnchanged = ownershipUnchanged2;
|
|
301
|
+
const { targets, state, stateBytes, config, roles } = snapshot(context);
|
|
302
|
+
const ledger = readInstallLedger(
|
|
303
|
+
context.installLedgerOptions ?? { homeDir: context.homeDir }
|
|
304
|
+
);
|
|
305
|
+
if (ledger.status === "invalid") throw new Error(ledger.error);
|
|
306
|
+
const recovery = join(
|
|
307
|
+
dirname(targets.managedModelsPath),
|
|
308
|
+
".thoth-agents-install-recovery.json"
|
|
309
|
+
);
|
|
310
|
+
const recoveryText = read(recovery);
|
|
311
|
+
if (recoveryText !== void 0) {
|
|
312
|
+
const raw = JSON.parse(recoveryText);
|
|
313
|
+
if (raw?.version !== 1 || !raw.roles || typeof raw.roles !== "object" || Array.isArray(raw.roles) || Object.values(raw.roles).some(
|
|
314
|
+
(value) => typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)
|
|
315
|
+
))
|
|
316
|
+
throw new Error("Unreadable recovery journal; repair manually.");
|
|
317
|
+
}
|
|
318
|
+
for (const role of roles) {
|
|
319
|
+
if (role.status === "owned" || role.status === "model-edited") {
|
|
320
|
+
messages.push(`Remove ${role.path} (${role.status})`);
|
|
321
|
+
mutations.push(() => {
|
|
322
|
+
if (ownershipUnchanged2() && unchanged2(role.path, role.bytes))
|
|
323
|
+
rmSync(role.path);
|
|
324
|
+
});
|
|
325
|
+
} else
|
|
326
|
+
messages.push(
|
|
327
|
+
`Preserve ${role.path} (${role.status}); review manually.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const root = planLegacyRootRemoval(targets.rootInstructionsPath);
|
|
331
|
+
messages.push(...root.diagnostics);
|
|
332
|
+
for (const item of root.items) {
|
|
333
|
+
const bytes = item.remainingRootBytes;
|
|
334
|
+
if (!bytes)
|
|
335
|
+
throw new Error("Legacy root removal is missing its preserved bytes.");
|
|
336
|
+
messages.push(item.description);
|
|
337
|
+
mutations.push(() => {
|
|
338
|
+
if (!unchanged2(item.targetPath, item.legacyRootBytes)) return;
|
|
339
|
+
assertSafeBackupDestination(`${item.targetPath}.bak`);
|
|
340
|
+
copyFileSync(item.targetPath, `${item.targetPath}.bak`);
|
|
341
|
+
if (bytes.every((byte) => byte === 32 || byte >= 9 && byte <= 13))
|
|
342
|
+
rmSync(item.targetPath);
|
|
343
|
+
else safeAtomicWriteFileSync(item.targetPath, bytes);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
let patched = config;
|
|
347
|
+
const configPaths = [];
|
|
348
|
+
if (state.priorThreadsValue !== void 0) {
|
|
349
|
+
const current = readCodexThreadsValue(config);
|
|
350
|
+
if (current !== null && Number(current.replaceAll("_", "")) === 6) {
|
|
351
|
+
configPaths.push(["agents", "max_concurrent_threads_per_session"]);
|
|
352
|
+
patched = state.priorThreadsValue === null ? removeScalar(patched, [
|
|
353
|
+
"agents",
|
|
354
|
+
"max_concurrent_threads_per_session"
|
|
355
|
+
]) : patchScalar(
|
|
356
|
+
patched,
|
|
357
|
+
["agents", "max_concurrent_threads_per_session"],
|
|
358
|
+
state.priorThreadsValue
|
|
359
|
+
);
|
|
360
|
+
messages.push(
|
|
361
|
+
`Restore threads setting to ${state.priorThreadsValue ?? "absent"}`
|
|
362
|
+
);
|
|
363
|
+
} else
|
|
364
|
+
messages.push(
|
|
365
|
+
"Preserve operator-changed threads setting; review config.toml manually."
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
if (state.ownedHookTrustKeys?.length) {
|
|
369
|
+
const { entries } = trust(context);
|
|
370
|
+
for (const key of state.ownedHookTrustKeys) {
|
|
371
|
+
const path = ["hooks", "state", key, "trusted_hash"];
|
|
372
|
+
const value = storedHash(patched, key);
|
|
373
|
+
if (entries[key] && value === entries[key]) {
|
|
374
|
+
configPaths.push(path);
|
|
375
|
+
patched = removeScalar(patched, path);
|
|
376
|
+
messages.push(`Remove owned hook trust ${key}`);
|
|
377
|
+
} else
|
|
378
|
+
messages.push(
|
|
379
|
+
`Preserve hook trust ${key}: changed or original hash unverifiable; review in Codex.`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (patched !== config)
|
|
384
|
+
mutations.push(() => {
|
|
385
|
+
if (!ownershipUnchanged2()) return;
|
|
386
|
+
let current;
|
|
387
|
+
try {
|
|
388
|
+
current = readConfig(targets.configPath);
|
|
389
|
+
} catch {
|
|
390
|
+
messages.push(
|
|
391
|
+
`Preserve concurrently changed ${targets.configPath}; review manually.`
|
|
392
|
+
);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
let rebased = current;
|
|
396
|
+
for (const path of configPaths) {
|
|
397
|
+
const before = scalarLocation(config, path).match?.value;
|
|
398
|
+
if (scalarLocation(current, path).match?.value !== before) {
|
|
399
|
+
messages.push(
|
|
400
|
+
`Preserve concurrently changed ${path.join(".")}; review config.toml manually.`
|
|
401
|
+
);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const after = scalarLocation(patched, path).match?.value;
|
|
405
|
+
rebased = after === void 0 ? removeScalar(rebased, path) : patchScalar(rebased, path, after);
|
|
406
|
+
}
|
|
407
|
+
if (rebased !== current) {
|
|
408
|
+
assertSafeBackupDestination(`${targets.configPath}.bak`);
|
|
409
|
+
copyFileSync(targets.configPath, `${targets.configPath}.bak`);
|
|
410
|
+
safeAtomicWriteFileSync(targets.configPath, rebased);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
messages.push(
|
|
414
|
+
`Run codex plugin remove ${context.pluginId ?? CODEX_PLUGIN_ID} --json`
|
|
415
|
+
);
|
|
416
|
+
messages.push(
|
|
417
|
+
`Preserve ${(context.pluginId ?? CODEX_PLUGIN_ID).split("@")[1]} marketplace: registration ownership/other consumers not determinable.`,
|
|
418
|
+
"Not removed: provider-owned thoth-mem surfaces and external skills."
|
|
419
|
+
);
|
|
420
|
+
for (const path of [recovery, targets.managedModelsPath]) {
|
|
421
|
+
const expected = readBytes(path);
|
|
422
|
+
if (expected !== void 0) {
|
|
423
|
+
messages.push(`Remove managed state ${path}`);
|
|
424
|
+
mutations.push(() => {
|
|
425
|
+
if (unchanged2(path, expected)) rmSync(path);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (ledger.status === "valid" && ledger.ledger.harnesses.codex) {
|
|
430
|
+
const next = {
|
|
431
|
+
...ledger.ledger,
|
|
432
|
+
harnesses: { ...ledger.ledger.harnesses }
|
|
433
|
+
};
|
|
434
|
+
delete next.harnesses.codex;
|
|
435
|
+
const ledgerBytes = readBytes(ledger.path);
|
|
436
|
+
messages.push("Remove codex install ledger entry");
|
|
437
|
+
mutations.push(() => {
|
|
438
|
+
if (unchanged2(ledger.path, ledgerBytes))
|
|
439
|
+
safeAtomicWriteFileSync(
|
|
440
|
+
ledger.path,
|
|
441
|
+
`${JSON.stringify(next, null, 2)}
|
|
442
|
+
`
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (!dryRun) {
|
|
447
|
+
if (patched !== config)
|
|
448
|
+
assertSafeBackupDestination(`${targets.configPath}.bak`);
|
|
449
|
+
for (const item of root.items)
|
|
450
|
+
assertSafeBackupDestination(`${item.targetPath}.bak`);
|
|
451
|
+
const result = (context.codexPluginCommandExecutor ?? defaultCommandExecutor)(
|
|
452
|
+
"codex",
|
|
453
|
+
["plugin", "remove", context.pluginId ?? CODEX_PLUGIN_ID, "--json"],
|
|
454
|
+
{
|
|
455
|
+
cwd: context.cwd
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
if (result.exitCode !== 0)
|
|
459
|
+
throw new Error(
|
|
460
|
+
`Native plugin removal failed: ${result.stderr}; local ownership state retained.`
|
|
461
|
+
);
|
|
462
|
+
for (const mutate of mutations) mutate();
|
|
463
|
+
}
|
|
464
|
+
return { success: true, messages };
|
|
465
|
+
} catch (error) {
|
|
466
|
+
return { success: false, messages: [...messages, String(error)] };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function doctorCodex(context) {
|
|
470
|
+
const messages = [];
|
|
471
|
+
let success = true;
|
|
472
|
+
let fix = repairCommand(context);
|
|
473
|
+
function finding(label, ok, fix2) {
|
|
474
|
+
messages.push(ok ? label : `${label}; Fix: ${fix2}`);
|
|
475
|
+
if (!ok) success = false;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const executor = context.codexPluginCommandExecutor ?? defaultCommandExecutor;
|
|
479
|
+
let selectedContext = context;
|
|
480
|
+
let pluginKnown = false;
|
|
481
|
+
try {
|
|
482
|
+
const result = executor("codex", ["plugin", "list", "--json"], {
|
|
483
|
+
cwd: context.cwd
|
|
484
|
+
});
|
|
485
|
+
if (result.exitCode !== 0)
|
|
486
|
+
throw new Error(
|
|
487
|
+
`codex plugin list --json exited ${result.exitCode}; stderr: ${result.stderr || "(empty)"}; stdout: ${result.stdout || "(empty)"}`
|
|
488
|
+
);
|
|
489
|
+
const plugins = JSON.parse(result.stdout).installed;
|
|
490
|
+
if (!Array.isArray(plugins))
|
|
491
|
+
throw new Error("Unreadable native installed plugin list.");
|
|
492
|
+
const candidates = plugins.filter(
|
|
493
|
+
(item) => item && typeof item.pluginId === "string" && item.pluginId.startsWith("thoth-agents@")
|
|
494
|
+
);
|
|
495
|
+
const plugin = candidates.length === 1 ? candidates[0] : void 0;
|
|
496
|
+
if (plugin) {
|
|
497
|
+
pluginKnown = true;
|
|
498
|
+
selectedContext = {
|
|
499
|
+
...context,
|
|
500
|
+
pluginId: plugin.pluginId,
|
|
501
|
+
localPluginBundlePath: plugin.source?.source === "local" && typeof plugin.source.path === "string" ? plugin.source.path : context.localPluginBundlePath
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
fix = repairCommand(selectedContext);
|
|
505
|
+
finding(
|
|
506
|
+
`Plugin: ${candidates.length > 1 ? `ambiguous (${candidates.map((item) => `${item.pluginId}, enabled=${item.enabled}`).join("; ")})` : plugin ? `installed (${plugin.pluginId})` : "missing"}, enabled=${plugin?.enabled ?? false}, version=${plugin?.version ?? "unknown"}`,
|
|
507
|
+
!!plugin?.enabled,
|
|
508
|
+
candidates.length > 1 ? "Select exactly one thoth-agents plugin in Codex native plugin management." : fix
|
|
509
|
+
);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
finding(
|
|
512
|
+
`Plugin inspection failed: ${String(error)}`,
|
|
513
|
+
false,
|
|
514
|
+
"Run codex plugin list --json; resolve the captured native error."
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const { targets, state, config, roles } = snapshot(selectedContext);
|
|
518
|
+
const { capability, entries } = pluginKnown ? trust(selectedContext) : { capability: { success: false }, entries: {} };
|
|
519
|
+
finding(
|
|
520
|
+
`Hook capability: ${capability.success ? "available" : "unavailable"}`,
|
|
521
|
+
capability.success,
|
|
522
|
+
fix
|
|
523
|
+
);
|
|
524
|
+
if (!Object.keys(entries).length)
|
|
525
|
+
for (const event of ["session_start", "subagent_start"])
|
|
526
|
+
finding(
|
|
527
|
+
`Hook ${codexHookSource(selectedContext.pluginId)}:${event}:0:0: not applicable (no hook-capable bundle)`,
|
|
528
|
+
true,
|
|
529
|
+
fix
|
|
530
|
+
);
|
|
531
|
+
for (const [key, hash] of Object.entries(entries)) {
|
|
532
|
+
const raw = storedHash(config, key);
|
|
533
|
+
const status = raw === void 0 ? "untrusted" : raw === hash ? "trusted" : "modified";
|
|
534
|
+
finding(
|
|
535
|
+
`Hook ${key}: ${status}`,
|
|
536
|
+
status === "trusted",
|
|
537
|
+
`Review and trust this handler in Codex native hook review; refresh declarations if needed with ${fix}.`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
const node = context.nodeAvailable?.() ?? spawnSync("node", ["--version"], { encoding: "utf8", timeout: 1e4 }).status === 0;
|
|
541
|
+
finding(
|
|
542
|
+
`Node on PATH: ${node ? "available" : "missing"}`,
|
|
543
|
+
node,
|
|
544
|
+
"Install Node.js >=22.19 and restart Codex with Node on PATH."
|
|
545
|
+
);
|
|
546
|
+
for (const role of roles)
|
|
547
|
+
finding(
|
|
548
|
+
`Agent ${role.role}: ${role.content && isMigratableLegacyRole(role.content, role.role) ? "legacy full TOML (migratable)" : role.status}`,
|
|
549
|
+
!isMigratableLegacyRole(role.content ?? "", role.role) && (role.status === "owned" || role.status === "model-edited"),
|
|
550
|
+
role.content && isMigratableLegacyRole(role.content, role.role) ? `Review/back up edits, then run ${fix} --replace-legacy-agents` : role.status === "drifted" ? `Review/back up edits, then run ${fix}` : fix
|
|
551
|
+
);
|
|
552
|
+
const threads = readCodexThreadsValue(config);
|
|
553
|
+
const managed = state.priorThreadsValue !== void 0 && threads !== null && Number(threads.replaceAll("_", "")) === 6;
|
|
554
|
+
finding(
|
|
555
|
+
`Threads: ${threads === null ? "absent" : managed ? "managed" : "operator-changed"} (${threads ?? "none"})`,
|
|
556
|
+
managed,
|
|
557
|
+
`Review config.toml; set agents.max_concurrent_threads_per_session = 6 and restart Codex; ${fix}.`
|
|
558
|
+
);
|
|
559
|
+
const legacy = planLegacyRootRemoval(targets.rootInstructionsPath);
|
|
560
|
+
const present = legacy.items.length > 0 || legacy.diagnostics.length > 0;
|
|
561
|
+
finding(
|
|
562
|
+
`Legacy AGENTS.md block: ${present ? "present" : "absent"}`,
|
|
563
|
+
!present,
|
|
564
|
+
legacy.diagnostics.length ? "Repair ambiguous markers manually." : fix
|
|
565
|
+
);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
success = false;
|
|
568
|
+
messages.push(
|
|
569
|
+
`${String(error)} Fix: inspect state/config manually, then ${fix}.`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return { success, messages };
|
|
573
|
+
}
|
|
574
|
+
|
|
94
575
|
// src/cli/install.ts
|
|
95
576
|
import { existsSync } from "fs";
|
|
96
577
|
import { homedir } from "os";
|
|
97
|
-
import { join } from "path";
|
|
578
|
+
import { join as join2 } from "path";
|
|
98
579
|
import { cwd } from "process";
|
|
99
580
|
var GREEN = "\x1B[32m";
|
|
100
581
|
var BLUE = "\x1B[34m";
|
|
@@ -441,7 +922,7 @@ async function runPiInstall(config, dependencies, packageVersion, executingPacka
|
|
|
441
922
|
const packageSkills = (dependencies.inspectPiPackageSkills ?? inspectPiPackageSkills)({ packageRoot: applied.configuredPackageRoot });
|
|
442
923
|
for (const skill of packageSkills.skills)
|
|
443
924
|
printInfo(
|
|
444
|
-
` - ${skill.name}: ${
|
|
925
|
+
` - ${skill.name}: ${join2(skill.destinationPath, "SKILL.md")}`
|
|
445
926
|
);
|
|
446
927
|
if (!packageSkills.success) {
|
|
447
928
|
printError(
|
|
@@ -516,7 +997,9 @@ async function install(args, dependencies = {}) {
|
|
|
516
997
|
printError(`Codex plugin install failed: ${pluginResult.error}`);
|
|
517
998
|
return 1;
|
|
518
999
|
}
|
|
1000
|
+
const trustHooks = await requestCodexHookConsent(args);
|
|
519
1001
|
const plan = buildCodexSetupPlan({
|
|
1002
|
+
trustHooks,
|
|
520
1003
|
dryRun: config.dryRun,
|
|
521
1004
|
reset: config.reset,
|
|
522
1005
|
scope: "user",
|
|
@@ -752,6 +1235,8 @@ Commands:
|
|
|
752
1235
|
(no command) Open the interactive TUI in a TTY; fall back to OpenCode install in CI/non-TTY
|
|
753
1236
|
install Install OpenCode, Codex, Claude Code, or Pi agent assets
|
|
754
1237
|
generate Generate harness-specific artifacts
|
|
1238
|
+
uninstall Remove attributable Codex surfaces (--agent=codex; --dry-run previews)
|
|
1239
|
+
doctor Diagnose Codex installation, read-only (--agent=codex)
|
|
755
1240
|
status Show official CLI-managed versions and managed install status
|
|
756
1241
|
list List managed surfaces and actions
|
|
757
1242
|
update Preview a complete selected-harness CLI refresh
|
|
@@ -763,6 +1248,7 @@ Options:
|
|
|
763
1248
|
--no-tui Non-interactive mode
|
|
764
1249
|
--dry-run Simulate install without writing files
|
|
765
1250
|
--apply Apply a reviewed update, sync, or model plan
|
|
1251
|
+
--trust-hooks Consent to Codex plugin hooks running outside the sandbox (install/update)
|
|
766
1252
|
--reset Repair managed installer-owned targets
|
|
767
1253
|
--agent=opencode|codex|claude|pi
|
|
768
1254
|
Select OpenCode plugin install (default), Codex/Claude setup, or complete Pi-native setup
|
|
@@ -796,7 +1282,7 @@ OpenCode install configures the adaptive six-role roster and native task delegat
|
|
|
796
1282
|
Provider capability is external and reported only from caller-supplied evidence.
|
|
797
1283
|
|
|
798
1284
|
External required skills are installed for every harness:
|
|
799
|
-
simplify, tdd, progressive-context-router, and
|
|
1285
|
+
simplify, tdd, progressive-context-router, and grill-me.
|
|
800
1286
|
Their installation is mandatory and cannot be skipped.
|
|
801
1287
|
|
|
802
1288
|
The generated OpenCode config supports the built-in OpenAI preset only.
|
|
@@ -947,6 +1433,26 @@ function printPlanOrApply(plan, args, applyPlan) {
|
|
|
947
1433
|
return result.applied ? 0 : 1;
|
|
948
1434
|
}
|
|
949
1435
|
async function runOperationCommand(command, args, services) {
|
|
1436
|
+
if (command === "doctor" || command === "uninstall") {
|
|
1437
|
+
if (args.harness !== "codex" || args.all || args.roles.length || args.trustHooks || args.replaceLegacyAgents || command === "doctor" && args.apply) {
|
|
1438
|
+
console.error(
|
|
1439
|
+
`${command} supports only --agent=codex with no model/trust options.`
|
|
1440
|
+
);
|
|
1441
|
+
return 1;
|
|
1442
|
+
}
|
|
1443
|
+
if (args.apply && args.dryRun) {
|
|
1444
|
+
console.error("--apply cannot be combined with --dry-run.");
|
|
1445
|
+
return 1;
|
|
1446
|
+
}
|
|
1447
|
+
const result = command === "doctor" ? doctorCodex(services.operationContext()) : uninstallCodex(services.operationContext(), args.dryRun ?? false);
|
|
1448
|
+
console.log(
|
|
1449
|
+
[
|
|
1450
|
+
`Codex ${command}${args.dryRun ? " (dry-run)" : ""}`,
|
|
1451
|
+
...result.messages
|
|
1452
|
+
].join("\n")
|
|
1453
|
+
);
|
|
1454
|
+
return result.success ? 0 : 1;
|
|
1455
|
+
}
|
|
950
1456
|
if (command === "status") {
|
|
951
1457
|
console.log(
|
|
952
1458
|
formatHarnessStatusReport(
|
|
@@ -959,9 +1465,23 @@ async function runOperationCommand(command, args, services) {
|
|
|
959
1465
|
console.log(formatHarnessList(listOperationHarnesses()));
|
|
960
1466
|
return 0;
|
|
961
1467
|
}
|
|
1468
|
+
if (args.replaceLegacyAgents && (command !== "update" || selectedHarness(args) !== "codex")) {
|
|
1469
|
+
console.error(
|
|
1470
|
+
"--replace-legacy-agents supports only update --agent=codex."
|
|
1471
|
+
);
|
|
1472
|
+
return 1;
|
|
1473
|
+
}
|
|
962
1474
|
if (command === "update" || command === "sync") {
|
|
1475
|
+
const trustHooks = selectedHarness(args) === "codex" && command === "update" ? await requestCodexHookConsent({
|
|
1476
|
+
trustHooks: args.trustHooks,
|
|
1477
|
+
dryRun: !args.apply || args.dryRun
|
|
1478
|
+
}) : false;
|
|
963
1479
|
return printPlanOrApply(
|
|
964
|
-
buildOperationPlan(command, args,
|
|
1480
|
+
buildOperationPlan(command, args, {
|
|
1481
|
+
...services.operationContext(),
|
|
1482
|
+
trustHooks,
|
|
1483
|
+
replaceLegacyAgents: args.replaceLegacyAgents
|
|
1484
|
+
}),
|
|
965
1485
|
args,
|
|
966
1486
|
services.applyOperationPlan ?? applyOperationPlan
|
|
967
1487
|
);
|
|
@@ -1019,46 +1539,15 @@ async function runCliCommand(parsed, modelServices = defaultModelCommandServices
|
|
|
1019
1539
|
}
|
|
1020
1540
|
|
|
1021
1541
|
// src/cli/parser.ts
|
|
1022
|
-
import { isAbsolute, resolve } from "path";
|
|
1023
|
-
|
|
1024
|
-
// src/cli/runtime.ts
|
|
1025
|
-
var automationEnvironmentKeys = [
|
|
1026
|
-
"CI",
|
|
1027
|
-
"CONTINUOUS_INTEGRATION",
|
|
1028
|
-
"BUILD_NUMBER",
|
|
1029
|
-
"GITHUB_ACTIONS",
|
|
1030
|
-
"GITLAB_CI",
|
|
1031
|
-
"TF_BUILD",
|
|
1032
|
-
"TEAMCITY_VERSION"
|
|
1033
|
-
];
|
|
1034
|
-
function isTruthyEnvValue(value) {
|
|
1035
|
-
if (value === void 0 || value === "") return false;
|
|
1036
|
-
const normalized = value.toLowerCase();
|
|
1037
|
-
return normalized !== "0" && normalized !== "false";
|
|
1038
|
-
}
|
|
1039
|
-
function detectRuntimeContext(processLike = process) {
|
|
1040
|
-
return {
|
|
1041
|
-
stdinIsTTY: processLike.stdin?.isTTY === true,
|
|
1042
|
-
stdoutIsTTY: processLike.stdout?.isTTY === true,
|
|
1043
|
-
stderrIsTTY: processLike.stderr?.isTTY === true,
|
|
1044
|
-
env: { ...processLike.env ?? {} }
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
function isInteractiveRuntime(context) {
|
|
1048
|
-
if (!context.stdinIsTTY || !context.stdoutIsTTY) return false;
|
|
1049
|
-
if (context.env.TERM?.toLowerCase() === "dumb") return false;
|
|
1050
|
-
return !automationEnvironmentKeys.some(
|
|
1051
|
-
(key) => isTruthyEnvValue(context.env[key])
|
|
1052
|
-
);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// src/cli/parser.ts
|
|
1542
|
+
import { isAbsolute, resolve as resolve2 } from "path";
|
|
1056
1543
|
var defaultRuntimeContext = {
|
|
1057
1544
|
stdinIsTTY: false,
|
|
1058
1545
|
stdoutIsTTY: false,
|
|
1059
1546
|
env: {}
|
|
1060
1547
|
};
|
|
1061
1548
|
var operationCommands = /* @__PURE__ */ new Set([
|
|
1549
|
+
"uninstall",
|
|
1550
|
+
"doctor",
|
|
1062
1551
|
"status",
|
|
1063
1552
|
"list",
|
|
1064
1553
|
"update",
|
|
@@ -1145,6 +1634,10 @@ function parseOperationArgs(args) {
|
|
|
1145
1634
|
result.all = true;
|
|
1146
1635
|
} else if (arg === "--apply") {
|
|
1147
1636
|
result.apply = true;
|
|
1637
|
+
} else if (arg === "--replace-legacy-agents") {
|
|
1638
|
+
result.replaceLegacyAgents = true;
|
|
1639
|
+
} else if (arg === "--trust-hooks") {
|
|
1640
|
+
result.trustHooks = true;
|
|
1148
1641
|
} else if (arg === "--dry-run") {
|
|
1149
1642
|
result.dryRun = true;
|
|
1150
1643
|
} else if (arg.startsWith("--role=")) {
|
|
@@ -1191,6 +1684,8 @@ function parseInstallArgs(args) {
|
|
|
1191
1684
|
result.tui = false;
|
|
1192
1685
|
} else if (arg.startsWith("--tmux=")) {
|
|
1193
1686
|
result.tmux = parseBooleanArg("--tmux", arg.split("=")[1] ?? "");
|
|
1687
|
+
} else if (arg === "--trust-hooks") {
|
|
1688
|
+
result.trustHooks = true;
|
|
1194
1689
|
} else if (arg === "--dry-run") {
|
|
1195
1690
|
result.dryRun = true;
|
|
1196
1691
|
} else if (arg === "--reset") {
|
|
@@ -1217,7 +1712,7 @@ function parseInstallArgs(args) {
|
|
|
1217
1712
|
if (!localPackageRoot || localPackageRoot.startsWith("--")) {
|
|
1218
1713
|
throw new Error("--local-package-root requires a value.");
|
|
1219
1714
|
}
|
|
1220
|
-
if (!isAbsolute(localPackageRoot) ||
|
|
1715
|
+
if (!isAbsolute(localPackageRoot) || resolve2(localPackageRoot) !== localPackageRoot) {
|
|
1221
1716
|
throw new Error(
|
|
1222
1717
|
"--local-package-root requires a normalized absolute path."
|
|
1223
1718
|
);
|
|
@@ -1237,7 +1732,7 @@ function parseInstallArgs(args) {
|
|
|
1237
1732
|
if (!localPiRuntimeRoot || localPiRuntimeRoot.startsWith("--")) {
|
|
1238
1733
|
throw new Error("--local-pi-runtime-root requires a value.");
|
|
1239
1734
|
}
|
|
1240
|
-
if (!isAbsolute(localPiRuntimeRoot) ||
|
|
1735
|
+
if (!isAbsolute(localPiRuntimeRoot) || resolve2(localPiRuntimeRoot) !== localPiRuntimeRoot) {
|
|
1241
1736
|
throw new Error(
|
|
1242
1737
|
"--local-pi-runtime-root requires a normalized absolute path."
|
|
1243
1738
|
);
|