Repository navigation
fix(cli): leave a user-owned OpenCode module untouched instead of appending the block - #2641
Conversation
…ending the block
The generated OpenCode adapter is an auto-loaded module, so it is installed as a
managed block rather than a whole-file write. When the file on disk carried
content but none of our markers, the upsert appended the block anyway, even
though the block's own header tells readers to "remove the markers to take
ownership".
The appended prelude redeclares module-level names the user's own module also
declares -- `import { spawn }`, `const BIN`, `function augment` -- and both
sides declare a default export once the block itself carries one, so the result
cannot be parsed at all:
SyntaxError: Identifier 'spawn' has already been declared
The client then drops the whole extension with only a load warning to show for
it. A hand-port around DeusData#2204 is therefore not a durable end state.
Probe block ownership before writing. A marker-less file that carries content is
left alone and reported in both the real run and the dry run, instead of being
turned into an unparseable hybrid. An absent, empty, whitespace-only, or
already-ours file still installs or refreshes exactly as before.
Refs DeusData#2204
Signed-off-by: Cristian Castillo <[email protected]>
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
DeusData
left a comment
There was a problem hiding this comment.
Thank you, @crisacm, for this fix and for the unusually thorough write-up. The before/after table and the pre-existing-failure comparison made it easy to follow. The behavior is right: the generated header says "remove the markers to take ownership", so a marker-less file at that path is the user's, and leaving it alone and saying so is what the installer should do. Several people hit this on #2204, so this will help.
CI stopped at lint, so the tests never ran. Two changes, then CI can run the suites:
-
Memory-core ratchet.
lint-memory-corefails becausesrc/cli/config_text_edit.cgains two rawfree()calls (72 -> 74). We route allocations throughsrc/foundation/mem_core.h, and the linter only lets each file's count of raw allocator calls go down.text_read_file()still hands back memory from plainmalloc, so swapping incbm_freeis not a drop-in fix here. The simplest way through is to let the probe reuse the read-validate-scan step thattext_upsert_managed_block()already performs (for example a small shared static helper, or a probe-only mode), so no newfree()site is added. You can check it locally withmake -f Makefile.cbm lint-memory-core. (Thecypher.cline in the same log asks us to tighten our own baseline; that one is not yours.) -
Install-plan receipt. When
g_install_planis set,install_generated_client_extension()still callsplan_record(label, "extension", path)before the ownership check, so the plan receipt lists a write that the real run will not make. The receipt is built from the same code path precisely so it cannot drift from the run. Could you skipplan_record()when the file is foreign? The message you print there already explains the skip. A check for that in your new cli test would be welcome if it is easy.
Your branch still merges cleanly with current main, so no rebase is needed. Thanks again for the careful work!
Signed-off-by: Martin Vogel <[email protected]>
…robe cbm_text_managed_block_status() repeated the read, validate and scan step of text_upsert_managed_block() with two raw free() calls of its own, which put src/cli/config_text_edit.c over its memory-core baseline (72 -> 74). Both now go through text_read_managed_document(), so the probe and the writer judge a file by the same rules, and the file is back at 72. An absent file still reads as an empty document, so the probe returns the same results as before. Signed-off-by: Martin Vogel <[email protected]>
With --plan, install_generated_client_extension() recorded the extension before the ownership check and printed the skip notice on stdout. The receipt therefore listed a write the run would not make, and `install --plan` no longer printed valid JSON when a user-owned module was present. For a user-owned module, plan mode now records and prints nothing. The dry run prints the "would be skipped" notice instead, alongside the hook-script notice from DeusData#1387, and the real run keeps its "left untouched" line. The cli test now also checks the receipt and plan-mode stdout, with a control that the receipt does list the module once it is no longer user-owned. Refs DeusData#2204 Signed-off-by: Martin Vogel <[email protected]>
|
Thank you, @crisacm, for this fix and for the care that went into it. The write-up, the before/after table and the side-by-side run showing that the one red cli test fails on main too made this easy to review, and the behavior is exactly right: a module at that path without our markers is the user's, and the installer should leave it alone and say so. Several people ran into this on #2204, so this will help them directly. To save you the round trip, we made the changes from our review ourselves and pushed them on top of your branch. Your commit is unchanged; here is what we added after it:
With these, the full cli suite (363 tests) passes locally. Once CI is green we will merge. Thanks again for a really careful contribution! |
text_upsert_managed_block read doc.bom_len into a function-scope local, but only the "no managed region yet" branch uses it. cppcheck 2.20 (the CI lint image) flags that as variableScope. Declare it inside the else branch instead; doc stays in scope and unmodified, so behaviour is unchanged. Signed-off-by: Martin Vogel <[email protected]>
|
Hi @crisacm, thank you again for this fix and for your patience with us here! This is a small follow-up to our own earlier fix-up on your branch, not to your change. Our refactor commit ("share the managed-block read step with the ownership probe") left a variable in a wider scope than it needs, and the cppcheck version our CI lint job uses flagged it.
No action is needed from you. Once CI is green we will take it from here. |
The byte check in cli_extension_leaves_user_owned_module_untouched_issue2204 ran only after a dry run and the plan receipt, and neither of them writes, so it could not catch the DeusData#2204 append. The test now also runs a real install over the user-owned module, then checks that the file is byte-identical and that the run reports it as left untouched. Each phase's stdout checks are bounded to that phase's slice of the capture, so no phase can pass on another's line. With the early return that ends the user-owned branch of install_generated_client_extension() dropped, the test now fails; before this change it still passed. Refs DeusData#2204 Signed-off-by: Martin Vogel <[email protected]>
Signed-off-by: Martin Vogel <[email protected]>
|
Hi @crisacm, thank you again for this fix, and for your patience while it waited on our side. Your change is the right one: a module at that path without our markers belongs to the user, and the installer now leaves it alone and says so. We added two small things on top of your branch so it can go through CI on today's tree:
No action is needed from you. If you push to this branch again, please pull first, since it now has our commits on top. Once CI is green we will merge. Thanks again for such a careful contribution! |
Signed-off-by: Martin Vogel <[email protected]>
|
Thanks again, @crisacm. Several other pull requests landed on main this evening, including changes in files this one also touches, so we merged current main into your branch once more (one signed-off merge commit, 4876266; your changes are untouched). That keeps the required checks honest against today's tree; we will merge once they are green. Please pull before pushing again. |
Signed-off-by: Martin Vogel <[email protected]>
|
Thank you for your patience, @crisacm. CI went green on this branch, but this evening's merges on main touched files this pull request also changes, so that green no longer covered today's main. We merged current main into the branch once more (one merge commit, 2a195bd; your changes are untouched) and will merge once the fresh checks are green. Please pull before pushing again. |
Signed-off-by: Martin Vogel <[email protected]>
|
Thank you for your patience, @crisacm. A pull request that merged into main a few minutes ago (#2290) changes src/cli/cli.c, which this pull request also changes, so the CI run in progress here could no longer vouch for today's main. We merged current main into your branch once more (one merge commit, 079e59d; it merged cleanly and your own change is untouched) and started a fresh run. We will merge once the required checks are green. Please pull before pushing again. |
Signed-off-by: Martin Vogel <[email protected]>
|
Thank you for your patience, @crisacm. Main has moved since the last CI run on this branch, and some of the new commits change src/cli/cli.c, which this pull request also changes, so that run can no longer vouch for today's main. We merged current main into your branch (one merge commit, 8f6974e; it merged cleanly and your own change is untouched) and started a fresh run. We will merge once the required checks are green. Please pull before pushing again. |
Signed-off-by: Martin Vogel <[email protected]>
|
Thank you for your patience, @crisacm. The macOS legs of the last CI run here were cancelled by GitHub itself: it is retiring its macOS 14 runners on November 2 and ran scheduled brownouts beforehand, so those jobs never got a runner. Nothing in your change caused it. Main now runs CI on macOS 15 (#2849), so we merged current main into your branch (one merge commit, 502d86a; it merged cleanly and your own change is untouched) to get a run on the new runners. Please pull before pushing again. |
Signed-off-by: Martin Vogel <[email protected]>
Signed-off-by: Martin Vogel <[email protected]>
Signed-off-by: Martin Vogel <[email protected]>
|
Thank you for your patience, @crisacm. Your change was green, but several pull requests merged yesterday touch src/cli/cli.c and tests/test_cli.c, which this pull request also changes, so that run could no longer vouch for today's main. We merged current main into your branch (no conflicts, your own change is unchanged) to get a fresh CI run on the combined tree. Please pull before pushing again. We will merge as soon as the required checks are green. |
Summary
The generated OpenCode adapter (
~/.config/opencode/plugins/cbm-augment.ts) is an auto-loaded module, so it is installed as a managed block rather than a whole-file write. That is deliberate: the directory is auto-loaded, so a user may legitimately keep their own module there.But when the file on disk carried content and none of our markers, the install appended our block to it anyway — even though the block's own header tells readers to "remove the markers to take ownership". That contradicts the promise and produces a file the client cannot parse.
Reproduction
install -yagainst 0.11.0, in an isolatedHOME, starting from the hand-port shape described in #2204:After the run the managed block sits after that content, so module-level names are declared twice:
OpenCode's loader then drops the whole extension and the only trace is a load warning. Once the block itself carries
export default(#2089), the same append turns into a duplicate default export instead. So a hand-port is not a durable end state — which is why #2385 matters — but the installer should not manufacture the broken file in the meantime.Change
cbm_text_managed_block_status()— a read-only ownership probe:0writable (absent, whitespace-only, or already ours),1the file carries content but none of our markers,-1unsafe state or I/O failure.install_generated_client_extension()probes before writing:--dry-run. The probe runs before generation so the preview names the refusal instead of promising a write the run will not make (the same honesty rule install on 0.9.1-rc.1 removes existing Claude Code hooks instead of installing them #1387 established for hook scripts).cbm_text_upsert_managed_block()and its other callers (instruction documents, where appending to a user's file is the intent) are untouched — the new semantics are scoped to the auto-loaded module case.If you would rather the run fail loudly than skip, that is a small change to the same call site; I picked "leave alone and say so" because it matches the ownership wording in the generated header.
Verification
End-to-end in isolated
HOMEs on the built binary:SyntaxErrorTests:
scripts/test.sh --suites config_text_edit— 23 passed, including the newconfig_text_managed_block_status_reports_foreign_module(absent / empty / whitespace / ours / foreign / unbalanced markers / malformed markers).scripts/test.sh --suites cli— 361 passed, 1 failed, and the newcli_extension_leaves_user_owned_module_untouched_issue2204is the+1over the baseline: it seeds a user-owned module, runs the dry run, and asserts both the warning and that the file is byte-identical afterwards (modelled on the existingcli_dry_run_predicts_refused_hook_script_issue1387).The single failure is pre-existing and unrelated. Same machine, same suite, same test:
mainat0ca6e7d(no changes)In both trees the failing test is
cli_registry_installs_gitlab_and_devin_lifecycle_contextwith the identical diagnosticplan=1 clean_plan=1 installed=0 cleaned=1 gitlab=1 devin=1 uninstall=0— it touches neither the OpenCode adapter nor the managed-block writer. Flagging it here so a redtest-unixjob is not read as fallout from this change.clang-format 20.1.8(CI's pinned version) reports no changes to any touched file.Refs #2204. The template side — registering the 2.x hooks in
setup— is #2385 and is deliberately not touched here. Happy to fold this into #2385 instead if you would rather keep the OpenCode changes on one branch.