Skip to content

Introduce first-class Codex runtime integration - #2546

Merged
namastex888 merged 6 commits into
devfrom
nmstx/codex-first-class
Jul 10, 2026
Merged

namastex888 merged 6 commits into
devfrom
nmstx/codex-first-class

Conversation

@namastex888

@namastex888 namastex888 commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds first-class Codex support across plugins, hooks, CLI flows, and runtime configuration.
  • Updates shared skills, docs, and release packaging so Genie can ship and validate Codex-aware workflows consistently.
  • Includes test coverage and validation updates for config loading, hooks dispatch, install/setup flows, and runtime integrations.

Testing

  • Not run (not requested).

Summary by CodeRabbit

  • New Features

    • Added Codex integration with plugin, MCP, agent roles, permission handling, and session continuity.
    • Added runtime selection for installation, including Codex, Claude, both, automatic detection, or none.
    • genie launch can now select the configured runtime and safely handle prompts.
    • Added Codex integration diagnostics and repair support.
  • Bug Fixes

    • Explicit integration installation failures now stop with an error.
    • Removed obsolete Codex telemetry configuration while preserving backups and unrelated settings.
  • Documentation

    • Updated workflow guidance for native delegation across supported runtimes.
  • Chores

    • Updated Genie and plugin versions to 5.260710.9.

@namastex888
namastex888 changed the base branch from main to dev July 10, 2026 19:14
@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fddaf385-7c72-49bf-bfe4-9397092db734

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Genie adds packaged Codex support alongside Claude, including runtime plugin installation, lifecycle hooks, provider-aware Omni execution, persisted Codex sessions, MCP fallback handling, updated launch behavior, diagnostics, and native delegation guidance.

Changes

Codex runtime integration

Layer / File(s) Summary
Runtime manifests, hooks, and packaging
.agents/..., .claude-plugin/..., plugins/genie/..., scripts/*, .github/workflows/*
Adds Codex plugin metadata, managed agent roles, runtime hooks, marketplace manifests, tarball payloads, and synchronized version updates.
Runtime integration lifecycle
src/lib/runtime-integrations.ts, src/genie-commands/{install,setup,uninstall,doctor}.ts, src/term-commands/init.ts
Installs, repairs, diagnoses, and removes Codex/Claude integrations, including agent files, marketplace entries, plugin state, MCP fallback configuration, and obsolete exporter migration.
Codex hook dispatch
src/hooks/*, plugins/genie/hooks/*
Adds Codex permission-request adaptation, runtime selection, ask/allow/deny handling, and expanded tool matchers.
Provider-aware execution
src/lib/omni-runner.ts, src/lib/v5/global-db.ts, src/types/genie-config.ts, src/lib/omni-config.ts
Adds Claude/Codex route selection, Codex JSONL execution and resume support, persisted agent sessions, and runtime configuration defaults.
Launch and validation
src/term-commands/launch.ts, src/*/*.test.ts
Uses configured or automatically detected agents, emits quoted Claude or interactive Codex commands, and expands related tests.
Native delegation guidance
skills/*, references/native-surfaces.md, src/__tests__/claude-md-drift.test.ts
Standardizes native subagent dispatch, follow-up messaging, role names, lifecycle contracts, and canonical guidance checks.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: first-class Codex runtime integration across the CLI, plugins, hooks, and config.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nmstx/codex-first-class

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces first-class Codex integration alongside the existing Claude Code support. It establishes a canonical, runtime-neutral contributor contract in AGENTS.md, updates the CLI commands (install, setup, uninstall, init, launch, doctor) to manage Codex plugins, role agents, and MCP configurations, and transitions shared skills to use runtime-neutral delegation terms. Additionally, it removes the obsolete loopback OTel exporter from Codex configurations, adds provider-neutral execution and thread persistence in the Omni runner, and adapts lifecycle hooks for Codex. The review feedback highlights a few important issues: a potential malformed TOML generation in runtime-integrations.ts due to unhandled indentation in regex replacement, a potential crash in genie doctor's MCP probe if non-JSON output is encountered, and the blocking of the single-threaded event loop in the Omni runner daemon due to synchronous file I/O in runCodexSession.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +100 to +102
const replacement = /(^|\n)enabled\s*=\s*(true|false)/.test(section)
? section.replace(/(^|\n)enabled\s*=\s*(true|false)/, `$1enabled = ${enabled}`)
: section.replace(header, `${header}\nenabled = ${enabled}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The regular expression /(^|\n)enabled\s*=\s*(true|false)/ does not account for any leading whitespace (such as spaces or tabs used for indentation) before the enabled key. If the user's config.toml has indented keys under the [plugins."genie@automagik"] section (e.g., enabled = true), the test will fail, and a duplicate enabled = false key will be appended directly under the header, resulting in a malformed TOML file that Codex will fail to parse.

To fix this, update the regex and replacement to match and preserve optional leading indentation.

Suggested change
const replacement = /(^|\n)enabled\s*=\s*(true|false)/.test(section)
? section.replace(/(^|\n)enabled\s*=\s*(true|false)/, `$1enabled = ${enabled}`)
: section.replace(header, `${header}\nenabled = ${enabled}`);
const replacement = /(^|\n)[ \t]*enabled\s*=\s*(true|false)/.test(section)
? section.replace(/(^|\n)([ \t]*)enabled\s*=\s*(true|false)/, "$1$2enabled = " + enabled)
: section.replace(header, header + "\nenabled = " + enabled);

Comment on lines +265 to +268
const replies = stdout
.trim()
.split('\n')
.map((line) => JSON.parse(line) as { id?: number; result?: unknown; error?: unknown });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the genie binary prints any non-JSON output (such as startup logs, deprecation warnings, or update notifications) to stdout, JSON.parse(line) will throw a SyntaxError, causing probeGenieMcp to fail and report a false-positive warning in genie doctor.

To make this check robust against process noise, wrap JSON.parse in a try-catch block and filter out any non-JSON lines, similar to how extractCodexJsonlReply handles JSONL streams.

    const replies = stdout
      .trim()
      .split("\n")
      .map((line) => {
        try {
          return JSON.parse(line) as { id?: number; result?: unknown; error?: unknown };
        } catch {
          return null;
        }
      })
      .filter((reply): reply is NonNullable<typeof reply> => reply !== null);

Comment thread src/lib/omni-runner.ts
opts: AgentExecutionOpts,
rawSpawn: RawCodexSpawn,
): Promise<AgentExecutionResult> {
const persona = opts.personaFile && existsSync(opts.personaFile) ? readFileSync(opts.personaFile, 'utf8') : '';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using synchronous file I/O (readFileSync) inside the asynchronous runCodexSession function blocks the single-threaded event loop of the Omni runner. Since the Omni runner is a resident daemon process handling incoming WhatsApp messages and approvals, blocking the event loop can introduce latency or cause timeouts for concurrent requests.

Consider using asynchronous file reading instead, such as Bun.file(path).text() (since the project runs on Bun) or fs.promises.readFile.

Suggested change
const persona = opts.personaFile && existsSync(opts.personaFile) ? readFileSync(opts.personaFile, 'utf8') : '';
const persona = opts.personaFile && existsSync(opts.personaFile) ? await Bun.file(opts.personaFile).text() : "";

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 878b5a28ad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +32 to +34
const installed = dirname(process.execPath);
if (existsSync(join(installed, 'plugins', 'genie'))) return installed;
return resolve(import.meta.dir, '..', '..');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve integrations from the normalized bundle root

On a fresh tarball install, install.sh extracts payloads under ~/.genie/bin and installCommand runs normalizeAuxLayout() before installRuntimeIntegrations(), which moves bin/plugins to ~/.genie/plugins. After that move this resolver only checks ~/.genie/bin/plugins/genie and then falls back to the source-tree-relative path, so Codex integration cannot find plugins/genie/codex-agents; explicit --integrations codex fails and auto install silently skips the new integration. Check the normalized ~/.genie root, or pass it as the bundle root, before falling back.

Useful? React with 👍 / 👎.

"repository": "https://github.com/automagik-dev/genie",
"license": "MIT",
"keywords": ["workflow", "orchestration", "codex", "skills", "agents"],
"skills": "./skills/",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bundle the skills directory inside the Codex plugin

The Codex manifest declares skills as ./skills/, and Codex plugin paths are resolved relative to the plugin root (OpenAI docs). The release payload and repo currently keep the shared skills as a sibling top-level skills/ tree, while plugins/genie/skills does not exist, so an installed Genie Codex plugin will not expose $wish, $work, $review, etc. Include/sync the shared skills under the plugin root (or point the manifest at a real bundled plugin-root path).

Useful? React with 👍 / 👎.

let count = 0;
for (const name of readdirSync(source)) {
if (!name.startsWith('genie-') || !name.endsWith('.toml')) continue;
copyFileSync(join(source, name), join(target, name));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve existing unmanaged Codex agent files

When genie setup --codex runs on a host that already has a user-created ~/.codex/agents/genie-*.toml, this unconditional copy overwrites that file with a Genie-managed marker; genie uninstall can then remove it because it now looks marker-owned. Please mirror the uninstall/agent-sync ownership policy here by only replacing absent or already marker-owned files, or by backing up unmanaged collisions first.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.genie/agents/metrics-updater/runs.jsonl:
- Line 6: Align the new metrics record with the established schema: change
errors back to an array and restore tools_used as the string-array field,
removing or renaming tools_generated as appropriate. Update the
record-generation logic associated with the metrics updater so all emitted
records consistently use these existing field types and names.

In `@skills/docs/SKILL.md`:
- Line 36: In the dispatch instruction, correct the article before “native
delegation surface call” from “an” to “a,” preserving the rest of the sentence.

In `@skills/genie/SKILL.md`:
- Line 53: The shared v5 runtime guidance in the referenced section hardcodes
client names, violating runtime-neutral skill conventions. Update the wording
around the v5 execution model and runtime mapping to use generic terms such as
“native subagents” and “native runtime,” while retaining client-specific
mappings only in native-surfaces.md.

In `@src/genie-commands/doctor.ts`:
- Line 244: Replace the hardcoded expected count in the doctor status check with
a dynamic value, preferably by counting TOML files in the staged
plugins/genie/codex-agents/ directory or reusing a shared agent-count constant.
Update the logic around the status calculation to compare count against that
derived value instead of 7.
- Around line 237-248: codexAgentCheck should handle filesystem errors from
readdirSync instead of allowing doctorCommand to crash. Wrap the agents
directory existence check and directory read in a try-catch, returning a warn
CheckResult with an appropriate failure detail and repair suggestion when an
exception occurs; preserve the current count-based result on success.

In `@src/genie-commands/install.test.ts`:
- Around line 75-98: The test for --skip-integrations makes an unverified first
installCommand call whose selection is overwritten. Add an assertion immediately
after the first call confirming selection is 'auto', then retain the second call
and final assertion confirming 'none', or remove the first call if the default
behavior is not intended to be tested.
- Around line 100-119: Add a test in the install command test suite covering an
unsupported integrations selection, casting the invalid string to
IntegrationSelection. Invoke installCommand with that value and assert it throws
with the exact “Invalid --integrations value” message, including the command’s
expected error behavior.

In `@src/genie-commands/setup.ts`:
- Around line 189-199: repairCodexIntegration currently removes the fallback
relative to process.cwd(), which can differ from the git root used by init.ts.
Resolve the repository root with resolveGitRoot(process.cwd()) and pass
join(root, '.codex', 'config.toml') to removeCodexMcpFallback, handling the
resolved root consistently with the existing integration setup.

In `@src/genie-commands/uninstall.ts`:
- Around line 233-234: Wrap the removeRuntimeIntegrations call in
performUninstall with tryRemoveStep, matching the other uninstall removal steps
so it reports progress and isolates errors while preserving the existing
removeMarketplace argument.

In `@src/hooks/codex-adapter.test.ts`:
- Around line 8-36: Add tests in the “Codex hook adapter” suite covering
non-JSON input to adaptCodexPreToolUseOutput (preserving it unchanged), allow
and deny PreToolUse outputs (also unchanged), undefined input to
codexPermissionDecision (returning an empty string), and
dispatchCodexPermissionRequest approvals producing allow and deny results with
formatted Codex PermissionRequest JSON rather than an empty string.

In `@src/lib/codex-config.test.ts`:
- Around line 1-34: Add an error-path test to the migrateDeadGenieOtel test
suite that passes a directory path as configPath, triggering readFileSync
failure and the catch branch. Assert the returned result has status 'error' and
verify the error response fields as appropriate, while ensuring the temporary
directory is cleaned up.

In `@src/lib/runtime-integrations.test.ts`:
- Around line 1-68: Add afterEach-based cleanup for all temporary directories
created in the test suite. Define a temp-directory collection and remove each
registered path recursively after tests, then update the mkdtempSync calls in
“restores an explicit Codex disabled state without touching other plugins” and
“fresh Codex home installs agents, migrates dead OTel, and runs idempotent
marketplace/plugin commands” to register their returned paths.

In `@src/lib/runtime-integrations.ts`:
- Around line 175-195: Make removeRuntimeIntegrations testable by adding an
injectable CommandRunner parameter, matching installRuntimeIntegrations. Default
it to defaultRunner, then replace every direct defaultRunner call for plugin and
marketplace removal with the injected runner while preserving existing behavior
and arguments.
- Around line 49-55: Update codexPluginState so enabled is undefined when
plugin.enabled is absent, and only map it to a boolean when the value is
actually boolean; retain version handling and installed detection. Apply the
same enabled mapping in claudePluginState so both runtime state parsers preserve
missing enabled values and prevent refresh logic from writing enabled = false.

In `@src/term-commands/init.test.ts`:
- Around line 240-263: The tests cover direct fallback behavior but not the
end-to-end Codex registration path. Add an integration test around
registerMcpConfigs that mocks Bun.which('codex') as available and
codexHasGeniePlugin() as absent, then assert the init result includes the Codex
fallback entry.

In `@src/term-commands/init.ts`:
- Around line 244-260: Update mergeCodexMcpFallback to serialize entry.args
instead of the hardcoded ["mcp"], preserving the expected TOML array format.
Handle an orphaned begin marker by replacing or repairing the existing
incomplete fallback block rather than appending another block; ensure the
resulting configuration contains exactly one complete BEGIN/END marker pair.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bc781f73-143e-4a96-bee7-d0b2cb5797e5

📥 Commits

Reviewing files that changed from the base of the PR and between 8c18ca5 and 878b5a2.

⛔ Files ignored due to path filters (7)
  • AGENTS.md is excluded by !*.md
  • CLAUDE.md is excluded by !*.md
  • README.md is excluded by !*.md
  • plugins/genie/scripts/session-context.cjs is excluded by !plugins/genie/scripts/**
  • plugins/genie/scripts/src/session-context.ts is excluded by !plugins/genie/scripts/**
  • plugins/genie/scripts/src/validate-completion.ts is excluded by !plugins/genie/scripts/**
  • plugins/genie/scripts/validate-completion.cjs is excluded by !plugins/genie/scripts/**
📒 Files selected for processing (63)
  • .agents/plugins/marketplace.json
  • .claude-plugin/marketplace.json
  • .genie/agents/metrics-updater/runs.jsonl
  • .genie/agents/metrics-updater/state.json
  • .github/workflows/build-tarballs.yml
  • install.sh
  • package.json
  • plugins/genie/.claude-plugin/plugin.json
  • plugins/genie/.codex-plugin/plugin.json
  • plugins/genie/codex-agents/genie-engineer-complex.toml
  • plugins/genie/codex-agents/genie-engineer-standard.toml
  • plugins/genie/codex-agents/genie-engineer-trivial.toml
  • plugins/genie/codex-agents/genie-final-gate.toml
  • plugins/genie/codex-agents/genie-fixer.toml
  • plugins/genie/codex-agents/genie-reviewer.toml
  • plugins/genie/codex-agents/genie-scout.toml
  • plugins/genie/hooks/claude-hooks.json
  • plugins/genie/hooks/hooks.json
  • plugins/genie/package.json
  • plugins/genie/references/native-surfaces.md
  • scripts/build-binary.sh
  • scripts/build.js
  • skills/brainstorm/SKILL.md
  • skills/council/SKILL.md
  • skills/docs/SKILL.md
  • skills/dream/SKILL.md
  • skills/fix/SKILL.md
  • skills/genie/SKILL.md
  • skills/pm/SKILL.md
  • skills/report/SKILL.md
  • skills/review/SKILL.md
  • skills/trace/SKILL.md
  • skills/wish/SKILL.md
  • skills/wizard/SKILL.md
  • skills/work/SKILL.md
  • src/__tests__/claude-md-drift.test.ts
  • src/genie-commands/doctor.ts
  • src/genie-commands/install.test.ts
  • src/genie-commands/install.ts
  • src/genie-commands/setup.ts
  • src/genie-commands/uninstall.ts
  • src/genie.ts
  • src/hooks/codex-adapter.test.ts
  • src/hooks/codex-adapter.ts
  • src/hooks/dispatch-command.ts
  • src/hooks/index.ts
  • src/lib/codex-config.test.ts
  • src/lib/codex-config.ts
  • src/lib/genie-config.ts
  • src/lib/omni-config.ts
  • src/lib/omni-registration.ts
  • src/lib/omni-runner.test.ts
  • src/lib/omni-runner.ts
  • src/lib/runtime-integrations.test.ts
  • src/lib/runtime-integrations.ts
  • src/lib/v5/global-db.test.ts
  • src/lib/v5/global-db.ts
  • src/term-commands/init.test.ts
  • src/term-commands/init.ts
  • src/term-commands/launch.test.ts
  • src/term-commands/launch.ts
  • src/term-commands/omni.test.ts
  • src/types/genie-config.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 16

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.genie/agents/metrics-updater/runs.jsonl:
- Line 6: Align the new metrics record with the established schema: change
errors back to an array and restore tools_used as the string-array field,
removing or renaming tools_generated as appropriate. Update the
record-generation logic associated with the metrics updater so all emitted
records consistently use these existing field types and names.

In `@skills/docs/SKILL.md`:
- Line 36: In the dispatch instruction, correct the article before “native
delegation surface call” from “an” to “a,” preserving the rest of the sentence.

In `@skills/genie/SKILL.md`:
- Line 53: The shared v5 runtime guidance in the referenced section hardcodes
client names, violating runtime-neutral skill conventions. Update the wording
around the v5 execution model and runtime mapping to use generic terms such as
“native subagents” and “native runtime,” while retaining client-specific
mappings only in native-surfaces.md.

In `@src/genie-commands/doctor.ts`:
- Line 244: Replace the hardcoded expected count in the doctor status check with
a dynamic value, preferably by counting TOML files in the staged
plugins/genie/codex-agents/ directory or reusing a shared agent-count constant.
Update the logic around the status calculation to compare count against that
derived value instead of 7.
- Around line 237-248: codexAgentCheck should handle filesystem errors from
readdirSync instead of allowing doctorCommand to crash. Wrap the agents
directory existence check and directory read in a try-catch, returning a warn
CheckResult with an appropriate failure detail and repair suggestion when an
exception occurs; preserve the current count-based result on success.

In `@src/genie-commands/install.test.ts`:
- Around line 75-98: The test for --skip-integrations makes an unverified first
installCommand call whose selection is overwritten. Add an assertion immediately
after the first call confirming selection is 'auto', then retain the second call
and final assertion confirming 'none', or remove the first call if the default
behavior is not intended to be tested.
- Around line 100-119: Add a test in the install command test suite covering an
unsupported integrations selection, casting the invalid string to
IntegrationSelection. Invoke installCommand with that value and assert it throws
with the exact “Invalid --integrations value” message, including the command’s
expected error behavior.

In `@src/genie-commands/setup.ts`:
- Around line 189-199: repairCodexIntegration currently removes the fallback
relative to process.cwd(), which can differ from the git root used by init.ts.
Resolve the repository root with resolveGitRoot(process.cwd()) and pass
join(root, '.codex', 'config.toml') to removeCodexMcpFallback, handling the
resolved root consistently with the existing integration setup.

In `@src/genie-commands/uninstall.ts`:
- Around line 233-234: Wrap the removeRuntimeIntegrations call in
performUninstall with tryRemoveStep, matching the other uninstall removal steps
so it reports progress and isolates errors while preserving the existing
removeMarketplace argument.

In `@src/hooks/codex-adapter.test.ts`:
- Around line 8-36: Add tests in the “Codex hook adapter” suite covering
non-JSON input to adaptCodexPreToolUseOutput (preserving it unchanged), allow
and deny PreToolUse outputs (also unchanged), undefined input to
codexPermissionDecision (returning an empty string), and
dispatchCodexPermissionRequest approvals producing allow and deny results with
formatted Codex PermissionRequest JSON rather than an empty string.

In `@src/lib/codex-config.test.ts`:
- Around line 1-34: Add an error-path test to the migrateDeadGenieOtel test
suite that passes a directory path as configPath, triggering readFileSync
failure and the catch branch. Assert the returned result has status 'error' and
verify the error response fields as appropriate, while ensuring the temporary
directory is cleaned up.

In `@src/lib/runtime-integrations.test.ts`:
- Around line 1-68: Add afterEach-based cleanup for all temporary directories
created in the test suite. Define a temp-directory collection and remove each
registered path recursively after tests, then update the mkdtempSync calls in
“restores an explicit Codex disabled state without touching other plugins” and
“fresh Codex home installs agents, migrates dead OTel, and runs idempotent
marketplace/plugin commands” to register their returned paths.

In `@src/lib/runtime-integrations.ts`:
- Around line 175-195: Make removeRuntimeIntegrations testable by adding an
injectable CommandRunner parameter, matching installRuntimeIntegrations. Default
it to defaultRunner, then replace every direct defaultRunner call for plugin and
marketplace removal with the injected runner while preserving existing behavior
and arguments.
- Around line 49-55: Update codexPluginState so enabled is undefined when
plugin.enabled is absent, and only map it to a boolean when the value is
actually boolean; retain version handling and installed detection. Apply the
same enabled mapping in claudePluginState so both runtime state parsers preserve
missing enabled values and prevent refresh logic from writing enabled = false.

In `@src/term-commands/init.test.ts`:
- Around line 240-263: The tests cover direct fallback behavior but not the
end-to-end Codex registration path. Add an integration test around
registerMcpConfigs that mocks Bun.which('codex') as available and
codexHasGeniePlugin() as absent, then assert the init result includes the Codex
fallback entry.

In `@src/term-commands/init.ts`:
- Around line 244-260: Update mergeCodexMcpFallback to serialize entry.args
instead of the hardcoded ["mcp"], preserving the expected TOML array format.
Handle an orphaned begin marker by replacing or repairing the existing
incomplete fallback block rather than appending another block; ensure the
resulting configuration contains exactly one complete BEGIN/END marker pair.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bc781f73-143e-4a96-bee7-d0b2cb5797e5

📥 Commits

Reviewing files that changed from the base of the PR and between 8c18ca5 and 878b5a2.

⛔ Files ignored due to path filters (7)
  • AGENTS.md is excluded by !*.md
  • CLAUDE.md is excluded by !*.md
  • README.md is excluded by !*.md
  • plugins/genie/scripts/session-context.cjs is excluded by !plugins/genie/scripts/**
  • plugins/genie/scripts/src/session-context.ts is excluded by !plugins/genie/scripts/**
  • plugins/genie/scripts/src/validate-completion.ts is excluded by !plugins/genie/scripts/**
  • plugins/genie/scripts/validate-completion.cjs is excluded by !plugins/genie/scripts/**
📒 Files selected for processing (63)
  • .agents/plugins/marketplace.json
  • .claude-plugin/marketplace.json
  • .genie/agents/metrics-updater/runs.jsonl
  • .genie/agents/metrics-updater/state.json
  • .github/workflows/build-tarballs.yml
  • install.sh
  • package.json
  • plugins/genie/.claude-plugin/plugin.json
  • plugins/genie/.codex-plugin/plugin.json
  • plugins/genie/codex-agents/genie-engineer-complex.toml
  • plugins/genie/codex-agents/genie-engineer-standard.toml
  • plugins/genie/codex-agents/genie-engineer-trivial.toml
  • plugins/genie/codex-agents/genie-final-gate.toml
  • plugins/genie/codex-agents/genie-fixer.toml
  • plugins/genie/codex-agents/genie-reviewer.toml
  • plugins/genie/codex-agents/genie-scout.toml
  • plugins/genie/hooks/claude-hooks.json
  • plugins/genie/hooks/hooks.json
  • plugins/genie/package.json
  • plugins/genie/references/native-surfaces.md
  • scripts/build-binary.sh
  • scripts/build.js
  • skills/brainstorm/SKILL.md
  • skills/council/SKILL.md
  • skills/docs/SKILL.md
  • skills/dream/SKILL.md
  • skills/fix/SKILL.md
  • skills/genie/SKILL.md
  • skills/pm/SKILL.md
  • skills/report/SKILL.md
  • skills/review/SKILL.md
  • skills/trace/SKILL.md
  • skills/wish/SKILL.md
  • skills/wizard/SKILL.md
  • skills/work/SKILL.md
  • src/__tests__/claude-md-drift.test.ts
  • src/genie-commands/doctor.ts
  • src/genie-commands/install.test.ts
  • src/genie-commands/install.ts
  • src/genie-commands/setup.ts
  • src/genie-commands/uninstall.ts
  • src/genie.ts
  • src/hooks/codex-adapter.test.ts
  • src/hooks/codex-adapter.ts
  • src/hooks/dispatch-command.ts
  • src/hooks/index.ts
  • src/lib/codex-config.test.ts
  • src/lib/codex-config.ts
  • src/lib/genie-config.ts
  • src/lib/omni-config.ts
  • src/lib/omni-registration.ts
  • src/lib/omni-runner.test.ts
  • src/lib/omni-runner.ts
  • src/lib/runtime-integrations.test.ts
  • src/lib/runtime-integrations.ts
  • src/lib/v5/global-db.test.ts
  • src/lib/v5/global-db.ts
  • src/term-commands/init.test.ts
  • src/term-commands/init.ts
  • src/term-commands/launch.test.ts
  • src/term-commands/launch.ts
  • src/term-commands/omni.test.ts
  • src/types/genie-config.ts
🛑 Comments failed to post (16)
.genie/agents/metrics-updater/runs.jsonl (1)

6-6: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Schema inconsistency in new metrics record.

The new record changes errors from an array ([]) to a number (0) and replaces tools_used (string array) with tools_generated (number). Any downstream parser expecting errors to be an array or tools_used to exist would break on this record.

If this is an intentional schema migration, ensure all consumers are updated. Otherwise, align with the existing schema:

🔧 Proposed alignment with existing schema
-{"timestamp":"2026-07-10T07:25:00Z","duration_ms":12400,"api_calls":2,"tools_generated":0,"errors":0,"metrics":{"releases_24h":9,"prs_merged_7d":39,"avg_merge_time_hours":2.9,"ship_rate_pct":23}}
+{"timestamp":"2026-07-10T07:25:00Z","duration_ms":12400,"api_calls":2,"tools_used":[],"errors":[],"metrics":{"releases_24h":9,"prs_merged_7d":39,"avg_merge_time_hours":2.9,"ship_rate_pct":23}}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

{"timestamp":"2026-07-10T07:25:00Z","duration_ms":12400,"api_calls":2,"tools_used":[],"errors":[],"metrics":{"releases_24h":9,"prs_merged_7d":39,"avg_merge_time_hours":2.9,"ship_rate_pct":23}}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.genie/agents/metrics-updater/runs.jsonl at line 6, Align the new metrics
record with the established schema: change errors back to an array and restore
tools_used as the string-array field, removing or renaming tools_generated as
appropriate. Update the record-generation logic associated with the metrics
updater so all emitted records consistently use these existing field types and
names.
skills/docs/SKILL.md (1)

36-36: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the article in the dispatch instruction.

Change “an native delegation surface call” to “a native delegation surface call” so the instruction remains clear and polished.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/docs/SKILL.md` at line 36, In the dispatch instruction, correct the
article before “native delegation surface call” from “an” to “a,” preserving the
rest of the sentence.
skills/genie/SKILL.md (1)

53-53: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep shared skill guidance runtime-neutral.

This shared skill now names Claude and Codex directly. Replace those names with generic “native subagents” or “native runtime” wording; keep client-specific mapping in plugins/genie/references/native-surfaces.md.

As per coding guidelines, shared skills must use roles and native delegation language, never a hardcoded client tool name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/genie/SKILL.md` at line 53, The shared v5 runtime guidance in the
referenced section hardcodes client names, violating runtime-neutral skill
conventions. Update the wording around the v5 execution model and runtime
mapping to use generic terms such as “native subagents” and “native runtime,”
while retaining client-specific mappings only in native-surfaces.md.

Source: Coding guidelines

src/genie-commands/doctor.ts (2)

237-248: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

codexAgentCheck lacks error handling around readdirSync.

readCodexPluginState and probeGenieMcp both wrap external calls in try-catch and return warn results on failure. codexAgentCheck uses existsSync before readdirSync, but if the directory exists and is unreadable (permissions), readdirSync throws and crashes the entire doctorCommand. Add a try-catch for consistency with the other Codex diagnostic helpers.

🛡️ Proposed fix
 function codexAgentCheck(): CheckResult {
   const agentsDir = join(getCodexHome(), 'agents');
-  const count = existsSync(agentsDir)
-    ? readdirSync(agentsDir).filter((name) => /^genie-.+\.toml$/.test(name)).length
-    : 0;
+  let count = 0;
+  try {
+    count = existsSync(agentsDir)
+      ? readdirSync(agentsDir).filter((name) => /^genie-.+\.toml$/.test(name)).length
+      : 0;
+  } catch {
+    // Directory exists but is unreadable
+  }
   return {
     name: 'Codex Genie role agents',
     status: count === 7 ? 'pass' : 'warn',
     detail: `${count}/7 installed`,
     suggestion: count === 7 ? undefined : 'Run `genie setup --codex` to repair marker-owned role agents.',
   };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

function codexAgentCheck(): CheckResult {
  const agentsDir = join(getCodexHome(), 'agents');
  let count = 0;
  try {
    count = existsSync(agentsDir)
      ? readdirSync(agentsDir).filter((name) => /^genie-.+\.toml$/.test(name)).length
      : 0;
  } catch {
    // Directory exists but is unreadable
  }
  return {
    name: 'Codex Genie role agents',
    status: count === 7 ? 'pass' : 'warn',
    detail: `${count}/7 installed`,
    suggestion: count === 7 ? undefined : 'Run `genie setup --codex` to repair marker-owned role agents.',
  };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genie-commands/doctor.ts` around lines 237 - 248, codexAgentCheck should
handle filesystem errors from readdirSync instead of allowing doctorCommand to
crash. Wrap the agents directory existence check and directory read in a
try-catch, returning a warn CheckResult with an appropriate failure detail and
repair suggestion when an exception occurs; preserve the current count-based
result on success.

244-244: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider deriving the expected agent count dynamically instead of hardcoding 7.

If a role agent is added or removed from the bundle, this magic number must be manually updated. Consider counting TOML files in the staged plugins/genie/codex-agents/ directory or extracting a shared constant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genie-commands/doctor.ts` at line 244, Replace the hardcoded expected
count in the doctor status check with a dynamic value, preferably by counting
TOML files in the staged plugins/genie/codex-agents/ directory or reusing a
shared agent-count constant. Update the logic around the status calculation to
compare count against that derived value instead of 7.
src/genie-commands/install.test.ts (2)

75-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

First installCommand call in --skip-integrations test is wasted.

The test calls installCommand twice with the same selection variable, but only asserts after the second call. The first call (with {}) sets selection to 'auto' but this is overwritten and never verified. Either add an assertion after the first call or remove it.

🧪 Suggested fix: add assertion after first call
     installCommand(
       {},
       makeCleanupSpy().runner,
       () => {},
       () => {},
       (options) => {
         selection = options?.selection ?? '';
         return [];
       },
     );
+    expect(selection).toBe('auto');
     installCommand(
       { skipIntegrations: true },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  test('--skip-integrations maps to none', () => {
    let selection = '';
    installCommand(
      {},
      makeCleanupSpy().runner,
      () => {},
      () => {},
      (options) => {
        selection = options?.selection ?? '';
        return [];
      },
    );
    expect(selection).toBe('auto');
    installCommand(
      { skipIntegrations: true },
      makeCleanupSpy().runner,
      () => {},
      () => {},
      (options) => {
        selection = options?.selection ?? '';
        return [];
      },
    );
    expect(selection).toBe('none');
  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genie-commands/install.test.ts` around lines 75 - 98, The test for
--skip-integrations makes an unverified first installCommand call whose
selection is overwritten. Add an assertion immediately after the first call
confirming selection is 'auto', then retain the second call and final assertion
confirming 'none', or remove the first call if the default behavior is not
intended to be tested.

Source: Learnings


100-119: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add test for invalid --integrations value.

install.ts lines 92-93 throw Invalid --integrations value for unsupported selections, but no test covers this boundary. Per learnings, every new CLI surface must test command boundaries including error exit codes and error-message formats.

🧪 Suggested test addition
+  test('invalid integrations value throws', () => {
+    expect(() =>
+      installCommand(
+        { integrations: 'bogus' as unknown as IntegrationSelection },
+        makeCleanupSpy().runner,
+        () => {},
+        () => {},
+        () => [],
+      ),
+    ).toThrow('Invalid --integrations value: bogus');
+  });

Note: IntegrationSelection is a string union, so the cast is needed to simulate an invalid CLI value at runtime.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  test('invalid integrations value throws', () => {
    expect(() =>
      installCommand(
        { integrations: 'bogus' as unknown as IntegrationSelection },
        makeCleanupSpy().runner,
        () => {},
        () => {},
        () => [],
      ),
    ).toThrow('Invalid --integrations value: bogus');
  });

  test('explicit integration failures are fatal while auto failures warn', () => {
    const failing = () => [{ runtime: 'codex' as const, ok: false, detail: 'missing' }];
    expect(() =>
      installCommand(
        {},
        makeCleanupSpy().runner,
        () => {},
        () => {},
        failing,
      ),
    ).not.toThrow();
    expect(() =>
      installCommand(
        { integrations: 'codex' },
        makeCleanupSpy().runner,
        () => {},
        () => {},
        failing,
      ),
    ).toThrow('Requested integration failed');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genie-commands/install.test.ts` around lines 100 - 119, Add a test in the
install command test suite covering an unsupported integrations selection,
casting the invalid string to IntegrationSelection. Invoke installCommand with
that value and assert it throws with the exact “Invalid --integrations value”
message, including the command’s expected error behavior.

Source: Learnings

src/genie-commands/setup.ts (1)

189-199: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Path mismatch: removeCodexMcpFallback targets process.cwd() but init.ts writes to the git root.

init.ts resolves the git root via resolveGitRoot(process.cwd()) and writes the MCP fallback to join(root, '.codex', 'config.toml'). But repairCodexIntegration uses join(process.cwd(), '.codex', 'config.toml'). If the user runs genie setup --codex from a subdirectory, the removal silently fails (returns 'skipped'), leaving stale fallback config in the git root's .codex/config.toml.

🔧 Proposed fix: resolve git root before removing fallback
 function repairCodexIntegration(): boolean {
   const result = installRuntimeIntegrations({ selection: 'codex' })[0];
   if (!result?.ok) {
     console.log(`  \x1b[31m\u2717\x1b[0m ${result?.detail ?? 'Codex integration failed'}`);
     return false;
   }
   console.log(`  \x1b[32m\u2713\x1b[0m ${result.detail}`);
-  removeCodexMcpFallback(join(process.cwd(), '.codex', 'config.toml'));
+  const root = resolveGitRoot(process.cwd()) ?? process.cwd();
+  removeCodexMcpFallback(join(root, '.codex', 'config.toml'));
   if (result.preservedDisabled) console.log('  \x1b[2mExisting disabled plugin state was preserved.\x1b[0m');
   console.log('  \x1b[33m!\x1b[0m Review Genie hooks with /hooks, then start a new Codex task.');
   return true;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

function repairCodexIntegration(): boolean {
  const result = installRuntimeIntegrations({ selection: 'codex' })[0];
  if (!result?.ok) {
    console.log(`  \x1b[31m\u2717\x1b[0m ${result?.detail ?? 'Codex integration failed'}`);
    return false;
  }
  console.log(`  \x1b[32m\u2713\x1b[0m ${result.detail}`);
  const root = resolveGitRoot(process.cwd()) ?? process.cwd();
  removeCodexMcpFallback(join(root, '.codex', 'config.toml'));
  if (result.preservedDisabled) console.log('  \x1b[2mExisting disabled plugin state was preserved.\x1b[0m');
  console.log('  \x1b[33m!\x1b[0m Review Genie hooks with /hooks, then start a new Codex task.');
  return true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genie-commands/setup.ts` around lines 189 - 199, repairCodexIntegration
currently removes the fallback relative to process.cwd(), which can differ from
the git root used by init.ts. Resolve the repository root with
resolveGitRoot(process.cwd()) and pass join(root, '.codex', 'config.toml') to
removeCodexMcpFallback, handling the resolved root consistently with the
existing integration setup.
src/genie-commands/uninstall.ts (1)

233-234: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Wrap removeRuntimeIntegrations in tryRemoveStep for consistency and resilience.

Every other removal step in performUninstall uses tryRemoveStep for progress output and error isolation. Calling removeRuntimeIntegrations directly means no progress is printed and an unexpected throw aborts the uninstall before the genie directory is removed.

♻️ Proposed refactor
-  removeRuntimeIntegrations(removeMarketplace);
+  tryRemoveStep(
+    'Removing runtime integrations...',
+    'Runtime integrations removed',
+    () => removeRuntimeIntegrations(removeMarketplace),
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  tryRemoveStep(
    'Removing runtime integrations...',
    'Runtime integrations removed',
    () => removeRuntimeIntegrations(removeMarketplace),
  );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genie-commands/uninstall.ts` around lines 233 - 234, Wrap the
removeRuntimeIntegrations call in performUninstall with tryRemoveStep, matching
the other uninstall removal steps so it reports progress and isolates errors
while preserving the existing removeMarketplace argument.
src/hooks/codex-adapter.test.ts (1)

8-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add edge case tests for uncovered adapter paths.

The suite covers the happy paths but misses several important branches:

  • adaptCodexPreToolUseOutput with non-JSON output (the catch block at line 16-18)
  • adaptCodexPreToolUseOutput with allow/deny (should preserve output unchanged)
  • codexPermissionDecision with undefined result (should return '')
  • dispatchCodexPermissionRequest with an approval that returns allow or deny (should return formatted Codex PermissionRequest JSON, not '')

These branches are reachable in production and should be tested as part of the specification.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/codex-adapter.test.ts` around lines 8 - 36, Add tests in the “Codex
hook adapter” suite covering non-JSON input to adaptCodexPreToolUseOutput
(preserving it unchanged), allow and deny PreToolUse outputs (also unchanged),
undefined input to codexPermissionDecision (returning an empty string), and
dispatchCodexPermissionRequest approvals producing allow and deny results with
formatted Codex PermissionRequest JSON rather than an empty string.

Source: Coding guidelines

src/lib/codex-config.test.ts (1)

1-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add test coverage for the error path.

The status: 'error' branch in migrateDeadGenieOtel is untested. Passing a directory path as configPath would trigger a readFileSync error and exercise the catch block. This is a quick addition that completes the specification.

🧪 Suggested error-path test
+  test('returns error status when config cannot be read', () => {
+    const root = mkdtempSync(join(tmpdir(), 'genie-codex-config-'));
+    roots.push(root);
+    // Passing a directory as configPath triggers a readFileSync error
+    const result = migrateDeadGenieOtel(root);
+    expect(result.status).toBe('error');
+    expect(result.error).toBeTruthy();
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

import { afterEach, describe, expect, test } from 'bun:test';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DEAD_GENIE_OTEL_EXPORTER, migrateDeadGenieOtel } from './codex-config.js';

const roots: string[] = [];
afterEach(() => {
  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});

describe('migrateDeadGenieOtel', () => {
  test('backs up and removes only the exact obsolete exporter', () => {
    const root = mkdtempSync(join(tmpdir(), 'genie-codex-config-'));
    roots.push(root);
    const path = join(root, 'config.toml');
    const original = `disable_paste_burst = true\n[otel]\n${DEAD_GENIE_OTEL_EXPORTER}\nlog_user_prompt = true\n`;
    writeFileSync(path, original);
    const result = migrateDeadGenieOtel(path, new Date('2026-07-10T00:00:00Z'));
    expect(result.status).toBe('changed');
    expect(readFileSync(result.backupPath!, 'utf8')).toBe(original);
    expect(readFileSync(path, 'utf8')).toBe('disable_paste_burst = true\n[otel]\nlog_user_prompt = true\n');
  });

  test('preserves unrelated exporters and writes no backup', () => {
    const root = mkdtempSync(join(tmpdir(), 'genie-codex-config-'));
    roots.push(root);
    const path = join(root, 'config.toml');
    const content = '[otel]\nexporter = { otlp-http = { endpoint = "http://localhost:4318/v1/traces" } }\n';
    writeFileSync(path, content);
    expect(migrateDeadGenieOtel(path).status).toBe('unchanged');
    expect(readFileSync(path, 'utf8')).toBe(content);
  });

  test('returns error status when config cannot be read', () => {
    const root = mkdtempSync(join(tmpdir(), 'genie-codex-config-'));
    roots.push(root);
    const result = migrateDeadGenieOtel(root);
    expect(result.status).toBe('error');
    expect(result.error).toBeTruthy();
  });
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/codex-config.test.ts` around lines 1 - 34, Add an error-path test to
the migrateDeadGenieOtel test suite that passes a directory path as configPath,
triggering readFileSync failure and the catch branch. Assert the returned result
has status 'error' and verify the error response fields as appropriate, while
ensuring the temporary directory is cleaned up.

Source: Coding guidelines

src/lib/runtime-integrations.test.ts (1)

1-68: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add afterEach cleanup for temporary directories.

The test creates temp directories via mkdtempSync at lines 29 and 39 but never cleans them up. Coding guidelines require temporary directories to use cleanup in afterEach.

🛡️ Proposed fix
 import { describe, expect, test } from 'bun:test';
-import { mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
+import { afterEach, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
 import { tmpdir } from 'node:os';
 import { join } from 'node:path';
 import {
@@ -10,6 +10,11 @@
 } from './runtime-integrations.js';

+const tempDirs: string[] = [];
+
+afterEach(() => {
+  for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
+});
+
 describe('runtime plugin state', () => {
   test('reads Codex and Claude enabled state', () => {

Then update each mkdtempSync call to push the result into tempDirs:

-    const root = mkdtempSync(join(tmpdir(), 'genie-plugin-state-'));
+    const root = mkdtempSync(join(tmpdir(), 'genie-plugin-state-'));
+    tempDirs.push(root);
-    const codexHome = mkdtempSync(join(tmpdir(), 'genie-codex-home-'));
+    const codexHome = mkdtempSync(join(tmpdir(), 'genie-codex-home-'));
+    tempDirs.push(codexHome);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/runtime-integrations.test.ts` around lines 1 - 68, Add
afterEach-based cleanup for all temporary directories created in the test suite.
Define a temp-directory collection and remove each registered path recursively
after tests, then update the mkdtempSync calls in “restores an explicit Codex
disabled state without touching other plugins” and “fresh Codex home installs
agents, migrates dead OTel, and runs idempotent marketplace/plugin commands” to
register their returned paths.

Source: Coding guidelines

src/lib/runtime-integrations.ts (2)

49-55: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant file and inspect the surrounding lines.
git ls-files 'src/lib/runtime-integrations.ts'
wc -l src/lib/runtime-integrations.ts
sed -n '1,220p' src/lib/runtime-integrations.ts

# Find the downstream call sites mentioned in the comment.
rg -n "codexPluginState|claudePluginState|setCodexPluginEnabled|preservedDisabled|enabled" src/lib/runtime-integrations.ts

Repository: automagik-dev/genie

Length of output: 9854


Preserve missing enabled as undefined

codexPluginState turns an absent enabled field into false, so installRuntimeIntegrations treats an installed plugin as explicitly disabled and writes enabled = false back during refresh. Keep enabled undefined unless Codex returns a boolean; apply the same mapping in claudePluginState for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/runtime-integrations.ts` around lines 49 - 55, Update
codexPluginState so enabled is undefined when plugin.enabled is absent, and only
map it to a boolean when the value is actually boolean; retain version handling
and installed detection. Apply the same enabled mapping in claudePluginState so
both runtime state parsers preserve missing enabled values and prevent refresh
logic from writing enabled = false.

175-195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

removeRuntimeIntegrations is untestable — add a runner parameter.

Unlike installRuntimeIntegrations, which accepts an injectable CommandRunner, removeRuntimeIntegrations calls defaultRunner directly. This makes it impossible to unit-test plugin/marketplace removal with mocks, and the test file only covers installation.

♻️ Proposed refactor
-export function removeRuntimeIntegrations(removeMarketplace = false): void {
+export function removeRuntimeIntegrations(removeMarketplace = false, runner: CommandRunner = defaultRunner): void {
   const codexHome = getCodexHome();
   const agentsDir = join(codexHome, 'agents');
   if (existsSync(agentsDir)) {
     for (const name of readdirSync(agentsDir)) {
       if (!/^genie-.+\.toml$/.test(name)) continue;
       const path = join(agentsDir, name);
       try {
         if (readFileSync(path, 'utf8').startsWith('# Managed by Genie.')) unlinkSync(path);
       } catch {
         // Preserve unreadable or user-modified files.
       }
     }
   }
-  if (commandExists('codex')) defaultRunner('codex', ['plugin', 'remove', 'genie@automagik']);
-  if (commandExists('claude')) defaultRunner('claude', ['plugin', 'uninstall', 'genie@automagik']);
+  if (commandExists('codex')) runner('codex', ['plugin', 'remove', 'genie@automagik']);
+  if (commandExists('claude')) runner('claude', ['plugin', 'uninstall', 'genie@automagik']);
   if (removeMarketplace) {
-    if (commandExists('codex')) defaultRunner('codex', ['plugin', 'marketplace', 'remove', 'automagik']);
-    if (commandExists('claude')) defaultRunner('claude', ['plugin', 'marketplace', 'remove', 'automagik']);
+    if (commandExists('codex')) runner('codex', ['plugin', 'marketplace', 'remove', 'automagik']);
+    if (commandExists('claude')) runner('claude', ['plugin', 'marketplace', 'remove', 'automagik']);
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export function removeRuntimeIntegrations(removeMarketplace = false, runner: CommandRunner = defaultRunner): void {
  const codexHome = getCodexHome();
  const agentsDir = join(codexHome, 'agents');
  if (existsSync(agentsDir)) {
    for (const name of readdirSync(agentsDir)) {
      if (!/^genie-.+\.toml$/.test(name)) continue;
      const path = join(agentsDir, name);
      try {
        if (readFileSync(path, 'utf8').startsWith('# Managed by Genie.')) unlinkSync(path);
      } catch {
        // Preserve unreadable or user-modified files.
      }
    }
  }
  if (commandExists('codex')) runner('codex', ['plugin', 'remove', 'genie@automagik']);
  if (commandExists('claude')) runner('claude', ['plugin', 'uninstall', 'genie@automagik']);
  if (removeMarketplace) {
    if (commandExists('codex')) runner('codex', ['plugin', 'marketplace', 'remove', 'automagik']);
    if (commandExists('claude')) runner('claude', ['plugin', 'marketplace', 'remove', 'automagik']);
  }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/runtime-integrations.ts` around lines 175 - 195, Make
removeRuntimeIntegrations testable by adding an injectable CommandRunner
parameter, matching installRuntimeIntegrations. Default it to defaultRunner,
then replace every direct defaultRunner call for plugin and marketplace removal
with the injected runner while preserving existing behavior and arguments.
src/term-commands/init.test.ts (1)

240-263: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good coverage of Codex MCP fallback merge/remove edge cases.

The two tests cover the critical contracts: idempotency (skip when user-owned [mcp_servers.genie] exists without markers) and selectivity (remove only the marker-owned block, preserving [mcp_servers.other]).

One gap: the integration test at lines 223-226 uses slice(0, 2) to avoid environment-dependent Codex entries, so the registerMcpConfigs → mergeCodexMcpFallback path isn't exercised end-to-end when Codex is present. Consider adding a test that mocks Bun.which('codex') and codexHasGeniePlugin() to verify the Codex fallback entry appears in the init result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/term-commands/init.test.ts` around lines 240 - 263, The tests cover
direct fallback behavior but not the end-to-end Codex registration path. Add an
integration test around registerMcpConfigs that mocks Bun.which('codex') as
available and codexHasGeniePlugin() as absent, then assert the init result
includes the Codex fallback entry.

Source: Learnings

src/term-commands/init.ts (1)

244-260: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

mergeCodexMcpFallback hardcodes args and doesn't handle orphaned begin markers.

Two issues in this function:

  1. Line 249: args = ["mcp"] is hardcoded instead of using entry.args. The entry parameter carries an args field that's silently ignored. If the MCP server args ever change, this function would write stale values.

  2. Lines 250-255: If the config file has a # BEGIN GENIE MCP FALLBACK marker but no matching # END GENIE MCP FALLBACK (corrupted or manually edited), start >= 0 but finish < 0, so the else branch appends a new block. The result has two begin markers and one end marker — malformed TOML.

🔧 Proposed fixes
 export function mergeCodexMcpFallback(configPath: string, entry: McpServerEntry): ArtifactAction {
   const raw = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
   const begin = '# BEGIN GENIE MCP FALLBACK';
   const end = '# END GENIE MCP FALLBACK';
   if (raw.includes('[mcp_servers.genie]') && !raw.includes(begin)) return 'skipped';
-  const block = `${begin}\n[mcp_servers.genie]\ncommand = ${JSON.stringify(entry.command)}\nargs = ["mcp"]\n${end}`;
+  const block = `${begin}\n[mcp_servers.genie]\ncommand = ${JSON.stringify(entry.command)}\nargs = ${JSON.stringify(entry.args)}\n${end}`;
   const start = raw.indexOf(begin);
   const finish = start >= 0 ? raw.indexOf(end, start) : -1;
   const updated =
-    start >= 0 && finish >= 0
+    start >= 0 && finish >= 0
       ? `${raw.slice(0, start)}${block}${raw.slice(finish + end.length)}`
-      : `${raw}${raw.length > 0 && !raw.endsWith('\n') ? '\n' : ''}\n${block}\n`;
+    : start >= 0
+      ? `${raw.slice(0, start)}${block}\n`
+      : `${raw}${raw.length > 0 && !raw.endsWith('\n') ? '\n' : ''}\n${block}\n`;
   if (updated === raw) return 'skipped';
   mkdirSync(dirname(configPath), { recursive: true });
-  writeFileSync(configPath, updated);
+  writeFileSync(configPath, updated, 'utf8');
   return raw.length === 0 ? 'created' : 'updated';
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export function mergeCodexMcpFallback(configPath: string, entry: McpServerEntry): ArtifactAction {
  const raw = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
  const begin = '# BEGIN GENIE MCP FALLBACK';
  const end = '# END GENIE MCP FALLBACK';
  if (raw.includes('[mcp_servers.genie]') && !raw.includes(begin)) return 'skipped';
  const block = `${begin}\n[mcp_servers.genie]\ncommand = ${JSON.stringify(entry.command)}\nargs = ${JSON.stringify(entry.args)}\n${end}`;
  const start = raw.indexOf(begin);
  const finish = start >= 0 ? raw.indexOf(end, start) : -1;
  const updated =
    start >= 0 && finish >= 0
      ? `${raw.slice(0, start)}${block}${raw.slice(finish + end.length)}`
      : start >= 0
        ? `${raw.slice(0, start)}${block}\n`
        : `${raw}${raw.length > 0 && !raw.endsWith('\n') ? '\n' : ''}\n${block}\n`;
  if (updated === raw) return 'skipped';
  mkdirSync(dirname(configPath), { recursive: true });
  writeFileSync(configPath, updated, 'utf8');
  return raw.length === 0 ? 'created' : 'updated';
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/term-commands/init.ts` around lines 244 - 260, Update
mergeCodexMcpFallback to serialize entry.args instead of the hardcoded ["mcp"],
preserving the expected TOML array format. Handle an orphaned begin marker by
replacing or repairing the existing incomplete fallback block rather than
appending another block; ensure the resulting configuration contains exactly one
complete BEGIN/END marker pair.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant