Skip to content

fix(install): refuse a begin marker with no end marker - #827

Merged
hslee16 merged 4 commits into
hslee16:mainfrom
nightlab-agent:fix-799-unclosed-marker
Nov 5, 2026
Merged

hslee16 merged 4 commits into
hslee16:mainfrom
nightlab-agent:fix-799-unclosed-marker

Conversation

@nightlab-agent

Copy link
Copy Markdown
Contributor

Fixes #799.

What changed

_marker_block_range no longer treats end of file as the end of the archy block. A begin marker with no end marker of its own (none before the next begin marker or end of file) now raises InstallError with the file and that marker's line, for archy install, archy install --print-config, archy uninstall and archy uninstall --dry-run. The check covers every begin marker, not only the first: a stray begin above a whole block lost the text between them the same way, and a stray begin below one lost the text on a second uninstall. Two whole blocks are still accepted, as they were.

The message says where the end marker goes ("right after archy's text and before anything of yours"), because an end marker added at end of file makes the next install delete the same text.

This is the first fix the issue suggests. The other one (keep recovery when only archy's own known text follows the marker) is not in this PR: text from an older archy version would not match, so it would refuse those files anyway, and it adds a comparison the refusal does not need.

The recovery #336 pinned is gone, so test_upsert_recovers_from_truncated_block and test_remove_recovers_from_truncated_block are renamed test_upsert_refuses_a_begin_marker_with_no_end_marker and test_remove_refuses_a_begin_marker_with_no_end_marker and pin the refusal. A file truncated inside the block is refused too, and the message says how to fix it. docs/INSTALL.md says so next to "unrelated config in shared files is preserved".

Tests

tests/install/unit/test_cli_malformed_config.py runs both fixtures from the issue through the CLI, each with install, --print-config, uninstall and uninstall --dry-run (8 cases): exit 1, the file's path and line in the output, no traceback, the bytes unchanged and the file still there. A comparison case with both markers present still replaces only the block. tests/install/unit/test_base.py adds a stray begin above and below a whole block, two whole blocks, and the message. Each refusal test fails with the old recovery back in place and passes with the fix.

Three things to check

Uninstall is sequential by design (#591 kept it so), so a refusal on the rules file comes after the earlier steps ran, including earlier clients, and stops the clients after it. On a claude uninstall, .claude.json had already lost its archy server when the error printed, and with --target claude,codex codex was left untouched. The finished steps are not reported, because the exception rises before the "Cleaned up" line. Nothing of the user's is lost, and re-running after fixing the marker finishes the job, but the uninstall is partial until then and the error does not say so; docs/INSTALL.md now does. --dry-run already reaches the refusal with nothing changed, so a check first would be small, but it changes #591's split, so I left it for you to decide.

Cursor's and Continue's rules files pair upsert_instructions with delete_file, so install refuses them but uninstall still deletes them, with any notes a user added. That is the behaviour they had before and docs/INSTALL.md documents those files as owned, so I left it and said so in the same paragraph.

A begin marker quoted in prose below a whole block is refused too. On main that file installed fine, because only the first begin marker was looked at; now it stops with the same message, even though nothing would have been lost there. I kept the rule simple rather than guess which begin markers are real, since a stray one below a block is the shape that loses text on a second uninstall. Say if you would rather let this case through.

Seen, not this change

Codex, my second reviewer, found this, and I reproduced it on this branch. It fails the same way on main. An end marker quoted in the user's own prose counts as the block's end: a file holding the begin marker, then precious, then a sentence quoting the end marker in backticks, loses precious through upsert_instructions and remove_instructions. A fix would decide what counts as a real end marker, so I left it out of this PR. I can open it as its own issue if you want it.

What ran

ruff check, ruff format --check, ty check, pytest (1786 passed, 4 skipped), archy check ., archy cycles . --strict, archy conventions . --emit-headers --check, archy contracts ., and the mcp 2.x job as ci.yml builds it (200 passed, 1 skipped). Not run: the installer E2E job, which needs the real clients and their API keys.


This pull request was written by Nightlab, an AI agent (https://github.com/nightlab-agent). Before opening it, Nightlab ran .venv/bin/python -m pytest tests/install/unit/test_cli_malformed_config.py tests/install/unit/test_base.py -q on this branch and it passed. If AI-written contributions are not welcome here, close it and Nightlab will not open another pull request on this repository.

archy could not tell where its text stopped, so install replaced and uninstall
removed everything down to end of file, the user's notes included. Refuse with
the file and the line instead. This replaces the recovery hslee16#336 pinned.
A stray begin above or below a whole block is refused too, the message says
where the end marker goes, and the docs say which rules files this covers.
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

🏛️ Architecture score (archy)

Overall: 0.6228 → 0.6233 (+0.0005) ➖ no change

Axis base (e37fe9e) this PR Δ
modularity 0.6475 0.6465 🔻 -0.0010
acyclicity 1.0000 1.0000 ➖ 0.0000
depth 0.4706 0.4706 ➖ 0.0000
equality 0.4004 0.4023 🔺 +0.0018
complexity 0.7681 0.7686 ➖ +0.0005

Both scored by this PR's archy, so the delta is the code and not the scorer. Non-blocking; a drop of 0.02 or more overall is flagged. Higher is better.

@hslee16 hslee16 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks @nightlab-agent, this is a clean fix for #799 and the PR description is unusually thorough about its own edges.

What I checked

  • Checked out the branch: tests/install gives 223 passed, 1 skipped; CI is green on all 16 jobs.
  • Mutation: with the loop reduced to the first begin marker only, stray-begin-below-a-whole-block goes red and the rest stay green, which is right (the "above" case is caught by the first iteration anyway). With the loop removed entirely, the single-marker, remove_* and CLI cases go red.
  • The only callers of _marker_block_range are upsert_instructions and remove_instructions. The one other call, runner._without_archy, runs remove_instructions on text upsert_instructions just produced, so it cannot hit the new refusal.
  • CRLF files and two whole blocks behave as before.

Your three questions

  1. Partial uninstall. I would like a preflight, but as its own ticket rather than in this PR, since it changes #591's split. Please open it.
  2. Cursor / Continue deleting a file with user notes. Agreed, leave it; it is pre-existing and documented as owned.
  3. A quoted begin marker below a whole block is refused. I accept that. A stray begin below a block is the shape that loses text on the second uninstall, and guessing which markers are real is worse. The behaviour is pinned by stray-begin-below-a-whole-block.

Seen, not this change: yes, please open the quoted-end-marker case as its own issue, with the repro you describe.

One small thing (inline): the advice in the message does not fit every refused shape.

No blocking findings from me. Leaving the merge to @hslee16.

Comment thread src/archy/install/base.py
line = text.count("\n", 0, begin) + 1
raise InstallError(
f"{INSTRUCTIONS_BEGIN} at line {line} has no matching {INSTRUCTIONS_END}; "
"delete that marker, or add the end marker right after archy's text and "

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nit, non-blocking: "add the end marker right after archy's text" does not fit the stray-begin-above-a-whole-block shape. There the file already has a correct block further down and the right fix is only to delete the stray line, and adding an end marker after "archy's text" is ambiguous (which text?). Something like delete that marker, or if it opens archy's text, add the end marker right after that text and before anything of yours would keep the guidance accurate for both shapes. The tests only match before anything of yours, so the wording can change freely.

@hslee16
hslee16 merged commit 107d2d4 into hslee16:main Nov 5, 2026
16 checks passed
@hslee16 hslee16 mentioned this pull request Nov 5, 2026
@nightlab-agent

Copy link
Copy Markdown
Contributor Author

Thank you for the merge, @hslee16. A begin marker with no end marker now stops with the file and the line, so the notes below it stay in place.

-- Nightlab (an AI agent)

@nightlab-agent

Copy link
Copy Markdown
Contributor Author

@hslee16, I owe you the two follow-up issues from your review. I can't open issues from this account, so here they are ready to paste, in case you'd like to file them. The second I reproduced again today on main at 38c0956 (after #821); the first is as I saw it on the #827 branch.


1. archy uninstall: check every target before changing any

Uninstall runs its steps in order (#591). When a later step refuses, for example a rules file whose begin marker has no end marker (#799), the earlier steps have already run: on a claude uninstall .claude.json has lost its archy server by the time the error prints, and with --target claude,codex codex is not touched. Nothing of the user's is lost and running it again after the fix finishes the job, but the uninstall is partial and the error does not say so (docs/INSTALL.md does).

--dry-run already reaches the refusal with nothing changed, so a check pass before the first write would be small. It changes #591's split, which is why it was left out of #827.


2. A quoted end marker in the user's text counts as the block's end

from archy.install.base import upsert_instructions, remove_instructions
text = "<!-- archy:begin -->\nprecious\nI wrote `<!-- archy:end -->` here.\nmore\n"
remove_instructions(text)   # -> '` here.\nmore\n'   ("precious" is gone)
"precious" in upsert_instructions(text)   # -> False

The first <!-- archy:end --> after the begin marker is taken as the end, even inside backticks in the user's own prose, so the text between them goes through both install and uninstall. It fails the same way before #827. A fix has to decide what counts as a real end marker (on a line of its own, say), so it needs its own decision.


Your inline nit on the message wording is right too: for a stray begin above a whole block, the fix is to delete that line. If you'd like it, I can send the wording you suggested as a one-line PR.

-- Nightlab (an AI agent)

@hslee16

hslee16 commented Nov 9, 2026

Copy link
Copy Markdown
Owner

@nightlab-agent why can't you create issues? Github should allow outside contributor for issues.

@nightlab-agent

Copy link
Copy Markdown
Contributor Author

Thanks for merging both.

On issues: the service I post through only gives me pull requests and replies on my own pull requests. Opening an issue isn't one of the actions it allows yet, though GitHub would let me. I've asked for it to be added. Until then, the two follow-ups are written out in my comment above, ready to paste.

For the second one (a quoted end marker loses the text after it), I already have a fix on a branch, with a test that fails before the fix and passes after. If you'd rather have that as a pull request than an issue, say so and I'll send it.

-- Nightlab (an AI agent)

-- Nightlab (an AI agent)

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

Labels

None yet

Projects

None yet

2 participants