diff --git a/CHANGELOG.md b/CHANGELOG.md index 4886857..b7422a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,27 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C ## [Unreleased] +### Fixed +- `agnes refresh-marketplace` (non-bootstrap path) now re-applies + `chmod +x` to every `.sh` under `~/.agnes/marketplace` after each + `git reset --hard FETCH_HEAD`, not just on the initial bootstrap + clone. `git reset --hard` rewrites the working tree from the tree + object — if the upstream tree stores a hook script as non- + executable (or on `core.filemode=false` setups), every refresh + silently re-strips the +x bit and the previously-fixed hooks fire + with "Permission denied" again on the next `SessionStart`. + Extracted `_chmod_clone_sh_files()` helper, called from both + `_bootstrap_clone` and `_git_fetch_and_reset`. Best-effort, no-op + on Windows NTFS. Closes the coverage gap Devin Review flagged on + PR #350. +- Stripped six stale unresolved merge-conflict markers + (`<<<<<<<` / `=======` / `>>>>>>>`) from the `[0.55.1]` section of + `CHANGELOG.md` that landed on `main` via PR #350's release-cut + commit. Markers were rendering as raw conflict text on GitHub and + in any tooling that parses the changelog; the HEAD-side content + inside each pair is what was kept (the incoming side held + superseded intermediate-commit duplicates). + ## [0.55.2] — 2026-05-19 ### Fixed @@ -205,7 +226,6 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C save) mirroring the Memory Domain pattern. ### Changed -<<<<<<< HEAD - **Bulk-assign tables → package** modal — package dropdown options now carry a `(N of M tables already in)` suffix so admins see the existing distribution before picking a target. Counts surface @@ -515,8 +535,6 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C - Single PR cutover (no two-phase rollout). Legacy `marketplace_plugins.is_system` + `user_plugin_optouts` retained per spec D1 — Marketplace was deliberately not touched. -======= -<<<<<<< HEAD - /home onboarding Step 2 retitled "turn on permission-skip for setup" and now leads with `claude --dangerously-skip-permissions` as the recommended session flag, because the Step 4 paste runs ~20 shell @@ -524,7 +542,6 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C The flag is session-scoped, drops on next plain `claude`. Auto-accept via Shift + Tab kept as the strict-review fallback for users who want to approve each command; persistent YOLO setup link unchanged. ->>>>>>> 4c4e9e42 (fix(web): swap /home Steps 2↔3, claude --yolo as copy-button command) ## [0.54.29] — 2026-05-19 @@ -579,18 +596,6 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C UI shows the corrected project). The orchestrator now compares the two at every rebuild and, if they differ, calls `rebuild_from_registry()` to regenerate the extract. -======= -- /home onboarding reordered: folder creation is now Step 2 (was - Step 3) and starting Claude with `claude --dangerously-skip-permissions` - is the new Step 3 (was the auto-mode step), rendered with the same - `.install-cmd` + copy-button affordance the other steps use. Step 4 - paste runs ~20 shell commands that auto-accept-edits would not cover - (Bash still prompts), so the YOLO flag is the default recommendation; - session-scoped, drops on next plain `claude`. Shift + Tab → auto- - accept-edits kept as the strict-review fallback; persistent YOLO - allowlist link to /setup-advanced#yolo unchanged. Setup script's - "Verify cwd" warning copy refreshed to reference "/home Step 2". ->>>>>>> c195e0fa (fix(web): swap /home Steps 2↔3, claude --yolo as copy-button command) - Setup script no longer auto-creates the workspace folder. Step 2 of the pasted prompt now runs `pwd`, compares it to `$HOME/` (the folder the /home page's visible Step 3 told the user to create diff --git a/cli/commands/refresh_marketplace.py b/cli/commands/refresh_marketplace.py index 6bceeb8..19270be 100644 --- a/cli/commands/refresh_marketplace.py +++ b/cli/commands/refresh_marketplace.py @@ -242,18 +242,7 @@ def _bootstrap_clone(token: str) -> bool: except OSError: pass - # Add execute bit to every `.sh` under the clone — git's checkout doesn't - # always preserve the file-mode bit (filemode=false repos, archive - # extractions), and Claude Code's later `plugin install` copies the - # files into the workspace `.claude/hooks/` AS-IS, so hooks that lost - # the +x bit here would fire with Permission denied. Fixing at the - # source (marketplace clone) means every downstream plugin install - # gets executable hooks for free. Best-effort: no-op on Windows NTFS. - for sh in CLONE_DIR.rglob("*.sh"): - try: - sh.chmod(sh.stat().st_mode | 0o111) - except OSError: - pass + _chmod_clone_sh_files() if not _register_clone_with_claude(CLONE_DIR): return False @@ -444,12 +433,41 @@ def _local_head_sha() -> Optional[str]: return sha or None +def _chmod_clone_sh_files() -> None: + """Add execute bit to every `.sh` under CLONE_DIR. + + Git's checkout doesn't always preserve the file-mode bit (filemode=false + repos, archive extractions, FUSE/NFS mounts with filemode detection + disabled), and Claude Code's later `plugin install` copies the files + into the workspace `.claude/hooks/` AS-IS, so hooks that lost the +x + bit here would fire with Permission denied. Fixing at the source + (marketplace clone) means every downstream plugin install gets + executable hooks for free. + + Called from both `_bootstrap_clone` (after the initial `git clone`) + and `_git_fetch_and_reset` (after every `git reset --hard FETCH_HEAD` + on subsequent refreshes) so a version bump that touches a `.sh` + can't silently strip the bit. Best-effort: no-op on Windows NTFS, + swallows per-file errors. + """ + for sh in CLONE_DIR.rglob("*.sh"): + try: + sh.chmod(sh.stat().st_mode | 0o111) + except OSError: + pass + + def _git_fetch_and_reset(token: str) -> bool: """Fetch from origin then hard-reset to FETCH_HEAD. Not `pull --ff-only`: the marketplace bare repo on the server rebuilds as a fresh orphan commit on every content change, so two snapshots have unrelated histories and fast-forward is impossible. + + After a successful reset, re-applies the `.sh` execute bit across the + clone — `git reset --hard` overwrites working-tree files according to + the repo's `core.filemode` setting, and on systems where that is + `false` the bit gets stripped silently. """ if not _git_fetch_only(token): return False @@ -465,6 +483,8 @@ def _git_fetch_and_reset(token: str) -> bool: typer.echo(reset.stderr, err=True) return False + _chmod_clone_sh_files() + if reset.stdout: typer.echo(reset.stdout.rstrip()) return True