Skip to content

fix(publish): follow-ups from the #381 review - #421

Merged
gaodan-fang merged 13 commits into
mainfrom
fix/publish-381-followups
Oct 16, 2026
Merged

gaodan-fang merged 13 commits into
mainfrom
fix/publish-381-followups

Conversation

@gaodan-fang

@gaodan-fang gaodan-fang commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #381. Fixes the three low-severity comments @jayaramkr left in the #381 approval, plus the rest of #419, which collects them with four more. One commit per item. publish.py is edited in plugin-source/, and the five platform copies are re-rendered with build_plugins.py render, so check passes.

Fixes items 1–7 of #419 (the publish items). Items 8–14, the segmentation follow-ups from the #382 review, are handled separately.

Changes

#419 item 6a: test for a symlinked entity file pointing outside its type dir (review comment on line 96)

  • Before: with ln -s ../../../outside/native.md .evolve/entities/feedback/note.md, removing not path.is_relative_to(type_dir) kept every publish test green. A real run then published the file and deleted outside/native.md.
  • After: a new test covers both the claude and codex copies, with and without --type. It asserts a non-zero exit, that the outside target is unchanged, that the symlink is still in place, and that nothing was written to the clone.
  • Mutation check: with that half of the condition removed, all 4 cases fail on assert result.returncode != 0 (0 != 0; publish succeeded).

#419 item 2: --type picks the exact dir name first (review comment on line 87)

  • Before: with both my_notes/x.md and my-notes/x.md present, no flag, --type my_notes and --type my-notes all reported ambiguous.
  • After: an exact raw-name match wins, and the sanitized match is the fallback. This is @jayaramkr's suggested ordering. The requested value must still pass the sanitize_type validity check first.
  • Tests:
    • each raw name picks exactly its own dir;
    • no flag, and --type My_Notes, are still ambiguous.

#419 item 1: name collision with another owner (review comment on line 182)

#419 item 3: symlinked type dir in the write clone

  • Before: with guideline -> ../../../../../out committed into the clone, Published: x.md -> <w>/out/x.md. That's exit 0, and the private copy was removed.
  • After: Error: refusing to publish into .evolve/entities/subscribed/memory/guideline: it is a symlink in the target repo clone. That's exit 1, and nothing is written. publish also refuses a type dir that resolves outside clone_root.resolve().
  • Tests:
    • a symlink to a dir outside the clone, and one to a dir inside it, are both refused;
    • a write clone root that is itself a symlink still publishes.

#419 item 4: the private copy can't be removed

  • Before: with the type dir at chmod 555, publish raised a PermissionError traceback and left both copies. There was no audit entry, and every retry said already published.
  • After: publish checks first that the private copy's directory is writable:
    Error: cannot publish x.md: <w>/p/.evolve/entities/guideline is not writable, so the private copy could not be removed afterwards. Fix its permissions and publish again. Nothing was published.
    
    If the unlink still fails after the write, publish keeps the published copy and warns, naming the leftover private copy to delete. The publish is still audited, with source_removed: false.
  • Tests:
    • the chmod 555 pre-check: nothing is written and there's no audit entry;
    • an injected unlink failure after the write: exit 0, a clear warning, both copies, and an audit entry.

#419 item 5: absolute trajectory path

  • Before: the manual learn flow's trajectory: /home/<user>/<project>/.evolve/trajectories/... was published as-is.
  • After: an absolute path inside the project is published relative to it (.evolve/trajectories/<file>). One outside the project is dropped. Relative values are unchanged.
  • Why relative rather than drop:
    • It's the form the stop-hook flow already writes and the learn skill documents, so published entities look the same whichever flow produced them.
    • Retention keys the link on the basename, so it keeps working.
    • Only a path outside the project has no meaningful relative form, so only that one is dropped.
  • Tests: inside the project; outside the project; relative.

#419 item 6b: differently-cased symlink target

  • Covers entities/feedback -> SUBSCRIBED/read/feedback, with and without --type and EVOLVE_DIR.
  • It's skipped on case-sensitive filesystems, so it runs on macOS but is skipped on Linux CI.
  • I checked the test body on Linux by aliasing SUBSCRIBED -> subscribed. It couldn't be mutation-tested here.

#419 item 7: wording

  • plugin.toml: "Publish one of my Evolve entities."
  • Claude, Codex and Bob READMEs: "Publishing Entities" heading, with the anchors updated.
  • Claude, Bob and claw-code config examples: "published entities".
  • The rendered copies, including the Codex plugin.json, were regenerated.

Checks

  • tests/platform_integrations/test_publish.py: 65 passed, 1 skipped (item 6b on Linux).
  • Platform tier (-m platform_integrations): 719 passed, 1 skipped.
  • Default tier: 2158 passed, 5 skipped.
  • ruff format --check, ruff check and mypy .: clean.
  • build_plugins.py check: clean.
  • Each behaviour fix's new tests fail on the commit before it.

Summary by CodeRabbit

  • New Features
    • Publishing distinguishes name conflicts with another owner from entities that are already published, and prefers exact type matches when selecting an entity.
    • Published entities use project-relative paths for eligible absolute trajectories; trajectories outside the project are omitted.
    • If the private copy cannot be removed after publishing, the shared copy remains published and a warning explains that manual cleanup is needed.
  • Bug Fixes
    • Publishing blocks unsafe destination paths and symlinks, and checks that the private entity directory has the required access.
  • Documentation
    • Updated publishing guidance to refer to entities rather than guidelines.

A symlinked entity file under entities/<type>/ that points outside its type
dir must not be published: publish unlinks the resolved source, so it would
delete data outside .evolve. Removing `not path.is_relative_to(type_dir)`
left every publish test green; this test now fails without it.

Refs #419
…tized match

With entities/my_notes/x.md and entities/my-notes/x.md both present, every
invocation reported x.md as ambiguous, including --type my_notes and
--type my-notes, because both dirs sanitize to my-notes. An exact dir name
now wins; the sanitized match is the fallback, and the requested value must
still pass the sanitize_type validity check first.

Refs #419
…e same name

adapt-memory names entities after the native memory, so two teammates'
user-role memories both map to <clone>/user/user-role.md. When the second
one published, publish said "Unpublish it first or delete it manually",
telling them to delete a teammate's entity. When the existing file's owner
differs from the publishing user, publish now reports a name collision with
that owner and asks the user to rename their memory or entity. The old
message stays when the owner is the same or absent (see #400 item 1 for
updating an entity you already published).

Refs #419
…te clone

dest_dir is <clone>/<type>, and the containment check compared dest_path
against the resolved dest_dir, so a symlink committed into the shared repo
(e.g. guideline -> ../../../../outside) redirected the write: publish wrote
outside/x.md, removed the private copy and reported success. publish now
refuses when the type dir in the clone is a symlink, and when it resolves
outside the resolved clone root. A clone root that is itself a symlink
still works.

Refs #419
Publish wrote the shared copy and then unlinked the private one. When the
unlink failed (e.g. a chmod 555 type dir), the result was a PermissionError
traceback, two copies, no audit entry, and "already published" on every
retry. publish now checks that the private copy's directory is writable
before writing anything and refuses with a clear message if not. If the
unlink still fails after the write, it keeps the published copy, names the
leftover private copy to delete, and records the publish in the audit log
with source_removed: false.

Refs #419
The manual learn flow stores save_trajectory's resolved path in each
entity's trajectory field, so publish shipped the publisher's home
directory, username and project path to every subscriber (it already
strips native_path). An absolute trajectory inside the project is now
published relative to it (.evolve/trajectories/<file>, the form the
stop-hook flow already writes); one outside the project is dropped, since
it has no meaningful relative form. Relative values are unchanged.

Refs #419
…sitive filesystems

entities/feedback -> SUBSCRIBED/read/feedback names the subscribed clone on
a case-insensitive filesystem, but resolve() keeps the spelling, so a
string-prefix comparison would let it through. The identity check
(_inside_any) refuses it; this test pins that and is skipped on
case-sensitive filesystems.

Refs #419
Publish handles every entity type since #381, but the Codex default prompt
still said "Publish one of my Evolve guidelines", the Claude, Codex and Bob
READMEs had a "Publishing Guidelines" heading, and the Claude, Bob and
claw-code config examples said the identity stamps ownership on "published
guidelines". Say entities instead, and re-render the platform copies.

Refs #419
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4b652936-1167-4183-8ba7-0a3dafd14ad2
📥 Commits

Reviewing files that changed from the base of the PR and between 98f8cea and 9d3ad45.

📒 Files selected for processing: 1
  • tests/platform_integrations/test_publish.py

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

📝 Walkthrough

The publish scripts now handle entity selection, trajectory paths, destination checks, ownership conflicts, and source-removal failures. Tests cover these behaviors. Plugin documentation and prompts now describe publishing entities instead of guidelines.

Changes: Entity publishing

Layer / File(s) Summary
Resolve entities and prepare publication
plugin-source/skills/evolve-lite/publish/scripts/publish.py, platform-integrations/*/plugins/evolve-lite/**/publish.py, tests/platform_integrations/test_publish.py
Publishers prefer exact type-directory names and normalize absolute trajectory paths within the project to relative paths. They omit absolute paths outside the project. Tests cover trajectory handling and ambiguous type names.
Check source, destination, and ownership
plugin-source/skills/evolve-lite/publish/scripts/publish.py, platform-integrations/*/plugins/evolve-lite/**/publish.py, tests/platform_integrations/test_publish.py
Publishers check source-directory permissions and reject unsafe destination directories. An existing entity with a different readable owner produces a name-collision error. Tests cover these checks and symlink cases.
Record source-removal results
plugin-source/skills/evolve-lite/publish/scripts/publish.py, platform-integrations/*/plugins/evolve-lite/**/publish.py, tests/platform_integrations/test_publish.py
A private-copy removal failure does not stop publishing. The audit entry records the failure, and the command warns that the private copy remains.
Update entity terminology
plugin-source/*/README.md, plugin-source/plugin.toml, platform-integrations/*/plugins/evolve-lite/README.md, platform-integrations/codex/plugins/evolve-lite/.codex-plugin/plugin.json
Documentation headings, links, configuration comments, and the Codex prompt now refer to publishing entities instead of guidelines.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Merge Risk: 🔵 Low · up to 9d3ad

One symlink test can pass without confirming the intended refusal. This limits confidence in that safeguard but does not appear to block merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 64.44% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 45 functions across 6 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed 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 identifies publish fixes resulting from the #381 review. It is concise and relevant to the main changes.
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@jayaramkr jayaramkr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: approve. Reviewed at 9d3ad45. I ran everything below at 03d1c19. The commits since then only merge main (#360 in experiments/, #398 in deps and hooks tests), and test_publish.py (66 passed) and build_plugins.py check pass again at 9d3ad45.

All seven of the #419 items this PR claims (1–7) are fixed, and each fix has a test that fails on the commit before it. I found nothing new at medium or above. The low-severity leftovers are filed as #419 items 24–28, so none of them needs to land here.

Baseline: ruff, mypy and build_plugins.py check are clean. The platform tier is 723 passed at head and 704 at base. test_publish.py goes from 47 to 66, with nothing skipped on macOS. All 5 copies of publish.py are byte-identical.

#419 items 1–7

Item Status
1. Name collision Fixed. Owner alice with --user bob gives "name collision … by 'alice'". The same owner or no owner keeps the old hint. Binary, unreadable or directory destinations fall back cleanly, with no traceback.
2. --type exact match first Fixed. With my_notes/ next to my-notes/, each name publishes from its own dir. No flag, or MY_NOTES, is still ambiguous.
3. Symlinked type dir in the clone Fixed. Symlinks pointing outside the clone, inside it, or nowhere are all refused, and nothing is written. On base these wrote outside the clone, wrote into another dir, and crashed, respectively. A symlinked clone root still publishes.
4. Private copy can't be removed Fixed. A chmod 555 type dir is refused before anything is created. A forced unlink failure gives exit 0, a warning on stderr, and source_removed: false in the audit. No false refusals for a read-only file, a sticky dir, or a symlinked writable dir.
5. Absolute trajectory Fixed. An absolute path inside the project becomes .evolve/trajectories/<file>, including through /tmp vs /private/tmp, .. and a symlinked project dir. A path outside the project is dropped, and a relative one is unchanged.
6a / 6b. Missing tests Added, and each catches its regression. 6b runs on macOS only; see #419 item 26.
7. Leftover wording Fixed. No #publishing-guidelines links are left dangling.

New, non-blocking (all in #419)

  1. Low, pre-existing: a committed symlink file in the write clone still redirects the write. (inline, publish.py:237; #419 item 24)
  2. Low, test quality: assert "symlink" in result.stderr matches the pytest temp-dir name. (inline, test_publish.py:207; #419 item 25)
  3. Low, CI gap: the case-variant test skips on case-sensitive filesystems, so Linux CI never runs it, and on macOS it's the only thing catching a regression to string comparison (#419 item 26).
  4. Low, fails safe: with an EVOLVE_DIR not named .evolve, the trajectory link becomes trajectories/<file>, and a path under <proj>/.evolve/ is dropped. Removing the realpath call breaks no test (#419 item 27).
  5. Nit: the owner comparison doesn't normalise --user: " alice" and Alice both count as different from alice (#419 item 28).

Also verified

  • No path prints "Published:" and then exits non-zero, and none exits 0 without publishing. The only success-with-warning case is the intended unlink failure.
  • The unlink-failure test patches Path.unlink inside the subprocess, so it works the same as root. The chmod test skips under root and restores permissions in a finally.
  • The tests run the real script, with no over-mocking. 15 of 17 mutations are caught. The two survivors are the realpath gap (item 27) and the is_relative_to(clone_root) guard, which is redundant once the symlink check has run.
  • The PR body's claims hold: each fix's new tests fail on its parent commit, and the render check catches hand-edited drift in rendered copies.

Thanks @gaodan-fang. This closes out the publish half of #419 cleanly.

if not dest_base.is_relative_to(clone_root.resolve()):
print(f"Error: refusing to publish into {dest_dir}: it resolves outside the target repo clone.", file=sys.stderr)
sys.exit(1)
dest_path = (dest_dir / args.entity).resolve()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Low, pre-existing: item 3 covers a symlinked type dir, but a symlinked entity file in the clone still redirects the write.

If a teammate commits guideline/g.md -> y.md (dangling), publishing g.md writes y.md, prints Published: g.md -> …/guideline/y.md with exit 0, and deletes the private g.md. So the entity ends up under the wrong name. Base behaves the same.

The write can't leave the type dir: a target outside it is refused, though with the misleading invalid entity name: 'g.md'. With g.md -> nested/x.md, both base and head crash with a FileNotFoundError traceback. And with g.md -> z.md where z.md has owner alice, the new collision message names g.md but shows z.md's path.

Refusing when dest_dir / args.entity is a symlink, as item 3 does for the dir, would close it. Tracked as #419 item 24.

result = run_publish(project_dir, ["--entity", "my-guideline.md"], expect_success=False)

assert result.returncode != 0
assert "symlink" in result.stderr

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Low (test quality): this assertion passes for the wrong reason in the [outside] case.

I removed the dest_dir.is_symlink() refusal, and [outside] still passed. The "resolves outside the target repo clone" guard refuses that case, and "symlink" matches the pytest temp-dir name in the path (…/test_refuses_a_symlinked_type_0/…), not the error. Only [inside] tests the refusal. Asserting on "is a symlink in the target repo clone" would fix it. Tracked as #419 item 25.

@gaodan-fang
gaodan-fang merged commit 28fc0a5 into main Oct 16, 2026
22 checks passed
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.

2 participants