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.
Files changed (42) hide show
  1. package/dist/agents/prompt-dialects.d.ts +2 -1
  2. package/dist/{chunk-QOIZYJ5C.js → chunk-ARUBELNT.js} +18 -7
  3. package/dist/{chunk-W5TSNGEE.js → chunk-FBLIVTKM.js} +4036 -3028
  4. package/dist/{chunk-P3GDM5RH.js → chunk-IJBKUC5J.js} +1 -1
  5. package/dist/{chunk-3MHJGWS4.js → chunk-V5GDCWFR.js} +1 -1
  6. package/dist/cli/codex-config-io.d.ts +34 -2
  7. package/dist/cli/codex-hook-consent.d.ts +5 -0
  8. package/dist/cli/codex-hook-trust.d.ts +23 -0
  9. package/dist/cli/codex-install.d.ts +21 -6
  10. package/dist/cli/codex-lifecycle.d.ts +13 -0
  11. package/dist/cli/codex-plugin-install.d.ts +21 -0
  12. package/dist/cli/index.js +539 -44
  13. package/dist/cli/managed-state-io.d.ts +19 -0
  14. package/dist/cli/operations/codex.d.ts +2 -0
  15. package/dist/cli/operations/types.d.ts +1 -1
  16. package/dist/cli/safe-atomic-write.d.ts +2 -0
  17. package/dist/cli/tui/index.js +56 -5
  18. package/dist/cli/tui/operations.d.ts +3 -1
  19. package/dist/cli/types.d.ts +4 -1
  20. package/dist/harness/adapters/codex-legacy-agents.d.ts +31 -0
  21. package/dist/harness/adapters/codex-surfaces.d.ts +1 -1
  22. package/dist/harness/adapters/codex.d.ts +3 -1
  23. package/dist/harness/codex-hook-script.d.ts +2 -0
  24. package/dist/harness/core/agent-pack.d.ts +2 -0
  25. package/dist/harness/writers/codex-plugin-package.d.ts +7 -0
  26. package/dist/index.js +2 -2
  27. package/dist/pi.js +2 -2
  28. package/package.json +1 -1
  29. package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +1 -1
  30. package/plugin/.claude-plugin/plugin.json +1 -1
  31. package/plugin/.codex-plugin/.thoth-agents-plugin-assets.json +48 -0
  32. package/plugin/.codex-plugin/plugin.json +3 -2
  33. package/plugin/agents/orchestrator.md +1 -1
  34. package/plugin/codex-hooks/context.cjs +22 -0
  35. package/plugin/codex-hooks/hooks.json +32 -0
  36. package/plugin/codex-hooks/payloads/designer.md +72 -0
  37. package/plugin/codex-hooks/payloads/explorer.md +76 -0
  38. package/plugin/codex-hooks/payloads/librarian.md +76 -0
  39. package/plugin/codex-hooks/payloads/oracle.md +83 -0
  40. package/plugin/codex-hooks/payloads/root.md +105 -0
  41. package/plugin/codex-hooks/payloads/worker.md +72 -0
  42. 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-3MHJGWS4.js";
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-W5TSNGEE.js";
79
+ } from "../chunk-FBLIVTKM.js";
63
80
  import "../chunk-62L3ORXN.js";
64
81
  import {
65
82
  piAdapter
66
- } from "../chunk-P3GDM5RH.js";
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-QOIZYJ5C.js";
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}: ${join(skill.destinationPath, "SKILL.md")}`
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 architectural-grilling.
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, services.operationContext()),
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) || resolve(localPackageRoot) !== 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) || resolve(localPiRuntimeRoot) !== 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
  );