Skip to content

Fix omo jsonc opencode config - #6187

Closed
offbyone wants to merge 3 commits into
manaflow-ai:mainfrom
offbyone:fix-omo-jsonc-opencode-config
Closed

offbyone wants to merge 3 commits into
manaflow-ai:mainfrom
offbyone:fix-omo-jsonc-opencode-config

Conversation

@offbyone

@offbyone offbyone commented Jun 15, 2026 •

Copy link
Copy Markdown

Summary

  • What changed? cmux now reads the user's ~/.config/opencode/opencode.json as JSONC instead of strict JSON. Added a single shared helper CMUXCLI.parseOpenCodeConfig(data:sourcePath:) that runs the existing JSONCParser.preprocess (strips // and /* */ comments and trailing commas) before JSONSerialization, and routed both code paths that read opencode.json through it: omoEnsurePlugin (the cmux omo path) and updateOpenCodePluginRegistration (session-plugin registration). The user-facing error message (Failed to parse <path>. Fix the JSON syntax and retry.) is preserved, so genuinely malformed configs still surface it.
  • Why? cmux omo aborted with Failed to parse <path>. Fix the JSON syntax and retry. whenever opencode.json contained JSONC syntax — most commonly a leading editor mode line such as // be in -*- jsonc -*- mode. opencode itself treats opencode.json as JSONC, and cmux already uses JSONCParser.preprocess for its other config reads; opencode.json was the one path still doing a strict parse, so it failed on the very first byte. Both affected entrypoints shared the identical bug, so per the repo's shared-behavior policy they were fixed through one common path.

Testing

  • Automated regression test (two-commit red/green): added cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift (Swift Testing) covering the exact repro (leading // mode line), inline/block comments + trailing commas, plain strict JSON, and a malformed-config case that must still throw. Commit 1 adds the test against the still-strict helper (red); commit 2 makes the helper JSONC-aware (green). The test file is wired into cmux.xcodeproj/project.pbxproj (all four sections) and ./scripts/lint-pbxproj-test-wiring.sh passes.
  • What I verified manually:
    • Reproduced the failure on the released app: cmux omo → Error: Failed to parse /Users/.../.config/opencode/opencode.json. Fix the JSON syntax and retry., and bisected it (via an isolated HOME copy) to the leading // comment — every comment form (//, /* */, line 1 or 2) triggered it; the comment-free file parsed.
    • Could not run the real Xcode test target on my machine (Command Line Tools only — no Xcode.app, GhosttyKit unbuilt — so xcodebuild/cmux-unit is unavailable). Instead I compiled the real Sources/JSONCParser.swift with a standalone swiftc harness using the exact test inputs and confirmed: the strict parse rejects the leading-comment repro (test is genuinely red without the fix), and the JSONC path parses leading comments, inline/block comments, trailing commas, and plain JSON while still throwing on malformed input. CI's tests job is the authoritative compile/run of the test target.

Demo Video

Not applicable — this is a CLI startup/config-parsing fix with no UI surface. Behavior change is covered by the automated regression test and the manual repro above.

  • Video URL or attachment: N/A

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally (repro + logic verified via swiftc harness against the real JSONCParser; the Xcode cmux-unit test target was NOT run locally — no Xcode.app on this machine — relying on CI)
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed (no docs/changelog change; release/changelog is handled via the repo's /release flow — flag if you want a CHANGELOG entry now)
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent) (do this after opening the PR)
  • All code review bot comments are resolved
  • All human review comments are resolved

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

cmux now parses ~/.config/opencode/opencode.json as JSONC and preserves comments/formatting when updating the plugin list, fixing cmux omo and session plugin registration failures and preventing comment stripping or plugin reordering.

  • Bug Fixes
    • Both paths now use a shared CMUXCLI.parseOpenCodeConfig(data:sourcePath:) that runs JSONCParser.preprocess before JSONSerialization.
    • Plugin registration edits the existing JSONC text via JSONCObjectEditor.setRootProperty, keeping comments, trailing commas, and formatting; falls back to pretty JSON only if needed.
    • No-op writes are skipped using CMUXCLI.openCodePluginListsEqual, which now canonicalizes and compares plugin arrays order-insensitively to avoid needless rewrites and reordering.
    • Added OmoOpenCodeConfigJSONCParsingTests covering JSONC parsing, comment-preserving plugin updates, order-insensitive no-op detection, plain JSON, and malformed input.

Written for commit 3b08caf. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced opencode.json handling with JSONC support (comments and trailing commas) and improved plugin registration updates.
  • Bug Fixes

    • Reduced unnecessary rewrites via more reliable no-op detection.
    • Standardized configuration parse failures with clearer “Fix the JSON syntax and retry” guidance.
  • Tests

    • Added unit coverage for JSONC parsing, comment-preserving plugin updates, and no-op detection.
  • Documentation

    • Refined the user-facing configuration parse error message.

`cmux omo` (and `cmux`'s session-plugin registration) abort with
"Failed to parse <path>. Fix the JSON syntax and retry." whenever the
user's ~/.config/opencode/opencode.json contains JSONC syntax — most
commonly a leading editor mode line such as `// be in -*- jsonc -*- mode`.

opencode itself treats opencode.json as JSONC (it tolerates `//` and
`/* */` comments and trailing commas), but cmux decodes it with strict
`JSONSerialization`, which rejects comments and fails on the very first
byte. The repo already ships `JSONCParser.preprocess` and uses it for
other config reads, so opencode.json is the odd one out.

This is the first commit of a two-commit red/green pair (per the repo's
regression test policy): it only adds the failing test plus a shared
`CMUXCLI.parseOpenCodeConfig` seam that both affected call sites
(`omoEnsurePlugin` and `updateOpenCodePluginRegistration`) now route
through. The helper still performs the old strict parse here, so the new
`OmoOpenCodeConfigJSONCParsingTests` fails — proving the test catches the
bug. The next commit makes the helper JSONC-aware to turn it green.
@offbyone

Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 99751e85-10ab-4b8b-87e5-12d6cbf2da4a

📥 Commits

Reviewing files that changed from the base of the PR and between a95c0fe and 3b08caf.

📒 Files selected for processing (2)
  • CLI/cmux.swift
  • cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift

📝 Walkthrough

Walkthrough

CLI/cmux.swift gains parseOpenCodeConfig(data:sourcePath:) that preprocesses config bytes through JSONCParser before JSONSerialization decoding, throwing a uniform CLIError on failure. Sources/JSONCParser.swift adds JSONCObjectEditor.setRootProperty(key:valueJSON:in:) to enable comment-preserving edits of root-level properties. Plugin registration is refactored to use these APIs for logical equality checking and in-place JSONC edits. Two new test suites validate JSONC parsing and plugin registration behavior, with full Xcode project wiring.

Changes

JSONC Config Parsing and Plugin Registration

Layer / File(s) Summary
JSONC parsing APIs
CLI/cmux.swift, Sources/JSONCParser.swift
Adds static func parseOpenCodeConfig(data:sourcePath:) in CLI/cmux.swift that routes bytes through JSONCParser preprocessing and JSONSerialization decoding with consistent CLIError on failure. Adds static func setRootProperty(key:valueJSON:in:) in JSONCObjectEditor to update or insert top-level object properties while preserving formatting and newline normalization.
Config read path migrations
CLI/cmux.swift
User config decoding call site migrates from inline JSONSerialization.jsonObject to Self.parseOpenCodeConfig(data:sourcePath:) for consistent JSONC preprocessing and error handling.
Plugin registration refactoring
CLI/cmux.swift
Adds openCodePluginListsEqual(_:_:) and openCodePluginListValueJSON(_:) helpers for logical plugin-array comparison and JSON serialization. Refactors updateOpenCodePluginRegistration to decode config via parseOpenCodeConfig, compute no-op state using logical equality, and prefer comment-preserving in-place updates via JSONCObjectEditor.setRootProperty when source text is available, falling back to strict JSON reserialization.
JSONC config parsing tests
cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift
OmoOpenCodeConfigJSONCParsingTests suite with four @Test methods validating parseOpenCodeConfig accepts leading // line comments, inline ////* */ block comments with trailing commas, and plain strict JSON, while correctly propagating CLIError for malformed input.
Plugin registration tests
cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift
OmoOpenCodePluginRegistrationTests suite validates that JSONCObjectEditor.setRootProperty preserves surrounding comments and formatting during insert and replace operations of the plugin property, that edited documents still decode with expected model and plugin arrays, and that openCodePluginListsEqual detects logical no-op conditions.
Test file Xcode wiring
cmux.xcodeproj/project.pbxproj
Test file registered via PBXBuildFile, PBXFileReference, group children entry, and PBXSourcesBuildPhase file list to include it in the test bundle compilation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#3960: Both PRs modify CLI/cmux.swift's OMO/OpenCode plugin list handling during opencode.json registration—one adds JSONC-preserving parsing and no-op detection, the other normalizes and deduplicates plugins.
  • manaflow-ai/cmux#4538: Both PRs extend Sources/JSONCParser.swift's JSONCObjectEditor to enable JSONC-preserving updates; this PR uses setRootProperty for plugin registration while the other uses setNestedObjectProperty for terminal command persistence.

Poem

🐇 A parser so sharp, JSONC's no foe!
Comments and commas flow line by line low.
With edits that keep the formatting dear,
The plugin registrations stay crystal and clear.
One helper, one path, all configs aligned—
A tidy solution the rabbit designed! 🥕


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error PR introduces a new user-facing error message with localization key 'cli.hooks.opencode.error.invalidJSON' in CLI/cmux.swift's parseOpenCodeConfig() method, but the key is missing from Resources/Lo... Add 'cli.hooks.opencode.error.invalidJSON' key to Resources/Localizable.xcstrings with translated values for all 19 supported locales; the English defaultValue is "Failed to parse %@. Fix the JSON syntax and retry."
Cmux Source Artifacts ❌ Error .claude/scheduled_tasks.lock is a hidden scratch/lock file (session lock tracking) that violates the artifact control rule; it should not enter source control as it's local tool state. Remove .claude/scheduled_tasks.lock from the diff or add .claude/scheduled_tasks.lock to .gitignore to exclude this local session state from version control.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix omo jsonc opencode config' is vague and uses abbreviated terminology that lacks clarity for readers unfamiliar with the codebase's conventions. Use a clearer title such as 'Support JSONC syntax in opencode.json parsing' or 'Fix opencode.json parsing to accept JSONC comments' to better convey the change.
✅ Passed checks (17 passed)
Check name Status Explanation
Description check ✅ Passed The description is comprehensive and covers all required template sections: Summary (what changed and why), Testing (automated tests and manual verification), Demo Video (N/A noted), and Checklist completed with detailed explanations.
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.
Cmux Swift Actor Isolation ✅ Passed All new production methods are pure utility helpers (parsing, comparison, serialization, text editing) with no actor isolation violations. No data models, service protocols, or shared mutable state...
Cmux Swift Blocking Runtime ✅ Passed The PR adds three new helper methods in CLI/cmux.swift (parseOpenCodeConfig, openCodePluginListsEqual, openCodePluginListValueJSON), one in JSONCParser.swift (setRootProperty), and a test file. Non...
Cmux Expensive Synchronous Load ✅ Passed PR adds JSONC config parsing helpers (parseOpenCodeConfig, openCodePluginListsEqual) used only in CLI startup paths (cmux omo, hook setup), not on interactive/main-actor paths. Operations are text...
Cmux Cache Substitution Correctness ✅ Passed PR reads opencode.json fresh from disk every time via Data(contentsOf:) in both omoEnsurePlugin and updateOpenCodePluginRegistration; no cache substitution occurs, cold/missing files are handled, a...
Cmux No Hacky Sleeps ✅ Passed All PR changes are in Swift files (CLI/cmux.swift, Sources/JSONCParser.swift, cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift, and Xcode project file), which are explicitly out of scope. The rul...
Cmux Algorithmic Complexity ✅ Passed Plugin arrays in opencode.json are explicitly bounded fixed-size collections (typically 1-5 items), not scalable collections. openCodePluginListsEqual's O(n log n) sorting is acceptable for tiny fi...
Cmux Swift Concurrency ✅ Passed PR introduces only synchronous code: JSONC parsing helpers, plugin list comparison, JSON serialization, and synchronous tests. No DispatchQueue, Task, Combine, or completion-handler patterns added.
Cmux Swift @Concurrent ✅ Passed All new functions are synchronous. No async functions lack @concurrent annotation, no @concurrent on synchronous functions, and no CPU/IO-heavy async calls from UI without explicit actor hops.
Cmux Swift File And Package Boundaries ✅ Passed PR adds 103 lines to oversized CLI/cmux.swift (under 250-line threshold) as a focused bug fix with clear extraction path; adds 36 lines to JSONCParser.swift; no mixed responsibilities; comprehensiv...
Cmux Swift Logging ✅ Passed No Swift logging violations detected. The PR adds 4 new helper methods (parseOpenCodeConfig, openCodePluginListsEqual, openCodePluginListValueJSON, setRootProperty) with zero NSLog, debugPrint, or...
Cmux User-Facing Error Privacy ✅ Passed PR uses preserved pre-existing error message "Failed to parse %@. Fix the JSON syntax and retry." with no vendor names, internal details, env vars, credentials, or sensitive data exposed.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI code; only CLI parsing, JSON utility, and test code. Check is not applicable.
Cmux Architecture Rethink ✅ Passed PR introduces small local correctness fixes: extracting shared parseOpenCodeConfig helper (DRY refactor), pure comparison/serialization helpers, and comment-preserving text edits. No timing, lock...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains only configuration parsing, JSON editing, and unit test changes with no NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes cmux omo and the session-plugin registration path aborting with a parse error whenever the user's opencode.json contains JSONC syntax (leading // mode lines, block comments, trailing commas). Both call sites now route through a shared CMUXCLI.parseOpenCodeConfig helper that runs JSONCParser.preprocess before JSONSerialization, and the session-plugin registration path gains a surgical, comment-preserving write via JSONCObjectEditor.setRootProperty plus a logical (order-insensitive) no-op guard so JSONC files are never unnecessarily rewritten.

  • parseOpenCodeConfig centralises JSONC-aware parsing with a single consistent user-facing error message; both the omo shadow-dir path and updateOpenCodePluginRegistration now use it.
  • openCodePluginListsEqual compares plugin arrays by canonicalising each element with JSONSerialization + .sortedKeys and sorting the results, making the comparison order-insensitive so a session plugin already present in a non-tail position is not treated as a change.
  • JSONCObjectEditor.setRootProperty adds the missing top-level-property setter that updateOpenCodePluginRegistration uses for a surgical JSONC edit; strict JSON re-serialisation is kept as a fallback when the editor cannot parse the source structure.

Confidence Score: 5/5

Safe to merge. The fix is scoped to two config-read paths, the surgical JSONC write preserves user comments, and all three issues from earlier review rounds are resolved in this HEAD.

Both affected call sites now parse opencode.json through the shared JSONC-aware helper. The updateOpenCodePluginRegistration path uses a surgical in-place text edit so user comments survive, and falls back to full re-serialisation only when the editor cannot parse the source structure (a narrow edge case that matches the pre-PR behaviour). The order-insensitive no-op guard correctly handles the case where the session plugin already exists in a non-tail position, preventing spurious rewrites. No correctness gaps remain in the changed paths.

No files require special attention.

Important Files Changed

Filename Overview
CLI/cmux.swift Three new static helpers address JSONC parsing, order-insensitive no-op detection, and comment-preserving writes; updateOpenCodePluginRegistration now uses the surgical JSONC edit path with a strict JSON fallback. Logic is sound and the three issues raised in earlier review rounds are all resolved in this HEAD.
Sources/JSONCParser.swift Adds setRootProperty to JSONCObjectEditor; mirrors the existing setNestedObjectProperty shape, re-uses all the same private helpers (rootObject, replacing, inserting, indentationBeforeLine, valueJSONForProperty, preferredNewline), and returns nil for non-object roots so callers can fall back safely.
cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift New test suite covering the exact repro (leading // mode line), inline/block comments + trailing commas, plain strict JSON, malformed-config error, comment-preserving plugin insertion/replacement, and order-insensitive no-op detection. All cases are well-targeted and directly exercise the production code paths.
cmux.xcodeproj/project.pbxproj Wires OmoOpenCodeConfigJSONCParsingTests.swift into all four required pbxproj sections (PBXBuildFile, PBXFileReference, PBXGroup, PBXSourcesBuildPhase). Standard mechanical change.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["opencode.json bytes (may be JSONC)"] --> B["parseOpenCodeConfig(data:sourcePath:)"]
    B --> C["JSONCParser.preprocess — strip // and /* */ comments, trailing commas"]
    C --> D["JSONSerialization.jsonObject → [String: Any]"]
    D -->|"parse failure"| E["throw CLIError: Failed to parse … Fix the JSON syntax and retry."]
    D -->|"success"| F{Caller}
    F -->|"omoEnsurePlugin"| G["Mutate config dict (add omo plugin)\nJSONSerialization → strict JSON\nWrite to SHADOW dir (safe)"]
    F -->|"updateOpenCodePluginRegistration"| H["openCodePluginListsEqual?\n(order-insensitive canonical comparison)"]
    H -->|"equal — no change"| I["return false (no-op, file untouched)"]
    H -->|"not equal"| J["JSONCParser.source → sourceText"]
    J -->|"sourceText available"| K["openCodePluginListValueJSON → valueJSON\nJSONCObjectEditor.setRootProperty\n(surgical, comment-preserving edit)"]
    K -->|"edit succeeds"| L["Write edited JSONC bytes\n(comments preserved)"]
    K -->|"edit returns nil"| M["Fallback: JSONSerialization\nWrite strict JSON\n(comments lost — edge case only)"]
    J -->|"sourceText nil"| M
    L --> N["return true"]
    M --> N
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["opencode.json bytes (may be JSONC)"] --> B["parseOpenCodeConfig(data:sourcePath:)"]
    B --> C["JSONCParser.preprocess — strip // and /* */ comments, trailing commas"]
    C --> D["JSONSerialization.jsonObject → [String: Any]"]
    D -->|"parse failure"| E["throw CLIError: Failed to parse … Fix the JSON syntax and retry."]
    D -->|"success"| F{Caller}
    F -->|"omoEnsurePlugin"| G["Mutate config dict (add omo plugin)\nJSONSerialization → strict JSON\nWrite to SHADOW dir (safe)"]
    F -->|"updateOpenCodePluginRegistration"| H["openCodePluginListsEqual?\n(order-insensitive canonical comparison)"]
    H -->|"equal — no change"| I["return false (no-op, file untouched)"]
    H -->|"not equal"| J["JSONCParser.source → sourceText"]
    J -->|"sourceText available"| K["openCodePluginListValueJSON → valueJSON\nJSONCObjectEditor.setRootProperty\n(surgical, comment-preserving edit)"]
    K -->|"edit succeeds"| L["Write edited JSONC bytes\n(comments preserved)"]
    K -->|"edit returns nil"| M["Fallback: JSONSerialization\nWrite strict JSON\n(comments lost — edge case only)"]
    J -->|"sourceText nil"| M
    L --> N["return true"]
    M --> N
Loading

Reviews (9): Last reviewed commit: "Address greptile/coderabbit review on PR..." | Re-trigger Greptile

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 15, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@offbyone I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,260 of the 240,000 allowed lines of code this month. Reviews resume on 1 July 2026 (in 16 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get PR details and changed files
gh pr view 6187 --repo manaflow-ai/cmux --json files,additions,deletions,commits

# Get the diff for the PR
gh pr diff 6187 --repo manaflow-ai/cmux

Length of output: 13387



@offbyone: This is a clean, well-scoped fix. The two-commit red/green structure is clearly documented in the commit messages, the shared helper centralizes both the JSONC preprocessing and the user-facing error message, and the tests cover the right cases. A few observations:

Implementation (CLI/cmux.swift)

The helper is correct. One minor note: JSONSerialization.jsonObject is called with try?, which silently discards the underlying Swift error if the preprocessed bytes fail to deserialize. This was the pre-existing pattern, so it's consistent, but it does mean that edge cases (e.g. a JSONC array [] rather than a {} object at the top level) will surface the same "Fix the JSON syntax" message rather than a more specific one. Not a blocker — just worth being aware of if you ever want to improve the diagnostic in a follow-up.

// Current — swallows the underlying JSONSerialization error
guard let object = try? JSONSerialization.jsonObject(with: sanitized) as? [String: Any] else {
    throw CLIError(message: "Failed to parse \(sourcePath). Fix the JSON syntax and retry.")
}

If you wanted richer diagnostics later, the pattern would be try JSONSerialization.jsonObject(...) wrapped in a do/catch that re-wraps to CLIError — but that's a separate concern.

Tests (cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift)

The four cases are well-chosen:

  • Exact repro (leading // mode line) ✅
  • Block comments + trailing commas ✅
  • Plain strict JSON regression ✅
  • Malformed config still throws CLIError ✅

The #expect(throws: CLIError.self) in throwsCLIErrorForMalformedConfig correctly verifies the specific error type rather than just any Error, which gives good regression protection against accidentally swallowing errors.

Overall: the fix is correct, minimal, and well-tested. LGTM.

[approve]

@greptile-apps

greptile-apps Bot commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes cmux omo and session-plugin registration crashing with "Failed to parse … Fix the JSON syntax and retry." when ~/.config/opencode/opencode.json contains JSONC syntax (comments, trailing commas) — the same syntax opencode itself accepts. It introduces a shared CMUXCLI.parseOpenCodeConfig(data:sourcePath:) helper that runs JSONCParser.preprocess before JSONSerialization, and routes both affected code paths through it.

  • omoEnsurePlugin is correctly fixed: JSONC is parsed, plugins are modified, and the result is written to the shadow directory — the user's original file is never touched.
  • updateOpenCodePluginRegistration is now able to read JSONC files, but after modifying the plugin list it serialises the dictionary back as strict JSON and writes it to the user's actual opencode.json, silently stripping every // comment, /* */ block comment, and trailing comma the user placed there.
  • The new OmoOpenCodeConfigJSONCParsingTests suite (wired correctly into project.pbxproj) covers the four key scenarios and is a clear red/green regression test for the fix.

Confidence Score: 3/5

The fix is sound for the omoEnsurePlugin (shadow-dir) path, but updateOpenCodePluginRegistration now silently rewrites the user's actual opencode.json as strict JSON whenever the session plugin is installed or removed, discarding all comments and trailing commas the user placed in the file.

The parseOpenCodeConfig helper and its use in omoEnsurePlugin are correct: JSONC is accepted, the result is written only to a throwaway shadow directory, and the user's file is never modified. However, updateOpenCodePluginRegistration round-trips the parsed dictionary through JSONSerialization.data(withJSONObject:options: [.prettyPrinted, .sortedKeys]) and writes that back to the user's real config file. For any user whose file contains JSONC syntax, the serialised bytes will never equal the original (comments gone, keys reordered), so existingData == output is always false and the write fires every time, permanently stripping comments without any warning. Before this PR, those same users saw a parse error and their file was left untouched; after this PR the error is gone but their carefully annotated config is silently overwritten.

CLI/cmux.swift around updateOpenCodePluginRegistration — specifically the output.write(to: configURL) path that now activates for JSONC files that were previously rejected.

Important Files Changed

Filename Overview
CLI/cmux.swift Adds shared parseOpenCodeConfig helper using JSONCParser.preprocess; fixes omoEnsurePlugin and updateOpenCodePluginRegistration to accept JSONC input. The updateOpenCodePluginRegistration path now silently rewrites the user's opencode.json as strict JSON, stripping any comments and trailing commas the user placed there.
cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift New Swift Testing suite covering the four key scenarios for parseOpenCodeConfig: leading-comment repro, inline/block comments + trailing commas, plain JSON, and malformed-config error path. Tests are correct and well-targeted.
cmux.xcodeproj/project.pbxproj Adds OmoOpenCodeConfigJSONCParsingTests.swift to all four required pbxproj sections (PBXBuildFile, PBXFileReference, group children, Sources build phase). Wiring looks complete and correct.

Sequence Diagram

sequenceDiagram
    participant User as User's opencode.json (JSONC)
    participant CMux as cmux
    participant Parse as parseOpenCodeConfig
    participant JSONC as JSONCParser.preprocess
    participant JSON as JSONSerialization

    CMux->>Parse: data, sourcePath
    Parse->>JSONC: "strip // and /* */ comments, trailing commas"
    JSONC-->>Parse: sanitized Data (strict JSON)
    Parse->>JSON: jsonObject(with: sanitized)
    JSON-->>Parse: [String: Any]
    Parse-->>CMux: config dict

    alt omoEnsurePlugin path
        CMux->>CMux: modify plugin list
        CMux->>JSON: data(withJSONObject: config) strict JSON
        CMux->>CMux: write to shadowDir/opencode.json (user original untouched)
    else updateOpenCodePluginRegistration path
        CMux->>CMux: modify plugin list
        CMux->>JSON: data(withJSONObject: config) strict JSON (comments stripped)
        CMux->>User: write strict JSON back to user opencode.json (JSONC lost)
    end
Loading

Comments Outside Diff (1)

  1. CLI/cmux.swift, line 27665-27681 (link)

    P1 Silent JSONC-to-strict-JSON rewrite destroys user's comments on plugin registration

    updateOpenCodePluginRegistration reads the user's opencode.json via parseOpenCodeConfig (which now correctly accepts JSONC), but then serialises the result back with JSONSerialization.data(withJSONObject:options:) — strict JSON, no comments, keys sorted. Because the serialised output will never equal the original JSONC bytes, existingData == output is always false for any JSONC file, so the write always fires and the user's handcrafted // mode line, block comments, and trailing commas are permanently discarded the first time any cmux session-plugin action runs.

    The omoEnsurePlugin path is safe because it writes to a shadow directory; only updateOpenCodePluginRegistration, which writes directly to configURL (the user's real ~/.config/opencode/opencode.json), has this problem. Before this PR, the strict-JSON parse would throw, so JSONC users never reached the write step — the comments were never at risk. After this fix the read succeeds but the write silently corrupts the file. Could the write path be changed to preserve the original bytes when the only logical change is adding/removing the plugin entry (e.g., using JSONCObjectEditor to splice in the new plugin list in-place rather than round-tripping through a dictionary)?

Reviews (2): Last reviewed commit: "Parse opencode.json as JSONC in omo and ..." | Re-trigger Greptile

@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: 1

🤖 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 `@CLI/cmux.swift`:
- Around line 27516-27527: The parseOpenCodeConfig function discards underlying
error details when handling parse failures. In the catch block for
JSONCParser.preprocess (around line 27521), append the actual error to the
CLIError message using String(describing: error). For the
JSONSerialization.jsonObject call (around line 27524), change the try? to try
and add a catch block to capture the actual error details, then include them in
the CLIError message to provide users with specific debugging information about
line numbers, character positions, or JSON structure problems that caused the
parse failure.
🪄 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: 90246607-89ff-49c8-b44b-4da055399940

📥 Commits

Reviewing files that changed from the base of the PR and between 65eca62 and dc910de.

📒 Files selected for processing (3)
  • CLI/cmux.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift

Comment thread CLI/cmux.swift
Comment on lines +27516 to +27527
static func parseOpenCodeConfig(data: Data, sourcePath: String) throws -> [String: Any] {
let sanitized: Data
do {
sanitized = try JSONCParser.preprocess(data: data)
} catch {
throw CLIError(message: "Failed to parse \(sourcePath). Fix the JSON syntax and retry.")
}
guard let object = try? JSONSerialization.jsonObject(with: sanitized) as? [String: Any] else {
throw CLIError(message: "Failed to parse \(sourcePath). Fix the JSON syntax and retry.")
}
return object
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider preserving underlying error context in CLI error messages.

The current implementation catches errors from JSONCParser.preprocess (line 27521) and JSONSerialization (line 27524) but discards the underlying error details. When users encounter parse failures, they won't see which line/character caused the issue or what specific JSON structure problem exists.

Per retrieved learnings, CLI error messages should use String(describing: error) to preserve full structured descriptions for diagnostic purposes. Appending the underlying error would improve debugging while keeping the user-facing message clear.

♻️ Preserve error context
     let sanitized: Data
     do {
         sanitized = try JSONCParser.preprocess(data: data)
     } catch {
-        throw CLIError(message: "Failed to parse \(sourcePath). Fix the JSON syntax and retry.")
+        throw CLIError(message: "Failed to parse \(sourcePath). Fix the JSON syntax and retry. Error: \(String(describing: error))")
     }
-    guard let object = try? JSONSerialization.jsonObject(with: sanitized) as? [String: Any] else {
-        throw CLIError(message: "Failed to parse \(sourcePath). Fix the JSON syntax and retry.")
+    guard let object = try JSONSerialization.jsonObject(with: sanitized) as? [String: Any] else {
+        throw CLIError(message: "Failed to parse \(sourcePath). Fix the JSON syntax and retry. Top-level JSON must be an object, not an array or primitive.")
     }
     return object

The JSONSerialization guard now uses try instead of try? so the error is available in the catch block above (if you add a catch for it), or you can keep the structure and just improve the type-mismatch message as shown.

🤖 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 `@CLI/cmux.swift` around lines 27516 - 27527, The parseOpenCodeConfig function
discards underlying error details when handling parse failures. In the catch
block for JSONCParser.preprocess (around line 27521), append the actual error to
the CLIError message using String(describing: error). For the
JSONSerialization.jsonObject call (around line 27524), change the try? to try
and add a catch block to capture the actual error details, then include them in
the CLIError message to provide users with specific debugging information about
line numbers, character positions, or JSON structure problems that caused the
parse failure.

Source: Learnings

@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

@offbyone is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@offbyone
offbyone force-pushed the fix-omo-jsonc-opencode-config branch from dc910de to e16964e Compare June 16, 2026 01:19
@offbyone

Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 16, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@offbyone I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,260 of the 240,000 allowed lines of code this month. Reviews resume on 1 July 2026 (in 15 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@coderabbitai

coderabbitai Bot commented Jun 16, 2026 •

Copy link
Copy Markdown

@offbyone: Sure! I'll take another look at the PR.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@offbyone
offbyone force-pushed the fix-omo-jsonc-opencode-config branch from e16964e to 7cef4db Compare June 16, 2026 01:53
Comment thread CLI/cmux.swift Outdated
Comment thread CLI/cmux.swift
Comment on lines +27679 to +27685
static func openCodePluginListsEqual(_ lhs: [Any], _ rhs: [Any]) -> Bool {
guard
let lhsData = try? JSONSerialization.data(withJSONObject: lhs, options: [.sortedKeys]),
let rhsData = try? JSONSerialization.data(withJSONObject: rhs, options: [.sortedKeys])
else { return false }
return lhsData == rhsData
}

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.

P1 openCodePluginListsEqual serializes both arrays with JSONSerialization using .sortedKeys, but that option only sorts keys inside JSON objects — it has no effect on JSON array element order. If the user's config already contains ["cmux-session", "other-plugin"], openCodePluginListRemovingSessionPlugin strips the session entry, then it is re-appended at the end, yielding ["other-plugin", "cmux-session"]. The two arrays serialise to different byte sequences, so the no-op guard returns false and the file is rewritten (reordering the plugin) on every install run until the user's original ordering is gone.

Suggested change
static func openCodePluginListsEqual(_ lhs: [Any], _ rhs: [Any]) -> Bool {
guard
let lhsData = try? JSONSerialization.data(withJSONObject: lhs, options: [.sortedKeys]),
let rhsData = try? JSONSerialization.data(withJSONObject: rhs, options: [.sortedKeys])
else { return false }
return lhsData == rhsData
}
static func openCodePluginListsEqual(_ lhs: [Any], _ rhs: [Any]) -> Bool {
func canonicalize(_ plugins: [Any]) -> [String] {
plugins.compactMap { element in
guard JSONSerialization.isValidJSONObject([element]),
let data = try? JSONSerialization.data(withJSONObject: [element], options: [.sortedKeys]),
let str = String(data: data, encoding: .utf8) else { return nil }
return str
}.sorted()
}
return canonicalize(lhs) == canonicalize(rhs)
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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: 2

🤖 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 `@CLI/cmux.swift`:
- Around line 27518-27531: The variable declared as configParseErorr on line
27518 contains a typo with "Erorr" instead of "Error". This causes a compilation
error because subsequent references on lines 27528 and 27531 correctly use
configParseError. Fix the typo by correcting the variable declaration from
configParseErorr to configParseError to match the references throughout the
code.

In `@cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift`:
- Around line 113-117: The assertions in this test block use the `contains`
method which is too permissive and allows extra or duplicate plugin entries to
pass without detection. Replace the weak substring checks with stronger
assertions that verify the exact structure of the plugin array and explicitly
check that the model line preserves its trailing comma. This ensures the
surgical insertion path correctly modifies the configuration without allowing
false-positive passes when duplicate entries or structural issues exist.
🪄 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: 6aa988a0-1030-45e4-9cc1-59a383c40abc

📥 Commits

Reviewing files that changed from the base of the PR and between e16964e and 7cef4db.

📒 Files selected for processing (3)
  • CLI/cmux.swift
  • Sources/JSONCParser.swift
  • cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift

Comment thread CLI/cmux.swift Outdated
Comment thread cmuxTests/OmoOpenCodeConfigJSONCParsingTests.swift
Route `CMUXCLI.parseOpenCodeConfig` through the existing
`JSONCParser.preprocess` before handing the bytes to `JSONSerialization`,
matching how opencode itself reads opencode.json and how cmux reads its
other JSONC config files.

This fixes `cmux omo` (and the session-plugin registration path) aborting
with "Failed to parse <path>. Fix the JSON syntax and retry." when the
user's opencode.json contains comments or trailing commas, e.g. a leading
`// be in -*- jsonc -*- mode` line. Genuinely malformed configs still
surface the same user-facing error, so real syntax mistakes are not
masked.

Turns the OmoOpenCodeConfigJSONCParsingTests regression test green.
@offbyone
offbyone force-pushed the fix-omo-jsonc-opencode-config branch from 7cef4db to a95c0fe Compare June 16, 2026 02:04
- openCodePluginListsEqual: compare plugin lists order-insensitively.
  .sortedKeys only sorts object keys, not array element order, so an
  already-registered config like ["cmux-session", "other"] re-serialized
  to ["other", "cmux-session"] looked changed and got rewritten,
  reordering the user's plugins and stripping comments. Canonicalize and
  sort elements before comparing so a reorder is a true no-op.
- Strengthen JSONC insertion test: assert the model line keeps its
  trailing comma and the resulting plugin array is exactly ["cmux-session"]
  instead of a permissive contains check.
- Extend the no-op equality test with order-insensitive and
  differing-membership cases.
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Before we can re-land this on main, please comment I have read the CLA Document v2.2 and I hereby sign the CLA.

@teamleaderleo teamleaderleo added area: agents Agent integrations (Claude Code, Codex, ACP), agent chat, hooks, status S3: minor Wrong behavior with a workaround labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document v2.2 and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Before we can re-land this on main, please comment I have read the CLA Document v2.2 and I hereby sign the CLA.

@teamleaderleo teamleaderleo added the ready-to-land Reviewed and ready to land when CI is green label Sep 30, 2026
@offbyone offbyone closed this Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: agents Agent integrations (Claude Code, Codex, ACP), agent chat, hooks, status ready-to-land Reviewed and ready to land when CI is green S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants