feat(init): install the post-commit hook by default, with the guards that make it safe - #2143
Open
RaghavChamadiya wants to merge 10 commits into
Open
feat(init): install the post-commit hook by default, with the guards that make it safe#2143RaghavChamadiya wants to merge 10 commits into
RaghavChamadiya wants to merge 10 commits into
Conversation
The distill omissions, precedent and staged-decision stores and the CLI hook ledger issued PRAGMA journal_mode=WAL bare, before any busy timeout. The switch needs a brief exclusive lock, so a reader holding an open transaction on the same file made every constructor raise database is locked. The ledger swallowed that and cached None, turning itself off for the rest of the process; the other three took their command down. One stdlib-only helper applies busy_timeout first, tolerates a failed switch, then sets synchronous, in the order the async engine listener already uses. It lives beside fts_query rather than in persistence/database because importing the engine module costs over a second on the distill and expand paths. The ledger keeps the same shape inline; it may not import repowise.core at all. Each store is tested by opening it while another connection holds the file.
…rving when a tool cannot import An MCP host restarts a stdio server that dies at startup, so two startup failures became crash loops. The lifespan created .repowise and opened the database unguarded, and a read-only directory or a .repowise that is a file escaped as a traceback on every respawn. ensure_full_surface imported every tool module unconditionally, so one missing optional dependency took the whole surface down the same way. The lifespan now raises StoreUnavailableError naming the directory and the fix, the mcp command turns that into one line on stderr and exit 1, and a tool module that cannot import is logged with the missing distribution and skipped while the rest of the surface serves.
…-hook init --yes and non-interactive runs skipped the hook offer with no message and no opt-out, so the paths the product recommends left the index stale after the first commit. Telemetry put 40 to 53 percent of tool calls on a stale index for that reason. The offer now installs on those paths and prints one line naming repowise hook uninstall, which is the consent pattern for a default that is on. An interactive run still asks. --no-hook skips it, and --no-editor-setup or REPOWISE_SKIP_EDITOR_SETUP keeps it off the way they keep the distill rewrite hook off, because a git hook is a write outside .repowise/. Workspace init applies the same rule to every indexed repo. A failed install is reported and never fails a finished init.
…kes a write Doctor had no row for the post-commit hook, so a hook that failed to install, or was deleted, was invisible on the one command people run to ask whether their setup is healthy. It now reads hooks.status, which asks git for the real hooks directory, so worktrees and core.hooksPath report correctly. Absence is informational: --no-hook is a choice. The .repowise row tested existence only, which read OK for a read-only directory, the one state that stops the MCP server from starting. It now writes and removes a probe file, since os.access is unreliable on Windows. The workspace summary told users to run repowise hook install to register an MCP client; that command installs the git hook. It now names repowise init.
The stdio MCP server is the longest-lived process the product runs and the one path that never checked its own currency, so a client that spawned it weeks ago kept talking to that version with no signal. serve checked once at startup and never again. The lifespan now polls the TTL-cached PyPI check on a worker thread, in both single-repo and workspace mode, and build_meta names a newer version in the first response after the poller sees it, then stays quiet for that version. serve re-runs its advisory on a daemon thread once per interval, announcing each release once. The disk cache is shared with the CLI advisory, so neither adds network beyond one fetch a day.
…on every startup failure Three things a hook that fires on every commit for everyone must not do. The script fell back to uv run repowise update when repowise was not on PATH, which resolves whatever project uv finds above the repo, on every commit, in repos that are not Python projects; it now looks only in the repo's own .venv. It fired whenever .repowise existed, so a dry-run directory or a deleted store failed on every commit forever; it now needs state.json, the precondition update itself checks. And install appended shell to any existing post-commit hook, breaking one written for node or python; it now refuses those and the init offer reports the refusal. The MCP lifespan gave a configured database the directory remedy; a REPOWISE_DB_URL failure now says to check the URL and the server. The workspace branch gets the same store protection as single-repo mode and cancels its background tasks when the registry fails to load. Docs that still called the hook an offer or a prompt say default now, and the --no-editor-setup entry names the hook among what it skips.
The package proxies every _state global as a module attribute; the two new release fields were not in its name set, so a write through the proxy landed on the package instead of the state.
|
🔍 2 things to check
✅ Health of changed files: 3.6 → 3.9 (+0.3) 📊 See the full report for this PR |
…hook # Conflicts: # packages/cli/src/repowise/cli/hooks.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Telemetry puts 40 to 53 percent of MCP tool calls on a stale index. The root cause is that
init --yesand non-interactive runs, the paths the docs recommend, skipped the post-commit hook offer with no message and no opt-out flag. Turning the hook on by default is safe only once the things that made it risky are fixed, so this PR does those first.What changes
The sidecar stores open under a contended WAL switch. The distill omissions store, the precedent store, the staged-decision store and the CLI hook ledger issued
PRAGMA journal_mode=WALbare, before any busy timeout. The switch needs a short exclusive lock, so a reader holding an open transaction on the same file made every constructor raisedatabase is locked. The ledger swallowed that and cachedNone, turning itself off for the rest of the process; the other three took their command down. One stdlib-only helper,repowise.core.sqlite_pragmas.apply_sqlite_pragmas, appliesbusy_timeoutfirst, tolerates a failed switch, then setssynchronous, in the order the async engine listener already uses. It lives besidefts_queryrather than inpersistence/databasebecause importing the engine module costs over a second on thedistillandexpandpaths (measured: 0.41 s to 1.69 s for the import chain). The ledger keeps the same shape inline; it may not importrepowise.coreat all. Each store is tested by opening it while another connection holds the file.The MCP server exits once, with the path, instead of respawning. An MCP host restarts a stdio server that dies at startup. The lifespan created
.repowiseand opened the database unguarded, so a read-only directory or a.repowisethat is a file escaped as a traceback on every respawn.ensure_full_surfaceimported every tool module unconditionally, so one missing optional dependency took the whole surface down the same way. The lifespan now raisesStoreUnavailableErrornaming the directory and the fix,repowise mcpturns that into one line on stderr and exit 1, and a tool module that cannot import is logged with the missing distribution and skipped while the rest serves.initinstalls the post-commit hook by default, with--hook/--no-hook. An interactive run still asks.--yesand non-interactive runs install it and print one line namingrepowise hook uninstall, which is the consent pattern for a default that is on.--no-hookskips it.--no-editor-setupandREPOWISE_SKIP_EDITOR_SETUPkeep it off the way they keep the distill rewrite hook off, because a git hook is a write outside.repowise/. Workspace init applies the same rule to every indexed repo. A failed install is reported and never fails a finished init (the second point on #1609).The hook itself is made boring before it goes on by default. The script fell back to
uv run repowise updatewhenrepowisewas not on PATH, which resolves whatever projectuvfinds above the repo, on every commit, in repos that are not Python projects; it now looks only in the repo's own.venv. It fired whenever.repowiseexisted, so a dry-run directory or a deleted store failed on every commit forever; it now needsstate.json, the preconditionupdateitself checks. Andinstallappended shell to any existing post-commit hook, breaking one written for node or python; it now refuses those and the offer reports the refusal instead of printing the consent line.doctorreports the hook and probes that.repowisetakes a write. Doctor had no row for the post-commit hook, so a hook that failed to install, or was deleted, was invisible on the one command people run to ask whether their setup is healthy. The new row readshooks.status, which asks git for the real hooks directory, so worktrees andcore.hooksPathreport correctly; absence is informational because--no-hookis a choice. The.repowiserow tested existence only, which read OK for a read-only directory, the one state that stops the MCP server from starting; it now writes and removes a probe file. The workspace summary told users to runrepowise hook installto register an MCP client; that installs the git hook. It now namesrepowise init.Release currency on the two long-lived processes. The stdio MCP server never checked its own version, and
servechecked once at startup. The lifespan now polls the TTL-cached PyPI check on a worker thread in both single-repo and workspace mode, andbuild_metanames a newer version in the first response after the poller sees it (_meta.newer_release), then stays quiet for that version.servere-runs its advisory on a daemon thread once per interval. The disk cache is shared with the CLI advisory, so neither adds network beyond one fetch a day.Interactions
core.hooksPath=.husky/_) is merged separately as the contributor's PR; this branch already reads theinstalledprefix so itsinstalled (<reason>)suffix cannot make the offer ask twice.editor_files: {vscode_mcp: false}ignored byinitandupdate) is untouched here. With the hook on by default every commit runsupdate, so a repo that deleted.vscode/after opting out will see it recreated per commit until that lands.Verification
pytest tests/unit/persistence -q: 797 passed.pytest tests/unit/server/mcp -q: 1733 passed, 4 failed; the four aretest_riskabsolute/tmppath normalization on Windows and twotest_why_workspace_rankingcases that pass when run alone, none touching files in this diff.pytest tests/unit/cli -q: 2449 passed, 1 failed; the failure istest_openai_compatible::test_persist_setup_saves_endpoint_and_key_with_consent, achmod 0o600assertion that cannot hold on Windows and is untouched by this diff.ruff check packages/ tests/: clean.init --yes --no-proseprinted✓ clone: post-commit hook installedandAuto-sync is on: repowise update runs after each commit. Run 'repowise hook uninstall' to turn it off.;.git/hooks/post-commitcarried the marker;repowise hook uninstallprintedremovedand deleted the file.repowise mcp .against a.repowisethat is a file: exit 1, nothing on stdout, oneError:line on stderr naming the path andrepowise init.