Repository navigation
feat(OMN-19607): a PR merge fast-forwards the matching canonical clones, and a launchd timer catches merges made elsewhere - #2346
Conversation
…es, and a launchd timer catches merges made elsewhere A PostToolUse hook on the merge verbs (gh pr merge, the REST merge endpoint by PUT, the bulk throttle arm operation, the GitHub MCP merge tool) starts a detached git-only fetch and ff-only advance of every canonical clone of that repository under OMNI_HOME and each registry root. The launchd agent ai.omninode.canonical-clone-sync runs the same engine over every clone every 180 s. Dirty, diverged, ahead, detached, bare, mid-operation and lane-branch clones are refused and logged, never forced. No GitHub API quota is spent. Also: tick-bundle-install.sh --only <label>, and the tick installer test no longer dies on SIGPIPE from an early-exiting awk under pipefail.
…e to a concurrent fetch
|
No OCC evidence companion was minted for this PR. product PR is a draft; companion suppressed (F-17) Matched in the state: To clear this: Mark the PR ready for review. The ready_for_review trigger re-fires the born path and the companion mints then. Reported by |
…iclaude#2346 (#11306) * evidence(OMN-19607): author OCC companion for OmniNode-ai/omniclaude#2346 OCC companion by node_pr_lifecycle_fix_effect (OMN-13317 F1 / OMN-13990 / OMN-14285). Product PR head 5d733a6776e0e9d16eb4d4f765547d381b89fa51. * evidence(OMN-19607): self-bind OCC#11306 + rebind contract_sha256 --------- Co-authored-by: omnimarket-bot <bot@omninode.ai>
… autobind companion merged
✅ Hostile Reviewer — PASSEDBlocking findings (critical/error): 0 Findings
Gate semantics (pilot phase)
Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8524) |
The three hook-invocation tests (trigger-and-return-fast, resolve-repo- from-cwd, advance-through-detached-child) relied on the invoking process's ambient environment to put post_tool_use_merge_clone_sync.sh into "full" mode. mode.sh's auto-detect falls back to $PWD (does the caller's cwd sit under a canonical-registry directory?) and then an omnibase_core dev-install probe, defaulting to "lite" -- a silent no-op -- when neither holds. On a bare CI checkout the cwd sits outside any such directory, so mode resolved to lite and the hook exited before writing the TRIGGERED log row or spawning the sync child, failing all three tests deterministically (reproduced by running pytest from an unrelated cwd with OMNICLAUDE_MODE unset: fails before this change, passes after). Registry.env() now pins OMNICLAUDE_MODE=full explicitly, so the tests assert on the hook's own logic rather than on the invoking machine's directory layout or shell profile.
|
Fix commit eecdcc5 (landing lane repo-drain-omniclaude, pass 1). Real red diagnosed on the GitHub-hosted `Hooks System Tests` job (ubuntu-24.04 runner): 3 of the new suite's tests failed deterministically, both on the original head and on a rerun - Root cause: `Registry.env()` in the test fixture copies the full ambient `os.environ`, so it silently inherited `OMNICLAUDE_MODE=full` from the authoring machine's shell profile. `mode.sh`'s auto-detect falls back to `$PWD` (full only under a canonical-registry checkout) and then an `omnibase_core` dev-install probe, defaulting to "lite" otherwise. A bare CI checkout's cwd matches neither, so mode resolved to "lite" and `post_tool_use_merge_clone_sync.sh` exited before ever invoking the Python engine - no TRIGGERED log row, no detached sync child, exactly the three observed failures. Reproduced locally: ran the three tests from a cwd outside any canonical-registry directory with `OMNICLAUDE_MODE` unset - all three failed with the identical errors seen in CI. Fix: `Registry.env()` now pins `OMNICLAUDE_MODE=full` explicitly so the tests assert on the hook's own logic rather than the invoking machine's directory layout or shell profile. Verification (worktree at the exact head, clean tree):
Not runtime-affecting (session hooks + host tooling only, matches the PR body's own classification). Companion `onex_change_control#11306` (Evidence-Source autobind) already merged. |
OMN-19607: a PR merge fast-forwards the matching canonical clones, and a launchd timer catches merges made elsewhere
Ticket: OMN-19607 (parent epic OMN-19470, GitHub quota). Operator, 2026-09-25 about 14:28Z: add a hook that pulls the canonical clones after every merge. Under the same day's quota ruling, lanes query the canonical clones instead of spending API reads, which only works while the clones are current.
Why the existing sync was not enough (measured 2026-09-25)
reconcile-host.sh, which couples the clone pull to the venv reconcile. One run held the reconcile lock from 13:25Z insidereconcile-workspace-venvs.sh, every tick after it declined (receipt log, exit 4 at 13:35Z through 14:25Z), and no clone moved for over an hour. The SessionStart line still read "in sync as of 13:15Z".ai.omninode.bare-clone-syncagent is not installed, runs the whole ofpull-all.sh(venv reconcile, plugin cache, branch switches), and its plist hardcodes a volume path. Extending it would have tied clone freshness to the same heavy stages again.What this adds
plugins/onex/hooks/lib/canonical_clone_sync.py: one standard-library engine with two entry points.hook: reads a PostToolUse payload. On a merge (gh pr mergeby number, URL or branch, with or without the repo flag orGH_REPO; the REST merge endpoint by PUT; the bulk throttle's arm operation; the GitHub MCP merge tool) it resolves the repository (from the command, else from the session cwd's origin) and starts a detachedsyncfor it. A failed or interrupted tool call, a GET of the merge endpoint, and every other gh call are ignored. An auto-merge arm waits 30 s before syncing, and the timer catches the rest.sync: for every canonical clone of the repository underOMNI_HOME, eachONEX_REGISTRY_ROOTSroot and eachroot=line of the registry-roots file (so launchd needs onlyOMNI_HOME), it fetches the checked-out tracking branch and runsgit merge --ff-onlyonto the fetched sha, then re-reads HEAD and compares it with the target. It never resets, checks out, stashes, cleans or forces.scripts/commit_lock.pyprotocol (the registry root's own clone, rule 19). There the fast-forward runs under that commit lock, and unstaged changes refuse only when the incoming commits touch the same path, because rule 19 names the ff-only merge as that tree's sanctioned sync and its rolling ledger is almost never clean.$ONEX_STATE_DIR/logs/canonical-clone-sync.jsonl(before, after and target sha, result, reason), plus a TRIGGERED line from the hook and a RUN_COMPLETE line with counts.plugins/onex/hooks/scripts/post_tool_use_merge_clone_sync.sh: registered on PostToolUse with matcher^(Bash|mcp__github__merge_pull_request)$, right after the reconcile tick. For a payload without the word "merge" it exits after one bash string match. Otherwise it starts Python only to parse the payload and returns while the fetch runs detached. It exits 0 on every path and prints nothing. It is registered in hooks.json with a carve-out record (owner, reason, expiry, restoration), in the hook inventory and in the distribution manifest.scripts/canonical-clone-sync-tick.shandscripts/launchd/ai.omninode.canonical-clone-sync.plist: the catch-all for merges this Mac did not make (auto-merge completions, other people). StartInterval 180 s over every clone.tick-bundle-install.sh --only <label>installs this one agent without handing the host the whole bundle.git fetchis the git transport, and the REST and GraphQL rate limits do not count it.Which branch a clone follows
The branch it has checked out.
pull-all.shleaves every clone whose origin hasdevondevand the others onmain(OMN-16502), and on the release-synced repos PRs land ondevwhilemainonly moves at a release.devis therefore where merged code first appears, and it is the right branch to query. The engine never switches branches: that is the one move the canonical-clone ref guard refuses, and pull-all is the sanctioned way to make it.How the webhook path replaces the timer
The GitHub-quota plan (OMN-19470, ruled 2026-09-25) creates the read-only
onexbot-pr-readerApp, whose webhook sends pull_request and push deliveries todev.api.omninode.ai. They reach the lab bus asonex.cmd.github.webhook-delivery.v1through the ingress node (T2.1, OMN-19492) and the gateway forwarder (T2.4, OMN-19593). When that path is live, a clone-sync effect node on each host consumes the push or merged-PR event for its repositories and calls this same engine'ssyncfor that one repository. The 180 s poll then stops (unregister the launchd agent) and the PostToolUse hook is retired in the same change. The engine, its refusal rules and its log stay as they are. The hooks.json carve-out record names this as its expiry and restoration.Tests (focused, local)
tests/hooks/test_canonical_clone_sync.py: 63 passed. Matcher cases for every verb and every ignored form. Engine cases against hermetic repos whose remotes resolve github.com/<owner>/<name>: advance, up to date, untracked files carried, and refusals for dirty, staged, diverged, ahead, detached, lane branch, bare and mid-merge, each asserting HEAD and the working file did not move. Also main-only clones, the shared commit-lock tree (unrelated dirt carried, overlapping dirt refused), repo filtering, a NO_CLONE line for an unknown repo, all roots plus the roots file, and linked worktrees skipped. Hook cases: silent and under one second on a non-merge, on four malformed payloads and with no OMNI_HOME, the TRIGGERED line, repo resolution from cwd, and a real advance through the detached child.tests/hooks/test_hooks_baseline_omn_13244.py,tests/hooks/test_hook_inventory.py,tests/hooks/test_workspace_reconcile.pyand the six other suites that read hooks.json PostToolUse: all passed.validate_hook_inventory.py: OK, 31 expected hooks.mypy --stricton the engine is clean, and pre-commit is clean on every changed file.tests/scripts/test_tick_bundle_install.shexited 141 onorigin/devbefore this change: an awk that exits early SIGPIPEs itsechounder pipefail, which kills the script underset -e, and the same pattern can read a Disabled=true template as enabled. Both awk programs now read their whole input. Tests 1 to 3 pass locally. Tests 4 and 5 write heredocs that hang in this host's sandbox, and no CI job runs this file.Lab-first readback (this Mac, 2026-09-25)
devmoved tof43e7d9b3a38(#4123, OMN-19338). At 15:31:28Z the canonical clone still sat at21d5d62161c5, because the timer's pass at 15:29Z had lost a ref-lock race to a concurrent fetch. The hook script from this branch, fed a PostToolUse payload for a PUT to that repo's merge endpoint, returned exit 0 in 117 ms. The clone reachedf43e7d9b3a384 s later, and the log showsADVANCEDwith trigger hook, verb gh-api-merge and after equal to the merge commit. That race is why the fetch now retries once on a lost ref lock (second commit).launchctl listshows it loaded. The first pass under ProcessType Background took 200 s at host load 60, and the knowledge-base-internal fetch hit the 120 s timeout. With the default process type the next pass took 12.8 s and exited 0. Later passes at load 60 took 45 to 105 s and advanced onex_change_control in both roots (75a944dc544eto0ffbf67cc90b) with no hook trigger involved.Deploy note
Hooks load from the omniclaude canonical clone's working tree, so after merge that clone needs to be at the merge commit, and a new session is needed before the hook is live. The launchd agent does that fast-forward itself. After merge, repoint the agent from this worktree to the canonical path with
bash omniclaude/scripts/tick-bundle-install.sh --only ai.omninode.canonical-clone-sync.Not runtime-affecting: the change covers session hooks and host tooling only. No node, contract or container image changes, so there is no dev-lane lab pass to run.
Evidence-Ticket: OMN-19607
Evidence-Source: OCC#11306